Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions docs/registry.html
Original file line number Diff line number Diff line change
Expand Up @@ -675,19 +675,20 @@ <h3><code>x-word</code></h3>
<h3><code>x-work</code></h3>
<div class="reference-copy">
<p>
Where this widget can show a transient local line for
<code>leaf status --on</code>: <code>content</code> appends it to a
block prose widget; <code>conversation</code> places it at the start of
the widget's declared x-conversation box and therefore inherits that
box's own predicate.
Where this widget can admit <code>leaf status --on</code> without a
pending reader move: <code>content</code> names a block prose seat;
<code>conversation</code> names the widget's declared x-conversation box
and therefore inherits that box's own predicate.
</p>
<details class="reference-more">
<summary aria-label="Full x-work contract">Full contract</summary>
<p>
An optional <code>when</code> narrows the instances that have the seat
under their replayed attributes. The declaration is permission for
runtime chrome, not a consequence of x-content: prose may itself be a
holder gesture or sit in a hidden panel.
An optional <code>when</code> narrows the instances that may admit
work under their replayed attributes. Once admitted, a page-widget
claim appears at the page edge through its Target Button even if a
later layer changes x-work. The declaration is permission for runtime
chrome, not a consequence of x-content: prose may itself be a holder
gesture or sit in a hidden panel.
</p>
</details>
</div>
Expand Down
3 changes: 3 additions & 0 deletions examples/ship-review.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
{"kind": "reply", "author": "user", "parent": "1c8f52d6", "revision": 1, "text": "Fine for the first patch — I would take the feature over holding it for someone else's release. What I don't want is this sitting open in a month because the day it shipped it stopped being anyone's problem. What happens if their release comes and goes without the fix in it?", "id": "3d90b7c2", "ts": "2026-08-15T09:41:12-07:00"}
{"kind": "reply", "author": "claude", "agent": "Claude", "parent": "1c8f52d6", "revision": 1, "text": "Then the workaround is what ships, and it keeps working: a delay stays a delay whether they fix it or not. What I don't want is for that to become the answer by inaction, which is the shape you're describing. Tick what I should carry into the patch and press Done when the list is right — none of it starts before the ship.", "markup": "<lf-decision id=\"off-slip-decision\">\n <h3>If their release comes and goes, what should I have ready?</h3>\n <lf-options id=\"off-slip\" choose multiple>\n <lf-option id=\"off-slip-chase\">Go back to them monthly until there is a date or a no</lf-option>\n <lf-option id=\"off-slip-clear\">Clear the stall ourselves, so nobody has to background the app</lf-option>\n <lf-option id=\"off-slip-say\">Say what the spinner is, where the reader is looking at it</lf-option>\n </lf-options>\n</lf-decision>", "id": "c904d6a2", "ts": "2026-08-15T09:58:30-07:00"}
{"kind": "action", "author": "user", "revision": 1, "widget": "off-slip", "action": "choose", "detail": {"options": ["off-slip-chase", "off-slip-say"]}, "generated": [], "id": "5e2a71b4", "ts": "2026-08-15T10:07:55-07:00"}
{"kind": "pickup", "author": "page", "events": ["5e2a71b4"], "id": "d8a6f2c1", "ts": "2026-08-15T10:07:56-07:00"}
{"kind": "action", "author": "user", "revision": 1, "widget": "off-workaround-review", "action": "choose", "detail": {"options": ["off-workaround-approve"]}, "generated": [], "id": "b9e3a841", "ts": "2026-08-15T10:08:10-07:00"}
{"kind": "pickup", "author": "page", "events": ["b9e3a841"], "id": "a4c29d10", "ts": "2026-08-15T10:08:11-07:00"}
{"kind": "comment", "author": "claude", "agent": "Claude", "revision": 1, "text": "The vendor release is the only schedule we do not control. Should this task stay blocked until their fix is in our build, even if the workaround ships first?", "anchor": {"section": "off-t-vendor"}, "id": "64df0b27", "ts": "2026-08-15T10:12:08-07:00"}
40 changes: 26 additions & 14 deletions skills/leaf/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ surfaces;
`runtime/conversation/thread-card.js` owns retained panel thread cards, their quote
state, and their reply, resolve, and reopen controls;
`runtime/conversation/thread-list.js` owns retained panel list reconciliation;
`runtime/conversation/work-lines.js` owns live claim seats; and
`runtime/conversation/acknowledgments.js` owns growing acknowledgment receipts and live claim seats; and
`runtime/conversation/reconcile.js` composes panel reconciliation;
`runtime/projection/authored.js` owns captured authored state and restore
statements; `runtime/projection/data.js` owns keyed runtime-data DOM
Expand Down Expand Up @@ -161,12 +161,12 @@ Each mutable fact has one writer:
| proof of what the DOM currently represents | `committedProjection` | `stageOutboxAction` and `reconcileState` |
| anchor paint | thread and composer anchor records | `paintAnchors` |
| where each thread's passage lands | this version's resolution of its anchor | `paintAnchors` writes `placed` |
| local agent work | the typed, log-projected claims in `status.work` | `paintWorkLines` paints every subject seat without becoming another store |
| reader acknowledgment and local agent work | the canonical acknowledgment projection plus typed claims in `status.work` | `paintAcknowledgments` paints conversation-local fallbacks; the living margin maps page subjects onto their existing Target Button without becoming another store |
| composer visibility | `composerOpen` and `fabAnchor` | `showComposer` and `showFab` |
| panel visibility | `panelOpen` | `setPanel` |
| the narrowing on the thread list | the reader's find words and waiting-on-you press | `renarrow` and `widen` |
| how much of the thread list's top a pinned heading covers | the tallest `.lf-pinned` box as rendered, while the panel is open | `paintHeadRoom` writes `--lf-head-room`, called by `renderThreads` and by a `ResizeObserver` on the list |
| the thread list's viewport position through reflow | the live reference card in the open panel | `renderThreads` and the held `paintWorkLines` call preserve it through reconciliation, provisional work, and resolution folds |
| the thread list's viewport position through reflow | the live reference card in the open panel | `renderThreads` and the held `paintAcknowledgments` call preserve it through reconciliation, provisional work, and resolution folds |
| where the thread holding the focus stands in the list | the band the list declares landable through `scroll-padding` | `threadsBox`'s `focusin`, and its press through `pointerdown`/`pointerup`; `stepThread` for a key press that moves no focus, `landIn` for the box it puts the reader in, `placeThreadEdge` for an explicit edge placement, and `revealThread` for a deliberate centring |
| tray visibility | `trayUp` | `showTray` writes reader gestures; `restoreTrays` loads saved intent and `restoreTray` paints it at presentation |
| region width the reader drew | the reader's store, per edge | `drawnEdge`'s `set` and `restore` |
Expand Down Expand Up @@ -702,7 +702,7 @@ The extension keys describe general behavior:
| `x-decision` | the complete reading and arrival region around one nested decision source |
| `x-awaits` | the condition, explicit answer verbs, and optional nested roll-up for a decision |
| `x-conversation` | the condition under which the widget owns a conversation seat, and whether its root requires a version response |
| `x-work` | the content or conversation seat in which local agent work may appear, with an optional condition |
| `x-work` | admits local agent work without a pending reader move, through a content or conversation seat and optional condition; an admitted page-widget claim then appears at the page edge through its Target Button |
| `x-exhibit` | this occurrence is evidence, not an actionable live widget |
| `x-wide` | whether width follows a box or a drawing |

