Agent pane: one DOM insertion no longer restyles the whole pane (pane-level :has() rules to state attributes)
Leo (rulebook): "when it comes to the left scroll rail thing, i already said we want it ultra fast ultra responsive, but the same goes for EVERYTHING IN THE WEB UI".
What was wrong
Any node inserted in the agent pane made Chromium restyle the whole pane. That covers a streamed reply's rows and tokens, a session rail tick, and a menu row. It cost 678 elements on the normal chat page and 2,186 on the 3,000-turn page, 4–9 ms of style per insertion. The draft rail investigation (#28540) found the effect. This PR measures it per rule and removes it.
The cause is one selector shape: a :has() before a combinator whose last compound has no class or attribute (… :has(…) > *, … :has(…) > :not(…), … :has(…) > span:not(…), label:has(…) *). With such a rule anywhere in the page, Chromium invalidates whole subtrees under the elements it tracks for :has() on every insertion. :has() on the subject itself, or followed by a compound with a class (.x:has(…) > .y), is cheap. The opt-in Chromium trace below measures each of the page's 143 :has() rules. With all 143 out, an insertion restyles 2 elements. With only the five below out, it restyles 43. Each of the five alone brings it back to about 870–900.
Changes (five rules)
| rule | where | replaced by |
|---|---|---|
.acpmux-composer-box:has(> .acpmux-chrome-area > .acpmux-composer-inner) > .acpmux-composer-field-menu > * (and the 11 sibling rules on the same :has, on the box, the form and the dock) | composerLocation.css | data-composer-grid that ComposerBox sets on the box and Composer sets on its form, for Claude's look (look !== "codex", the condition that already adds .acpmux-composer-inner). The asks stack's radius uses a sibling :has(~ .acpmux-composer[data-composer-grid]) |
… .cv-turn-actions…:has(> .acpmux-pr-dock) > :not(.acpmux-pr-dock), … .cv-reply-actions:has(> .acpmux-pr-dock) > :not(…) and the matching opacity: 1 rule | summary/summary.css | data-pr-dock on the action row, set by TurnFooter and ReplyActions through useHasPullRequestDock(), the same prs?.length condition under which PullRequestDock draws |
.cv-codeblock__header:has(.acpmux-pr-dock) > span:not(.cv-codeblock__actions) | summary/summary.css | removed: it repeated, declaration for declaration, the label's unconditional rule in conversation.css |
:where(…, label[for], label:has(checkbox, radio)) * { cursor: inherit } | pages/shared/desktop.css | :where(…, label) *: everything inside any label takes the label's cursor |
label:not([for]):has(disabled):not(:has(enabled)) :not(clickables) { cursor: default !important } | pages/shared/desktop.css | the subject rule alone. The locked label's contents inherit its arrow through the label rule above |
The two desktop.css rewrites cascade to the same cursors in every fixture of clickable-cursor.test.ts. That test gains four selectable-text label fixtures (a choice, a locked choice, a field label and its text field), which pass before and after. One edge differs: a descendant of a label that has its own non-clickable surface cursor rule now keeps that rule. Before, !important default overrode it for a locked label, and auto overrode it for other labels inside .selectable. No current fixture or source hits that case.
Specificity: the composer rules drop from :has(…) to one attribute. I scanned every rule that sets the same properties on the same elements, and none sits in the range that drop would change. The style dumps below check this per state.
Overlap: #28533 (bottom stack) is not touched. Its new :has() rules are the cheap shapes (subject, or a class after the combinator). bottomStack.css's own composer-inner :has rules are cheap too and stay. #27616 edits composerLocation.css lines 257–360 (the access menu). This PR edits lines 1280–1395.
No new observer, timer or listener.
Performance
Same harness, same CI job and same runner for both arms: test/pane-has-restyle.browser.test.ts (PANE_HAS_MEASURE=1), a Chromium trace of UpdateLayoutTree.elementCount and duration. The job ran after, before, after, before by applying and reverting this PR's webviews/src diff. The table gives the median of the two runs per arm. Pages: flows.html?mock&recents=demo (normal) and flows.html?mock&turns=3000&resident (3,000 turns).
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| elements restyled per inserted row node, normal page | 678 | 41 | −94% | run |
| style ms per inserted node, normal page | 3.6–4.2 | 0.55–0.72 | −83% | same |
| elements restyled per inserted row node, 3,000 turns | 2,186 | 41–46 | −98% | same |
| style ms per inserted node, 3,000 turns | 8.6–9.0 | 0.77–1.01 | −90% | same |
| elements restyled per streamed delta (12 chars / 16 ms, 2 s), normal | 185 | 45 | −76% | same |
| style ms per streamed delta, normal | 1.42 | 0.64 | −55% | same |
| largest recalc while streaming (p95 elements), 3,000 turns | 386 | 26 | −93% | same |
| style ms per streamed delta, 3,000 turns | 1.49 | 0.69 | −54% | same |
| rail wheel step, elements restyled (p50), 3,000 turns | 2,615 | 166 | −94% | same |
| rail wheel step style ms p50 / p95, 3,000 turns | 9.8 / 10.5 | 1.4 / 4.1 | −86% / −61% | same |
| slash menu open, elements restyled / style ms, normal | 1,474 / 7.5 | 259 / 2.2 | −82% / −71% | same |
| slash menu open, elements restyled / style ms, 3,000 turns | 4,466 / 17.3 | 243 / 2.5 | −95% / −86% | same |
| streaming frame p95 (headless 60 Hz) | 16.8 ms | 16.8 ms | 0 | same; headless Chromium paces frames at 60 Hz and dropped none in either arm, so this metric shows nothing here |
Visual parity
test/pane-has-parity.browser.test.ts (PANE_HAS_PARITY=1) captured 17 gallery states, each in light and dark: the composer (idle, focused, right-click menu open, long draft, image attached, queue, Codex harness), Claude's and Codex's looks, the three draft scrollbar placements, and the PR links (inline, hovered turn, one-first, no footer, code tail). All 34 stills are byte-identical between the arms in both rounds.
| state | before | after |
|---|---|---|
| Composer, right-click menu open (dark) | ![]() | ![]() |
| PR links beside a turn (light) | ![]() | ![]() |
Viewed after image: the PR links beside the turn's footer show #131 atlas-web, #130 atlas-web and +2, with the footer's own buttons hidden until hover; in the composer the right-click menu sits over the prompt and the prompt keeps its grid (+ at left, mic and Send at right). Each is byte-identical to its before still.
Tests
src/agent-session/acpmux/summary/prDockAttribute.test.tsx: a turn footer and a reply's actions carrydata-pr-dockexactly when the dock draws links. Red on the base half of the job, green on this PR.test/composer-grid-attribute.browser.test.ts: in Claude's look and Codex's, the mark stands exactly where the old:has()matched, and the box is still a grid. Red on base (Claude states), green here.test/clickable-cursor.test.ts: new label fixtures, green before and after.test/pane-has-restyle.browser.test.tsandtest/pane-has-parity.browser.test.ts: opt-in measurements, skipped unless their env var is set (no CI gate).
Observed / Not run
- Observed: the runs above.
bun run checkpassed on the head (in the same job). - Not run: a lint for new costly
:has()shapes. The repo rules say no new CI gates, so the rule is documented in desktop.css's comment instead. Also not run: a native capture. Nothing native changed, and the webview stills above are pixel-identical.
Changelog
Fixed: the agent pane no longer restyles the whole pane on every streamed row, rail tick or menu row.
Agent session: agent session
🤖 Generated with Claude Code



