Agent chat: the transcript's scrollbar stays shown
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.tsadds a shown scroller (SHOWN_SCROLLER_ATTRIBUTE). Its rules are appended after the legacy "Always" thumb rules inSCROLLER_ROOT_CSS, and their:rootprefix outranks them. The thumb's ink is a registered custom property (--cmux-shown-scroller-ink). Underdata-cmux-scroller-fadeon<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: scrollon 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
followShownScrollerFadeeases 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.swiftand reaches the page with the theme (scrollerFade,AgentPaneTheme.swift).shared/theme.tstoggles 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: truethere 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
- red, test-only commit e87cc56e06a8d44b8e75c1288a66610a5d5d21ae (fleet step 9bf18e92573d9088bd1e55b1): fails as expected in both engines. No sample shows the thin thumb (rest, after a scroll, mid-turn, after the turn), and with the fade on there's no thumb while scrolling either.
- green, ca25141e5cebf4eab8de3c3567b47d4994a98c14 (fleet step 37648928c6595b0ee9b1ddd7): 3 of 4 cases passed in Chromium and WebKit. WebKit's fade case failed with no part-way frames, because the thumb only snapped between full and none. 2b0bbd4e44e fixes that (above).
- green, head 2b0bbd4e44ecdfd13094eeb324e336599d65fe36: pending (fleet step 459c041eddb100e61174b030, queued) and PR CI.
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.pyfound no new violations, andcheck-scrollbars.shhas no webviews hits. A fulltsc --noEmitof 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