fix(ui): make overlay scrollbar persistent on desktop shells (#2581)

PR #2520 fixed #2475 in the wrong layer (overflow classes on
SettingsView.tsx wrapper divs that aren't the actual scroll containers),
so the "no scrollbar in Settings" bug shipped in v1.17.2 unchanged, and
the sessions feed has the same underlying issue (#2402).

The actual scroll containers render through ScrollableOverlay ->
OverlayScrollbar, which hides the native scrollbar and draws its own
JS thumb that auto-hides ~1s after scroll activity stops, with a
second CSS rule fully hiding that thumb inside Settings specifically.

Fix both at the shared component/CSS layer so every consumer (Settings,
sessions feed, git panels, chat, etc.) gets a persistent scrollbar on
desktop shells (Electron + VS Code webview) in one change:

- OverlayScrollbar.tsx: skip the auto-hide timer and show the thumb
  immediately on mount when content overflows, on desktop runtimes.
  userIntentOnly consumers (chat auto-follow scroll, reasoning blocks)
  keep today's intent-gated behavior unchanged.
- index.css: scope the Settings-specific hide rule to non-desktop, so
  mobile/web keep existing behavior and desktop gets the thumb back.
This commit is contained in:
Sérgio Pedro
2026-08-06 21:10:22 +03:00
committed by GitHub
parent 3f5a453d31
commit fa63866cbb
2 changed files with 22 additions and 4 deletions
@@ -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<OverlayScrollbarProps> = ({
containerRef,
minThumbSize = 32,
@@ -128,8 +134,15 @@ const OverlayScrollbarComponent: React.FC<OverlayScrollbarProps> = ({
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<OverlayScrollbarProps> = ({
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) => {
+4 -2
View File
@@ -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;
}