Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/cli/tui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,6 +53,7 @@ type Route =
autoSession?: boolean;
}
| { name: 'logs' }
| { name: 'dev' }
| { name: 'create' }
| { name: 'add' }
| { name: 'status' }
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -271,6 +271,21 @@ function AppContent({
return <LogsScreen isInteractive={isInteractive} onExit={handleBack} />;
}

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 (
<DevScreen
onBack={handleBack}
onLaunchBrowser={() => {
setExitAction({ type: 'dev' });
exit();
}}
/>
);
}

if (route.name === 'status') {
return <StatusScreen isInteractive={isInteractive} onExit={handleBack} />;
}
Expand Down
70 changes: 70 additions & 0 deletions src/cli/tui/screens/dev/__tests__/DevScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<DevScreen onBack={noop} onLaunchBrowser={onLaunchBrowser} />);
await new Promise(r => setTimeout(r, 50));

expect(lastFrame()).toContain('No agents or harnesses defined in project.');
expect(onLaunchBrowser).not.toHaveBeenCalled();
});
});
Loading