From a1b6c85f8d2b9bd31a8faebbdd8d2ba12bddea4a Mon Sep 17 00:00:00 2001 From: naps62-yolo Date: Wed, 24 Jun 2026 15:59:49 +0000 Subject: [PATCH] fix(maestro): reliable touch scrollback on mobile xterm's native touch scrolling crawled a line at a time and stalled after a few. Map finger drags on the viewport to term.scrollLines (skipped on the alt screen, which the app owns) and claim vertical pans via touch-action: pan-y. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/attach.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ web/src/styles.css | 3 +++ 2 files changed, 48 insertions(+) diff --git a/web/src/attach.ts b/web/src/attach.ts index 526d2ae..f5be8bf 100644 --- a/web/src/attach.ts +++ b/web/src/attach.ts @@ -130,6 +130,51 @@ export function createAttachConnection( term.loadAddon(fit); term.open(body); + // Touch scrollback: xterm's built-in touch handling on the viewport is jittery + // (drags crawl a line at a time and stall after a few), so map finger drags to + // term.scrollLines ourselves. Skipped on the alternate screen (full-screen + // TUIs like tmux/Claude Code) — there's no scrollback to move and the app owns + // the gesture. `touch-action: pan-y` (CSS) tells the browser we handle Y. + { + const vp = el.querySelector(".xterm-viewport") as HTMLElement | null; + const cellH = () => { + const core = (term as unknown as { + _core?: { _renderService?: { dimensions?: { css?: { cell?: { height: number } } } } }; + })._core; + return core?._renderService?.dimensions?.css?.cell?.height || 16; + }; + if (vp) { + let lastY = 0; + let remainder = 0; + vp.addEventListener( + "touchstart", + (e) => { + if (e.touches.length !== 1) return; + lastY = e.touches[0].clientY; + remainder = 0; + }, + { passive: true }, + ); + vp.addEventListener( + "touchmove", + (e) => { + if (e.touches.length !== 1) return; + if (term.buffer.active.type === "alternate") return; // app-owned + const y = e.touches[0].clientY; + const px = lastY - y + remainder; // finger up → scroll toward newer + lastY = y; + const lines = Math.trunc(px / cellH()); + remainder = px - lines * cellH(); + if (lines !== 0) { + term.scrollLines(lines); + e.preventDefault(); + } + }, + { passive: false }, + ); + } + } + // Mobile: the special-key bar is only useful while the soft keyboard is up, // so reveal it only when the terminal's hidden textarea is focused (CSS still // gates it to coarse pointers). Tapping a keycap keeps focus via pointerdown diff --git a/web/src/styles.css b/web/src/styles.css index bfd3865..23fb99d 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1148,6 +1148,9 @@ textarea { .attach-body .xterm .xterm-viewport { overflow-y: auto; scrollbar-width: none; + /* We drive scrollback from touchmove (see attach.ts); claim vertical pans so + the browser doesn't fight the handler or trigger gestures mid-scroll. */ + touch-action: pan-y; } .attach-body .xterm .xterm-viewport::-webkit-scrollbar {