feat: update ask_followup_question format to use nested XML elements

- Changed from attribute-based format (<suggest mode="architect">) to nested element format (<suggest><mode>architect</mode><content>...</content></suggest>)
- 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
This commit is contained in:
Roo Code 2025-08-09 18:06:32 +00:00 committed by Merge Resolver
parent 613abe08fc
commit 55884030c4
3 changed files with 158 additions and 20 deletions

View file

@ -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 <suggest> 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 <suggest> tag with nested <content> element. Suggestions must be complete, actionable answers without placeholders. Optionally include <mode> element to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
<suggest>
<content>Your suggested answer here</content>
</suggest>
<suggest>
<mode>code</mode>
<content>Implement the solution</content>
</suggest>
</follow_up>
</ask_followup_question>
@ -19,9 +24,33 @@ Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
<suggest>./src/frontend-config.json</suggest>
<suggest>./config/frontend-config.json</suggest>
<suggest>./frontend-config.json</suggest>
<suggest>
<content>./src/frontend-config.json</content>
</suggest>
<suggest>
<content>./config/frontend-config.json</content>
</suggest>
<suggest>
<content>./frontend-config.json</content>
</suggest>
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest>
<mode>code</mode>
<content>Start implementing the solution</content>
</suggest>
<suggest>
<mode>architect</mode>
<content>Plan the architecture first</content>
</suggest>
<suggest>
<content>Continue with more details</content>
</suggest>
</follow_up>
</ask_followup_question>`
}

View file

@ -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:
"<suggest><content>Option 1</content></suggest><suggest><content>Option 2</content></suggest>",
},
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:
"<suggest><mode>code</mode><content>Write code</content></suggest><suggest><mode>debug</mode><content>Debug issue</content></suggest>",
},
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:
"<suggest><content>Regular option</content></suggest><suggest><mode>architect</mode><content>Plan architecture</content></suggest>",
},
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",

View file

@ -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) }
}
})