Skip to content

fix(shell): enable native touch scrolling in the mobile terminal#986

Open
mayankdebnath wants to merge 1 commit into
siteboon:mainfrom
mayankdebnath:fix/mobile-terminal-touch-scroll
Open

fix(shell): enable native touch scrolling in the mobile terminal#986
mayankdebnath wants to merge 1 commit into
siteboon:mainfrom
mayankdebnath:fix/mobile-terminal-touch-scroll

Conversation

@mayankdebnath

@mayankdebnath mayankdebnath commented Jul 10, 2026

Copy link
Copy Markdown

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

  1. user-select: text hijacks the drag. src/index.css forced
    -webkit-user-select: text !important on .xterm/.xterm-viewport. On iOS
    Safari 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 the
    DOM, so this rule was hurting more than it helped.
  2. Non-passive touch listeners disable threaded scrolling. Every terminal
    touch listener was registered { passive: false }, forcing the browser to
    block 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.)
  3. Custom JS inertia fights the browser. A requestAnimationFrame loop
    mutated viewport.scrollTop after touchend, competing with the browser's
    own 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.)
  4. Misleading "Canvas fallback". The WebGL addon was loaded before
    terminal.open(), so it always threw and the terminal silently fell back to
    xterm's slow DOM renderer. @xterm/addon-canvas wasn'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

  • Native scrolling via CSS. Drop the forced user-select: text; add
    touch-action: pan-y, overscroll-behavior: contain and
    -webkit-overflow-scrolling: touch on .xterm-viewport. Native text selection
    is re-enabled only while a custom selection gesture is active
    (.shell-mobile-selecting). overscroll-behavior: contain also stops the page
    from rubber-banding while you scroll the terminal.
  • Passive touchmove + touch-action arbitration. The terminal touchmove
    listener is now passive, so one-finger scrolling stays on the compositor
    thread. Selection/pinch gestures no longer preventDefault; instead the
    terminal's touch-action flips to none while selecting, dragging a handle,
    or pinching (resolveTerminalTouchAction). touchstart/touchend stay
    non-passive so pinch-start and the keyboard-suppression on selection still
    work.
  • Native momentum. The custom JS inertia is removed; the natively-scrolled
    viewport provides momentum for free.
  • Real renderer fallback. Load the renderer after terminal.open(), add
    @xterm/addon-canvas as 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 — pass
  • npm run lint — pass (0 errors)
  • Full node:test suite (client + server) — pass, including new unit tests for
    the touch-action gesture resolution and the move-threshold helper
    (src/components/shell/utils/mobileTerminalSelection.test.ts)
  • npm run build — pass
  • App boots and serves the built client

On-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)

  1. Open a project's Shell tab on iOS Safari and let some output scroll in.
  2. Try to drag the terminal up/down with one finger — it selects text or does
    nothing instead of scrolling.
  3. With this change, the same drag scrolls the buffer natively with momentum,
    and long-press selection / pinch-zoom still work.

Summary by CodeRabbit

  • New Features

    • Added accelerated terminal rendering with WebGL and automatic canvas fallback.
    • Improved mobile terminal scrolling, text selection, pinch-zooming, and selection-handle gestures.
    • Added touch-friendly scrolling behavior with better overscroll support.
  • Bug Fixes

    • Terminal rendering now recovers gracefully when WebGL loses its graphics context.
  • Tests

    • Added coverage for mobile touch actions and movement-threshold behavior.

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
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The terminal now loads WebGL with a CanvasAddon fallback, while mobile touch handling uses passive scrolling, gesture-driven touch-action, active selection styling, and native momentum behavior instead of JavaScript inertia.

Changes

Terminal renderer fallback

Layer / File(s) Summary
Renderer fallback wiring
package.json, src/components/shell/hooks/useShellTerminal.ts, vite.config.js
Adds @xterm/addon-canvas, loads it when WebGL fails or loses context, and includes it in the xterm vendor chunk.

Gesture state and touch lifecycle

Layer / File(s) Summary
Gesture state and style arbitration
src/components/shell/utils/mobileTerminalSelection.ts, src/index.css
Adds gesture-state helpers, synchronizes touch-action and selection classes, and limits text selection styles to active selection gestures.
Passive touch lifecycle and validation
src/components/shell/utils/mobileTerminalSelection.ts, src/components/shell/utils/mobileTerminalSelection.test.ts
Makes touchmove passive, removes JavaScript inertia, updates gesture transitions and cleanup, and tests touch-action resolution and movement thresholds.

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
Loading

Poem

A rabbit hops where touch trails glide,
WebGL sparkles, Canvas waits beside.
Passive paws let scrollwinds flow,
Selection blooms when gestures show.
No scripted fling—just moonlit ease. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: improving mobile terminal scrolling behavior.
Linked Issues check ✅ Passed The changes address all stated goals: passive touch scrolling, removal of custom inertia, and a real canvas fallback for WebGL failure.
Out of Scope Changes check ✅ Passed The touched files stay focused on shell touch handling, renderer setup, tests, and related build config with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lift

Passive touchmove still leaves long-press drag vulnerable to native scroll. touch-action is latched when the touch starts, so switching to none inside startSelection cannot 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5884573 and 6dc49e0.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • package.json
  • src/components/shell/hooks/useShellTerminal.ts
  • src/components/shell/utils/mobileTerminalSelection.test.ts
  • src/components/shell/utils/mobileTerminalSelection.ts
  • src/index.css
  • vite.config.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant