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();