how it works

How leaf works

Architecture

Three parts carry a page between the agent and the user. The agent runs in a harness, such as Claude Code or Codex. Leaf's Python code runs as the leaf commands the agent calls and as the page server the user's browser talks to; both read and write the page directory. Leaf's browser runtime draws the page and sends the user's comments, picks, and drags back to the server.

flowchart TD
  accTitle: Harness, Leaf and browser
  accDescr: The agent runs leaf commands and edits the page; the page server answers the browser over HTTP; Leaf's hook, extension or adapter carries new updates back to the agent as a delivery. The page directory sits beside the commands and server.
  subgraph Harness["harness"]
    Agent["agent session"]
    Watcher["Leaf hook, extension or adapter"]
  end
  subgraph Leaf["leaf (Python)"]
    CLI["leaf commands"]
    Server["page server"]
    Dir[("page directory")]
  end
  Browser["user's browser"]
  Agent -- "runs" --> CLI
  Browser <-- "HTTP" --> Server
  Leaf -- "new updates" --> Watcher
  Watcher -- "delivery" --> Agent
  Agent -. "edits index.html" .-> Dir
  CLI -.- Dir
  Server -.- Dir
  class Dir dim
  • One path into the log. The page server, for an update from the user's browser, and a leaf command, for the agent's reply or report, append to the page's event log through the same validation (Current state).
  • One reading of the page. From the directory and the session's records, Leaf's Python code derives current component state, threads, asks, which updates still need an answer, and what the banner says, all from one consistent snapshot. leaf page state prints a selection of the reading the browser receives.
  • Browser runtime. The runtime combines that reading with the authored defaults and with the user's updates still on their way to the server, and draws the result. It does not age the agent's activity on its own clock; where the reading says the activity will change at a given time, the browser reads again then (Runtime).
  • Delivery to the agent. A watcher notices new updates in the log and hands the agent a delivery: Claude Code's hooks, Pi's extension, and Codex's adapter between turns, or a leaf wait the agent runs inside its turn (Harness differences). The agent answers with leaf commands and file edits.
  • Registry. Each component's entry in its package's registry.json declares its attributes, the actions it accepts, and its authoring instructions. The browser runtime, the validator, and leaf page instructions read those entries, so a new component needs only its entry, its module, and its styles (Registry keys).
  • Page directory. The authored HTML sets each component's initial state, an append-only event log records every update and agent action since, and Leaf derives the current page from both on each read (Data).
  • State home. One directory per machine holds session claims, delivery records, and each serving page's activity summary. Sessions running different versions of Leaf share it, so a record missing fields this version expects is ignored (Page files and history).

Typical flows

The harness supplies the agent's tools and runs its turns. The examples below follow an agent asking a user to choose a queue policy for an importer.

Publish a page

The agent learns Leaf from instructions it reads while it works. The Leaf skill, which the agent loads when a task calls for a page or a delivery arrives, lists the steps below and names the reference to read before each one, such as page authoring before the agent writes and its harness's contract before the handoff. The instructions set goals for what the user sees and leave wording and format to the agent.

The agent creates a page directory and writes index.html: ordinary HTML for the page's content and layout. It can include Leaf components for choices, tabs, diagrams, and other interactions. In the commands below, ./review is that directory, relative to where the commands run.

leaf page init copies Leaf's browser JavaScript, styles, and selected component packages into the directory. When the server delivers the HTML, it adds the browser runtime. That runtime supplies comments attached to passages, drawing, thread panels, keyboard controls, and live revisions. The Pages view shows how authored HTML and Leaf components fit together.

leaf server start starts an HTTP server for the page directory and registers the current agent session as the recipient of the user's updates. This registration is called a page claim. The command returns a URL containing the page's access key. The agent hands that URL to the user and ends its turn; the harness integration continues watching for updates.

leaf page init --package diagram ./review
# Write ./review/index.html with the harness's file tools.
leaf page check ./review --render
leaf page stamp ./review --text "Queue policy ready for review"
leaf status ./review waiting "Which queue policy fits the importer?"
leaf server start ./review

leaf page check --render checks the source and renders it for review. Once the server is running, saving valid HTML updates the open page. A stamp records a changelog entry and gives the current revision a version address such as /versions/v1.html. The agent reviews the rendered result before handing it over. The check reports the agent's mistakes to the agent, before the user meets them (Checks before a revision goes live).

Package instructions, evals, and an existing page

Each package adds instructions for its components, written for an audience such as the page's author. leaf page instructions ./review prints the packages' shared instructions as JSON, keyed by audience; leaf page instructions ./review --use lf-diagram | jq -er '.author' prints how to author a diagram.

The repository scores each change to these instructions with evals: cases a headless agent completes in Claude Code and Codex, run on the old and the new instructions.

To resume an existing page, the agent reads its active HTML and leaf page state ./review, which returns current state as JSON. leaf page claim ./review registers the current session as the recipient of the page's updates without starting its server. Starting the server also makes that claim.

Answer updates

A comment or choice in the browser sends an HTTP request to Leaf. Leaf validates the update, appends it to the event log, and sends the agent a delivery: a JSON batch of updates with thread context and instructions for answering. The harness supplies either the batch itself or an id that the agent opens with leaf delivery read. Each event in the batch names the handling instructions that apply to it, assembled from the registry.

For a comment requesting a policy change, the agent reads the delivery and follows its receipt instructions. Its first command says in the thread what it will do, a progress message that also starts work on that event, so the user sees at once which update it is handling. The agent reads the current page and thread, revises the policy, and answers in the thread. This example uses a CLI command to post the reply; the harness routes below explain when Leaf captures a turn's messages instead.

leaf response reply <answer.ref> --ephemeral --text "Checking the importer's queue policy."
leaf page state ./review
leaf page state ./review <thread-id>
# Revise ./review/index.html.
leaf page check ./review
leaf response reply <answer.ref> --done --text "The importer policy is updated."
leaf status ./review waiting "Does the revised policy match the workload?"

A comment receives a thread reply. A page can also pose an ask through a decision widget, such as a choice between queue policies. Each ask contains its prompt, typed answer, and lifecycle. The widget owns its value; a standalone decision widget also creates an ask. An optional lf-ask supplies context around it. The agent records that decision in a stamped revision. The browser fetches the updated HTML and current state, so the user sees the result on the same page.

Example: answer a page choice

The page asks which queue policy to use with lf-options id="queue-policy" choose. When the user chooses Drop oldest, Leaf delivers that choice to the agent. The agent starts the choice event, reads the current choice, and revises the HTML to show the selected option:

<lf-options id="queue-policy" choose>
  <lf-option id="drop-oldest" chosen>Drop oldest</lf-option>
  <lf-option id="block-producer">Block the producer</lf-option>
</lf-options>

After checking and reviewing the result, the agent stamps the revision to record the decision. The stamped markup answers the choice event and ends its start:

leaf task start ./review <event-id> "Recording the chosen queue policy"
leaf page state ./review
# Add chosen to the picked option in ./review/index.html; check and review.
leaf page stamp ./review --text "Queue policy: drop oldest"
Delivery fields and answer semantics

In a delivery requiring a thread reply, answer.to is the thread id used to read the conversation. The event's id identifies the update to start, and answer.for identifies the update the reply answers. The example batch uses 9d41b20f for all three because the comment opens a new thread. A later message has its own event id within that thread.

A user comment requires a reply in its thread. A decision on a page ask requires a stamped revision. If the ask's component declares a record form, the revised HTML uses it to show the decision, such as the chosen option. An ask embedded in a thread message receives a thread reply, because that message's HTML is fixed. Resolving a thread or moving a card can require no answer; the agent still receives the event. A multi-select ask submits the choice when the user presses Done.

A delivery is one JSON document, whose format field names its version. Its envelope contains one or more page batches, preserving the captured event order, thread context, and handling instructions. A delivery id supplied by the harness names this whole envelope; an event's id names one recorded event. The agent opens a delivery pointer with leaf delivery read <delivery-id>, follows its next commands through every part, then runs the displayed acknowledge instruction.

Reading Meaning for the agent
acknowledge The instruction for confirming receipt, or null when the harness confirms it.
page, through_seq The batch's page and captured log position.
subject, threads, says The affected page, thread or widget, its discussion context, and authored words for named gesture targets.
answer The response owed at capture: reply, turn, or markup, with its address.
Event handling Ordered clause ids to read in the batch's handling object.

