cmux NEXT: opening a chat on the prewarmed page draws it first time (a hidden adopted page reports its paint)
HOLD: with adoption on, an adopted reopen is slower than a cold open, and under load adopted opens still time out (table below). Not to land until an adopted open beats the cold one idle and under load.
Leo, 2026-11-01: "i had to retry to get it to load???" and "i hate the "Starting Claude Code... cancel" thing that it starts with then the stupid ass layout shift surely we can have a better load/faster load/intro than that"
Opening an existing chat adopts the app's prewarmed agent page again (#27759, #31033), and the adopted page now shows its chat on the first try. #31486 had turned adoption off because of this bug. This turns it back on.
Cause
The parked spare page can be hidden: Chromium marks the page hidden, so document.visibilityState is hidden. A hidden document runs no animation frames.
The page told the host it drew its chat (pane.painted) one animation frame after that render. The host keeps an adopted page away and covered until that report (AgentPaneView+SessionAdopt.swift), and the cover keeps it hidden. So the report never came. Every reopen held its cover until the loading state gave up, about 15 s later, and so did some sidebar opens.
A diagnostic bench at 7d8c57e6 read this from the page itself in each failed open:
visibilityStatewas "hidden";- the last animation frame came before the report was asked for;
- the chat itself was drawn (88 transcript rows,
chatRead,showsWhatItIs).
Fix
paintReport.tsafterDrawnFrame. The report still waits one frame, then a task. A page that is hidden, or goes hidden while it waits, reports on a task instead. The host then shows the page, and its own leave already re-syncs Chromium's visibility (AgentPaneLoadingView.leave).- One
visibilitychangelistener, removed when the report runs (stop condition). It reads no data, and it is listed incheck-switch-fetches.py.
- One
- The chat-open trace. An adopted page counts as drawn once its pane painted (
traceSurface). The page's own first frame (page_painted) came before the trace started, back when it drew New Tab, so every adopted open readpage_emptyin the bench. AgentPaneSpareReadiness.adoptsExistingChatsis gone. That was #31486's switch, so the spare serves existing chats again.
Takes back #31486: it switched adoption off until the adopted page drew its chat; this PR is that fix, so adoption is on again.
Changelog
Fixed: opening an existing chat in cmux NEXT draws it from the prewarmed page on the first try, without the long loading cover.
Tests
webviews/test/paint-report.test.ts:
- a hidden page reports although it draws no frame;
- a shown page reports after its frame;
- a page hidden while it waits reports exactly once.
Command: cd webviews && bun test test/paint-report.test.ts.
- Red on the first commit (the old behaviour, moved unchanged into
paintReport.ts): 2 fail. - Green at the head: 3 pass.
test/spare-session-adopt.test.ts passes at the head.
Performance
scripts/measure/chat-open.sh (chat-open-bench.py: milliseconds from the action to the first frame of chat content, median of 5), run on the same fleet display Mac (Mac16,10) for both columns. Before is the feat-cmux-next tip this head merges (a0741bc5, adoption off since #31486: cold opens); after is this head.
| metric | before (a0741bc5, cold) | after (fe669616, adopted) | delta | harness or run link |
|---|---|---|---|---|
| sidebar open, idle (median ms) | 770.6 | 780.2 | +9.6 | chat-open.sh, 5 runs |
| sidebar open, load | 2508.9 | 2 of 5 valid | regressed | same |
| reopen, idle | 806.2 | 1115.3 | +309 | same |
| reopen, load | 2476.5 | 2 of 5 valid (13.6 s, 15.0 s) | regressed | same |
| flashes, idle sidebar / reopen | 0 / 0 | 3 / 5 | worse | same |
At e143c04d (the paint fix only), idle sidebar was 628.6 ms and idle reopen 1092.5 ms. No idle open hangs any more: the ~15 s cover is gone. But an adopted page takes about 800 ms from adoption to its paint report, where a cold page takes about 600 ms in total, and under load adopted opens still wait out the page's 10 s hold. The likely cause is under investigation: the parked spare was seen hidden before its adoption (diagnostic run), and Chromium throttles a hidden page's tasks.
Timers and observers
One visibilitychange listener per paint report, removed when the report runs. No new timers.
Observed / Not run
- Ran:
bun run checkin webviews (green), the cmux-next pre-push gate, both bun suites above, and the diagnostic bench described above. - Ran: the CmuxNext compile check at e143c04d (passed).
- Pending: the before/after bench.
Risk: normal. This turns the warm-spare open path back on for every existing local chat. A spare that is not connected still opens the chat cold (servesChat).
Agent session: agent session
🤖 Generated with Claude Code