Keyboard scroll bindings are routed through the mouse-wheel acceleration state

Status Open
Reported on v2.1.259
Maintainer reply None cached
Activity 0 comments · opened Sep 3, 2026

Summary

In the fullscreen renderer, a key bound to scroll:lineUp / scroll:lineDown
does not scroll a fixed number of lines. It feeds the mouse-wheel
acceleration state machine — the same object that carries mult, wheelMode
and the burst counter for wheel events. Holding the key therefore ramps, and
the amount scrolled depends on how fast the key repeats rather than on how
many times it fired.

The same two action names already have a non-accelerated implementation in the
codebase: in the Transcript context they are dispatched through the plain
stepper (the one scroll:halfPage* / scroll:fullPage* use), which moves
exactly ±1 line and consults neither the acceleration setting nor the base
scroll speed. Only the Scroll context sends them into the wheel
accelerator.

The result: there is no way to get a fixed 1–3 lines per keypress in the
conversation view while leaving the wheel accelerated, and no user-facing knob
reaches the difference.

Behavior

With the "window (native)" curve, the step returned for an event depends on
the gap since the previous event:

  • gap > 40ms — the multiplier is reset to base and base lines are

returned. No ramp.

  • gap ≤ 40ms — the event feeds the ramp (+3 per event, capped at

base * 2).

  • wheelMode expires after 1500ms of no scrolling.

Standard OS key auto-repeat is roughly 30–33 repeats per second, i.e. a ~30ms
gap, which lands just inside the 40ms window. So an ordinary held key is
treated as a fast wheel gesture. This is not a tuning problem — a keypress
carries no velocity, and nothing about it needs the wheel's state.

There is a second, "decay" curve selected on some platforms; the constants
above describe the "window (native)" one.

Why the existing settings do not cover this

  • wheelScrollAccelerationEnabled is described in settings as ramping

mouse-wheel speed, but it also gates the key path, because both paths
share one accelerator. Turning it off does give sane keyboard scrolling —
and simultaneously reduces the wheel to base lines per notch, which makes
scrolling through any real distance a hand exercise.

  • CLAUDE_CODE_SCROLL_SPEED sets base for both sources at once. Raising it

to make the un-accelerated wheel usable makes each keypress jump equally
far, which is the opposite of fine-grained keyboard scrolling.

The two knobs are per-install, not per-input-device, so no combination of
their values produces "1–3 lines per keypress, accelerated wheel".

Requested fix

Either of these would do it, and the first one is already written:

  1. Dispatch scroll:lineUp / scroll:lineDown in the Scroll context

through the plain stepper, exactly as the Transcript context does, and
keep the accelerator on the wheelup / wheeldown bindings only. The
accelerator is a property of the wheel, so it belongs on the wheel
bindings rather than on the action.

  1. Failing that, expose a stepped action that keyboard bindings can use —

a scroll:lines taking a count, or separate keyboard actions — so that
keybindings.json can ask for a fixed step without inheriting wheel
physics.

Workarounds considered, and why they are not acceptable

Listing these because the two earlier reports were closed with the underlying
request unaddressed, and each of these has since been suggested to me:

  • Slowing the global OS key-repeat rate so the gap clears 40ms. This

changes every application on the desktop to accommodate one TUI's scroll
dispatch. A key-repeat rate is a decades-stable personal setting; it is not
a place to absorb an application bug.

  • Emulating key repeat externally, with a tool that fires the key on a

fixed timer while the terminal window has focus. Hand-rolled repeat, bound
to one key, matched against one window, maintained per-application — a
large amount of fragile glue standing in for a change at one dispatch site.

  • Disabling wheel acceleration — covered above: it fixes the keyboard by

breaking the wheel.

  • Binding half-page / full-page keys. Paging is not a substitute for line

scrolling: the point of scrolling a few lines at a time is that you keep
reading while it moves. A half-page jump loses your place just as a full
page does.

  • Staying on the classic renderer, which is what I do. That avoids the

scrolling entirely, at the cost of the terminal's own scrollback: because
the TUI hard-wraps its output, text that scrolled off is never reflowed
when the window is resized or the zoom level changes. Exiting and resuming
the session is the only way to get a correctly wrapped tail.

Prior reports

  • #66534 — closed as not planned.
  • #62294 — closed as not planned (stale).

Both argued from symptoms. This report names the dispatch: two contexts, one
pair of action names, one of them already having the wanted behavior.

Environment

  • Claude Code 2.1.259
  • Windows Terminal on WSL2 (Windows 11), TERM_PROGRAM unset in WSL
  • "tui": "default" (classic renderer), set because of this issue

View original on GitHub ↗