The registry is Leaf's machine-readable definition of components and events. It declares event instructions under $events.handling and response instructions under $events.answering. Leaf combines the applicable clauses in the batch's handling object. The agent follows those clauses and reads current state before answering: a newer update may already have settled the response the captured batch requested.

{
  "handling": {
    "c1": "Read the thread context before answering.",
    "c2": "Write the answer with leaf response reply using its answer.ref."
  },
  "events": [
    {
      "id": "9d41b20f",
      "kind": "comment",
      "handling": ["c1", "c2"],
      "answer": {
        "kind": "reply",
        "to": "9d41b20f",
        "for": "9d41b20f",
        "ref": "6ba31df0:0:9d41b20f",
        "writer": "agent"
      }
    }
  ]
}

The event's list selects c1 and then c2 from this batch. These ids belong only to that batch. The event id identifies the update being answered.

Change the live page

The agent can update the page while the user reads it. It can save valid HTML, replace a data source that components read, or use a Leaf command to change a component's state. The browser checks the server for changes and fetches the updated document and state (Freshness checks and state reads).

leaf data set ./review queue-metrics --file ./metrics.json

Here, queue-metrics is a data source declared by the page, and metrics.json supplies its complete new value. For a component state change, leaf page report records an event using that component's declared operation and fields. An HTML save updates the live document; leaf page stamp adds a version address and changelog entry.

These changes preserve the user's recorded choices. If an edit changes what a decision was about, the author marks the component restated to invalidate that decision. The Revisions view explains how decisions survive page edits.

Source errors

If an HTML edit is invalid, Leaf keeps the last valid revision live and reports the error to the agent and browser. The file on disk can therefore differ from the page the user sees. The agent reads leaf page state alongside the active HTML to check that distinction.

Wait for updates or finish

leaf status ./review waiting says the agent needs more from the user. The harness integration keeps watching after the turn ends and sends later updates back to the same session.

leaf status ./review idle marks the page finished and stops watching it. Leaf refuses it while an update awaits receipt or an answer, or an agent to-do is open. If the agent ends a turn with a delivered update unanswered, the Stop hook tells it so (Stop hook checks).

Background work and durable to-dos

Work longer than a few minutes runs in background workers, whose results start another agent turn.

leaf task start names the update or to-do the agent is handling in this turn, and a reply or stamped decision answers the update. Work that outlasts the turn, or that no update asked for, gets a to-do, which stays open across replies, revisions and sessions until the agent ends it. The agent opens it on a thread, a widget that takes to-dos, or the whole page, and starts it with the id leaf task open prints:

leaf task open ./review page "Check the importer policy"
leaf task start ./review <todo-id> "Checking the importer under load"
# Run the check and put its result on the page.
leaf task end ./review <todo-id> done "The load check is recorded on the page"

A to-do ends as done, failed, or dropped, with a note saying where its result is or why there is none. A stamp with --completes <widget-id> ends every open to-do on that widget as done. Until then the to-do stays on the agent's list, On Claude.

Stop hook checks

When the agent ends a turn, Leaf's Stop hook checks the pages its session holds. If a delivered update still needs an answer, or a page would go unwatched, the hook keeps the turn going and tells the agent why; Claude Code shows this as context from the Stop hook, not as an error. Each reason is given once. When the agent ends the turn again, it ends, unless a new update that needs an answer has arrived, which the hook hands into the turn. An update that needs no answer waits for the next turn.

    flowchart TD
      accTitle: Checks before an agent turn ends
      accDescr: New input is handed into the turn. Otherwise Leaf checks whether it already extended the turn, whether a delivered update remains unanswered, and whether a watcher runs or the page is closed; it either lets the turn end or reminds the agent to continue.
      Stop(["the agent ends its turn"]) --> New(["an update that needs an answer arrived?"])
      New -- yes --> Hand["Leaf hands it into this turn"]
      New -- no --> Again(["Leaf kept this turn going already?"])
      Again -- yes --> Ends["the turn ends"]
      Again -- no --> Owed(["a delivered update unanswered?"])
      Owed -- yes --> Claimed(["this turn started it?"])
      Claimed -- no --> Remind["Leaf names the reason; the turn goes on"]
      Claimed -- yes --> Watched(["a watcher runs, or the page is closed?"])
      Owed -- no --> Watched
      Watched -- no --> Remind
      Watched -- yes --> Ends
      Hand -.-> Stop
      Remind -. the agent acts, then ends again .-> Stop
      class Hand,Remind goes
      class Ends ends
    
  • An update started in this turn lets the turn end. Long work runs in background workers whose results start a later turn, so the Stop hook doesn't hold a turn open for an update the agent started in it. The update reads Working, with the agent's line beside it, until the next turn answers it or starts it again; otherwise it soon reads Update stale.
  • Between turns, a page needs a watcher: Claude Code's or Pi's watch hook, a leaf wait, or Codex's adapter. Without one, the user's next update waits in the event log until the agent's next turn, which in Claude Code Leaf starts by messaging the session. A closed page or a developer's preview needs no watcher.

Agent command inventory

The table groups Leaf's commands by what the agent uses them for, with arguments omitted. Running leaf alone lists them all, and each command's --help gives its syntax. The examples above show commands for publishing a page and answering updates.

Purpose and result Commands
Prepare a page
Copy selected packages into the page directory, read their authoring instructions, and copy media into the page.
leaf page init
leaf page instructions
leaf page media
Check and publish
Validate the source, record a revision, or produce an offline snapshot.
leaf page check
leaf page stamp
leaf page export
Read page evidence
Read current JSON state, raw JSONL events, or a Markdown exchange. These reads do not confirm receipt of input.
leaf page state
leaf page events
leaf page transcript
Receive updates
Read a delivery, confirm receipt when its transport requires it, or wait for updates directly.
leaf delivery read
leaf delivery ack
leaf wait
Declare work and report progress
Set page status to waiting or idle, or report a widget's declared state transition.
leaf status
leaf page report
Track work
Open work that outlasts a turn or answers no update, start an update or to-do with its current operation, or end a to-do with its outcome.
leaf task open
leaf task start
leaf task end
Write a conversation
Open or answer a thread, edit an agent message or title, close a thread, or summarize a range of messages.
leaf thread open
leaf response reply
leaf thread reply
leaf thread edit
leaf thread resolve
leaf thread summarize
Publish external data
Replace or remove the current value of a declared data source.
leaf data set
leaf data clear
Edit a local file
Let the page read and write one chosen text file on the machine serving it.
leaf file bind
Run the page server
Start or stop the HTTP server, or register the current session to receive an existing page's updates.
leaf server start
leaf server run
leaf server stop
leaf page claim
Connect a Codex task
Connect delivery explicitly, or launch the experimental terminal client with its own App Server.
leaf codex start
leaf codex launch
Extend the vocabulary
Create, validate, or install a package, or run one of its scripts through stdin and stdout.
leaf package init
leaf package check
leaf package install
leaf package run

Harness differences

In Claude Code, a Leaf hook watches the page in the background between turns and wakes the agent when the user sends an update, and Leaf's prompt hook puts the update into the new turn. In Codex, a detached adapter queues the update as the task's next message or, where Leaf is a client of the task's Codex App Server, starts a turn with it. In Pi (experimental), Leaf's extension does inside the Pi process what Claude Code's hooks do. Every route hands the agent a delivery of the same shape, which says who confirms receipt.

Leaf counts an update as received only when the agent or the harness confirms it. A harness discards the output of a hook that times out, so a hook confirms by a deadline well inside that timeout, and puts a delivery into the turn whole only once it has confirmed it. When a delivery is too large for the turn's context, or the hook could not confirm it in time, the hook passes a pointer instead. The agent reads it with leaf delivery read <delivery-id>, which confirms nothing, follows any next commands until it has every part, and then confirms with leaf delivery ack <delivery-id>, as the delivery's acknowledge field says. A Codex task running its own wait confirms with leaf wait --ack <delivery-id>; until then the events stay available to the next wait, in case the printed output was lost or cut off. Stages of an update shows what sets Picked up under each harness.

