fix(desktop): the side panel arriving on Luke's code no longer scrolls the window sideways

Merged
DA
11d ago
+121-7

Summary

Reproduced and fixed the broken layout Dean saw after the side panel opened by itself during a call. The panel sat at the window's left edge under the traffic lights, and the sidebar and document were gone.

Root cause. When Luke first puts code on screen, usePanelArrivals opens the panel on the Code tab (#1851), and the panel slides in from past the window's right edge (#1850). On that same commit the Code tab's CodeLines scrolled its pointed line into view with scrollIntoView({ block: "center" }). scrollIntoView scrolls every scrollable box around the element, horizontally too ("inline: nearest"). The pointed line was still off past the right edge, and .desktop-shell[data-sidebar-collapsed] (always set on the Plans shell) clipped the sliding sidebar with overflow: hidden, which makes the shell a scroll container. So the browser scrolled the whole window layout sideways, by as much as the panel's width.

It stayed scrolled after the slide. The toolbar's and call bar's ::before hairlines reach 100vw past the column, which keeps that much horizontal overflow in place, so the scroll offset is never clamped back. The outcome:

  • the docked panel shifted left. With a large enough shift, a full-screen panel stood at x≈0 with its tab strip under the traffic lights.
  • the sidebar and document scrolled out of view, leaving empty ground at the right that only the call bar's hairline (its ::before) crosses. That matches the screenshot.

Fix. Two lines of defence:

  1. .desktop-shell[data-sidebar-collapsed] clips with overflow: clip instead of hidden. clip clips the same but is not a scroll container, so nothing (scrollIntoView, focus(), find-in-page) can scroll the window layout. This is the same fix base.css already applies to html, body, #root for the same reason.
  2. The code pane centres its pointed line by scrolling its own lines alone (centreWithin sets .code-lines' scrollTop and brings it back to the lines' start) rather than with scrollIntoView.

Investigation

I mounted the real DesktopShell, useSidePanel, useSidebarCollapse, and usePanelArrivals with the built styles.css in headless Chrome (google-chrome, puppeteer-core). Then I drove them with a fuzzer: random sequences of 14 actions (show/hide panel, full screen, sidebar fold, code arriving/leaving, plan switch, new-plan page and back, window resize) with random 0–300ms gaps so actions land mid-animation. After each sequence settles it checks these invariants:

  • nothing is scrolled sideways

  • the work column starts at the sidebar's edge

  • a docked panel's right edge meets the column's and the document stays visible beside it

  • a full-screen panel covers exactly the column

  • the panel's tabs clear the traffic lights and title-bar controls

  • no leftover clip or transform, and no exit still under way

  • Before: 2 of 25 sequences broke, both by this scroll. One left a full-screen panel at x=2 with the tabs under the traffic lights, as in Dean's screenshot. The single step "code arrives while the panel is closed" reproduces it every time.

  • After: 0 of 125 sequences broke (five seeds × 25).

The other suspects from the brief — auto-open while the sidebar is folded, mid-enter/exit, during full screen or its exit, on a plan switch, in a narrow window — gave no failure once the scroll was gone. The FLIP measurement and the overlay/docked geometry held throughout.

Evidence

  • Platform-independent checks: ./scripts/check.sh exits 0 locally.
  • macOS Electron verification (./scripts/verify.sh): not run. This was built on a Linux cloud VM. CI builds nothing for the Mac and could not verify this UI change. LUKE-159 is the manual Mac pass.

Visual evidence

Headless Chrome on Linux, from the real components and stylesheet (not the Mac app), 1400×860 window.

Before: Luke's first code opens the panel with the sidebar folded; the whole window ends up scrolled 400px sideways:
before, docked

Before: a fuzzed sequence ending in full screen; the panel at x≈0 with its tabs under the traffic lights, as in Dean's screenshot:
before, full screen

After: the same arrival, sidebar folded and unfolded:
after, docked
after, docked with sidebar

Tests

pane-motion.test.ts, through the real window (DesktopShell + useSidePanel + usePanelArrivals). New test: "the panel opening on Luke's first code slides in with the pointed line in the middle of its lines, scrolling nothing else in the window".

  • jsdom has no scrollIntoView, so the test stands one in the way a browser behaves: every box around the element scrolls sideways until the element is back in the window.
  • The test lays the arriving code pane out past the window's right edge, where the slide starts.
  • Without the fix it fails with every box from the shell down scrolled 410px. With the fix nothing scrolls sideways and the pointed line is centred in .code-lines.
  • The CSS half (overflow: clip) has no jsdom test, because jsdom applies no stylesheet and does no layout. The headless-Chrome runs above are its evidence.

Test count: +2 (pane-motion.test.ts, 9 → 11). The second, "lines Luke points at next are centred from their start, even where a long line was scrolled along", covers what scrollIntoView's inline: "nearest" used to do (raised by Devin Review): a newly pointed range after the developer scrolled a long line sideways is shown from the lines' start.

Invariants

  • ./scripts/check.sh green; ./scripts/verify.sh not run (Linux VM, see above).
  • JSON Schema goldens unchanged.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (The CSS half is covered by the Chrome evidence; see Tests.)
  • Gateway envelope goldens unchanged.
  • No new as type assertion outside the allowlisted files.
  • No Effect.runPromise/runSync/runFork outside the runtime edges.
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch.
  • No Effect, Stream, or Layer written as a statement and dropped.
  • Test count delta listed above.
  • No hand-rolled file replaced.
  • No CLAUDE.md/AGENTS.md sentence names the changed parts.
  • PRIVACY.md unchanged.
  • No dependency changes.
  • Barrel/door rule unaffected.

Notes

  • Blockers or follow-up verification: the Mac pass (LUKE-159) should confirm the arrival on a real call.

🤖 Generated with Claude Code