From 3d4733adb72f08d69b3e946786264dad32f4e3ad Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 2 Nov 2026 03:50:59 +0000 Subject: [PATCH] fix(chat): the wheel over the bottom stack's parts scrolls nothing in Chromium Chromium sent a wheel over the running tray's head to the transcript under the floating stack (the tray head took the pointer, but nothing held the wheel), so agent-pane-dock-gutters failed on chromium ("tray": boxScrolls true). A non-passive listener on the stack now holds a wheel over a part that has nothing of its own left to scroll that way; gutters and Jump to latest still pass through to the transcript. Co-Authored-By: Claude Opus 5.5 --- webviews/src/agent-session/acpmux/App.tsx | 4 ++- .../agent-session/acpmux/wheelPassThrough.ts | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/webviews/src/agent-session/acpmux/App.tsx b/webviews/src/agent-session/acpmux/App.tsx index 21a6fb8f324f..70e05d8ae3b8 100644 --- a/webviews/src/agent-session/acpmux/App.tsx +++ b/webviews/src/agent-session/acpmux/App.tsx @@ -452,7 +452,7 @@ import { ArrowDown, ChevronRight } from "./conversation/icons"; import { CopyButton } from "./conversation/CopyButton"; import { PreviewCard } from "./conversation/PreviewCard"; import { installWheelGlide } from "./wheelGlide"; -import { installWheelPassThrough } from "./wheelPassThrough"; +import { installDockWheelHold, installWheelPassThrough } from "./wheelPassThrough"; import { turnPreviewUrl } from "./conversation/previewUrl"; import { projectInboxRows } from "./conversation/laneNoticeBatches"; import { @@ -2923,6 +2923,8 @@ export function VirtualTranscript({ withdrawn.current = withdrawnSet; const dockRef = useRef(dock); dockRef.current = dock; + // The wheel over the stack's parts scrolls nothing (wheelPassThrough.ts installDockWheelHold). + useEffect(() => (dock ? installDockWheelHold(dock) : undefined), [dock]); const queueSession = useRef(sessionId); queueSession.current = sessionId; /// The chat and the stack `queuedIds` were noted in. diff --git a/webviews/src/agent-session/acpmux/wheelPassThrough.ts b/webviews/src/agent-session/acpmux/wheelPassThrough.ts index 666576bca7be..e94f48b4f327 100644 --- a/webviews/src/agent-session/acpmux/wheelPassThrough.ts +++ b/webviews/src/agent-session/acpmux/wheelPassThrough.ts @@ -60,3 +60,30 @@ export function installWheelPassThrough(scroller: HTMLElement, main: HTMLElement main.addEventListener("wheel", onWheel, { passive: true }); return () => main.removeEventListener("wheel", onWheel); } + +/// Whether a box from `target` up to `stop` (not included) can still scroll `deltaY` its way. +function scrollsUnder(target: Element, stop: Element, deltaY: number): boolean { + for (let at: Element | null = target; at && at !== stop; at = at.parentElement) { + if (at.scrollHeight <= at.clientHeight) continue; + const overflow = getComputedStyle(at).overflowY; + if (overflow !== "auto" && overflow !== "scroll") continue; + if (deltaY < 0 ? at.scrollTop > 0 : at.scrollTop + at.clientHeight < at.scrollHeight - 1) return true; + } + return false; +} + +/// Holds the wheel over the stack's parts (`dock`): a wheel over a part that has nothing of its own +/// left to scroll that way scrolls nothing, rather than whatever Chromium's compositor finds under +/// the stack (the transcript, where the stack floats over its rows). A part's gutters and empty room +/// still go to the transcript (installWheelPassThrough). Not passive, so it is on the stack alone: +/// a wheel over the transcript never waits on it. Returns the remover. +export function installDockWheelHold(dock: HTMLElement): () => void { + const onWheel = (event: WheelEvent) => { + const target = event.target instanceof Element ? event.target : null; + if (!target || event.ctrlKey || passesThrough(target)) return; + if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return; + if (!scrollsUnder(target, dock, event.deltaY)) event.preventDefault(); + }; + dock.addEventListener("wheel", onWheel, { passive: false }); + return () => dock.removeEventListener("wheel", onWheel); +}