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');
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
: 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 });

View File

@ -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<Language> {
const options: { label: string; value: Language }[] = [
@ -44,15 +45,22 @@ export async function promptLanguageSelection(): Promise<Language> {
{ 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;
}
/**

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.
* 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>(
message: string,
options: { label: string; value: T }[],
defaultValue: T
): Promise<T> {
): Promise<T | null> {
if (options.length === 0) return defaultValue;
// 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,
}));
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];