cmux NEXT agent chat: a relaunch never draws a black picture of a see-through page
Leo, 2026-10-26: "also as for loading in stuff, on restarting after an update/maybe in general, we dont wanna see a black background on the agent chat. idk why it's black. chromium not loading fast enough? idk, the sidebar loads in the background/theming pretty quick. feels weird."
Leo, 2026-10-27: "Okay, well, what about in terms of when you just restart and update the app? There's still that black screen and what not in the agent chat area."
Why it was black
The agent chat shows in Chromium by default (agent.chromium is on). At quit, each shown chat saves a picture of its page, and the next launch draws that picture as the pane's first frame until the live page paints (AgentPaneLaunchImages). A released page that is built again draws its picture the same way (AgentPageHibernation).
A Chromium picture is a JPEG (Page.captureScreenshot), which has no alpha. In a glass, frosted or see-through window, or over a background image, the page is clear, and those clear pixels come out black. PageImageBackground.fill only flattens over an opaque color, so the relaunch drew that black picture for as long as the page took to paint again. The sidebar has no saved picture: it draws the theme and backdrop from its first frame, which is why only the chat looked black.
The New Tab cover already refuses a Chromium picture for this reason (PageWebView.snapshotKeepsClearAreas, #24692). The launch and hibernation pictures did not.
What changes
- Save side.
AgentPaneView.pageImage()returns nil when the picture can't keep clear pixels and the page paints no opaque background of its own. It reads that background in the pane's own theme scope (performWithTheme, so a workspace's own picture or glass counts) fromWebTheme.pageBackground, the one source of--cmux-surface-background. With no picture, the pane shows its loading state on the window's own backdrop (the theme and glass the sidebar shows), and the page fades in over that. - Draw side. The first launch after an update reads the pictures the previous version saved.
drawLastPageapplies the same rule before drawing one (AgentPaneView.savedPictureCovers), using the tab's record (isNewTabPage) for a tab its launch defers, which has no view yet. - In an opaque window, or with an opaque
appearance.surfacesbackground, the page paints opaque, so a JPEG has no clear pixels to lose. That picture still saves and draws as before, and the relaunch keeps its instant first frame. - WebKit pictures keep alpha (
snapshotKeepsClearAreas), so they are unchanged in every window. PageImageBackground.cover/coversare the one rule. The tests cover each case.- The chat-open bench gets
--record-launch SECONDS(the app's own launch recorder on each restore relaunch, frames archived) and--backdrop. Each restore line also names the saved pictures the relaunch could draw. That turns "no black frame" into something a fleet run can check.
No timers, observers or animation loops are added. No default changes. Nothing is removed.
Not in this PR
- Leo's second half of ea3e3aaab8 ("this flicker render when it starts trying to render a bunch of stuff again"): rows that draw twice after the reveal. That depends on #27365 / #26718 (upward-scroll rendering, owned by other lanes), and this PR doesn't touch it.
- 63db10208c's red error box was already fixed by #26115 (neutral reconnecting).
Risk
Low. The change is in what the quit saves, never in what a live page draws. In a translucent window with Chromium, a relaunch now shows the loading state (backdrop plus a delayed spinner) instead of a black picture, until the page paints. Opaque windows and WebKit pages are unchanged.
Validation
- App compile (fleet
compile-check --app) passed at the fix commit. - Fleet
compile-check --apppassed at 313130b. - Focused fleet suites at 313130b:
PageImageBackgroundTests,AgentPaneColdChatRevealTests,WebThemeTests: all passed. - Independent review (gpt-6-astra high, review router): SHIP at 313130b. Its two P2 follow-ups are listed below.
Relaunch on a fleet Mac, frosted window, Chromium chat
scripts/measure/chat-open.sh --cases=sidebar,restore --conditions=idle --runs=2 --backdrop=frosted, same worker class (aws-m4pro, Mac16,11). Before: this PR's bench on base (03c7706). After: dbde555; the commit after it changes only the deferred-tab path and the bench's wait. The app's own chat-open trace records, frame by frame, which surface the pane showed. A flash is a return to an empty surface after content.
| run | saved picture at quit | relaunch drew it at |
|---|---|---|
| before, restore 0 | tab_…@1x.jpg (Chromium JPEG) | 764 ms |
| before, restore 1 | tab_…@1x.jpg | 748 ms |
| after, restore 0 | none | never |
| after, restore 1 | none | never |
The restore case counted 2 flashes before and 0 after.
Performance
| metric | before | after | delta | harness |
|---|---|---|---|---|
| restore: first chat content (ms, median of 2) | 2126.3 | 2214.3 | +88 (noise: on the same runs the sidebar case moved +1410, so the after worker was slower) | chat-open.sh, frosted, idle |
| restore: saved picture drawn | yes, at 756 ms | no | the black frame is gone | same |
| restore: flashes | 2 | 0 | -2 | same |
| restore: longest empty surface (ms) | 710.4 | 1603.3 | +893: the themed loading state now shows where the black picture did | same |
No screenshots: the app's launch recorder (--record-launch) wrote no frames on these display-less fleet workers (an empty archive both times), and a capture tour can't relaunch the app. The frame-by-frame evidence is the trace above.
Follow-ups (review, P2)
- The bench matches the tag folder by substring (
tags/chatopenalso matchestags/chatopen2). - A JPEG saved while the page was clear is drawn when the window was made opaque between quit and launch. The black pixels would then show once. Persisting the capture's opacity beside the picture closes it.
🤖 Generated with Claude Code
Agent session: agent session