Merge pull request #4216 from NilsR0711/fix/4197-agent-end-destroys-streaming-message

fix(pi-coding-agent): finalize streaming component on agent_end instead of removing it
This commit is contained in:
Jeremy McSpadden 2026-04-14 21:01:05 -05:00 committed by GitHub
commit 15ca855b3e
2 changed files with 87 additions and 4 deletions

View file

@ -813,3 +813,83 @@ test("chat-controller updates pinned zone after sub-turn shrink", async () => {
// Finalize so the module-level pinned spinner (setInterval) is torn down and the test process can exit.
await handleAgentEvent(host, { type: "message_end", message: makeAssistant([{ type: "text", text: "second" }, t2]) } as any);
});
test("chat-controller: agent_end without message_end must not remove streaming component from DOM (regression #4197)", async () => {
const host = createHost();
await handleAgentEvent(host, {
type: "message_start",
message: makeAssistant([]),
} as any);
// Simulate partial streaming that creates an AssistantMessageComponent
await handleAgentEvent(host, {
type: "message_update",
message: makeAssistant([{ type: "text", text: "partial answer" }]),
assistantMessageEvent: {
type: "text_delta",
contentIndex: 0,
delta: "partial answer",
partial: makeAssistant([{ type: "text", text: "partial answer" }]),
},
} as any);
// Precondition: component is in DOM
assert.equal(
host.chatContainer.children.length,
1,
"streaming component must be in DOM after message_update",
);
const comp = host.chatContainer.children[0];
// Simulate abort: agent_end fires WITHOUT message_end
await handleAgentEvent(host, { type: "agent_end" } as any);
assert.equal(
host.chatContainer.children.length,
1,
"agent_end must NOT remove the streaming component from the DOM (issue #4197)",
);
assert.equal(
host.chatContainer.children[0],
comp,
"the same component instance must remain in the DOM after agent_end",
);
});
test("chat-controller: agent_end after message_end must not alter DOM", async () => {
const host = createHost();
const content = [{ type: "text", text: "complete answer" }];
await handleAgentEvent(host, {
type: "message_start",
message: makeAssistant([]),
} as any);
await handleAgentEvent(host, {
type: "message_update",
message: makeAssistant(content),
assistantMessageEvent: {
type: "text_delta",
contentIndex: 0,
delta: "complete answer",
partial: makeAssistant(content),
},
} as any);
await handleAgentEvent(host, {
type: "message_end",
message: makeAssistant(content),
} as any);
const countAfterMessageEnd = host.chatContainer.children.length;
assert.ok(countAfterMessageEnd > 0, "component must be present after message_end");
await handleAgentEvent(host, { type: "agent_end" } as any);
assert.equal(
host.chatContainer.children.length,
countAfterMessageEnd,
"agent_end after message_end must not add or remove DOM nodes",
);
});

View file

@ -546,11 +546,14 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
host.loadingAnimation = undefined;
host.statusContainer.clear();
}
if (host.streamingComponent) {
host.chatContainer.removeChild(host.streamingComponent);
host.streamingComponent = undefined;
host.streamingMessage = undefined;
if (host.streamingComponent && host.streamingMessage) {
host.streamingComponent.setShowMetadata(true);
host.streamingComponent.updateContent(host.streamingMessage);
}
host.streamingComponent = undefined;
host.streamingMessage = undefined;
renderedSegments = [];
lastContentLength = 0;
host.pendingTools.clear();
// Pinned output is only useful while work is actively streaming.
// Keep chat history as the single source after completion.