Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit f389733

Browse files
committed
Fix double tool rendering by moving finalizeRawChunks before partialBlocks capture
The issue was that partialBlocks were captured BEFORE finalizeRawChunks(), causing tools to be presented twice: 1. Once during finalizeRawChunks() -> presentAssistantMessage() 2. Again at line 3476 when presenting the captured partialBlocks By moving finalizeRawChunks() logic earlier and capturing partialBlocks AFTER finalization, we ensure each tool is only presented once. Fixes rendering issue where tools appeared duplicated in the UI.
1 parent f8b0fe6 commit f389733

1 file changed

Lines changed: 55 additions & 52 deletions

File tree

src/core/task/Task.ts

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2983,58 +2983,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
29832983
}
29842984
}
29852985

2986-
// Finalize any remaining streaming tool calls that weren't explicitly ended
2987-
// This is critical for MCP tools which need tool_call_end events to be properly
2988-
// converted from ToolUse to McpToolUse via finalizeStreamingToolCall()
2989-
const finalizeEvents = NativeToolCallParser.finalizeRawChunks()
2990-
for (const event of finalizeEvents) {
2991-
if (event.type === "tool_call_end") {
2992-
// Finalize the streaming tool call
2993-
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
2994-
2995-
// Get the index for this tool call
2996-
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
2997-
2998-
if (finalToolUse) {
2999-
// Store the tool call ID
3000-
;(finalToolUse as any).id = event.id
3001-
3002-
// Get the index and replace partial with final
3003-
if (toolUseIndex !== undefined) {
3004-
this.assistantMessageContent[toolUseIndex] = finalToolUse
3005-
}
3006-
3007-
// Clean up tracking
3008-
this.streamingToolCallIndices.delete(event.id)
3009-
3010-
// Mark that we have new content to process
3011-
this.userMessageContentReady = false
3012-
3013-
// Present the finalized tool call
3014-
presentAssistantMessage(this)
3015-
} else if (toolUseIndex !== undefined) {
3016-
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
3017-
// We still need to mark the tool as non-partial so it gets executed
3018-
// The tool's validation will catch any missing required parameters
3019-
const existingToolUse = this.assistantMessageContent[toolUseIndex]
3020-
if (existingToolUse && existingToolUse.type === "tool_use") {
3021-
existingToolUse.partial = false
3022-
// Ensure it has the ID for native protocol
3023-
;(existingToolUse as any).id = event.id
3024-
}
3025-
3026-
// Clean up tracking
3027-
this.streamingToolCallIndices.delete(event.id)
3028-
3029-
// Mark that we have new content to process
3030-
this.userMessageContentReady = false
3031-
3032-
// Present the tool call - validation will handle missing params
3033-
presentAssistantMessage(this)
3034-
}
3035-
}
3036-
}
3037-
30382986
// Create a copy of current token values to avoid race conditions
30392987
const currentTokens = {
30402988
input: inputTokens,
@@ -3282,6 +3230,61 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
32823230
// the case, `presentAssistantMessage` relies on these blocks either
32833231
// to be completed or the user to reject a block in order to proceed
32843232
// and eventually set userMessageContentReady to true.)
3233+
3234+
// Finalize any remaining streaming tool calls that weren't explicitly ended
3235+
// This is critical for MCP tools which need tool_call_end events to be properly
3236+
// converted from ToolUse to McpToolUse via finalizeStreamingToolCall()
3237+
const finalizeEvents = NativeToolCallParser.finalizeRawChunks()
3238+
for (const event of finalizeEvents) {
3239+
if (event.type === "tool_call_end") {
3240+
// Finalize the streaming tool call
3241+
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
3242+
3243+
// Get the index for this tool call
3244+
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
3245+
3246+
if (finalToolUse) {
3247+
// Store the tool call ID
3248+
;(finalToolUse as any).id = event.id
3249+
3250+
// Get the index and replace partial with final
3251+
if (toolUseIndex !== undefined) {
3252+
this.assistantMessageContent[toolUseIndex] = finalToolUse
3253+
}
3254+
3255+
// Clean up tracking
3256+
this.streamingToolCallIndices.delete(event.id)
3257+
3258+
// Mark that we have new content to process
3259+
this.userMessageContentReady = false
3260+
3261+
// Present the finalized tool call
3262+
presentAssistantMessage(this)
3263+
} else if (toolUseIndex !== undefined) {
3264+
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
3265+
// We still need to mark the tool as non-partial so it gets executed
3266+
// The tool's validation will catch any missing required parameters
3267+
const existingToolUse = this.assistantMessageContent[toolUseIndex]
3268+
if (existingToolUse && existingToolUse.type === "tool_use") {
3269+
existingToolUse.partial = false
3270+
// Ensure it has the ID for native protocol
3271+
;(existingToolUse as any).id = event.id
3272+
}
3273+
3274+
// Clean up tracking
3275+
this.streamingToolCallIndices.delete(event.id)
3276+
3277+
// Mark that we have new content to process
3278+
this.userMessageContentReady = false
3279+
3280+
// Present the tool call - validation will handle missing params
3281+
presentAssistantMessage(this)
3282+
}
3283+
}
3284+
}
3285+
3286+
// IMPORTANT: Capture partialBlocks AFTER finalizeRawChunks() to avoid double-presentation.
3287+
// Tools finalized above are already presented, so we only want blocks still partial after finalization.
32853288
const partialBlocks = this.assistantMessageContent.filter((block) => block.partial)
32863289
partialBlocks.forEach((block) => (block.partial = false))
32873290

0 commit comments

Comments
 (0)