Skip to content

A revived session stops typing the mouse at its own prompt, and the mobile controls stop shouting - #67

Merged
karngyan merged 2 commits into
mainfrom
fix/mobile-regressions
Aug 12, 2026
Merged

A revived session stops typing the mouse at its own prompt, and the mobile controls stop shouting#67
karngyan merged 2 commits into
mainfrom
fix/mobile-regressions

Conversation

@karngyan

Copy link
Copy Markdown
Contributor

What was happening

Every pointer move over a terminal put a run of 35;61;22M35;61;21M… at the shell prompt; on a phone every touch did the same.

Those are SGR mouse reports with the ESC[< eaten by the prompt. Cb=35 is 32 | 3 — motion, no button — which needs any-event tracking (?1003h). So the browser terminal was reporting the pointer to a plain shell.

Why

A snapshot's ring is bytes, not state. Registry.Revive preloads those bytes into a fresh shell, so replaying one re-runs every mode change the dead shell ever wrote — including the sequence that turned mouse tracking on inside a program that was killed with the daemon and therefore never wrote the sequence that turns it off. The client replays that, ends the replay armed, and reports the pointer to a shell that has no idea what an SGR report is.

This is the third member of a family flue already knew about: answerQueries and the replay mute gate exist because a replay also re-runs device queries.

The fix, at both ends

Daemon. Revive writes a reset between the replayed scrollback and its marker: mouse tracking in every protocol and encoding, focus reporting, bracketed paste, application cursor keys, and the display state (scrolling region, charset, autowrap, cursor visibility) that would otherwise make the new shell unreadable rather than merely noisy.

Client. Emulator.stopReporting() clears mouse and focus reporting the moment a replayed backlog has drained — which covers the other way in, a program killed mid-session with no restart involved. Only those two, deliberately: application cursor keys and bracketed paste are stale in the same way, but they change what a keystroke means rather than inventing keystrokes, so clearing them against a live program would break arrows and pastes in a client that had nothing wrong with it.

The alternate screen is deliberately not left. A dead full-screen program will have been in it, but swapping in a main buffer whose contents were evicted from the ring long ago trades a tidy terminal for an empty one where somebody's scrollback used to be.

Known cost: a browser attaching to a session where a mouse-driven TUI is live loses mouse reporting in that client until the program re-enables it.

And the controls

#66 took the floating chips to 48px squares and the key bar to eight tall chips. Those no longer fit, so the bar became a scrolling strip across the bottom of the screen — and every chip fires on pointerdown, so dragging that strip sent whatever key the thumb started on. The paste chip fired the clipboard. All reverted to the pre-#66 sizes.

Paste goes back to the platform. xterm keeps its input in a real textarea but ships it zero-sized and parked off-page, so a long press has nothing to land on and no phone offers a paste menu over a div. On a coarse pointer that element now gets the terminal's own box, so the press finds editable text and the paste travels xterm's own path — which is what keeps newline handling and bracketed-paste mode right. font-size: 16px on it is a threshold and not a size: below that iOS zooms the page on focus.

Tests

  • make test — 1329 web, 163 relay, every Go package green
  • make lint

New coverage: TestReviveSettlesTheModesTheDeadShellLeftBehind pins the reset and its ordering against the replay; emulator tests pin stopReporting clearing a replayed arm and leaving a live one alone; terminal tests pin that it lands after the backlog drains, never mid-backlog, and never on a fresh spawn; styles.build.test.ts pins the long-press rule surviving a real build with its !important intact.

Needs a device check before merge

The long-press paste is the one thing I could not verify. It is a platform behaviour, so it needs a real phone — please check on yours that a long press over the terminal offers Paste, that typing and touch-scrolling still work, and that tapping does not zoom the page. If it misbehaves, the CSS block in styles.css is self-contained and reverting it costs nothing else.

🤖 Generated with Claude Code

karngyan and others added 2 commits August 12, 2026 21:00
… prompt

A session that came back from a daemon restart typed at itself. Every
pointer move over the terminal put a run of "35;61;22M35;61;21M…" at the
shell prompt, and on a phone every touch did the same.

Those are SGR mouse reports. A snapshot's ring is bytes, not state, so
reviving one re-runs every mode change the dead shell ever wrote —
including the sequence that turned mouse tracking on inside a program
that was killed with the daemon and so never wrote the sequence that
turns it back off. The client replays that, ends the replay armed, and
reports the pointer to a brand new shell that has no idea what an SGR
report is.

Fixed at both ends, because either alone leaves a hole. Revive now writes
a reset between the replayed scrollback and its marker, which covers the
modes a dead program can strand: mouse tracking in every protocol and
encoding, focus reporting, bracketed paste, application cursor keys, and
the display state — scrolling region, charset, autowrap, cursor
visibility — that would otherwise make the new shell unreadable rather
than merely noisy. And the client clears mouse and focus reporting the
moment a replayed backlog has drained, which covers the other way in: a
program killed mid-session, with no restart involved. Only those two
there, deliberately. Application cursor keys and bracketed paste are
stale in the same way but change what a keystroke means rather than
inventing keystrokes, so clearing them against a live program would break
arrows and pastes in a client that had nothing wrong with it.

The alternate screen is deliberately not left. A dead full-screen program
will have been in it, but swapping in a main buffer whose contents were
evicted from the ring long ago would trade a tidy terminal for an empty
one where somebody's scrollback used to be.

Also undoes two things #66 did to the controls. The floating chips went to
48px squares and the key bar to eight tall chips, which no longer fit and
turned the bar into a scrolling strip across the bottom of the screen —
and every chip on it fires on pointerdown, so dragging that strip sent
whatever key the thumb started on. The paste chip fired the clipboard.

Paste goes back to the platform. xterm keeps its input in a real textarea
but ships it zero-sized and parked off-page, so a long press has nothing
to land on and no phone will offer a paste menu over a div. On a coarse
pointer that element now gets the terminal's own box, so the press finds
editable text and the paste travels xterm's own path, which is what keeps
newline handling and bracketed-paste mode right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The long-press box gave xterm's input element the terminal's own
dimensions, and a browser rings a focused control to say where typing
will land — so the ring became a blue line around the whole terminal for
as long as the session had focus.

Dropped, and nothing is owed for it. The element is a proxy: what the
ring would announce is already announced by the block cursor blinking in
the cells. It had been focused and outlined all along, parked off-page at
zero size, where nobody could see either.

Safari's tap highlight is separate and ignores `outline`, so it goes too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@karngyan
karngyan merged commit 7a0a0c1 into main Aug 12, 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