Skip to content

fix(cli): freeze off-screen entry renders to prevent scrollback-clearing redraws - #1139

Merged
Astro-Han merged 3 commits into
mainfrom
fix/cli-offscreen-mutation-freeze
Jul 16, 2026
Merged

fix(cli): freeze off-screen entry renders to prevent scrollback-clearing redraws#1139
Astro-Han merged 3 commits into
mainfrom
fix/cli-offscreen-mutation-freeze

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Closes #1135.

Five transcript event paths mutated the rendered lines of entries that had scrolled above pi-tui's live viewport. Any such change made pi-tui's differential renderer hit its firstChanged < viewportTop path and emit ESC[2J ESC[H ESC[3J, clearing pre-Maka scrollback and resetting the scroll position.

The invariant #1130 established for toggles should own all of these: the rendered prefix of off-screen entries must stay frozen.

The fix is a single render-layer freeze point plus two targeted guards:

  1. Render freezerenderTranscriptEntryMemoized checks whether the entry's first line sits above the live viewport (using fix(cli): anchor Ctrl+O/Ctrl+T expansion to the live viewport so toggles never clear scrollback #1130's renderGeometry). If so, it serves the cached render matching the current width instead of re-rendering. The underlying entry state still updates — only the visual output is frozen. This covers the ticker, settle/ownership updates, thinking_complete replacement, and tool_progress/tool_output_delta appends.

  2. Splice guard — poll-card removal (tool_result fold for Read/StopBackgroundTask) gets an entryInLiveViewport check: splicing an off-screen entry shifts subsequent line numbers, which the freeze alone cannot prevent. The stale entry stays in place until the next session switch.

  3. Ticker skiprefreshRunningShellRunElapsed skips off-screen running cards, so it returns false when no visible card needs an update and the 1s interval stops automatically.

Verification

  • packages/cli: tsc --noEmit clean, npm test 465/465 (3 new), root npm run lint clean.
  • New behavioral tests through the real pi-tui renderer asserting no ESC[3J for: off-screen running-Bash ticker updates (2.5s wait past two ticks), off-screen shell-run settle, and off-screen thinking_complete.
  • Updated pi-tui-layout.test.ts: the "in-place edit above the top" sub-case now expects viewportTop to stay put (the freeze prevents the rendered change from reaching the shadow diff), matching the new invariant.

…ing redraws (#1135)

Off-screen transcript entries live in terminal scrollback, which is
immutable. Five event paths mutated their rendered lines in place,
forcing pi-tui's differential renderer into a full redraw
(ESC[2J ESC[H ESC[3J) that clears pre-Maka scrollback and resets the
scroll position:

- refreshRunningShellRunElapsed: 1s ticker rewrote off-screen running
  cards every second
- shell-run settle/ownership: applied status/result changes to off-screen
  Bash cards
- setThinking: late thinking_complete replaced streamed text in place
- tool_result poll-card splice: removed an off-screen entry, shifting
  subsequent line numbers
- tool_progress/tool_output_delta: appended to expanded off-screen cards

The fix introduces a single render-layer freeze: when an entry's first
line sits above the live viewport (already recorded by #1130's
renderGeometry), renderTranscriptEntryMemoized serves the cached render
matching the current width instead of re-rendering. The underlying entry
state still updates — only the visual output is frozen, so session
switch, resume, and re-expand stay correct.

The splice path gets a separate entryInLiveViewport guard: splicing an
off-screen entry shifts line numbers above the viewport, which the freeze
alone cannot prevent. The stale entry stays in place until the next
session switch.

The ticker skips off-screen running cards entirely, so
refreshRunningShellRunElapsed returns false when no visible card needs an
update and the 1s interval stops.
Codex review found three issues in the initial freeze approach:

P1: Freezing an entry whose first line is in scrollback but whose tail
is still visible stopped streaming text from appearing. The invariant
is that scrollback lines are immutable, not that the entire entry is
frozen. Now only entries sitting entirely above the viewport (firstLine
+ height <= viewportTop) are frozen; straddling entries re-render
normally — append-only changes fall inside the viewport, so pi-tui's
firstChanged never touches scrollback.

P2: The ticker skip stopped the 1s interval when all running cards were
off-screen, but a resize making them visible again never restarted it.
Removed the skip: the render freeze already prevents visual changes for
off-screen cards, and the extra render cycle is negligible.

P2: The splice guard left off-screen poll cards as stale 'running'
entries. A width-change full redraw would re-render them as duplicate
running cards. Now the guard also updates the entry's status to 'done'
with the folded result, so a future full redraw renders it correctly.

Added a streaming-past-viewport test that verifies a long assistant
reply keeps appending visible content after crossing the viewport
boundary, with no ESC[3J.
…review

Two findings from the second Codex review:

1. Zero-height cache gap: a blank thinking_delta renders zero lines; when
   pushed off-screen and then thinking_complete writes non-empty text, the
   entryHeight=0 check prevented the freeze, inserting new lines into
   scrollback. Now entries whose first line is above the viewport are
   frozen regardless of cached height.

2. Poll card duplication: off-screen Read/StopBackgroundTask poll cards
   that were kept in place (to avoid line-number shifts) but marked done
   would re-render as duplicate cards on a width-change full redraw. Now
   they get a hidden flag that makes them contribute zero lines, so a
   future full redraw skips them entirely. They are cleaned on the next
   session switch.
@Astro-Han
Astro-Han merged commit 3f89ca0 into main Jul 16, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the fix/cli-offscreen-mutation-freeze branch July 24, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli): in-place mutations of off-screen entries still trigger scrollback-clearing redraws

1 participant