diff --git a/src/cli/tui/App.tsx b/src/cli/tui/App.tsx index 81795d16d..618f87413 100644 --- a/src/cli/tui/App.tsx +++ b/src/cli/tui/App.tsx @@ -12,6 +12,7 @@ import { DatasetFlow } from './screens/dataset-hub'; import { DeployScreen } from './screens/deploy/DeployScreen'; import { EvalHubScreen, EvalScreen } from './screens/eval'; import { ExportHarnessFlow } from './screens/export'; +import { FeedbackScreen } from './screens/feedback'; import { FetchAccessScreen } from './screens/fetch-access'; import { HelpScreen, HomeScreen } from './screens/home'; import { ImportFlow } from './screens/import'; @@ -80,6 +81,7 @@ type Route = | { name: 'dataset' } | { name: 'import' } | { name: 'export-harness' } + | { name: 'feedback' } | { name: 'cli-only'; commandId: string }; // Commands that don't require being at the project root @@ -199,6 +201,8 @@ function AppContent({ return; } setRoute({ name: 'export-harness' }); + } else if (id === 'feedback') { + setRoute({ name: 'feedback' }); } }; @@ -493,6 +497,10 @@ function AppContent({ ); } + if (route.name === 'feedback') { + return ; + } + if (route.name === 'cli-only') { const info = CLI_ONLY_EXAMPLES[route.commandId]; if (info) { diff --git a/src/cli/tui/__tests__/app-command-coverage.test.ts b/src/cli/tui/__tests__/app-command-coverage.test.ts index 8eaa1eeea..458cc3a9a 100644 --- a/src/cli/tui/__tests__/app-command-coverage.test.ts +++ b/src/cli/tui/__tests__/app-command-coverage.test.ts @@ -33,6 +33,7 @@ const ROUTED_COMMANDS = new Set([ 'config-bundle', 'dataset', 'batch-evaluations', + 'feedback', ]); describe('TUI home screen command coverage', () => { @@ -52,4 +53,19 @@ describe('TUI home screen command coverage', () => { expect(unhandled).toEqual([]); }); + + it('the cliOnly flag and CLI_ONLY_EXAMPLES agree for every visible command', () => { + const program = createProgram(); + const commands = getCommandsForUI(program, { inProject: true }); + + // A command can never be in the main interactive list while routing to the + // cli-only dead-end: cliOnly is true iff selecting it hits CliOnlyScreen. + for (const cmd of commands) { + const hasCliOnlyExamples = cmd.id in CLI_ONLY_EXAMPLES; + expect(cmd.cliOnly, `${cmd.id}: cliOnly flag must match CLI_ONLY_EXAMPLES membership`).toBe(hasCliOnlyExamples); + } + + // feedback is interactive (real FeedbackScreen), not a cli-only dead-end. + expect('feedback' in CLI_ONLY_EXAMPLES).toBe(false); + }); }); diff --git a/src/cli/tui/copy.ts b/src/cli/tui/copy.ts index 794dc000b..57ddc0818 100644 --- a/src/cli/tui/copy.ts +++ b/src/cli/tui/copy.ts @@ -135,16 +135,8 @@ export const CLI_ONLY_EXAMPLES: Record'], - }, }; diff --git a/src/cli/tui/utils/commands.ts b/src/cli/tui/utils/commands.ts index 7d48eae79..a9ba3ead6 100644 --- a/src/cli/tui/utils/commands.ts +++ b/src/cli/tui/utils/commands.ts @@ -1,3 +1,4 @@ +import { CLI_ONLY_EXAMPLES } from '../copy'; import type { Command } from '@commander-js/extra-typings'; export interface CommandMeta { @@ -15,9 +16,13 @@ export interface CommandMeta { const HIDDEN_FROM_TUI = ['help', 'telemetry', 'promote'] as const; /** - * Commands that are CLI-only (shown but marked as requiring CLI invocation). + * Single source of truth for "this top-level command cannot run interactively in + * the TUI". Derived from CLI_ONLY_EXAMPLES so the `cliOnly` flag (list placement) + * can never diverge from App.tsx's dead-end routing: a command is flagged cliOnly + * iff selecting it routes to the CliOnlyScreen. Multi-word keys in CLI_ONLY_EXAMPLES + * (e.g. 'run eval') are subcommand paths, not top-level commands, so they never match. */ -const CLI_ONLY_COMMANDS = ['traces', 'pause', 'resume', 'stop', 'archive'] as const; +const CLI_ONLY_COMMANDS = new Set(Object.keys(CLI_ONLY_EXAMPLES)); /** * Commands hidden from TUI when inside an existing project. @@ -53,6 +58,6 @@ export function getCommandsForUI(program: Command, options: GetCommandsOptions = .filter(sub => !HIDDEN_SUBCOMMANDS.includes(sub.name() as (typeof HIDDEN_SUBCOMMANDS)[number])) .map(sub => sub.name()), disabled: false, - cliOnly: CLI_ONLY_COMMANDS.includes(cmd.name() as (typeof CLI_ONLY_COMMANDS)[number]), + cliOnly: CLI_ONLY_COMMANDS.has(cmd.name()), })); }