@@ -11,6 +11,7 @@ import type {
1111 VoiceRecordResponse
1212} from '../gatewayTypes.js'
1313import { isAction , isCopyShortcut , isMac , isVoiceToggleKey } from '../lib/platform.js'
14+ import { computePrecisionWheelStep , initPrecisionWheel } from '../lib/precisionWheel.js'
1415import { computeWheelStep , initWheelAccelForHost } from '../lib/wheelAccel.js'
1516
1617import { getInputSelection } from './inputSelectionStore.js'
@@ -21,8 +22,6 @@ import { patchTurnState } from './turnStore.js'
2122import { getUiState } from './uiStore.js'
2223
2324const isCtrl = ( key : { ctrl : boolean } , ch : string , target : string ) => key . ctrl && ch . toLowerCase ( ) === target
24- const PRECISION_WHEEL_MIN_GAP_MS = 80
25- const PRECISION_WHEEL_STICKY_MS = 80
2625
2726export function useInputHandlers ( ctx : InputHandlerContext ) : InputHandlerResult {
2827 const { actions, composer, gateway, terminal, voice, wheelStep } = ctx
@@ -38,9 +37,7 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
3837 // rows = wheelStep × accelMult. State mutates in place across renders.
3938 const wheelAccelRef = useRef ( initWheelAccelForHost ( ) )
4039
41- const precisionWheelRef = useRef < { active : boolean ; dir : 0 | - 1 | 1 ; lastEventAtMs : number ; lastScrollAtMs : number } > (
42- { active : false , dir : 0 , lastEventAtMs : 0 , lastScrollAtMs : 0 }
43- )
40+ const precisionWheelRef = useRef ( initPrecisionWheel ( ) )
4441
4542 useEffect ( ( ) => ( ) => clearTimeout ( scrollIdleTimer . current ?? undefined ) , [ ] )
4643
@@ -291,40 +288,26 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
291288 if ( key . wheelUp || key . wheelDown ) {
292289 const dir : - 1 | 1 = key . wheelUp ? - 1 : 1
293290 const now = Date . now ( )
294- // Modifier-held wheel = precision mode: at most one wheelStep per short
295- // interval. Smooth mice / trackpads emit many raw wheel events for one
296- // intended line step, so raw 1:1 still moves too far .
291+ // Modifier-held wheel = precision mode: one row per frame, no accel.
292+ // Smooth mice / trackpads emit tiny same-frame bursts; coalesce those
293+ // without the old 80ms throttle that made opt-scroll feel stepped .
297294 // SGR/X10 mouse encoding only carries shift/meta/ctrl bits; Cmd on
298295 // macOS is intercepted by the terminal, so we honor Option (meta) on
299296 // Mac / Alt (meta) on Win+Linux / Ctrl as a portable fallback. Shift
300297 // is reserved for selection extension.
301298 const hasModifier = key . meta || key . ctrl
302- const precision = precisionWheelRef . current
303- // Keep precision active through the current wheel burst after the
304- // modifier is released. Otherwise a stream of queued/momentum wheel
305- // events can hand off mid-burst into the accelerated path and jump.
306- const precisionSticky = now - precision . lastEventAtMs < PRECISION_WHEEL_STICKY_MS
307-
308- if ( hasModifier || precisionSticky ) {
309- if ( ! precision . active ) {
310- precision . active = true
311- wheelAccelRef . current = initWheelAccelForHost ( )
312- }
313-
314- precision . lastEventAtMs = now
299+ const precision = computePrecisionWheelStep ( precisionWheelRef . current , dir , hasModifier , now )
315300
316- if ( dir === precision . dir && now - precision . lastScrollAtMs < PRECISION_WHEEL_MIN_GAP_MS ) {
317- return
301+ if ( precision . active ) {
302+ // Entering precision mode must discard any accelerated wheel state;
303+ // otherwise the next normal wheel event inherits stale momentum.
304+ if ( precision . entered ) {
305+ wheelAccelRef . current = initWheelAccelForHost ( )
318306 }
319307
320- precision . lastScrollAtMs = now
321- precision . dir = dir
322-
323- return scrollTranscript ( dir * wheelStep )
308+ return precision . rows ? scrollTranscript ( dir * wheelStep ) : undefined
324309 }
325310
326- precision . active = false
327-
328311 // 0 = direction-flip bounce deferred; skip the no-op scroll.
329312 const rows = computeWheelStep ( wheelAccelRef . current , dir , now )
330313
0 commit comments