feat(next): scroll the session rail independently

TE
8d ago
+238-10

The compact session rail scrolls its own list once its ticks outgrow its height (design/session-rail.md, "Height and overflow"). A wheel or trackpad scroll over a rail with more ticks than fit scrolls the rail, and only the rail. A rail whose ticks all fit is not a scroll container, so the wheel over it scrolls the transcript as before.

How it works:

  • The list gets data-rail-scrolls only while it overflows. CSS gives it overflow-y: auto; overscroll-behavior: contain only under that attribute and otherwise keeps overflow: hidden, so a rail that fits never takes the wheel. The native scrollbar is hidden in both engines, so the transcript's scrollbar stays the only one on screen.
  • The transcript's reader listener is a capture listener on its scroller, an ancestor of the rail, so it runs before anything on the rail can stop the event. It now skips a wheel over a rail that scrolls (railOwnsWheel, new threadMinimap/railWheel.ts). Wheeling up over the rail no longer releases the transcript's auto-follow or ends a jump in flight. A capture listener on the list still stops the event before the transcript's bubbling listeners (glide, fold release, loading older pages, the step reader).
  • Once someone browses the rail, it keeps their offset while the transcript scrolls. Clicking a tick hands control back to the transcript. The browsing latch remembers its session, so a reused transcript that switches to another chat starts following that chat's reader again.

Leo said (2026-10-29):

"but how about fixing up the left side scroll rail thing (moving it to the left, giving it faster scrttoll/hover over) and being able to scroll it? and the various other noticeable ui or perf things"

Earlier ask behind this PR:

"Likewise, there's this odd stuff going on where sometimes the left scroll rail, it doesn't return to the left when, you know, the right pane closes and whatnot. And also along that same line, I think we should be able to have a taller max height for it, like a far taller max height and initial height for it. So it's, like, different from the rendering of the current pane and whatever. And also I think we can get stuff going a lot faster, like just being able to scroll through stuff. I mean, if you look at the ChatGPT rail, it's just super duper fast. Like they have skeletons sometimes, but a lot of the time it's also, like, they just—I mean, their stuff just renders faster, and they can even scroll through the rail thing. Like that becomes a scrollable area, and it also has a longer initial height and all that stuff."

Overflow treatment, from the gallery: the rail keeps its top and bottom edge fades and hides its native scrollbar. The visible-scrollbar option is rejected because the session-rail spec forbids a second visible scrollbar. Nothing changes at rest: the rail looks the same and only gains scrolling, so there is no new before/after still. The height gallery (threadMinimapHeight.gallery.ts) still shows the overflowing rail.

Takeover: the original owner session died (429). The 9ee8e20a review BLOCKED on (1) the ancestor capture listener still releasing follow and (2) the browsing latch surviving a session switch. Commit 456f3bbc fixes both and adds the pass-through rule.

Tests (webviews/test/thread-minimap-rail.test.tsx, thread-minimap-default-wave.test.tsx):

  • An upward wheel over an overflowing rail never reaches the reader (followTrace.input is not called). The same wheel over the transcript does.
  • A rail that fits its prompts carries no data-rail-scrolls, and the wheel over it reaches the transcript's listeners as the reader's.
  • After a session switch, a rail that was browsed in the old chat follows the new chat's reader to the top.
  • CSS: the base list is overflow: hidden, and only [data-rail-scrolls] scrolls, with overscroll-behavior: contain.

Performance

No perf numbers: this changes which element takes the wheel over the rail and adds no per-frame or per-token work, only one closest() check per wheel event in the transcript's reader listener and no new timer, observer or loop.

Agent session: agent session

🤖 Generated with Claude Code