From bbea8460b5659d2bd15cfdd38a203ad912c1d3e5 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 25 Mar 2026 12:49:48 -0600 Subject: [PATCH] fix(claude-code-cli): render tool calls above text response - Filter toolcall_start/delta/end events from streaming to prevent out-of-order rendering in the TUI's accumulated message content - Collect tool calls from intermediate SDK turns and include them BEFORE text content in the final AssistantMessage - The agent loop's externalToolExecution path emits proper tool_execution_start/end events for each intermediate tool call - Result: tool activity renders above the text response, not below Co-Authored-By: Claude Opus 4.6 (1M context) --- .../claude-code-cli/stream-adapter.ts | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/resources/extensions/claude-code-cli/stream-adapter.ts b/src/resources/extensions/claude-code-cli/stream-adapter.ts index d07aacd75..ab106b1dc 100644 --- a/src/resources/extensions/claude-code-cli/stream-adapter.ts +++ b/src/resources/extensions/claude-code-cli/stream-adapter.ts @@ -147,6 +147,8 @@ async function pumpSdkMessages( /** Track the last text content seen across all assistant turns for the final message. */ let lastTextContent = ""; let lastThinkingContent = ""; + /** Collect tool calls from intermediate SDK turns for tool_execution events. */ + const intermediateToolCalls: AssistantMessage["content"] = []; try { // Dynamic import — the SDK is an optional dependency. @@ -225,7 +227,14 @@ async function pumpSdkMessages( const assistantEvent = builder.handleEvent(event); if (assistantEvent) { - stream.push(assistantEvent); + // Skip toolcall events — the agent loop's externalToolExecution + // path emits tool_execution_start/end events after streamSimple + // returns. Streaming toolcall events would render tool calls + // out of order in the TUI's accumulated message content. + const t = assistantEvent.type; + if (t !== "toolcall_start" && t !== "toolcall_delta" && t !== "toolcall_end") { + stream.push(assistantEvent); + } } break; } @@ -251,13 +260,16 @@ async function pumpSdkMessages( const userMsg = msg as SDKUserMessage; if (userMsg.parent_tool_use_id !== null) break; - // Capture accumulated text from the builder before resetting + // Capture content from the completed turn before resetting if (builder) { for (const block of builder.message.content) { if (block.type === "text" && block.text) { lastTextContent = block.text; } else if (block.type === "thinking" && block.thinking) { lastThinkingContent = block.thinking; + } else if (block.type === "toolCall") { + // Collect tool calls for externalToolExecution rendering + intermediateToolCalls.push(block); } } } @@ -269,15 +281,22 @@ async function pumpSdkMessages( case "result": { const result = msg as SDKResultMessage; - // Build final message with all content from the last assistant turn. - // Tool calls are preserved — the agent loop's externalToolExecution - // mode handles them without local dispatch. - let finalContent: AssistantMessage["content"] = []; + // Build final message. Include intermediate tool calls so the + // agent loop's externalToolExecution path emits tool_execution + // events for proper TUI rendering, followed by the text response. + const finalContent: AssistantMessage["content"] = []; + // Add tool calls from intermediate turns first (renders above text) + finalContent.push(...intermediateToolCalls); + + // Add text/thinking from the last turn if (builder && builder.message.content.length > 0) { - finalContent = [...builder.message.content]; + for (const block of builder.message.content) { + if (block.type === "text" || block.type === "thinking") { + finalContent.push(block); + } + } } else { - // Fall back to captured text from complete assistant messages if (lastThinkingContent) { finalContent.push({ type: "thinking", thinking: lastThinkingContent }); }