Skip to content

Read the settled ask's disclosure keys from DISCLOSE, and repaint every disclosure row's shortcuts - #132

Merged
max-sixty merged 3 commits into
mainfrom
fix/ci-33324080398
Aug 30, 2026
Merged

Read the settled ask's disclosure keys from DISCLOSE, and repaint every disclosure row's shortcuts#132
max-sixty merged 3 commits into
mainfrom
fix/ci-33324080398

Conversation

@leaf-agent

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

Copy link
Copy Markdown
Collaborator

Problem

Run 33324080398 failed with 63 tests red on 6794ddc. The group #127 recorded as unfixed under "The generic CONTROL contract went with the old offer" was two readings that went quiet when 335bf9a moved widget chrome onto native elements. The selectableOffer half has since landed on main, so this branch is only the other one.

335bf9a gave lf-options' settled row a keys() scope binding PRESS. lineRows gives a nearer scope only the bindings it names, so the runtime's disclosure scope was left holding the arrow alone. Standing on a settled ask, the key line read ⏎ / space "open or close" beside a stray "open" — two chips for one press, and neither saying which way this section goes.

Solution

The row's bindings come from DISCLOSE. leaf.js's note on that primitive asks for this by name — "a widget's own row re-wording the same press in its own terms … named once here so the two cannot come to name different sets" — and the arrows test asks for it in its docstring too. The line word follows the row the way the runtime's own does, so the two merge into one chip. The scope's title and does string are untouched, which is what test_settled_options_collapse_without_going_out_of_reach asks of the "?" reference, and the row states aria-expanded at birth so the pair DISCLOSE reads is never briefly incomplete.

And the shortcuts attribute repaints with it — from the watch, not from the widget. A row bound through DISCLOSE answers from the disclosure's own state, and that state is named on two surfaces: the line the reader sees, and the aria-keyshortcuts a listener is read. Only the line had a watch. keys() writes the attribute once at declaration, so an open group's line read ⏎ / space / ← close while its attribute still said Enter Space ArrowRight — naming the arrow that no longer moves the section and withholding the one that does, on the surface a screen reader gets.

The repaint belongs in the document's disclosure watch rather than in lf-options. That MutationObserver over open and aria-expanded is already the thing that hears both spellings of the write, in the page and in every shadow root shadowStage hands it; paintKeys() is the superset of the paintHere() it was calling, and ends in it. So one call covers every DISCLOSE row at once — including the two that already existed and had no repaint at all, lf-diff's file summary and lf-draft's edit history, both of which carried the exact defect described above on a widget this branch does not touch. lf-options drops its own call and the import behind it, and a widget declaring a disclosure row owes no repaint of its own.

Testing

On 0d7e0d0 with nothing applied, then on 0f95be6:

test 0d7e0d0 0f95be6
test_render_navigation.py::test_the_arrows_say_which_way_the_section_under_the_reader_goes fail — ⏎ / space "open or close" then "open" pass
test_render_anchors.py::test_a_widgets_native_control_names_the_press_the_platform_makes pass — it did not read the attribute pass, with the two new readings
tests/test_render_options.py (whole file, --run-nightly) 1 failed 1 failed — the same test_a_specimen_holds_a_wide_exhibit_inside_the_column

Both tests read aria-keyshortcuts once and unretried, for the same reason they already read the line that way: the two-second heartbeat repaints scopes too, so expect(...).to_have_attribute goes green on whichever tick lands inside its five-second budget. Putting the watch back on paintHere() fails both — 'Enter Space ArrowRight' == 'Enter Space ArrowLeft' on the settled row, and the mirror of it on the staged diff summary.

  • uv run pytest tests — 723 passed, 6 skipped, on the code as pushed. (The skills/leaf/CLAUDE.md paragraph was written after that run and is documentation only.)
  • tests/test_render_navigation.py + tests/test_render_widgets.py --run-nightly: 4 failed, 119 passed — the same four already standing on this branch's previous head.
  • tests/test_render_anchors.py --run-nightly: 4 failed, 53 passed; all four reproduce with these changes reverted, so none is this change.
  • pre-commit run --files over the four touched files: every hook passes.

