Fix stale rows when repainting in place while scrolled back - #620
Merged
migueldeicaza merged 2 commits intoAug 7, 2026
Merged
Conversation
The terminal's update range is recorded in buffer.y space, relative to yBase. updateDisplay turns it into an invalidation rect as if buffer row y were screen row y, but drawTerminalContents maps screen rects back to buffer rows through yDisp. Those agree only while the viewport is pinned to the bottom. Scrolled back by k rows, a change at buffer row y renders at screen row y + k while the invalidation still covers screen row y, so the rows that actually changed are never repainted and keep stale content until a resize or another scroll forces a full redraw. Scrolling output masks it, since scroll() dirties both scrollTop and scrollBottom and the rowEnd == rows - 1 branch then invalidates the whole view anyway. What stays exposed is in-place repainting, where the update range remains a strict subset: scroll up a few lines while a TUI redraws a status block and the visible rows stop tracking the buffer. Invalidate the whole view when yDisp != yBase. The pinned case keeps the existing partial rect untouched, including the restricted-region extension from migueldeicaza#582, and the draw still repaints only the rows intersecting the dirty rect, each read from its correct yDisp-relative line.
Owner
|
This looks good, just a couple of small edits coming in in a second |
Owner
|
Thank you for your contribution! |
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.
Problem
The terminal's update range is recorded in
buffer.yspace, relative toyBase.updateDisplayturns it into an invalidation rect as if buffer rowywere screen rowy, butdrawTerminalContentsmaps screen rects back to buffer rows throughyDisp:Those agree only while the viewport is pinned to the bottom. Scrolled back by
krows, a change at buffer rowyrenders at screen rowy + kwhile the invalidation still covers screen rowy, so the rows that actually changed are never repainted. They stay stale until a resize or another scroll forces a full redraw.Scrolling output masks it:
scroll()ends withupdateRange(scrollTop)andupdateRange(scrollBottom), so on a full-height screen rows 0 androws-1are both dirty and therowEnd == rows - 1branch invalidates everything anyway. What stays exposed is in-place repainting, where the update range remains a strict subset. Agent TUIs that redraw a status block several times a second (Claude Code, Codex) hit this constantly: scroll up a few lines while one is working and the visible rows stop tracking the buffer.The alternate screen is unaffected, it has no scrollback so
yDisp == yBasealways.Fix
Invalidate the whole view when
yDisp != yBase.The pinned-to-bottom case keeps the existing partial rect untouched, including the restricted-region extension from #582. It is less blunt than it reads:
drawTerminalContentsstill repaints only the rows intersecting the dirty rect, each read from its correctyDisp-relative line.CoreGraphics path only. Metal builds its own
metalDirtyRangea few lines below and is not affected.Tests
Added a regression test: 20-row view, scrolled back 3 rows, cursor parked on row 2, then a write that neither scrolls nor moves the cursor. Update range is
(2,2), and before the fix the invalidated rect is[256, 288]while the change renders at[224, 240]. Fails on main, passes with the fix. Rest of the suite is green.