Harness / route Updates and answers
Claude Code Hooks supply updates when a turn starts or ends, and wake the session between turns. The hook confirms a delivery it passes whole.
Pi (experimental) Leaf's extension checks the session's pages as each run settles and puts the delivery in a new run, or in the current run after its tool calls. The extension confirms a delivery it passes whole.
Codex queue Leaf's adapter queues a delivery id for the next turn and confirms it; a tool hook can pass a pointer during work.
Codex App Server Leaf starts an agent turn with the updates and confirms receipt. That turn's opening and final messages become the thread reply.
Direct wait Without a hook integration, leaf wait prints the delivery in the current turn, and the next leaf wait --ack confirms it and waits again.

On every route the agent can answer with leaf response reply <answer.ref>. One task can receive updates by several routes, and each delivery says how to confirm it and how to answer.

Claude Code integration

leaf hook handles Claude Code's prompt and Stop hooks, and leaf session-end releases the session's pages. A background Stop hook runs leaf hook --watch and wakes the session when an update arrives.

Leaf reads Claude Code's session registry to tell whether a session is busy, waiting or idle. If the background watch has stopped, Leaf can message the session through its socket, or resume a retired background session with claude --bg --resume. The socket accepting a message doesn't show that the agent read it.

Codex entry points and the public website

leaf server start connects the page to the current Codex task. leaf codex start ./review connects delivery when claiming a page without serving it. The desktop app uses the queue route, even though it runs an App Server internally.

leaf codex launch starts a terminal client with a private App Server and sets LEAF_CODEX_APP_SERVER for the task. Leaf can connect to that endpoint; this terminal route is experimental.

On leaf.page, the website host starts a Codex task over App Server for each page a visitor changes. The hosted agent answers deliveries on that page rather than starting a new one. The website describes how the site serves pages.

Owning contracts

The delivered event batches reference owns the envelope and handling lookup. Conversation loop owns to-dos, starts, statuses, and the response loop.

Harness routes are specified in Claude Code, Codex queue delivery, Codex App Server, and Pi. Session lifetime owns the hooks, adapters, and App Server protocol behind them. Page storage owns revisions and browser replacement.

Progress messages

Before each step, the agent names the work and the item it is handling. That item can be an update, such as a comment or choice, or a to-do the agent opened for separate work. leaf task start identifies the item and supplies the sentence describing the work. The banner shows that sentence, and it also appears beside the thread or component the work concerns. The agent's replies carry the discussion and results.

An agent session is the conversation attached to the page in a harness such as Claude Code or Codex. A turn is one run of the agent within that session. A watcher is the background integration that receives updates between turns.

  1. observation Sent The user comments on the lede. The page shows the comment at once, marked Sent.
  2. observation Picked up Leaf's hook puts the comment in the agent's turn and confirms receipt. The comment reads Picked up, and until the agent writes its own words the banner reads Claude working — picked up the comment.
  3. intervention The work is named Before doing the work, the agent replies in the user's thread with what it will do, leaf response reply <answer.ref> --ephemeral --text "Cutting the lede to two sentences.", which also puts that line in the banner, and the comment reads Working.
  4. intervention Each new phase gets a new line When the work moves from editing to checking, the agent starts the same update again: Claude working — checking the page at phone width.
  5. observation Answered The revision goes live, the agent's reply reports it, and a waiting status names what the agent wants back: Claude awaits — does the shorter lede keep enough?

Stages of an update

When the user comments on a passage, picks an option, or drags a card, the page shows the update at once. A label beside the update then shows how far the agent has got with it, and clears when a reply or a revision answers it. Leaf changes the label only on evidence it records. Each harness reports different steps; the explanations name those differences. A harness skips any stage it never reports.

Label Evidence
Sending The user's browser is still sending the update to the server.
Sent The server checked the update and added it to the page's event log.
Queued Codex accepted the update's delivery as the task's next message.
Picked up

Claude Code and Pi. The prompt or Stop hook put the delivery in the agent's turn, or the agent confirmed the complete pointer reading with leaf delivery ack.

A leaf wait loop. The agent's next leaf wait --ack confirmed the delivery the last wait printed.

Codex. The turn that the queued message opens began, or the agent confirmed a complete mid-turn reading with leaf delivery ack.

Codex App Server. App Server opened the turn that Leaf started with the delivery.

Working The agent took the update in hand with one line naming the work, as a progress message in its thread or with leaf task start, or started a to-do on the update's widget.
Replying On Codex App Server, the turn's answer is streaming into the thread.
Answered (label clears)

Claude Code, Pi, Codex, or a leaf wait loop. The agent replied with leaf response reply <answer.ref>, or published a version whose markup records the update.

Codex App Server. The turn ended with a final message, which Leaf posts as the thread's reply, or the agent published a version whose markup records the update.

Waiting for pickup The update is still Sent two minutes after the user sent it.
Turn ended The turn that picked up the update ended, or was interrupted, before the agent answered it.
Update stale The agent's start on the update is 15 minutes old, or the turn that wrote it ended more than two minutes ago, and the agent has not started the update again since. A Picked up update reads the same once nothing shows its turn still running (no status, start, or step for 15 minutes), or once the agent is in a later turn. On Codex App Server, so does a Replying update whose stream disconnects.
Interrupted Codex App Server stopped the turn before it answered.
Not answered The harness gave up on the update: its turn failed, or Leaf could not confirm that the delivery reached a turn. The leaf.page website and the Codex adapter record this.

A comment, or a reply in a thread, needs a reply in that thread. An answer to a page-widget ask needs a version whose markup records it. Resolving a thread, editing a draft, and moving a card need no answer, though the agent still reads them with the rest of the delivery. The label on a draft edit or a card move clears once a version records or replaces the change; in a widget inside a thread message, it clears once the agent next writes in that thread or the thread is resolved. In an ask with a Done button, a pick has no label until the user presses Done, because the ask is not answered until then.

Where the agent's messages appear

The agent writes work declarations, statuses, replies, and version notes. Leaf adds observations about delivery and agent activity. The diagram shows where each one appears; starting work puts its line in the banner and beside the update or to-do being handled.

flowchart LR
  accTitle: Agent commands and visible outcomes
  accDescr: Replies appear in threads, asks and to-dos in the On panel, stamped versions on the page, reports on their widgets, and status in the banner and nearby labels.
  subgraph Sources["Who writes it"]
    Reply["agent: leaf response reply · proactive leaf thread reply · leaf thread open"]
    Awaits["agent: leaf response reply --ask · leaf task open --on user"]
    Stamp["agent: leaf page stamp"]
    Report["agent: leaf page report"]
    Status["agent: leaf task start · leaf status"]
    Pickup["Leaf: how far the user's update got"]
    Turn["Leaf: the agent's turn and watcher"]
  end
  subgraph Reads["Where the user reads it"]
    Thread["the thread"]
    Queue["the On panel"]
    Versions["the page and its versions menu"]
    Widget["the widget"]
    Banner["the banner, tab icon and Pages drawer"]
    Beside["the label beside the user's comment or widget"]
  end
  Reply --> Thread
  Awaits --> Queue
  Stamp --> Versions
  Report --> Widget
  Status --> Banner
  Status --> Beside
  Turn --> Banner
  Turn --> Beside
  Pickup --> Beside
  class Pickup,Turn leaf

Written by the agent

  • leaf response reply <answer.ref> --ephemeral What it is about to do, in the user's thread, before it reads or edits anything. The line also stands in the banner, the user's comment reads Working, and the message folds behind Show 1 progress message once the answer arrives.
  • leaf task start … <id> "…" The current operation, in the banner: Claude working — running the browser suite. The id names the update being answered or an open to-do. The same line appears beside its thread or widget until the update is answered or the to-do ends. The tab icon takes the banner's colour, and the page's row in the Pages drawer repeats the line.
  • leaf status … waiting "…" What it wants from the user, as in Claude awaits — pick one of the two layouts, shown while a watcher or agent turn runs. This clears current starts; to-dos remain open. With no sentence the banner invites a comment: Claude awaits — select text to comment.
  • leaf status … idle The agent has finished with the page, and the banner reads Page closed (Wait for updates or finish says when Leaf refuses it).
  • leaf response reply <answer.ref> An answer in the user's thread, which clears the label beside their comment. It says what it leaves: --done when it delivers what they asked, --ask when it asks them back, or --todo when it promises work, which stays on the agent's list, On Claude, until the agent ends it.
  • leaf thread reply … <thread> A later message in a thread already answered, such as the result of work the first reply promised.
  • leaf response reply … --ask A prose ask for the user, which stays open until they reply or settle it with a reaction, or the agent ends it with leaf task end: as answered elsewhere when the user answered in chat, or as withdrawn. The thread reads My turn to answer, and the ask joins the user's list, On me, in the On panel.
  • leaf task open … --on user Something the agent needs the user to do that no ask or widget answers, such as trying a build, as a to-do on the user's On me list, where m reaches it. Its Done, on its row or x where m lands, ends it and tells the agent. An ask in a thread has no Done: the user's reply answers it.
  • leaf thread open, leaf thread edit, leaf thread resolve A thread the agent opens on a passage, a message it corrects, marked edited, or a thread it closes (✓ Resolved by Claude).
  • leaf page stamp --text "…" A named version and its change note. 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 the user reads an earlier revision, a chip offers the new one: New page available → open v4.

leaf page report records a component state change. A component supplied by an optional package can use these reports to show a background worker's progress.

When the agent says what it is doing

  • When an update arrives. The user's update comes before the work in hand: the agent names the work it asks for before starting it, or answers at once when it asks for none. 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, by starting the item again; a start always carries one, 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 the user's next comment. Its start names what is still running, beside the update the work answers, and it starts the update again when a worker reports (what the Stop hook checks).
  • When it is the user's turn. 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.

Banner readings

The banner combines the agent's last start or status with observed activity: whether the session is running, whether a turn is active, and whether an integration is watching for updates. The server computes this activity, so browser tabs and leaf page state report the same result. The Pages drawer lists other pages using the activity summaries their servers publish.

  • The turn. The harness integration records when a turn starts and ends. Leaf treats it as active while its opening, a start, a status declaration, or a reported step is less than 15 minutes old. Claude Code also reports the session's activity directly, including permission dialogs and interrupted turns. An interruption can therefore mark the turn ended as soon as the user presses Escape.
  • The watcher. Claude Code's watch hook, leaf wait, or Codex's adapter registers with Leaf while watching for updates. Leaf treats the agent as available for updates while a watcher runs. A running Claude Code turn can also receive updates through its hooks.
  • The start. A start stops counting as active once it is 15 minutes old, or two minutes after the turn that wrote it ends, if that occurs sooner. This delay lets an immediately resumed agent renew its start before Leaf reports that the work stopped.
flowchart TD
  accTitle: How Leaf describes agent activity
  accDescr: Idle closes the page and a stopped session has no owner. An active session is classified by current work, input acceptance, stale starts and stalled input into working, waiting, stalled, nudge or away states.
  Idle(["status idle?"]) -- no --> Alive(["session running?"])
  Idle -- yes --> Closed["Page closed"]
  Alive -- no --> Unheld["No session"]
  Alive -- yes --> Believed(["work still active?"])
  Believed -- yes --> Working["Claude working"]
  Believed -- no --> Taking(["taking input?"])
  Taking -- yes --> Stale(["stale start?"])
  Stale -- yes --> Stalled["turn ended · last checked in"]
  Stale -- no --> Listening["Claude awaits"]
  Taking -- no --> Stuck(["input stalled?"])
  Stuck -- yes --> Nudge["Nudge Claude in terminal"]
  Stuck -- no --> Away["Claude away"]

Leaf shows work as active when a start is still current, when the harness reports a step in an active turn, or when that turn has received input requiring an answer. The freshness rules above determine when a start or turn stops counting as active. Input is stalled if it remains Sent for two minutes, or if the turn that received it ends without answering. After about a minute, the banner also shows the working reading's age, for example Claude working · 5m ago — running the browser suite.

Banner What Leaf knows
Claude working — picked up the comment The agent's running turn picked up the user's comment, or their answer to an ask, and the agent has not yet said what it is doing.
Codex thinking, Codex using a tool, Claude waiting for me The harness reports the session's current activity. Waiting for the user, for input, or for approval refers to a dialog in the harness's own window; Leaf announces that wait on the page.
Claude awaits — pick a layout The agent is taking input, and its last status says what it wants from the user. While an update is on its way to the agent it reads Claude listening · 1 waiting instead.
Claude’s turn ended 3m ago The turn that wrote a start ended more than two minutes ago without a new start. The integration is still available for updates. Expanding the banner shows the start's line.
Claude last checked in 20m ago The agent has not renewed its start for 15 minutes. The integration is still available to receive updates.
Claude away No integration is currently receiving updates. Comments remain saved for the next agent turn. Leaf sends a wake message to Claude Code when a comment arrives after its turn ends.
Nudge Claude in terminal No integration is receiving updates, and an update has stalled. Sending a message in the agent's terminal starts another turn.
No session The session attached to the page has exited. Another session can resume the page by starting its server.
Page closed The agent wrote leaf status … idle.

Status line notices

The bottom status line briefly announces new replies, requests for input, and page updates. A screen reader receives the same announcements. Events arriving together share a notice, such as Claude replied; Input needed. Existing activity is not announced when the page opens, except for agent messages the user has not read.

Notice When
Claude replied, Claude commented, Claude updated a reply, 3 replies in 2 threads An agent message the user hasn't read arrives, or one is edited.
Input needed, 2 items waiting on me A new ask requests a decision on the page or in a thread, or an agent reply asks the user a question.
Response failed, Response interrupted The harness reports that a response failed or was interrupted.
Agent active on this page An agent starts watching or working on the page after none was.
Updated to v4 A new revision replaces the page in place.

Revisions

Every changed, valid save of index.html creates an immutable revision containing the HTML and its local dependencies. That revision becomes live immediately. leaf page stamp gives a revision a version number and a stable address, such as /versions/v2.html. The live URL follows new revisions; a browser tab displaying v1 stays on v1.

An open page installs each new revision in place, keeping the reader's position, and waits while doing so would interrupt the user (Live revisions).

A page requesting approval includes <meta name="lf-review" content="sign-off">. A stamped version with this declaration offers an approval control in the banner. The version's sign-off is itself an ask. Its approval control becomes available after every widget ask on the page and in unresolved threads is answered or withdrawn. Prose asks and widgets in resolved threads do not block approval. Undo reopens the approval ask.

Checks before a revision goes live

Before making a revision live or stamping a version, Leaf validates the HTML structure, component declarations, dependencies, and recorded user decisions. An invalid save keeps the last valid revision live and reports the error. leaf page check --render additionally opens the page in Chrome in both color schemes to check for console errors, invisible components, and horizontal overflow.

Decisions across revisions

Dragging a card or choosing an option records a structured action in the event log. The browser shows the action immediately, and the agent receives it alongside comments. The recorded choice survives reloads, other browser tabs, and later revisions. The event log reference shows a choice and its undo, and how Leaf computes their result.

An edit normally preserves recorded choices. If the edit invalidates a decision, the agent marks the affected element restated and explains the change in the version note. Without that declaration, the recorded choice still applies. The agent may revise wording, labels, placement, and vocabulary; historical choices do not veto the edit. Their original meaning remains in the event log.

The z shortcut withdraws the user's last undoable action. Repeating it withdraws earlier actions. Each undo appends an event naming the action being withdrawn; the browser recomputes the result without that action. Reloading the page therefore preserves the undo.

A reaction records brief feedback on a passage or agent reply without opening a thread. The default reactions request keeping, changing, clarifying, shortening, supporting, or prioritizing content. Packages can declare additional reactions.

Parts of a page

The agent writes an HTML document containing the page's title, description, content, and layout. When the server delivers it, Leaf adds the page identity, visual theme, browser JavaScript, and canonical URL. The browser runtime supplies the commenting and collaboration interface. Authors build the content with these tools, starting with ordinary HTML:

  1. Ordinary HTML. Leaf styles headings, paragraphs, tables, code, and details without requiring CSS classes. These elements provide most of a page's content.
  2. Theme classes. Leaf supplies classes for common presentation patterns, including .eyebrow, .lede, .callout, aside.sidenote for a margin note, and ol.steps for a numbered sequence like this list.
  3. lf-* components. Custom HTML elements add interaction, such as <lf-options> for choices and <lf-board> for cards in columns. The registry declares their attributes and behavior using JSON Schema. The browser, validator, and agent instructions read those same declarations.
  4. Page-specific presentation. A local <style> and inline SVG can define arrangements and drawings specific to the page. They use the theme's shared color and type tokens to stay consistent with the rest of Leaf. The Layout tab's drawings use this approach.

Interactive components load their JavaScript modules when the page uses them. Behavior specific to one page can use inline module scripts or local modules under page/. Reusable behavior belongs in a package, a directory containing components and their supporting files. The page-authoring reference defines module and dependency rules.

Widgets

This page uses Leaf's own theme and components. Each example is wrapped in lf-sample, which marks it as an illustration. An example choice is therefore distinct from a decision the page is asking the user 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
Options with explanations render as cards; options containing only labels render as rows. The content determines the presentation. A row can link to a section that explains the choice. In a live Leaf page, clicking an option records the selection for the agent, and the agent's next stamped version marks it as chosen. The group also offers an empty option for an answer the author did not list.
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 action.

The component catalog also includes diagrams, charts, diffs with collapsible files, annotated code, editable drafts, playgrounds, and recorded browser traces. The examples gallery opens complete pages using them.

Runtime and packages

Each page directory contains its own copy of Leaf's runtime and selected packages. This validated composition is called its layer. page init builds it. Each revision captures the layer it uses, so an installed plugin update cannot change a version already approved. The package contract defines how packages are combined and replaced.

Leaf's core, called the kernel, supplies the browser runtime, shared controls, and collaboration protocol. Packages supply reusable elements, JavaScript modules, styles, and authoring instructions. The default package provides common components; optional packages add capabilities such as diagrams and diffs. Project packages use the same format and can replace or extend the page's selected layer. The extension guide lists the bundled packages and demonstrates a small custom package.

Styles and overrides

To restyle anything on a Leaf page, write an ordinary CSS rule. Put it in the page's <style> to change that page, or in a theme package's theme.css to change every page that uses the package. A theme package is a package that declares no widgets. A rule in either place overrides what Leaf, its Layouts and its widget packages set, and the page's rule overrides the theme package's.

To change Write
Leaf's colors :root { --accent: teal; }
A Layout's width :root { --col: 46rem; }
A widget lf-card { padding: 8px; }
Controls Leaf or a widget builds <style data-controls>lf-board .lf-grip { color: teal; }</style>

A Layout reads tokens such as --col, --sidebar and --page-pad-start, and any other rule changes it too, such as main { padding-top: 28px; }.

A bare button or input rule reaches only the page's own controls. To restyle a control Leaf or a widget builds, put its rules in a stylesheet explicitly marked data-controls, as in the last row. Leaf's banner and panels take their colors and type from the tokens on :root, such as --accent and --sans.

Which rule wins

Leaf sorts CSS into tiers by who wrote it. The tiers are CSS cascade layers (@layer), and a rule in a higher tier wins. Each switch below puts the rule beside it into the tier its page or package writes into, for the same card, so the card shows what the browser decides.

Rules in tier order, highest first. The card takes its padding from the highest rule that is on, which is outlined.

A lower tier's rule loses however specific its selector is, as the widget package's :scope.featured shows. Within a tier, the more specific selector wins; between equally specific selectors, a widget package's rule, scoped to its widget, beats an unscoped one; after that, the later rule wins. !important reverses the order of the tiers, so a page's !important loses to Leaf's own; an ordinary rule is all an override needs.

The tiers

The page's document

  1. The page's <style> Page Your page's rules, unlayered. Leaf's banner, panels and marks are unlayered too and come after yours, so they win a tie.
  2. The page's @layer blocks Page Declared after Leaf's tiers, so above them, as a CSS framework such as Tailwind writes them.
  3. lf-state Leaf
  4. lf-theme Theme packages Each theme package's theme.css, in the page's package order.
  5. lf-layouts Leaf The Layout classes, such as layout-column.
  6. lf-base Leaf and widget packages Leaf's theme, then each widget package's theme.css. A widget package's rules reach only its own widgets.
  7. lf-reset Leaf

A widget's shadow tree

  1. Marks Leaf
  2. lf-state Leaf
  3. lf-theme Theme packages Each theme package's shadow.css.
  4. lf-shadow Leaf and widget packages Leaf's and each widget package's shadow.css.
  5. lf-base Widget packages Defaults the widget module adopts with inBaseLayer().

Page rules and Layouts stop at the shadow root. Inherited tokens such as --accent cross it, as does a part the widget exports with ::part.

The tiers, highest first. The narrow rows are Leaf's own and need no overriding: lf-state hides what a decision removed, lf-reset clears the browser's form-control styles, and marks are the highlights Leaf draws over a widget.

Writing a package's styles

A widget package styles its own widgets in its theme.css, where :scope names the widget itself and ordinary selectors name what it contains, and reaches nothing outside them. Its shadow.css reaches the private parts inside each widget's shadow tree. A theme package's shadow.css reaches the same parts from the tier above. The package reference lists the supported tokens.

Layouts

A Layout is a CSS class that arranges the element carrying it. On main, it gives the page a reading column, a body beside a side column, or a workspace. A workspace contains panes: regions with optional headers and footers and, in a full-height workspace, their own scrolling. The tile layout arranges the children of any block.

Class Arrangement
layout-column Text in a centred 720px column, which figures, boards and wide tables can extend past. A plan, review, report or explanation read top to bottom, as most pages are, this one included, and a playground, code comparison or trace, which extends past it.
layout-workspace The window's full height, divided into panes that scroll separately. A queue beside the item it opens, or a playground's controls beside its preview.
layout-sidebar A body beside a narrower side column, with the header across both, as wide as the window up to 1600px. A board and its status, a release dashboard, or a review with its verdict beside the diff.
layout-tiles Equal cells, as many to a row as fit. A row of metrics or status tiles.
Prose in the reading column, with a wide block growing past it wide block
A column pagelayout-column on main. Text keeps the 720px column, and a block with a declared width, such as a diagram, extends past it.
A sidebar page: a body beside its side column, each block starting at its region's left edge main.layout-sidebar board aside
A sidebar pagelayout-sidebar on main widens the page and sets a body beside its side column. Every block starts at its region's left edge, and text retains its reading width.
A workspace fills the window, and each pane scrolls separately queue detail
A workspacelayout-workspace fills the window, and each pane scrolls separately.

Leaf's Layout classes are ordinary CSS, applied beneath the page's own stylesheet in the cascade. A page can adjust them with CSS rules, for example to change a column's share of the width or a gap. It can also use its own grid when the supplied layouts do not fit.

The reading column

On a layout-column page, headings, paragraphs, lists, and code occupy a centred column up to 720px wide. On a sidebar page too, the theme limits paragraphs, list items, and headings to that reading width. A workspace sets no reading width, and none of the widths below: every block fills the pane it stands in.

A diagram, board, or wide table can extend into the margins beyond the reading column. The text above and below it keeps its width and position. These width settings control the block:

  • column is the text measure. In a wider region, such as a sidebar page's body, a column component starts at the same left edge as the text.
  • wide extends up to 1080px. Wide blocks share this width so they align with one another.
  • available uses the window's width after subtracting the outer page margins and space for margin notes and controls.

The registry, which declares each component's behavior, also gives it a default width. For example, lf-options uses column, lf-board uses wide, and lf-diagram uses available. A data-width attribute asks for wide or available on any block, overriding the default. If the margins have unequal space, the block extends farther into the wider margin. A table given a width takes its columns' natural width up to that room rather than spreading across it.

data-width on a figure extends it past the reading column main figure data-width
data-width on a blockThe block extends past the column, and the paragraphs around it keep their width.

A log, feed, or long listing with data-bound scrolls within a limited height and opens at its newest entry. Its growing content therefore does not make the whole page longer.

Margins

A sufficiently wide window places these controls and notes beside the reading column:

  • The rail, on the right, where Leaf places a marker beside each commented or decided passage. It needs a window about 960px wide, or about 1010px on a touch screen.
  • A sidebar, the page's first aside.sidebar, on the left. It holds a table of contents, a legend or reference facts, and stays in view under the banner as the page scrolls. It needs about 1130px.
  • Sidenotes, each an aside.sidenote, on the right, level with the block they annotate. They need about 1150px.

The column shifts sideways to make room for a sidebar or sidenotes. In a narrow window, those notes appear within the column at their original positions in the HTML.

Workspace

layout-workspace fits the page between Leaf's top banner and bottom bar. A single-row header holds the title and status tags. The body takes the remaining height and contains an lf-pane, a component that arranges its own panes, or a grid of panes defined by the page's CSS. Each pane scrolls its content beneath its own header. A pane's footer stays visible, keeping its actions within reach.

An lf-tabs list="side" in the workspace body displays a list of tabs as a queue, such as tickets or findings. The selected item's content appears beside that list.

The workspace fills the window's height when the window is at least 720px wide and 480px tall. In a smaller window, panes expand to fit their content and the whole page scrolls.

A workspace fills the window and its panes scroll queue detail footer
A large windowThe workspace fills the window. Each pane scrolls under its header, and a footer's actions stay in view.
In a small window, a workspace's panes take their natural height and the page scrolls queue detail
A small windowBelow 720px wide or 480px tall, the panes take their natural height and the page scrolls.
<main class="layout-workspace density-working">
  <header><h1>Payments alerts</h1></header>
  <div id="regions">
    <lf-pane label="Queue">…</lf-pane>
    <lf-pane label="Alert">…</lf-pane>
  </div>
</main>

