Skip to content

Commit 15b4579

Browse files
committed
Fix ask_followup_question streaming issue and add missing tool cases
- Add ask_followup_question case to NativeToolCallParser.createPartialToolUse() - Add 11 other missing tool cases (apply_diff, browser_action, codebase_search, etc.) - Simplify AskFollowupQuestionTool.handlePartial() to only show question during streaming - Add test coverage for native protocol streaming behavior Fixes issue where ask_followup_question was not showing during streaming after PR #9542 introduced tool_call_partial chunks. The tool was missing from the createPartialToolUse() switch statement, preventing nativeArgs from being populated during streaming.
1 parent 0327f12 commit 15b4579

File tree

3 files changed

+217
-1
lines changed

3 files changed

+217
-1
lines changed

src/core/assistant-message/NativeToolCallParser.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,117 @@ export class NativeToolCallParser {
355355
}
356356
break
357357

358+
case "ask_followup_question":
359+
if (partialArgs.question !== undefined || partialArgs.follow_up !== undefined) {
360+
nativeArgs = {
361+
question: partialArgs.question,
362+
follow_up: Array.isArray(partialArgs.follow_up) ? partialArgs.follow_up : undefined,
363+
}
364+
}
365+
break
366+
367+
case "apply_diff":
368+
if (partialArgs.path !== undefined || partialArgs.diff !== undefined) {
369+
nativeArgs = {
370+
path: partialArgs.path,
371+
diff: partialArgs.diff,
372+
}
373+
}
374+
break
375+
376+
case "browser_action":
377+
if (partialArgs.action !== undefined) {
378+
nativeArgs = {
379+
action: partialArgs.action,
380+
url: partialArgs.url,
381+
coordinate: partialArgs.coordinate,
382+
size: partialArgs.size,
383+
text: partialArgs.text,
384+
}
385+
}
386+
break
387+
388+
case "codebase_search":
389+
if (partialArgs.query !== undefined) {
390+
nativeArgs = {
391+
query: partialArgs.query,
392+
path: partialArgs.path,
393+
}
394+
}
395+
break
396+
397+
case "fetch_instructions":
398+
if (partialArgs.task !== undefined) {
399+
nativeArgs = {
400+
task: partialArgs.task,
401+
}
402+
}
403+
break
404+
405+
case "generate_image":
406+
if (partialArgs.prompt !== undefined || partialArgs.path !== undefined) {
407+
nativeArgs = {
408+
prompt: partialArgs.prompt,
409+
path: partialArgs.path,
410+
image: partialArgs.image,
411+
}
412+
}
413+
break
414+
415+
case "list_code_definition_names":
416+
if (partialArgs.path !== undefined) {
417+
nativeArgs = {
418+
path: partialArgs.path,
419+
}
420+
}
421+
break
422+
423+
case "run_slash_command":
424+
if (partialArgs.command !== undefined) {
425+
nativeArgs = {
426+
command: partialArgs.command,
427+
args: partialArgs.args,
428+
}
429+
}
430+
break
431+
432+
case "search_files":
433+
if (partialArgs.path !== undefined || partialArgs.regex !== undefined) {
434+
nativeArgs = {
435+
path: partialArgs.path,
436+
regex: partialArgs.regex,
437+
file_pattern: partialArgs.file_pattern,
438+
}
439+
}
440+
break
441+
442+
case "switch_mode":
443+
if (partialArgs.mode_slug !== undefined || partialArgs.reason !== undefined) {
444+
nativeArgs = {
445+
mode_slug: partialArgs.mode_slug,
446+
reason: partialArgs.reason,
447+
}
448+
}
449+
break
450+
451+
case "update_todo_list":
452+
if (partialArgs.todos !== undefined) {
453+
nativeArgs = {
454+
todos: partialArgs.todos,
455+
}
456+
}
457+
break
458+
459+
case "use_mcp_tool":
460+
if (partialArgs.server_name !== undefined || partialArgs.tool_name !== undefined) {
461+
nativeArgs = {
462+
server_name: partialArgs.server_name,
463+
tool_name: partialArgs.tool_name,
464+
arguments: partialArgs.arguments,
465+
}
466+
}
467+
break
468+
358469
// Add other tools as needed
359470
default:
360471
break

src/core/tools/AskFollowupQuestionTool.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
9191
}
9292

