Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 5521e0d

Browse files
committed
feat: implement multi-question support with options and UI enhancements
1 parent 08aa544 commit 5521e0d

15 files changed

Lines changed: 420 additions & 114 deletions

File tree

packages/types/src/global-settings.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ export const globalSettingsSchema = z.object({
197197
hasOpenedModeSelector: z.boolean().optional(),
198198
lastModeExportPath: z.string().optional(),
199199
lastModeImportPath: z.string().optional(),
200+
201+
/**
202+
* Whether to show multiple questions one by one or all at once.
203+
* @default false (all at once)
204+
*/
205+
showQuestionsOneByOne: z.boolean().optional(),
200206
})
201207

202208
export type GlobalSettings = z.infer<typeof globalSettingsSchema>
@@ -364,6 +370,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
364370
mode: "code", // "architect",
365371

366372
customModes: [],
373+
showQuestionsOneByOne: false,
367374
}
368375

369376
export const EVALS_TIMEOUT = 5 * 60 * 1_000

packages/types/src/vscode-extension-host.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ export type ExtensionState = Pick<
260260
| "enterBehavior"
261261
| "includeCurrentTime"
262262
| "includeCurrentCost"
263+
| "showQuestionsOneByOne"
263264
| "maxGitStatusFiles"
264265
| "requestDelaySeconds"
265266
> & {

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,9 @@ export class NativeToolCallParser {
394394
break
395395

396396
case "ask_followup_question":
397-
if (partialArgs.question !== undefined || partialArgs.follow_up !== undefined) {
397+
if (partialArgs.questions !== undefined || partialArgs.follow_up !== undefined) {
398398
nativeArgs = {
399-
question: partialArgs.question,
399+
questions: Array.isArray(partialArgs.questions) ? partialArgs.questions : undefined,
400400
follow_up: Array.isArray(partialArgs.follow_up) ? partialArgs.follow_up : undefined,
401401
}
402402
}
@@ -676,9 +676,9 @@ export class NativeToolCallParser {
676676
break
677677

678678
case "ask_followup_question":
679-
if (args.question !== undefined && args.follow_up !== undefined) {
679+
if (args.questions !== undefined && args.follow_up !== undefined) {
680680
nativeArgs = {
681-
question: args.question,
681+
questions: Array.isArray(args.questions) ? args.questions : undefined,
682682
follow_up: args.follow_up,
683683
} as NativeArgsFor<TName>
684684
}

src/core/prompts/tools/native-tools/ask_followup_question.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,26 @@ import type OpenAI from "openai"
33
const ASK_FOLLOWUP_QUESTION_DESCRIPTION = `Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
44
55
Parameters:
6-
- question: (required) A clear, specific question addressing the information needed
6+
- questions: (required) A list of questions to ask. Each question can be a simple string or an object with "text" and "options" for multiple choice.
77
- follow_up: (required) A list of 2-4 suggested answers. Suggestions must be complete, actionable answers without placeholders. Optionally include mode to switch modes (code/architect/etc.)
88
99
Example: Asking for file path
10-
{ "question": "What is the path to the frontend-config.json file?", "follow_up": [{ "text": "./src/frontend-config.json", "mode": null }, { "text": "./config/frontend-config.json", "mode": null }, { "text": "./frontend-config.json", "mode": null }] }
10+
{ "questions": ["What is the path to the frontend-config.json file?"], "follow_up": [{ "text": "./src/frontend-config.json", "mode": null }, { "text": "./config/frontend-config.json", "mode": null }, { "text": "./frontend-config.json", "mode": null }] }
11+
12+
Example: Asking with multiple questions and choices
13+
{
14+
"questions": [
15+
{ "text": "Which framework are you using?", "options": ["React", "Vue", "Svelte", "Other"] },
16+
"What is your project name?",
17+
{ "text": "Include telemetry?", "options": ["Yes", "No"] }
18+
],
19+
"follow_up": [{ "text": "I've answered the questions", "mode": null }]
20+
}
1121
1222
Example: Asking with mode switch
13-
{ "question": "Would you like me to implement this feature?", "follow_up": [{ "text": "Yes, implement it now", "mode": "code" }, { "text": "No, just plan it out", "mode": "architect" }] }`
23+
{ "questions": ["Would you like me to implement this feature?"], "follow_up": [{ "text": "Yes, implement it now", "mode": "code" }, { "text": "No, just plan it out", "mode": "architect" }] }`
1424

15-
const QUESTION_PARAMETER_DESCRIPTION = `Clear, specific question that captures the missing information you need`
25+
const QUESTIONS_PARAMETER_DESCRIPTION = `List of questions to ask. Each question can be a string or an object with "text" and "options" for multiple choice.`
1626

1727
const FOLLOW_UP_PARAMETER_DESCRIPTION = `Required list of 2-4 suggested responses; each suggestion must be a complete, actionable answer and may include a mode switch`
1828

@@ -29,9 +39,29 @@ export default {
2939
parameters: {
3040
type: "object",
3141
properties: {
32-
question: {
33-
type: "string",
34-
description: QUESTION_PARAMETER_DESCRIPTION,
42+
questions: {
43+
type: "array",
44+
items: {
45+
anyOf: [
46+
{
47+
type: "string",
48+
},
49+
{
50+
type: "object",
51+
properties: {
52+
text: { type: "string" },
53+
options: {
54+
type: "array",
55+
items: { type: "string" },
56+
},
57+
},
58+
required: ["text", "options"],
59+
additionalProperties: false,
60+
},
61+
],
62+
},
63+
description: QUESTIONS_PARAMETER_DESCRIPTION,
64+
minItems: 1,
3565
},
3666
follow_up: {
3767
type: "array",
@@ -55,7 +85,7 @@ export default {
5585
maxItems: 4,
5686
},
5787
},
58-
required: ["question", "follow_up"],
88+
required: ["questions", "follow_up"],
5989
additionalProperties: false,
6090
},
6191
},

src/core/tools/AskFollowupQuestionTool.ts

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ interface Suggestion {
1010
mode?: string
1111
}
1212

13+
interface Question {
14+
text: string
15+
options?: string[]
16+
}
17+
1318
interface AskFollowupQuestionParams {
14-
question: string
15-
questions?: string[]
19+
questions: Array<string | Question>
1620
follow_up: Suggestion[]
1721
}
1822

@@ -25,21 +29,33 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
2529
const follow_up_xml = params.follow_up
2630

2731
const suggestions: Suggestion[] = []
28-
const questions: string[] = []
32+
const questions: Array<string | Question> = []
2933

3034
if (questions_xml) {
3135
try {
36+
// Handle both simple <question> tags and more complex <question> tags with options
3237
const parsedQuestions = parseXml(questions_xml, ["question"]) as {
33-
question: string[] | string
38+
question: any[] | any
3439
}
3540

3641
const rawQuestions = Array.isArray(parsedQuestions?.question)
3742
? parsedQuestions.question
38-
: [parsedQuestions?.question].filter((q): q is string => q !== undefined)
43+
: [parsedQuestions?.question].filter((q): q is any => q !== undefined)
3944

4045
for (const q of rawQuestions) {
4146
if (typeof q === "string") {
4247
questions.push(q)
48+
} else if (typeof q === "object" && q !== null) {
49+
const text = q["#text"] || ""
50+
const optionsStr = q["@_options"]
51+
if (optionsStr) {
52+
questions.push({
53+
text,
54+
options: optionsStr.split(",").map((o: string) => o.trim()),
55+
})
56+
} else {
57+
questions.push(text)
58+
}
4359
}
4460
}
4561
} catch (error) {
@@ -49,8 +65,12 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
4965
}
5066
}
5167

68+
// If no questions array but we have a single question, use that
69+
if (questions.length === 0 && question) {
70+
questions.push(question)
71+
}
72+
5273
if (follow_up_xml) {
53-
// Define the actual structure returned by the XML parser
5474
type ParsedSuggestion = string | { "#text": string; "@_mode"?: string }
5575

5676
try {
@@ -62,13 +82,10 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
6282
? parsedSuggest.suggest
6383
: [parsedSuggest?.suggest].filter((sug): sug is ParsedSuggestion => sug !== undefined)
6484

65-
// Transform parsed XML to our Suggest format
6685
for (const sug of rawSuggestions) {
6786
if (typeof sug === "string") {
68-
// Simple string suggestion (no mode attribute)
6987
suggestions.push({ text: sug })
7088
} else {
71-
// XML object with text content and optional mode attribute
7289
const suggestion: Suggestion = { text: sug["#text"] }
7390
if (sug["@_mode"]) {
7491
suggestion.mode = sug["@_mode"]
@@ -84,34 +101,32 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
84101
}
85102

86103
return {
87-
question,
88104
questions,
89105
follow_up: suggestions,
90106
}
91107
}
92108

93109
async execute(params: AskFollowupQuestionParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
94-
const { question, questions, follow_up } = params
95-
const { handleError, pushToolResult, toolProtocol } = callbacks
110+
const { questions, follow_up } = params
111+
const { handleError, pushToolResult } = callbacks
96112

97113
try {
98-
if (!question && (!questions || questions.length === 0)) {
114+
if (!questions || questions.length === 0) {
99115
task.consecutiveMistakeCount++
100116
task.recordToolError("ask_followup_question")
101117
task.didToolFailInCurrentTurn = true
102-
pushToolResult(await task.sayAndCreateMissingParamError("ask_followup_question", "question"))
118+
pushToolResult(await task.sayAndCreateMissingParamError("ask_followup_question", "questions"))
103119
return
104120
}
105121

106122
// Transform follow_up suggestions to the format expected by task.ask
107-
const follow_up_json = {
108-
question,
123+
const followup_json = {
109124
questions,
110125
suggest: follow_up.map((s) => ({ answer: s.text, mode: s.mode })),
111126
}
112127

113128
task.consecutiveMistakeCount = 0
114-
const { text, images } = await task.ask("followup", JSON.stringify(follow_up_json), false)
129+
const { text, images } = await task.ask("followup", JSON.stringify(followup_json), false)
115130
await task.say("user_feedback", text ?? "", images)
116131
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
117132
} catch (error) {
@@ -120,15 +135,15 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
120135
}
121136

122137
override async handlePartial(task: Task, block: ToolUse<"ask_followup_question">): Promise<void> {
123-
// Get question from params (for XML protocol) or nativeArgs (for native protocol)
124-
const question: string | undefined = block.params.question ?? block.nativeArgs?.question
125-
// For now we don't stream multiple questions, only the main one if present
126-
// We could improve this to stream multiple questions but it requires UI changes to handle partial arrays
138+
// Get first question from questions array for streaming display
139+
const questions = block.nativeArgs?.questions ?? []
140+
const firstQuestion = questions[0]
141+
const questionText = typeof firstQuestion === "string" ? firstQuestion : firstQuestion?.text
127142

128-
// During partial streaming, only show the question to avoid displaying raw JSON
129-
// The full JSON with suggestions will be sent when the tool call is complete (!block.partial)
143+
// During partial streaming, only show the first question to avoid displaying raw JSON
144+
// The full JSON with all questions and suggestions will be sent when the tool call is complete
130145
await task
131-
.ask("followup", this.removeClosingTag("question", question, block.partial), block.partial)
146+
.ask("followup", this.removeClosingTag("question", questionText, block.partial), block.partial)
132147
.catch(() => {})
133148
}
134149
}

0 commit comments

Comments
 (0)