Note that ci runs only the everyday suite on a pull request, so this PR's own checks do not exercise any of the browser tests above.

Still red on main, not addressed here


Automated fix for failed run

@leaf-agent

Copy link
Copy Markdown
Collaborator Author

One coupling worth knowing at merge time, from the next run's ci-fix (33326754065): lf-command.js and lf-worktree.js are both preview inputs. preview_build.capture_input_files() hashes every file under each package named in examples/layer.json, and command-hub is the one entry there — so this branch moves docs/example-previews.json's inputs_sha256 and test_site.py::test_the_public_catalog_is_a_visual_index_of_full_page_routes needs scripts/example-previews.py rerun after it lands.

That test is already red on main for the same reason (ac5ba54 changed the runtime without a recapture), so this doesn't add a failure. #133 has the rest, including why the rerun wants the authoring machine rather than a runner.

I reproduced the selectableOffer half of this change independently before finding the PR, and can confirm the two test_example_renders cases it names go green on d7f9d0f: the theme is already written for it — .lf-command-facts > [role="button"] matches the span and never matched the <button>, so the fleet counts get their chip capsule back along with their selectability.

@leaf-agent

Copy link
Copy Markdown
Collaborator Author

One more test this closes, which isn't in the table above: test_render_controls.py::test_the_composed_corpus_declares_every_control_row_archetype. Same three counts, same root cause, and it fails for a reason the reachability report doesn't name — the command-view archetype's coverage is .lf-command-facts > [role=button], which nothing matched while the counts were native buttons. So each count reported no archetype, and command-view never entered observed, which the sweep's closing observed == expected asks for too. The archetype table and the corpus panel both arrived in fff16e6, so run 33326403400 is the first one it went red on; it is still red at d8d1f1e.

Measured on this branch's head, 0462dec, with --run-nightly -n0:

test d7f9d0f 0462dec
test_the_composed_corpus_declares_every_control_row_archetype fail — Command: button "3 running": no archetype (×3) pass
test_example_renders[command-hub] fail pass
test_example_renders[corpus] fail pass

Reading it alongside the theme point already on this thread: the coverage selector and the stylesheet were both written for a span wearing the role, so the conversion is what the surrounding code already assumed rather than a new shape.

@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 three readings the description names do land, and I reproduced the before/after against the merge base d8d1f1e rather than taking the table on trust: test_example_renders[command-hub], test_example_renders[corpus] and test_the_arrows_say_which_way_the_section_under_the_reader_goes all go red→green, and so does test_render_controls.py::test_the_composed_corpus_declares_every_control_row_archetype, which the description does not claim. Nothing regressed — every other failure in test_render_navigation.py, test_render_gate.py, test_render_controls.py, test_render_options.py, test_render_projection.py and test_render_widgets.py under --run-nightly reproduces identically with the three widget files reverted to their base versions.

One finding inline: the settled row's dynamic bindings reach the key line but not aria-keyshortcuts, so that attribute now promises a key that does nothing.

Nightly runs, this head against the merge base

uv run pytest tests/test_render_navigation.py tests/test_render_gate.py tests/test_render_controls.py --run-nightly — 17 failed on base, 14 on this head. The four that flipped are the ones listed above. test_render_commands.py+test_render_options.py: 1 failed both ways (test_a_specimen_holds_a_wide_exhibit_inside_the_column, confirmed on base at -n0). test_render_projection.py+test_render_widgets.py: 3 failed both ways, all three confirmed on base.

Two of the 14 did not fail on the base run of the same command (test_a_reader_arrives_at_what_they_left_rather_than_watching_it_arrive, test_covering_panel_takes_the_page_scroll_with_it). Neither is this change: the second reproduces on base at -n0, and the first is a coin-flip in both trees — 12 runs at -n0 on this head gave 5 passed / 7 failed, 9 runs on base gave 5 passed / 4 failed, always on the same CSSTransition padding-* 220ms at lf-new reading. That is the read-before-the-page-is-ready class running-tend describes, not a regression.

