Repository navigation
Description
What happens
On Windows 11 with a touchscreen, dragging a finger vertically over a terminal pane running opencode does nothing. Expected: the swipe should scroll the transcript the same way the mouse wheel does. The wheel works in the same pane; tap, selection and keyboard are unaffected, so it is specifically pan that is inert.
I have only tested this with opencode so far, not with a plain shell pane or other agents.
Steps to reproduce
- Windows 11 touchscreen device, wmux 2.11.2.
- In a pane, run
opencodeand build up output longer than the pane. - Drag a finger up or down over the pane. Nothing moves. A mouse wheel in the same pane scrolls the transcript.
wmux version
2.11.2
Windows version
Windows 11 Enterprise; Lenovo ThinkPad Z16 Gen 2 (touchscreen)
Anything else
Why this happens, from reading wmux and xterm sources:
- wmux pins
@xterm/xterm6.0.0 exactly (package.json). 6.0.0 is where touch scrolling regressed upstream (Regression - Touch scrolling not functioning in 6.0.0 xtermjs/xterm.js#5489): 5.5.0 had a native overflow scroller a finger could drag; 6.0 replaced the viewport with the VS CodeSmoothScrollableElement(custom overlay scrollbar, wheel only) and shipped no touch replacement. The upstream fix, PR Fix touch scrolling xtermjs/xterm.js#5563 ("Fix touch scrolling"), merged 2025-12-31 with milestone 7.0.0. Ancestry check: 6.0.0 is 257 commits behind it,6.1.0-beta.304contains it, and no stable release has it yet. - wmux's renderer adds no touch fallback either: no
touchstart/touchmovehandling and nopointerType === 'touch'branch on the terminal surface (the only pointer handlers are the pane-drag/hover UI). A pan produces no wheel event and noscrollLinescall, so the gesture is inert. - A dependency bump alone would not fix this case. The upstream fix only moves the scrollable position, which drives buffer scrollback (
Viewport.handleTouchScroll->_onRequestScrollLines). The alternate buffer has no scrollback, and opencode's TUI (OpenTUI renderer) switches to it (\x1b[?1049h), so touch would still do nothing in exactly the panes wmux is built around (opencode, Claude Code, Codex). The wheel works in those panes because it goes through xterm's wheel pipeline (src/browser/services/MouseService.ts): mouse protocol with wheel support -> button-64/65 reports to the app; alt screen without a wheel protocol -> synthesized arrow keys (Touch scrolling should send arrow keys like how wheel events are translated xtermjs/xterm.js#1007); otherwise -> scrollback. Touch never enters that pipeline.
A fix that would actually resolve this: translate a vertical touch pan on the terminal surface into a synthetic WheelEvent, so it inherits all three wheel behaviors:
- pointer events filtered to
pointerType === 'touch'(Chromium/Electron delivers pointer events; the Touch Events API may not be enabled in Electron), withtouch-action: noneon the terminal host so the pointermove stream stays cancelable, listeners registered{ passive: false }; - accumulate fractional cell height (touchmove deltas are typically well under one cell), clamp per event, ignore deltas inside a tap's wobble;
- dispatch
new WheelEvent('wheel', { deltaY, deltaMode: 0, bubbles: true, cancelable: true })on the terminal element, thenpreventDefault()the pointermove.
Terminal creation lives in src/renderer/hooks/useTerminal.ts; the touch wiring would sit alongside it. A later @xterm/xterm bump (first stable containing #5563) restores the native path for scrollback, but per (3) it is not the whole fix.
Happy to test a build with this on the touchscreen device.
Activity
fix(terminal): translate a touch pan into a wheel, so a finger scrolls (
chore(release): bump to 2.13.0, and CLAUDE.md for #242/#243
amirlehmam commentedon Sep 18, 2026
Fixed in 2.13.0. Your analysis was correct on all three points and it shaped the fix — in particular point 3, which is the reason this isn't a dependency bump.
On point 3, you were right, and it's even more true than you could see from outside. wmux doesn't use xterm's wheel pipeline at all.
handleTerminalWheel(src/renderer/hooks/useTerminal.ts) takes the wheel on the capture phase — xterm's own forwarding is unreliable after the WebGL context swap (#41), and an adjacent<webview>compositor otherwise steals un-prevented wheel events (#47) — and it already implements all three outcomes itself: scrollback, SGR wheel reports, and arrow keys for a non-mouse pager on the alt screen.So your suggested fix was exactly the right one, and for a slightly better reason than stated: synthesizing a
WheelEventdoesn't just inherit the three wheel behaviours, it inherits wmux's three, which already cover the alt-screen agent panes upstream's #5563 would leave inert. A finger now behaves identically to the wheel in an opencode pane.What shipped, following your outline:
pointerType === 'touch',touch-action: noneon the terminal container, listeners{ passive: false },preventDefault()on the pointermove — only while actually panning, so a horizontal drag is left alonepointercancelhandled too, or the tracker keeps a finger down forever and the feature works exactly onceclientX/clientYcarried onto the synthetic event, because with mouse tracking onhandleTerminalWheelreports the wheel at the pointer's cell — without them every flick would report at the top-left cornerOne departure from your suggestion: no fractional-cell accumulator in the touch path.
wheelDeltaToLinesalready keeps a per-surface fractional accumulator against the user's measured cell height, so the gesture just feeds it pixels. A second accumulator would have been a second answer to one question, free to drift from the first — and it would have stopped tracking font size.The recognizer is pure (
src/renderer/utils/touch-pan.ts, 18 tests) precisely because the device that reproduces this isn't the one CI runs on. I also verified the end-to-end chain against a real xterm 6.0.0 in real Chromium — a capture-phase listener receiving an event dispatched on its own element was the load-bearing assumption and no unit test proves it. All 9 checks pass: direction, tap-inert, horizontal-inert, coordinates, and the computedtouch-action.That said, none of that is a touchscreen. You offered to test a build and I'd genuinely like to take you up on it — 2.13.0 is building now and the zip will be on the releases page shortly. What I can't verify here is feel: whether the 8px slop is right for a real finger, and whether momentum/inertia is missed (there is none — a pan stops when the finger stops). Both are easy to tune if they're wrong.
amirlehmam commentedon Sep 18, 2026
Shipped in v2.13.0.
Closing since the fix is out, but the offer stands and the feedback is genuinely wanted — if the pan feels wrong on the Z16 (8px slop too eager or too sticky, or the lack of inertia being missed), reopen or file a follow-up and I'll tune it. The recognizer is isolated in
src/renderer/utils/touch-pan.ts, so the constants are a one-line change.Mattheinrichs commentedon Sep 18, 2026
Follow-up after real-device testing and tuning on a ThinkPad Z16 Gen 2 (Windows 11), v2.13.0.
Verified: touch pan works in opencode's alt-screen pane, tap-inert, horizontal-inert, second-finger reject, pointercancel recovery, no wheel/mouse regressions. 8px slop feels right on a real finger; no jump at slop crossing.
Momentum: missed. A pan stopping dead at lift-off is noticeable on a finger, and reading a long agent transcript means repeated swipes. If you add a hand-rolled velocity/deceleration pass, happy to test and tune it here (flick threshold, decay).
One conflict found while tuning opencode's
scroll_speed(tui.json): wheel and touch can't be tuned independently, because both arrive as SGR wheel reports and opencode applies one multiplier per report.scroll_speed: 1→ wheel ~4-6 rows per detent (too fast), touch tracks the finger (right).scroll_speed: 0.25→ wheel 1-2 rows per detent (right), touch far too slow.The wheel number is inflated because wmux forwards one SGR report per terminal line, so a ~100px notch (~5-6 lines) becomes 5-6 reports; xterm sends one report per wheel event. Filed separately as #245 (pre-existing, not a 2.13.0 regression). Either a touch-only gain on the synthetic
deltaYor one-report-per-wheel-event for trusted input would let both be tuned sanely.Core fix is good; no urgency on either item from my side.
Mattheinrichs commentedon Sep 19, 2026
Final retest on v2.13.1 (ThinkPad Z16 Gen 2, Windows 11):
scroll_speed3 gives a consistent 3 rows per detent, no overshoot.That is the last bit of the shared multiplier: wheel wants 3, touch wants ~2 or less, and both are one SGR report per step in the app. If the touch path could apply its own gain (a constant or a setting), I could leave the wheel at 3 and scale the synthetic delta until the pan tracks the finger. Happy to test values here.
Momentum remains the other open ask; filed as #248.
For what it's worth, those two patches together have turned the touchscreen from unusable-in-practice into something I now use as the primary input in wmux. Fast turnaround too, both same-day.
feat(terminal): momentum after a touch flick (#248)