9393
override async handlePartial(task: Task, block: ToolUse<"ask_followup_question">): Promise<void> {
94-
const question: string | undefined = block.params.question
94+
// Get question from params (for XML protocol) or nativeArgs (for native protocol)
95+
const question: string | undefined = block.params.question ?? block.nativeArgs?.question
96+
97+
// During partial streaming, only show the question to avoid displaying raw JSON
98+
// The full JSON with suggestions will be sent when the tool call is complete (!block.partial)
9599
await task
96100
.ask("followup", this.removeClosingTag("question", question, block.partial), block.partial)
97101
.catch(() => {})

src/core/tools/__tests__/askFollowupQuestionTool.spec.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { askFollowupQuestionTool } from "../AskFollowupQuestionTool"
22
import { ToolUse } from "../../../shared/tools"
3+
import { NativeToolCallParser } from "../../assistant-message/NativeToolCallParser"
34

45
describe("askFollowupQuestionTool", () => {
56
let mockCline: any
@@ -101,4 +102,104 @@ describe("askFollowupQuestionTool", () => {
101102
false,
102103
)
103104
})
105+
106+
describe("handlePartial with native protocol", () => {
107+
it("should only send question during partial streaming to avoid raw JSON display", async () => {
108+
const block: ToolUse<"ask_followup_question"> = {
109+
type: "tool_use",
110+
name: "ask_followup_question",
111+
params: {
112+
question: "What would you like to do?",
113+
},
114+
partial: true,
115+
nativeArgs: {
116+
question: "What would you like to do?",
117+
follow_up: [{ text: "Option 1", mode: "code" }, { text: "Option 2" }],
118+
},
119+
}
120+
121+
await askFollowupQuestionTool.handle(mockCline, block, {
122+
askApproval: vi.fn(),
123+
handleError: vi.fn(),
124+
pushToolResult: mockPushToolResult,
125+
removeClosingTag: vi.fn((tag, content) => content || ""),
126+
toolProtocol: "native",
127+
})
128+
129+
// During partial streaming, only the question should be sent (not JSON with suggestions)
130+
expect(mockCline.ask).toHaveBeenCalledWith("followup", "What would you like to do?", true)
131+
})
132+
133+
it("should handle partial with question from params", async () => {
134+
const block: ToolUse<"ask_followup_question"> = {
135+
type: "tool_use",
136+
name: "ask_followup_question",
137+
params: {
138+
question: "Choose wisely",
139+
},
140+
partial: true,
141+
}
142+
143+
await askFollowupQuestionTool.handle(mockCline, block, {
144+
askApproval: vi.fn(),
145+
handleError: vi.fn(),
146+
pushToolResult: mockPushToolResult,
147+
removeClosingTag: vi.fn((tag, content) => content || ""),
148+
toolProtocol: "xml",
149+
})
150+
151+
expect(mockCline.ask).toHaveBeenCalledWith("followup", "Choose wisely", true)
152+
})
153+
})
154+
155+
describe("NativeToolCallParser.createPartialToolUse for ask_followup_question", () => {
156+
beforeEach(() => {
157+
NativeToolCallParser.clearAllStreamingToolCalls()
158+
NativeToolCallParser.clearRawChunkState()
159+
})
160+
161+
it("should build nativeArgs with question and follow_up during streaming", () => {
162+
// Start a streaming tool call
163+
NativeToolCallParser.startStreamingToolCall("call_123", "ask_followup_question")
164+
165+
// Simulate streaming JSON chunks
166+
const chunk1 = '{"question":"What would you like?","follow_up":[{"text":"Option 1","mode":"code"}'
167+
const result1 = NativeToolCallParser.processStreamingChunk("call_123", chunk1)
168+
169+
expect(result1).not.toBeNull()
170+
expect(result1?.name).toBe("ask_followup_question")
171+
expect(result1?.params.question).toBe("What would you like?")
172+
expect(result1?.nativeArgs).toBeDefined()
173+
// Use type assertion to access the specific fields
174+
const nativeArgs = result1?.nativeArgs as {
175+
question: string
176+
follow_up?: Array<{ text: string; mode?: string }>
177+
}
178+
expect(nativeArgs?.question).toBe("What would you like?")
179+
// partial-json should parse the incomplete array
180+
expect(nativeArgs?.follow_up).toBeDefined()
181+
})
182+
183+
it("should finalize with complete nativeArgs", () => {
184+
NativeToolCallParser.startStreamingToolCall("call_456", "ask_followup_question")
185+
186+
// Add complete JSON
187+
const completeJson =
188+
'{"question":"Choose an option","follow_up":[{"text":"Yes","mode":"code"},{"text":"No","mode":null}]}'
189+
NativeToolCallParser.processStreamingChunk("call_456", completeJson)
190+
191+
const result = NativeToolCallParser.finalizeStreamingToolCall("call_456")
192+
193+
expect(result).not.toBeNull()
194+
expect(result?.name).toBe("ask_followup_question")
195+
expect(result?.partial).toBe(false)
196+
expect(result?.nativeArgs).toEqual({
197+
question: "Choose an option",
198+
follow_up: [
199+
{ text: "Yes", mode: "code" },
200+
{ text: "No", mode: null },
201+
],
202+
})
203+
})
204+
})
104205
})

0 commit comments

Comments
 (0)