diff --git a/src/__tests__/prompt.test.ts b/src/__tests__/prompt.test.ts index 7c30f06..6095dbe 100644 --- a/src/__tests__/prompt.test.ts +++ b/src/__tests__/prompt.test.ts @@ -315,5 +315,30 @@ describe('prompt', () => { const result = await selectOptionWithDefault('Test:', [], 'fallback'); expect(result).toBe('fallback'); }); + + it('should have return type that allows null (cancel)', async () => { + const { selectOptionWithDefault } = await import('../prompt/index.js'); + // When options are empty, default is returned (not null) + const result: string | null = await selectOptionWithDefault('Test:', [], 'fallback'); + expect(result).toBe('fallback'); + }); + }); + + describe('selectOptionWithDefault cancel behavior', () => { + it('handleKeyInput should return cancel with optionCount when hasCancelOption is true', () => { + // Simulates ESC key press with cancel option enabled (as selectOptionWithDefault now does) + const result = handleKeyInput('\x1B', 0, 4, true, 3); + expect(result).toEqual({ action: 'cancel', cancelIndex: 3 }); + }); + + it('handleKeyInput should support navigating to Cancel item', () => { + // With 3 options + cancel, totalItems = 4, cancel is at index 3 + const downResult = handleKeyInput('\x1B[B', 2, 4, true, 3); + expect(downResult).toEqual({ action: 'move', newIndex: 3 }); + + // Confirming on cancel index (3) should return confirm with selectedIndex 3 + const confirmResult = handleKeyInput('\r', 3, 4, true, 3); + expect(confirmResult).toEqual({ action: 'confirm', selectedIndex: 3 }); + }); }); }); diff --git a/src/cli.ts b/src/cli.ts index c0d96b4..f9ec44a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -163,11 +163,18 @@ program ? DEFAULT_WORKFLOW_NAME : availableWorkflows[0] || DEFAULT_WORKFLOW_NAME); - selectedWorkflow = await selectOptionWithDefault( + const selected = await selectOptionWithDefault( 'Select workflow:', options, defaultWorkflow ); + + if (selected === null) { + info('Cancelled'); + return; + } + + selectedWorkflow = selected; } log.info('Starting task execution', { task, workflow: selectedWorkflow }); diff --git a/src/config/initialization.ts b/src/config/initialization.ts index 146dfdf..132fca6 100644 --- a/src/config/initialization.ts +++ b/src/config/initialization.ts @@ -37,6 +37,7 @@ export function needsLanguageSetup(): boolean { /** * Prompt user to select language for resources. * Returns 'en' for English (default), 'ja' for Japanese. + * Exits process if cancelled (initial setup is required). */ export async function promptLanguageSelection(): Promise { const options: { label: string; value: Language }[] = [ @@ -44,15 +45,22 @@ export async function promptLanguageSelection(): Promise { { label: '日本語 (Japanese)', value: 'ja' }, ]; - return await selectOptionWithDefault( + const result = await selectOptionWithDefault( 'Select language for default agents and workflows / デフォルトのエージェントとワークフローの言語を選択してください:', options, DEFAULT_LANGUAGE ); + + if (result === null) { + process.exit(0); + } + + return result; } /** * Prompt user to select provider for resources. + * Exits process if cancelled (initial setup is required). */ export async function promptProviderSelection(): Promise<'claude' | 'codex'> { const options: { label: string; value: 'claude' | 'codex' }[] = [ @@ -60,11 +68,17 @@ export async function promptProviderSelection(): Promise<'claude' | 'codex'> { { label: 'Codex', value: 'codex' }, ]; - return await selectOptionWithDefault( + const result = await selectOptionWithDefault( 'Select provider (Claude Code or Codex) / プロバイダーを選択してください:', options, 'claude' ); + + if (result === null) { + process.exit(0); + } + + return result; } /** diff --git a/src/prompt/index.ts b/src/prompt/index.ts index 8d507e3..14df192 100644 --- a/src/prompt/index.ts +++ b/src/prompt/index.ts @@ -275,13 +275,13 @@ export async function promptInput(message: string): Promise { /** * Prompt user to select from a list of options with a default value. * Uses cursor navigation. Enter immediately selects the default. - * @returns Selected option value + * @returns Selected option value, or null if cancelled (ESC pressed) */ export async function selectOptionWithDefault( message: string, options: { label: string; value: T }[], defaultValue: T -): Promise { +): Promise { if (options.length === 0) return defaultValue; // Find default index @@ -294,12 +294,11 @@ export async function selectOptionWithDefault( label: opt.value === defaultValue ? `${opt.label} ${chalk.green('(default)')}` : opt.label, })); - const selectedIndex = await interactiveSelect(message, decoratedOptions, initialIndex, false); + const selectedIndex = await interactiveSelect(message, decoratedOptions, initialIndex, true); - // Escape pressed - use default - if (selectedIndex === -1) { - console.log(chalk.gray(` Using default: ${defaultValue}`)); - return defaultValue; + // Cancel selected (last item) or Escape pressed + if (selectedIndex === options.length || selectedIndex === -1) { + return null; } const selected = options[selectedIndex];