Notes, inserts and Submit
On this page
A note is how you ask for anything you would rather not do by hand. An insert is a placeholder for something new: an image or a whole section. Submit hands everything waiting to your agent for one round (a run): it picks up every note and placeholder, changes the file, and answers each one while you watch.
Leaving notes
Press N and click the part of the page you mean. Write what should change, in plain words. Paste or drop a screenshot if it helps; by default the note also carries a picture of the part you clicked, taken without the editor on it, so your agent sees what you saw. Links you type become attachments. ⌘/Ctrl+Enter saves.
Every note also keeps the page's state at the moment you started it: its address, the window's size, how far you had scrolled, the markup around the part, the part's classes and states, and a freeze frame, a picture of the window exactly as you saw it, drawn from the page's own markup at that instant. It arrives on the note a few seconds after you save. Your agent reads it all and is told to find the source that produces what you saw at that moment, including what a script, a step or an animation put there. This matters most when you edit in place, on a page that moves.
Tick Send this part to Canva to rearrange to ask for a Canva design of that part instead (below).
Each note records:
- the element it is pinned to: its tag, its text, a CSS selector, its line in the file, and a snippet of its HTML
- the movement it sits in, when the page marks its steps with
data-movement="<id>", so the note is attributable to a step of the page - any images you attached (saved in
.zest/attachments/), and its freeze frame - the page's state when you made it (
state: see Editing in place) - when you left it
A numbered pin marks each waiting note on the page. Changes lists them with the same numbers. Click a note's label to scroll to its element. Use + Note on the whole page for anything that is not about one place.
Notes above the page
On a live page, your team's extension can name levels above the page, such as every page in a section or every page on the site. A note on the whole page then asks what it applies to: This page (the default) or one of those levels. Changes shows the level in place of "Whole page". Your agent reads that the note is about those pages, not only this one, and changes the shared source they read. When the change needs a decision above the page (a name, a claim, a price, a section's rules), it edits nothing and answers with a proposal. A note about a level always goes to the composing model. A note on an element is about that element and has no level.
Until your agent answers it, a note is yours: click its text to change it, or remove it. Once answered, it is history and read-only.
The three states
Every change in Zest is in one of three states, and the editor uses the same words for each everywhere:
| State | Covers | Where you see it |
|---|---|---|
| Saved to the file | Words, images, links and settings you changed in edit mode | On the page at once, and in Changes under Saved to the file since the last run |
| Waiting for your agent | Notes, placeholders, and edits Zest could not write safely | Pins and placeholders on the page, the count on Changes and Submit, and Waiting for your agent |
| With your agent | Everything a Submit sent, until it is answered | A pulse on the page over each one, and Working on the bar |
| With another agent | Notes Submit handed to an agent already working in the checkout (below) | A still outline marked With <its name>, and its own list in Changes |
| Done in rev N | Every answered note | Under Done, grouped by run, with your agent's answer |
Inserting something new
Press I (or Insert on the bar). Point between two blocks of the page: a line shows where the new thing goes and names the block it sits next to ("below the section \"Pricing\""). Near the edge of a bigger block, the line moves out to that block's edge. Click, or use the arrow keys and Enter, then choose:
- Image: pick one in the image picker, from any tab. It is copied into the project, and a dashed frame on the page shows it where it goes. Add alt text, what it is for, and a size.
- Section: say what the new block should do for the reader. That one field is required, because your agent builds from it. You can also say what goes in it (words, numbers, a quote, cards, steps, a call to action, logos, questions), point at a block on the page it should look like, paste the words to use, and drop in a sketch.
Either way the page gets a placeholder: dashed, hatched and labelled as a note, so nobody mistakes it for the design. It is never written to the file. The editor draws it from the note every time the page loads, and Edit and Remove sit on it until you Submit. The note carries what your agent needs to build the real thing in the page's own markup: where it goes, the rows around it and their markup, the page's outline with the spot marked, and the block it should look like. A selected block's Insert button and the arrow keys do the same without a mouse.
Handing over: Submit
Press Submit on the bar. Your agent starts on this laptop, works through everything waiting, and the banner shows each step in plain words ("Reading your notes", "Editing index.html", "Taking a screenshot"), with Stop. Everything you submitted pulses: a soft orange over the block around each note, and the placeholders themselves. As your agent answers each one, its pulse stops and a check marks the spot (an amber mark if it could not, with the reason); hover or click it for the answer. A mark is for reading once: close its answer, or mark it Right or Not right, and it leaves the page. A part you cannot see, such as one inside a closed menu, gets its mark when it shows. When the round ends, the page reloads with the changes. Notes you leave while it works wait for the next round.
Before you press it, Submit shows which model takes the round and the most it may spend ("Claude Opus · up to $8", or "3 tasks · up to $3" when the round splits into parts); hover it for each part and why it went to that model. Every round leaves a trace: open Trace in the corner when the round ends, or in Changes later, to see what each model did, how long it took and what it cost. Click a check (or open Changes) to mark the answer Right or Not right, with a line on why if you like. Zest uses those marks to choose models, and your mark overrules any automatic reviewer's.
Until it is set up, the bar says Turn on Submit, and only when something is waiting. Click it for a checklist that ticks itself off: Claude Code is installed, new enough and logged in, and you said yes in Claude Code. That yes is given once, in Claude Code, never in the page: paste /artifact-editor:setup. It shows exactly what a round may do before you agree:
- read and edit files in the page's folder, and nowhere else
- run
zestcommands, and ask a fresh subagent for a second opinion - no web, no web search, no MCP servers (an extension your team approved may name a tool it needs, such as Canva)
- stop at a spend cap and a turn cap you choose ($2 and 100 unless you say otherwise)
Your own Claude Code settings, hooks and deny rules still apply. zest runner off takes the yes back. If a run needs something it is not allowed, it says so and leaves the note open: copy the prompt and run it in Claude Code, where you can approve it. A run that stops without finishing the round says that too; it never shows as done.
Your team's extension may set Submit up for its own pages: its own round, the folder it starts in, the app folder it may edit, and the tools it may use (see Your team's setup). Changes and Setup list exactly what a round may do either way.
Another model
A round can run on another model behind an Anthropic-compatible endpoint (a cheaper model, or a gateway your company runs). Claude Code still drives the round; the other model answers it:
zest runner worker --url https://api.moonshot.ai/anthropic --key KIMI_API_KEY --model kimi-k3 --label "Kimi k3"
zest runner worker off # back to Claude, with your login
--key names the variable that holds the key; Zest reads it from your environment, a .env beside the page or its config file when a round starts, and sends it only to that address. Your own Anthropic key is never passed to it. Tools that need your claude.ai login (its connectors, such as Canva) are not there on another model, and the spend cap is still counted at Claude's prices.
A team's extension can choose per round: its listener looks at the notes waiting and sends a round that needs the strongest model to one of Claude's own ({ label, model }, on your login), and the rest to the cheaper one. The banner names which. See Your team's setup.
What a round remembers
When a round finishes, Zest keeps what it did beside the page, in .zest/grooves.json: the files it read before its first edit, the files it edited, the commands it ran, how long it took and on which model. The next round on that page whose notes are of the same kind (an image swap, a new section, a hand edit) gets that as a few lines at the top of zest notes pending and of the copied prompt: start from these files, search only for what they do not answer. The first round of a kind does the finding; the rounds after it should not have to. zest grooves <file> lists them.
Rounds split into tasks
A team's extension can turn on Zest's own round (see Your team's setup). Then Submit does more than start one agent:
- It records the page's git checkout as it is (a snapshot commit that no branch points at, kept under
refs/zest/, never pushed) and screenshots the page at desktop and phone width. - It splits the notes into tasks by the kind of work. Composing work (a new section, anything interactive, animated, drawn, re-laid out or cloned from a reference, or a note about a level above the page) goes together into one task on the strongest model, in the checkout, where it can look at the live page. Composing also takes a note that asks for judgment or reaches past one spot (match a style, follow a reference, "everywhere", the brand, a rewrite) and a long note or one with several asks. Mechanical work (words, an image swap, a hand edit the editor could not write, in a short note) goes to a fast model. A mechanical task that stops with its notes still open (it ran out of budget or turns) runs once more on the composing lane, in the same round. With fan-out on, each mechanical task runs at the same time as the others, in its own copy of the checkout (a git worktree), and comes back by a three-way merge. A task whose change collides with another's runs again in the checkout. Canva notes run on your own Claude, because Canva needs your claude.ai login.
- Each task gets one prompt that holds everything it needs: its notes with their full ids, the files behind the page, your team's context, the brief and the last round's groove. Before it edits, it records how it read each note and its plan (
zest notes plan), and answers each note as it goes. Mechanical tasks do not take screenshots or run the checks; Zest does both once, after the round. - When every task stops, Zest runs your team's checks in the checkout. If one fails, the round gets its output once, as a repair turn.
- Zest closes the round, screenshots the page again, and writes the trace.
Every task starts slim: no settings files or CLAUDE.md from the folders it starts in, only the hooks and MCP servers your team names, and a short list of tools. That takes the first prompt from tens of thousands of tokens to about ten thousand, which is most of what makes a small change quick.
Traces
Every round leaves a trace in .zest/traces/<page>/<round>/: what each task planned and what it did, which model ran it and why, where the time went (starting up, the model thinking, tools), how many model calls it made and how long each took, the prompt's size, the files it read before its first edit and the files it changed, the checks, the patch, and the page before and after at desktop and phone width. Open it from Trace when a round ends, or with zest trace page.html --open. zest trace page.html prints it. On a page that marks its steps with data-movement, the trace also lists which movements changed and the notes each one carried.
Labels and evals
Mark an answer Right or Not right (or run zest label page.html <id> pass|partial|fail --why "..."). A team's extension can also turn on a reviewer: after each round, a fresh Claude Code that never saw the round's reasoning reads the notes, the plan, the patch, the screenshots and the checks, and labels each answer. It reads; it changes nothing. Your label always wins over the reviewer's.
Labels make evals. zest eval from page.html turns a round whose answers were labelled right into cases: the notes, the checkout before, and the accepted result as the mark. zest eval add sets a case by hand from any two commits, for a mark no round produced. zest eval run page.html replays the cases on other models, each in its own worktree with its own dev server when your team's extension can start one, and the reviewer scores each replay against the mark: pass means at least as good as the mark at what the notes asked. zest eval report shows, per kind of work and per model, how many passed and how long they took. zest eval tune --apply moves each kind of work to the fastest model that passes as often as the best one, with at least three cases behind it, and Submit shows that evidence beside the model it picks.
The ledger
Traces, labels and plans live beside the page, in the checkout that ran the round, and a worktree that is deleted takes them with it. The ledger does not. It is one file per machine, ~/.zest/ledger.jsonl, outside every checkout, with a line for each of these:
- request: a note a round took, its words, where it pointed and the page's state when you left it.
- reading: how the agent read the note before it edited: what it thinks is asked (copy, layout, motion, picture, structure, content-model, bug), how exactly the note says it (exact, directional, open), what it assumed and what it followed. The round asks for this with
zest notes plan. - result: what the round did for the note: the files it changed with lines added and removed, each file's part (the page, a shared component, content, a style or token file, an asset, a test), which model ran it and why, the steps, time, cost and checks.
- outcome: what happened next. A label (Right, Not right, or the reviewer's), with the kind of miss and, from the reviewer, what would have helped. A note you leave on the same element within a day (a follow-up). A hand edit to words the round wrote, within a day. A restore to a version from before the round (undone).
zest ledger sweepadds merged (the change is onorigin/main) and reverted (it landed on its branch, then went).
zest ledger sums it up; zest ledger --rows prints the rows for an agent to read. Eval cases live in the main checkout's .zest/evals/, which every worktree of the repo shares, so a case made in a worktree outlives it too. ZEST_LEDGER=0 turns the ledger off. What it stores and who sees it: Security.
When someone is already working here
Two agents in one checkout trip over each other: the second edits files the first is halfway through. If your team's extension can tell that an agent is already working in the page's checkout (an owner hook, see Your team's setup), Submit asks before it starts a second one. It asks only when that agent did something in the last 15 minutes. With no extension that can tell, Submit works exactly as above.
- Send to <its name> (the default; Enter picks it): Zest hands every note Submit would have sent to that agent. It folds them into its own work, and may hand simple ones to helpers. Zest says whether the agent was woken or will see them the next time it acts.
- Run here anyway: today's round, as if nobody were there.
- Wait until it is quiet: Zest keeps the notes and asks every minute whether that agent is still working. Once it has done nothing for 15 minutes, or is gone, your round starts by itself. The bar says Waiting; click it to run now or stop waiting. The wait survives closing the page and restarting Zest.
A note you sent stays open and shows With <its name> on the page instead of the pulse. Changes lists it apart, says whether the agent has read it yet, and offers Take back, which puts it back in what Submit sends. When the agent answers it, the mark turns into the usual check, and Changes shows the answer under Answered by the agent working here. Submit never sends a note that is with another agent, and a round leaves it alone.
How the other agent gets them
Zest writes the notes to that agent's mailbox, one file per agent in Zest's own folder, outside every checkout: ~/.zest/mailbox/<agent id>.jsonl (ZEST_HOME moves it, with the ledger). The agent reads it with the command line:
zest inbox <agent id> # what is waiting for it
zest inbox <agent id> --take # take it: each note's words, where it is pinned, what the page showed, how to answer
zest inbox --all # every agent with notes waiting
Then it answers each note as usual, zest notes answer page.html <id> --said "...", and the person sees the answer on the page. A note the person took back, or that someone already answered, is listed as not the agent's any more.
Each mailbox entry is one JSON line:
{ "schema": 1, "type": "request", "id": "m_3f9c0a2be41d7756", "at": "…", "to": { "id": "…", "label": "…" },
"file": "/abs/path/page.html", "page": { "route": "/pricing", "origin": "http://localhost:3000" },
"repo": { "name": "site", "origin": "…" }, "by": "person",
"notes": [{ "id": "n_8f3a2c1d", "kind": "note", "text": "…",
"anchor": { "selector": "…", "tag": "h1", "text": "…", "movement": "hero" }, "level": null,
"state_summary": { "url": "…", "route": "/pricing", "scroll": { "x": 0, "y": 1200 }, "movement": "hero" } }] }
file is the page Zest keys its notes on; page is null unless the page is a live page. Taking an entry appends { "schema": 1, "type": "taken", "about": "<entry id>", "at": "…", "by": "…" }. The file only grows, and a line a crash cut short is skipped. The ledger gets a request row for each note sent, with request.routed_to, under the entry's id as its round; the result row follows when the agent answers.
Handing over: the prompt
Until Submit is set up, or whenever you would rather run the round yourself, copy the prompt from Changes. There are two:
- Claude Code + Zest:
/artifact-editor:run /path/to/page.html. The skill does the rest. - Any agent: the full instructions with every note inline. Use it with Cursor, Codex, claude.ai, or any agent with a shell. If the agent cannot run commands, it can still read the notes and tell you what it changed.
Canva
Canva is for trying a layout. Send a part to Canva from the note box; on the next run your agent turns that part into a Canva design whose words and shapes you can move (Canva's Magic Layers), and answers with the link. Under the answer you get Open in Canva, Bring my Canva edits back and Send it again. Bring them back, and the run after reads the design and changes the page to match. The page itself changes only then. A note whose text contains a Canva design link is a "bring it back" note too.
This uses your own Canva connector in Claude Code (claude.ai, Settings, Connectors). Without it, the run says so and leaves the note open.
Where notes live
Beside the page, in .zest/, unless your team keeps them in a shared store through an extension (see Your team's setup). Either way the editor, Submit and the agent's zest notes commands read and write the same notes.
What a run does
The /artifact-editor:run skill tells the agent to:
- Read everything first:
zest notes pending page.htmlprints the brief, every open note with its element and line, attached screenshots, and the edits you saved by hand. - Start the run:
zest notes start page.html. Your tab now says your agent is working; notes you leave from here wait for the next run. Then say how it reads each note:zest notes plan page.html --note <id> --intent copy --specificity exact --change "...". - Change the HTML file itself (or the source of a built or live page), keeping your hand edits. A placeholder becomes the real thing, in the page's own markup.
- Look at the result:
zest shoot page.html --full(and--width 390for a phone) and open the screenshot. - Answer each note in your terms, as soon as it is done, so its pulse stops:
zest notes answer page.html <id> --said "...", or decline with a reason:zest notes drop page.html <id> --why "...". - Finish:
zest notes finish page.html --summary "...". Your tab reloads and shows the answers.
A run is never left half done. A note the agent cannot do is answered saying why.
The brief
More > Brief holds four lines: who reads the page, what they believe now, what it should move them to, and what they do next without replying. A fifth, Settled, lists calls you have made that no run may reopen ("keep the headline", "no pricing on this page"). Your agent reads all of it before every run, so each change stays true to what the page is for and nothing you settled comes back. It is optional, and worth filling in for anything that leaves your laptop.
The .zest/ folder
Zest keeps everything it knows beside the page, in plain JSON:
.zest/
README.md what this folder is
page.html.json notes, runs, hand edits, the brief
provenance.json where every placed image came from, and its licence
history/page.html/ versions, one file each
attachments/page.html/ screenshots pasted into notes
candidates/page.html/ generated images not yet picked
originals/ images before Shrink
shots/ your agent's screenshots
traces/page/<round>/ each round's trace, patch, before and after screenshots, report.html
labels/page.html.jsonl Right, Not right and the reviewer's labels
plans/page.html.jsonl each task's plan per note
grooves.json what past rounds did, for the next round of the same kind
Three things live outside it, in ~/.zest/: the ledger, ledger.jsonl (see The ledger); the mailboxes, mailbox/, and the pages waiting for quiet, waiting.json (see When someone is already working here).
Evals live at the top of the main git checkout, in .zest/evals/ (cases and runs), shared by every worktree of the repo (a case an older Zest wrote inside a worktree is still read from there), and the snapshots they replay from are kept under refs/zest/. zest never pushes them; git push only sends them if you ask for those refs by name.
Commit it if you want your team to see the notes. Add .zest/history/ and .zest/candidates/ to .gitignore if you do not want those in git. Deleting .zest/ loses notes and versions, never your page.
page.html.json in short:
{
"schema": 1,
"rev": 12,
"run": null,
"runs": [{ "name": "rev 1", "started": "…", "finished": "…", "summary": "…", "answered": 3, "left_open": 0 }],
"notes": [{ "id": "n_8f3a2c1d", "kind": "note", "text": "…", "anchor": { "tag": "p", "text": "…", "line": 42, "selector": "…" },
"attachments": [], "status": "open", "answer": null }],
"edits": [{ "kind": "text", "line": 47, "summary": "\"Old\" -> \"New\"", "run": null }],
"brief": { "reader": "…", "believes": "…", "moves_to": "…", "next": "…", "settled": "…" }
}
kind is note, edit (a hand edit Zest could not write), image (an image change it could not write), canva and canva-back (the Canva round trip), or insert (a placeholder; its insert field holds what it is, where it goes and what it is for, and its text says all of that in sentences). A note on the whole page about more than this page carries level: { id, label }. A note handed to an agent already working in the checkout carries routed: { to: { id, label }, at, entry } and stays open. status is open, answered or dropped. Writes go through a lock and a temp-file rename, so the editor and the CLI never write over each other. If the file stops parsing (a bad merge, a hand edit), Zest leaves it exactly where it is, shows no notes, says so in a banner, and refuses to save new notes until you repair the JSON or run zest notes repair page.html, which keeps the damaged copy beside it and starts fresh. It never moves or overwrites your notes on its own.