The worktree head's press survives the span conversion — test_render_projection.py::test_command_hub_derives_the_operator_reading_from_its_goal_tree focuses #tree-w-1 > .lf-worktree-snapshot > .lf-worktree-head and presses Enter twice, and passes here.

Comment thread skills/leaf/packages/default/widgets/lf-options.js
335bf9a gave lf-options' settled row a keys() scope binding PRESS. A
nearer scope keeps only the bindings it names, so the runtime's own
disclosure scope was left holding the arrow alone: standing on a settled
ask the key line read "⏎ / space  open or close" beside a stray "→
open", instead of one chip saying which way this section goes.

leaf.js's note on DISCLOSE asks for exactly this — a widget re-wording
that press reads its bindings from the primitive, so the widget's row
and the runtime's cannot come to name different sets. The row's section
and wording in the "?" reference are unchanged; only its binding set and
its line word now follow the row. The row also states aria-expanded from
birth, so the pair DISCLOSE reads is never briefly incomplete.
The row's bindings now answer from its own aria-expanded, and only the
key line had a watch: keys() writes aria-keyshortcuts once at
declaration, and the disclosure watcher repaints the line alone. So the
attribute a screen reader reads kept promising the arrow that no longer
moves the section and withholding the one that does, while the line
beside it was right.

paintKeys() where #open already writes the pair, which is the repaint
scopes.js asks of a control whose keys change with its state. The arrows
test reads the attribute in both states, once and unretried for the
reason it already reads the line that way — the heartbeat repaints
scopes too, so a retrying assertion goes green on whichever tick lands
inside its budget.
@leaf-agent leaf-agent changed the title Put a widget's page-speaking control text back where a selection can reach it Read the settled ask's disclosure keys from DISCLOSE, and repaint them with its state Aug 30, 2026

@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 disclosure reading lands, and I reproduced it rather than taking the table on trust: test_the_arrows_say_which_way_the_section_under_the_reader_goes goes red→green on 0d7e0d0e234822, and the nightly test_render_navigation.py + test_render_widgets.py pair is 4 failed / 119 passed on this head — the same four the description already accounts for (test_a_widget_that_renames_its_role_keeps_the_press_offer_gave_it plus the three standing test_render_widgets.py ones). Reading the row's bindings through DISCLOSE is what leaf.js's note on that primitive asks for, and writing aria-expanded before the keys() call is what keeps the declaration from naming both arrows.

One finding inline: the repaint is put in the widget rather than in the watcher that already owns this state transition, and the two sibling widgets reading DISCLOSE off a <summary> keep the exact defect this PR just fixed.

Smaller, and only because the diff makes it false: #isOpen's declaration comment still reads #open renders it, nothing reads it back — the new line cell reads it back.

Comment thread skills/leaf/packages/default/widgets/lf-options.js Outdated
The repaint the settled ask carried was widget-local, so every future row
bound through DISCLOSE had to remember it — and the two that already exist,
lf-diff's file summary and lf-draft's edit history, never did: their
aria-keyshortcuts kept whichever way the section was standing when the scope
was declared, naming the arrow that no longer moves it.

The document's disclosure watch is the thing that hears both spellings of the
write, in the page and in each shadow root. Painting the register there rather
than the line alone covers every DISCLOSE row at once, so lf-options drops its
own call and the import behind it.