Expand Down Expand Up @@ -1344,6 +1344,16 @@ disclosure Button. `…` appears only when the cluster has secondary controls,
information beside a direct action, or temporary communication choices. A target
with one unambiguous control therefore gets one Button, not a row of variants.

An unsettled reader action reuses that same Button rather than growing a status row
inside authored content. Its information face advances from **Sent** or **Waiting for
pickup** to **Picked up**, then to **Active** only when a typed local claim exists; an
action's standing outcome supplies the same retained target cluster throughout. A
thread's existing Thread Button remains the page-edge route to the exact receipt in
the full conversation; an **Active** claim joins that cluster under `…`. A standalone
page-widget claim gets an **Active** Button directly. When no page edge exists—inside
the full thread panel or a widget frozen into conversation chrome—the compact
`.lf-receipt` remains the local fallback.

Content modules contribute through `registerMarginItem`; they own their verbs and
events, never placement or control styling. Every press in a contribution is built
with `marginAction(control, {glyph, label, behavior, tone, collapse})`. That is the
Expand Down Expand Up @@ -2376,16 +2386,18 @@ help, inspection paint, legend, and address layer. The page and panel are
separate scroll regions. Opening or closing one calls its state setter, updates
the persisted intent, and schedules the shared layout and key paint.

`.lf-work-line` is transient runtime chrome that may also stand inside a page
widget. `paintWorkLines` is its one writer. A thread subject paints in the panel
and every inline conversation seat; a page-widget subject paints only in the
content or conversation seat its active `x-work` declaration names. A content
seat is block prose, but block prose alone grants no seat. The line wears `lf-ui`
and `data-lf-gen`:
it is an account of the widget, not authored words of the widget, so selection
and diff readings skip it. Reconcile widget state first and paint work afterward,
`.lf-receipt` is transient runtime chrome for a subject with no page-edge Button.
`paintAcknowledgments` is its one writer. An unsettled reader message paints after
that exact message in the full thread panel, and an event-backed widget frozen into
conversation chrome paints beneath its owner. Inline page conversations and page
widgets use their target's existing margin cluster instead; an explicit page-widget
claim is the cluster's **Active** reading. The fallback receipt wears `lf-ui` and
`data-lf-gen`: it is an account of the conversation, not authored words, so selection
and diff readings skip it. Reconcile widget state first and paint receipts afterward,
because a module may rebuild the subtree that seats it. Keep surviving nodes
across state applications so an unchanged claim is not re-announced.
across state applications so an unchanged phase is not re-announced. Its live
state span changes only with semantic phase or detail; the separate age clock
may repaint on a heartbeat without entering the live region.

