diff --git a/packages/ui/src/components/ui/OverlayScrollbar.tsx b/packages/ui/src/components/ui/OverlayScrollbar.tsx index d1026abb..7a409d00 100644 --- a/packages/ui/src/components/ui/OverlayScrollbar.tsx +++ b/packages/ui/src/components/ui/OverlayScrollbar.tsx @@ -25,6 +25,12 @@ const isSameThumbMetrics = (a: ThumbMetrics, b: ThumbMetrics): boolean => { return Math.abs(a.length - b.length) < METRIC_EPSILON && Math.abs(a.offset - b.offset) < METRIC_EPSILON; }; +// Desktop shells (Electron, VS Code webview) use persistent, not +// auto-hiding, scrollbars. Reads the same `desktop-runtime` root class that +// index.css keys off, so JS and CSS never disagree on what counts as desktop. +const isDesktopScrollbarRuntime = (): boolean => + typeof document !== "undefined" && document.documentElement.classList.contains("desktop-runtime"); + const OverlayScrollbarComponent: React.FC = ({ containerRef, minThumbSize = 32, @@ -128,8 +134,15 @@ const OverlayScrollbarComponent: React.FC = ({ if (isHoveringRef.current) { return; } + // Desktop shells keep the thumb visible once shown instead of + // auto-hiding it after a delay. userIntentOnly callers (e.g. chat + // auto-follow scroll) opt out of persistence on purpose, so they keep + // the existing auto-hide behavior even on desktop. + if (isDesktopScrollbarRuntime() && !userIntentOnly) { + return; + } hideTimeoutRef.current = setTimeout(() => setVisible(false), hideDelayMs); - }, [hideDelayMs]); + }, [hideDelayMs, userIntentOnly]); const markUserIntent = React.useCallback(() => { lastUserIntentAtRef.current = Date.now(); @@ -162,7 +175,10 @@ const OverlayScrollbarComponent: React.FC = ({ if (!container) return; updateMetrics(); - setVisible(false); + // On desktop shells, show the thumb immediately if content overflows + // instead of waiting for the first scroll event (persistent affordance, + // matching native desktop scrollbar conventions). + setVisible(isDesktopScrollbarRuntime() && !userIntentOnly); const onScroll = () => handleScroll(); const onKeyDown = (event: KeyboardEvent) => { diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css index b6462769..4805a2c3 100644 --- a/packages/ui/src/index.css +++ b/packages/ui/src/index.css @@ -1804,8 +1804,10 @@ input[aria-label="Terminal input"] { } -/* Settings dialog: hide the overlay scrollbar; wheel/keyboard scroll still works. */ -[data-settings-view="true"] .overlay-scrollbar { +/* Settings dialog: hide the overlay scrollbar on mobile/web; wheel/keyboard + scroll still works. Desktop shells keep the persistent scrollbar so the + Settings sub-panels aren't left with no scroll affordance at all. */ +html:not(.desktop-runtime) [data-settings-view="true"] .overlay-scrollbar { display: none; }