From b4889616093e960c9279ca77918ae63e2cf63d0d Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 11 Apr 2026 17:41:50 -0500 Subject: [PATCH] fix(tui): clear pinned output on message_end to prevent duplicate display The pinned "Latest Output" zone was only cleared at agent_end, but during flows with form elicitation (e.g. discuss-phase), there is a gap between message_end and agent_end where the agent waits for user input. During this gap, the same content was visible in both the chat history and the pinned zone. Clear the pinned zone at message_end when the assistant message is finalized in the chat container. --- .../src/core/chat-controller-ordering.test.ts | 50 +++++++++++++++++++ .../controllers/chat-controller.ts | 9 ++++ 2 files changed, 59 insertions(+) diff --git a/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts b/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts index a7a826975..eb7795508 100644 --- a/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +++ b/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts @@ -386,6 +386,56 @@ test("chat-controller clears pinned zone when the agent turn ends", async () => assert.equal(host.pinnedMessageContainer.children.length, 0, "pinned zone should clear on agent_end"); }); +test("chat-controller clears pinned zone when assistant message ends", async () => { + (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + fg: (_key: string, text: string) => text, + bg: (_key: string, text: string) => text, + bold: (text: string) => text, + italic: (text: string) => text, + truncate: (text: string) => text, + }; + + const host = createHost(); + const toolCall = { + type: "toolCall", + id: "tool-msg-end-1", + name: "exec_command", + arguments: { cmd: "echo hi" }, + }; + + await handleAgentEvent(host, { type: "message_start", message: makeAssistant([]) } as any); + + host.getMarkdownThemeWithSettings = () => ({}); + const msgContent = [{ type: "text", text: "Summary after tools." }, toolCall]; + await handleAgentEvent( + host, + { + type: "message_update", + message: makeAssistant(msgContent), + assistantMessageEvent: { + type: "toolcall_end", + contentIndex: 1, + toolCall: { + ...toolCall, + externalResult: { + content: [{ type: "text", text: "ok" }], + details: {}, + isError: false, + }, + }, + partial: makeAssistant(msgContent), + }, + } as any, + ); + + assert.ok(host.pinnedMessageContainer.children.length > 0, "pinned zone should be populated during streaming"); + + // End the assistant message (e.g. before form elicitation) — pinned zone should clear + await handleAgentEvent(host, { type: "message_end", message: makeAssistant(msgContent) } as any); + + assert.equal(host.pinnedMessageContainer.children.length, 0, "pinned zone should clear on message_end to prevent duplicate display"); +}); + test("chat-controller does not pin when there are no tool calls", async () => { (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, 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 634f8d28f..88d887ffd 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 @@ -380,6 +380,15 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & { } host.streamingComponent = undefined; host.streamingMessage = undefined; + // Clear pinned output once the message is finalized in the chat + // container — prevents duplicate display when the agent continues + // (e.g. form elicitation) after the assistant message ends. + if (pinnedBorder) pinnedBorder.stopSpinner(); + host.pinnedMessageContainer.clear(); + lastPinnedText = ""; + hasToolsInTurn = false; + pinnedBorder = undefined; + pinnedTextComponent = undefined; host.footer.invalidate(); } host.ui.requestRender();