A reattach stops disarming the modes a live program still holds - #120
Merged
Conversation
Every backlog replay ended with stopReporting(), which writes the mouse-tracking and focus-reporting resets into the local emulator. That was aimed at revived sessions, whose dead shell never wrote its own reset — but it fired on every reattach, including one to a live session whose program legitimately holds those modes armed. With Claude Code's renderer that combination is poisonous: it holds the alternate screen plus mouse tracking, and an emulator sitting on the alt buffer with tracking cleared is exactly the state xterm.js answers by turning every wheel tick into arrow keys at the program's stdin. Leave a session, come back, reattach replays, the clear lands — and scrolling is broken until the program's next full redraw happens to re-assert its modes, minutes of arrow-key noise later. Verified against a live ring: the replayed tail held ?1049h and ?1000-1006h with re-assertions hundreds of kilobytes apart. A live session's replay ends at the program's present state, so the right amount of client-side settling is none. The revived case never needed the client's help: the daemon ends every revive preload with settleModes, so the dead shell's reset is already in the replayed bytes. stopReporting and reportsPointer leave the emulator seam with their only caller — reportsPointer existed only to test the reset. 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.
The bug
"Scroll wheel is sending arrow keys · use PgUp/PgDn to scroll" after leaving a session idle and coming back — and no way to scroll at all.
Every backlog replay ended with
emulator.stopReporting(), which clears mouse tracking and focus reporting in the local emulator. That call was aimed at revived sessions (PR #67), whose dead shell never wrote its own reset. But it fired after every replay, including a reattach to a live session whose program still holds those modes armed.Claude Code's renderer holds the alternate screen plus full mouse tracking (
?1049h ?1000h ?1002h ?1003h ?1006h). Normally a wheel tick becomes an SGR mouse report and Claude scrolls its own transcript. After the reattach clear, the emulator sits on the alt buffer (no scrollback) with tracking off — the exact state xterm.js answers by converting every wheel tick into arrow keys at the program's stdin (CoreBrowserTerminal.ts, the!buffer.hasScrollbackbranch). Claude sees arrow bursts, shows the hint, and scrolling stays broken until its next full redraw happens to re-assert the modes.Evidence
Dumped the live session's ring through its holder socket. The replayed tail held an unmatched
?1049hplus tracking arms, with Claude's re-assertions landing hundreds of kilobytes apart — minutes of broken scrolling per reattach. A fresh tab attaching to the same session reproduced the stuck state instantly, which cleared the client-side leak theory: the replay tail is the program's true present state, and the client must not overrule it.The fix
Delete the clear. A live session's replay ends at the program's present state, so the right amount of client-side settling is none. The revived case never needed the client's help: the daemon ends every revive preload with
settleModes(internal/session/snapshot.go), so a dead shell's reset is already inside the replayed bytes.stopReportingandreportsPointerleave the emulator seam with their only caller —reportsPointerexisted only to test the reset — along with thereplayingbookkeeping that existed only to fire it.Regression test: a replay carrying
?1003hmust drain with nothing appended after it.Testing
pnpm vitest run— 88 files, 1837 tests passpnpm run lint— clean🤖 Generated with Claude Code