The menu bar had the same defect; the fix is now one shared module
Re-measuring the tab-reveal after the previous commit took the mutation
count from 23 to 5. The 18 removed were the toolbar's and are confirmed
gone. The 5 that remained were all "disabled: \"\" -> \"\" [REDUNDANT]",
all in fixtures/menu.mjs's sync:
el.disabled = false
el.disabled = !(view && command && command(view.state, null, view))
Both unconditional, on the same per-update path — menu.sync runs from
the toolbar's own plugin through onSync, so it fires on every
ProseMirror state update too. Setting a reflected boolean property to
the value it already holds still sets the content attribute, so it
still records a mutation and still invalidates style, for exactly the
reason the toolbar's setAttribute writes counted.
Worth saying plainly why it was there: menu.mjs was written AFTER the
toolbar, by a different session, and reproduced the defect from
scratch because nothing stopped it. A local pair of helpers in
toolbar.mjs would have let the next surface do it again. So setAttr and
setProp move to fixtures/dom-writes.mjs, which both bars import; there
is one copy of the comparison and one place the reasoning is written
down.
menu.mjs's two roving-tabindex writes go through setProp as well. They
are not on the update path and were not part of the flash, but leaving
them direct would have meant either a structural rule that exempts them
or a module with two different disciplines. Through the helper, arrowing
along the bar also stops rewriting the five titles whose tabindex was
already right.
The guard extends the same way rather than gaining a second file.
fixtures/toolbar.test.mjs now mounts BOTH bars the way editor-entry.js
does -- one plugin, the menu on its onSync, two roots because
[docs-toolbar] and [docs-menubar] both hold data-docs-command elements
-- so the property is held once across the whole chrome: after a state
update that changes nothing, zero writes. The structural rule covers
toolbar.mjs and menu.mjs, and is now simply "no direct DOM write in
either file". Two named files with a comment saying why: plenty of
modules here write to the DOM and should, but these two run on every
state update, from one plugin, which is what makes a redundant write
cost a frame.
Watched failing first, with menu.mjs reverted and toolbar.mjs fixed. The
no-op update logged exactly four writes, every one from === to, all of
them the menu's -- and none the toolbar's, which is the previous
commit holding:
+ { from: false, id: 'menu-strong', name: 'disabled', to: false },
+ { from: true, id: 'menu-undo', name: 'disabled', to: true },
+ { from: true, id: 'menu-clear_formatting', name: 'disabled', to: true },
+ { from: false, id: 'menu-print', name: 'disabled', to: false }
- []
and the structural test: "menu.mjs should get its writers from
dom-writes.mjs". Three mutations confirm the guards still bite: an
unconditional setAttribute added to menu.mjs's sync fails both the
behavioural and the structural test (the latter naming menu.mjs:311);
deleting a write that matters fails the counterweight test; and removing
setProp's own comparison in dom-writes.mjs fails the no-op test for both
bars at once.
menu.test.mjs keeps its own tests and gains a note saying what it cannot
see: it records the state an element ends up in, not the calls made to
get there, which is precisely why this defect lived under a green gate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.amadan/ci.d/10-gate+19 −13fixtures/dom-writes.mjs+53 −0fixtures/menu.mjs+15 −4fixtures/menu.test.mjs+8 −0fixtures/toolbar.mjs+7 −37fixtures/toolbar.test.mjs+186 −97internal/docs/static/{editor.4dc0a272328103ed.js => editor.5dc0309cb2a5ef6b.js}+13 −13
Intent
<task-notification> <task-id>aac896ffcecc70973</task-id> <tool-use-id>toolu_01QJnJAeYr9QHNjv2Tb9DwWd</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-oficina-docs/4a30c557-129a-4e57-8aa3-0c7518c36cf1/tasks/aac896ffcecc70973.output</output-file> <status>completed</status> <summary>Agent "Fix the toolbar's redundant writes" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>**Status:** Done. Fix, guard, gate, and report all landed on `worktree-toolbar-flash`. **Commit:** `47c9080` — "Toolbar: write to the DOM only when the value changed" **Change:** two helpers in `fixtures/toolbar.mjs` (`setAttr`, `setProp`) that write only when the value differs; every DOM write in the module routed through them (button `disabled`/`aria-pressed`, select and combo `disabled`, `nameCombo`'s aria-label, all three `select.value` branches, `showInCombo`'s `input.value`). Resulting DOM identical; the load-bearing comments (server-rendered-disabled selects, combobox looked up not captured) untouched. **Tests:** 3 in new `fixtures/toolbar.test.mjs`, added to the gate. Seam is a recording DOM stand-in driving the real plugin (logs writes whether or not they change anything) **plus** a source-structural rule that no write bypasses the helpers — I skipped a pure-helper unit test because the defect is a bypassing call site, which it cannot see. Before: no-op update logged **37 writes, every one `from === to`** (test 1 fail); structural test fail (`0 !== 2` helpers). After: 3/3 pass. Re-adding one unconditional `setAttribute` to the loop fails tests 1 and 3; deleting the aria-pressed write fails test 2. **Gate:** `GATE EXIT: 0`, tree clean, rebuilt bundle committed (`editor.d8d3d5d9… → 4dc0a272…`). Also ran `scripts/browser-drive.sh` unsandboxed as verification: **44/44**, including bold-via-toolbar, font/style controls, undo availability, toolbar arrow keys. **Concerns:** (1) the test stubs `globalThis.document` for `showInCombo`'s one global read rather than changing the module — a stub, not injection; (2) the structural check's comment stripper doesn't parse strings/regex (exact today, false-positive-prone later, never a false pass) and its write-shape list is a denylist, not a proof; (3) `toolbar-roving.mjs` is unchanged and out of the structural rule's scope — its `setStop` writes `tabIndex` unconditionally when it runs, though `ensureReachable` early-returns on a no-op update. Full detail in `.superpowers/flash-report.md`.</result> <usage><subagent_tokens>146881</subagent_tokens><tool_uses>59</tool_uses><duration_ms>998437</duration_ms></usage> </task-notification>
Attributed to this commit: 45.3 g CO₂e (5952930 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on worktree-toolbar-flash, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
.amadan/ci.d/10-gateLoading diff…
fixtures/dom-writes.mjsLoading diff…
fixtures/menu.mjsLoading diff…
fixtures/menu.test.mjsLoading diff…
fixtures/toolbar.mjsLoading diff…
fixtures/toolbar.test.mjsLoading diff…
internal/docs/static/{editor.4dc0a272328103ed.js => editor.5dc0309cb2a5ef6b.js}Loading diff…