Skip to content

Bind the worktree head's keys through DISCLOSE, and name the tab strip's own press - #140

Merged
max-sixty merged 4 commits into
mainfrom
fix/ci-33328596524
Aug 30, 2026
Merged

Bind the worktree head's keys through DISCLOSE, and name the tab strip's own press#140
max-sixty merged 4 commits into
mainfrom
fix/ci-33328596524

Conversation

@leaf-agent

@leaf-agent leaf-agent commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

Two leftovers from 335bf9a "Delegate page chrome to browser standards", which moved control activation off a page-wide CONTROL scope and into rows declared where each control is. Both are on ci's red list for the run this was dispatched for or reachable from the same change, and both still reproduce at 1ca4f40.

lf-worktree's head names fewer keys than work it. The commit gave the head a row binding PRESS — Enter and Space — over a button wearing aria-expanded, which is ARIA's disclosure pattern. skills/leaf/CLAUDE.md states 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: lineRows prints 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 in aria-keyshortcuts, which is written once when the scope is declared and so froze at Enter Space. Nothing tested it; the identical bug in lf-options is what #132 is for, and that PR does not reach this package.

test_a_widget_that_renames_its_role_keeps_the_press_offer_gave_it still asks for the deleted scope's word. It asserts the key line reads press it over a focused tab. press it was CONTROL's line, and 335bf9a removed the scope in the same commit that gave lf-tabs its own tab.activate row reading open the tab. The commit updated several tests in that file and dropped test_the_reference_names_the_space_that_works_a_control outright; 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 from DISCLOSE(head), the way lf-diff and lf-draft already do, and keeps its own run. DISCLOSE hands 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 from selectableOffer, so a copy frozen into thread markup has no platform pair underneath it the way a details > summary does — the row's own press is the only thing there, which is the shape #132 keeps for the sibling widget. Its line reads the way the tree is standing, and aria-expanded is written at birth so the head is never briefly a control the runtime cannot place — without that write DISCLOSE hands back both arrows, and that is the value aria-keyshortcuts is written with when the scope is declared. The repaint that keeps the line and aria-keyshortcuts together is the document's disclosure watch, which #132 moved from paintHere() to paintKeys() while this branch was open; the widget owes none of its own, which is what skills/leaf/CLAUDE.md now 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.py for 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 and aria-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, where aria-keyshortcuts reads Enter Space, the arrow moves nothing, and Enter and Space fold it. The repaint behind both surfaces is held by test_a_widgets_native_control_names_the_press_the_platform_makes, which #132 added.

Testing

uv run pytest tests at c292244 — 723 passed, 6 skipped. After merging main (which brought #132 and #143) and taking the repaint out at 890e7ff, test_render_projection.py, test_render_navigation.py, and test_render_anchors.py — 193 passed, 4 failed, all four the version-menu group #137 owns and all four failing identically on origin/main in a clean worktree. With --run-nightly, test_render_projection.py and test_render_navigation.py — 137 passed, 1 failed, that one being test_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 base 1ca4f40.

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' on main's lf-worktree.js, and the thread-markup half at aria-expanded staying "false" under Enter with the row's run removed.

What this does not fix in that run

The run reported 34 failures. Most of the rest already have a PR open: the -15 root 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. DISCLOSE exists 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 keys DISCLOSE gives 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

…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 leaf-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread skills/leaf/packages/command-hub/widgets/lf-worktree.js
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 leaf-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  • run removed from the widget row → test_worktree_evidence_names_the_arrow_that_stands_on_it fails.
  • paintKeys() removed → passes, 3 runs of 3.
  • Birth head.setAttribute("aria-expanded", …) removed → passes.
  • Probe with paintKeys instrumented to record its stack: one call per gesture, paintAnchors → paintThreadQuotes → paintKeys, and aria-keyshortcuts already reads Enter Space ArrowLeft on the read taken immediately after the press (and after a pointer click) with the widget's paintKeys() 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….

Comment thread skills/leaf/packages/command-hub/widgets/lf-worktree.js Outdated
Comment thread tests/test_render_projection.py Outdated
max-sixty pushed a commit that referenced this pull request Aug 30, 2026
)

## 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.
@max-sixty
max-sixty merged commit 71a5c17 into main Aug 30, 2026
10 checks passed
@max-sixty
max-sixty deleted the fix/ci-33328596524 branch August 30, 2026 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants