From c0dfa1aec36de3b25727d98918501e0d99a2a6c8 Mon Sep 17 00:00:00 2001 From: ruomeng Date: Thu, 2 Apr 2026 15:42:53 -0400 Subject: [PATCH] Enable 'Other' option for yesno question type (#24545) --- .../src/ui/components/AskUserDialog.test.tsx | 47 +++++++++++++++++++ .../cli/src/ui/components/AskUserDialog.tsx | 27 +++++------ packages/core/src/confirmation-bus/types.ts | 4 +- .../coreToolsModelSnapshots.test.ts.snap | 12 ++--- .../model-family-sets/default-legacy.ts | 6 +-- .../definitions/model-family-sets/gemini-3.ts | 6 +-- 6 files changed, 74 insertions(+), 28 deletions(-) diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx index 4f1cca7d8c..5217455358 100644 --- a/packages/cli/src/ui/components/AskUserDialog.test.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx @@ -1409,6 +1409,53 @@ describe('AskUserDialog', () => { expect(lastFrame()).toMatchSnapshot(); }); }); + + it('supports "Other" option for yesno questions', async () => { + const questions: Question[] = [ + { + question: 'Is this correct?', + header: 'Confirm', + type: QuestionType.YESNO, + }, + ]; + + const onSubmit = vi.fn(); + const { stdin, lastFrame, waitUntilReady } = await renderWithProviders( + , + { width: 80 }, + ); + + // Navigate to "Other" (3rd option: 1. Yes, 2. No, 3. Other) + writeKey(stdin, '\x1b[B'); // Down to No + writeKey(stdin, '\x1b[B'); // Down to Other + + await waitFor(async () => { + await waitUntilReady(); + expect(lastFrame()).toContain('Enter a custom value'); + }); + + // Type feedback + for (const char of 'Yes, but with caveats') { + writeKey(stdin, char); + } + + await waitFor(async () => { + await waitUntilReady(); + expect(lastFrame()).toContain('Yes, but with caveats'); + }); + + // Submit + writeKey(stdin, '\r'); + + await waitFor(async () => { + expect(onSubmit).toHaveBeenCalledWith({ '0': 'Yes, but with caveats' }); + }); + }); }); it('expands paste placeholders in multi-select custom option via Done', async () => { diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx index 483fcb5055..295d54eb73 100644 --- a/packages/cli/src/ui/components/AskUserDialog.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.tsx @@ -511,8 +511,9 @@ const ChoiceQuestionView: React.FC = ({ }) => { const keyMatchers = useKeyMatchers(); const isAlternateBuffer = useAlternateBuffer(); - const numOptions = - (question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0); + const hasAll = question.multiSelect && (question.options?.length ?? 0) > 1; + // Calculate total options including 'All' and 'Other' to ensure consistent numbering column width + const numOptions = (question.options?.length ?? 0) + (hasAll ? 1 : 0) + 1; const numLen = String(numOptions).length; const radioWidth = 2; // "● " const numberWidth = numLen + 2; // e.g., "1. " @@ -735,17 +736,15 @@ const ChoiceQuestionView: React.FC = ({ list.push({ key: 'all', value: allItem }); } - // Only add custom option for choice type, not yesno - if (question.type !== 'yesno') { - const otherItem: OptionItem = { - key: 'other', - label: customOptionText || '', - description: '', - type: 'other', - index: list.length, - }; - list.push({ key: 'other', value: otherItem }); - } + // Add custom option for choice and yesno types + const otherItem: OptionItem = { + key: 'other', + label: customOptionText || '', + description: '', + type: 'other', + index: list.length, + }; + list.push({ key: 'other', value: otherItem }); if (question.multiSelect) { const doneItem: OptionItem = { @@ -759,7 +758,7 @@ const ChoiceQuestionView: React.FC = ({ } return list; - }, [questionOptions, question.multiSelect, question.type, customOptionText]); + }, [questionOptions, question.multiSelect, customOptionText]); const handleHighlight = useCallback( (itemValue: OptionItem) => { diff --git a/packages/core/src/confirmation-bus/types.ts b/packages/core/src/confirmation-bus/types.ts index bb65fbdab7..fb28c01be7 100644 --- a/packages/core/src/confirmation-bus/types.ts +++ b/packages/core/src/confirmation-bus/types.ts @@ -183,13 +183,13 @@ export enum QuestionType { export interface Question { question: string; header: string; - /** Question type: 'choice' renders selectable options, 'text' renders free-form input, 'yesno' renders a binary Yes/No choice. */ + /** Question type: 'choice' renders selectable options, 'text' renders free-form input, 'yesno' renders a Yes/No choice with an optional 'Other' feedback field. */ type: QuestionType; /** Selectable choices. REQUIRED when type='choice'. IGNORED for 'text' and 'yesno'. */ options?: QuestionOption[]; /** Allow multiple selections. Only applies when type='choice'. */ multiSelect?: boolean; - /** Placeholder hint text. For type='text', shown in the input field. For type='choice', shown in the "Other" custom input. */ + /** Placeholder hint text. For type='text', shown in the input field. For type='choice' and 'yesno', shown in the 'Other' custom input. */ placeholder?: string; /** Allow the question to consume more vertical space instead of being strictly capped. */ unconstrainedHeight?: boolean; diff --git a/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap b/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap index 5676b42132..a4790dc188 100644 --- a/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap +++ b/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap @@ -88,7 +88,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps "type": "boolean", }, "options": { - "description": "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added. Not needed for 'text' or 'yesno' types.", + "description": "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added for 'choice' and 'yesno' types. Not needed for 'text' or 'yesno'.", "items": { "properties": { "description": { @@ -109,7 +109,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps "type": "array", }, "placeholder": { - "description": "Hint text shown in the input field. For type='text', shown in the main input. For type='choice', shown in the 'Other' custom input.", + "description": "Hint text shown in the input field. For type='text', shown in the main input. For type='choice' and 'yesno', shown in the 'Other' custom input.", "type": "string", }, "question": { @@ -118,7 +118,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps }, "type": { "default": "choice", - "description": "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation.", + "description": "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation with optional 'Other' feedback.", "enum": [ "choice", "text", @@ -918,7 +918,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > "type": "boolean", }, "options": { - "description": "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added. Not needed for 'text' or 'yesno' types.", + "description": "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added for 'choice' and 'yesno' types. Not needed for 'text' or 'yesno'.", "items": { "properties": { "description": { @@ -939,7 +939,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > "type": "array", }, "placeholder": { - "description": "Hint text shown in the input field. For type='text', shown in the main input. For type='choice', shown in the 'Other' custom input.", + "description": "Hint text shown in the input field. For type='text', shown in the main input. For type='choice' and 'yesno', shown in the 'Other' custom input.", "type": "string", }, "question": { @@ -948,7 +948,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > }, "type": { "default": "choice", - "description": "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation.", + "description": "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation with optional 'Other' feedback.", "enum": [ "choice", "text", diff --git a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts index dcf9e6e86e..60a52fc6ad 100644 --- a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts +++ b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts @@ -695,12 +695,12 @@ The agent did not use the todo list because this task could be completed by a ti enum: ['choice', 'text', 'yesno'], default: 'choice', description: - "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation.", + "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation with optional 'Other' feedback.", }, [ASK_USER_QUESTION_PARAM_OPTIONS]: { type: 'array', description: - "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added. Not needed for 'text' or 'yesno' types.", + "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added for 'choice' and 'yesno' types. Not needed for 'text' or 'yesno'.", items: { type: 'object', required: [ @@ -729,7 +729,7 @@ The agent did not use the todo list because this task could be completed by a ti [ASK_USER_QUESTION_PARAM_PLACEHOLDER]: { type: 'string', description: - "Hint text shown in the input field. For type='text', shown in the main input. For type='choice', shown in the 'Other' custom input.", + "Hint text shown in the input field. For type='text', shown in the main input. For type='choice' and 'yesno', shown in the 'Other' custom input.", }, }, }, diff --git a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts index b69ca43e5a..a86a20378e 100644 --- a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts +++ b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts @@ -671,12 +671,12 @@ The agent did not use the todo list because this task could be completed by a ti enum: ['choice', 'text', 'yesno'], default: 'choice', description: - "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation.", + "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation with optional 'Other' feedback.", }, [ASK_USER_QUESTION_PARAM_OPTIONS]: { type: 'array', description: - "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added. Not needed for 'text' or 'yesno' types.", + "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added for 'choice' and 'yesno' types. Not needed for 'text' or 'yesno'.", items: { type: 'object', required: [ @@ -705,7 +705,7 @@ The agent did not use the todo list because this task could be completed by a ti [ASK_USER_QUESTION_PARAM_PLACEHOLDER]: { type: 'string', description: - "Hint text shown in the input field. For type='text', shown in the main input. For type='choice', shown in the 'Other' custom input.", + "Hint text shown in the input field. For type='text', shown in the main input. For type='choice' and 'yesno', shown in the 'Other' custom input.", }, }, },