Skip to content

feat(web): restore tap/click/wheel mouse interaction when server strips mouse DECSETs#144

Merged
Ark0N merged 3 commits into
Ark0N:masterfrom
TeigenZhang:pr/mouse-restore-decset-strip
Jul 12, 2026
Merged

feat(web): restore tap/click/wheel mouse interaction when server strips mouse DECSETs#144
Ark0N merged 3 commits into
Ark0N:masterfrom
TeigenZhang:pr/mouse-restore-decset-strip

Conversation

@TeigenZhang

Copy link
Copy Markdown
Contributor

Summary

Restores mouse interaction with the terminal (tap-to-position cursor, desktop click-to-position, and scroll-wheel forwarding) for sessions whose output the server strips of mouse-tracking DECSETs.

Root cause. To keep the wheel scrolling scrollback, the server strips ?1000/1002/1003/1006 DECSET sequences from claude/codex/gemini output (isAltScreenStripMode, session.ts). Side effect: the browser xterm's mouseTrackingMode is permanently 'none' for those sessions, even though the TUI on the PTY side still has tracking ON and understands SGR reports. Any UI that gates on xterm's live tracking mode — mobile touchend tap, desktop click, wheel forwarding — silently does nothing.

Fix. When a session is one the server strips, hand-encode the SGR report from the pointer position and send it straight to the PTY, bypassing xterm's dormant encoder:

  • Mobile tap — touchend tap encodes an SGR press+release (was already partly handled; consolidated here).
  • Desktop click — a plain left-click encodes SGR press+release. Every click that already means something else is skipped: synthetic/compat clicks after a touch tap, modified clicks (Shift keeps xterm's selection override), double/triple clicks (word/line select), drag-selections, off-grid clicks, and sessions where xterm's own encoder is live.
  • Wheel — Claude Code 2.1.187+ scrolls its own transcript on SGR wheel reports and no longer captures wheel as select-menu navigation (verified against 2.1.202: the /model menu highlight ignores wheel reports). So the wheel is forwarded to the TUI for strip-mode sessions at the buffer bottom, coalesced into one PTY write per ~40ms to avoid a send-keys process storm. Shift+wheel and any scrolled-up viewport stay on xterm's local scrollback.

Scrollback coordinate guard. SGR coordinates are viewport-relative, so a report computed while scrolled up would hit-test the wrong row. Synthetic taps/clicks/wheel are gated on the viewport being at the buffer bottom.

Behavior is unchanged for shell/opencode sessions and when a session genuinely has mouse tracking off.

Changes

  • terminal-ui.js_handleDesktopTerminalClick, _sendSyntheticSgrTap/Wheel, _shouldForwardWheelToApp, _terminalViewportAtBottom, _clientPointToCell; wheel handler forwards to the app for strip-mode sessions
  • test/terminal-touch-tap.test.ts — 12 cases (SGR encoding, grid clamping, click-skip conditions, wheel gating/coalescing, scrollback guard, shell-mode exclusion)

Test plan

  • npm test -- test/terminal-touch-tap.test.ts — 12/12 pass
  • npm run check:frontend-syntax
  • Verified E2E via Playwright against a live instance: desktop click emits SGR press+release; wheel up/down forward SGR 64/65; Shift+wheel stays on local scrollback
  • Verified the TUI side by injecting SGR bytes into a real claude session in an isolated tmux: click expands a collapsed tool-output block; wheel scrolls the transcript; the /model select menu ignores wheel reports

Teigen and others added 3 commits July 7, 2026 17:52
…-encode SGR when server strips mouse DECSETs

v1.1.7 (3172bef, arrived via the master merge) strips mouse-tracking DECSET
sequences from claude/codex/gemini output so the wheel keeps scrolling
scrollback. Side effect: the browser xterm's mouseTrackingMode is permanently
'none' for those sessions, and the mobile touchend tap branch gates its
synthetic click on exactly that mode — so tap-to-position-cursor silently died.

Fix: when tracking reads 'none' but the session mode is one the server strips
(claude/codex/gemini — the PTY-side TUI still has tracking ON), encode the SGR
press+release report directly from the touch point and send it to the PTY,
bypassing xterm's mouse encoder. No DOM click is dispatched, so xterm's local
selection cannot trigger either.

Tests: 3 new cases in test/terminal-touch-tap.test.ts (SGR encoding, grid
clamping, shell-mode exclusion); verified E2E via Playwright iPhone emulation
against both a stripped-stream instance and the production bundle.
…ssions

Desktop click-to-position-cursor died under the server's mouse-DECSET strip
(same root cause as the mobile touchend tap regression): xterm's native mouse
encoder only emits SGR while mouseTrackingMode is ON, but the server strips the
enabling DECSETs from claude/codex/gemini output. Hand-encode the report for
plain left-clicks (_handleDesktopTerminalClick), skipping every click that
already means something else (synthetic/compat, modified, double/triple,
drag-selection, off-grid, xterm encoder live).

Also widen forwarding to the wheel: Claude Code 2.1.187+ scrolls its own
transcript on SGR wheel reports and no longer captures wheel as select-menu
navigation (verified against 2.1.202), so forward the wheel to the TUI for
strip-mode sessions at the buffer bottom (40ms-coalesced to avoid a tmux
send-keys storm). Shift+wheel and any scrolled-up viewport stay on xterm's
local scrollback. Guard synthetic taps/clicks on viewport-at-bottom so a
scrolled-up report can't hit-test the wrong row.

Tests: 12 cases in test/terminal-touch-tap.test.ts. Verified E2E via Playwright
against the live instance (wheel up/down forward, Shift+wheel local, click).
…ck double-fire (PR Ark0N#144)

- _shouldForwardWheelToApp: claude sessions forward wheel to the TUI only
  when the banner-parsed cliVersion is known AND >= 2.1.187 (older/unknown
  Claude Code captures wheel as select-menu navigation → keep local
  scrollLines); new dependency-free _cliVersionAtLeast semver-ish compare
- gemini excluded from wheel forwarding entirely (TUI wheel behavior
  unverified); codex keeps forwarding (verified); taps/clicks still
  forwarded for all strip modes
- link double-fire: registerFilePathLinkProvider links now track hover
  state via ILink hover/leave callbacks (_linkHovered) and
  _handleDesktopTerminalClick bails while a link is hovered, so a link
  click no longer also sends a synthetic SGR press/release to the TUI
- help modal: document Shift+Wheel (scroll local history when mouse
  passthrough is active)
- tests: version gate (2.1.186/unknown/garbage no forward, 2.1.187+
  forwards), codex/gemini split, link-hover click suppression

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Ark0N Ark0N merged commit 3f23621 into Ark0N:master Jul 12, 2026
2 checks passed
@Ark0N

Ark0N commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Merged — thank you @TeigenZhang! Restoring tap/click/wheel over the server-side DECSET strip closes a real interaction gap. Review appended one commit: wheel-forwarding is now version-gated (claude ≥ 2.1.187 — older Ink select menus capture wheel as option navigation, the exact bug the strip guarded against; unknown versions fall back to local scroll), gemini is excluded until its TUI behavior is verified, clicking a file-path/URL link no longer double-fires a synthetic click into the TUI, and Shift+wheel (local history) is documented in the shortcuts modal. 🙏

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