Skip to content

Repository files navigation

leaf

Not ready for general use. Watch this space, and hopefully there'll be more to say soon.

A Claude Code and Codex plugin for collaborating with your agent in HTML. The agent presents plans, write-ups, and work in flight as a web page rather than a wall of terminal text. Select any line to comment on it like a shared doc; your comment reaches the session, and the agent ships a revised version. It comments back the same way — in the margin, on the passage in question, rather than in the terminal. A page tracking work in progress keeps up with it: items go from planned to done as the agent works through them, and the browser follows each new version on its own.

leaf demo

docs/ is the site, published at https://leaf.page/: index.html is the tour (what it does, and one session end to end), examples.html lists the example pages, how-it-works.html covers the mechanism, and customizing.html covers themes and project widgets. Each uses leaf's own theme, so they double as specimens, and each opens the same from a checkout as from the web.

Install

Claude Code:

/plugin marketplace add max-sixty/leaf
/plugin install leaf@leaf

Codex:

codex plugin marketplace add max-sixty/leaf
codex plugin add leaf@leaf

No config or account is required. Leaf needs uv on PATH (interact.py declares its dependencies in a PEP 723 header) and a browser on the same machine as the session.

Then ask the agent for a page. The explicit skill is /leaf [topic] in Claude Code and $leaf [topic] in Codex; with no argument it presents whatever the session is currently about.

Customize

Project customizations live in .leaf/; user customizations live in ~/.config/leaf/. A short theme file cascades over the defaults, and a widget scaffold adds a registry entry, CSS, and optionally an ES module:

leaf customize theme
leaf customize widget lf-callout

Add --upgrade to the widget's first scaffold command when it needs browser behavior.

The next leaf page init <page-dir> vendors the merged layer. The customization guide covers the file contracts and the project/user precedence.

Examples

examples/ holds a complete page for each kind of write-up, including a dashboard meant to change as work finishes. gallery.html puts them on one page as tabs (generated by scripts/gallery.py; edit the examples, not it). Serve one against the shipped layer to try it:

plugins/leaf/bin/leaf page init /tmp/demo
cp examples/triage-board.html /tmp/demo/versions/v1.html
plugins/leaf/bin/leaf version publish /tmp/demo --version 1 --text "demo"
plugins/leaf/bin/leaf server run /tmp/demo

The website

scripts/site.py assembles https://leaf.page/ into .tmp/site, and .github/workflows/publish-site.yaml runs it on every push to main that touches the pages, the examples, or the layer. The docs pages are copied with their three checkout-relative paths substituted (the theme, a link into the payload, and a link to an example), and each example is exported through the shipped version export, so what a visitor reads is Chrome's own drawing of the page with the comment layer removed. The build resolves every local link it wrote and refuses a site holding one that reaches nothing.

scripts/site.py

Developing

The suite is integration tests over the real thing. test_interact.py exercises the lint, vendoring, publishing, catalog, export, thread-markup validation, and the anchors leaf comment writes by reading a version file. test_render.py loads the shipped examples in a real browser (both color schemes) and asserts what a static lint can't reach: every widget upgrades into a box with usable size, the document and the comment panel scroll in separate regions, the comment box grows without any script sizing it, and neither pressing a control nor news arriving on its own moves the controls beside it. One journey test drives the whole loop through the real UI (select a passage, comment, drag a card, follow the next version, find the comment still anchored) and pins the event log it leaves. test_product_page.py holds the pages under docs/ to the shipped theme and widget registry, and test_site.py builds the site and reads it back: the theme it serves is the shipped file, each exported example stands up with its scripts gone, both palettes reach the site's own layer, and no page scrolls sideways on a phone. Playwright attaches to the Chrome already installed (channel="chrome"), so there is no browser download and still no build step. Driving a page by hand to check a change works the same way: run page init for the directory, then serve it from interact.handler_for(page_dir, token) in-process as the fixtures do, opening the page with that key in the query (?t=…). server run instead puts a live page behind the session, and the loop's hooks then hold it to watching that page.

The suite runs in the environment pyproject.toml names and uv.lock pins. That is the developer's environment only: leaf declares what it needs in interact.py's PEP 723 header, which is what installs it with no build step, and the project file leaves that alone. The tests need the same set anyway, because they load interact.py by path.

uv run pytest tests

Two minutes rather than eleven, because pyproject.toml shards it across eight workers. That is the whole command: no variable in front of it and no step before it. The fixtures move the two XDG directories leaf reads (config_home, state_home) and leave the rest of the home alone, so every leaf the suite shells out to finds the uv cache the developer already has, and a fresh checkout fills it the way any other run would.

One resolution sits outside uv.lock: the Playwright bin/leaf supplies to version export on top of the script's header (it says why), which uv asks the index for whenever its cached answer has gone stale — a second or so, once. On a machine with no network, hold the whole run to the cache instead:

UV_OFFLINE=1 uv run pytest tests

Ruff and prettier run from .pre-commit-config.yaml, which says what each covers and why. wt merge runs that set and then the suite as pre-merge hooks (.config/wt.toml), and refuses a tree that doesn't pass; .github/workflows/ci.yaml runs both again on main and on every pull request. Before then:

pre-commit run --all-files

CI is also the only place either gate meets a platform that isn't macOS, and what the two disagree about is what a browser test measures: how wide a system font sets a word, whether a scrollbar takes a gutter out of the window. scripts/linux-suite.sh runs the suite where CI runs it, in a container carrying the runner's Chrome and its fonts, so a failure reported there is one to reproduce rather than one to guess at. It takes pytest's arguments, and needs a Docker daemon that can run linux/amd64:

scripts/linux-suite.sh

Rebuilding the syntax bundle

Code blocks are colored in the browser from plugins/leaf/skills/leaf/assets/vendor/highlight.esm.js, which upstream doesn't ship in a form a page can import — so it is bundled here. scripts/vendor-highlight.sh rebuilds it, reading the language list out of the registry's $languages.names so the bundle can't offer a language the lint rejects. Add a language there, then rerun the script.

Thread messages render their Markdown in the browser too, from vendor/marked.esm.js — upstream ships that one as a single dependency-free ESM file, so scripts/vendor-marked.sh is a copy at a pinned version, not a build.

Recording the demo

scripts/record-demo.sh drives a session through the shipped server and Chrome, then writes the resulting animation to docs/demo.gif.

Related

notes/comparisons.md reads the nearby projects against leaf, and covers where leaf is the wrong choice.

License

MIT. See LICENSE.

About

Review Claude's plans and findings like a document: Claude serves a commentable HTML page on localhost, you mark it up in the browser, and revised versions ship with changelogs.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages