Skip to content

A reattach stops disarming the modes a live program still holds - #120

Merged
karngyan merged 1 commit into
mainfrom
worktree-live-reattach-keeps-modes
Aug 19, 2026
Merged

A reattach stops disarming the modes a live program still holds#120
karngyan merged 1 commit into
mainfrom
worktree-live-reattach-keeps-modes

Conversation

@karngyan

Copy link
Copy Markdown
Contributor

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.hasScrollback branch). 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 ?1049h plus 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. stopReporting and reportsPointer leave the emulator seam with their only caller — reportsPointer existed only to test the reset — along with the replaying bookkeeping that existed only to fire it.

Regression test: a replay carrying ?1003h must drain with nothing appended after it.

Testing

  • pnpm vitest run — 88 files, 1837 tests pass
  • pnpm run lint — clean
  • TDD: regression test watched failing against the old code before the fix

🤖 Generated with Claude Code

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>
@karngyan
karngyan merged commit 07fd324 into main Aug 19, 2026
1 check passed
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.

1 participant