Enable 'Other' option for yesno question type (#24545)

This commit is contained in:
ruomeng
2026-04-02 15:42:53 -04:00
committed by GitHub
parent 2567ad5cd9
commit 40b486fbca
6 changed files with 74 additions and 28 deletions
@@ -1409,6 +1409,53 @@ describe('AskUserDialog', () => {
expect(lastFrame()).toMatchSnapshot(); 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(
<AskUserDialog
questions={questions}
onSubmit={onSubmit}
onCancel={vi.fn()}
width={80}
/>,
{ 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 () => { it('expands paste placeholders in multi-select custom option via Done', async () => {
@@ -511,8 +511,9 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
}) => { }) => {
const keyMatchers = useKeyMatchers(); const keyMatchers = useKeyMatchers();
const isAlternateBuffer = useAlternateBuffer(); const isAlternateBuffer = useAlternateBuffer();
const numOptions = const hasAll = question.multiSelect && (question.options?.length ?? 0) > 1;
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0); // 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 numLen = String(numOptions).length;
const radioWidth = 2; // "● " const radioWidth = 2; // "● "
const numberWidth = numLen + 2; // e.g., "1. " const numberWidth = numLen + 2; // e.g., "1. "
@@ -735,17 +736,15 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
list.push({ key: 'all', value: allItem }); list.push({ key: 'all', value: allItem });
} }
// Only add custom option for choice type, not yesno // Add custom option for choice and yesno types
if (question.type !== 'yesno') { const otherItem: OptionItem = {
const otherItem: OptionItem = { key: 'other',
key: 'other', label: customOptionText || '',
label: customOptionText || '', description: '',
description: '', type: 'other',
type: 'other', index: list.length,
index: list.length, };
}; list.push({ key: 'other', value: otherItem });
list.push({ key: 'other', value: otherItem });
}
if (question.multiSelect) { if (question.multiSelect) {
const doneItem: OptionItem = { const doneItem: OptionItem = {
@@ -759,7 +758,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
} }
return list; return list;
}, [questionOptions, question.multiSelect, question.type, customOptionText]); }, [questionOptions, question.multiSelect, customOptionText]);
const handleHighlight = useCallback( const handleHighlight = useCallback(
(itemValue: OptionItem) => { (itemValue: OptionItem) => {
+2 -2
View File
@@ -183,13 +183,13 @@ export enum QuestionType {
export interface Question { export interface Question {
question: string; question: string;
header: 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; type: QuestionType;
/** Selectable choices. REQUIRED when type='choice'. IGNORED for 'text' and 'yesno'. */ /** Selectable choices. REQUIRED when type='choice'. IGNORED for 'text' and 'yesno'. */
options?: QuestionOption[]; options?: QuestionOption[];
/** Allow multiple selections. Only applies when type='choice'. */ /** Allow multiple selections. Only applies when type='choice'. */
multiSelect?: boolean; 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; placeholder?: string;
/** Allow the question to consume more vertical space instead of being strictly capped. */ /** Allow the question to consume more vertical space instead of being strictly capped. */
unconstrainedHeight?: boolean; unconstrainedHeight?: boolean;
@@ -88,7 +88,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
"type": "boolean", "type": "boolean",
}, },
"options": { "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": { "items": {
"properties": { "properties": {
"description": { "description": {
@@ -109,7 +109,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
"type": "array", "type": "array",
}, },
"placeholder": { "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", "type": "string",
}, },
"question": { "question": {
@@ -118,7 +118,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps
}, },
"type": { "type": {
"default": "choice", "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": [ "enum": [
"choice", "choice",
"text", "text",
@@ -918,7 +918,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
"type": "boolean", "type": "boolean",
}, },
"options": { "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": { "items": {
"properties": { "properties": {
"description": { "description": {
@@ -939,7 +939,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
"type": "array", "type": "array",
}, },
"placeholder": { "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", "type": "string",
}, },
"question": { "question": {
@@ -948,7 +948,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview >
}, },
"type": { "type": {
"default": "choice", "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": [ "enum": [
"choice", "choice",
"text", "text",
@@ -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'], enum: ['choice', 'text', 'yesno'],
default: 'choice', default: 'choice',
description: 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]: { [ASK_USER_QUESTION_PARAM_OPTIONS]: {
type: 'array', type: 'array',
description: 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: { items: {
type: 'object', type: 'object',
required: [ 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]: { [ASK_USER_QUESTION_PARAM_PLACEHOLDER]: {
type: 'string', type: 'string',
description: 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.",
}, },
}, },
}, },
@@ -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'], enum: ['choice', 'text', 'yesno'],
default: 'choice', default: 'choice',
description: 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]: { [ASK_USER_QUESTION_PARAM_OPTIONS]: {
type: 'array', type: 'array',
description: 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: { items: {
type: 'object', type: 'object',
required: [ 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]: { [ASK_USER_QUESTION_PARAM_PLACEHOLDER]: {
type: 'string', type: 'string',
description: 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.",
}, },
}, },
}, },