From 263d725ecde3cb5c3f7b827a1ecf29565f1bcc40 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Wed, 25 Mar 2026 14:38:39 -0600 Subject: [PATCH] fix: render tool calls above text response for external providers - Add insertChildBefore() to Box component for positional insertion - In chat controller, insert tool_execution components before the last assistant message component (instead of appending after) when tools were executed externally - Simplify agent-loop externalToolExecution path back to basic tool_execution_start/end emission - Toolcall streaming events are filtered in the Claude Code adapter to prevent duplicate rendering via message_update Result: externally-executed tool calls render above the text response, matching the expected visual flow. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/pi-agent-core/src/agent-loop.ts | 5 +++-- .../interactive/controllers/chat-controller.ts | 13 ++++++++++++- packages/pi-tui/src/components/box.ts | 10 ++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/pi-agent-core/src/agent-loop.ts b/packages/pi-agent-core/src/agent-loop.ts index ff2bab0f9..a544b58c1 100644 --- a/packages/pi-agent-core/src/agent-loop.ts +++ b/packages/pi-agent-core/src/agent-loop.ts @@ -234,8 +234,9 @@ async function runLoop( const toolResults: ToolResultMessage[] = []; if (hasMoreToolCalls && config.externalToolExecution) { - // External execution mode: tools were handled by the provider (e.g., Claude Code SDK). - // Emit synthetic tool events for TUI rendering but skip local dispatch. + // External execution mode: tools were handled by the provider + // (e.g., Claude Code SDK). Emit tool_execution events for each + // tool call. The TUI adds these as components after the message. for (const tc of toolCalls as AgentToolCall[]) { stream.push({ type: "tool_execution_start", diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts index 7f9fe7044..f9f7a5c79 100644 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +++ b/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts @@ -210,7 +210,18 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & { host.ui, ); component.setExpanded(host.toolOutputExpanded); - host.chatContainer.addChild(component); + + // For external tool execution: insert tool components before the + // last message component so tools render above the text response. + // The last child is the message that just finished streaming. + const children = host.chatContainer.children; + const lastChild = children.length > 0 ? children[children.length - 1] : undefined; + if (lastChild instanceof AssistantMessageComponent && !host.streamingComponent) { + host.chatContainer.insertChildBefore(component, lastChild); + } else { + host.chatContainer.addChild(component); + } + host.pendingTools.set(event.toolCallId, component); host.ui.requestRender(); } diff --git a/packages/pi-tui/src/components/box.ts b/packages/pi-tui/src/components/box.ts index c99b8600b..9dd692750 100644 --- a/packages/pi-tui/src/components/box.ts +++ b/packages/pi-tui/src/components/box.ts @@ -31,6 +31,16 @@ export class Box implements Component { this.invalidateCache(); } + insertChildBefore(component: Component, before: Component): void { + const index = this.children.indexOf(before); + if (index !== -1) { + this.children.splice(index, 0, component); + } else { + this.children.push(component); + } + this.invalidateCache(); + } + removeChild(component: Component): void { const index = this.children.indexOf(component); if (index !== -1) {