test_a_widgets_native_control_names_the_press_the_platform_makes now reads the
staged diff summary's aria-keyshortcuts either side of a toggle, once and by
the line's own clock; both it and the settled ask's reading fail with the watch
back on paintHere.
@leaf-agent leaf-agent changed the title Read the settled ask's disclosure keys from DISCLOSE, and repaint them with its state Read the settled ask's disclosure keys from DISCLOSE, and repaint every disclosure row's shortcuts Aug 30, 2026
leaf-agent added a commit that referenced this pull request Aug 30, 2026
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.
max-sixty pushed a commit that referenced this pull request Aug 30, 2026
… after the chrome change (#138)

## Problem

[Run
33327310444](https://github.com/max-sixty/leaf/actions/runs/33327310444)
on d2dd825 failed with 34 tests red. Most of them are owned: #121 has
the `scrollbar-gutter` set, #132 the generic CONTROL contract, #134 the
catalog stills, #136 the sidebar width, #137 the versions menu, and #133
tracks the digest. What was left unowned is five tests that all fail on
one surface — what the panel's opening does to the page around it — and
each is a reading
[335bf9a](335bf9a)
"Delegate page chrome to browser standards" moved without a second
reading moving with it. All five pass at 335bf9a's parent; each was
reproduced at `-n0` before it was touched.

## Solution

Four readings, each corrected where it is taken. No assertion is
loosened and no test is changed.

**The margin's 900px floor asks the window again.** That number is half
of a pair: `@container lf-shell (min-width: 900px)` grants the rail and
the marker's strip, and `@media screen and (max-width: 899px)` stops
drawing the living margin at all. 335bf9a converted the grant to a
container query and left the hide a media query, so the two came apart
the moment the panel narrowed body: at 1200px with the panel open the
shell is 765px, the grant lapsed, and the markers stayed on screen with
no strip reserved for them. Measured on `SUGGESTION_PAGE`, `--strip-r`
resolved to `max(0px, 0px, 0px, 0px)` and every suggestion's control row
docked beside an empty rail. The floors below it stay container queries:
1152 and 1416 ask how much room is left beside the panel, which is the
question the runtime's old `data-lf-cramped` veto asked, and their
answer is supposed to move with the panel.

**Body's inline size is back under the layout observer.** 335bf9a
dropped `layoutSizes.observe(document.body)`, and with it the only thing
that re-ran `syncLayout` while the panel's eased margin was still
narrowing the page. `setPanel` runs one synchronous pass at the press,
reads the wide box, and returns; nothing reads the narrow one. So a
composer placed in a wide window's margin kept a place the narrowed page
no longer had — an absolute child past body's client box, which is
sideways-scrollable overflow, with the box standing on the panel that
displaced it. Restored with the content-box width filter the removed
code carried, so ordinary document-height growth still does not feed
back into a writer that reserves flow content.

**The panel layer is shown before anything is rendered into it.** The
panel is a `<dialog>` now, and a dialog nobody has shown is `display:
none` — but `setPanel` called `renderPanel()` first and
`syncPanelLayer()` (which calls `show()`) second. `renderPanel` is where
the anchor pass runs for the threads it draws, and a mark hangs on the
boxes its element shows through, so a widget an agent sent in a reply
resolved to an element measuring 0×0, took no mark, and never got one
afterwards: the thread stayed open in the panel pointing at nothing on
either side. Swapping the two lines is the whole fix; the focus target
is `threadsBox`, which the render fills rather than replaces.

**The margin preview shows without naming the marker as its source.**
`showPopover({source})` puts the card in the sequential focus order
directly after the control that invoked it — but this card is not
invoked. It opens on the marker merely taking focus, so a reader walking
the margin was made to Tab through a close button and two preview
actions they had not asked for, three stops between one suggestion's
controls and the next, with the card's contents changing under them as
they went. Its position comes from the `anchor-name` the code already
writes on the button, so the placement asks nothing of `source`.

## Testing

- Everyday suite green on this branch: `uv run pytest tests` — 723
passed, 6 skipped.
- The five: `test_suggestion_controls_stay_out_of_the_column`,
`test_suggestions_sharing_a_block_keep_source_and_keyboard_order`,
`test_a_settlement_in_a_reply_leaves_its_own_anchor_on_the_page`,
`test_a_mark_in_the_layer_promises_no_press_the_layer_will_not_take`,
`test_a_float_the_panel_displaces_hands_the_page_no_sideways_scroll` —
all pass with `--run-nightly`.
- Whole files: `test_render_margin.py`, `test_render_conversations.py`,
`test_render_widgets.py`, `test_render_outbox.py`,
`test_render_navigation.py` — the only failures left in them are ones
that also fail on `origin/main` unchanged, at identical values.
- `prettier --check` and `node --check` pass on both JavaScript files;
CSS is prettier-excluded here by `.pre-commit-config.yaml`.

<details><summary>How each was measured</summary>

The margin floor, on `SUGGESTION_PAGE` at 1200×900, before and after
pressing Threads:

```
BEFORE  bodyRight 1185  main 768  --strip-r "max(0px, 250px, 0px, 0px)"  --claim-rail 250px
AFTER   bodyRight  765  main 765  --strip-r "max(0px, 0px, 0px, 0px)"    --claim-rail ""
```

The same probe at 335bf9a's parent reports `--claim-rail: 250px` in both
postures, with `data-lf-cramped` standing — d226ee2 had already taken
that veto off the rail claim, so the 900 floor was the window's question
and nothing else.

The float, on `LONG_PAGE` at 1600×900 with the composer in the margin
and the panel opened:

```
body.scrollWidth 1487   body.clientWidth 1165   composer.left 1166.9   composer.right 1486.9
```

The mark, on `REPLY_TRAVEL_PAGE` with an `lf-options` frozen into a
reply: `#tv-decision` is present and `checkVisibility()` is true once
the panel is open, its box 355×32, and `.lf-mark-el` is on nothing — at
+6s as much as at +0. The same probe at 335bf9a's parent has the mark
the moment the panel opens.

The tab walk from a suggestion's Accept, before: accept → reject →
marker → **preview close → preview action → preview action** → next
row's accept.

</details>

## What this does not fix

Two of the four groups #127 recorded are still open, and both are a
product decision rather than a stale reading.

- **A covering panel is a modal dialog now**, so the document behind it
is inert:
`test_selection_hints_do_not_name_page_content_behind_a_covering_panel`
presses `s` at 700×900 and gets no hints because the page cannot hear
the key. Whether a covering panel should take the page out of reach
entirely, or cover it and leave it working, is the call —
`test_coarse_pointer_chrome_gives_its_compact_controls_humane_aims`,
`test_workspaces_replace_each_other_instead_of_stacking[500]` and
`test_coarse_pointer_resize_reach_stays_reachable_without_trapping_scroll`
fail as `<dialog … class="lf-ui lf-panel open"> intercepts pointer
events` on the same fact.
- **Escape belongs to whichever layer the browser is holding** — the
four `test_render_anchors.py` failures and
`test_esc_hands_the_page_back_after_it_has_closed_the_last_panel`. #137
is open on this.

Separately, [0d7e0d0](0d7e0d0)
"Open threads beside their source" landed while this was being verified
and brought two of its own new tests red on `main` —
`test_the_shipped_long_thread_opens_beside_its_source_in_the_right_margin`
and
`test_the_full_thread_posture_follows_the_page_container_and_left_claims`.
They fail identically with and without this branch's changes; noted here
so the next run does not re-derive them.

---
Automated fix for [failed
run](https://github.com/max-sixty/leaf/actions/runs/33327310444)

---------

Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
max-sixty pushed a commit that referenced this pull request Aug 30, 2026
#141)

## Problem

[`ci` run
33327519405](https://github.com/max-sixty/leaf/actions/runs/33327519405)
on e7299eb failed with 33 tests red. One of them is
`test_render_controls.py::test_esc_hands_the_page_back_after_it_has_closed_the_last_panel`,
which #133 lists among the unowned remainder ("`esc` handback"). It
still reproduces deterministically on `main` at 1ca4f40, in about 7
seconds.

The test's first assertion after a pointer open is the one that fails:
the panel is visible and the toggle is no longer focused.

```
FAILED tests/test_render_controls.py::test_esc_hands_the_page_back_after_it_has_closed_the_last_panel
E       AssertionError: Locator expected to be focused
E       Actual value: inactive
E         - waiting for locator(".lf-threads-toggle")
E           14 × locator resolved to <button aria-expanded="true" class="lf-btn lf-threads-toggle" ...>
tests/test_render_controls.py:2089
```

`syncPanelLayer` raises the panel with `panel.show()` beside the page
and `panel.showModal()` over it. Both run the dialog focusing steps, so
the browser moves focus into the dialog whichever one is called — the
panel takes the focus off whatever raised it. The function already knew
to put focus back, but only for a focus it found *inside* the panel:
`active` is `null` whenever the reader was standing outside it, and the
restore arm reads `active`. So the pointer route lost the toggle, and
what the test's docstring bills for lands: the button holds
`aria-expanded` with no ring on it, and the reader's next Space is that
button rather than the page's scroll — the panel they dismissed comes
back and nothing says why.

## Solution

Carry the element that was standing outside the layer alongside the one
found inside it, and restore that too when nothing else claims the
focus. Beside the page a panel is a column that arrived, so the reader
stays where they were; a covering panel is a mode and still takes them
in, which is the existing `focusEnteredLayer` arm and is unchanged. Body
is not somewhere to put anyone back, so it is not carried.

This is `syncPanelLayer`'s own restore, extended to the case it did not
name — not a second focus policy beside it, and not a call at the press
site, which would have to be repeated at every door the panel has.

## Scope

Not the other 32. Measured against the current tip, they are owned or
out of reach here:

| | |
| --- | --- |
| #121 | 8 of them (`scrollbar-gutter: stable` on the root scroller) |
| #132 | `test_example_renders[command-hub]`,
`test_example_renders[corpus]` |
| #137 | the four version-menu cases in `test_render_anchors.py` |
| #134 / #133 | the catalog preview digest |
| this PR | `esc` handback |

I reached the version-menu focus break independently and had the same
one-line fix (`versionBtn.focus()` before `showPopover`) before #137
appeared; #137 covers it and the two contracts around it, so nothing of
it is here.

<details><summary>How the split was measured</summary>

Each row is a local `-n0` or `-n3` run on the named tree, not an
inference from the failure text.

- The 33 node ids from the run were re-run on `main` (26dbf42): 31 still
failed. Applying #121's `chrome-style.js` hunk alone took that to 23;
adding #132's branch took it to 20.
- #138 also edits `chrome-layout.js`, so its hunk was applied to `main`
on its own and the `esc` test re-run against it — still red. The two
changes are in different functions (`setPanel` and the `ResizeObserver`
there, `syncPanelLayer` here) and do not overlap.
- Four failures that appeared only under `-n3` —
`test_render_navigation.py::test_a_label_press_keeps_the_controls_keyboard_standing`,
`test_render_outbox.py::test_a_withdrawal_restores_what_still_stands_not_what_stood_then`,
`test_render_startup.py::test_a_fast_first_replay_does_not_flash_the_waiting_surface`
and
`test_render_controls.py::test_covering_panel_takes_the_page_scroll_with_it`
— each pass at `-n0` on both trees, or fail on both. None of them is
this change.

</details>

## Testing

`test_esc_hands_the_page_back_after_it_has_closed_the_last_panel` is the
regression: it asserts both halves the docstring names — the ring the
pointer reader never asked for, and the Space that reopens what they
just dismissed — and it goes from red to green here. No new test,
because that one already owns the behaviour.

<details><summary>Runs on this branch, against the same files on clean
1ca4f40</summary>

```
uv run pytest tests -q -n3                      723 passed, 6 skipped
uv run pytest tests/test_render_controls.py \
             tests/test_render_conversations.py 10 failed, 101 passed
uv run pytest tests/test_render_outbox.py \
             tests/test_render_margin.py \
             tests/test_render_drafts.py         3 failed, 108 passed
```

Every remaining failure above reproduces on clean `main`;
`test_esc_hands_the_page_back_after_it_has_closed_the_last_panel` is the
only test whose result this change moves. Earlier on 26dbf42 the wider
render set (`anchors`, `gate`, `navigation`, `widgets`, `projection`,
`startup`, `drafts`, `outbox`, `options`, `pages`, `aim`, `reactions`,
`margin`, `semantic_selection`, `commands`, `export`, `site`) ran the
same way with the same result.

</details>

---

Automated fix for [failed
run](https://github.com/max-sixty/leaf/actions/runs/33327519405)

Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>

@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 repaint moving to the watch lands, and I ran the counterfactual rather than taking the commit message on trust: with disclosureWatch put back on paintHere(), test_a_widgets_native_control_names_the_press_the_platform_makes fails on 'Enter Space ArrowLeft' == 'Enter Space ArrowRight' and test_the_arrows_say_which_way_the_section_under_the_reader_goes fails on the same pair inverted — each on the arrow the other names, which is the reading the description claims. Restored, uv run pytest tests is 723 passed / 6 skipped on the merged tree, and test_render_navigation.py + test_render_widgets.py --run-nightly is 4 failed / 119 passed, the same four the earlier review already accounted for.

The removal is complete as well as correct: the only paintKeys() calls left in skills/leaf/packages/ are lf-gloss's and lf-shot's, and neither is a disclosure — lf-shot's hangs off a checkbox change and lf-gloss's off its own state. So after this there is no widget-local disclosure repaint left in the tree, which is what the new CLAUDE.md paragraph asserts.

One thing to know at merge time, and the only reason for a comment rather than silence. #140 adds back exactly the pattern this PR retires: lf-worktree.js gains the paintKeys import and calls it beside its own aria-expanded write, guarded on a change, for the same reason lf-options had it. That row's open lives on an element the same watch already hears, so once this lands the call is redundant and the tree contradicts the rule this PR just wrote down — "a widget declaring a disclosure row owes no repaint of its own". Whichever of the two lands second wants that call and its import dropped; on the current queue that is #140.

Runs

Merged tree at 0f95be6 (base c7b3706).

  • uv run pytest tests — 723 passed, 6 skipped.
  • uv run pytest tests/test_render_navigation.py tests/test_render_widgets.py --run-nightly — 4 failed, 119 passed: test_suggestions_sharing_a_block_keep_source_and_keyboard_order, test_suggestion_controls_stay_out_of_the_column, test_the_asks_tray_takes_room_rather_than_covering_the_column, and the ? more key-line reading in test_render_navigation.py.
  • Counterfactual: paintKeys()paintHere() in disclosureWatch, nothing else changed. test_a_widgets_native_control_names_the_press_the_platform_makes and test_the_arrows_say_which_way_the_section_under_the_reader_goes both fail; both pass again on restore.

#settle() runs from connectedCallback, and widgets arrive through widget-loader.js's dynamic import(), so watchDisclosures(document) has already run by the time a settled row writes its first aria-expanded; stageShadow watches a root before replaceChildren puts anything in it, so a row frozen into thread markup is covered on the same terms.

@max-sixty
max-sixty merged commit 7a878f6 into main Aug 30, 2026
9 checks passed
@max-sixty
max-sixty deleted the fix/ci-33324080398 branch August 30, 2026 21:01
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>
leaf-agent added a commit that referenced this pull request Aug 30, 2026
#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 pushed a commit that referenced this pull request Aug 30, 2026
…p's own press (#140)

## Problem

Two leftovers from
[335bf9a](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](https://github.com/max-sixty/leaf/actions/runs/33328596524) 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](#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](#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](#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](#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.

<details><summary>What this does not fix in that run</summary>

The run reported 34 failures. Most of the rest already have a PR open:
the `-15` root scrollbar-gutter family is
[#121](#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](#137), the settled ask's
disclosure keys were [#132](#132),
now merged, the margin and float placements are
[#138](#138), the gallery stills
are [#134](#134), and the
sidebar's stated window is
[#136](#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.

</details>

---
Automated fix for [failed
run](https://github.com/max-sixty/leaf/actions/runs/33328596524)

---------

Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
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