Skip to content

Stop replaying <Static> output on full-clear frames - #1005

Merged
sindresorhus merged 5 commits into
vadimdemedes:masterfrom
costajohnt:static-no-replay
Sep 20, 2026
Merged

sindresorhus merged 5 commits into
vadimdemedes:masterfrom
costajohnt:static-no-replay

Conversation

@costajohnt

@costajohnt costajohnt commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #994. Since the full-clear path preserves scrollback, it still rewrites the accumulated <Static> output (fullStaticOutput) after every clear. Each overflowing rerender therefore stamps another copy of the whole static history into scrollback and rewrites all of it to the terminal. This is the "duplicate stamping" described in #990, and it is not in a release yet (7.1.1 predates #994).

Reproduced by running a fixture in a pty and replaying the bytes through xterm.js headless (8 rows, a <Static> with 3 items, a 12-line live region, 3 rerenders): master leaves 5 copies of the static lines in the terminal buffer; with this change, one.

Change

The full clear now erases only the previous live frame and never replays static output:

  • When the previous frame filled the viewport (lastOutputHeight >= viewportRows), homeAndEraseDown already erases exactly that frame, and it keeps the absolute erase that Windows consoles need (Ink 7 rendering broken on all Windows terminals聽#969).
  • When it did not (a small frame growing past the viewport), erase it relative to the cursor via log.clear() instead, so <Static> and console rows that are still in the viewport survive and scroll into history as the new frame is written.
  • fullStaticOutput is only accumulated in debug mode now. A boolean keeps the unmount guard from Static + exit results in broken/mismatched output聽#397.
  • New <Static> output from the current frame is still written once, ahead of the frame, in both erase branches. The full clear previously dropped it.

Behavior change

When the live region shrinks from fullscreen back to a small frame, the frame is written at the top of an empty viewport; the static log is one page up in scrollback. Before, the replay made the viewport look continuous (static tail + frame) at the cost of duplicating the log. If the continuous look is preferred, replaying only the last rows - frameHeight static lines would keep it without scrolling anything into history, at the cost of one bounded duplicate per shrink. Happy to switch to that.

Tests

full clears do not replay <Static> output into scrollback and growing into an overflowing frame keeps <Static> rows that are still in the viewport, plus <Static> items added while the frame overflows are written once, for standard and incremental rendering, using reconstructTerminalLines. All fail on master. The existing #450, #935 and #969 tests still pass.

Since the full-clear path started preserving scrollback (vadimdemedes#994), it still
rewrote the accumulated `<Static>` output after every clear. Each
overflowing rerender therefore stamped another copy of the whole static
history into scrollback and rewrote it to the terminal.

Erase only the previous frame instead. When it filled the viewport,
home + erase-down already erases exactly that frame. When it did not (a
small frame growing past the viewport), erase it relative to the cursor
so `<Static>` and console rows still in the viewport survive and scroll
into history naturally. `fullStaticOutput` is now only accumulated in
debug mode; a boolean keeps the unmount guard from vadimdemedes#397.
Dropping the `fullStaticOutput` replay also dropped the only write of
the current frame's `staticOutput` on the full-clear path, so any
`<Static>` item added while the live region overflowed the viewport
never reached the terminal. Write it once, ahead of the frame, in both
erase branches.

Test: `<Static>` items added while the frame overflows are written once,
for standard and incremental rendering. Fails without the fix.
@sindresorhus

Copy link
Copy Markdown
Collaborator

Could we keep replaying the accumulated <Static> output in alternate-screen mode?

This works on the primary screen because previously rendered static lines remain in scrollback. The alternate screen has no scrollback, though. If the live frame overflows and later shrinks, <Static> has no new output to emit, so the full clear writes only the live frame and the existing static content disappears permanantly. The previous implementation restored it from the accumulated output.

I think the simplest fix is to keep accumulating and replaying the full static output only when the alternate screen is active, while retaining the new write-once behavior for the primary screen. Otherwise we would need to explicitly document <Static> with overflowing alternate-screen output as unsupported, which would be a regression from the current behavior.

The alternate screen has no scrollback, so <Static> rows that a full clear erases or an overflowing frame pushes off the top are gone for good. Keep accumulating the static output there and replay it ahead of the frame on every full clear. The primary screen still writes each item once.
@costajohnt

Copy link
Copy Markdown
Contributor Author

Yes, that was a regression on the alternate screen. Fixed by keeping the accumulation and replay only when alternateScreen is on: renderFrame appends to fullStaticOutput in that mode and the full-clear path writes homeAndEraseDown + fullStaticOutput + frame as before. The primary screen keeps the write-once behavior.

Added two tests for standard and incremental rendering: the frame grows past a 5-row viewport and shrinks back with three static items, asserting the reconstructed screen is S1, S2, S3, live; and static items added while the frame overflows, asserting every full clear writes them once in order. Both fail on the previous commit.

One pre-existing gap, not from this PR: resuming from suspend re-enters the alternate screen and redraws only the frame, so static rows are lost until the next full clear. Happy to do that as a follow-up if you want it.

@sindresorhus

Copy link
Copy Markdown
Collaborator

One pre-existing gap, not from this PR: resuming from suspend re-enters the alternate screen and redraws only the frame, so static rows are lost until the next full clear. Happy to do that as a follow-up if you want it.

馃憤

@sindresorhus
sindresorhus merged commit d661543 into vadimdemedes:master Sep 20, 2026
2 checks passed
@costajohnt
costajohnt deleted the static-no-replay branch September 20, 2026 20:38
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.

2 participants