Skip to content

Fix scrollback not accumulating for top-aligned scroll regions#33

Open
L-jasmine wants to merge 1 commit into
doy:mainfrom
second-state:fix/scrollback-top-aligned-region
Open

Fix scrollback not accumulating for top-aligned scroll regions#33
L-jasmine wants to merge 1 commit into
doy:mainfrom
second-state:fix/scrollback-top-aligned-region

Conversation

@L-jasmine

Copy link
Copy Markdown

Problem

Grid::scroll_up discards scrolled-off rows whenever a scroll region is set, so they never reach the scrollback buffer:

if self.scrollback_len > 0 && !self.scroll_region_active() {
    self.scrollback.push_back(removed);
    ...
}

scroll_region_active() is true whenever the region isn't the full screen (scroll_top != 0 || scroll_bottom != rows - 1). So any scroll-region-scoped scrolling — even when the region includes the top of the screen — drops the removed row instead of saving it.

Per xterm, a line that leaves the top of the screen should enter the scrollback whenever the scroll region includes the top of the screen (scroll_top == 0), regardless of scroll_bottom.

How I hit this

Discovered while running OpenAI Codex CLI inside a terminal built on this crate. Codex's inline TUI pushes finalized chat history into the host terminal's scrollback using a top-aligned scroll region — SetScrollRegion(1..viewport_top) followed by \r\n and reverse-index (ESC M) — see codex-rs/tui/src/insert_history.rs, and the related discussion in openai/codex#10331. With this crate that history was silently dropped and the scrollback stayed empty — there was nothing to scroll back to.

Plain full-screen output (e.g. cat) is unaffected, since no scroll region is set.

Fix

Gate the save on "the region includes the screen top":

if self.scrollback_len > 0 && self.scroll_top == 0 {
  • Full-screen scrolling: unchanged (scroll_top == 0 holds, identical to before).
  • Region with scroll_top == 0, scroll_bottom < last (the inline-TUI case): now saved to scrollback (previously dropped).
  • Region with scroll_top > 0 (doesn't reach the screen top): still not saved (lines scroll into the area above the region, not scrollback) — unchanged.

The now-unused private helper scroll_region_active is removed.

Tests

Adds scrollback_in_top_aligned_region in tests/scroll.rs: sets a 1..4 scroll region, writes lines with newlines, and asserts 3 rows land in the scrollback. It fails on main and passes with this change. The full cargo test suite is green.

🤖 Generated with Claude Code

scroll_up used `!scroll_region_active()` to decide whether a scrolled-off
row is pushed to scrollback, dropping the row whenever ANY scroll region was
set. Inline TUIs that insert finalized history via SetScrollRegion(1..N) +
\r\n (e.g. codex's standard history-insertion mode) therefore never built
any scrollback. Per xterm, a line leaving the screen top enters scrollback
as long as the scroll region includes the top of the screen (scroll_top==0),
regardless of scroll_bottom. Restricted the condition accordingly; full-screen
scrolling is unchanged.
L-jasmine added a commit to second-state/vibetty that referenced this pull request Jul 12, 2026
…C[6n

vt100: patch Grid::scroll_up via [patch.crates-io] ->
second-state/vt100-rust @ a34c13a, so a scrolled-off row enters the
scrollback when the scroll region includes the screen top (scroll_top == 0).
Upstream's `!scroll_region_active()` dropped it whenever any scroll region
was set, so codex's inline history insertion (SetScrollRegion(1..viewport_top)
+ \r\n / reverse-index) never built scrollback — nothing to scroll back to.
Upstream PR: doy/vt100-rust#33.

pty: only answer the ESC[6n cursor-position report that Windows ConPTY sends
in its first output chunk (ConPTY blocks without a reply). Drop the full
DA1/DA2/DSR query answering — empirically codex and other TUIs render fine
without those replies.

Co-Authored-By: Claude <noreply@anthropic.com>
@ReagentX

Copy link
Copy Markdown

I independently reproduced this issue and verified both the diagnosis and the proposed fix.

Context

I maintain fleetcom, a PTY supervisor built on this crate. Codex CLI sessions running under fleetcom retained no scrollback.

I traced the behavior to the same condition in Grid::scroll_up before finding this PR. Codex’s inline TUI commits finalized history through a top-anchored scroll region:

CSI 1;{viewport_top} r

It then emits one \r\n per line while the cursor is at the bottom of that region. Under the current gate, each finalized history line is discarded instead of being added to scrollback.

Minimal Reproduction

This reproduces the problem against 0.16.2 without requiring Codex:

// Codex-style history insertion: DECSTBM 1..20 on a 24-row screen,
// cursor parked at the region bottom, one \r\n per history line.
let mut p = vt100::Parser::new(24, 80, 10_000);
p.process(b"\x1b[1;20r\x1b[20;1H");

for i in 0..30 {
    p.process(format!("\r\nhistory {i}").as_bytes());
}

p.process(b"\x1b[r");
p.screen_mut().set_scrollback(usize::MAX);

assert_eq!(p.screen().scrollback(), 0); // expected 30

Stock 0.16.2 drops all 30 rows. With this PR’s one-line change, the same input retains all 30.

Real-Session Verification

As a sanity check, I captured 18 KB of raw output while resuming a long Codex session under a 40×120 PTY.

The capture contained:

  • Seven DECSTBM updates: 1;33, 1;35, and so on
  • No alternate-screen activation
  • No mouse capture

Replaying the same capture produced the following results:

Implementation Retained scrollback
Stock 0.16.2 0 rows
This PR’s change 85 rows

Emulator Behavior

The proposed rule is also consistent with other terminal emulators: retain history when the scroll region begins at row 0. The bottom margin is irrelevant.

  • xterm.js: The scrollback push is gated on buffer.scrollTop === 0. The alternate branch states that when scrollTop is nonzero, no line enters scrollback (BufferService.ts:82).
  • Alacritty: “Only rotate the entire history if the active region starts at the top,” implemented as if region.start == 0 { self.increase_scroll_limit(...) } (grid/mod.rs:271–274, v0.16.1).
  • WezTerm: let scrollback_ok = scroll_region.start == 0 && self.allow_scrollback; (screen.rs:655).

As a result, a vt100-based multiplexer hosting an inline-viewport TUI can silently discard finalized history. This affects Codex today and can affect any TUI that inserts history above a ratatui-style viewport.

@L-jasmine

Copy link
Copy Markdown
Author

@ReagentX — thank you for the detailed writeup! The independent reproduction, the real-session capture (0 → 85 rows), and the cross-references to xterm.js / Alacritty / WezTerm are all super valuable. It is great to have the fix validated against a real fleetcom workload, and the Codex case is a strong concrete motivator.

As a fun aside: this bug was actually tracked down and the patch written within about half an hour, pairing GLM-5.2 with Claude Code — so it is especially nice to see it hold up under your independent test.

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.

3 participants