takt: ワークフロー選択時にescキーをおしたらキャンセルにしてほしい。

This commit is contained in:
nrslib 2026-01-28 11:56:00 +09:00
parent 4ebee4f5af
commit e8a8044c9f
4 changed files with 55 additions and 10 deletions

View File

@ -315,5 +315,30 @@ describe('prompt', () => {
const result = await selectOptionWithDefault('Test:', [], 'fallback'); const result = await selectOptionWithDefault('Test:', [], 'fallback');
expect(result).toBe('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 });
});
}); });
}); });

View File

@ -163,11 +163,18 @@ program
? DEFAULT_WORKFLOW_NAME ? DEFAULT_WORKFLOW_NAME
: availableWorkflows[0] || DEFAULT_WORKFLOW_NAME); : availableWorkflows[0] || DEFAULT_WORKFLOW_NAME);
selectedWorkflow = await selectOptionWithDefault( const selected = await selectOptionWithDefault(
'Select workflow:', 'Select workflow:',
options, options,
defaultWorkflow defaultWorkflow
); );
if (selected === null) {
info('Cancelled');
return;
}
selectedWorkflow = selected;
} }
log.info('Starting task execution', { task, workflow: selectedWorkflow }); log.info('Starting task execution', { task, workflow: selectedWorkflow });

View File

@ -37,6 +37,7 @@ export function needsLanguageSetup(): boolean {
/** /**
* Prompt user to select language for resources. * Prompt user to select language for resources.
* Returns 'en' for English (default), 'ja' for Japanese. * Returns 'en' for English (default), 'ja' for Japanese.
* Exits process if cancelled (initial setup is required).
*/ */
export async function promptLanguageSelection(): Promise<Language> { export async function promptLanguageSelection(): Promise<Language> {
const options: { label: string; value: Language }[] = [ const options: { label: string; value: Language }[] = [
@ -44,15 +45,22 @@ export async function promptLanguageSelection(): Promise<Language> {
{ label: '日本語 (Japanese)', value: 'ja' }, { label: '日本語 (Japanese)', value: 'ja' },
]; ];
return await selectOptionWithDefault( const result = await selectOptionWithDefault(
'Select language for default agents and workflows / デフォルトのエージェントとワークフローの言語を選択してください:', 'Select language for default agents and workflows / デフォルトのエージェントとワークフローの言語を選択してください:',
options, options,
DEFAULT_LANGUAGE DEFAULT_LANGUAGE
); );
if (result === null) {
process.exit(0);
}
return result;
} }
/** /**
* Prompt user to select provider for resources. * Prompt user to select provider for resources.
* Exits process if cancelled (initial setup is required).
*/ */
export async function promptProviderSelection(): Promise<'claude' | 'codex'> { export async function promptProviderSelection(): Promise<'claude' | 'codex'> {
const options: { label: string; value: 'claude' | 'codex' }[] = [ const options: { label: string; value: 'claude' | 'codex' }[] = [
@ -60,11 +68,17 @@ export async function promptProviderSelection(): Promise<'claude' | 'codex'> {
{ label: 'Codex', value: 'codex' }, { label: 'Codex', value: 'codex' },
]; ];
return await selectOptionWithDefault( const result = await selectOptionWithDefault(
'Select provider (Claude Code or Codex) / プロバイダーを選択してください:', 'Select provider (Claude Code or Codex) / プロバイダーを選択してください:',
options, options,
'claude' 'claude'
); );
if (result === null) {
process.exit(0);
}
return result;
} }
/** /**

View File

@ -275,13 +275,13 @@ export async function promptInput(message: string): Promise<string | null> {
/** /**
* Prompt user to select from a list of options with a default value. * Prompt user to select from a list of options with a default value.
* Uses cursor navigation. Enter immediately selects the default. * 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<T extends string>( export async function selectOptionWithDefault<T extends string>(
message: string, message: string,
options: { label: string; value: T }[], options: { label: string; value: T }[],
defaultValue: T defaultValue: T
): Promise<T> { ): Promise<T | null> {
if (options.length === 0) return defaultValue; if (options.length === 0) return defaultValue;
// Find default index // Find default index
@ -294,12 +294,11 @@ export async function selectOptionWithDefault<T extends string>(
label: opt.value === defaultValue ? `${opt.label} ${chalk.green('(default)')}` : opt.label, 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 // Cancel selected (last item) or Escape pressed
if (selectedIndex === -1) { if (selectedIndex === options.length || selectedIndex === -1) {
console.log(chalk.gray(` Using default: ${defaultValue}`)); return null;
return defaultValue;
} }
const selected = options[selectedIndex]; const selected = options[selectedIndex];