mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: enhance ask_followup_question to support multiple questions
This commit is contained in:
parent
9bf7173725
commit
b7ea247f45
8 changed files with 178 additions and 17 deletions
|
|
@ -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<SuggestionItem>
|
||||
}
|
||||
|
|
@ -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(),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <suggest> 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 <question> tags inside.
|
||||
- follow_up: (optional) A list of suggested answers, each in its own <suggest> tag.
|
||||
|
||||
Usage:
|
||||
<ask_followup_question>
|
||||
|
|
@ -15,6 +16,14 @@ Usage:
|
|||
</follow_up>
|
||||
</ask_followup_question>
|
||||
|
||||
Usage with multiple questions:
|
||||
<ask_followup_question>
|
||||
<questions>
|
||||
<question>Question 1?</question>
|
||||
<question>Question 2?</question>
|
||||
</questions>
|
||||
</ask_followup_question>
|
||||
|
||||
Example:
|
||||
<ask_followup_question>
|
||||
<question>What is the path to the frontend-config.json file?</question>
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string>>): 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<void> {
|
||||
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<void> {
|
||||
// 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)
|
||||
|
|
|
|||
|
|
@ -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>Question 1</question><question>Question 2</question>",
|
||||
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"> = {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2 ml-6">
|
||||
<Markdown
|
||||
markdown={message.partial === true ? message?.text : followUpData?.question}
|
||||
/>
|
||||
<FollowUpSuggest
|
||||
suggestions={followUpData?.suggest}
|
||||
onSuggestionClick={onSuggestionClick}
|
||||
ts={message?.ts}
|
||||
onCancelAutoApproval={onFollowUpUnmount}
|
||||
isAnswered={isFollowUpAnswered}
|
||||
isFollowUpAutoApprovalPaused={isFollowUpAutoApprovalPaused}
|
||||
/>
|
||||
{followUpData?.questions && followUpData.questions.length > 0 ? (
|
||||
<MultiQuestionHandler
|
||||
questions={followUpData.questions}
|
||||
onSendResponse={(response) => {
|
||||
onSuggestionClick?.({ answer: response })
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Markdown
|
||||
markdown={message.partial === true ? message?.text : followUpData?.question}
|
||||
/>
|
||||
<FollowUpSuggest
|
||||
suggestions={followUpData?.suggest}
|
||||
onSuggestionClick={onSuggestionClick}
|
||||
ts={message?.ts}
|
||||
onCancelAutoApproval={onFollowUpUnmount}
|
||||
isAnswered={isFollowUpAnswered}
|
||||
isFollowUpAutoApprovalPaused={isFollowUpAutoApprovalPaused}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
76
webview-ui/src/components/chat/MultiQuestionHandler.tsx
Normal file
76
webview-ui/src/components/chat/MultiQuestionHandler.tsx
Normal file
|
|
@ -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<string[]>(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 (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="font-bold">
|
||||
{t("chat:questions.questionNumberOfTotal", {
|
||||
current: currentQuestionIndex + 1,
|
||||
total: questions.length,
|
||||
})}
|
||||
</div>
|
||||
<div>{questions[currentQuestionIndex]}</div>
|
||||
<Textarea
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
rows={3}
|
||||
placeholder={t("chat:questions.typeAnswer")}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{currentQuestionIndex > 0 && (
|
||||
<Button variant="secondary" onClick={handlePrevious}>
|
||||
{t("chat:questions.previous")}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="primary" onClick={handleNext}>
|
||||
{currentQuestionIndex < questions.length - 1
|
||||
? t("chat:questions.next")
|
||||
: t("chat:questions.finish")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue