Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
21605e3
Make Python the canonical browser projector
max-sixty Aug 28, 2026
83678e5
Merge remote-tracking branch 'origin/main' into codex/canonical-brows…
max-sixty Aug 28, 2026
b09a5c3
Format served browser projection
max-sixty Aug 28, 2026
db1a120
Forward view revisions in the state test seam
max-sixty Aug 28, 2026
7eb89f0
Keep stale-state coverage on projected responses
max-sixty Aug 28, 2026
f8ecbbc
Merge branch 'main' into codex/canonical-browser-projection
max-sixty Aug 28, 2026
651d356
Keep undo restoration in the served projection
max-sixty Aug 28, 2026
02ea9a6
Merge remote-tracking branch 'origin/main' into codex/canonical-brows…
max-sixty Aug 28, 2026
d1a7afb
Keep undo admission on the active document
max-sixty Aug 28, 2026
663c982
Extract inline conversation rendering
max-sixty Aug 28, 2026
22e467f
Split aim and design browser tests
max-sixty Aug 28, 2026
04990ad
Split render command browser tests
max-sixty Aug 28, 2026
37a670d
Merge remote-tracking branch 'origin/main' into codex/canonical-brows…
max-sixty Aug 28, 2026
67967e7
Forward view revisions through the export test seam
max-sixty Aug 28, 2026
9514e64
Merge remote-tracking branch 'origin/main' into codex/canonical-brows…
max-sixty Aug 28, 2026
2d1ad9f
Format the extracted event contract import
max-sixty Aug 28, 2026
0ba083e
Settle comments before measuring label focus
leaf-agent Aug 28, 2026
09e1d05
Merge remote-tracking branch 'origin/main' into codex/canonical-brows…
max-sixty Aug 28, 2026
8a3bd1d
Format split validation modules
max-sixty Aug 28, 2026
4d8326d
Merge remote-tracking branch 'origin/main' into codex/canonical-brows…
max-sixty Aug 28, 2026
de87ea9
Format split render gate modules
max-sixty Aug 28, 2026
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
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ the decision rests on. Use `restated` when a rewrite invalidates one. An `undo`
event names the gesture withdrawn; it never deletes or invents a counter-event.

Actions and reports share the registry-declared coordinate of owner widget,
fold unit, and facet. Python and JavaScript derive winners, retractions, and
settlement from the same declarations. Page-widget state is bounded by document
version; widgets frozen into thread markup use the conversation window.
fold unit, and facet. Python derives winners, retractions, settlement, asks,
threads, and updates in one transaction-consistent browser view. JavaScript
resolves that view onto live DOM nodes and overlays only unresolved local
gestures. Page-widget state is bounded by document version; widgets frozen into
thread markup use the conversation window.

The page directory is the durable record and deployment unit. `index.html` is
mutable author source; revisions and stamped versions are immutable. The event
Expand Down
341 changes: 292 additions & 49 deletions docs/session.js

Large diffs are not rendered by default.

92 changes: 51 additions & 41 deletions plugins/leaf/skills/leaf/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ highlight rules;
`runtime/geometry.js` owns the shared readings of visible boxes and clipping;
`runtime/navigation.js` owns reader travel and scroller selection;
`runtime/anchors.js` owns anchor resolution, paint, and anchor-specific travel;
`runtime/conversation/model.js` owns the thread fold;
`runtime/conversation/model.js` adapts server-projected threads to browser callers;
`runtime/conversation/messages.js` owns message rendering;
`runtime/conversation/replies.js` owns reply drafts, mirrored send state, and delivery;
`runtime/conversation/inline.js` owns conversation seats rendered into the page;
Expand All @@ -97,7 +97,8 @@ highlight rules;
`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
reconciliation; `runtime/projection/fold.js` owns canonical action and report state;
reconciliation; `runtime/projection/fold.js` adapts canonical action and report state
to live DOM nodes and the local outbox;
`runtime/projection.js` owns projection reconciliation and undo. The entry module
composes their mutually dependent callbacks.

Expand Down Expand Up @@ -287,11 +288,11 @@ accepted event because its rendering failed.

A recorded action may be optimistic because its gesture has already changed the
DOM. Drag and edit are examples. `stageOutboxAction` gives that local value the
same semantic coordinate as logged state and commits it on the exact widget and
unit nodes that carry it. `stateProjection` overlays all surviving recorded
outbox actions after logged winners in `outboxOrder`. Until a complete read
accounts for an attempt, its local winner outranks any older log winner on the
same coordinate.
same semantic coordinate as the server view and commits it on the exact widget
and unit nodes that carry it. The browser projection adapter overlays all
surviving recorded outbox actions after authoritative winners in `outboxOrder`.
Until a complete read accounts for an attempt, its local winner outranks any
older log winner on the same coordinate.

A press whose result has not changed the DOM waits for the log. Recordless
settlements and completion presses do not enter the optimistic overlay. The
Expand All @@ -316,11 +317,11 @@ represents the DOM. Delivery may continue while that correction waits for a
live drag or editor to finish.

`accountOutbox` runs only after `receiveState` has installed and rendered a
complete state. It links accepted events to entries, resolves readers waiting on
complete state. It links receipt events to entries, resolves readers waiting on
those events, removes non-action entries whose delivery is complete, and calls
`releaseProjectedOutbox` for actions. Never remove an action merely because a
POST returned 200 or because an attempt appears in an event array that failed
partway through rendering.
`releaseProjectedOutbox` for actions. Never remove an action merely because a POST
returned 200 or because an attempt appears in a receipt list that failed partway
through rendering.

`unaccountedGesture` is true while undo is in flight, the outbox is nonempty, or
a widget is visibly dragging. Navigation and undo both consult it. Navigating
Expand Down Expand Up @@ -352,7 +353,7 @@ therefore survives its response.
The DOM is a projection of three ordered inputs:

1. the authored state captured from this version;
2. standing actions and reports in the authoritative log window;
2. standing actions and reports in the server's transaction-consistent browser view;
3. surviving optimistic recorded actions in the outbox.

The semantic coordinate is
Expand All @@ -371,16 +372,24 @@ projections. `x-awaits.answers` says which actions actually close the request;
orthogonal actions do not, and neither does a conversation standing in the widget's
declared `x-conversation` seat — that takes the request off the reader's list without
answering it, which is why this gate reads the projection with no seats in
it. `x-awaits.rollup` derives a nested request from direct
interventions and child roll-ups, using the same reducer in the browser and file
projection.

`stateProjection(upto)` is the pure derived view. It classifies every action and
report, applies version and retraction windows, drops withdrawn actions and
answered reports, folds the last action for each coordinate, retains report
winners, overlays unresolved local records, and gives a reader action precedence
over a provisional report on the same coordinate. Winners on independent
coordinates compose in event order through `compareProjected`.
it. `x-awaits.rollup` derives a nested request from direct interventions and child
roll-ups in Python; the browser receives the resulting ids and awaiting values.

Python's `state_projection` is the durable derived view. Under the same page
transaction as `/api/state`, `browser_state` serializes its classified events and
winners, asks, conversations, updates, undo candidates, receipts, and coverage at
one `through_seq`. A normal response projects the revision the tab shows and the
active revision it may install next. A version comparison requests its older base
from `/api/view` at the exact `through_seq` already applied to the live DOM, so every
view used together has the same sequence basis without every state read parsing all
historical revisions. Page coordinates use that revision's document window;
conversation coordinates use the unbounded frozen-markup window.

The browser's `stateProjection` is a DOM adapter. It resolves those declared
coordinates back to current widget modules and overlays unresolved local records.
It does not derive retractions, settlements, thread structure, asks, updates, or
undo eligibility from raw events. Winners on independent coordinates still
compose in event order through `compareProjected`.

The two durable channels share the coordinate model but retain their meaning:

Expand All @@ -394,8 +403,8 @@ The two durable channels share the coordinate model but retain their meaning:
`stateProjection` is uncached because registry declarations resolve through the
live DOM. Thread construction and recordless restoration can replace node
identities. Its result has four views: `actions`, `reports`, `classified`, and
`desired`. Add a consumer to one of these views instead of building another fold
over raw `events`.
`desired`. Add a browser consumer to one of these views or extend the Python wire
view instead of building another fold over raw `events`.

`committedProjection` is not a second state authority. It is a checkpoint of
what node identities and semantic winner the DOM currently represents. Each
Expand All @@ -406,11 +415,11 @@ its authored baseline stands.

`projectionCommitted` compares the desired coordinate with that checkpoint.
Terminal events count as committed because this version has no applicable state
to paint. `projectionCoverage` converts coordinate commits back to event
coverage for `data-lf-applied`: superseded actions and answered reports are
covered when the coordinate that represents them is committed, and an undo is
covered when its target's coordinate has moved to the prior winner or authored
baseline.
to paint. The server supplies coverage records and `projectionCoverage` checks
their coordinates against DOM commits for `data-lf-applied`: superseded actions
and answered reports are covered when the coordinate that represents them is
committed, and an undo is covered when its target's coordinate has moved to the
prior winner or authored baseline.

### Reconciliation

Expand Down Expand Up @@ -518,9 +527,9 @@ documents written after this version. A widget instantiated inside frozen thread
markup is in chrome and reads the whole action sequence because the conversation,
not a page version, owns it.

Threads also read the whole log. `retractionFloors(Infinity)` keeps a
conversation current on a pinned page even when the document projection remains
historical. Registry-declared `x-conversation` seats show an exact-section
The server projects threads from the whole log, so a conversation stays current
on a pinned page even when the document projection remains historical.
Registry-declared `x-conversation` seats show an exact-section
textual view while the owner exists in the current document; the Comments panel
keeps the complete thread and its interactive replies. A root declared with
`response: {kind: version, verb: <answer>}` keeps that exact-section view
Expand All @@ -529,8 +538,9 @@ response. Dropping the owner drops only the inline seat.

`restated` and answered-report relations persist through version notes. The note
records the version floor for each affected id or report event; silence in a
later version does not revive retracted state. `retractedIds` uses containment,
not a global id lookup, when deciding which detailed parts an action rests on.
later version does not revive retracted state. Python's projection uses
containment, not a global id lookup, when deciding which detailed parts an action
rests on.

### Event sequences for modules

Expand Down Expand Up @@ -572,12 +582,12 @@ field, verb, or widget name. Claims use their required detail as `detail.text`
and `text`. The state boundary performs this normalization once, before
downstream code sees private status storage.

`sequence` is the action traversal. It applies widget, optional verb, version
window, and liveness in one place, then returns structured clones so modules
cannot mutate the private event list. `watchActions` and `watchUpdates` subscribe
the two public readings to `lf-actions` and invoke the callback immediately. The
same rendering function therefore handles a module connected before the first
state and one constructed by a later thread reconcile.
`actionSequence` traverses the classified events in the installed server view,
then returns structured clones so modules cannot mutate the reading.
`updateSequence` filters the server-normalized update feed. `watchActions` and
`watchUpdates` subscribe the two public readings to `lf-actions` and invoke the
callback immediately. The same rendering function therefore handles a module
connected before the first state and one constructed by a later thread reconcile.

`lf-actions` fires after a complete state has reconciled, including a read whose
event list did not grow and the heartbeat's re-application of the state the page
Expand All @@ -596,7 +606,7 @@ assertion is as old as its revision.
`actionStands` answers whether one accepted action is still the reader's winner
for its semantic coordinate. It treats a newly accepted event as standing when
the tab has not yet installed an event list containing its id, then asks
`stateProjection` once authoritative history contains it. Modules use this
the installed projection once an authoritative receipt contains it. Modules use this
after a send whose visible choreography depends on whether the accepted action
survived later events.

Expand Down
51 changes: 14 additions & 37 deletions plugins/leaf/skills/leaf/assets/leaf.js
Original file line number Diff line number Diff line change
Expand Up @@ -1619,7 +1619,7 @@ mirrorDraft(generalInput, syncGeneral, "general");

let approving = false;
function paintApproval() {
const approved = runtime.events.some(
const approved = (runtime.browser?.conversation?.done ?? []).some(
(e) =>
e.kind === "done" &&
e.revision === runtime.currentRevision &&
Expand Down Expand Up @@ -2006,27 +2006,16 @@ function allButTheReference(binding) {
// page stands down under them — and each declares what it keeps, which is how the
// reference's own key goes on working while every other one is suspended.

const { askEntry, isAwaiting, projectedParent, threadMarkupAwaiting, unansweredAsks } =
createAskModel({
authoredParentOf: (node) => authoredParents.get(node),
awaitsAgent,
buildThreads,
closestAcross: (...args) => closestAcross(...args),
elementById: (...args) => elementById(...args),
inChrome: (node) => inChrome(node),
matchesProjectedWhen: (...args) => matchesProjectedWhen(...args),
matchesWhen,
pagePresented,
projectedFacet: (...args) => projectedFacet(...args),
quoted,
registry,
runtime,
seatRoot,
settledAway: (...args) => settledAway(...args),
stateCoordinate: (...args) => stateCoordinate(...args),
stateProjection: (...args) => stateProjection(...args),
tagsDeclaring,
});
const { askEntry, isAwaiting, projectedParent, unansweredAsks } = createAskModel({
authoredParentOf: (node) => authoredParents.get(node),
closestAcross: (...args) => closestAcross(...args),
elementById: (...args) => elementById(...args),
pagePresented,
registry,
runtime,
stateProjection: (...args) => stateProjection(...args),
tagsDeclaring,
});

const {
ASK_CONTROL,
Expand Down Expand Up @@ -3101,12 +3090,12 @@ const {
chooserLabel: () => labelOf(CHOOSER),
domFacet: (...args) => domFacet(...args),
elementById: (...args) => elementById(...args),
foldedFacet: (...args) => foldedFacet(...args),
inChrome: (...args) => inChrome(...args),
quoted,
projectionFromView: (...args) => projectionFromView(...args),
sameLayer,
showToast,
stateCoordinate: (...args) => stateCoordinate(...args),
stateProjection: (...args) => stateProjection(...args),
stateSpecs: (...args) => stateSpecs(...args),
textBlockSelector: () => TEXT_BLOCK,
versionBtn,
Expand Down Expand Up @@ -3392,9 +3381,6 @@ const runtimeProjection = createProjection(runtime, {
projectedParent,
quoteFrom,
reachScrollers,
// Whether a reaction still paints, off the conversation's last fold — built after this
// module, so asked lazily.
reactionStanding: (e) => conversationRuntime.reactionStanding(e),
rememberPassageParts,
removeOutbox,
renderQuiet,
Expand All @@ -3418,11 +3404,11 @@ const {
committedProjection,
coordinateProjectionCommitted,
domFacet,
foldedFacet,
markSettled,
matchesProjectedWhen,
paintPending,
projectedFacet,
projectionFromView,
projectionCommitted,
rebuild,
reconcileKnownState,
Expand All @@ -3431,13 +3417,10 @@ const {
rememberAuthoredMarkup,
resetAuthoredPage,
requirementMatches,
retractedIds,
retractionFloors,
stageOutboxAction,
stateCoordinate,
stateProjection,
stateSpecs,
takenBack,
undoable,
unitOf,
withdraw,
Expand Down Expand Up @@ -3519,8 +3502,6 @@ conversationRuntime = createConversation({
renderQuiet,
renderSaid,
reportPageError,
retractedIds,
retractionFloors,
runtime,
saveDraft,
scrollToElement,
Expand All @@ -3529,10 +3510,8 @@ conversationRuntime = createConversation({
sendDraft,
setPanel,
settling,
takenBack,
tellDraft,
threadsBox,
threadMarkupAwaiting,
toggleBtn,
updateSequence,
visualPartLabel,
Expand All @@ -3543,10 +3522,8 @@ conversationRuntime = createConversation({
updateRuntime = createUpdates(runtime, {
closestAcross,
coordinateProjectionCommitted,
inChrome,
projectionCommitted,
stateProjection,
threadList: () => conversationRuntime.threadList,
});

anchorRuntime = createAnchors({
Expand Down
Loading
Loading