Agent chat: the transcript's scrollbar stays shown

Merged
TE
14d ago
+539-3

Summary

In cmux NEXT's agent chat, the transcript's scrollbar hid itself at rest. The transcript used the engine's own overlay scroller, which shows while the view scrolls and fades out about a second later. So the bar was missing whenever you stopped scrolling, and it flashed back every time a sent prompt or a streamed reply moved the followed view.

Now the transcript keeps a thin scrollbar (a thumb with no track) the whole time the chat overflows, at rest too, whatever the macOS "Show scroll bars" setting. A chat that fits shows no thumb. This is the default for everyone.

Fading is opt-in. Debug Settings > Agent chat scrollbar fades at rest (agentPane.debug.scrollerFade, off by default) makes the thumb fade in while the transcript scrolls and fade out once it stops. That's a real animation over the pane's motion durations, so it follows ui.animationSpeed and Reduce Motion, and with animations off it applies at once.

How it works:

  • webviews/src/scrollers.ts adds a shown scroller (SHOWN_SCROLLER_ATTRIBUTE). Its rules are appended after the legacy "Always" thumb rules in SCROLLER_ROOT_CSS, and their :root prefix outranks them. The thumb's ink is a registered custom property (--cmux-shown-scroller-ink). Under data-cmux-scroller-fade on <html>, that property transitions on the element and the thumb repaints with it, because a scrollbar part can't take a transition of its own.
  • Its lane is always reserved (overflow-y: scroll on no track), so the scrollbar never appears or disappears as rows arrive.
  • The transcript's spacer extends under that lane (VirtualTranscript lane), and the column math uses the scroller's full width, so rows still line up with the composer. Without this, the reserved lane would push the rows 4 px left of the composer on a wide pane.
  • WebKit doesn't restyle a scrollbar part on a CSS transition's steps (on the fleet it jumped from full ink to none), so followShownScrollerFade eases the ink once per frame from script and then hands back to the stylesheet. With a fade duration of 0 it applies at once.
  • The tunable lives in AgentPaneTunables+ScrollerFade.swift and reaches the page with the theme (scrollerFade, AgentPaneTheme.swift). shared/theme.ts toggles the root attribute.

Overlap: the gate-rules lane's SCROLLBARS-VISIBLE PR (it supersedes SCROLLBARS-THIN) rewrites the header of webviews/src/scrollers.ts, the "Always" legacy thumb rule in SCROLLER_ROOT_CSS, thin-scrollers.test.ts, check-scrollbars.sh and ThinScrollersTests.swift. This PR leaves all of those alone. It adds constants after SCROLLING_HIDE_MS and rules after the legacy thumb rules. Whoever lands second rebases. #23854 (the composer's scrollbar) and #23967 (sideways scrollers in code cards) also touch scrollers.ts additively, in other places.

Leo-ask: ask-4a87927673

Testing

webviews/test/agent-pane-scrollbar-shown.test.ts is new. It runs in Chromium (scrollbars on) and WebKit, against the pane's dev entry with a stand-in acpmux that streams a new turn:

  • The thumb is drawn, with no track, in a lane of 10 px or less: at rest, 2 s after a wheel scroll, mid-way through a new turn the view follows, and 2 s after that turn. The lane has one width on every painted frame.
  • With scrollerFade: true there is no thumb at rest. It shows on a scroll, and some screenshots during the fade-out catch it part-way between full ink and none.

AgentPaneScrollerFadeTests.swift checks that the tunable defaults to off, is registered, and reaches the theme payload.

Focused command (fleet): cmux-ci run --class light --script scripts/ci/run-webviews-tests.sh --env CMUX_BROWSER_TESTS=1 --env CMUX_WEB_TEST_FILES=test/agent-pane-scrollbar-shown.test.ts

Neighbor suites at 3faf46cac5f (fleet step a8d7dedba774cf468480bc9f) all passed: agent-pane-chat-column, agent-pane-scrollbar-full-height, agent-pane-chat-width and thin-scrollers. The new test skips an engine that can't launch on a fleet worker, as agent-pane-follow-live does. On GitHub Actions, requireBrowserLane still fails the file when an engine is missing.

ask-1953eeb021 (the flashing on send) is only partly covered here. The show/hide flicker is gone: the thumb stays drawn and the lane keeps one width on every frame. The thumb still jumps in size and position on a send, from the followed view's growth glide and the sent prompt's estimated height. #23807 takes the glide out; the rest is tracked on cmux/chat-scrollbar-smooth-thumb.

Observed / Not run

  • Observed: scripts/web-lint.sh (Oxfmt, Oxlint, type check, browser-lane and fixed-wait checks) passed. prepush-gate.py found no new violations, and check-scrollbars.sh has no webviews hits. A full tsc --noEmit of webviews was clean.
  • Not run: the green browser run at the head, and the Swift test (AgentPaneScrollerFadeTests). Both wait on the queued fleet step and on PR CI. Nothing here was built or tested on this Mac.

No capture: this is a webview-only change. The gallery advisory run renders the transcript states, and the browser test above reads the drawn thumb from real screenshots.

Changelog

  • Changed: The agent chat transcript's scrollbar stays shown while the chat overflows, instead of hiding at rest and flashing back as new messages arrive. Debug Settings can make it fade at rest.

Agent session: Air-Blue

🤖 Generated with Claude Code