mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-22 07:41:23 -07:00
feat: wire up AskUserTool with dialog (#17411)
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
type AskUserResponse,
|
||||
} from '../confirmation-bus/types.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { ASK_USER_TOOL_NAME } from './tool-names.js';
|
||||
import { ASK_USER_TOOL_NAME, ASK_USER_DISPLAY_NAME } from './tool-names.js';
|
||||
|
||||
export interface AskUserParams {
|
||||
questions: Question[];
|
||||
@@ -33,7 +33,7 @@ export class AskUserTool extends BaseDeclarativeTool<
|
||||
constructor(messageBus: MessageBus) {
|
||||
super(
|
||||
ASK_USER_TOOL_NAME,
|
||||
'Ask User',
|
||||
ASK_USER_DISPLAY_NAME,
|
||||
'Ask the user one or more questions to gather preferences, clarify requirements, or make decisions.',
|
||||
Kind.Communicate,
|
||||
{
|
||||
@@ -62,15 +62,14 @@ export class AskUserTool extends BaseDeclarativeTool<
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['choice', 'text', 'yesno'],
|
||||
default: 'choice',
|
||||
description:
|
||||
"Question type. 'choice' (default) shows selectable options, 'text' shows a free-form text input, 'yesno' shows a binary Yes/No choice.",
|
||||
"Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation.",
|
||||
},
|
||||
options: {
|
||||
type: 'array',
|
||||
description:
|
||||
"Required for 'choice' type, ignored for 'text' and 'yesno'. The available choices (2-4 options). Do NOT include an 'Other' option - one is automatically added for 'choice' type.",
|
||||
minItems: 2,
|
||||
maxItems: 4,
|
||||
"The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added. Not needed for 'text' or 'yesno' types.",
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['label', 'description'],
|
||||
@@ -78,12 +77,12 @@ export class AskUserTool extends BaseDeclarativeTool<
|
||||
label: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.',
|
||||
'The display text for this option (1-5 words). Example: "OAuth 2.0"',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Explanation of what this option means or what will happen if chosen. Useful for providing context about trade-offs or implications.',
|
||||
'Brief explanation of this option. Example: "Industry standard, supports SSO"',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -91,12 +90,12 @@ export class AskUserTool extends BaseDeclarativeTool<
|
||||
multiSelect: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
"Only applies to 'choice' type. Set to true to allow multiple selections.",
|
||||
"Only applies when type='choice'. Set to true to allow selecting multiple options.",
|
||||
},
|
||||
placeholder: {
|
||||
type: 'string',
|
||||
description:
|
||||
"Optional hint text for 'text' type input field.",
|
||||
"Only applies when type='text'. Hint text shown in the input field.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -107,6 +106,51 @@ export class AskUserTool extends BaseDeclarativeTool<
|
||||
);
|
||||
}
|
||||
|
||||
protected override validateToolParamValues(
|
||||
params: AskUserParams,
|
||||
): string | null {
|
||||
if (!params.questions || params.questions.length === 0) {
|
||||
return 'At least one question is required.';
|
||||
}
|
||||
|
||||
for (let i = 0; i < params.questions.length; i++) {
|
||||
const q = params.questions[i];
|
||||
const questionType = q.type ?? QuestionType.CHOICE;
|
||||
|
||||
// Validate that 'choice' type has options
|
||||
if (questionType === QuestionType.CHOICE) {
|
||||
if (!q.options || q.options.length < 2) {
|
||||
return `Question ${i + 1}: type='choice' requires 'options' array with 2-4 items.`;
|
||||
}
|
||||
if (q.options.length > 4) {
|
||||
return `Question ${i + 1}: 'options' array must have at most 4 items.`;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate option structure if provided
|
||||
if (q.options) {
|
||||
for (let j = 0; j < q.options.length; j++) {
|
||||
const opt = q.options[j];
|
||||
if (
|
||||
!opt.label ||
|
||||
typeof opt.label !== 'string' ||
|
||||
!opt.label.trim()
|
||||
) {
|
||||
return `Question ${i + 1}, option ${j + 1}: 'label' is required and must be a non-empty string.`;
|
||||
}
|
||||
if (
|
||||
opt.description === undefined ||
|
||||
typeof opt.description !== 'string'
|
||||
) {
|
||||
return `Question ${i + 1}, option ${j + 1}: 'description' is required and must be a string.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected createInvocation(
|
||||
params: AskUserParams,
|
||||
messageBus: MessageBus,
|
||||
@@ -148,16 +192,28 @@ export class AskUserInvocation extends BaseToolInvocation<
|
||||
if (response.correlationId === correlationId) {
|
||||
cleanup();
|
||||
|
||||
// Build formatted key-value display
|
||||
const formattedAnswers = Object.entries(response.answers)
|
||||
.map(([index, answer]) => {
|
||||
const question = this.params.questions[parseInt(index, 10)];
|
||||
const category = question?.header ?? `Q${index}`;
|
||||
return ` ${category} → ${answer}`;
|
||||
})
|
||||
.join('\n');
|
||||
// Handle user cancellation
|
||||
if (response.cancelled) {
|
||||
resolve({
|
||||
llmContent: 'User dismissed ask user dialog without answering.',
|
||||
returnDisplay: 'User dismissed dialog',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const returnDisplay = `User answered:\n${formattedAnswers}`;
|
||||
// Build formatted key-value display
|
||||
const answerEntries = Object.entries(response.answers);
|
||||
const hasAnswers = answerEntries.length > 0;
|
||||
|
||||
const returnDisplay = hasAnswers
|
||||
? `**User answered:**\n${answerEntries
|
||||
.map(([index, answer]) => {
|
||||
const question = this.params.questions[parseInt(index, 10)];
|
||||
const category = question?.header ?? `Q${index}`;
|
||||
return ` ${category} → ${answer}`;
|
||||
})
|
||||
.join('\n')}`
|
||||
: 'User submitted without answering questions.';
|
||||
|
||||
resolve({
|
||||
llmContent: JSON.stringify({ answers: response.answers }),
|
||||
|
||||
Reference in New Issue
Block a user