Skip to content

Collapsing the sidebar on a chat route makes the un-collapse button unclickable: an Electron drag region covers it #74

Description

@Broccolito

Describe the bug

On macOS, once the sidebar is collapsed from a chat route (/ Hub or /pair), the sidebar toggle button in the titlebar becomes unclickable, so the sidebar cannot be brought back. The button is still drawn in its normal place and looks perfectly live — it just does not respond to hover or click. Clicking it drags the window instead.

The only way out is to leave the chat route (e.g. click Home — reachable only if you happen to know the keyboard route, or via another window), at which point the very same button starts working again.

The button is not covered by another DOM element in the usual sense. It is covered by an Electron draggable region, which is computed independently of z-index and paint order. The sidebar toggle sits at z-[190], above everything around it, and DOM hit-testing would reach it fine — but the OS-level drag region is decided before the DOM ever sees the click.

The New Window button lives in the same control strip and is dead in exactly the same way and for the same reason. The report below covers both.


To Reproduce

  1. Launch the desktop app on macOS and land on a chat route — either the Hub (/, a new session) or /pair with chats loaded. Either works; no particular tab count is needed.
  2. Make sure the window is wide enough that the sidebar is in normal "push" mode (i.e. not already auto-collapsed — window wider than SIDEBAR_COMPACT_WIDTH).
  3. Click the sidebar toggle in the titlebar (the panel-left icon just right of the traffic lights, data-testid="titlebar-sidebar-toggle"). The sidebar collapses correctly.
  4. Try to click the same button again to bring the sidebar back.
  5. Nothing happens. The button does not even show its hover highlight. Press-and-drag on it moves the whole window.
  6. Try the New Window button immediately to its right — also dead.
  7. Navigate to any non-chat route (Home, Settings, Extensions, Knowledge…) — both buttons work again immediately. Navigate back to chat with the sidebar collapsed — both are dead again.

Workarounds that do work (useful for users hitting this today):

  • ⌘B toggles the sidebar. The shortcut is registered on window by SidebarProvider (ui/desktop/src/components/ui/sidebar.tsx:22,86-96) and is not affected by the drag region.
  • Widening/narrowing the window across the compact threshold can restore it via the auto-collapse watcher.
  • Navigating to a non-chat route, as above.

Expected behavior

The sidebar toggle (and the New Window button beside it) stay clickable in every state and on every route. Collapsing the sidebar must never make the control that un-collapses it unreachable — that is a one-way door in the UI.


Screenshots

Sidebar expanded on a new session: the two titlebar controls sit at the far left of the chat header, right of the traffic lights, and both work.

Sidebar collapsed on the same session: the two controls are still drawn in exactly the same place and look identical — the tab strip has correctly shifted right to make room for them — but neither responds to hover or click.


Please provide the following information

  • OS & Arch: macOS 26.5.2 (25F84), arm64 (Apple Silicon)
  • Interface: UI (Electron desktop)
  • Version: v1.88.6 (present on main at 37f3f587; the code paths involved are long-standing, so earlier 1.8x builds are very likely affected too)
  • Extensions enabled: not relevant — reproduces with any extension set
  • Provider & Model: not relevant — reproduces with no turn ever run (a brand-new empty session is enough)

Additional context

Root cause

This is an Electron draggable-region ordering bug, not a CSS stacking bug.

Chromium collects -webkit-app-region rectangles by walking the layout tree in tree order and appending them to a flat list. Electron then folds that list into a single region in list order, unioning drag rects and subtracting no-drag rects — shell/browser/ui/drag_util.cc:

SkRegion DraggableRegionsToSkRegion(
    const std::vector<blink::mojom::DraggableRegionPtr>& regions) {
  SkRegion sk_region;
  for (const auto& region : regions) {
    sk_region.op(
        SkIRect::MakeLTRB(region->bounds.x(), region->bounds.y(),
                          region->bounds.right(), region->bounds.bottom()),
        region->draggable ? SkRegion::kUnion_Op : SkRegion::kDifference_Op);
  }
  return sk_region;
}

Because the fold is sequential, a drag rect that appears later in the DOM re-covers a no-drag rect subtracted earlier, regardless of z-index. This is a well-known, long-standing Electron limitation — see electron#7605, electron#11768, electron#41695.

Our DOM lays the three relevant regions out in precisely the losing order:

