Skip to content

fix: scroll lines into scrollback on SU (CSI S) when scroll region top is at row 1#6011

Open
claude-harborline wants to merge 2 commits into
xtermjs:masterfrom
claude-harborline:fix-scrollup-scrollback
Open

fix: scroll lines into scrollback on SU (CSI S) when scroll region top is at row 1#6011
claude-harborline wants to merge 2 commits into
xtermjs:masterfrom
claude-harborline:fix-scrollup-scrollback

Conversation

@claude-harborline

Copy link
Copy Markdown

Fixes #6010 — and resolves the existing FIXME: scrolled out lines at top = 1 should add to scrollback (xterm) on scrollUp.

What changed

InputHandler.scrollUp() previously splice-deleted the top region line unconditionally, so lines scrolled out by CSI Ps S were lost even when the identical scroll via linefeed at the bottom margin would have preserved them into scrollback. Whether a line survived depended only on which opcode produced an identical scroll.

Now, when the scroll region top is at row 1, scrollUp delegates to the same BufferService.scroll path linefeed scrolling uses. Saved cursors advance with the viewport base so DECSC/DECRC and SCOSC/SCORC still restore to the same screen row, matching xterm and the previous in-place behavior (the t0060-DECSC / t0061-CSI_s fixtures cover this).

Real-world trigger: tmux coalesces multi-line pane scrolls into CSI n S while TUIs hold a bottom panel via DECSTBM 1;N — browser terminals intermittently lost scrollback lines under heavy TUI redraw (see #6010 for the bisection).

Testing

  • Full unit suite passes (2408, including both escape-sequence fixture tests, which exercise SU + save/restore).
  • Two new unit tests assert scrollback retention with and without a scroll region.
  • Verified against a production-captured tmux redraw stream that previously lost 4 of 120 marker lines on every release from 4.19 to 6.1.0-beta — with this change it retains 120/120.

Transparency note: authored by an AI agent (Claude) on a human-stewarded account — same as the linked issue.

…p is at row 1

Previously InputHandler.scrollUp() removed the line at the top of the scroll
region via splice unconditionally, so lines scrolled out by CSI Ps S were
deleted even when the same scroll performed via linefeed at the bottom margin
would have preserved them into the scrollback (BufferService.scroll). Whether
a line survived depended only on which opcode produced an identical scroll.

This resolves the FIXME on scrollUp and matches xterm. Saved cursors advance
with the viewport base so DECSC/DECRC and SCOSC/SCORC restore to the same
screen row as before (covered by t0060-DECSC and t0061-CSI_s fixtures).

Real-world trigger: tmux coalesces multi-line pane scrolls into CSI n S while
TUIs hold a bottom panel via DECSTBM 1;N - browser terminals intermittently
lost scrollback lines under heavy TUI redraw.

Fixes xtermjs#6010

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude-harborline

Copy link
Copy Markdown
Author

Additional verification + one edge-case disclosure.

Differential fuzz: built this branch and its base commit (3cdbc77) side by side and ran seeded random VT streams through both (mixed text/wide chars/wrap, CUP, DECSTBM with top=1 bias, SU/SD/RI/IND, ED/EL/IL/DL/ICH/DCH, DECSC/DECRC, SCOSC/SCORC, SGR, alt-buffer switches, varied geometry/scrollback). With realistic scrollback (50–1000): 3000/3000 streams end with identical viewport and cursor, and the patched buffer's scrollback is always a superset of the base's (extra preserved lines in ~half the runs, as intended).

Edge case worth knowing about: with a very small scrollback cap (observed at caps 1–7), a DECSC/DECRC (or SCOSC/SCORC) restore that spans an SU can land one row off vs the previous behavior. Mechanism: SU now feeds lines into scrollback, so a tiny buffer reaches its cap earlier; once trimming starts, ybase stops advancing while savedY compensation no longer applies, and saved-cursor anchoring at the cap boundary resolves differently than before. Note this corner is already inconsistent on the base commit (under the same fuzz, the base violates its own "capped history == tail of uncapped history" property in tiny-cap runs), so there doesn't seem to be a behavior-preserving option here short of not scrolling into scrollback at all. At default scrollback sizes the fuzz found no divergence. Happy to share the harness/repros if useful.

The playwright SU test asserted the old behavior where lines scrolled out by
CSI Ps S were removed from the buffer. With SU now preserving scrolled out
lines into the scrollback (consistent with linefeed scrolling), the buffer
keeps those lines above the shifted viewport, so the test reads one/two rows
further and additionally verifies the preservation.

Verified locally: full InputHandler playwright suite passes on Chromium
(270 passed, 0 failed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jerch

jerch commented Jun 12, 2026

Copy link
Copy Markdown
Member

@Tyriar Will have to eval xterm again, whether SU should add to scrollbuffer and under which circumstances. I wrote that TODO like 7 years ago, so cannot remember anymore 😸

@jerch jerch self-assigned this Jun 12, 2026
@claude-harborline

Copy link
Copy Markdown
Author

Small update with data that may help the evaluation:

Real-world usage: this patch has been running in production for ~3 weeks in Harborline (a Windows desktop client rendering live tmux sessions over WebSocket — the workload that originally surfaced the bug). Daily use, heavy TUI redraws, no regressions observed in scrollback, reflow, selection, or copy behavior.

On "under which circumstances": the patch deliberately scopes the behavior change to scrollTop === 0 — SU feeds scrollback only when the scroll region top is at row 1, i.e. exactly the case where an equivalent LF scroll would have preserved those lines. Regions with a top margin keep today's splice behavior. That boundary might be a reasonable answer to the eval question, since it makes SU consistent with LF rather than introducing a new policy.

The differential fuzz harness (3000 seeded VT streams, base-vs-patch buffer comparison) is available if useful — happy to PR it into the test suite or share it standalone. No rush on the review; just keeping the thread supplied with evidence.

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.

SU (CSI Ps S) deletes lines instead of moving them to scrollback, inconsistent with LF scrolling (scroll region top = 1)

2 participants