Fix stale frames on Windows when output exactly fills the terminal#971
Merged
sindresorhus merged 2 commits intoJun 12, 2026
Merged
Conversation
ansi-escapes resolves clearTerminal per process: the win32-spoofed fixture on a Linux kernel emits the legacy variant while the test process counts the modern one. Count the shared eraseScreen prefix instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #969
Problem
On Windows, an app whose output is exactly the height of the terminal leaves a stale copy of the previous frame behind on every rerender. The duplicates pile up above the UI as the app repaints. The reporter of #969 hit this through qwen-code, which upgraded ink 6 to 7 in its 0.16.0 release and whose UI sizes itself to fill the terminal (screenshots of the accumulation are in their report on the qwen-code tracker).
Root cause
Ink 6 wrote
clearTerminalbefore every frame wheneveroutputHeight >= rows. 32d96a6 narrowed that to strict overflow (>) so that frames exactly at terminal height rerender through the incrementaleraseLinespath instead, which fixed the fullscreen flicker. That path moves the cursor uppreviousLineCount - 1rows and assumes nothing scrolled in between.That assumption does not hold on Windows. Windows consoles scroll the buffer when the bottom-right cell is written, while xterm-like terminals defer the wrap until the next character. A fullscreen frame with a full-width bottom line (separators, status bars) therefore scrolls one row per repaint, the cursor-up arithmetic lands one row off, and one stale line of the previous frame leaks out the top on every render.
I verified the mechanism by capturing the raw bytes ink 6.8.0 and 7.0.5 emit for the same fullscreen app in a fixed-size PTY and replaying both streams through a terminal model with each wrap semantic: under deferred wrap both are clean, under the Windows scroll behavior ink 6 stays clean (the per-frame clear wipes any drift) while ink 7 leaks one stale line per repaint.
Fix
On Windows only, treat fullscreen-height frames the way ink 6 did: full clear between frames. Non-Windows platforms keep the incremental path and the flicker fix from 32d96a6. Windows is also the one platform where the flicker tradeoff is moot, since the clear-vs-incremental flicker difference is dwarfed by the broken output.
I could not verify on physical Windows hardware, only against the documented console behavior and the byte-stream replay above. The fix restores the exact write pattern ink 6 used on Windows for years, so the risk is the known ink 6 behavior. Happy to adjust if you'd rather gate this differently.
Test
The new fixture reuses the #450 rerender harness with
process.platformoverridden towin32before ink loads, so the Windows branch is exercised on the Linux CI matrix. The test asserts aclearTerminalper fullscreen rerender (the inverse of the #450 incremental assertion) and fails without the src change.tsc --noEmit,xo, and the full ava suite pass (1019 tests, the 4 pre-existingtest.failingcases unchanged).