From 4e670d3e4c7912b59fb8c6ee489a999b2e9e7962 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Fri, 24 Jul 2026 03:25:07 -0400 Subject: [PATCH] fix(terminal): stop wheel replay when mouse reporting is disabled (#8616) --- .../pane-terminal-mouse-wheel.test.ts | 79 +++++++++++++++++++ .../pane-manager/pane-terminal-mouse-wheel.ts | 28 +++++-- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts b/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts index 05190661d..0e2bfb505 100644 --- a/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts @@ -393,6 +393,7 @@ describe('terminal mouse wheel multiplier', () => { handlers.push(handler) }, element: target, + modes: { mouseTrackingMode: 'any' }, rows: 24 }, { getTuiMouseWheelMultiplier: () => 1 } @@ -418,6 +419,81 @@ describe('terminal mouse wheel multiplier', () => { expect(shouldMultiplyTerminalMouseWheel(dispatched[0]!, target)).toBe(false) }) + it('does not replay with a stale active mouse-reporting class', async () => { + vi.stubGlobal('WheelEvent', TestWheelEvent) + const handlers: ((event: WheelEvent) => boolean)[] = [] + const target = Object.assign(new EventTarget(), { + classList: { + contains: (className: string) => className === 'enable-mouse-events' + } + }) as unknown as EventTarget & HTMLElement + const dispatched: WheelEvent[] = [] + target.addEventListener('wheel', (event) => dispatched.push(event as WheelEvent)) + const terminal = { + attachCustomWheelEventHandler: (handler: (event: WheelEvent) => boolean) => { + handlers.push(handler) + }, + element: target, + modes: { mouseTrackingMode: 'none' as const }, + rows: 24 + } + attachTerminalMouseWheelMultiplier(terminal) + + const event = new TestWheelEvent('wheel', { + bubbles: true, + cancelable: true, + deltaMode: DOM_DELTA_PIXEL, + deltaY: 12 + }) as WheelEvent + Object.defineProperty(event, 'wheelDeltaY', { + configurable: true, + value: -120 + }) + + expect(handlers[0]?.(event)).toBe(true) + await Promise.resolve() + + expect(dispatched).toHaveLength(0) + }) + + it('discards pending replay when mouse reporting turns off before drain', async () => { + vi.stubGlobal('WheelEvent', TestWheelEvent) + const handlers: ((event: WheelEvent) => boolean)[] = [] + const target = Object.assign(new EventTarget(), { + classList: { + contains: (className: string) => className === 'enable-mouse-events' + } + }) as unknown as EventTarget & HTMLElement + const dispatched: WheelEvent[] = [] + target.addEventListener('wheel', (event) => dispatched.push(event as WheelEvent)) + const terminal = { + attachCustomWheelEventHandler: (handler: (event: WheelEvent) => boolean) => { + handlers.push(handler) + }, + element: target, + modes: { mouseTrackingMode: 'any' as 'any' | 'none' }, + rows: 24 + } + attachTerminalMouseWheelMultiplier(terminal) + + const event = new TestWheelEvent('wheel', { + bubbles: true, + cancelable: true, + deltaMode: DOM_DELTA_PIXEL, + deltaY: 12 + }) as WheelEvent + Object.defineProperty(event, 'wheelDeltaY', { + configurable: true, + value: -120 + }) + + expect(handlers[0]?.(event)).toBe(false) + terminal.modes.mouseTrackingMode = 'none' + await Promise.resolve() + + expect(dispatched).toHaveLength(0) + }) + it('replays trackpad-like TUI pixel scrolling with responsive direction reversal', async () => { vi.stubGlobal('WheelEvent', TestWheelEvent) const handlers: ((event: WheelEvent) => boolean)[] = [] @@ -434,6 +510,7 @@ describe('terminal mouse wheel multiplier', () => { handlers.push(handler) }, element: target, + modes: { mouseTrackingMode: 'any' }, rows: 24 }, { getTuiMouseWheelMultiplier: () => 1 } @@ -486,6 +563,7 @@ describe('terminal mouse wheel multiplier', () => { handlers.push(handler) }, element: target, + modes: { mouseTrackingMode: 'any' }, rows: 24 }, { getTuiMouseWheelMultiplier: () => 1 } @@ -520,6 +598,7 @@ describe('terminal mouse wheel multiplier', () => { handlers.push(handler) }, element: target, + modes: { mouseTrackingMode: 'any' }, rows: 24 }, { diff --git a/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.ts b/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.ts index f35c31efb..63a2e157a 100644 --- a/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.ts +++ b/src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.ts @@ -21,7 +21,9 @@ const XTERM_MOUSE_REPORTING_CLASS = 'enable-mouse-events' const REPLAYED_WHEEL_EVENT_PROPERTY = '__orcaReplayedTerminalWheelEvent' const DOM_DELTA_LINE = 1 -type TerminalWheelTarget = Pick +type TerminalWheelTarget = Pick & { + modes: Pick +} type TerminalMouseWheelMultiplierOptions = { getTuiMouseWheelMultiplier?: () => number | undefined @@ -117,7 +119,10 @@ export function shouldMultiplyTerminalMouseWheel( return true } -function drainTerminalTuiWheelReports(state: TerminalTuiMouseWheelReplayState): void { +function drainTerminalTuiWheelReports( + state: TerminalTuiMouseWheelReplayState, + terminal: TerminalWheelTarget +): void { const target = state.pendingTarget const event = state.pendingEvent if (!target || !event || state.pendingReports <= 0) { @@ -125,6 +130,15 @@ function drainTerminalTuiWheelReports(state: TerminalTuiMouseWheelReplayState): return } + if (terminal.modes.mouseTrackingMode === 'none') { + state.pendingReports = 0 + state.drainScheduled = false + state.pendingDirection = 0 + state.pendingEvent = null + state.pendingTarget = null + return + } + const reportsToDispatch = state.pendingReports for (let i = 0; i < reportsToDispatch; i += 1) { target.dispatchEvent(cloneWheelReportEvent(event)) @@ -138,6 +152,7 @@ function drainTerminalTuiWheelReports(state: TerminalTuiMouseWheelReplayState): function queueTerminalTuiWheelReports( state: TerminalTuiMouseWheelReplayState, + terminal: TerminalWheelTarget, target: EventTarget, event: WheelEvent, reportCount: number @@ -164,7 +179,7 @@ function queueTerminalTuiWheelReports( // Why: dispatch after xterm returns from the original wheel handler, but do // not frame-cap reports; fullscreen TUIs need the full wheel distance. queueMicrotask(() => { - drainTerminalTuiWheelReports(state) + drainTerminalTuiWheelReports(state, terminal) }) } @@ -174,7 +189,10 @@ export function attachTerminalMouseWheelMultiplier( ): void { const replayState = createTerminalTuiMouseWheelReplayState() terminal.attachCustomWheelEventHandler((event) => { - if (!shouldMultiplyTerminalMouseWheel(event, terminal.element)) { + if ( + terminal.modes.mouseTrackingMode === 'none' || + !shouldMultiplyTerminalMouseWheel(event, terminal.element) + ) { return true } @@ -195,7 +213,7 @@ export function attachTerminalMouseWheelMultiplier( rows: terminal.rows } ) - queueTerminalTuiWheelReports(replayState, target, event, reportCount) + queueTerminalTuiWheelReports(replayState, terminal, target, event, reportCount) return false })