From e8a8044c9f10a02b75b342f8c55e81cf0957ab12 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:56:00 +0900 Subject: [PATCH] =?UTF-8?q?takt:=20=E3=83=AF=E3=83=BC=E3=82=AF=E3=83=95?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E9=81=B8=E6=8A=9E=E6=99=82=E3=81=ABesc?= =?UTF-8?q?=E3=82=AD=E3=83=BC=E3=82=92=E3=81=8A=E3=81=97=E3=81=9F=E3=82=89?= =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=B3=E3=82=BB=E3=83=AB=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=81=BB=E3=81=97=E3=81=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/prompt.test.ts | 25 +++++++++++++++++++++++++ src/cli.ts | 9 ++++++++- src/config/initialization.ts | 18 ++++++++++++++++-- src/prompt/index.ts | 13 ++++++------- 4 files changed, 55 insertions(+), 10 deletions(-) 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];