The thread list reconciles nodes rather than rebuilding them. `setChildren`
preserves existing message, reply, and textarea nodes when the same event still
Expand All @@ -2398,7 +2410,7 @@ Tests pin the thread's box rather than a particular scroll offset.
card under the pointer while the pointer is in the list, then the card containing
focus, then the topmost visible card. It records later visible cards before the
mutation and refreshes their baselines after each correction, so a live successor can
take over if the first leaves or becomes hidden. The held `paintWorkLines` call covers
take over if the first leaves or becomes hidden. The held `paintAcknowledgments` call covers
claim-only mutations too. The list follows changes in the card's content position,
keeping reflow out from under the pointer without fighting an intentional scroll.
Browser scroll anchoring is disabled only for the life of a hold so those two
Expand Down
17 changes: 12 additions & 5 deletions skills/leaf/assets/leaf.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ import {
createPresence,
observeServerNow,
quietSince,
waitingForPickupSince,
} from "./runtime/presence.js";
import { createPointer } from "./runtime/pointer.js";
import { createReactions, paintReactionStanding } from "./runtime/reactions.js";
Expand Down Expand Up @@ -1116,7 +1117,7 @@ commentsEdge.over.addEventListener("change", () => {
// distinction for a Threads panel restored or opened during startup; its General
// composer stays usable while the log-derived list says what it is waiting for.

// The threads the panel last reconciled. A work line repaints on the heartbeat's clock and
// The threads the panel last reconciled. A receipt repaints on the heartbeat's clock and
// not only on the log's, because its age is half of what it says and a claim nobody
// renews is exactly the one whose age has stopped moving. Keeping the last fold is what
// makes that cheap: buildThreads walks the log and the page, and a second walk every two
Expand Down Expand Up @@ -3050,8 +3051,8 @@ function awaitsReader(...args) {
function setChildren(...args) {
return conversationRuntime.setChildren(...args);
}
function paintWorkLines(...args) {
return conversationRuntime.paintWorkLines(...args);
function paintAcknowledgments(...args) {
return conversationRuntime.paintAcknowledgments(...args);
}
function widen(...args) {
return conversationRuntime.widen(...args);
Expand Down Expand Up @@ -3210,7 +3211,7 @@ const runtimeProjection = createProjection(runtime, {
pageShifted,
paintAnchors,
paintKeys,
paintWorkLines,
paintAcknowledgments,
post,
projectedParent,
quoteFrom,
Expand Down Expand Up @@ -3328,6 +3329,7 @@ conversationRuntime = createConversation({
post,
PRESS,
quietSince,
waitingForPickupSince,
reachScrollers,
reachedForWords,
reactDone: () => setReact(false),
Expand Down Expand Up @@ -3425,6 +3427,7 @@ const { ITEM, NOTE } = anchorRuntime;

livingMargin = createLivingMargin({
anchorLabel,
acknowledgments: () => runtime.browser?.acknowledgments ?? [],
announce,
approveBtn,
blockAt,
Expand All @@ -3434,7 +3437,9 @@ livingMargin = createLivingMargin({
comparisonChanges,
compact: commentsEdge.over,
closestAcross,
currentRevision: () => runtime.currentRevision,
designIsOn: () => designOn,
droppedAt,
el,
elementById,
focused,
Expand All @@ -3448,6 +3453,7 @@ livingMargin = createLivingMargin({
panelIsOpen: chromeLayout.panelIsOpen,
paintKeys,
placedAt,
quietSince,
renderMarginThread: conversationRuntime.renderMarginThread,
scrollBehavior,
scrollToElement,
Expand All @@ -3459,6 +3465,7 @@ livingMargin = createLivingMargin({
toggleBtn,
updateSequence,
versionBtn,
waitingForPickupSince,
});

viewRuntime = createViewContinuity({
Expand Down Expand Up @@ -3526,7 +3533,7 @@ stateApplication = createStateApplication({
observeServerNow,
paintAnchors,
paintApproval,
paintWorkLines,
paintAcknowledgments,
panelIsOpen,
presented,
reconcileState,
Expand Down
Loading
Loading