how it works
How leaf works
No daemon, no database, no build step: the server starts with the session and
stops with it. One leaf process serves the page and mediates an
append-only event log; an ES-module runtime renders the widgets and injects
the comments, threads, banner, and version picker.
The loop
One turn, from the agent's side
A file write and four commands. The output below is a real page's, on a version asking which of two ways to shed load when a queue backs up:
$ 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 validate, one module script + theme link, protected ids and decisions carried over, nothing overflows the 720px column
$ leaf version stamp ./review --text "Two ways to shed load — which?"
{"kind": "note", "version": 1, "revision": 1, "text": "Two ways to shed load — which?", …}
$ leaf wait ./review
# … held here, for as long as the reader takes …
{"page": "/home/you/review", "threads": []}
{"kind": "action", "author": "user", "revision": 1, "widget": "shed", "action": "choose", "detail": {"options": ["opt-drop"]}, "generated": [], "id": "a0a372ca", "ts": "2026-08-16T02:35:07-07:00", "seq": 2}
{"kind": "comment", "author": "user", "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", "quote": "Callers get a 429 immediately"}, "id": "a56486d1", "ts": "2026-08-16T02:35:07-07:00", "seq": 3}
$ leaf ack ./review 3
The batch carries two events. The pick is an action, and every later revision
replays it, so the decision survives the page being rewritten under it. The
comment carries the passage it was drawn on — the reader selected
Callers get a 429 immediately and wrote against it — so the objection
reaches the agent attached to the sentence that prompted it, rather than as a
paraphrase of where they were looking. The pick belongs to no conversation and
the comment opens its own, so the first line's threads is empty;
a later reply in that thread names only the message it answers, and the
exchange behind it comes along there. The agent answers by updating
index.html; the valid save goes live, and a meaningful checkpoint
can then be stamped as v2.
The two hosts carry that context through different loops. On Claude Code,
leaf wait is a background task whose completion joins the same
queue as a terminal message: an idle session takes it as a new turn, and a
working one folds it in between tool calls. On Codex, a watcher task may keep
the unified-exec wait active and send its complete batch into the page's task
with a background follow-up. Leaf emits the same transport-neutral JSON batch
in both loops; the watcher adds the receiving task's instruction when it
forwards that data. The named wait transfers the page's claim to the watcher,
so the page's task can sleep while the watcher remains held to the loop.
Without an explicitly authorized watcher task, the page's own turn stays
active and polls the exact wait session instead; a detached command alone has
no completion that starts a future turn.
The wait owner acknowledges a complete, untruncated JSON batch only after it reaches the next durable consumer. That is model context in a direct loop and receiver acceptance when an adapter forwards the batch. A truncated batch is acknowledged not at all and retrieved again with enough room for the whole result. Until acknowledgement the next wait prints the events again. That separation makes “the process printed it” and “the next durable step has it” two different facts, so an abandoned wait cannot silently consume the user's words.
The server and the wait both go down with the session, and the page directory
is the whole state. Which is what lets a page outlive every session that works
on it: run server start from a shell of your own and it belongs
to no session, so nothing takes it down but leaf server stop.
Sessions then come and go around a page that stays up, and between them the
banner says so — no session holds this page, and it picks up again when one
does. The tab says it too: a leaf's icon wears the color of that banner's dot,
so a row of them tells you which page is working, which is waiting on you, and
which has nobody behind it, without opening any.
A command never edits the document
The agent has two ways in, and only one of them is HTML. The source is a file:
the agent writes the whole document to index.html, an ordinary
file write with nothing mediating it. Everything it has to say goes
through the leaf command instead — a reply in a thread, a comment
it opens on a passage of its own, the changelog note that stamps a version —
and each of those appends one event to the log. The log takes a command rather
than a file write because it isn't the agent's alone: the browser appends to
it too, and one writer is what gives every event an id to reply to and a place
in the order.
So the agent's words reach the screen without the document changing. What the
user looks at is the active revision composed with the log, recomposed when
the server names a change on the stream the tab holds open: the reply appears
in a thread in the injected panel, the passage it answers is lit by the
browser's highlight registry — which paints a range and touches no nodes — and
the banner's line is whatever
leaf status last wrote. Nothing in the document was rewritten to
put any of it there, and it is the same composition that shows the user their
own edits before a later revision carries them.
The HTML can change on every save, without spending a public version number.
Leaf validates index.html, copies each changed valid save to an
immutable revision such as r2-a81c…html, and moves the live root
to it. An invalid save creates no revision, leaves the last valid one live,
and shows its diagnostic. version stamp copies the exact current
source to the next public file, such as v2.html, and records
which revision it names. A tab at the root follows revisions while a tab
pinned to v1 stays there. A live tab hears that the page moved and follows
onto the new revision, keeping the reader's place, and holds while the user is
mid-comment or mid-drag. Coming back the other way,
leaf wait starts after the last acknowledged user event, so a
comment posted between a wait and its acknowledgement remains beyond the
cursor and still lands.
What a page is made of
A revision is one complete HTML document, and the required scaffold is two lines: the theme stylesheet and the runtime module. Everything between them is the agent's, drawn from the four building blocks below in ascending order of specialization; each earns its place only where the one before it has no way to carry what the page needs.
-
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,.tag,.callout,.facts,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 reader answers,<lf-board>for work in columns. Attributes carry scalars, children carry prose, a leading<strong>is an item's title, and 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 riding the theme's tokens; the architecture diagram above is one. This is the escape hatch for presentation only this page needs, never a second palette.
Each language has one owner. HTML is the author's whole medium: pages carry
markup and never script, and interactivity is authored as markup too, so
writing <lf-board> is what adds dragging. CSS is the
theme's: it renders every widget's look, and many widgets need nothing else.
JS is the runtime's, opt-in per widget: a registry flag
(x-upgrade) names the tags whose behavior module loads, and the
behavior attaches to markup that has already rendered, so a module that fails
to load leaves a readable page rather than a blank one.
The head's meta channel carries what the page asks of its reader: one that
seeks approval declares
<meta name="lf-review" content="sign-off">, and only then
does the banner offer "✓ Looks good". The declaration versions with the
document; without it the banner carries no terminal control at all, a page
that asks nothing having nothing for the reader to answer.
This page is set in the plugin's own theme.css, and the widgets
below are rendering in it right now, with no script involved. Each sits in a
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 committed catalog runs wider: mermaid diagrams, unified 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 browser view.
Edits are events; the document stays the state
Dragging a card or clicking an option reaches the agent as a structured action on the same channel as comments, and the next version ships with the board as you left it and your pick marked. Until it ships, the page shows the version plus your own edits; a reload replays them, and a second tab follows along live.
z takes back the last change you made here — a card returned to
the column this version had it in, a thread you resolved by mistake open
again, a suggestion you accepted pending again — and pressing it again steps
further back. Nothing is erased and nothing is restated: the log gains one
word withdrawing the change, and the page is this version plus whatever still
stands, which is what a reload has always shown you.
The cheapest answer is one token. Select a passage, ⌥-click an element, or
stand in an agent reply, then open … or press r.
The ellipsis becomes the ok, no, lost,
cut, more, and this buttons; digits are
optional shortcuts. The token paints as a glyph in the margin and a faint wash
on the words, opens no thread, and comes off with a press on the glyph. An
ok on the agent's latest reply request takes the thread out of
"waiting on you" without a word typed. The tokens are the layer's, not the
machine's: a project's .leaf/ can rename, add, or remove them.
Declining is a version too. When the agent disagrees with an edit, it ships the next version without the change, says why in the changelog, and the page rolls back to the authored state as that version lands.
Each page owns its layer
page init composes the kernel and packages into one checked
layer, then vendors its runtime, theme, registry, and widget modules into the
page's directory. Because pages are self-contained, a version you approved
can't change under you when the defaults do. The
package contract
owns composition order and replacement rules.
The kernel is the machine: the runtime and chrome, shared tokens and idioms, and the registry contracts the collaboration loop itself reads. The bundled default package carries broadly useful fallback shapes, such as boards, diagrams, drafts, and diffs. An explicit package may contain one widget, while another contains only theme rules. Package size and provenance do not change the format. Packages are composed rather than added as runtime branches, so a default widget reaches the page through the same door as a project widget. You or the agent can restyle or replace an entry or file and add new ones beside it; the checked result must still satisfy the kernel's generic event and runtime contracts. The package tutorial builds the smallest useful example.
Nothing broken goes live
Before a source save becomes a revision, Leaf runs the same deterministic lint
as version check, with no browser: the HTML parses with balanced
tags, the scaffold is exactly the theme link and the runtime module, every
lf-* element validates against the page's registry along with the
head's lf-* metas, ids stay unique and every comment anchor from
the previous revision survives, and nothing is wider than the readable column.
A stamp checks the exact current source again and refuses a failing source, so
neither the live root nor a version can expose broken markup. Before a page's
URL first goes out, version check --render also loads it in the
machine's own Chrome and refuses what only a layout engine can see — a console
error, a widget rendered into a box of no size, a sideways scroll — in both
color schemes.
Served where you are
A page is served on the address its session arrived on: over SSH, the one your
client just reached the machine on; otherwise 127.0.0.1. The URL
leaf prints opens as-is either way, with no port to forward and nothing to
configure. It carries a key — the machine's, shared by every page it serves —
and the browser keeps that key in a cookie from the first request, so the open
port is still a page nobody without a link can read or comment on.
When a jump host or NAT leaves that address unroutable from your browser, say
so and the agent re-serves the page with
server start --host NAME on a hostname you do reach (a tailnet
name works). The key still gates every request, and leaf serves only networks
the machine is already on: there is no public tunnel.