diff --git a/src/cli/tui/screens/create/CreateScreen.tsx b/src/cli/tui/screens/create/CreateScreen.tsx index 9b9e7907f..31317b835 100644 --- a/src/cli/tui/screens/create/CreateScreen.tsx +++ b/src/cli/tui/screens/create/CreateScreen.tsx @@ -274,6 +274,16 @@ export function CreateScreen({ cwd, isInteractive, onExit, onNavigate }: CreateS onExit, ]); + // Esc on the create-type prompt returns to the project-name input (matching the + // "Esc back" footer); on every other phase it falls through to the top-level exit. + const handleBack = useCallback(() => { + if (flow.phase === 'create-type-prompt') { + flow.goBackToInput(); + } else { + handleExit(); + } + }, [flow, handleExit]); + // Auto-exit when project creation completes successfully useEffect(() => { if (allSuccess) { @@ -287,7 +297,7 @@ export function CreateScreen({ cwd, isInteractive, onExit, onNavigate }: CreateS onSelect: item => { flow.handleCreateTypeSelection(item.id as 'harness' | 'agent' | 'skip'); }, - onExit: handleExit, + onExit: handleBack, isActive: flow.phase === 'create-type-prompt', }); @@ -342,7 +352,7 @@ export function CreateScreen({ cwd, isInteractive, onExit, onNavigate }: CreateS : undefined; return ( - + {phase === 'existing-project-error' && ( A project already exists at this location. diff --git a/src/cli/tui/screens/create/__tests__/CreateScreen.test.tsx b/src/cli/tui/screens/create/__tests__/CreateScreen.test.tsx new file mode 100644 index 000000000..bd01f34cc --- /dev/null +++ b/src/cli/tui/screens/create/__tests__/CreateScreen.test.tsx @@ -0,0 +1,39 @@ +import { CreateScreen } from '../CreateScreen.js'; +import { render } from 'ink-testing-library'; +import { mkdtempSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const ENTER = '\r'; +const ESCAPE = '\x1B'; + +const tick = () => new Promise(resolve => setTimeout(resolve, 20)); + +afterEach(() => vi.restoreAllMocks()); + +describe('CreateScreen Esc on create-type prompt', () => { + it('returns to the project-name input without exiting the CLI', async () => { + const cwd = mkdtempSync(join(tmpdir(), 'create-screen-')); + const onExit = vi.fn(); + const { stdin, lastFrame } = render(); + + // Wait for the existing-project check to resolve into the input phase. + await tick(); + expect(lastFrame()).toContain('Create a new AgentCore project'); + + // Submit a valid project name -> advances to the create-type prompt. + stdin.write('MyProject'); + await tick(); + stdin.write(ENTER); + await tick(); + expect(lastFrame()).toContain('What would you like to build?'); + + // Esc must go back to the name input, not quit the CLI. + stdin.write(ESCAPE); + await tick(); + expect(lastFrame()).toContain('Create a new AgentCore project'); + expect(onExit).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/tui/screens/create/useCreateFlow.ts b/src/cli/tui/screens/create/useCreateFlow.ts index c1b909531..9243ff814 100644 --- a/src/cli/tui/screens/create/useCreateFlow.ts +++ b/src/cli/tui/screens/create/useCreateFlow.ts @@ -73,6 +73,7 @@ interface CreateFlowState { // Project name actions setProjectName: (name: string) => void; confirmProjectName: () => void; + goBackToInput: () => void; // Create type selection handleCreateTypeSelection: (choice: 'harness' | 'agent' | 'skip') => void; // Add agent config (set when AddAgentScreen completes) @@ -180,6 +181,10 @@ export function useCreateFlow(cwd: string): CreateFlowState { setPhase('create-type-prompt'); }, []); + const goBackToInput = useCallback(() => { + setPhase('input'); + }, []); + const updateStep = (index: number, update: Partial) => { setSteps(prev => prev.map((s, i) => (i === index ? { ...s, ...update } : s))); }; @@ -756,6 +761,7 @@ export function useCreateFlow(cwd: string): CreateFlowState { logFilePath, setProjectName, confirmProjectName, + goBackToInput, // Create type selection handleCreateTypeSelection, // Add agent