From b7ea247f45cd6ed31843f6958ad6e659e8a7a73a Mon Sep 17 00:00:00 2001
From: ScDor <18174994+ScDor@users.noreply.github.com>
Date: Fri, 16 Jan 2026 18:30:15 +0200
Subject: [PATCH 1/5] feat: enhance ask_followup_question to support multiple
questions
---
packages/types/src/followup.ts | 3 +
.../prompts/tools/ask-followup-question.ts | 15 +++-
src/core/tools/AskFollowupQuestionTool.ts | 33 +++++++-
.../__tests__/askFollowupQuestionTool.spec.ts | 26 +++++++
src/shared/tools.ts | 1 +
webview-ui/src/components/chat/ChatRow.tsx | 34 ++++++---
.../components/chat/MultiQuestionHandler.tsx | 76 +++++++++++++++++++
webview-ui/src/i18n/locales/en/chat.json | 7 +-
8 files changed, 178 insertions(+), 17 deletions(-)
create mode 100644 webview-ui/src/components/chat/MultiQuestionHandler.tsx
diff --git a/packages/types/src/followup.ts b/packages/types/src/followup.ts
index 1a5424cd11e..dc21fc26dd6 100644
--- a/packages/types/src/followup.ts
+++ b/packages/types/src/followup.ts
@@ -8,6 +8,8 @@ import { z } from "zod"
export interface FollowUpData {
/** The question being asked by the LLM */
question?: string
+ /** Array of questions being asked by the LLM */
+ questions?: string[]
/** Array of suggested answers that the user can select */
suggest?: Array
}
@@ -35,6 +37,7 @@ export const suggestionItemSchema = z.object({
*/
export const followUpDataSchema = z.object({
question: z.string().optional(),
+ questions: z.array(z.string()).optional(),
suggest: z.array(suggestionItemSchema).optional(),
})
diff --git a/src/core/prompts/tools/ask-followup-question.ts b/src/core/prompts/tools/ask-followup-question.ts
index c40684b8bc7..c00fd707f85 100644
--- a/src/core/prompts/tools/ask-followup-question.ts
+++ b/src/core/prompts/tools/ask-followup-question.ts
@@ -1,10 +1,11 @@
export function getAskFollowupQuestionDescription(): string {
return `## 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.
+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. You may ask multiple questions at once.
Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
+- question: (required) A clear, specific question addressing the information needed.
+- questions: (optional) A container for asking multiple questions. Use tags inside.
+- follow_up: (optional) A list of suggested answers, each in its own tag.
Usage:
@@ -15,6 +16,14 @@ Usage:
+Usage with multiple questions:
+
+
+Question 1?
+Question 2?
+
+
+
Example:
What is the path to the frontend-config.json file?
diff --git a/src/core/tools/AskFollowupQuestionTool.ts b/src/core/tools/AskFollowupQuestionTool.ts
index b75ca3b618e..69fe6c38d69 100644
--- a/src/core/tools/AskFollowupQuestionTool.ts
+++ b/src/core/tools/AskFollowupQuestionTool.ts
@@ -12,6 +12,7 @@ interface Suggestion {
interface AskFollowupQuestionParams {
question: string
+ questions?: string[]
follow_up: Suggestion[]
}
@@ -20,9 +21,33 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
parseLegacy(params: Partial>): AskFollowupQuestionParams {
const question = params.question || ""
+ const questions_xml = params.questions
const follow_up_xml = params.follow_up
const suggestions: Suggestion[] = []
+ const questions: string[] = []
+
+ if (questions_xml) {
+ try {
+ const parsedQuestions = parseXml(questions_xml, ["question"]) as {
+ question: string[] | string
+ }
+
+ const rawQuestions = Array.isArray(parsedQuestions?.question)
+ ? parsedQuestions.question
+ : [parsedQuestions?.question].filter((q): q is string => q !== undefined)
+
+ for (const q of rawQuestions) {
+ if (typeof q === "string") {
+ questions.push(q)
+ }
+ }
+ } catch (error) {
+ throw new Error(
+ `Failed to parse questions XML: ${error instanceof Error ? error.message : String(error)}`,
+ )
+ }
+ }
if (follow_up_xml) {
// Define the actual structure returned by the XML parser
@@ -60,16 +85,17 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
return {
question,
+ questions,
follow_up: suggestions,
}
}
async execute(params: AskFollowupQuestionParams, task: Task, callbacks: ToolCallbacks): Promise {
- const { question, follow_up } = params
+ const { question, questions, follow_up } = params
const { handleError, pushToolResult, toolProtocol } = callbacks
try {
- if (!question) {
+ if (!question && (!questions || questions.length === 0)) {
task.consecutiveMistakeCount++
task.recordToolError("ask_followup_question")
task.didToolFailInCurrentTurn = true
@@ -80,6 +106,7 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
// Transform follow_up suggestions to the format expected by task.ask
const follow_up_json = {
question,
+ questions,
suggest: follow_up.map((s) => ({ answer: s.text, mode: s.mode })),
}
@@ -95,6 +122,8 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
override async handlePartial(task: Task, block: ToolUse<"ask_followup_question">): Promise {
// Get question from params (for XML protocol) or nativeArgs (for native protocol)
const question: string | undefined = block.params.question ?? block.nativeArgs?.question
+ // For now we don't stream multiple questions, only the main one if present
+ // We could improve this to stream multiple questions but it requires UI changes to handle partial arrays
// During partial streaming, only show the question to avoid displaying raw JSON
// The full JSON with suggestions will be sent when the tool call is complete (!block.partial)
diff --git a/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts
index 074617130c9..0c3ab5d0dc3 100644
--- a/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts
+++ b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts
@@ -103,6 +103,32 @@ describe("askFollowupQuestionTool", () => {
)
})
+ it("should parse multiple questions from XML", async () => {
+ const block: ToolUse = {
+ type: "tool_use",
+ name: "ask_followup_question",
+ params: {
+ questions: "Question 1Question 2",
+ follow_up: "",
+ },
+ partial: false,
+ }
+
+ await askFollowupQuestionTool.handle(mockCline, block as ToolUse<"ask_followup_question">, {
+ askApproval: vi.fn(),
+ handleError: vi.fn(),
+ pushToolResult: mockPushToolResult,
+ removeClosingTag: vi.fn((tag, content) => content),
+ toolProtocol: "xml",
+ })
+
+ expect(mockCline.ask).toHaveBeenCalledWith(
+ "followup",
+ expect.stringContaining('"questions":["Question 1","Question 2"]'),
+ false,
+ )
+ })
+
describe("handlePartial with native protocol", () => {
it("should only send question during partial streaming to avoid raw JSON display", async () => {
const block: ToolUse<"ask_followup_question"> = {
diff --git a/src/shared/tools.ts b/src/shared/tools.ts
index f893a3d332e..602ebd785e6 100644
--- a/src/shared/tools.ts
+++ b/src/shared/tools.ts
@@ -76,6 +76,7 @@ export const toolParamNames = [
"old_string", // search_replace and edit_file parameter
"new_string", // search_replace and edit_file parameter
"expected_replacements", // edit_file parameter for multiple occurrences
+ "questions", // ask_followup_question parameter for multiple questions
] as const
export type ToolParamName = (typeof toolParamNames)[number]
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index a609d2dc7ec..8c0ef5e61ea 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -38,6 +38,7 @@ import McpResourceRow from "../mcp/McpResourceRow"
import { Mention } from "./Mention"
import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
import { FollowUpSuggest } from "./FollowUpSuggest"
+import { MultiQuestionHandler } from "./MultiQuestionHandler"
import { BatchFilePermission } from "./BatchFilePermission"
import { BatchDiffApproval } from "./BatchDiffApproval"
import { ProgressIndicator } from "./ProgressIndicator"
@@ -1632,17 +1633,28 @@ export const ChatRowContent = ({
)}
-
-
+ {followUpData?.questions && followUpData.questions.length > 0 ? (
+ {
+ onSuggestionClick?.({ answer: response })
+ }}
+ />
+ ) : (
+ <>
+
+
+ >
+ )}
>
)
diff --git a/webview-ui/src/components/chat/MultiQuestionHandler.tsx b/webview-ui/src/components/chat/MultiQuestionHandler.tsx
new file mode 100644
index 00000000000..34bf5e3ca75
--- /dev/null
+++ b/webview-ui/src/components/chat/MultiQuestionHandler.tsx
@@ -0,0 +1,76 @@
+import React, { useState, useEffect } from "react"
+import { Button, Textarea } from "@/components/ui"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+
+interface MultiQuestionHandlerProps {
+ questions: string[]
+ onSendResponse: (response: string) => void
+}
+
+export const MultiQuestionHandler = ({ questions, onSendResponse }: MultiQuestionHandlerProps) => {
+ const { t } = useAppTranslation()
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
+ const [answers, setAnswers] = useState(new Array(questions.length).fill(""))
+ const [inputValue, setInputValue] = useState("")
+
+ useEffect(() => {
+ setInputValue(answers[currentQuestionIndex] || "")
+ }, [currentQuestionIndex, answers])
+
+ const handleNext = () => {
+ const newAnswers = [...answers]
+ newAnswers[currentQuestionIndex] = inputValue
+ setAnswers(newAnswers)
+
+ if (currentQuestionIndex < questions.length - 1) {
+ setCurrentQuestionIndex(currentQuestionIndex + 1)
+ } else {
+ // Finish
+ const combined = questions
+ .map((q, i) => `Question: ${q}\nAnswer: ${newAnswers[i] || "(skipped)"}`)
+ .join("\n\n")
+ onSendResponse(combined)
+ }
+ }
+
+ const handlePrevious = () => {
+ const newAnswers = [...answers]
+ newAnswers[currentQuestionIndex] = inputValue
+ setAnswers(newAnswers)
+
+ if (currentQuestionIndex > 0) {
+ setCurrentQuestionIndex(currentQuestionIndex - 1)
+ }
+ }
+
+ return (
+
+
+ {t("chat:questions.questionNumberOfTotal", {
+ current: currentQuestionIndex + 1,
+ total: questions.length,
+ })}
+
+
{questions[currentQuestionIndex]}
+
+ )
+}
diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json
index 3ab2c037af2..e288bfa5a9a 100644
--- a/webview-ui/src/i18n/locales/en/chat.json
+++ b/webview-ui/src/i18n/locales/en/chat.json
@@ -287,7 +287,12 @@
"completionInstructions": "Subtask completed! You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task."
},
"questions": {
- "hasQuestion": "Roo has a question"
+ "hasQuestion": "Roo has a question",
+ "questionNumberOfTotal": "Question {{current}} of {{total}}",
+ "typeAnswer": "Type your answer here...",
+ "previous": "Previous",
+ "next": "Next",
+ "finish": "Finish"
},
"taskCompleted": "Task Completed",
"error": "Error",
From 08aa544b6aecbe56642c04f3caf207b764e57ad4 Mon Sep 17 00:00:00 2001
From: ScDor <18174994+ScDor@users.noreply.github.com>
Date: Sat, 17 Jan 2026 12:07:01 +0200
Subject: [PATCH 2/5] Fix multi-question UX: only send responses when Finish is
clicked
- Separate navigation logic from submission logic in MultiQuestionHandler
- Add dedicated handleFinish function to send combined responses
- Prevent immediate sending when navigating between questions
- Add comprehensive tests to verify the UX fix
- All existing functionality preserved, tests passing
---
.../components/chat/MultiQuestionHandler.tsx | 29 ++-
.../__tests__/MultiQuestionHandler.spec.tsx | 195 ++++++++++++++++++
2 files changed, 213 insertions(+), 11 deletions(-)
create mode 100644 webview-ui/src/components/chat/__tests__/MultiQuestionHandler.spec.tsx
diff --git a/webview-ui/src/components/chat/MultiQuestionHandler.tsx b/webview-ui/src/components/chat/MultiQuestionHandler.tsx
index 34bf5e3ca75..38eb4fbf706 100644
--- a/webview-ui/src/components/chat/MultiQuestionHandler.tsx
+++ b/webview-ui/src/components/chat/MultiQuestionHandler.tsx
@@ -24,12 +24,6 @@ export const MultiQuestionHandler = ({ questions, onSendResponse }: MultiQuestio
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex(currentQuestionIndex + 1)
- } else {
- // Finish
- const combined = questions
- .map((q, i) => `Question: ${q}\nAnswer: ${newAnswers[i] || "(skipped)"}`)
- .join("\n\n")
- onSendResponse(combined)
}
}
@@ -43,6 +37,15 @@ export const MultiQuestionHandler = ({ questions, onSendResponse }: MultiQuestio
}
}
+ const handleFinish = () => {
+ const newAnswers = [...answers]
+ newAnswers[currentQuestionIndex] = inputValue
+ setAnswers(newAnswers)
+
+ const combined = questions.map((q, i) => `Question: ${q}\nAnswer: ${newAnswers[i] || "(skipped)"}`).join("\n\n")
+ onSendResponse(combined)
+ }
+
return (
@@ -65,11 +68,15 @@ export const MultiQuestionHandler = ({ questions, onSendResponse }: MultiQuestio
{t("chat:questions.previous")}
)}
-
+ {currentQuestionIndex < questions.length - 1 ? (
+
+ ) : (
+
+ )}
)
diff --git a/webview-ui/src/components/chat/__tests__/MultiQuestionHandler.spec.tsx b/webview-ui/src/components/chat/__tests__/MultiQuestionHandler.spec.tsx
new file mode 100644
index 00000000000..942d5fc368c
--- /dev/null
+++ b/webview-ui/src/components/chat/__tests__/MultiQuestionHandler.spec.tsx
@@ -0,0 +1,195 @@
+import { render, screen, fireEvent } from "@/utils/test-utils"
+import { vi, describe, it, expect, beforeEach } from "vitest"
+import TranslationProvider from "@src/i18n/TranslationContext"
+import { MultiQuestionHandler } from "../MultiQuestionHandler"
+
+// Mock ExtensionStateContext
+vi.mock("@src/context/ExtensionStateContext", () => ({
+ useExtensionState: () => ({
+ language: "en",
+ }),
+}))
+
+// Mock react-i18next
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ i18n: {
+ t: (key: string, options?: Record) => {
+ // Mock specific translations used in tests
+ if (key === "chat:questions.questionNumberOfTotal" && options) {
+ return `Question ${options.current} of ${options.total}`
+ }
+ if (key === "chat:questions.typeAnswer") return "Type your answer..."
+ if (key === "chat:questions.previous") return "Previous"
+ if (key === "chat:questions.next") return "Next"
+ if (key === "chat:questions.finish") return "Finish"
+ return key
+ },
+ changeLanguage: vi.fn(),
+ },
+ }),
+}))
+
+// Mock translations
+vi.mock("@src/i18n/setup", () => ({
+ default: {
+ t: (key: string, options?: Record) => {
+ // Mock specific translations used in tests
+ if (key === "chat:questions.questionNumberOfTotal" && options) {
+ return `Question ${options.current} of ${options.total}`
+ }
+ if (key === "chat:questions.typeAnswer") return "Type your answer..."
+ if (key === "chat:questions.previous") return "Previous"
+ if (key === "chat:questions.next") return "Next"
+ if (key === "chat:questions.finish") return "Finish"
+ return key
+ },
+ changeLanguage: vi.fn(),
+ },
+ loadTranslations: vi.fn(),
+}))
+
+const TestWrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+)
+
+describe("MultiQuestionHandler", () => {
+ const mockOnSendResponse = vi.fn()
+
+ beforeEach(() => {
+ mockOnSendResponse.mockClear()
+ })
+
+ it("should render single question correctly", () => {
+ const questions = ["What is your name?"]
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByText("Question 1 of 1")).toBeInTheDocument()
+ expect(screen.getByText("What is your name?")).toBeInTheDocument()
+ expect(screen.getByText("Finish")).toBeInTheDocument()
+ })
+
+ it("should render multiple questions with navigation", () => {
+ const questions = ["What is your name?", "What is your age?"]
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByText("Question 1 of 2")).toBeInTheDocument()
+ expect(screen.getByText("What is your name?")).toBeInTheDocument()
+ expect(screen.getByText("Next")).toBeInTheDocument()
+ expect(screen.queryByText("Previous")).not.toBeInTheDocument()
+ expect(screen.queryByText("Finish")).not.toBeInTheDocument()
+ })
+
+ it("should navigate between questions without sending responses", () => {
+ const questions = ["What is your name?", "What is your age?"]
+ render(
+
+
+ ,
+ )
+
+ const textarea = screen.getByPlaceholderText("Type your answer...")
+ fireEvent.change(textarea, { target: { value: "John" } })
+
+ const nextButton = screen.getByText("Next")
+ fireEvent.click(nextButton)
+
+ expect(screen.getByText("Question 2 of 2")).toBeInTheDocument()
+ expect(screen.getByText("What is your age?")).toBeInTheDocument()
+ expect(mockOnSendResponse).not.toHaveBeenCalled()
+
+ const previousButton = screen.getByText("Previous")
+ fireEvent.click(previousButton)
+
+ expect(screen.getByText("Question 1 of 2")).toBeInTheDocument()
+ expect(screen.getByText("What is your name?")).toBeInTheDocument()
+ expect(textarea).toHaveValue("John")
+ expect(mockOnSendResponse).not.toHaveBeenCalled()
+ })
+
+ it("should only send response when Finish is clicked", () => {
+ const questions = ["What is your name?", "What is your age?"]
+ render(
+
+
+ ,
+ )
+
+ const textarea = screen.getByPlaceholderText("Type your answer...")
+ fireEvent.change(textarea, { target: { value: "John" } })
+
+ const nextButton = screen.getByText("Next")
+ fireEvent.click(nextButton)
+
+ fireEvent.change(textarea, { target: { value: "25" } })
+
+ const finishButton = screen.getByText("Finish")
+ fireEvent.click(finishButton)
+
+ expect(mockOnSendResponse).toHaveBeenCalledTimes(1)
+ expect(mockOnSendResponse).toHaveBeenCalledWith(
+ "Question: What is your name?\nAnswer: John\n\nQuestion: What is your age?\nAnswer: 25",
+ )
+ })
+
+ it("should handle skipped questions", () => {
+ const questions = ["What is your name?", "What is your age?"]
+ render(
+
+
+ ,
+ )
+
+ const nextButton = screen.getByText("Next")
+ fireEvent.click(nextButton) // Skip first question
+
+ const textarea = screen.getByPlaceholderText("Type your answer...")
+ fireEvent.change(textarea, { target: { value: "25" } })
+
+ const finishButton = screen.getByText("Finish")
+ fireEvent.click(finishButton)
+
+ expect(mockOnSendResponse).toHaveBeenCalledWith(
+ "Question: What is your name?\nAnswer: (skipped)\n\nQuestion: What is your age?\nAnswer: 25",
+ )
+ })
+
+ it("should preserve answers when navigating back and forth", () => {
+ const questions = ["Q1", "Q2", "Q3"]
+ render(
+
+
+ ,
+ )
+
+ const textarea = screen.getByPlaceholderText("Type your answer...")
+
+ // Answer Q1
+ fireEvent.change(textarea, { target: { value: "A1" } })
+ fireEvent.click(screen.getByText("Next"))
+
+ // Answer Q2
+ fireEvent.change(textarea, { target: { value: "A2" } })
+ fireEvent.click(screen.getByText("Next"))
+
+ // Go back to Q1
+ fireEvent.click(screen.getByText("Previous"))
+ fireEvent.click(screen.getByText("Previous"))
+
+ expect(textarea).toHaveValue("A1")
+
+ // Go to Q2
+ fireEvent.click(screen.getByText("Next"))
+ expect(textarea).toHaveValue("A2")
+
+ expect(mockOnSendResponse).not.toHaveBeenCalled()
+ })
+})
From 5521e0df823981784aa8a0efd2e709fc8d7ec0d1 Mon Sep 17 00:00:00 2001
From: ScDor <18174994+ScDor@users.noreply.github.com>
Date: Sat, 31 Jan 2026 13:40:25 +0200
Subject: [PATCH 3/5] feat: implement multi-question support with options and
UI enhancements
---
packages/types/src/global-settings.ts | 7 +
packages/types/src/vscode-extension-host.ts | 1 +
.../assistant-message/NativeToolCallParser.ts | 8 +-
.../native-tools/ask_followup_question.ts | 46 +++-
src/core/tools/AskFollowupQuestionTool.ts | 63 ++++--
.../__tests__/askFollowupQuestionTool.spec.ts | 107 +++++++--
src/shared/tools.ts | 2 +-
.../components/chat/MultiQuestionHandler.tsx | 207 +++++++++++++-----
.../__tests__/MultiQuestionHandler.spec.tsx | 47 ++++
.../src/components/settings/SettingsView.tsx | 3 +
.../src/components/settings/UISettings.tsx | 29 +++
.../settings/__tests__/UISettings.spec.tsx | 1 +
.../src/components/ui/autosize-textarea.tsx | 2 +-
.../src/context/ExtensionStateContext.tsx | 10 +
webview-ui/src/i18n/locales/en/chat.json | 1 +
15 files changed, 420 insertions(+), 114 deletions(-)
diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts
index 9a17834ced7..0ec74e347ac 100644
--- a/packages/types/src/global-settings.ts
+++ b/packages/types/src/global-settings.ts
@@ -197,6 +197,12 @@ export const globalSettingsSchema = z.object({
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),
lastModeImportPath: z.string().optional(),
+
+ /**
+ * Whether to show multiple questions one by one or all at once.
+ * @default false (all at once)
+ */
+ showQuestionsOneByOne: z.boolean().optional(),
})
export type GlobalSettings = z.infer
@@ -364,6 +370,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
mode: "code", // "architect",
customModes: [],
+ showQuestionsOneByOne: false,
}
export const EVALS_TIMEOUT = 5 * 60 * 1_000
diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts
index 86d8b2ddbbe..ebddd0ef64c 100644
--- a/packages/types/src/vscode-extension-host.ts
+++ b/packages/types/src/vscode-extension-host.ts
@@ -260,6 +260,7 @@ export type ExtensionState = Pick<
| "enterBehavior"
| "includeCurrentTime"
| "includeCurrentCost"
+ | "showQuestionsOneByOne"
| "maxGitStatusFiles"
| "requestDelaySeconds"
> & {
diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts
index 56d71eb3dd0..961b113d8d6 100644
--- a/src/core/assistant-message/NativeToolCallParser.ts
+++ b/src/core/assistant-message/NativeToolCallParser.ts
@@ -394,9 +394,9 @@ export class NativeToolCallParser {
break
case "ask_followup_question":
- if (partialArgs.question !== undefined || partialArgs.follow_up !== undefined) {
+ if (partialArgs.questions !== undefined || partialArgs.follow_up !== undefined) {
nativeArgs = {
- question: partialArgs.question,
+ questions: Array.isArray(partialArgs.questions) ? partialArgs.questions : undefined,
follow_up: Array.isArray(partialArgs.follow_up) ? partialArgs.follow_up : undefined,
}
}
@@ -676,9 +676,9 @@ export class NativeToolCallParser {
break
case "ask_followup_question":
- if (args.question !== undefined && args.follow_up !== undefined) {
+ if (args.questions !== undefined && args.follow_up !== undefined) {
nativeArgs = {
- question: args.question,
+ questions: Array.isArray(args.questions) ? args.questions : undefined,
follow_up: args.follow_up,
} as NativeArgsFor
}
diff --git a/src/core/prompts/tools/native-tools/ask_followup_question.ts b/src/core/prompts/tools/native-tools/ask_followup_question.ts
index b0591206ade..45465962926 100644
--- a/src/core/prompts/tools/native-tools/ask_followup_question.ts
+++ b/src/core/prompts/tools/native-tools/ask_followup_question.ts
@@ -3,16 +3,26 @@ import type OpenAI from "openai"
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.
Parameters:
-- question: (required) A clear, specific question addressing the information needed
+- 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.
- 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.)
Example: Asking for file path
-{ "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 }] }
+{ "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 }] }
+
+Example: Asking with multiple questions and choices
+{
+ "questions": [
+ { "text": "Which framework are you using?", "options": ["React", "Vue", "Svelte", "Other"] },
+ "What is your project name?",
+ { "text": "Include telemetry?", "options": ["Yes", "No"] }
+ ],
+ "follow_up": [{ "text": "I've answered the questions", "mode": null }]
+}
Example: Asking with mode switch
-{ "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" }] }`
+{ "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" }] }`
-const QUESTION_PARAMETER_DESCRIPTION = `Clear, specific question that captures the missing information you need`
+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.`
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`
@@ -29,9 +39,29 @@ export default {
parameters: {
type: "object",
properties: {
- question: {
- type: "string",
- description: QUESTION_PARAMETER_DESCRIPTION,
+ questions: {
+ type: "array",
+ items: {
+ anyOf: [
+ {
+ type: "string",
+ },
+ {
+ type: "object",
+ properties: {
+ text: { type: "string" },
+ options: {
+ type: "array",
+ items: { type: "string" },
+ },
+ },
+ required: ["text", "options"],
+ additionalProperties: false,
+ },
+ ],
+ },
+ description: QUESTIONS_PARAMETER_DESCRIPTION,
+ minItems: 1,
},
follow_up: {
type: "array",
@@ -55,7 +85,7 @@ export default {
maxItems: 4,
},
},
- required: ["question", "follow_up"],
+ required: ["questions", "follow_up"],
additionalProperties: false,
},
},
diff --git a/src/core/tools/AskFollowupQuestionTool.ts b/src/core/tools/AskFollowupQuestionTool.ts
index 69fe6c38d69..b1f00f3f9c1 100644
--- a/src/core/tools/AskFollowupQuestionTool.ts
+++ b/src/core/tools/AskFollowupQuestionTool.ts
@@ -10,9 +10,13 @@ interface Suggestion {
mode?: string
}
+interface Question {
+ text: string
+ options?: string[]
+}
+
interface AskFollowupQuestionParams {
- question: string
- questions?: string[]
+ questions: Array
follow_up: Suggestion[]
}
@@ -25,21 +29,33 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
const follow_up_xml = params.follow_up
const suggestions: Suggestion[] = []
- const questions: string[] = []
+ const questions: Array = []
if (questions_xml) {
try {
+ // Handle both simple tags and more complex tags with options
const parsedQuestions = parseXml(questions_xml, ["question"]) as {
- question: string[] | string
+ question: any[] | any
}
const rawQuestions = Array.isArray(parsedQuestions?.question)
? parsedQuestions.question
- : [parsedQuestions?.question].filter((q): q is string => q !== undefined)
+ : [parsedQuestions?.question].filter((q): q is any => q !== undefined)
for (const q of rawQuestions) {
if (typeof q === "string") {
questions.push(q)
+ } else if (typeof q === "object" && q !== null) {
+ const text = q["#text"] || ""
+ const optionsStr = q["@_options"]
+ if (optionsStr) {
+ questions.push({
+ text,
+ options: optionsStr.split(",").map((o: string) => o.trim()),
+ })
+ } else {
+ questions.push(text)
+ }
}
}
} catch (error) {
@@ -49,8 +65,12 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
}
}
+ // If no questions array but we have a single question, use that
+ if (questions.length === 0 && question) {
+ questions.push(question)
+ }
+
if (follow_up_xml) {
- // Define the actual structure returned by the XML parser
type ParsedSuggestion = string | { "#text": string; "@_mode"?: string }
try {
@@ -62,13 +82,10 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
? parsedSuggest.suggest
: [parsedSuggest?.suggest].filter((sug): sug is ParsedSuggestion => sug !== undefined)
- // Transform parsed XML to our Suggest format
for (const sug of rawSuggestions) {
if (typeof sug === "string") {
- // Simple string suggestion (no mode attribute)
suggestions.push({ text: sug })
} else {
- // XML object with text content and optional mode attribute
const suggestion: Suggestion = { text: sug["#text"] }
if (sug["@_mode"]) {
suggestion.mode = sug["@_mode"]
@@ -84,34 +101,32 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
}
return {
- question,
questions,
follow_up: suggestions,
}
}
async execute(params: AskFollowupQuestionParams, task: Task, callbacks: ToolCallbacks): Promise {
- const { question, questions, follow_up } = params
- const { handleError, pushToolResult, toolProtocol } = callbacks
+ const { questions, follow_up } = params
+ const { handleError, pushToolResult } = callbacks
try {
- if (!question && (!questions || questions.length === 0)) {
+ if (!questions || questions.length === 0) {
task.consecutiveMistakeCount++
task.recordToolError("ask_followup_question")
task.didToolFailInCurrentTurn = true
- pushToolResult(await task.sayAndCreateMissingParamError("ask_followup_question", "question"))
+ pushToolResult(await task.sayAndCreateMissingParamError("ask_followup_question", "questions"))
return
}
// Transform follow_up suggestions to the format expected by task.ask
- const follow_up_json = {
- question,
+ const followup_json = {
questions,
suggest: follow_up.map((s) => ({ answer: s.text, mode: s.mode })),
}
task.consecutiveMistakeCount = 0
- const { text, images } = await task.ask("followup", JSON.stringify(follow_up_json), false)
+ const { text, images } = await task.ask("followup", JSON.stringify(followup_json), false)
await task.say("user_feedback", text ?? "", images)
pushToolResult(formatResponse.toolResult(`\n${text}\n`, images))
} catch (error) {
@@ -120,15 +135,15 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
}
override async handlePartial(task: Task, block: ToolUse<"ask_followup_question">): Promise {
- // Get question from params (for XML protocol) or nativeArgs (for native protocol)
- const question: string | undefined = block.params.question ?? block.nativeArgs?.question
- // For now we don't stream multiple questions, only the main one if present
- // We could improve this to stream multiple questions but it requires UI changes to handle partial arrays
+ // Get first question from questions array for streaming display
+ const questions = block.nativeArgs?.questions ?? []
+ const firstQuestion = questions[0]
+ const questionText = typeof firstQuestion === "string" ? firstQuestion : firstQuestion?.text
- // During partial streaming, only show the question to avoid displaying raw JSON
- // The full JSON with suggestions will be sent when the tool call is complete (!block.partial)
+ // During partial streaming, only show the first question to avoid displaying raw JSON
+ // The full JSON with all questions and suggestions will be sent when the tool call is complete
await task
- .ask("followup", this.removeClosingTag("question", question, block.partial), block.partial)
+ .ask("followup", this.removeClosingTag("question", questionText, block.partial), block.partial)
.catch(() => {})
}
}
diff --git a/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts
index 0c3ab5d0dc3..c17489ba301 100644
--- a/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts
+++ b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts
@@ -129,18 +129,79 @@ describe("askFollowupQuestionTool", () => {
)
})
+ it("should handle multiple questions in native protocol", async () => {
+ const block: ToolUse<"ask_followup_question"> = {
+ type: "tool_use",
+ name: "ask_followup_question",
+ params: {},
+ nativeArgs: {
+ questions: ["Question A", "Question B"],
+ follow_up: [{ text: "Okay", mode: "code" }],
+ },
+ partial: false,
+ }
+
+ await askFollowupQuestionTool.handle(mockCline, block, {
+ askApproval: vi.fn(),
+ handleError: vi.fn(),
+ pushToolResult: mockPushToolResult,
+ removeClosingTag: vi.fn((tag, content) => content),
+ toolProtocol: "native",
+ })
+
+ expect(mockCline.ask).toHaveBeenCalledWith(
+ "followup",
+ expect.stringContaining('"questions":["Question A","Question B"]'),
+ false,
+ )
+ })
+
+ it("should handle multiple-choice questions in native protocol", async () => {
+ const block: ToolUse<"ask_followup_question"> = {
+ type: "tool_use",
+ name: "ask_followup_question",
+ params: {},
+ nativeArgs: {
+ questions: [
+ { text: "Framework?", options: ["React", "Vue"] },
+ "Project name?",
+ { text: "Deploy?", options: ["Yes", "No"] },
+ ],
+ follow_up: [{ text: "Done", mode: "code" }],
+ },
+ partial: false,
+ }
+
+ await askFollowupQuestionTool.handle(mockCline, block, {
+ askApproval: vi.fn(),
+ handleError: vi.fn(),
+ pushToolResult: mockPushToolResult,
+ removeClosingTag: vi.fn((tag, content) => content),
+ toolProtocol: "native",
+ })
+
+ expect(mockCline.ask).toHaveBeenCalledWith(
+ "followup",
+ expect.stringContaining(
+ '"questions":[{"text":"Framework?","options":["React","Vue"]},"Project name?",{"text":"Deploy?","options":["Yes","No"]}]',
+ ),
+ false,
+ )
+ })
+
describe("handlePartial with native protocol", () => {
- it("should only send question during partial streaming to avoid raw JSON display", async () => {
+ it("should only send first question during partial streaming to avoid raw JSON display", async () => {
const block: ToolUse<"ask_followup_question"> = {
type: "tool_use",
name: "ask_followup_question",
- params: {
- question: "What would you like to do?",
- },
+ params: {},
partial: true,
nativeArgs: {
- question: "What would you like to do?",
- follow_up: [{ text: "Option 1", mode: "code" }, { text: "Option 2" }],
+ questions: ["What would you like to do?"],
+ follow_up: [
+ { text: "Option 1", mode: "code" },
+ { text: "Option 2", mode: "architect" },
+ ],
},
}
@@ -152,18 +213,20 @@ describe("askFollowupQuestionTool", () => {
toolProtocol: "native",
})
- // During partial streaming, only the question should be sent (not JSON with suggestions)
+ // During partial streaming, only the first question should be sent (not JSON with suggestions)
expect(mockCline.ask).toHaveBeenCalledWith("followup", "What would you like to do?", true)
})
- it("should handle partial with question from params", async () => {
+ it("should handle partial with multiple questions", async () => {
const block: ToolUse<"ask_followup_question"> = {
type: "tool_use",
name: "ask_followup_question",
- params: {
- question: "Choose wisely",
- },
+ params: {},
partial: true,
+ nativeArgs: {
+ questions: ["Question 1", "Question 2"],
+ follow_up: [],
+ },
}
await askFollowupQuestionTool.handle(mockCline, block, {
@@ -171,10 +234,11 @@ describe("askFollowupQuestionTool", () => {
handleError: vi.fn(),
pushToolResult: mockPushToolResult,
removeClosingTag: vi.fn((tag, content) => content || ""),
- toolProtocol: "xml",
+ toolProtocol: "native",
})
- expect(mockCline.ask).toHaveBeenCalledWith("followup", "Choose wisely", true)
+ // Should show first question during streaming
+ expect(mockCline.ask).toHaveBeenCalledWith("followup", "Question 1", true)
})
})
@@ -184,34 +248,33 @@ describe("askFollowupQuestionTool", () => {
NativeToolCallParser.clearRawChunkState()
})
- it("should build nativeArgs with question and follow_up during streaming", () => {
+ it("should build nativeArgs with questions and follow_up during streaming", () => {
// Start a streaming tool call
NativeToolCallParser.startStreamingToolCall("call_123", "ask_followup_question")
// Simulate streaming JSON chunks
- const chunk1 = '{"question":"What would you like?","follow_up":[{"text":"Option 1","mode":"code"}'
+ const chunk1 = '{"questions":["What would you like?"],"follow_up":[{"text":"Option 1","mode":"code"}'
const result1 = NativeToolCallParser.processStreamingChunk("call_123", chunk1)
expect(result1).not.toBeNull()
expect(result1?.name).toBe("ask_followup_question")
- expect(result1?.params.question).toBe("What would you like?")
expect(result1?.nativeArgs).toBeDefined()
// Use type assertion to access the specific fields
const nativeArgs = result1?.nativeArgs as {
- question: string
+ questions: string[]
follow_up?: Array<{ text: string; mode?: string }>
}
- expect(nativeArgs?.question).toBe("What would you like?")
+ expect(nativeArgs?.questions).toEqual(["What would you like?"])
// partial-json should parse the incomplete array
expect(nativeArgs?.follow_up).toBeDefined()
})
- it("should finalize with complete nativeArgs", () => {
+ it("should finalize with complete nativeArgs including complex questions", () => {
NativeToolCallParser.startStreamingToolCall("call_456", "ask_followup_question")
// Add complete JSON
const completeJson =
- '{"question":"Choose an option","follow_up":[{"text":"Yes","mode":"code"},{"text":"No","mode":null}]}'
+ '{"questions":[{"text":"Framework?","options":["React","Vue"]},"Name?"],"follow_up":[{"text":"Yes","mode":"code"},{"text":"No","mode":"architect"}]}'
NativeToolCallParser.processStreamingChunk("call_456", completeJson)
const result = NativeToolCallParser.finalizeStreamingToolCall("call_456")
@@ -223,10 +286,10 @@ describe("askFollowupQuestionTool", () => {
// Type guard: regular tools have type 'tool_use', MCP tools have type 'mcp_tool_use'
if (result?.type === "tool_use") {
expect(result.nativeArgs).toEqual({
- question: "Choose an option",
+ questions: [{ text: "Framework?", options: ["React", "Vue"] }, "Name?"],
follow_up: [
{ text: "Yes", mode: "code" },
- { text: "No", mode: null },
+ { text: "No", mode: "architect" },
],
})
}
diff --git a/src/shared/tools.ts b/src/shared/tools.ts
index 602ebd785e6..1f51af73e2d 100644
--- a/src/shared/tools.ts
+++ b/src/shared/tools.ts
@@ -98,7 +98,7 @@ export type NativeToolArgs = {
edit_file: { file_path: string; old_string: string; new_string: string; expected_replacements?: number }
apply_patch: { patch: string }
ask_followup_question: {
- question: string
+ questions: Array
follow_up: Array<{ text: string; mode?: string }>
}
browser_action: BrowserActionParams
diff --git a/webview-ui/src/components/chat/MultiQuestionHandler.tsx b/webview-ui/src/components/chat/MultiQuestionHandler.tsx
index 38eb4fbf706..7f1c21a945d 100644
--- a/webview-ui/src/components/chat/MultiQuestionHandler.tsx
+++ b/webview-ui/src/components/chat/MultiQuestionHandler.tsx
@@ -1,82 +1,181 @@
-import React, { useState, useEffect } from "react"
-import { Button, Textarea } from "@/components/ui"
+import { useState, useEffect } from "react"
+import { Button, AutosizeTextarea } from "@/components/ui"
import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { useExtensionState } from "@src/context/ExtensionStateContext"
+
+interface Question {
+ text: string
+ options?: string[]
+}
interface MultiQuestionHandlerProps {
- questions: string[]
+ questions: Array
onSendResponse: (response: string) => void
}
+interface QuestionItemProps {
+ question: string | Question
+ title: string
+ textValue: string
+ selectedOption?: string
+ onTextChange: (value: string) => void
+ onOptionClick: (option: string) => void
+}
+
+const QuestionItem = ({
+ question,
+ title,
+ textValue,
+ selectedOption,
+ onTextChange,
+ onOptionClick,
+}: QuestionItemProps) => {
+ const { t } = useAppTranslation()
+ const qText = typeof question === "string" ? question : question.text
+ const options = typeof question === "string" ? undefined : question.options
+
+ return (
+
+
{title}
+
{qText}
+ {options && options.length > 0 && (
+
+ {options.map((option, idx) => (
+
+ ))}
+
+ )}
+
onTextChange(e.target.value)}
+ minHeight={21}
+ maxHeight={200}
+ placeholder={t("chat:questions.typeAnswer")}
+ className="w-full py-2 pl-3 pr-3 rounded border border-transparent"
+ />
+
+ )
+}
+
export const MultiQuestionHandler = ({ questions, onSendResponse }: MultiQuestionHandlerProps) => {
const { t } = useAppTranslation()
+ const { showQuestionsOneByOne } = useExtensionState()
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
- const [answers, setAnswers] = useState(new Array(questions.length).fill(""))
- const [inputValue, setInputValue] = useState("")
+ const [selectedOptions, setSelectedOptions] = useState<(string | undefined)[]>(
+ new Array(questions.length).fill(undefined),
+ )
+ const [textAnswers, setTextAnswers] = useState(new Array(questions.length).fill(""))
+ const [oneByOneInputValue, setOneByOneInputValue] = useState("")
useEffect(() => {
- setInputValue(answers[currentQuestionIndex] || "")
- }, [currentQuestionIndex, answers])
+ if (showQuestionsOneByOne) {
+ setOneByOneInputValue(textAnswers[currentQuestionIndex] || "")
+ }
+ }, [currentQuestionIndex, textAnswers, showQuestionsOneByOne])
- const handleNext = () => {
- const newAnswers = [...answers]
- newAnswers[currentQuestionIndex] = inputValue
- setAnswers(newAnswers)
+ const updateTextAnswer = (index: number, value: string) => {
+ const next = [...textAnswers]
+ next[index] = value
+ setTextAnswers(next)
+ }
- if (currentQuestionIndex < questions.length - 1) {
- setCurrentQuestionIndex(currentQuestionIndex + 1)
- }
+ const handleNext = () => {
+ updateTextAnswer(currentQuestionIndex, oneByOneInputValue)
+ if (currentQuestionIndex < questions.length - 1) setCurrentQuestionIndex(currentQuestionIndex + 1)
}
const handlePrevious = () => {
- const newAnswers = [...answers]
- newAnswers[currentQuestionIndex] = inputValue
- setAnswers(newAnswers)
+ updateTextAnswer(currentQuestionIndex, oneByOneInputValue)
+ if (currentQuestionIndex > 0) setCurrentQuestionIndex(currentQuestionIndex - 1)
+ }
- if (currentQuestionIndex > 0) {
- setCurrentQuestionIndex(currentQuestionIndex - 1)
- }
+ const handleOptionClick = (index: number, option: string) => {
+ setSelectedOptions((prev) => {
+ const next = [...prev]
+ next[index] = next[index] === option ? undefined : option
+ return next
+ })
}
const handleFinish = () => {
- const newAnswers = [...answers]
- newAnswers[currentQuestionIndex] = inputValue
- setAnswers(newAnswers)
+ let finalAnswers = textAnswers
+ if (showQuestionsOneByOne) {
+ finalAnswers = [...textAnswers]
+ finalAnswers[currentQuestionIndex] = oneByOneInputValue
+ }
- const combined = questions.map((q, i) => `Question: ${q}\nAnswer: ${newAnswers[i] || "(skipped)"}`).join("\n\n")
+ const combined = questions
+ .map((q, i) => {
+ const qText = typeof q === "string" ? q : q.text
+ const text = finalAnswers[i].trim()
+ const option = selectedOptions[i]
+ const answer = option && text ? `${option}: ${text}` : option || text || "(skipped)"
+ return `Question: ${qText}\nAnswer: ${answer}`
+ })
+ .join("\n\n")
onSendResponse(combined)
}
- return (
-
-
- {t("chat:questions.questionNumberOfTotal", {
- current: currentQuestionIndex + 1,
- total: questions.length,
- })}
-
-
{questions[currentQuestionIndex]}
-
diff --git a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx
index 2a21a410b38..f8f3eed5c65 100644
--- a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx
+++ b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx
@@ -6,6 +6,7 @@ describe("UISettings", () => {
const defaultProps = {
reasoningBlockCollapsed: false,
enterBehavior: "send" as const,
+ showQuestionsOneByOne: false,
setCachedStateField: vi.fn(),
}
diff --git a/webview-ui/src/components/ui/autosize-textarea.tsx b/webview-ui/src/components/ui/autosize-textarea.tsx
index d23f7a43600..26c660f6e56 100644
--- a/webview-ui/src/components/ui/autosize-textarea.tsx
+++ b/webview-ui/src/components/ui/autosize-textarea.tsx
@@ -91,7 +91,7 @@ export const AutosizeTextarea = React.forwardRef void
includeCurrentCost?: boolean
setIncludeCurrentCost: (value: boolean) => void
+ showQuestionsOneByOne?: boolean
+ setShowQuestionsOneByOne: (value: boolean) => void
}
export const ExtensionStateContext = createContext(undefined)
@@ -279,6 +281,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
openRouterImageGenerationSelectedModel: "",
includeCurrentTime: true,
includeCurrentCost: true,
+ showQuestionsOneByOne: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
@@ -301,6 +304,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [prevCloudIsAuthenticated, setPrevCloudIsAuthenticated] = useState(false)
const [includeCurrentTime, setIncludeCurrentTime] = useState(true)
const [includeCurrentCost, setIncludeCurrentCost] = useState(true)
+ const [showQuestionsOneByOne, setShowQuestionsOneByOne] = useState(false)
const setListApiConfigMeta = useCallback(
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@@ -346,6 +350,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
if ((newState as any).includeCurrentCost !== undefined) {
setIncludeCurrentCost((newState as any).includeCurrentCost)
}
+ // Update showQuestionsOneByOne if present in state message
+ if ((newState as any).showQuestionsOneByOne !== undefined) {
+ setShowQuestionsOneByOne((newState as any).showQuestionsOneByOne)
+ }
// Handle marketplace data if present in state message
if (newState.marketplaceItems !== undefined) {
setMarketplaceItems(newState.marketplaceItems)
@@ -596,6 +604,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setIncludeCurrentTime,
includeCurrentCost,
setIncludeCurrentCost,
+ showQuestionsOneByOne,
+ setShowQuestionsOneByOne,
}
return {children}
diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json
index e288bfa5a9a..5dd7a052c30 100644
--- a/webview-ui/src/i18n/locales/en/chat.json
+++ b/webview-ui/src/i18n/locales/en/chat.json
@@ -288,6 +288,7 @@
},
"questions": {
"hasQuestion": "Roo has a question",
+ "questionNumber": "Question {{number}}",
"questionNumberOfTotal": "Question {{current}} of {{total}}",
"typeAnswer": "Type your answer here...",
"previous": "Previous",
From 5393c2b43e914be7c37ea807aa2a6b042590e9d3 Mon Sep 17 00:00:00 2001
From: ScDor <18174994+ScDor@users.noreply.github.com>
Date: Thu, 5 Feb 2026 13:49:13 +0200
Subject: [PATCH 4/5] fix: resolve automated review issues for
ask_followup_question
---
packages/types/src/followup.ts | 21 +++++++++++++++++--
src/shared/tools.ts | 5 +++--
.../components/chat/MultiQuestionHandler.tsx | 10 +++------
webview-ui/src/i18n/locales/en/settings.json | 4 ++++
4 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/packages/types/src/followup.ts b/packages/types/src/followup.ts
index dc21fc26dd6..4d7047665f2 100644
--- a/packages/types/src/followup.ts
+++ b/packages/types/src/followup.ts
@@ -9,7 +9,7 @@ export interface FollowUpData {
/** The question being asked by the LLM */
question?: string
/** Array of questions being asked by the LLM */
- questions?: string[]
+ questions?: FollowUpQuestion[]
/** Array of suggested answers that the user can select */
suggest?: Array
}
@@ -24,6 +24,12 @@ export interface SuggestionItem {
mode?: string
}
+/**
+ * Type definition for a follow-up question
+ * Can be a simple string or an object with text and options
+ */
+export type FollowUpQuestion = string | { text: string; options?: string[] }
+
/**
* Zod schema for SuggestionItem
*/
@@ -32,12 +38,23 @@ export const suggestionItemSchema = z.object({
mode: z.string().optional(),
})
+/**
+ * Zod schema for FollowUpQuestion
+ */
+export const followUpQuestionSchema = z.union([
+ z.string(),
+ z.object({
+ text: z.string(),
+ options: z.array(z.string()).optional(),
+ }),
+])
+
/**
* Zod schema for FollowUpData
*/
export const followUpDataSchema = z.object({
question: z.string().optional(),
- questions: z.array(z.string()).optional(),
+ questions: z.array(followUpQuestionSchema).optional(),
suggest: z.array(suggestionItemSchema).optional(),
})
diff --git a/src/shared/tools.ts b/src/shared/tools.ts
index 8d8064d9d1c..e9c94e72bf4 100644
--- a/src/shared/tools.ts
+++ b/src/shared/tools.ts
@@ -7,6 +7,7 @@ import type {
ToolName,
BrowserActionParams,
GenerateImageParams,
+ FollowUpQuestion,
} from "@roo-code/types"
export type ToolResponse = string | Array
@@ -110,7 +111,7 @@ export type NativeToolArgs = {
list_files: { path: string; recursive?: boolean }
new_task: { mode: string; message: string; todos?: string }
ask_followup_question: {
- questions: Array
+ questions: FollowUpQuestion[]
follow_up: Array<{ text: string; mode?: string }>
}
browser_action: BrowserActionParams
@@ -237,7 +238,7 @@ export interface AccessMcpResourceToolUse extends ToolUse<"access_mcp_resource">
export interface AskFollowupQuestionToolUse extends ToolUse<"ask_followup_question"> {
name: "ask_followup_question"
- params: Partial, "question" | "follow_up">>
+ params: Partial, "question" | "follow_up" | "questions">>
}
export interface AttemptCompletionToolUse extends ToolUse<"attempt_completion"> {
diff --git a/webview-ui/src/components/chat/MultiQuestionHandler.tsx b/webview-ui/src/components/chat/MultiQuestionHandler.tsx
index 7f1c21a945d..dea9da65d0c 100644
--- a/webview-ui/src/components/chat/MultiQuestionHandler.tsx
+++ b/webview-ui/src/components/chat/MultiQuestionHandler.tsx
@@ -2,19 +2,15 @@ import { useState, useEffect } from "react"
import { Button, AutosizeTextarea } from "@/components/ui"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useExtensionState } from "@src/context/ExtensionStateContext"
-
-interface Question {
- text: string
- options?: string[]
-}
+import { FollowUpQuestion } from "@roo-code/types"
interface MultiQuestionHandlerProps {
- questions: Array
+ questions: FollowUpQuestion[]
onSendResponse: (response: string) => void
}
interface QuestionItemProps {
- question: string | Question
+ question: FollowUpQuestion
title: string
textValue: string
selectedOption?: string
diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json
index de76ba4c679..f6cf71670ff 100644
--- a/webview-ui/src/i18n/locales/en/settings.json
+++ b/webview-ui/src/i18n/locales/en/settings.json
@@ -126,6 +126,10 @@
"requireCtrlEnterToSend": {
"label": "Require {{primaryMod}}+Enter to send messages",
"description": "When enabled, you must press {{primaryMod}}+Enter to send messages instead of just Enter"
+ },
+ "showQuestionsOneByOne": {
+ "label": "Show questions one by one",
+ "description": "When enabled, questions from the model will be displayed one at a time."
}
},
"prompts": {
From 08d7bfa6f660dc504d73c9347bc312193803c667 Mon Sep 17 00:00:00 2001
From: ScDor <18174994+ScDor@users.noreply.github.com>
Date: Thu, 5 Feb 2026 13:56:41 +0200
Subject: [PATCH 5/5] fix: revert unwanted changes in zh-TW README
---
locales/zh-TW/README.md | 32 ++++++++++++++++----------------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md
index 76415a77975..b872f1316ad 100644
--- a/locales/zh-TW/README.md
+++ b/locales/zh-TW/README.md
@@ -6,15 +6,15 @@
- 快速取得協助 → 加入 Discord • 偏好非同步溝通?→ 加入 r/RooCode
+ 快速取得協助 → 加入 Discord • 偏好非同步?→ 加入 r/RooCode
# Roo Code
-> 您的 AI 驅動開發團隊,就在您的編輯器中
+> 你的 AI 驅動開發團隊,就在你的編輯器裡
- 🌐 支援語言
+ 🌐 可用語言
- [English](../../README.md)
- [Català](../ca/README.md)
@@ -35,7 +35,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
-
+
---
@@ -51,14 +51,14 @@
## 模式
-Roo Code 會配合您的工作方式,而非要您配合它:
+Roo Code 適應您的工作方式,而不是相反:
-- 程式碼模式:日常開發、編輯和檔案操作
+- 程式碼模式:日常編碼、編輯和檔案操作
- 架構師模式:規劃系統、規格和遷移
- 詢問模式:快速回答、解釋和文件
-- 偵錯模式:追蹤問題、新增日誌、鎖定根本原因
+- 偵錯模式:追蹤問題、新增日誌、隔離根本原因
- 自訂模式:為您的團隊或工作流程建置專門的模式
-- Roomote Control:Roomote Control 讓您能遠端控制在本機 VS Code 執行個體中運行的工作。
+- Roomote Control:Roomote Control 讓你能遠端控制在本機 VS Code 執行個體中運行的工作。
更多資訊:[使用模式](https://docs.roocode.com/basic-usage/using-modes) • [自訂模式](https://docs.roocode.com/advanced-usage/custom-modes) • [Roomote Control](https://docs.roocode.com/roo-code-cloud/roomote-control)
@@ -82,12 +82,12 @@ Roo Code 會配合您的工作方式,而非要您配合它:
- **[YouTube 頻道](https://youtube.com/@roocodeyt?feature=shared):** 觀看教學和功能實際操作。
- **[Discord 伺服器](https://discord.gg/roocode):** 加入社群以獲得即時協助和討論。
- **[Reddit 社群](https://www.reddit.com/r/RooCode):** 分享您的經驗,看看其他人正在建立什麼。
-- **[GitHub Issues](https://github.com/RooCodeInc/Roo-Code/issues):** 回報問題並追蹤開發進度。
+- **[GitHub 問題](https://github.com/RooCodeInc/Roo-Code/issues):** 回報錯誤並追蹤開發進度。
- **[功能請求](https://github.com/RooCodeInc/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop):** 有想法嗎?與開發人員分享。
---
-## 本機設定與開發
+## 本地設定與開發
1. **複製**儲存庫:
@@ -95,7 +95,7 @@ Roo Code 會配合您的工作方式,而非要您配合它:
git clone https://github.com/RooCodeInc/Roo-Code.git
```
-2. **安裝相依套件**:
+2. **安裝依賴套件**:
```sh
pnpm install
@@ -107,7 +107,7 @@ pnpm install
### 開發模式(F5)
-若要進行開發,請使用 VSCode 的內建偵錯功能:
+對於積極的開發,請使用 VSCode 的內建偵錯功能:
在 VSCode 中按 `F5`(或前往 **執行** → **開始偵錯**)。這將在執行 Roo Code 擴充功能的新 VSCode 視窗中開啟。
@@ -127,7 +127,7 @@ pnpm install:vsix [-y] [--editor=]
- 詢問要使用的編輯器命令(code/cursor/code-insiders) - 預設為“code”
- 解除安裝任何現有版本的擴充功能。
- 建置最新的 VSIX 套件。
-- 安裝新建置的 VSIX。
+- 安裝新建立的 VSIX。
- 提示您重新啟動 VS Code 以使變更生效。
選項:
@@ -144,7 +144,7 @@ pnpm install:vsix [-y] [--editor=]
pnpm vsix
```
2. 將在 `bin/` 目錄中產生一個 `.vsix` 檔案(例如 `bin/roo-cline-.vsix`)。
-3. 使用 VSCode CLI 手動安裝:
+3. 使用 VSCode CLI 手動安裝
```sh
code --install-extension bin/roo-cline-.vsix
```
@@ -163,7 +163,7 @@ pnpm install:vsix [-y] [--editor=]
## 貢獻
-我們歡迎社群貢獻!請從閱讀我們的 [CONTRIBUTING.md](CONTRIBUTING.md) 開始。
+我們歡迎社群貢獻!請閱讀我們的 [CONTRIBUTING.md](CONTRIBUTING.md) 開始。
---
@@ -173,4 +173,4 @@ pnpm install:vsix [-y] [--editor=]
---
-**享受 Roo Code!** 無論您是想嚴格控管它,還是讓它自主運作,我們都迫不及待地想看看您會打造些什麼。如果您有問題或功能想法,請造訪我們的 [Reddit 社群](https://www.reddit.com/r/RooCode/)或 [Discord](https://discord.gg/roocode)。祝您開發愉快!
+**享受 Roo Code!** 無論您是將它拴在短繩上還是讓它自主漫遊,我們迫不及待地想看看您會建構什麼。如果您有問題或功能想法,請造訪我們的 [Reddit 社群](https://www.reddit.com/r/RooCode/)或 [Discord](https://discord.gg/roocode)。祝您開發愉快!