From 0aabcac6eaaea122e63a594cb612cff8c3f92479 Mon Sep 17 00:00:00 2001 From: Adam Firestone Date: Tue, 18 Aug 2026 12:35:58 -0500 Subject: [PATCH] fix(web): limit minimap wheel scrolling to the hit strip - Ignore wheel events over the expanded minimap preview - Add coverage for hit-strip targeting --- .../chat/MessagesTimeline.logic.test.ts | 23 +++++++++++++++++++ .../components/chat/MessagesTimeline.logic.ts | 20 ++++++++++++++++ .../src/components/chat/MessagesTimeline.tsx | 13 ++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts index ca1b7928ceee..2f3a854aca78 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts @@ -7,6 +7,7 @@ import { resolveAssistantMessageCopyState, resolveTimelineMinimapWheelIndex, shouldPreserveAssistantLineBreaks, + timelineMinimapWheelTargetsHitStrip, } from "./MessagesTimeline.logic"; describe("resolveTimelineMinimapWheelIndex", () => { @@ -34,6 +35,28 @@ describe("resolveTimelineMinimapWheelIndex", () => { }); }); +describe("timelineMinimapWheelTargetsHitStrip", () => { + it("accepts wheel events over the minimap bars", () => { + expect( + timelineMinimapWheelTargetsHitStrip({ + pointerX: 24, + hitStripLeft: 12, + hitStripWidth: 40, + }), + ).toBe(true); + }); + + it("rejects wheel events over the expanded preview", () => { + expect( + timelineMinimapWheelTargetsHitStrip({ + pointerX: 100, + hitStripLeft: 12, + hitStripWidth: 40, + }), + ).toBe(false); + }); +}); + describe("shouldPreserveAssistantLineBreaks", () => { it("preserves Claude insight formatting without changing regular markdown", () => { expect( diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index 7f954b3e8882..9e3e8c6fdc20 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -104,6 +104,26 @@ export function resolveTimelineMinimapWheelIndex(input: { return Math.max(0, Math.min(input.itemCount - 1, currentIndex + direction)); } +export function timelineMinimapWheelTargetsHitStrip(input: { + readonly pointerX: number; + readonly hitStripLeft: number; + readonly hitStripWidth: number; +}): boolean { + if ( + !Number.isFinite(input.pointerX) || + !Number.isFinite(input.hitStripLeft) || + !Number.isFinite(input.hitStripWidth) || + input.hitStripWidth <= 0 + ) { + return false; + } + + return ( + input.pointerX >= input.hitStripLeft && + input.pointerX <= input.hitStripLeft + input.hitStripWidth + ); +} + export function resolveTimelineMinimapHasPersistentGutter(viewportWidth: number): boolean { if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) { return false; diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 810fca389bcd..7d4df881ff49 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -86,6 +86,7 @@ import { resolveTimelineMinimapTopPercent, resolveTimelineMinimapWheelIndex, shouldPreserveAssistantLineBreaks, + timelineMinimapWheelTargetsHitStrip, type StableMessagesTimelineRowsState, type MessagesTimelineRow, TIMELINE_MINIMAP_MIN_ITEMS, @@ -787,6 +788,16 @@ function TimelineMinimap({ } const rect = button.getBoundingClientRect(); + if ( + !timelineMinimapWheelTargetsHitStrip({ + pointerX: event.clientX, + hitStripLeft: rect.left, + hitStripWidth, + }) + ) { + return; + } + const pointerIndex = resolveTimelineMinimapIndexFromPointer({ itemCount: items.length, railTop: rect.top, @@ -815,7 +826,7 @@ function TimelineMinimap({ wheelIndexRef.current = nextIndex; onSelect(nextItem); }, - [items, onSelect], + [hitStripWidth, items, onSelect], ); useEffect(() => {