fix(tui): compact transcript retention without losing resize history#5239
fix(tui): compact transcript retention without losing resize history#5239RensTillmann wants to merge 3 commits into
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Follow-up fix for a live crash exposed by this PR's compaction path:
PR: #5298
|
Crash follow-up for this PRLive crash after applying this PR: Cause
FixGuard Verification
I opened #5298 but it was auto-closed (author not vouched). Please cherry-pick this onto the PR branch: From c14513a8b03c26d989c78da68484302be926d7df Mon Sep 17 00:00:00 2001
From: "T.C. Ferguson" <tc.ferguson@forwardthinking.com>
Date: Sun, 12 Jul 2026 09:52:39 -1000
Subject: [PATCH] fix(tui): skip sparse compacted segments in
isBlockUncommitted
After #5239 compaction, a later render only fills segments from
#compactedChildStart, leaving undefined holes in the prefix. Iterating
those entries crashed when retiring IRC/ephemeral cards
(`segment2.component`). Guard undefined segment slots and cover the
sparse-hole path with a red/green regression.
---
.../modes/components/transcript-container.ts | 6 +++++-
.../components/transcript-container.test.ts | 20 +++++++++++++++++++
2 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/packages/coding-agent/src/modes/components/transcript-container.ts b/packages/coding-agent/src/modes/components/transcript-container.ts
index 428b46831..576dec064 100644
--- a/packages/coding-agent/src/modes/components/transcript-container.ts
+++ b/packages/coding-agent/src/modes/components/transcript-container.ts
@@ -242,9 +242,13 @@ export class TranscriptContainer
*/
isBlockUncommitted(component: Component): boolean {
const index = this.children.indexOf(component);
+ // Compacted prefix is already committed native history and must not be
+ // retracted. Compacted slots may be sparse holes after a later re-render
+ // (render only fills from #compactedChildStart), so the loop below must
+ // skip undefined entries.
if (index >= 0 && index < this.#compactedChildStart) return false;
for (const segment of this.#segments) {
- if (segment.component !== component) continue;
+ if (segment === undefined || segment.component !== component) continue;
return segment.rowCount === 0 || segment.startRow >= this.#committedRows;
}
return true;
diff --git a/packages/coding-agent/test/modes/components/transcript-container.test.ts b/packages/coding-agent/test/modes/components/transcript-container.test.ts
index 56b24164c..6a812f0ba 100644
--- a/packages/coding-agent/test/modes/components/transcript-container.test.ts
+++ b/packages/coding-agent/test/modes/components/transcript-container.test.ts
@@ -700,6 +700,26 @@ describe("TranscriptContainer isBlockUncommitted", () => {
container.setNativeScrollbackCommittedRows(100);
expect(container.isBlockUncommitted(empty)).toBe(true);
});
+
+ it("survives sparse compacted segment holes when checking uncommitted status", () => {
+ const container = new TranscriptContainer();
+ const committed = new StreamingBlock(["committed"], true);
+ const live = new StreamingBlock(["live"], true);
+ container.addChild(committed);
+ container.addChild(live);
+
+ expect(container.render(40)).toEqual(["committed", "", "live"]);
+ container.setNativeScrollbackCommittedRows(2);
+ // Compaction first rewrites the compacted prefix into zero-row placeholders.
+ expect(container.render(40)).toEqual(["live"]);
+ // The next render only repopulates from #compactedChildStart, leaving a
+ // sparse hole at the compacted index. Retiring IRC/ephemeral cards walks
+ // every segment and must not crash on those undefined entries.
+ expect(container.render(40)).toEqual(["live"]);
+ expect(() => container.isBlockUncommitted(live)).not.toThrow();
+ expect(() => container.isBlockUncommitted(committed)).not.toThrow();
+ expect(container.isBlockUncommitted(committed)).toBe(false);
+ });
});
describe("TranscriptContainer renderViewportTail", () => {
--
2.50.1 (Apple Git-155)
|
After can1357#5239 compaction, a later render only fills segments from #compactedChildStart, leaving undefined holes in the prefix. Iterating those entries crashed when retiring IRC/ephemeral cards (`segment2.component`). Guard undefined segment slots and cover the sparse-hole path with a red/green regression.
Summary
NativeScrollbackReplaycontractFixes #4820.
Context
This builds on the approach in #4841 by @cexll. That PR's review identified a P1 data-loss case: after compaction, a non-multiplexer resize or reset clears native scrollback but the compacted prefix cannot be replayed. This version adds an explicit pre-full-paint replay hook, so
TranscriptContainerre-renders the complete child history for that paint and resumes compaction on the following frame.Tests
bun test packages/tui/test/component-render.test.ts packages/coding-agent/test/modes/components/transcript-container.test.ts packages/tui/test/markdown.test.ts— 170 passedbun run check:typesinpackages/tui— passedbun run check:typesinpackages/coding-agent— passed--version,--help, andomp read /etc/hostnamesmoke tests — passedRegression coverage