You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Wheel forwarding sends one SGR report per terminal line, so one notch over-scrolls mouse-tracking apps by ~5x #245
In an alt-screen / mouse-tracking pane (opencode, Claude Code), one wheel detent scrolls far more than it does in a normal terminal. opencode moves its scroll_speed per wheel report (default 3 rows), so a single detent moves ~15-18 rows. At scroll_speed: 1 it moves 4-6 rows. A normal terminal moves ~scroll_speed rows per detent.
Why
writeWheelToPty loops one SGR report per line:
for (let i = 0; i < Math.abs(count); i++) window.wmux.pty.write(ptyId, seq);
where count = wheelDeltaToLines(...). A Windows detent is ~100px of deltaY, which at ~17-20px cell height is 5-6 lines, so one DOM wheel event becomes 5-6 app-level wheel reports.
xterm does the opposite: MouseService._sendEvent (case 'wheel') uses the line count only to decide whether this event crosses a line, then sends at most ONE report per wheel event. An app therefore sees one scroll step per detent, not one per line.
Impact
Users tune opencode's scroll_speed down to 0.25 to tame the wheel, which makes touch pan far too slow, since the touch stream (correctly, 1:1 per cell of finger travel) shares that multiplier. Measured on a ThinkPad Z16 Gen 2 (Windows 11):
default (scroll_speed 3): ~15-18 rows per detent
scroll_speed: 1: 4-6 rows per detent
scroll_speed: 0.25: 1-2 rows per detent (right for the wheel, touch unusably slow)
Suggested fix
In the mouse-tracking branch, emit one SGR report per DOM wheel event instead of one per line (keep the pixel/line accumulator and the remainder handling). The touch-synthesized stream should keep per-line reporting, since that is what makes pan track the finger; a source flag or ev.isTrusted can gate it. The arrow-key branch for non-mouse pagers (less/vim) should keep looping per line: 5-6 arrows per detent is correct there.
Not caused by the #243 touch work; the loop predates it.
Confirmed, and fixed in 2.13.1 (9cc8fca). Your diagnosis was exactly right, including that #243 didn't cause it — the loop predates the touch work by a long way.
What was wrong
The two forwarding branches speak different units, and writeWheelToPty treated them as one:
branch
unit
why
SGR mouse report
a step
the app decides how far a step goes
arrow keys
a line
an arrow is one line; nobody scales it
Emitting Math.abs(count) reports is correct for the second and wrong for the first, because the app then multiplies on top. Your 5-6 lines per detent × opencode's scroll_speed 3 is the 15-18 rows you measured.
So the mouse-report branch now sends one report per DOM wheel event, and the line count is demoted to a gate — "did this event cross a line at all?". That's what xterm does; I checked the pinned 6.0.0 bundle rather than going from the description, and in Terminal._bindMouse, case 'wheel', consumeWheelEvent() is only ever tested against 0 before a single triggerMouseEvent. The pixel/line accumulator and its sub-line remainder are untouched.
One place I went the other way from your suggestion
You said the arrow-key branch should keep looping per line. It does — but not for parity with xterm, because xterm doesn't do that either. That same grep turned up its non-mouse fallback, and it sends exactly one arrow per wheel event:
consti=C0.ESC+(applicationCursorKeys ? "O" : "[")+(ev.deltaY<0 ? "A" : "B");this.coreService.triggerDataEvent(i,true);
Looping is therefore a deliberate divergence, and it needed an actual reason rather than "xterm does it". The reason is internal consistency: wmux's own scrollback branch moves lines rows per event, so one arrow per detent would make less in an alt-screen pane crawl at a fifth of the speed of the scrollback in the pane right beside it. #245 was the multiplier, and the arrow branch has none. Written down in the source so nobody "fixes" it back.
Touch
Kept per-line, as you asked — a finger is a position, not a detent, and deriving pan speed from a per-event report would tie it to the frame rate. Worth being honest about the limit: wmux can't know the app's rows-per-report multiplier, so one report per cell crossed is the closest it can get, and it's exactly 1:1 only where that multiplier is 1.
The source rides on the synthetic event as a private Symbol rather than ev.isTrusted. Trust answers "who dispatched this", not "what gesture is this", so the next synthetic wheel from anywhere else would silently inherit touch semantics — and a real-Chromium harness confirmed both synthetic events read isTrusted: false, so trust isn't observable in a test either.
What to expect
You should be able to put scroll_speed back to 3. One detent → one report → ~3 rows, the same as any other terminal, and the pan keeps tracking your finger at that setting instead of needing 0.25.
The decision is pure and pinned in tests/unit/wheel-forward.test.ts — the count is invisible in a screenshot and only reproduces on a touchscreen ThinkPad, so it isn't something I can eyeball here. Thanks for measuring it at three scroll_speed values; that's what made the multiplier obvious rather than the line count.
2.13.1 is building now and will be on the releases page shortly. Please reopen if the wheel still overshoots on the Z16.
Confirmed on v2.13.1, ThinkPad Z16 Gen 2: no overshoot, one detent = scroll_speed rows (a consistent 3 at scroll_speed: 3). Wheel finally matches a normal terminal, and touch pan still works. Nice fix.
Activity
fix(terminal): one wheel report per detent, not one per line (#245)
amirlehmam commentedon Sep 19, 2026
Confirmed, and fixed in 2.13.1 (
9cc8fca). Your diagnosis was exactly right, including that #243 didn't cause it — the loop predates the touch work by a long way.What was wrong
The two forwarding branches speak different units, and
writeWheelToPtytreated them as one:Emitting
Math.abs(count)reports is correct for the second and wrong for the first, because the app then multiplies on top. Your 5-6 lines per detent × opencode'sscroll_speed3 is the 15-18 rows you measured.So the mouse-report branch now sends one report per DOM wheel event, and the line count is demoted to a gate — "did this event cross a line at all?". That's what xterm does; I checked the pinned 6.0.0 bundle rather than going from the description, and in
Terminal._bindMouse,case 'wheel',consumeWheelEvent()is only ever tested against0before a singletriggerMouseEvent. The pixel/line accumulator and its sub-line remainder are untouched.One place I went the other way from your suggestion
You said the arrow-key branch should keep looping per line. It does — but not for parity with xterm, because xterm doesn't do that either. That same grep turned up its non-mouse fallback, and it sends exactly one arrow per wheel event:
Looping is therefore a deliberate divergence, and it needed an actual reason rather than "xterm does it". The reason is internal consistency: wmux's own scrollback branch moves
linesrows per event, so one arrow per detent would makelessin an alt-screen pane crawl at a fifth of the speed of the scrollback in the pane right beside it. #245 was the multiplier, and the arrow branch has none. Written down in the source so nobody "fixes" it back.Touch
Kept per-line, as you asked — a finger is a position, not a detent, and deriving pan speed from a per-event report would tie it to the frame rate. Worth being honest about the limit: wmux can't know the app's rows-per-report multiplier, so one report per cell crossed is the closest it can get, and it's exactly 1:1 only where that multiplier is 1.
The source rides on the synthetic event as a private
Symbolrather thanev.isTrusted. Trust answers "who dispatched this", not "what gesture is this", so the next synthetic wheel from anywhere else would silently inherit touch semantics — and a real-Chromium harness confirmed both synthetic events readisTrusted: false, so trust isn't observable in a test either.What to expect
You should be able to put
scroll_speedback to3. One detent → one report → ~3 rows, the same as any other terminal, and the pan keeps tracking your finger at that setting instead of needing0.25.The decision is pure and pinned in
tests/unit/wheel-forward.test.ts— the count is invisible in a screenshot and only reproduces on a touchscreen ThinkPad, so it isn't something I can eyeball here. Thanks for measuring it at threescroll_speedvalues; that's what made the multiplier obvious rather than the line count.2.13.1 is building now and will be on the releases page shortly. Please reopen if the wheel still overshoots on the Z16.
Mattheinrichs commentedon Sep 19, 2026
Confirmed on v2.13.1, ThinkPad Z16 Gen 2: no overshoot, one detent =
scroll_speedrows (a consistent 3 atscroll_speed: 3). Wheel finally matches a normal terminal, and touch pan still works. Nice fix.