<style>
  #regions { display: grid; grid-template-columns: 1fr 2fr; }
  @media (width < 720px) { #regions { grid-template-columns: 1fr; } }
</style>

density-working reduces type size and spacing for a compact working surface. It is independent of the Layout: a workspace can use ordinary reading sizes, and a pane or table on another page can use working density.

Examples: Notification playground, Merge a worktree, Alert review.

Tiles

layout-tiles arranges an element's children in equal cells at least 9rem wide, placing as many cells in each row as fit. Cells in the same row have equal heights. A page can set its own columns when it needs a particular grouping, such as four tiles in two rows of two. A row of headline numbers can use a layout-tiles block of native dl elements with labels in dt and values in dd.

Page tabs

An lf-tabs at the top of a page creates tabs like the ones on this page. Their tab strip stays below the banner while the page scrolls, and one tab's content is visible at a time. All tabs share the page's width, Layout, threads, and version history.

Banner, rail and panels

Leaf adds controls around the authored HTML. This interface is called chrome: the top banner, bottom bar, comment and decision markers, thread panels, and the On panel. An ask contains a request for user input, its answer, and its lifecycle; widgets own their typed answer values. The On panel lists what each side has to act on, in the user's own voice. On me holds widget and prose asks, stamped-version sign-off, and to-dos the agent put on the user. On Claude, named for the agent, holds unanswered updates and the agent's open to-dos. The panel shows one list at a time, under a tab named for it, and folds what left that list under it as Done: answered asks under On me, sent replies under On Claude. The banner's On me door, beside Threads, counts the user's list, and the On Claude count after the status counts the agent's; each opens the panel on its own list. Opening these controls preserves the page's text wrapping and reading position.

The banner and bottom bar have fixed heights, and the page scrolls between them. The bottom bar shows keyboard hints and status messages; the Phone tab describes the banner on a touch screen.

The rail is the strip to the right of main where Leaf places comment and decision markers. It appears when the window has enough space: about 960px on a column page or about 1920px on a sidebar page. data-rail="right" keeps it visible above 640px, except on a workspace, whose panes scroll on their own. Where no rail fits, and inside a scrolling pane, a small pin appears beside the target on the page. Leaf places it clear of text and controls when space permits. The o shortcut hides pins and passage highlights.

A thread card opens beside its marker in the margin. If the margin is narrower than the card, it overlaps the text's edge. The Threads panel and the On panel open over the page's right edge, one at a time, and the Pages drawer over its left.

The banner and the bottom bar have fixed heights, and the rail sits in the margin beside the column Banner Bottom bar Rail
Banner, bottom bar and railThe banner and the bottom bar have fixed heights. On a column page, the rail sits in the margin beside the column and takes no width from it.
The Threads or On panel opens over the page's right edge, the Pages drawer over its left Pages drawer Threads or On
Panels over the pageThe Threads and On panels share the right edge, one at a time, and the Pages drawer opens on the left. The page keeps its width underneath.
A thread card opens in the rail beside its marker Thread card
A thread cardIt opens in the margin and overlaps the edge of the text only where the margin is too narrow. Nothing below it moves.

Narrow windows

In a narrow window, page regions stack vertically in HTML order. Margin notes move into the reading column, a side column appears above or below its body, and workspace panes expand to fit their content. Tiles can still fit two per row on a 390px phone.

A sidebar page's regions, numbered in source order, stack into one column on a phone 1 2 3 4 1 2 3 4
A side column on a phoneThe side column stacks under the body, so region 3 follows region 2.

Records

Leaf keeps few records and derives everything else from them. A page directory records three kinds of thing. The server folds them into the threads, workflows, asks and lists a page shows, each time the page is read. The browser combines that reading with the authored page and the updates it is still sending. Each kind below has one writer, and every other part of Leaf reads it from that writer.

Kind What it is Identity
Recorded in the page directory
Revision An immutable copy of the page's HTML, written when the agent saves a valid page. The active revision gives each widget its initial state. its number, such as v4
Event One recorded transition: a comment, reply, reaction, widget action, undo, pickup or to-do. The browser and the agent's commands write events through the same validation step. its position in the log
Data source The latest value of one external feed, replaced whole. its name
Folded by the server on each read
Thread One conversation, anchored to a passage, a widget, or the page as a whole. It holds its messages, an optional title, its attention, and which agent messages the user has not read. the id of its opening message
Message One turn in a thread: the user's comment or reply, the agent's reply or progress message, or a reaction. An edit is a new version of the same message. the id of the event that wrote it
Workflow How far the agent has got with one user update: Sent, Queued, Picked up, Working, Replying, Answered, or a condition such as Failed. It names the thread it stands in, if any. the update's event id
Ask A request for the user's input, holding its typed answer and whether it is open, answered or withdrawn. widget:, reply: or approval:, then its source
To-do Explicit work the agent or the user owes until it ends. One the agent opens on a thread keeps that thread waiting on the agent. its event id
Attention Whose move a thread waits on, and why: the user, for an ask or an update to send again; the agent, for a workflow or a to-do; or neither. one per thread
On me, On agent, Done The user's list, the agent's list (On Claude, say), and what has left them, selected from the asks, to-dos and workflows. one selection per reading
Combined in the browser
Publication The one read-only reading of current state: the authored initial values, the server's latest reading, and the updates awaiting an answer. its epoch number
Presentation proof Which parts of the page have drawn which epoch. one per renderer and epoch
Browser-owned state Focus, scroll, standing, open disclosures and unsent drafts. They last as long as the element holding them and never enter the publication. the element, or the draft's thread

Relations

A thread is the unit most surfaces show. The user's messages and widget actions are both updates, and each update has one workflow tracking the agent's progress on it. A thread's attention follows from the workflows standing in it, the asks it carries, and any to-do holding it. Each workflow and to-do names the thread it stands in, and the lists add up the same asks, to-dos and workflows across the page.

flowchart TD
  accTitle: How threads, messages, updates, workflows, asks and to-dos relate
  accDescr: A thread is anchored to a passage, a widget or the page and holds messages. A user's message, like an action on a widget, is an update, and each update has one workflow. A reply or a widget may carry an ask. Workflows, asks and to-dos decide each thread's attention, and together they make the On me, On agent and Done lists.
  Thread["thread"]
  Place["passage, widget or page"]
  Message["message"]
  Widget["widget"]
  Update["update"]
  Workflow["workflow"]
  Ask["ask"]
  Todo["to-do"]
  Attention["each thread's attention"]
  Lists["On me · On agent · Done"]
  Thread -- "anchored to" --> Place
  Thread -- "holds" --> Message
  Message -- "the user's is an" --> Update
  Widget -- "an action on it is an" --> Update
  Message -- "a reply may carry" --> Ask
  Widget -- "may hold" --> Ask
  Update -- "tracked by one" --> Workflow
  Workflow --> Attention
  Ask --> Attention
  Todo --> Attention
  Workflow --> Lists
  Ask --> Lists
  Todo --> Lists
  class Attention,Lists computed

Page files and history

A page directory stores its HTML, revision history, event log, and external data. Copying it preserves the page and its recorded decisions. Leaf derives the visible content and state from immutable document revisions, an append-only event log, and JSON files containing the latest external values.

flowchart TD
  accTitle: Page files, event log and external data produce current state
  accDescr: Authored edits create immutable revisions, browser and command actions append to the event log, and external processes replace source files. Their combined reading supplies the browser and agent; view state and drafts remain local to the browser.
  subgraph Writers["Who writes"]
    Agent["agent edits index.html"]
    You["user, in the browser"]
    Cmd["agent commands"]
    Proc["any process"]
  end
  subgraph Dir["page directory"]
    Rev[("revisions/ (immutable)")]
    Log[("events.jsonl (append-only)")]
    Data[("data/ (replaced whole)")]
  end
  Agent -- "valid save" --> Rev
  You --> Log
  Cmd --> Log
  Proc -- "leaf data set" --> Data
  Rev -- "markup" --> Read["current state, computed on every read"]
  Log -- "apply recorded actions" --> Read
  Data -- "values" --> Read
  Read --> Browser["user's browser"]
  Read --> PageState["agent: leaf page state"]
  Browser -.- Local["this browser only: view state, drafts"]
  class Read computed

Leaf also records conversation and diagnostic information. These files support delivery and observation; they do not supply component state or thread messages.

Where What it tracks
cursor.json The position in the event log through which delivery is confirmed
viewed.json The last time a browser tab showed the page
status.json The agent's declared waiting or idle status and observed activity
observations.jsonl A diagnostic trace of requests and browser interactions
the machine's state home, outside any page A shared directory for session claims, delivery batches, neighboring page activity summaries, and the machine's access key

Current state

The active revision's HTML supplies each component's initial state. Leaf computes current state by applying recorded actions to that starting point whenever it reads the page. An action is keyed by [widget, unit, verb]: the component, the part of its state being changed, and the operation. For each key, the latest action still in effect determines the result. leaf page state reports that result. The browser adds the updates it is still sending (Updates awaiting an answer).

Every event passes through the same validation step before Leaf appends it to the log. Leaf checks the operation against the page's component declarations and records what the action meant when it was taken. Later readers use that meaning even if the original component is gone. Undo appends a new event identifying the action being withdrawn; it does not remove the original event. The event log reference follows a choice and its undo through this computation.

Data flow

Each step below has one owner, and every surface draws from the same publication. The server owns whatever depends on the whole log: whose turn a thread is, how far a workflow has got, which asks are open. The browser adds only the updates it is still sending. Between the publication and the surfaces sit the readings: one function per question a surface asks of a thread, such as its name or its word (Thread readings), each listed in the runtime's owner table. An update the user makes joins the updates awaiting an answer and goes to the log (POST /api/event), so its result shows before the log answers.

flowchart TD
  accTitle: Data flow from the page directory to each surface
  accDescr: Revisions, the event log and data sources feed the server's folds. The browser combines the folds' reading with the revision's authored state and the updates awaiting an answer into one publication. One owner per reading reads it, and every surface paints those readings.
  Rev[("revisions")]
  Log[("event log")]
  Data[("data/")]
  Folds["server folds: threads, workflows, asks, to-dos, attention, unread, lists"]
  Authored["authored initial state"]
  Ledger["updates awaiting an answer"]
  Pub["publication"]
  Readings["one owner per reading: name, word, id"]
  Surfaces["Threads · margin · Page Map · On panel · floating panel · activity feed · widgets"]
  Rev --> Folds
  Log --> Folds
  Data --> Folds
  Rev -- "document" --> Authored
  Folds -- "GET /api/state" --> Pub
  Authored --> Pub
  Ledger --> Pub
  Pub --> Readings
  Readings --> Surfaces
  class Folds,Pub,Readings computed

A surface decides where a reading stands and how much of it fits, and nothing else. It does not decide what a thread is called or whose turn it is, and it never reads a fact back from what another surface drew. The one exception is identity: a press lands on a drawn thread, which carries its thread's id so the press can find the thread.

External data

External values, such as benchmark results or build status, use one JSON file per data source. The page declares the format each source must provide; data set records the current contract in data.json. A later revision may replace that contract. Any process can replace the source file. Leaf validates its value on each read and displays a source error if it does not meet the contract.

Replacing a source file updates the open page without creating a document revision. Each file holds only its latest value. An lf-num freezes a number into the authored text, so it keeps that value when the source changes. leaf page check advises when the number's source has changed.

Data lifetime

Data written to the page directory survives closing a tab or restarting the server. Draft text and interface arrangements are stored in the browser; the table shows which events preserve them.

What Reload Close the tab Server restarts Another browser
Picks, drags, and comments the user has sent ✓ ✓ ✓ ✓
External values, until the next write replaces them ✓ ✓ ✓ ✓
Words the user has typed and not sent ✓ ✓ ✓ ✗
Arrangements of Leaf controls ✓ ✓ ✓ ✗
Reading position, the open tab or panel, design mode ✓ ✗ ✓ ✗

The browser stores unsent draft text and interface state. Scroll position, the active tab or panel, and design mode belong to one browser tab. Draft text is shared between browser tabs showing the same page; arranged controls are shared across pages in that browser. Submitted comments and actions enter the page's event log, where all tabs and the agent can read them.

Server lifetime

The HTTP server makes the page directory available to a browser. A server started by an agent session stops when no live session claims the page. A claim registers a session to receive that page's updates. A server started from a shell, or with --persistent, runs until leaf server stop.

On leaf.page, a private container runs the same server for each visitor (How leaf.page works).

Addresses and access

For a local agent session, Leaf binds to loopback, so the page opens only on the serving machine. For an SSH session, it uses the session's SSH destination. The URL contains an access key shared by all Leaf pages on that machine. The browser stores the key in a cookie; sharing it grants access to those pages.

If the default URL cannot be reached, the agent restarts the server with leaf server start --host NAME, using a reachable hostname. Leaf uses the machine's existing networks and creates no public tunnel. The serving reference explains addresses, authentication, and server lifetimes.

Browser runtime

The browser runtime is the JavaScript Leaf adds to every page it serves. It upgrades the page's components, draws threads and Leaf's controls, sends the user's updates to the server, and keeps the page current while the agent works. It draws from three inputs: the authored document, which gives each component its initial state; the latest state the server sent; and the user's updates the server hasn't answered yet.

flowchart LR
  accTitle: Browser runtime inputs
  accDescr: The authored document, the server's latest state, and the updates awaiting an answer combine into one current state that components, threads and the On panel draw. Updates go to the page server, whose answers and later state reads replace the server's state.
  subgraph Inputs["inputs"]
    Doc["authored document: initial values"]
    Reading["server's latest state"]
    Pending["updates awaiting an answer, in order"]
  end
  Current["current state, read-only"]
  Parts["components · threads · On panel"]
  Server["page server"]
  User["user's update"]
  User --> Pending
  Doc --> Current
  Reading --> Current
  Pending --> Current
  Current --> Parts
  Pending -- "POST /api/event" --> Server
  Server -- "answer, with the state" --> Reading
  Server -- "GET /api/state when the freshness token changes" --> Reading
  class Current computed

Whenever an input changes, the runtime recomputes current state in one place: each component's state, each thread and whose turn it is, the open asks, and the On panel's lists. Components, threads and Leaf's controls draw from that result. None keeps its own copy of state or reads state back from the page.

The server does most of the work (Current state). The state it sends lists the recorded actions in effect for the revision the tab shows, with the threads, asks and agent activity. The browser applies those actions to the authored initial values and adds only the updates it is still sending.

Freshness checks and state reads

A visible tab asks the server for a short freshness token about four times a second (GET /api/news). The token is a digest of the page directory's file stamps and of the agent's presence: whether its session is alive, in a turn, or watching, and the neighboring pages the Pages drawer shows. A new revision, an appended event, a replaced data source or a change in the agent's activity therefore changes it. When the token differs from the one behind the state the tab last applied, the tab reads the full state (GET /api/state). Each check is an ordinary request, so an open page holds no long-lived connection. A hidden tab stops asking and catches up when it is shown again. The token request also tells the server that a user is looking at the page.

One state read is in flight at a time. Tokens that change while it runs queue a single read behind it. The server's answer to each update also carries the full state, which the tab applies the same way. Each state names the log position it was read through and the time the server took it, and the tab drops one older than the one it holds, so an answer that arrives late never takes the page back. A two-second clock updates displayed ages such as “2m ago”, retries a failed read, and reads again when the agent's activity is due to change.

When no server answers, the page keeps showing its authored state, and the banner says the server is offline and reconnecting. The next successful read applies as usual.

Updates awaiting an answer

When the user picks an option, drags a card or sends a comment, the runtime adds the update to an ordered list of updates awaiting an answer and recomputes current state with it. The result is drawn in the same frame that sends the request: the option shows as picked, the card stands in its new column, and the comment appears in its thread labeled Sending (Stages of an update). A spinner or a disabled control does not count as showing the result. Sent words leave the box they were typed in, and the browser's draft store keeps them until the server accepts the update.

Updates go to the server one at a time, in the order the user made them (POST /api/event). If the server cannot be reached, the update stays drawn, the bottom bar says the change is retrying, and the runtime sends it again every two seconds.

The server's answer is final. An accepted update's answer carries the event the log recorded and the full state, which the tab adopts. From then on the log's event draws the result, and the update leaves the list once every part of the page that shows it has drawn the server's version. A refused update drops out of current state at once, so the page returns to what the server holds. The bottom bar gives the server's reason, and a refused comment's words return to the box they were typed in.

Which widget asks are open, answered or withdrawn depends on the whole log, so only the server computes it. Answering one changes its component, and the answer the On panel shows, at once. The ask leaves the user's list when the tab adopts the state in the server's answer to that update, a moment later. Approving a version and replying to an ask in a thread take effect at once. Whose turn a thread is also comes from the server, with two local adjustments: a comment the tab is still sending passes the turn to the agent, and a refused one passes it back to the user.

Drawing current state

Computing current state and drawing it are separate steps. Each part of the page that draws state registers with the runtime: every component, the threads, the ask view, the On panel, and the marks Leaf adds to authored elements, such as who set a value or whether an ask is answered. Each new state is numbered, and each part reports when it has drawn the newest number. Leaf keeps these reports apart from the state, so a slow or failed drawing never changes what the state is.

Leaf's own parts draw in a fixed order: first the marks on authored elements, since they can add words to the text; then the threads, which find their passages in that text; then the ask view, which finds each ask's element among the nodes the threads placed; then the On panel, which shows each answer against that element. The order is declared once, so code that changes state never names the parts that must redraw.

A part can put off drawing a new state while drawing would disturb the user, such as during a drag or while a component's editor is open, and draws it when the user finishes. These wait until every part has drawn, not just until the state is computed:

  • Applying the server's state. Leaf counts it as applied, and writes notices of what changed, only once every part has drawn it. If drawing fails, the page reports the failure, keeps the accepted state, and tries again with the next one.
  • Removing an answered update. An update leaves the list of updates awaiting an answer only once every part that draws it has drawn the server's version, so the old state never shows in between.
  • Render checks. leaf page check --render waits the same way, so it judges the page the user would see.

Browser-owned state

Focus, scroll position, text selection, which disclosures are open, words typed but not sent, and a drag in progress belong to the browser. The log does not record them, the server's state does not carry them, and they never change current state. Each lasts as long as the element holding it. Data lifetime lists which of them the browser also stores across a reload.

When a redraw replaces an element, the replacement takes over that state under the identity the redraw already uses to match old and new. A thread redrawn by its id keeps its reply box's words and caret, and a component redrawn in place keeps the child elements that survive, with their focus. A box holding typed words closes only when the user closes it, with Send, Cancel, Escape or a press elsewhere, or when the passage it is about leaves the document. A scroll, a resize, a closing panel or news from the server never closes it.

Live revisions

When the server's state names a newer revision, the tab installs it in one of two ways, chosen by the revision's executable identity: its component registry, the JavaScript modules it loads, and its inline module scripts. If those are unchanged, the runtime patches the open document. Elements whose markup did not change stay as they are, with their focus, scroll and state, and changed components are captured again from the new source. Prose, styles and media change this way. If the executable identity changed, the tab loads a fresh document. The revision and the state that goes with it reach the page together.

Both installs carry the user across. A replaced element with the same authored id gets back its typed value, checked state, open or closed disclosure, inner scroll offset, focus and caret, wherever those differ from its authored markup. An element without an authored id carries nothing, so focus is never guessed. The reading position returns to the passage the user was reading rather than to a pixel offset, since content above it may have changed height. A fresh document receives all of this through a one-time handoff stored for the tab, which a later reload or a step back through history does not replay. Drafts and the arrangement of Leaf's controls have their own browser stores and need no handoff.

The install waits while it would interrupt the user: while a comment or reply is being written, a passage is selected, a card is being dragged, an undo or another update is awaiting its answer, or the version menu is open. Pressing the banner's newer-version chip installs the waiting revision at once.

Loading the runtime

The Loading tab describes when reading, commenting, and saved-state controls become available.

Two small scripts run before the first paint. One tells the theme to give each component the box its module will draw it in. The other reserves room for the banner and bottom bar, holds keys the user presses early, and shows a startup failure if the runtime cannot load. Loading the runtime later therefore moves nothing.

The runtime sends its first state read, restores the user's arrangement of Leaf's controls, loads the registry, and records each component's authored state from the HTML before any module changes it. It then imports the modules for the component tags the page uses, and no others.

Phones and touch screens

Leaf is designed for a desktop browser with a keyboard. On a phone or another touch screen, a user can read a page, comment on it and operate its widgets, but parts of the interface are unfinished. Apart from the gaps listed below, each action a key reaches can also be reached by touch, either by a direct gesture or by a control in the banner. How a page's regions stack in a narrow window is described under Narrow windows.

Banner and More menu

On a touch screen Leaf omits the bottom bar, which shows keyboard hints. In a window up to 480px wide the banner is one row: the agent's status in words, the Threads icon with its count of open threads, and the More menu (⋯). A notice replaces the status words for a few seconds.

More holds each page command a key reaches on a desktop and a finger has no other way to make, such as Select element, Search page, Undo, Draw mode, Design mode and Hide annotations, along with the controls a desktop banner shows on its row: On me, Comment on the page, and Approve version on a page that asks for sign-off. More shows a dot while an approval or another ask behind it is still open. While a mode or a search is open, its steps take the Threads icon's place on the row: Exit Draw mode, for example, or Previous, Next and Close for a search.

Comments, threads and widgets

To comment on words, the user selects them; the banner then shows Comment on selection, and tapping it opens the comment field below the block. Select element in More comments on a whole block, such as a table or a figure.

A phone has no room for the rail, so comment and decision markers appear as pins on their blocks. Tapping a pin highlights its passage and opens the thread card below the block. The Threads panel covers the whole screen. Hide annotations in More hides the pins.

A board narrower than 640px shows a Move to row on each card, so a card can reach a column off screen without dragging.

Known gaps

  • Some explanations appear only on hover.
  • Some text names a key, and a few actions open only by key.
  • Some exits a key provides have no touch gesture.
  • Some controls are smaller than a fingertip, and some diagrams are clipped.
  • Phone behavior is tested in browser emulation, not on a real device.

Page loading

The document, browser interface, saved state, and agent become available separately. The browser loads the JavaScript and requests saved state in parallel. Reading and new comments can begin before that state arrives.

Stage Available on the page Banner
HTML and styles arrive Text, links, and scrolling. The banner appears when the JavaScript starts.
JavaScript initializes the interface Leaf components and comment controls are being prepared. Loading…
Comment controls are ready; saved state is still loading New comments and drawings appear immediately. They remain Sending until the server accepts them. Choices that change saved state wait for the server's current reading. Loading saved state…
Saved state arrives Saved threads and choices join the page, preserving new comments and comment drafts. Saved choices become available for changes. The page’s normal status; each comment shows its delivery status.

Commands that need saved history wait for that history; document controls and new comments work as soon as the interface is ready. The banner shows Loading saved state while the request is unanswered. Only a failed read shows Offline; Decisions wait for a complete reading. A later answer joins the page normally. The interface and saved state can be ready before the agent starts.