Acknowledge reader actions through Target Buttons - #180
Conversation
# Conflicts: # tests/test_render_options.py
leaf-agent
left a comment
There was a problem hiding this comment.
Two of these I confirmed by running the repo's own code against a DECISION_PAGE fixture; transcripts are folded below.
status --on now accepts a claim that then wedges version stamp and page init. work_subject gained the has_receipt escape so a widget with an unsettled action can take a claim without an x-work seat, but widget_work_without_seats — which backs the hard exits in publishing.py and vendoring.py's _refuse_unseated_work — still requires widget_work_seat(...) to be truthy. lf-options declares no x-work, so the ordinary loop the PR's own docs describe (status … --on <widget>, then stamp the version that answers it) dead-ends. Related: skills/leaf/CLAUDE.md's $keys table still describes x-work as "the content or conversation seat in which local agent work may appear", which is no longer what the value governs for a page widget — the seat is now the Target Button either way.
A verb whose x-state spec has no record never settles its acknowledgment. page_action_unsettled returns authored is NO_RECORD or authored != folded, and NO_RECORD means the spec declares no authored representation at all — so the predicate is permanently true. lf-options' answer (facet completion, no record) is exactly that verb, and it is what the reader's Done press sends on every multi-select ask, including the one examples/ship-review.jsonl stages. session-lifetime.md in this PR says "A reply, resolution, or authored state that honors the move settles the acknowledgment"; for completion no authored state can, so the target keeps a "Waiting for pickup" reading in its secondary cluster indefinitely. lf-suggestion's accept/reject (facet settlement) have the same shape, though they reach the page through the frozen-conversation path instead.
The third is a one-line inline suggestion on record_pickup.
Transcripts
Both scripts run against tests/render_cases_interaction.DECISION_PAGE on the merged tree.
The stamp/vendor wedge:
status --on jobs -> 0
plain stamp -> 1 refusing to stamp index.html: it would remove the local seat for active work on 'jobs'; pass --completes for each widget this version completes
re-init/vendor -> 1 the incoming layer would remove the local seat for active widget work on 'jobs'; stamp a later version with --completes for that work before re-vendoring
The stamp there is an unrelated later version that removes nothing, so the message is also inaccurate. test_a_widget_move_reuses_one_target_button_until_the_page_honors_it passes completes=("jobs",) on its stamp, which is why the suite stays green over this.
The unsettleable completion coordinate — one choose and one answer, then a stamp whose markup carries chosen:
--- after choose + answer (Done)
{'target': {'kind': 'widget', 'id': 'jobs'}, 'coordinate': ['jobs', 'jobs', 'selection'], 'phase': 'sent'}
{'target': {'kind': 'widget', 'id': 'jobs'}, 'coordinate': ['jobs', 'jobs', 'completion'], 'phase': 'sent'}
--- after stamping the honored markup
{'target': {'kind': 'widget', 'id': 'jobs'}, 'coordinate': ['jobs', 'jobs', 'completion'], 'phase': 'sent'}
selection settles as designed; completion survives the version that answered it, and --completes is not an escape here because it demands an active work claim.
leaf-agent
left a comment
There was a problem hiding this comment.
The seq and admission-predicate fixes both check out, and I confirmed the recordless-settlement regression passes once 17df5f1 lands the positional count. One finding remains on the new settlement rule, inline.
# Conflicts: # skills/leaf/references/internals/page-storage.md
Leaf currently shows local work as inline dotted work lines, which makes reader actions look like additional page content. This change projects interaction-scoped acknowledgments from the event log and reuses each target's existing right-edge Button for page actions and widget claims. The Button advances through Sent, Picked up, and Active without changing its semantic outcome coordinate; full thread panels and frozen conversation widgets keep a compact local receipt where no page edge exists.
The change also moves pickup and active-state projection into shared Python and browser contracts, retains receipt nodes across phase changes, filters version-bounded claims, and updates fixtures, documentation, render checks, and browser coverage. Receipt-backed claims now survive later revisions through their live page target even without
x-work; recordless page moves settle on the next later version note, preserving moves made from superseded live revisions; and pickup records leave derived sequence numbers to the event parser. The presentation adapter is intentionally narrow and carries a reconciliation TODO because the Target Button implementation is changing in parallel.Tests:
uv run pytest tests(761 passed);pre-commit run --all-files; visual sweep at 1200×900 and 1440×900 in light and dark.