From dc84694c65298edfb209e1fbbd06e5f8df8f863e Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 13 Apr 2026 08:16:16 -0500 Subject: [PATCH] fix(tui): stop pinned latest-output mirror from duplicating streaming text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pinned `Working · Latest Output` border above the editor mirrors the assistant's latest text block while tools run, so prose stays visible after a tool's output scrolls it off-screen. The mirror walked content blocks from the end and picked the last text block — but when the assistant streams a *new* text block after a tool call (sequence `[text1, tool1, text2_streaming]`), it picked `text2`, which was also being streamed live into the chat container. Result: identical tokens rendered in two places at once. Restrict the search to text blocks whose index is strictly less than the index of the most recent tool call. Text after the last tool call stays in the chat container only; earlier prose (e.g. `text1`) remains mirrored the entire time the new text streams, so context isn't lost and the loading-animation handoff is undisturbed. Fixes #4120 Co-Authored-By: Claude Opus 4.6 --- .../interactive/controllers/chat-controller.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 2c79fbd58..aff3d04d0 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 @@ -286,9 +286,20 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & { if (hasTools) hasToolsInTurn = true; if (hasToolsInTurn) { - // Collect the latest text block(s) from the assistant message - let latestText = ""; + // Mirror the latest text block that precedes the most recent tool + // call. Text blocks that come *after* the last tool call are still + // streaming live into the chat container, so mirroring them would + // duplicate the same tokens in two places at once. + let lastToolIdx = -1; for (let i = contentBlocks.length - 1; i >= 0; i--) { + const c = contentBlocks[i] as any; + if (c.type === "toolCall" || c.type === "serverToolUse") { + lastToolIdx = i; + break; + } + } + let latestText = ""; + for (let i = lastToolIdx - 1; i >= 0; i--) { const c = contentBlocks[i] as any; if (c.type === "text" && c.text?.trim()) { latestText = c.text.trim();