fix(tui): skip full clears for off-viewport mutations during streaming - #1227
Conversation
… during streaming scroll (#1222) When Atomic streams output and the user scrolls a non-fullscreen terminal, pi-tui's `TUI.doRender()` falls back to a destructive full clear + scrollback wipe (`CSI 2J/H/3J`) whenever a changed logical line sits above the bottom-anchored viewport. Repeated clears read as flicker and wipe the scrollback the user is reading. This patches `@earendil-works/pi-tui@0.78.0` via Bun `patchedDependencies` to make the off-viewport diff classifier viewport-safe: same-shape off-viewport text mutations and append-only tail growth update renderer state / repaint only visible rows instead of full-clearing, while truly unsafe cases (image/Kitty changes, shrink/deletion, geometry changes, and structural inserts above the viewport) keep the conservative full clear. Because `@bastani/atomic` publishes as an npm package, the patched pi-tui plus its runtime closure (`marked`, `get-east-asian-width`) is bundled into the tarball via `bundleDependencies` + prepack/postpack materialize and an isolated install/import verifier. Adds a focused regression suite and a CHANGELOG entry. Known limitation (see PR description): a structural insert immediately above the viewport combined with visible-row mutations can still be misclassified as append-only. Draft / not yet merge-ready. Refs #1222
|
Review — PR #1227 (pi-tui off-viewport redraw fix) Reviewed as requested. Credit where due: this is an exceptionally honest, well-documented draft — the description, the inline "known limitation," and the defensive scripting all reflect real care. Feedback by severity. 🔴 The known P2 bug — I would take door #2 The classifier tries to distinguish append from structural insert from rendered text alone, with no stable row identity. That is an inherently unsolvable problem — every one of the 10 ralph rounds confirmed it by surfacing a new boundary case, and the final one (insert at index 69 + mutate visible rows 70–71 → misclassified as append → stale native scrollback) is the predictable result. I strongly endorse the author direction #2: conservatively full-clear for all ambiguous above-viewport growth. The failure modes are asymmetric:
Trading guaranteed correctness for a heuristic that removes some flicker but can corrupt scrollback is the wrong side of that trade. The simpler rule (safe == line-count-stable same-shape mutations and pure tail append; everything else full-clears) is what the suite already largely encodes, and it lets you delete the entire 🟠 Maintainability of the classifier + compiled patch The off-viewport branch is now a ~7-flag boolean state machine ( 🟠 bundleDependencies pins Bundling 🟡 Verifier uses
🟡 prepack/postpack cleanup on the publish path The manifest-driven cleanup, 🟡 Per-frame O(n) scan while scrolled When scrolled above the viewport during streaming, the new branch scans 🟢 Tests / process
Bottom line Correctly marked draft — do not merge as-is. Recommendation: collapse to the conservative-always-correct classifier (#2) for the interim patch, track the upstream row-identity fix (#1) as the real resolution, and add a CI guard to auto-retire the bundling carrier. The bundling/materialize/verify infrastructure is solid; it is specifically the heuristic classifier I would cut. Automated review — please weigh against your own judgment. |
…1222) Assistant-model: OpenAI GPT-5
Review — #1227 (TUI off-viewport redraw fix)Thanks for the careful work here — the renderer patch is well-reasoned, the path-traversal guards in the materialize/clean scripts are solid, and the honest "Known Limitation (P2)" callout is appreciated. The fix itself ( 🔴 Description ↔ code divergences (please reconcile before merge)
🟠 Correctness / robustness
🟡 Tests / flakiness
🟢 Maintainability (non-blocking)
✅ Looks good
Overall the core fix is sound; the main asks are (1) reconcile the description with the committed tests/patch, and (2) add tests for the image-change and insert-at-boundary paths the fix claims to preserve — precisely the un-covered risk areas. Automated review — verify findings before acting. |
Assistant-model: GPT-5.5
|
Code Review — PR 1227: skip full-screen clears for off-viewport text diffs Thanks for the detailed write-up and the explicit P2 limitation section — that honesty about the heuristic blind spots makes this much easier to reason about. Overall the approach (patch upstream pi-tui + materialize the patched closure into the published tarball via 🐛 Potential bugs / correctness
🧪 Test coverage
🔧 Maintainability
✅ Things done well
Nice work overall — the bundling machinery is the riskiest part and it is the most carefully guarded. Main asks: resolve/scope the stale-scrollback behavior in item 1, close the test gap in items 2 and 3, and fix the test-count claim. |
Assistant-model: GPT-5.5
Implementation NotesTask: Re-implement the fix for GitHub issue #1222 (TUI flicker when scrolling during streaming outside fullscreen) in THIS worktree, which is already on branch === CRITICAL CONTEXT — READ FIRST === The current worktree carries a PREVIOUS attempt that tried to CLASSIFY off-viewport diffs as 'append' vs 'structural insert' from rendered text. THAT APPROACH IS FORBIDDEN and must be removed — it is inherently ambiguous and was rejected across 10 review rounds. Do NOT reintroduce any append-vs-insert / shifted-run / same-index-anchor heuristic. === THE REQUIRED APPROACH (correct-by-construction; from upstream PRs #3105 + #4204) ===
Reference (for the agents, do not fetch unless useful): upstream PR earendil-works/pi#3105 (offscreen-only skip + commitState) and earendil-works/pi#4204 (fullRender mode split / preserve scrollback). The buggy branch on upstream main is packages/tui/src/tui.ts around the === SCOPE / KEEP ===
=== CONSTRAINTS ===
=== VALIDATION (all must pass) ===
=== DELIVERABLE === Running Notes
Iteration 1/6 implementation notes (2026-06-04)
Iteration 2/6 implementation notes (2026-06-04)
|
Review: PR #1227 — skip full-screen clears for off-viewport text diffsThanks for the detailed write-up and the regression suite — the problem statement, the classifier table, and the documented P2 limitation make this much easier to reason about. Overall the renderer change is targeted and well-tested at the unit level. Most of my comments are about the bundling machinery and a couple of correctness/robustness questions. What works well
Correctness questions
Bundling / release concerns
Minor
Test coverageGood coverage of the happy paths (zero-byte skip, mixed → Nice, careful work overall. The renderer change is sound; my main asks are gating the known scrollback-corruption case and putting |
…e verify:bundled-pi-tui into CI (#1222) Addresses automated PR review feedback on #1227 (no renderer behavior change): - Add regression: a differential render *following* a no-write off-viewport skip lands on the correct row (asserts exact `\x1b[4A` cursor move), proving commitState() cursor bookkeeping is sound (review point #1). - Add regression: an insert immediately above the viewport + a visible mutation (not the strict same-count skip) takes the conservative `fullRender(true)` path — clears the viewport (`\x1b[2J\x1b[H`) but never wipes scrollback (`\x1b[3J`). Pins the safe behavior against regression (review point #2 / coverage gap b). - Wire `verify:bundled-pi-tui` into CI: a gate in publish.yml before `npm publish` (a broken bundle closure can no longer silently ship) and a Linux-only early-signal step in test.yml (review point #3). - Document the implicit `prepack`-on-`bun pm pack` lifecycle assumption in verify-bundled-pi-tui-install.ts (review point #4). - Comment RENDER_SETTLE_MS (matches the 16ms render throttle) and add temporary-mechanism notes near the marker constant / destination-conflict guard (review points #5, #6, minor). Refs #1222
PR Review — #1227 (fix/1222 TUI streaming-scroll flicker)Thorough, well-documented work. The renderer patch is small and surgical, the bundling machinery is defensive, and the regression suite is genuinely good (the post-skip cursor-row assertion Highest concern: verification packs with Bun, but production publishes with npmThe publish gate (
Recommendations (either is fine):
Worth a local check: Interrupted pack leaves residue that hard-fails the next pack
Patch targets compiled
|
|
Thanks for the thorough review — addressed in
|
#1227) * fix: avoid pi-tui full screen/scrollback clears on off-viewport diffs during streaming scroll (#1222) When Atomic streams output and the user scrolls a non-fullscreen terminal, pi-tui's `TUI.doRender()` falls back to a destructive full clear + scrollback wipe (`CSI 2J/H/3J`) whenever a changed logical line sits above the bottom-anchored viewport. Repeated clears read as flicker and wipe the scrollback the user is reading. This patches `@earendil-works/pi-tui@0.78.0` via Bun `patchedDependencies` to make the off-viewport diff classifier viewport-safe: same-shape off-viewport text mutations and append-only tail growth update renderer state / repaint only visible rows instead of full-clearing, while truly unsafe cases (image/Kitty changes, shrink/deletion, geometry changes, and structural inserts above the viewport) keep the conservative full clear. Because `@bastani/atomic` publishes as an npm package, the patched pi-tui plus its runtime closure (`marked`, `get-east-asian-width`) is bundled into the tarball via `bundleDependencies` + prepack/postpack materialize and an isolated install/import verifier. Adds a focused regression suite and a CHANGELOG entry. Known limitation (see PR description): a structural insert immediately above the viewport combined with visible-row mutations can still be misclassified as append-only. Draft / not yet merge-ready. Refs #1222 * fix(coding-agent): preserve scrollback for off-viewport TUI redraws (#1222) Assistant-model: OpenAI GPT-5 * fix(coding-agent): reset TUI shrink redraw high-water mark (#1222) Assistant-model: GPT-5.5 * fix(coding-agent): clarify TUI clear-mode patch bookkeeping (#1222) Assistant-model: GPT-5.5 * test(coding-agent): cover post-skip render + conservative insert; wire verify:bundled-pi-tui into CI (#1222) Addresses automated PR review feedback on #1227 (no renderer behavior change): - Add regression: a differential render *following* a no-write off-viewport skip lands on the correct row (asserts exact `\x1b[4A` cursor move), proving commitState() cursor bookkeeping is sound (review point #1). - Add regression: an insert immediately above the viewport + a visible mutation (not the strict same-count skip) takes the conservative `fullRender(true)` path — clears the viewport (`\x1b[2J\x1b[H`) but never wipes scrollback (`\x1b[3J`). Pins the safe behavior against regression (review point #2 / coverage gap b). - Wire `verify:bundled-pi-tui` into CI: a gate in publish.yml before `npm publish` (a broken bundle closure can no longer silently ship) and a Linux-only early-signal step in test.yml (review point #3). - Document the implicit `prepack`-on-`bun pm pack` lifecycle assumption in verify-bundled-pi-tui-install.ts (review point #4). - Comment RENDER_SETTLE_MS (matches the 16ms render throttle) and add temporary-mechanism notes near the marker constant / destination-conflict guard (review points #5, #6, minor). Refs #1222
Summary
Fixes TUI flicker and scrollback wipes when Atomic streams output in a short (non-fullscreen) terminal and the user has manually scrolled. Root cause:
@earendil-works/pi-tui@0.78.0'sTUI.doRender()unconditionally fell back to a destructive full clear + scrollback wipe (CSI 2J/CSI H/CSI 3J) whenever a changed logical line sat above the bottom-anchored viewport — a condition that fires constantly during live streaming in short terminals.0.78.0is the latest published@earendil-works/pi-tui, and upstream has not fixed this (the matching issues — #4785, #4044, #4260, #4506, #3756 — were closed without a merged fix; the bug is still present onpimain). So there is nothing to bump to, and the fix is carried as a patched, bundled dependency.Closes #1222
The fix — correct by construction, no heuristics
The renderer change adds exactly one safe no-write skip plus a scrollback-preserving clear split. It does not try to classify off-viewport diffs (no append-vs-insert / shifted-run guessing).
Patch (
patches/@earendil-works%2Fpi-tui@0.78.0.patch):commitState()— advances renderer bookkeeping (previousLines,previousViewportTop,maxLinesRendered, cursor row, etc.) and writes zero bytes.fullRender(clear)split intofalse | true | "scrollback":true→CSI 2J CSI H(viewport clear, scrollback preserved); writes only the lastheightrows so preserved scrollback isn't duplicated."scrollback"→CSI 2J CSI H CSI 3J(full wipe), used only on terminal width change (existing scrollback was wrapped at the old width).fullRender(true)— now scrollback-preserving. This is the conservative path: a viewport repaint that never wipes scrollback. There is intentionally no attempt to be cleverer than that.Delivery (bundled patched dependency)
Since
@bastani/atomicis an npm package, a root-only Bun patch wouldn't reach consumers. The patched@earendil-works/pi-tuiplus its runtime closure (marked,get-east-asian-width) is materialized intonode_modulesat pack time (prepack/postpack) and declared inbundleDependencies, with an isolated pack-install-import verifier (verify:bundled-pi-tui). This is a temporary mechanism to be removed once an upstream pi-tui release ships the fix.Tests (
test/suite/regressions/1222-tui-offviewport-redraw.test.ts)FakeTerminal+MutableLinesregressions over the patched build (imported viapatchedDependencies):fullRedrawsunchanged.CSI 4Acursor move) — validatescommitState()cursor bookkeeping.fullRender(true): writesCSI 2J CSI H, neverCSI 3J(scrollback preserved).fullRender("scrollback")(CSI 3J).clearOnShrinkfull clear does not repeat on the next no-op render.CI
publish.yml—verify:bundled-pi-tuiruns as a gate beforenpm publish, so a broken bundle closure cannot silently ship.test.yml—verify:bundled-pi-tuiruns on the Linux matrix entry for early PR feedback.Validation
bun run typecheck✅ ·bun run lint✅bun run --cwd packages/coding-agent test -- test/suite/regressions/1222-tui-offviewport-redraw.test.ts✅ (7)bun run --cwd packages/coding-agent test -- test/edit-tool-no-full-redraw.test.ts✅ (3)SKIP_BUILD=1 bun run --cwd packages/coding-agent verify:bundled-pi-tui✅0.78.0install (+ reverse dry-run).Out of scope / follow-up
A perfect cure for the rarer "streaming markdown reflow rewraps a line that already scrolled above the fold" case needs a stable-prefix / freeze-above-the-fold discipline in
AssistantMessageComponent.updateContent()(which clears+rebuilds the fullMarkdowneach token). That's a larger, separate change and is intentionally not attempted here; this PR makes the renderer scrollback-safe and eliminates the destructive wipe + off-viewport repaints.