Conversation
flushPendingWrites() captured the viewport of a user who was reading scrollback, called terminal.write(), and restored the anchor on the next line. xterm parses on its own schedule, so at that point the buffer has not moved: the guard `viewportY !== preserveViewportY` was false, scrollToLine was never called at all, and the Codex redraw landed a tick later and took the viewport to the live bottom with nothing left to pull it back. Scrolling up during a stream still got dragged down, which is what #358 reports, and a refresh was the only way back to a coherent view. The restore moves inside xterm's write callback, the first moment the redraw's effect exists, and runs before _scheduleTerminalWriteFlush() so a deferred remainder re-captures the restored anchor rather than the bottom. Two things follow from it running later: - A live anchor now wins over the sticky scroll-to-bottom. The two are captured at different moments (_wasAtBottomBeforeWrite at the frame's first batchTerminalWrite, the anchor at flush time), so a scroll-up in between leaves both set, and running both would jump to the bottom and come back a frame later instead of staying put. - The anchor is dropped if the active session changed or a buffer load started while the write was in flight. It indexes the buffer it was captured from, and selectSession() resets the terminal and chunk-loads a different scrollback. The existing regression passed throughout, because its write mock moved the viewport synchronously, which real xterm never does. The harness now models an asynchronous parse (redraw lands, then the callback fires), and all five of the anchor tests fail against the old code. Fixes #358 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7d02423 to
de864e7
Compare
|
Rebased onto Full gate on the rebased branch, run on an otherwise idle machine: 383 files passed, 0 failed, 7256 tests passed, 12 skipped. CI on the pushed head is green on both jobs. Worth noting for the record, since it affected my two earlier runs: each of those reported one failed test file with zero failed tests, a different file each time ( |
Fixes #358.
The bug
flushPendingWrites()captured the viewport of a user reading scrollback, calledterminal.write(), and restored the anchor on the very next line:xterm parses on its own schedule, so at that moment the buffer has not moved.
viewportYstill equals the anchor, the guard is false, andscrollToLineis never called at all. The parse lands a tick later, a cursor-addressed Codex redraw takes the viewport to the live bottom, and nothing is left to pull it back. That is the timing gap the issue describes, and it is why a refresh was the only way back to a coherent view.The existing regression passed the whole time because its mock moved the viewport synchronously inside
write(), which real xterm never does. A test that models the renderer wrongly is a test that certifies the bug.The fix
The restore moves into xterm's write callback, the first moment at which the redraw's effect exists, and runs before
_scheduleTerminalWriteFlush()so a deferred remainder re-captures the restored anchor rather than the bottom.Two consequences of it running later, both handled:
_wasAtBottomBeforeWriteat the frame's firstbatchTerminalWrite, the anchor at flush time), so a scroll-up in between leaves both set. Previously the sync restore made that harmless; now running both would jump to the bottom and come back a frame later instead of simply staying put.selectSession()resets the terminal and chunk-loads a different scrollback, so replaying row 40 into that one would be a jump to an arbitrary place rather than a restore. This window did not exist while the restore was synchronous.Tests
test/terminal-flush-budget.test.tsgrows a shared harness whosewrite()parses asynchronously the way xterm does (the redraw lands, then the callback fires), and seven cases on top of it:Five of them fail against master, including the rewritten original. Full gate green on this branch, rebased onto 1.29.0 and run on an idle machine: 383 files passed, 0 failed, 7256 tests passed.
Not in this PR
The issue also asks for a browser smoke test using synchronized, cursor-addressed redraws while scrolled up. That suite (
test/browser) is excluded from the CI gate, so a guard living only there would be invisible to CI, which is the blind spot that let two semantically conflicting mobile PRs merge green once before. The vm-level tests here drive the realflushPendingWrites()against a faithful model of xterm's async parse, and CI can actually see them.