# Element app-region Where
1 .titlebar-drag-region (fixed, full width, 32px tall) drag ui/desktop/src/App.tsx:628 + ui/desktop/src/styles/main.css:1765-1777
2 TitlebarControls strip — the sidebar toggle + New Window no-drag ui/desktop/src/components/Layout/TitlebarControls.tsx:62-91, mounted at ui/desktop/src/components/Layout/AppLayout.tsx:169-173
3 BaseChat's 52px session header drag ui/desktop/src/components/BaseChat.tsx:2126-2140
3b the chat tab strip wrap, inside that header drag ui/desktop/src/components/chatGroups/ChatTabStrip.tsx:238-256

TitlebarControls is rendered before <Sidebar> and <SidebarInset> in AppLayout, so its no-drag subtraction is applied to the region before BaseChat's header union is applied on top of it. The header wins, and the buttons are inside a draggable region as far as the OS is concerned.

Why it only breaks when the sidebar is collapsed

Pure geometry — the two rects only overlap in the collapsed state:

  • The control strip is placed at left: 100px (MACOS_TRAFFIC_LIGHT_RESERVE), top: 8px, width 64px (two 32px controls), height 32px → it occupies x 100–164, y 8–40 (TitlebarControls.tsx:6-15,65-71).
  • SidebarInset is pushed right by --sidebar-width = 15rem = 240px when the sidebar is expanded (ui/desktop/src/components/ui/sidebar.tsx:19,305). BaseChat's header therefore starts at x = 240 — comfortably right of 164, no overlap, button works.
  • Collapsed, the inset drops to ml-0 (sidebar.tsx:306), so the header starts at x = 0 and its rect spans x 0–width, y 0–52 — which fully contains the control strip. Overlap, button dead.

The chat tab strip's own paddingLeft reserve (172px on macOS, from getSessionTitlePaddingTitlebarControls.tsx:36-49, consumed at ChatTabStrip.tsx:217,255) correctly keeps the tabs from sliding under the traffic lights and the controls, which is why the collapsed screenshot still looks right. But that padding is part of the wrap's own box, and the wrap declares WebkitAppRegion: 'drag' — so the reserve that visually protects the buttons is itself part of what electrically kills them.

Why it only breaks on the chat route

BaseChat's 52px WebkitAppRegion: 'drag' header is the only later-in-DOM drag region in the app. Non-chat routes (Home, Settings, Extensions, Knowledge, …) never render it, so nothing re-unions over the no-drag subtraction and the buttons behave. That matches the reported "navigate out of the page and it works again" exactly.

Note that body.biorouter-chat-route-active .titlebar-drag-region { pointer-events: none } (main.css:1779-1781) does not help here: pointer-events has no bearing on -webkit-app-region, and the offending region is BaseChat's header, not the fixed 32px strip.

Platform scope

macOS only, as far as the mechanism goes. The window is frameless there (titleBarStyle: 'hidden', frame: falseui/desktop/src/main.ts:1051,1054), which is the only configuration in which -webkit-app-region is honoured. Windows and Linux get frame: true and a native title bar, so the drag regions are inert and the buttons should keep working. Not verified on Windows/Linux.

Why the test suite does not catch this

TitlebarControls.test.tsx:47 already does fireEvent.click(screen.getByTestId('titlebar-sidebar-toggle')) and asserts the toggle fires. It passes, and will keep passing, because jsdom has no concept of -webkit-app-region and no OS drag regions — the synthetic click always reaches the button. Any jsdom-level test of this control is structurally incapable of failing on this bug. A regression gate has to be either a real-Electron interaction test, or a static/structural assertion about DOM ordering and rect overlap of app-region declarations.

Possible directions for a fix

Listed for discussion; no code changed in filing this.

  1. Reorder, so the no-drag wins. Render TitlebarControls after SidebarInset in AppLayout (it is absolutely positioned, so this is a DOM-order change only, not a visual one). Cheapest fix; entirely dependent on a Chromium implementation detail, so it needs a comment saying why the order is load-bearing or it will be "cleaned up" later.
  2. Punch a permanent hole. Keep a separate always-last no-drag element that mirrors the control strip's rect, mounted at the end of the tree, so the subtraction is always the final operation.
  3. Stop making the header a blanket drag region. Give BaseChat's header an explicit left inset for the reserved strip instead of drag-ing from x=0, so its rect never overlaps the controls in the first place. Most robust; touches the drag-to-move affordance in the reserved band, which is currently dead space anyway.
  4. Independent of which of the above lands: never let the sidebar become unrecoverable. Even with the drag region fixed, this class of bug is a one-way door. A visible fallback affordance in the collapsed state (a hover-revealed rail, or the existing SidebarRail component in sidebar.tsx:269-292, which is defined but never mounted) would make the state recoverable by construction. ⌘B exists but is undiscoverable.

Whichever route is taken, the fix should come with a gate that can actually fail — see the test note above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions