how it works
How leaf works
A leaf is a page an agent hands you, and the loop that carries what you do on it back to the agent.
Your feedback goes back to the agent
Leaf serves the agent's page beside an append-only event log, and your browser combines the two into the current document, your choices, and every thread. What you do there goes back to the agent session that owns the page.
sequenceDiagram participant Agent as agent session participant Leaf as leaf server participant Browser as your browser Agent->>Leaf: authored files · commands Leaf->>Browser: page + current state Browser->>Leaf: comments · drags · picks Leaf->>Agent: leaf wait carries the event batch
What you see after you send
Select a passage and comment on it, pick an option, or drag a card. The page shows your move at once, and a status beside it follows the agent's side: Sent once the server has it, Picked up once an agent confirms receipt, Working while it acts. The status clears once a reply or revision answers the move. A move that stays at Sent has reached no agent yet. Updates lists every label.
One turn, from the agent's side
This delivery comes from a page asking which of two ways to shed load when a queue backs up. Each wait runs in the background, so the agent can answer while the next input is being watched.
$ leaf page init ./review
initialized /home/you/review
# the agent writes ./review/index.html — an ordinary file write
$ leaf version check ./review
✓ index.html: parses, widgets, authored modules, and theme validate, protected ids and decisions carried over, nothing overflows the 720px column
$ leaf version stamp ./review --text "Two ways to shed load — which?"
stamped v1 — Two ways to shed load — which?
$ leaf wait ./review
# the background task completes when the user sends input, printing the
# delivery as one line of JSON; here it is indented as `leaf delivery read` shows it
{
"format": "leaf-delivery-v3",
"id": "2d5bdcc4",
"created_at": 1790100943.144468,
"carrier": "wait",
"acknowledge": "Whoever ran the `leaf wait` that printed this delivery acknowledges it; until then the user's moves read Sent rather than Picked up. If the output was cut off, acknowledge nothing and rerun the wait with room for the whole envelope. If you handle it, acknowledge before any other work; if you forward it, acknowledge once it durably arrives there. To acknowledge, start `leaf wait --ack 2d5bdcc4` as the next background task: it confirms this delivery and waits for the next.",
"batches": [
{
"page": "/home/you/review",
"through_seq": 3,
"threads": [
{
"id": "ebe8f367",
"title": null,
"anchor": {
"section": "shed-premise",
"quote": "Callers get a 429 immediately"
},
"detached_from": null,
"resolved": null,
"elided": {
"messages": 0,
"actions": 0
},
"messages": [],
"summaries": [],
"actions": []
}
],
"handling": {
"h1": "The user already sees the action applied.",
"h2": "If you need clarification, ask in a thread on the picked option. To override the pick in a later version, mark the element `restated` and say why in the version note.",
"h3": "It answers the page's Ask, so the page shows it as waiting on you until the markup records it. Name the work it selects or unblocks with `leaf status <page> working \"<the work, in a line>\"` before starting it, then do it, write it in, and stamp a version.",
"h4": "This event's thread has no title yet: name it with `leaf thread title <page> <thread-id> --text \"<a few words>\"`, so the thread panel lists its subject rather than its opening words.",
"h5": "Answer with `leaf thread reply <page> --for <event-id>` before any other work, including the work this event asks for: give the answer, or say what you are about to do and where the result will appear. When work follows, name it on the page in the same command, as in `leaf thread reply <page> --for <event-id> --text \"…\" && leaf status <page> working \"<the work, in a line>\" --on <address>`, where `<address>` is the event's `answer.to`: the line stands beside the thread and in the banner until your next reply there. Once that work lands, put its result on the page, with whatever shows it (captures, a figure, the numbers), then report it with `leaf thread reply <page> --to <address>`: what changed, in a sentence or two, linking the section that shows it. The page carries the work, and the thread only the conversation about it. `leaf thread reply` checks any saved page edit and makes it live before posting. If a revision removes what the thread is anchored on, move the thread in the reply that reports it with `--section <id>` (narrowed by `--quote` or `--part` if needed), or `--detach` it if its subject has left the page. Add `--awaits` only if your reply's prose asks the user something.",
"h6": "Your reply renders as Markdown, so show code or markup in a fenced block."
},
"events": [
{
"kind": "action",
"revision": 1,
"widget": "shed",
"action": "choose",
"detail": {
"options": [
"opt-drop"
]
},
"author": "user",
"meaning": {
"scope": "page",
"unit": "shed",
"depends": [
"opt-drop",
"shed"
],
"answer": null
},
"id": "0b0fd3b7",
"ts": "2026-09-22T11:15:42-07:00",
"seq": 2,
"subject": {
"kind": "widget",
"id": "shed"
},
"threads": [],
"says": {
"opt-drop": "Drop the oldest Keep accepting, and discard the oldest queued work."
},
"handling": [
"h1",
"h2",
"h3"
],
"answer": {
"kind": "markup",
"action": "0b0fd3b7"
}
},
{
"kind": "comment",
"revision": 1,
"text": "Rejecting at the door is right for the API, but the batch importer retries blindly — it would hammer us. Drop oldest there.",
"anchor": {
"section": "shed-premise",
"quote": "Callers get a 429 immediately"
},
"author": "user",
"id": "ebe8f367",
"ts": "2026-09-22T11:15:42-07:00",
"seq": 3,
"subject": {
"kind": "thread",
"id": "ebe8f367"
},
"threads": [
"ebe8f367"
],
"handling": [
"h4",
"h5",
"h6"
],
"answer": {
"kind": "reply",
"to": "ebe8f367",
"for": "ebe8f367"
}
}
]
}
]
}
# after the complete envelope reaches the agent, start the next background task
$ leaf wait --ack 2d5bdcc4
# while that wait runs, answer the comment and name the work it starts
$ leaf thread reply ./review --for ebe8f367 --text "I’ll use drop-oldest for the batch importer and keep rejection at the API boundary." && leaf status ./review working "splitting the shed policy between the API and the importer" --on ebe8f367
# revise ./review/index.html; a valid save goes live at once
$ leaf thread reply ./review --to ebe8f367 --text "Done: [the shed section](#shed-premise) now splits the policy by caller."
$ leaf status ./review waiting "Does the API/importer split match what you need?"
The batch carries a pick and a comment. The comment names the passage it
was left on, so the agent receives the objection with its context, and
each event's handling says what that kind of feedback asks
for. The agent answers in the comment's thread and revises
index.html. A valid save goes live at once; the agent stamps
it as v2 once the revision is worth pointing at.
Each host delivers the same envelope
Claude Code receives feedback through leaf wait. In Codex a
detached adapter queues it as the task's next message, or, where Leaf is a
client of the task's Codex App Server, starts a turn with it. Every route
carries a delivery of the same shape, which names the route and says who
acknowledges it. The host owns delivery, so an open page does not prove
that an agent received your comments.
Behind those words, Leaf tracks three separate facts about each move:
-
Receipt: which input reached the agent. In the direct
loop,
leaf wait --ack <delivery-id>confirms the whole envelope and waits for the next one; printed output can be lost or cut off, so unconfirmed events stay available to the next wait. On other hosts the adapter acknowledges, and the agent follows its route rather than starting a competing wait. -
Work: what the agent is doing. A delivery entering the
agent's turn marks it working;
leaf statusnames the operation, andleaf delivery claim <id> --event <event-id> --detail "…"names the move being worked on. - Response: what settled the move: a reply in its thread, a revision, or a request's outcome. Receipt and work leave it open. Once it is answered, the agent names your next move with a waiting status.
A command never edits the document
The agent writes the document to index.html. Replies,
comments, and version notes go through the leaf command,
which appends them to the event log. Browser
gestures enter through the same validation and append boundary, giving
every event a stable identity and order.
The browser redraws when the server announces a change: replies appear in the thread panel beside their passages, and the banner and tab icon say whether an agent is working, waiting for you, or absent.
The agent says what it is doing before it does it
While an agent works on a page, it keeps a status: one sentence naming the operation and its subject, written before the step starts, so what you read is what is happening now. The status stands in the banner at the top of the page and beside the thread or widget the work is about, and replies in the thread carry the rest.
leaf wait --ack. Your comment reads Picked up, and until
the agent writes its own words the banner reads
Claude working — on your update.
leaf status … working "cutting the lede to two sentences" --on
<thread>
puts that line beside the thread and in the banner.
Every update, and where you read it
Some updates the agent writes with a command. The rest Leaf writes itself,
from what it can observe: how far your input has got, and whether the
agent's turn and watcher are still running. A status and the agent's turn
each show in two places; the dotted line is a status written with
--on.
flowchart LR
subgraph Sources["Who writes it"]
Reply["agent: leaf thread reply · leaf thread open"]
Awaits["agent: leaf thread reply --awaits"]
Stamp["agent: leaf version stamp"]
Receipt["agent: leaf experimental report · receipt"]
Status["agent: leaf status"]
Pickup["Leaf: how far your input got"]
Turn["Leaf: the agent's turn and watcher"]
end
subgraph Reads["Where you read it"]
Thread["the thread"]
Asks["the Asks tray"]
Versions["the page and its versions menu"]
Widget["the widget"]
Banner["the banner, tab icon and Leaves tray"]
Beside["the label beside your comment or widget"]
end
Reply --> Thread
Awaits --> Asks
Stamp --> Versions
Receipt --> Widget
Status --> Banner
Status -.-> Beside
Turn --> Banner
Turn --> Beside
Pickup --> Beside
classDef leaf fill:var(--ok-tint),stroke:var(--ok),color:var(--ok-ink)
class Pickup,Turn leaf
Written by the agent
-
leaf status … working "…"What it is doing, in the banner: Claude working — running the browser suite. With--on, the same line stands beside that thread or widget until the agent answers there. The tab icon takes the banner's colour, and the page's row in the Leaves tray repeats the line. -
leaf status … waiting "…"What it wants from you, as in Claude awaits — pick one of the two layouts. -
leaf status … idleThe agent has finished with the page, and the banner reads Leaf closed. It cannot close while your input is unanswered. -
leaf thread reply --for …An answer in your thread, which clears the label beside your comment. -
leaf thread reply --to …A later message in a thread already answered, such as the result of work the first reply promised. -
leaf thread reply … --awaitsA question for you. The thread reads On you, and the question joins the Asks tray. -
leaf thread open,leaf thread edit,leaf thread resolveA thread the agent opens on a passage, a message it corrects, marked edited, or a thread it closes (✓ Resolved by Claude). -
leaf version stamp --text "…"A revision. The page updates in place, the status line says Updated to v4, and the text joins the versions menu as that revision's note. While you read an earlier revision, a chip offers the new one: New page available → open v4.
leaf experimental report and
leaf experimental receipt let optional packages' widgets show
a worker's progress and the outcome of a request you made. Both may
change.
Written by Leaf beside your input
Beside each comment, pick, or drag, a label follows your input through the agent's turn. The label clears once a reply or revision answers it.
| Label | What Leaf knows |
|---|---|
| Sending | Your browser is still sending it to the server. |
| Sent | The server has your input. |
| Queued | The agent's host queued it for the agent's next turn. |
| Picked up | The agent's turn has taken it up. |
| Working | A status names this input's thread or widget. |
| Replying | The answer is streaming into the thread. |
| Waiting for pickup | Two minutes after sending, no agent has taken it up. |
| Turn ended | The turn that picked it up closed before the agent said anything about it. |
| Update stale | The status about it went a quarter of an hour without renewal, or the turn behind that status ended. |
| Interrupted, Not answered | The host stopped the turn, or reported that it failed, before it answered. |
Queued, Replying, and the failure labels need the agent's host to report more than a pickup, so a host that doesn't report them never shows them. The same holds for the banner's step readings. Today Codex reports all of these, and Claude Code none.
When the agent writes one
- When your input arrives. Your comment comes before the work in hand: the agent confirms receipt, answers or says what it is about to do, and names that work, all before starting it. Each delivery repeats these steps to the agent beside the events they apply to.
-
When the work changes. A new subject, or a new phase
such as reading, editing, testing, or waiting on a result, gets a new
line.
leaf statusrefusesworkingwith no sentence, since the banner already says that the agent is working. - During long work. Work longer than a few minutes goes to background workers, so the agent stays free to answer your next comment. Its status names what is still running, and it writes it again when a worker reports.
- When it is your move. A waiting status names the answer or decision the agent wants, rather than saying that it is waiting.
The conversation loop reference is the agent's copy of these rules.
What the banner says without an update
The banner states only what the page directory can prove, so a quiet agent reads differently from a busy one. Once an update is about a minute old, the banner shows its age, as in Claude working · 5m ago — running the browser suite.
| Banner | What Leaf knows |
|---|---|
| Claude working — on your update | The agent's current turn picked up your input and has not yet said what it is doing. |
| Codex thinking, Codex using a tool | The agent's host reported its current step. |
| Claude’s turn ended 3m ago | The agent's turn finished without renewing its last update. The disclosure keeps that update's words. |
| Claude last checked in 20m ago | Nothing has renewed the update for about a quarter of an hour. |
| Claude away | No watcher is running. Your comments are saved, and the agent picks them up on its next turn. |
| Nudge Claude in terminal | No watcher is running, and nothing has renewed the update for about a quarter of an hour. Your comments are saved until a message in the agent's terminal starts its next turn. |
What the status line announces
News that arrives without you doing anything shows for a few seconds in the status line at the bottom of the page, and a screen reader hears the same words. News arriving together shares one notice, as in Claude replied; Input needed. What was already there when you opened the page isn't announced, except messages you haven't read.
| Notice | When |
|---|---|
| Claude replied, Claude commented, Claude updated a reply, 3 replies in 2 threads | An agent message you haven't read arrives, or one is edited. |
| Input needed, 2 items need your input | Something new waits on you: an Ask on the page or in a thread, or a thread whose turn is yours. |
| Response failed, Response interrupted | The agent's host reports that an answer to you failed or stopped. |
| Request succeeded, Request failed | A request you made from a widget gets its outcome. |
| Agent active on this page | An agent starts watching or working on the page after a time with none. |
| Updated to v4 | A new revision replaces the page in place. |
Every valid save is a revision
Each changed, valid save of index.html becomes an immutable
revision, including the document and its local dependencies, and goes live
at once. leaf version stamp marks a revision as a public
version, served at an address such as /versions/v2.html: the
live address follows revisions, and a tab pinned to v1 stays there.
A live update waits while you are composing, dragging, or undoing, have a gesture still on its way to the server, or have the version menu open, and it keeps your reading position.
A page that asks for approval declares
<meta name="lf-review" content="sign-off">. A stamped
version that declares it offers the banner's approval control, enabled
once you have answered every Ask, the questions the page puts to you.
Checked before it goes live
Before activating a revision or stamping a version, Leaf checks the
source's structure, vocabulary, dependencies, and your standing decisions.
An invalid save leaves the last valid revision live and shows its
diagnostic. For a finished record,
leaf version check --render also opens the page in Chrome in
both color schemes, catching console errors, invisible widgets, and
horizontal overflow.
Your choices survive revisions
Dragging a card or choosing an option records a structured action. The browser applies it immediately, the agent receives it alongside comments, and the log keeps it across reloads, other tabs, and later revisions. The event log reference shows the records a pick and its undo write, and how they fold into state.
To override a choice, the agent marks the rewritten element
restated and explains the change in the version note. Without
that declaration the recorded choice still applies, and a revision that
contradicts it is refused.
z withdraws your last undoable gesture; pressing it again
steps further back. The log records the withdrawal, and the browser
redraws the state without that gesture, so a reload gives the same result.
A reaction attaches brief feedback to a passage or agent reply without opening a thread. The default tokens ask the agent to keep, change, clarify, shorten, support, or prioritize something; packages can declare others.
What a page is made of
An authored HTML document supplies the page's title, description, and content. Leaf adds the security policy, identity, theme, runtime, and canonical address when serving it. Authors reach for four things, in this order:
-
Plain semantic HTML. The theme styles bare elements, so
headings, prose, tables, code, and
detailscarry the page's voice unclassed. Most of any page is this. -
Class idioms. A small closed set the theme defines for
shapes HTML has no element for, among them
.eyebrow,.lede,.callout,aside.sidenotefor a note in the page's margin, andol.steps, which numbers this list. -
lf-*widgets. Custom elements that say what things are:<lf-options>for a question the user answers,<lf-board>for work in columns. A JSON-Schema registry keeps the renderer, the linter, and the agent's documentation agreeing about the vocabulary. -
Bespoke presentation. A page-local
<style>and hand-drawn inline SVG, both using the theme's tokens: the last escape hatch for presentation only one page needs, never a second palette. The drawings on the Layout tab are this.
Interactive elements load their declared behavior modules when the page
uses them. Page-specific behavior can use inline module scripts or local
modules under page/; reusable behavior belongs in a package.
The
page-authoring reference
defines the module and dependency rules.
Widgets you can operate
This page runs the plugin's own theme and widget modules. Each example
sits in an lf-specimen, the gutter that marks quoted
material, so an example decision never reads as one the page is asking you
to make.
Whichever way that goes, which of the three jobs behind it are worth starting now?
The catalog runs wider: diagrams, diffs with per-file collapse, annotated code walkthroughs, file trees, timelines, metric rows, plans with milestones, and dense command surfaces. The examples gallery opens each complete page in its own view.
Each page owns its layer
page init composes the kernel and selected packages into a
checked layer in the page directory. Revisions capture the layer they use,
so changing the installed defaults cannot change a version already
approved. The
package contract
owns composition order and replacement rules.
The kernel owns the shared runtime, chrome, and collaboration protocol. Packages supply reusable elements, modules, styles, and guidance: the default package provides common shapes, and optional packages add capabilities such as diagrams and diffs. Project packages use the same format and can replace or extend the selected layer. The extension guide lists the bundled packages and builds a small one.
Every page is a stack of blocks
A Leaf page has one kind: blocks read top to bottom in the reading column.
A block changes the layout by declaring what it needs. A wide block grows
past the column, data-width on main widens the
whole page, and a workspace that is the page's only block holds the
window.
data-width on
main widens the whole page. Every block starts at one
left edge, and text keeps its measure.
lf-workspace that is
main's only block holds the window, and each pane scrolls
on its own.
A page grows without changing kind: a report that gains live status gains a grid, and its comments and anchors stay where they were.
Page tabs don't make another kind. They are navigation over sections of
one page: the strip sticks under the banner, one panel shows at a time,
and each panel takes the page's width like any other block, so
data-width on main widens every tab.
The room a page has depends only on the window
The room a page has is the window less three fixed reservations, and the surfaces Leaf opens while you read stand over the page. So the page is laid out as the agent wrote it, and opening Threads or a thread card never moves what you were reading.
A touch screen draws no shortcut band, and a window too narrow for the rail draws no rail. A margin entry with no rail to stand in, such as one for a passage inside a pane, sits in the text beside what it marks.
Elements name a role, attributes size a block, classes only style
Leaf reads a page's markup to lay it out and to act on it. What it has to recognise is an element, what sizes a block is an attribute, and everything else is a class for the theme.
data-width on any block, or on main for the
whole page. A widget declares its own default width the same way.
section.panel,
aside.sidebar, .callout. The theme styles
these, and a page can write its own CSS in their place.
A wide page lays its regions on one set of tracks
When the regions are the page, such as a release in progress, a board and
its status, or a review whose verdict stays beside the diff,
<main data-width="available"> widens the whole page.
Every block, the title included, starts at one left edge, one gutter in
from the window, and text inside a region keeps the reading measure.
A wide page reads as one structure when its regions stand on the same
vertical lines. Give it one set of tracks, usually a wide body beside a
narrow column of status, and stack each track's regions as panels. A new
grid per row puts each row's split somewhere new, and
leaf version check --render advises when it finds that.
<main data-width="available">
<h1>Release 4.2 rollout</h1>
<lf-grid columns="2fr 1fr">
<lf-grid columns="1">…</lf-grid> <!-- body: panels you work through -->
<lf-grid columns="1">…</lf-grid> <!-- status, counts, contents -->
</lf-grid>
</main>
Examples: Live progress, Triage board, Command hub.
A workspace holds the window
An lf-workspace keeps regions in view together while each
scrolls on its own: a queue beside the item it opens, a playground's
controls beside its preview. A page whose only block is a workspace is a
wide page without declaring one, and the workspace holds the window's
height as well as its width: each lf-pane's body scrolls
under the pane's header, and actions in a pane's footer stay in reach. The
title goes in the workspace's header.
<main>
<lf-workspace>
<header><h1>Payments alerts</h1></header>
<lf-grid columns="1fr 2fr">
<lf-pane label="Queue">…</lf-pane>
<lf-pane label="Alert">…</lf-pane>
</lf-grid>
</lf-workspace>
</main>
Examples: Notification playground, Merge a worktree.
On a phone, every page is one column
A grid's tracks stack where its narrowest would fall below 14rem, and a workspace's panes flow once the window cannot hold them, so on a phone every page becomes one scrolling column in source order. Write regions in an order that reads top to bottom.
The page directory is the record
Everything a page is lives in its directory: the mutable
index.html, an immutable copy of each revision with the layer
it used, the append-only events.jsonl, and one JSON file per
external data source under data/. Copying the directory
copies the page, its history, and every decision on it.
A server is only a way to reach that directory. One started by an agent
session retires when no live session claims the page; one started from
your shell, or with --standing, stays up until
leaf server stop.
Served where you are
Leaf serves on the session's SSH destination, or on loopback for a local session, where the URL opens only on the serving machine. The URL carries a machine-wide access key, which the browser keeps in a cookie, so sharing that key grants access to every Leaf page on the machine.
When the default address is unreachable, the agent re-serves the page with
leaf server start --host NAME on a hostname you can reach.
Leaf uses the machine's existing networks and opens no public tunnel. The
serving reference
covers addresses, authentication, and server lifetimes.