diff --git a/src/tui/components/chat-log.test.ts b/src/tui/components/chat-log.test.ts index 8d3e77f2cacc..9f248445e791 100644 --- a/src/tui/components/chat-log.test.ts +++ b/src/tui/components/chat-log.test.ts @@ -387,6 +387,38 @@ describe("ChatLog", () => { expect(normalizeTestText(chatLog.render(120).join("\n"))).toContain("Still streaming."); }); + it("preserves a delayed shared prompt and its active reply when scrollback is full", () => { + const chatLog = new ChatLog(20); + chatLog.updateAssistant("Already streaming.", "shared-run"); + for (let index = 0; index < 19; index += 1) { + chatLog.addSystem(`Older notice ${index}.`); + } + + chatLog.addLiveUser("Sent from the other client.", { + messageId: "shared-overflow-user", + runId: "shared-run", + }); + chatLog.addLiveUser("Sent from the other client.", { + messageId: "shared-overflow-user", + runId: "shared-run", + }); + + const rendered = normalizeTestText(chatLog.render(120).join("\n")); + expect(chatLog.children).toHaveLength(20); + expect(rendered).toContain("Sent from the other client."); + expect(rendered).toContain("Already streaming."); + expect(rendered).not.toContain("Older notice 0."); + expect(rendered.match(/Sent from the other client\./g)).toHaveLength(1); + expect(rendered.indexOf("Sent from the other client.")).toBeLessThan( + rendered.indexOf("Already streaming."), + ); + + chatLog.updateAssistant("Still streaming after overflow.", "shared-run"); + expect(normalizeTestText(chatLog.render(120).join("\n"))).toContain( + "Still streaming after overflow.", + ); + }); + it("deduplicates authoritative user events and adopts the matching pending prompt", () => { const chatLog = new ChatLog(40); chatLog.addPendingUser("shared-run", "Persisted prompt."); diff --git a/src/tui/components/chat-log.ts b/src/tui/components/chat-log.ts index d7db5a429e2f..ec47a01236df 100644 --- a/src/tui/components/chat-log.ts +++ b/src/tui/components/chat-log.ts @@ -87,9 +87,11 @@ export class ChatLog extends Container { } } - private pruneOverflow() { + private pruneOverflow(protectedComponents?: ReadonlySet) { while (this.children.length > this.maxComponents) { - const oldest = this.children[0]; + const oldest = protectedComponents + ? this.children.find((component) => !protectedComponents.has(component)) + : this.children[0]; if (!oldest) { return; } @@ -231,12 +233,13 @@ export class ChatLog extends Container { frozen?.values().next().value ?? (options.runId ? this.streamingRuns.get(options.runId) : undefined); const assistantIndex = assistant ? this.children.indexOf(assistant) : -1; - if (assistantIndex >= 0) { + if (assistant && assistantIndex >= 0) { // Transcript broadcasts can trail the first delta; insert their prompt - // before the existing reply without resetting live stream or tool state. + // before the existing reply. Preserve both when full scrollback evicts + // older components so the newly recovered prompt cannot disappear. this.repeatableSystemMessage = null; this.children.splice(assistantIndex, 0, component); - this.pruneOverflow(); + this.pruneOverflow(new Set([component, assistant])); return component; } this.appendNonSystem(component);