From 55884030c491c86b4229dfaa705f327dfb2c1637 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 9 Aug 2025 18:06:32 +0000 Subject: [PATCH] feat: update ask_followup_question format to use nested XML elements - Changed from attribute-based format () to nested element format (architect...) - Updated documentation in ask-followup-question.ts with new format and examples - Modified parsing logic in askFollowupQuestionTool.ts to handle new format while maintaining backward compatibility - Added comprehensive tests for both new format and backward compatibility - All existing tests pass --- .../prompts/tools/ask-followup-question.ts | 41 ++++++-- .../__tests__/askFollowupQuestionTool.spec.ts | 95 ++++++++++++++++++- src/core/tools/askFollowupQuestionTool.ts | 42 +++++--- 3 files changed, 158 insertions(+), 20 deletions(-) diff --git a/src/core/prompts/tools/ask-followup-question.ts b/src/core/prompts/tools/ask-followup-question.ts index c40684b8bc..14adb42d69 100644 --- a/src/core/prompts/tools/ask-followup-question.ts +++ b/src/core/prompts/tools/ask-followup-question.ts @@ -4,14 +4,19 @@ Description: Ask the user a question to gather additional information needed to 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.) +- follow_up: (required) A list of 2-4 suggested answers, each in its own tag with nested element. Suggestions must be complete, actionable answers without placeholders. Optionally include element to switch modes (code/architect/etc.) Usage: Your question here -First suggestion -Action with mode switch + +Your suggested answer here + + +code +Implement the solution + @@ -19,9 +24,33 @@ Example: What is the path to the frontend-config.json file? -./src/frontend-config.json -./config/frontend-config.json -./frontend-config.json + +./src/frontend-config.json + + +./config/frontend-config.json + + +./frontend-config.json + + + + +Example: Asking a question with mode switching options + +How would you like to proceed with this task? + + +code +Start implementing the solution + + +architect +Plan the architecture first + + +Continue with more details + ` } diff --git a/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts index fbb9ef9eb3..7d0e9d29c0 100644 --- a/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts +++ b/src/core/tools/__tests__/askFollowupQuestionTool.spec.ts @@ -21,7 +21,96 @@ describe("askFollowupQuestionTool", () => { }) }) - it("should parse suggestions without mode attributes", async () => { + it("should parse suggestions without mode (new nested format)", async () => { + const block: ToolUse = { + type: "tool_use", + name: "ask_followup_question", + params: { + question: "What would you like to do?", + follow_up: + "Option 1Option 2", + }, + partial: false, + } + + await askFollowupQuestionTool( + mockCline, + block, + vi.fn(), + vi.fn(), + mockPushToolResult, + vi.fn((tag, content) => content), + ) + + expect(mockCline.ask).toHaveBeenCalledWith( + "followup", + expect.stringContaining('"suggest":[{"answer":"Option 1"},{"answer":"Option 2"}]'), + false, + ) + }) + + it("should parse suggestions with mode (new nested format)", async () => { + const block: ToolUse = { + type: "tool_use", + name: "ask_followup_question", + params: { + question: "What would you like to do?", + follow_up: + "codeWrite codedebugDebug issue", + }, + partial: false, + } + + await askFollowupQuestionTool( + mockCline, + block, + vi.fn(), + vi.fn(), + mockPushToolResult, + vi.fn((tag, content) => content), + ) + + expect(mockCline.ask).toHaveBeenCalledWith( + "followup", + expect.stringContaining( + '"suggest":[{"answer":"Write code","mode":"code"},{"answer":"Debug issue","mode":"debug"}]', + ), + false, + ) + }) + + it("should handle mixed suggestions with and without mode (new nested format)", async () => { + const block: ToolUse = { + type: "tool_use", + name: "ask_followup_question", + params: { + question: "What would you like to do?", + follow_up: + "Regular optionarchitectPlan architecture", + }, + partial: false, + } + + await askFollowupQuestionTool( + mockCline, + block, + vi.fn(), + vi.fn(), + mockPushToolResult, + vi.fn((tag, content) => content), + ) + + expect(mockCline.ask).toHaveBeenCalledWith( + "followup", + expect.stringContaining( + '"suggest":[{"answer":"Regular option"},{"answer":"Plan architecture","mode":"architect"}]', + ), + false, + ) + }) + + // Backward compatibility tests for old format + it("should parse suggestions without mode attributes (old format - backward compatibility)", async () => { const block: ToolUse = { type: "tool_use", name: "ask_followup_question", @@ -48,7 +137,7 @@ describe("askFollowupQuestionTool", () => { ) }) - it("should parse suggestions with mode attributes", async () => { + it("should parse suggestions with mode attributes (old format - backward compatibility)", async () => { const block: ToolUse = { type: "tool_use", name: "ask_followup_question", @@ -77,7 +166,7 @@ describe("askFollowupQuestionTool", () => { ) }) - it("should handle mixed suggestions with and without mode attributes", async () => { + it("should handle mixed suggestions with and without mode attributes (old format - backward compatibility)", async () => { const block: ToolUse = { type: "tool_use", name: "ask_followup_question", diff --git a/src/core/tools/askFollowupQuestionTool.ts b/src/core/tools/askFollowupQuestionTool.ts index e736936887..9ab45c9835 100644 --- a/src/core/tools/askFollowupQuestionTool.ts +++ b/src/core/tools/askFollowupQuestionTool.ts @@ -34,15 +34,18 @@ export async function askFollowupQuestionTool( } if (follow_up) { - // Define the actual structure returned by the XML parser - type ParsedSuggestion = string | { "#text": string; "@_mode"?: string } + // Define the actual structure returned by the XML parser for the new format + type ParsedSuggestion = + | string // For backward compatibility with old format or when stopNodes is used + | { "#text": string; "@_mode"?: string } // For backward compatibility with old format let parsedSuggest: { suggest: ParsedSuggestion[] | ParsedSuggestion } try { - parsedSuggest = parseXml(follow_up, ["suggest"]) as { + // Don't use stopNodes for suggest elements to allow proper parsing of nested structure + parsedSuggest = parseXml(follow_up) as { suggest: ParsedSuggestion[] | ParsedSuggestion } } catch (error) { @@ -58,17 +61,34 @@ export async function askFollowupQuestionTool( : [parsedSuggest?.suggest].filter((sug): sug is ParsedSuggestion => sug !== undefined) // Transform parsed XML to our Suggest format - const normalizedSuggest: Suggest[] = rawSuggestions.map((sug) => { + const normalizedSuggest: Suggest[] = rawSuggestions.map((sug: any) => { if (typeof sug === "string") { - // Simple string suggestion (no mode attribute) + // Simple string suggestion (backward compatibility) return { answer: sug } - } else { - // XML object with text content and optional mode attribute - const result: Suggest = { answer: sug["#text"] } - if (sug["@_mode"]) { - result.mode = sug["@_mode"] + } else if (sug && typeof sug === "object") { + // Check for new nested element format + if ("content" in sug) { + const result: Suggest = { answer: sug.content } + if (sug.mode) { + result.mode = sug.mode + } + return result } - return result + // Old attribute format (backward compatibility) + else if ("#text" in sug) { + const result: Suggest = { answer: sug["#text"] } + if (sug["@_mode"]) { + result.mode = sug["@_mode"] + } + return result + } + // Fallback for any other object structure + else { + return { answer: JSON.stringify(sug) } + } + } else { + // Fallback for any unexpected type + return { answer: String(sug) } } })