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

The agent writes version files and runs leaf commands, both reaching the browser through leaf's loopback server; leaf wait carries a comment back into the agent's context. agent session version files · commands leaf server loopback port · event log your browser comments, drags, picks leaf wait carries a comment into the agent's context

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.

  1. Plain semantic HTML. The theme styles bare elements, so headings, prose, tables, code, and details carry the page's voice unclassed. Most of any page is this.
  2. Class idioms. A small closed set the theme defines for shapes HTML has no element for, among them .eyebrow, .lede, .tag, .callout, .facts, aside.sidenote for a note in the page's margin, and ol.steps, which numbers this list.
  3. 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.
  4. 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.

effort: highrisk: medRewrite the importer A clean slate on the new schema; the old importer keeps running until parity. effort: lowrisk: highShim the old schema Fastest to ship; every future field pays the translation tax. effort: medrisk: lowMigrate in stages New fields land on the new schema while reads fall back, table by table. My take: this keeps the rollback boundary small.

Whichever way that goes, which of the three jobs behind it are worth starting now?

Backfill last month's rows Contract tests against the old schema A read fallback behind a flag
The form follows the options, and nothing declares it: options that argue their own case render as cards, bare labels as rows — a question about the page, where each row can name the section that makes its case. In a live leaf either form takes your pick by click, the next version ships with your choice marked, and the group ends with one more option: an empty one you write yourself, for the answer nobody listed.
Flaky auth test Fails one run in twenty on CI only. Dark-mode contrast pass Rate-limit the export Tokenize the palette
In a live leaf, cards drag between columns by a grip, and each drop reaches the agent as a structured move.

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.