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
leafcommand, 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 stateprints 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 waitthe agent runs inside its turn (Harness differences). The agent answers withleafcommands and file edits. -
Registry. Each component's entry in its package's
registry.jsondeclares its attributes, the actions it accepts, and its authoring instructions. The browser runtime, the validator, andleaf page instructionsread 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.
Threads explains anchors and how they survive revisions, drawings, titles, summaries, and the Threads panel.
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 initleaf page instructionsleaf page media
|
|
Check and publish Validate the source, record a revision, or produce an offline snapshot. |
leaf page checkleaf page stampleaf 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 stateleaf page eventsleaf page transcript
|
|
Receive updates Read a delivery, confirm receipt when its transport requires it, or wait for updates directly. |
leaf delivery readleaf 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 statusleaf 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 openleaf 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 openleaf response replyleaf thread replyleaf thread editleaf thread resolveleaf thread summarize
|
|
Publish external data Replace or remove the current value of a declared data source. |
leaf data setleaf 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 startleaf server runleaf server stopleaf page claim
|
|
Connect a Codex task Connect delivery explicitly, or launch the experimental terminal client with its own App Server. |
leaf codex startleaf codex launch
|
|
Extend the vocabulary Create, validate, or install a package, or run one of its scripts through stdin and stdout. |
leaf package initleaf package checkleaf package installleaf 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.
- observation Sent The user comments on the lede. The page shows the comment at once, marked Sent.
- 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.
-
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. - 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.
- 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
A
Codex. The turn that the queued message opens
began, or the agent confirmed a complete mid-turn reading with
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
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> --ephemeralWhat 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 … idleThe 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:--donewhen it delivers what they asked,--askwhen it asks them back, or--todowhen 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 … --askA prose ask for the user, which stays open until they reply or settle it with a reaction, or the agent ends it withleaf 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 userSomething 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, wheremreaches it. Its Done, on its row or x wheremlands, 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 resolveA 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:
-
Ordinary HTML. Leaf styles headings, paragraphs,
tables, code, and
detailswithout requiring CSS classes. These elements provide most of a page's content. -
Theme classes. Leaf supplies classes for common
presentation patterns, including
.eyebrow,.lede,.callout,aside.sidenotefor a margin note, andol.stepsfor a numbered sequence like this list. -
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. -
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.
Whichever way that goes, which of the three jobs behind it are worth starting now?
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.
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
-
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. -
The page's
@layerblocks Page Declared after Leaf's tiers, so above them, as a CSS framework such as Tailwind writes them. -
lf-stateLeaf -
lf-themeTheme packages Each theme package'stheme.css, in the page's package order. -
lf-layoutsLeaf The Layout classes, such aslayout-column. -
lf-baseLeaf and widget packages Leaf's theme, then each widget package'stheme.css. A widget package's rules reach only its own widgets. -
lf-resetLeaf
A widget's shadow tree
- Marks Leaf
-
lf-stateLeaf -
lf-themeTheme packages Each theme package'sshadow.css. -
lf-shadowLeaf and widget packages Leaf's and each widget package'sshadow.css. -
lf-baseWidget packages Defaults the widget module adopts withinBaseLayer().
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.
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. |
layout-column on
main. Text keeps the 720px column, and a block with a
declared width, such as a diagram, extends past it.
layout-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.
layout-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:
-
columnis the text measure. In a wider region, such as a sidebar page's body, acolumncomponent starts at the same left edge as the text. -
wideextends up to 1080px. Wide blocks share this width so they align with one another. -
availableuses 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 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.
-
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.
<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.
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.
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 --renderwaits 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.
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.