fix(shell): enable native touch scrolling in the mobile terminal#986
fix(shell): enable native touch scrolling in the mobile terminal#986mayankdebnath wants to merge 1 commit into
Conversation
One-finger scrolling in the web shell was broken on iOS Safari and janky on other touch platforms. Root causes: - `.xterm-viewport` forced `-webkit-user-select: text !important`, so on iOS Safari a one-finger drag became a text-selection gesture instead of a scroll; with the WebGL/canvas renderer there is no selectable DOM text, so the drag did nothing at all. - All terminal touch listeners were registered non-passive, forcing the browser to block every scroll frame on the main thread and disabling compositor-thread scrolling, which stutters while the terminal is producing output. - A custom requestAnimationFrame inertia loop mutated `scrollTop` after touchend, fighting the browser's own momentum and snapping to a stop instead of easing. - The WebGL addon was loaded before `terminal.open()`, so it always threw and the terminal silently fell back to xterm's slow DOM renderer; the "Canvas fallback" log was misleading and `@xterm/addon-canvas` was not a dependency. Fix: - Scope the viewport to native scrolling: drop the forced `user-select: text`, add `touch-action: pan-y`, `overscroll-behavior: contain` and `-webkit-overflow-scrolling: touch`. Native text selection is re-enabled only while a custom selection gesture is active (`.shell-mobile-selecting`). - Register the terminal `touchmove` listener passive and arbitrate gestures with `touch-action` (`pan-y` normally, `none` while selecting or pinching) instead of `preventDefault`, keeping one-finger scrolling on the compositor thread. - Remove the custom JS inertia; the natively scrolled viewport provides momentum. - Load the renderer after `terminal.open()`, add `@xterm/addon-canvas` as the real fallback (with WebGL context-loss recovery), and fix the log message. Add unit tests for the touch-action gesture resolution and the move-threshold helper. Refs siteboon#954
📝 WalkthroughWalkthroughThe terminal now loads WebGL with a CanvasAddon fallback, while mobile touch handling uses passive scrolling, gesture-driven ChangesTerminal renderer fallback
Gesture state and touch lifecycle
Sequence Diagram(s)sequenceDiagram
participant ShellTerminal
participant Xterm
participant WebGL
participant Canvas
ShellTerminal->>Xterm: open terminal
ShellTerminal->>WebGL: load renderer
WebGL-->>ShellTerminal: success or failure
ShellTerminal->>Canvas: load fallback on failure or context loss
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/shell/utils/mobileTerminalSelection.ts (1)
390-433: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftPassive touchmove still leaves long-press drag vulnerable to native scroll.
touch-actionis latched when the touch starts, so switching tononeinsidestartSelectioncannot change the current finger’s pan decision. If one-finger extend-drag must stay reliable, it needs a scroll-blocking path from the start; otherwise keep selection extension on the handles only.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/shell/utils/mobileTerminalSelection.ts` around lines 390 - 433, Prevent long-press selection dragging from relying on changing touch-action after the gesture begins: either establish a scroll-blocking touch path before selection can start and use it throughout the gesture, or restrict extend-selection behavior to handle dragging only. Update onTerminalTouchMove and the related startSelection/touch-action setup so native scrolling cannot cancel or interfere with one-finger selection extension.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/components/shell/utils/mobileTerminalSelection.ts`:
- Around line 390-433: Prevent long-press selection dragging from relying on
changing touch-action after the gesture begins: either establish a
scroll-blocking touch path before selection can start and use it throughout the
gesture, or restrict extend-selection behavior to handle dragging only. Update
onTerminalTouchMove and the related startSelection/touch-action setup so native
scrolling cannot cancel or interfere with one-finger selection extension.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 093b3a0a-40f0-4c5d-b396-c3e7a6972401
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
package.jsonsrc/components/shell/hooks/useShellTerminal.tssrc/components/shell/utils/mobileTerminalSelection.test.tssrc/components/shell/utils/mobileTerminalSelection.tssrc/index.cssvite.config.js
Summary
One-finger scrolling in the web Shell terminal is broken on iOS Safari and janky
on other touch platforms. This makes the fix from #954: it moves the terminal
back to native, compositor-driven scrolling instead of the current
non-passive/JS-driven approach.
Closes #954.
The bug
On iOS Safari you cannot drag-scroll the terminal at all; on Android and other
touch platforms scrolling stutters, especially while the terminal is producing
output.
Root cause
user-select: texthijacks the drag.src/index.cssforced-webkit-user-select: text !importanton.xterm/.xterm-viewport. On iOSSafari that turns a one-finger drag into a text-selection gesture instead of a
scroll — and with the WebGL/canvas renderer there is no selectable DOM text to
grab, so the drag does nothing at all. The terminal's own selection is done
through xterm's selection model (see
mobileTerminalSelection.ts), not theDOM, so this rule was hurting more than it helped.
touch listener was registered
{ passive: false }, forcing the browser toblock each scroll frame on the main thread — which is busy parsing/rendering
PTY output — so scrolling stutters. (Root cause env.example is missing #1 from iPhone: Shell terminal scrolling is janky — non-passive touch listeners disable threaded scrolling; custom JS inertia and DOM-renderer fallback compound it #954.)
requestAnimationFrameloopmutated
viewport.scrollTopaftertouchend, competing with the browser'sown momentum and snapping to a stop instead of easing. (Root cause Dev #2 from
iPhone: Shell terminal scrolling is janky — non-passive touch listeners disable threaded scrolling; custom JS inertia and DOM-renderer fallback compound it #954.)
terminal.open(), so it always threw and the terminal silently fell back toxterm's slow DOM renderer.
@xterm/addon-canvaswasn't even a dependency,so the "Canvas fallback" log was wrong. (Root cause Toggle for think mode #3 from iPhone: Shell terminal scrolling is janky — non-passive touch listeners disable threaded scrolling; custom JS inertia and DOM-renderer fallback compound it #954.)
The fix
user-select: text; addtouch-action: pan-y,overscroll-behavior: containand-webkit-overflow-scrolling: touchon.xterm-viewport. Native text selectionis re-enabled only while a custom selection gesture is active
(
.shell-mobile-selecting).overscroll-behavior: containalso stops the pagefrom rubber-banding while you scroll the terminal.
touchmove+touch-actionarbitration. The terminaltouchmovelistener is now passive, so one-finger scrolling stays on the compositor
thread. Selection/pinch gestures no longer
preventDefault; instead theterminal's
touch-actionflips tononewhile selecting, dragging a handle,or pinching (
resolveTerminalTouchAction).touchstart/touchendstaynon-passive so pinch-start and the keyboard-suppression on selection still
work.
viewport provides momentum for free.
terminal.open(), add@xterm/addon-canvasas the real fallback (with WebGL context-loss recovery),and fix the log message. WebGL is preferred, canvas is the fallback, and the
slow DOM renderer is only a last resort.
No regression is intended to long-press selection, selection-handle dragging,
pinch-to-zoom font sizing, mouse/wheel scrolling, or tap-to-focus + keyboard —
those paths are preserved; only the scroll mechanism changed.
Tests / verification
npm run typecheck— passnpm run lint— pass (0 errors)node:testsuite (client + server) — pass, including new unit tests forthe
touch-actiongesture resolution and the move-threshold helper(
src/components/shell/utils/mobileTerminalSelection.test.ts)npm run build— passOn-device note (please read): this is a WebKit/iOS-specific interaction — a
desktop browser (even with touch emulation) scrolls fine with the old CSS, so
an emulated before/after does not reproduce the broken "before." I verified the
logic via the unit tests and the mechanism above, but I could not run it on a
physical iPhone. A maintainer spot-check on real iOS Safari / iPadOS would be
appreciated; I'm happy to add an on-device before/after recording if someone can
capture one.
Reproduction (on an iPhone, before this change)
nothing instead of scrolling.
and long-press selection / pinch-zoom still work.
Summary by CodeRabbit
New Features
Bug Fixes
Tests