diff --git a/src/cli/tui/App.tsx b/src/cli/tui/App.tsx index 81795d16d..65ff51050 100644 --- a/src/cli/tui/App.tsx +++ b/src/cli/tui/App.tsx @@ -10,6 +10,7 @@ import { ConfigBundleFlow } from './screens/config-bundle-hub'; import { CreateScreen } from './screens/create'; import { DatasetFlow } from './screens/dataset-hub'; import { DeployScreen } from './screens/deploy/DeployScreen'; +import { DevScreen } from './screens/dev/DevScreen'; import { EvalHubScreen, EvalScreen } from './screens/eval'; import { ExportHarnessFlow } from './screens/export'; import { FetchAccessScreen } from './screens/fetch-access'; @@ -52,6 +53,7 @@ type Route = autoSession?: boolean; } | { name: 'logs' } + | { name: 'dev' } | { name: 'create' } | { name: 'add' } | { name: 'status' } @@ -142,9 +144,7 @@ function AppContent({ } if (id === 'dev') { - setExitAction({ type: 'dev' }); - exit(); - return; + setRoute({ name: 'dev' }); } else if (id === 'exec') { setExitAction({ type: 'exec' }); exit(); @@ -271,6 +271,21 @@ function AppContent({ return ; } + if (route.name === 'dev') { + // Render the dev picker in-TUI so agent-less projects get a guided error + // screen (Esc to go back) instead of crashing out of the TUI. When agents + // exist, the picker hands off to browser dev mode via the exit action. + return ( + { + setExitAction({ type: 'dev' }); + exit(); + }} + /> + ); + } + if (route.name === 'status') { return ; } diff --git a/src/cli/tui/screens/dev/__tests__/DevScreen.test.tsx b/src/cli/tui/screens/dev/__tests__/DevScreen.test.tsx new file mode 100644 index 000000000..6e3adf790 --- /dev/null +++ b/src/cli/tui/screens/dev/__tests__/DevScreen.test.tsx @@ -0,0 +1,70 @@ +import { DevScreen } from '../DevScreen.js'; +import { render } from 'ink-testing-library'; +import React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const { mockLoadProjectConfig, mockGetDevSupportedAgents } = vi.hoisted(() => ({ + mockLoadProjectConfig: vi.fn(), + mockGetDevSupportedAgents: vi.fn(), +})); + +vi.mock('../../../../operations/dev', () => ({ + loadProjectConfig: mockLoadProjectConfig, + getDevSupportedAgents: mockGetDevSupportedAgents, + getEndpointUrl: vi.fn(), +})); + +// The dev hooks would otherwise try to spin up a real dev server / deploy. +vi.mock('../../../hooks/useDevServer', () => ({ + useDevServer: () => ({ + logs: [], + status: 'idle', + isStreaming: false, + conversation: [], + streamingResponse: null, + config: null, + configLoaded: true, + actualPort: 8080, + invoke: vi.fn(), + execCommand: vi.fn(), + execInContainer: vi.fn(), + isContainer: false, + clearConversation: vi.fn(), + restart: vi.fn(), + stop: vi.fn(), + logFilePath: undefined, + hasUndeployedMemory: false, + hasVpc: false, + protocol: undefined, + mcpTools: [], + fetchMcpTools: vi.fn(), + showMcpHint: false, + a2aAgentCard: undefined, + a2aStatus: undefined, + fetchAgentCard: vi.fn(), + }), +})); + +vi.mock('../../../hooks/useDevDeploy', () => ({ + useDevDeploy: () => ({ steps: [], deployMessages: [], isComplete: false, error: undefined }), +})); + +const noop = () => undefined; + +describe('DevScreen', () => { + afterEach(() => vi.clearAllMocks()); + + // Regression: selecting "dev" in a project with no agents/harnesses must render + // the in-TUI error screen (not crash the TUI out to the shell). See issue #1588. + it('renders the in-TUI error when no agents or harnesses are defined', async () => { + mockLoadProjectConfig.mockResolvedValue({ runtimes: [], harnesses: [] }); + mockGetDevSupportedAgents.mockReturnValue([]); + + const onLaunchBrowser = vi.fn(); + const { lastFrame } = render(); + await new Promise(r => setTimeout(r, 50)); + + expect(lastFrame()).toContain('No agents or harnesses defined in project.'); + expect(onLaunchBrowser).not.toHaveBeenCalled(); + }); +});