Bind the worktree head's keys through DISCLOSE, and name the tab strip's own press - #140
Conversation
…p's own press Two leftovers from 335bf9a, which moved control activation off a page-wide CONTROL scope and into rows declared where each control is. lf-worktree's head bound PRESS over a button wearing aria-expanded. That row is nearer than the runtime's disclosure scope, and lineRows keeps only the keys the nearer row names, so the arrow that opens the evidence went on working and stopped being named — on the key line, and in aria-keyshortcuts, which is written once at declaration and froze at "Enter Space". It now reads DISCLOSE the way lf-diff and lf-draft do, binds no run so the arrows stay a direction rather than a second toggle, writes aria-expanded at birth, and repaints its keys when that attribute changes. test_a_widget_that_renames_its_role_keeps_the_press_offer_gave_it still asked the key line for "press it", which was the deleted scope's word; the same commit gave lf-tabs its own tab.activate row reading "open the tab". The assertion and the docstring now name where the press is declared, and the two facts after it are unchanged. New coverage for the worktree head, since nothing exercised it: both surfaces at both readings, and a second arrow on an open tree.
leaf-agent
left a comment
There was a problem hiding this comment.
The worktree head is a span[role="button"] from selectableOffer, not a native disclosure, so dropping run takes its press away wherever the runtime's DISCLOSURE scope does not reach. That scope is standingOn(...), whose at reads Boolean(el?.matches?.(sel)) && !inChrome(el), and nothing has bound a press over [data-lf-offer] since 335bf9a retired CONTROL — so on a head frozen into thread markup the row names ⏎ / space and nothing runs them.
lf-diff and lf-draft survive the same shape because their disclosure is details > summary and the platform answers the pair there. test_the_arrows_say_which_way_the_section_under_the_reader_goes says so about the staged diff in as many words: "The platform's pair still works it, so what differs is the offer rather than the capability." A span has no platform half to fall back on.
Measured on this branch and on the base, appending <lf-roster><lf-agent><lf-worktree source="atlas-worktrees"> as a comment's markup and focusing the head in the threads panel:
aria-keyshortcuts |
Enter | Space | |
|---|---|---|---|
base 1ca4f40 |
Enter Space |
opens | closes |
15d18b7 |
Enter Space |
nothing | nothing |
The premise for dropping the binding — that a run over the arrows would make them a second toggle rather than a direction — does not hold. DISCLOSE only ever offers the arrow that changes the state, which is what the runtime's own row already says: "One toggle answers all three: the arrow bound is the one that changes this disclosure, so a press cannot mean anything else." Its run is focused().click(), a toggle. #132 reaches the same conclusion for the sibling widget and keeps run: () => this.#row.click() beside its keys: () => DISCLOSE(this.#row).
With run: () => head.click() restored, test_worktree_evidence_names_the_arrow_that_stands_on_it still passes whole — the second → on an open tree included, since the widget row binds only the arrow DISCLOSE named — and the head answers Enter and Space in the panel again. Nothing in the suite covers a worktree head in chrome, which is why the new test could not see this; a case beside the staged-diff one in test_the_arrows_say_which_way_the_section_under_the_reader_goes is where it would live.
The runtime's disclosure scope refuses to reach into the chrome — its `at` asks `!inChrome` — and the head is a span from `selectableOffer`, so a frozen copy in thread markup has no platform pair underneath it the way a `details > summary` does. Dropping the row's `run` left that head naming ⏎ / space with nothing running them. DISCLOSE hands over only the arrow that changes the state, so the press stays a direction rather than a second toggle, which is what #132 keeps for the sibling widget. The worktree test now walks the same head in the threads panel: the arrow is gone from aria-keyshortcuts and moves nothing there, and Enter and Space fold it.
leaf-agent
left a comment
There was a problem hiding this comment.
The run restoration in c292244 holds up — I removed it and test_worktree_evidence_names_the_arrow_that_stands_on_it fails at the frozen head's Enter, so that half of the widget row is genuinely covered. Resolving that thread.
Two findings on what the same commit does not cover, and where the repaint it adds belongs. Both come from deleting the line and re-running, on this branch's merged tree.
The paintKeys() call and the birth aria-expanded write are each unproven by the new test: delete either one and the test stays green (3/3 for the paintKeys() deletion). The docstring claims the opposite in as many words, and takes a deliberate precaution — reading aria-keyshortcuts once rather than retrying — against the two-second heartbeat. The masking repaint is not the heartbeat: it is paintAnchors → paintThreadQuotes → paintKeys, which runs on the same gesture, so aria-keyshortcuts is already correct on the read taken immediately after the press, with the widget's own writes removed.
And the repaint sits a layer below where skills/leaf/CLAUDE.md puts it. Open #132 moves disclosureWatch from paintHere() to paintKeys() so the document's watch — which already hears this exact aria-expanded write — keeps both surfaces together, and adds the sentence "a widget declaring a disclosure row owes no repaint of its own". Landing both leaves lf-worktree carrying a local repaint that the doc arriving with #132 says it does not owe, and a second full scope revalidation per toggle behind the watch's.
Separately, and worth saying because it raises this PR's urgency rather than lowering it: the test_a_widget_that_renames_its_role_keeps_the_press_offer_gave_it change is a real red-main fix, not test polish. On the merged tree the base assertion fails — the key line reads ⏎ / space open the tab, which is lf-tabs' own tab.activate row from 335bf9a, and nothing in the tree says "press it" any more. main still carries the old assertion.
How each was measured
All on the PR's merged tree (c292244 + base), uv run pytest against the sandbox's own browser.
runremoved from the widget row →test_worktree_evidence_names_the_arrow_that_stands_on_itfails.paintKeys()removed → passes, 3 runs of 3.- Birth
head.setAttribute("aria-expanded", …)removed → passes. - Probe with
paintKeysinstrumented to record its stack: one call per gesture,paintAnchors → paintThreadQuotes → paintKeys, andaria-keyshortcutsalready readsEnter Space ArrowLefton the read taken immediately after the press (and after a pointer click) with the widget'spaintKeys()gone. - Base assertion restored in
test_a_widget_that_renames_its_role_keeps_the_press_offer_gave_it→ fails; actual key line is⏎ / spaceopen the tabesclet go← / →walk the tabs….
) ## Problem `test_render_gate.py::test_example_renders[corpus]` has been red on every `ci` run since [26dbf42](26dbf42) "Add external link treatment", in both colour schemes, and it still reproduces deterministically on `1ca4f40` in 11 seconds. #132 diagnosed it as "a different finding, from 26dbf42's external-link treatment rather than from 335bf9a" and left it; no PR or issue owns it. ``` assert ['[light] <lf-tab id=corpus-pr-walkthrough> puts "opens in a new tab" under .lf-ui, where no comment can reach it', '[dark] ...same...'] == [] ``` The subject is not the example. `render_version` is the pass `version check --render` runs on an agent-authored page, so any page with a cross-origin link inside a widget now fails handover on a word the reader is never shown. [`unreachableWords`](https://github.com/max-sixty/leaf/blob/03b4a0b2776102041a2d48580046fbf03b5e3446/skills/leaf/scripts/leaf/render-checks/reachability.js#L208) walks every text node under a widget's `.lf-ui` and reports the ones no declaration claims. Its excuse list held `.lf-quiet` and nothing else. 26dbf42's `renderExternalLinks` writes a note beside each external link — `class="lf-ui lf-external-note"`, `hidden`, textContent "opens in a new tab" — as the `aria-describedby` target for the link, and the walk found it: not `data-lf-said`, not inside the link's own `CONTROL` box (a describedby target is referenced by id, so it sits beside the link rather than in it), not `.lf-quiet`. ## Solution Excuse `[hidden]` beside `.lf-quiet`, one line and its comment. The check's own opening sentence is "Words the page shows that no user can select", and a `[hidden]` subtree shows none. The note cannot simply become a `.lf-quiet` word instead — which would have been the tidier answer, since `quietWord` is the repo's one idiom for "a word for a reader listening, silent on screen". `renderExternalLinks` is enrolled on declared shadow roots as well as the document, and the note is placed inside whatever root its link stands in, while `.lf-quiet`'s clip is a rule in the document stylesheet that no shadow tree adopts (only `MARK_RULES` is adopted, in `shadow.js`). A `.lf-quiet` note in an `x-shadow` widget would render as visible text next to the link. `hidden` is the silence available in every root. `[hidden]` is safe for this reading to trust rather than a hole for hidden-but-revealable words: the browser removes the attribute when it reveals `hidden="until-found"` content, so a word the reader finds is a word this check sees again, at the moment it is on screen. The two sibling gate readings that ask what a box shows already spell the same pair — [`widgets.js`](https://github.com/max-sixty/leaf/blob/03b4a0b2776102041a2d48580046fbf03b5e3446/skills/leaf/scripts/leaf/render-checks/widgets.js#L146) and [`standalone.js`](https://github.com/max-sixty/leaf/blob/03b4a0b2776102041a2d48580046fbf03b5e3446/skills/leaf/scripts/leaf/render-checks/standalone.js#L34). No assertion is loosened and no runtime file is touched. `skills/leaf/CLAUDE.md`'s row for this gate already reads "**visible** page words remain in reachable flow". ## Testing - `test_example_renders[corpus]` and `test_example_renders[pr-walkthrough]` — pass with `--run-nightly`; the first fails on `origin/main` unchanged at the two findings above. - `test_render_reports_words_a_widget_puts_out_of_reach` gains the pair: the same `.lf-ui` note inside `lf-option` reports nothing when `hidden` and is still reported when shown. Bug-back: with the `[hidden]` excuse reverted, the hidden half fails with exactly the corpus's finding. - `uv run pytest tests` — 723 passed, 6 skipped, on this branch rebased onto `c7b3706`. - `uv run pytest tests/test_render_commands.py tests/test_render_gate.py --run-nightly` — 77 passed, 4 failed, and all four are in the failed run's own list and owned elsewhere: `test_the_render_gate_measures_sideways_room_at_the_root_scrollport`, `test_the_reader_draws_an_edge_to_the_width_they_want[comments]` and `test_a_window_with_no_room_for_a_chosen_width_does_not_un_choose_it[comments]` are #121's `-15`, and `test_a_reader_arrives_at_what_they_left_rather_than_watching_it_arrive` is the intermittent one #121 documents. - `node --check`, pinned `prettier --check`, `ruff check` and `ruff format --check` pass on the two touched files. `pre-commit` is not on this sandbox's PATH, so its remaining hooks went unrun here. The preview digest is unmoved: `capture_input_files()` hashes `examples/`, `skills/leaf/assets`, the selected packages and three site scripts, and `skills/leaf/scripts/` is in none of them. ## What this does not fix The failed run had 32 red. Everything else in it sits with an open PR or issue — #121, #132, #134, #136, #137, #138, #140, #141, and issues #133 and #139 — with two exceptions I did not reach: - `test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus` reports one unlit ring, `version-diff (.lf-version-diff:is(:focus-visible, .lf-focus-visible))`. That control is inside the versions menu #137 is rewriting, so it is likely downstream of that branch rather than its own cause; I have not verified it against #137's tree. - `test_render_navigation.py::test_a_widget_that_renames_its_role_keeps_the_press_offer_gave_it` is the generic-press question #132 raised and deliberately left as a product decision. --- Automated fix for [failed run](https://github.com/max-sixty/leaf/actions/runs/33330752582) Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
#132 landed while this branch was open. It moves `disclosureWatch` from `paintHere()` to `paintKeys()`, so the watch that already hears this head's `aria-expanded` write now keeps both surfaces naming the row's keys together, and `skills/leaf/CLAUDE.md` states the consequence: "a widget declaring a disclosure row owes no repaint of its own". The widget's own `paintKeys()` is that repaint, so it goes, and the guarded write goes back to the unconditional one it replaced — the watch compares each record against the attribute's current value, so restating it paints nothing. The birth `aria-expanded` write stays, and its comment now says what it buys: without it `DISCLOSE` reads a control it cannot place and returns both arrows, which is the value `aria-keyshortcuts` would be written with when the scope is declared. `lf-options` keeps the same write for the same reason. The new test's docstring claimed the two surfaces fail apart and that this test holds both, which was true of the tree it was written against and is not true now: the repaint belongs to the watch, and `test_a_widgets_native_control_names_the_press_the_platform_makes` is what holds it. What this test holds is the row — that the keys it names come from `DISCLOSE`, on both surfaces and in both places the head stands.
Problem
Two leftovers from 335bf9a "Delegate page chrome to browser standards", which moved control activation off a page-wide
CONTROLscope and into rows declared where each control is. Both are onci's red list for the run this was dispatched for or reachable from the same change, and both still reproduce at1ca4f40.lf-worktree's head names fewer keys than work it. The commit gave the head a row bindingPRESS— Enter and Space — over a button wearingaria-expanded, which is ARIA's disclosure pattern.skills/leaf/CLAUDE.mdstates what that costs: "every row over a disclosure reads [DISCLOSE] — this scope's, and a widget's own row re-wording the same press. Two rows naming different sets is not two promises but one:lineRowsprints the nearer row and drops the other whole, so a widget naming one key fewer takes the rest off the line". The widget row is nearer, so the arrow that opens the evidence went on working and stopped being named — on the key line and inaria-keyshortcuts, which is written once when the scope is declared and so froze atEnter Space. Nothing tested it; the identical bug inlf-optionsis what #132 is for, and that PR does not reach this package.test_a_widget_that_renames_its_role_keeps_the_press_offer_gave_itstill asks for the deleted scope's word. It asserts the key line readspress itover a focused tab.press itwasCONTROL'sline, and 335bf9a removed the scope in the same commit that gavelf-tabsits owntab.activaterow readingopen the tab. The commit updated several tests in that file and droppedtest_the_reference_names_the_space_that_works_a_controloutright; this assertion was missed, so the test fails before reaching the two facts it exists to hold.Solution
lf-worktree's head takes its keys fromDISCLOSE(head), the waylf-diffandlf-draftalready do, and keeps its ownrun.DISCLOSEhands over only the arrow that changes the state, so the press stays a direction rather than a second toggle; it also answers for where the head stands, and the runtime's disclosure scope stops at the chrome. The head is a span fromselectableOffer, so a copy frozen into thread markup has no platform pair underneath it the way adetails > summarydoes — the row's own press is the only thing there, which is the shape #132 keeps for the sibling widget. Itslinereads the way the tree is standing, andaria-expandedis written at birth so the head is never briefly a control the runtime cannot place — without that writeDISCLOSEhands back both arrows, and that is the valuearia-keyshortcutsis written with when the scope is declared. The repaint that keeps the line andaria-keyshortcutstogether is the document's disclosure watch, which #132 moved frompaintHere()topaintKeys()while this branch was open; the widget owes none of its own, which is whatskills/leaf/CLAUDE.mdnow says in as many words.The tabs test asserts
open the tab— the strip's own word for the same press — and its docstring now describes where that press is declared rather than the scope that used to supply it. Everything after the assertion is unchanged: Enter still has to re-select the focused tab, and Space still has to be consumed rather than scrolling the page out from under it.New coverage in
test_render_projection.pyfor the worktree head, since nothing exercised it. It holds the row rather than the repaint: on the page, both surfaces naming the row's keys — the line andaria-keyshortcuts— plus a second→on an open tree, so a scope answering nothing at all could not pass it. In a message: the same head in the threads panel, wherearia-keyshortcutsreadsEnter Space, the arrow moves nothing, and Enter and Space fold it. The repaint behind both surfaces is held bytest_a_widgets_native_control_names_the_press_the_platform_makes, which #132 added.Testing
uv run pytest testsatc292244— 723 passed, 6 skipped. After mergingmain(which brought #132 and #143) and taking the repaint out at890e7ff,test_render_projection.py,test_render_navigation.py, andtest_render_anchors.py— 193 passed, 4 failed, all four the version-menu group #137 owns and all four failing identically onorigin/mainin a clean worktree. With--run-nightly,test_render_projection.pyandtest_render_navigation.py— 137 passed, 1 failed, that one beingtest_the_arrows_say_which_way_the_section_under_the_reader_goes, which is #132's settled ask and untouched here; it fails the same way at the merge base1ca4f40.Each half of the new test was run against the code it holds and fails there: the page half at
assert 'Enter Space' == 'Enter Space ArrowRight'onmain'slf-worktree.js, and the thread-markup half ataria-expandedstaying"false"under Enter with the row'srunremoved.What this does not fix in that run
The run reported 34 failures. Most of the rest already have a PR open: the
-15root scrollbar-gutter family is #121 (assert -15 == 0, the drawn edge landing a bar's width off the hand, the covering panel's blank strip), the version-menu group is #137, the settled ask's disclosure keys were #132, now merged, the margin and float placements are #138, the gallery stills are #134, and the sidebar's stated window is #136.No guard stops the third widget from declaring a fixed key pair over a disclosure.
DISCLOSEexists so the two surfaces cannot come apart, but a widget has to remember to call it, and two of the four widgets over a disclosure did not. A corpus-wide check — every focusable disclosure names the keysDISCLOSEgives it — would catch the next one, and is left out of this PR because it belongs beside the corpus checks rather than in a fix for two widgets.Automated fix for failed run