Easel #
A coding interface for the terminal. Lives at easel/ in the
Aesthetic Computer repository.
cd easel && ./install.sh
ac
ac and aesthetic open the same full-screen interface in the current
directory. Pass a directory to open another workspace:
ac ~/project
The interface owns the conversation, streaming, tool activity, interruption, and approvals. It does not launch a managed provider client. Behind it sits an engine bridge: a subprocess on stdio that the interface drives. A native local-inference engine can replace the bridge without changing the interface.
Each live TUI publishes its own Slab marker, so the menubar and prox ledger can
name, focus, wake, close, and track it as easel, including which
@handle it acts as. The marker also carries the piece and its address, which the
menubar draws as a scannable code on the session's prompt rock.
A signed-in session owns its code channel by name — <handle>/<slug> — and
/run accepts a push to it only from that handle's token. Ownership is the
token, not the secrecy of the name, so the channel's name can be the piece's
public address: prompt.ac/@handle/slug is what the rock shows, from the moment
the session opens until after it closes. Signed out there is nothing published
and no channel to own, so the code falls back to the opaque ~<channel> route.
The interface uses the Aesthetic Computer prompt's palette (purple ground, pink
prompt, orange highlight, magenta handle) and shows the signed-in @handle and
the piece currently being worked on in the header.
Inside the TUI: /login, /logout, /whoami, /publish [file] [slug],
/autopublish [on|off], /piece [name], /runtime [mjs|lisp|processing],
/backend [claude|codex],
/model [name], /qr, /live, /new, /clear, /help, /quit. Press
ctrl-c to interrupt a running turn or exit while idle.
Engine bridges #
Three bridges can drive a session:
ac # claude, on claude-opus-5
ac --backend codex # codex app-server
ac --backend ac # AC hosted, using your handle's budget
ac --model claude-opus-5 # a different model on the same bridge
ac --piece path/to/fogozo.mjs # reopen an existing piece and its versions
/backend and /model switch mid-session, preserving the visible conversation,
piece, channel and QR. A new provider thread receives recent user/assistant
context (up to 24,000 characters) and the current piece; provider thread IDs and
tool history are not portable. A failed connection returns to the prior engine.
/new explicitly starts a fresh conversation. /backend lists account options;
/model lists hosted choices or accepts a model name for your own vendor CLI.
AC hosted keeps GLM as its default. /model sonnet and /model gpt select
premium models and consume the same handle allowance. Model IDs were checked
against the OpenRouter catalog.
The allowance measures weighted tokens, not dollars, and is not an atomic spend
reservation. Unavailable budget checks refuse inference. New hosted choices
require the matching Lith endpoint deployment.
/about, or clicking EASEL, opens the feature map. Click @handle to open
your profile in a browser. Header targets highlight on hover in terminals that
support mouse reporting. /mouse off restores terminal selection; /mouse on
enables interaction again. EASEL_MOUSE=0 disables it at launch.
Wheel and Page Up/Page Down scroll the transcript internally, keeping the input
and footer fixed. Incoming output preserves your reading position. End with an
empty input, or /latest, returns to the live end. The about map scrolls too;
Esc returns to the conversation.
/performance [frames] measures the current JavaScript piece's headless logic
with seeded randomness and drawing-call counts. The default is 600 measured
frames at 800×600 after warmup. It runs in a restricted child with a timeout;
Ctrl-C cancels it. Browser rendering, rasterization and display latency are
excluded. Unsupported APIs/imports report an error. It requires Node permission
support (Node 24 or newer recommended).
The Claude bridge runs claude --print --input-format stream-json --output-format stream-json, the same headless protocol the Claude Agent SDK
speaks, driven directly over a pipe. That is why Easel still has no
dependencies: a subprocess on stdio is the same shape as codex app-server --stdio, and it carries streaming, tool calls and approvals without a package
tree behind it. Each bridge signs in with the vendor CLI's own credentials
already on the machine.
Approvals come back to this terminal on both bridges: y once, a for the
session, n to deny. Neither bridge inherits the user's own agent
configuration — Codex is pinned to on-request approvals and a
workspace-write sandbox, and Claude is launched with --setting-sources ""
and --strict-mcp-config — so nothing but the person watching can approve a
command in a session, and an a is never written to a settings file.
On the Claude bridge the session also carries Easel's own tools, served by
src/tools.mjs as the one MCP server the strict config admits: ac_api (the
piece API — runtime signatures, docs and real call sites, read off
lib/disk.mjs and lib/graph.mjs by bin/build-api-map.mjs into
context/api.json), ac_examples (pieces that call a symbol), ac_outline
(a piece's top-level symbols with line spans) and ac_symbol (one symbol's
source). They exist because the first ten sessions each spent six to twelve
shell calls — grep function circle( graph.mjs, sed -n 6590,6650p disk.mjs,
grep -rn "synth({" disks/ — rebuilding the same picture before the first
edit. The guides are inlined into the first turn for the same reason. All four
tools are read-only and pre-allowed; npm run context rebuilds the map and
npm test fails when it is stale.
The two are not equivalent on containment. Codex runs commands inside an
operating-system sandbox with the network off; Claude Code has no such sandbox,
so on that bridge the approval prompt is the whole boundary. The difference is
written down in docs/local-contract.md.
The session's piece, live on a phone #
Opening Easel opens a new blank piece. It gets a random pronounceable name, it is a real file in the workspace, and a QR code for it sits in the bottom right of the interface. Scan the code and the piece runs on your phone; every edit the agent makes reaches it a moment later.
The link works through the code channel Aesthetic Computer already uses for
live editing. The QR opens prompt~channel%20<channel>~!autorun, which runs the
prompt's own channel command, and the interface POSTs each save to /run,
which relays it over Redis and the session server to everything watching that
channel. It is the same path the VS Code extension has always used, so nothing
new runs on the server.
The encoded space is load-bearing. Aesthetic Computer routes anything beginning
prompt~ by handing the whole remainder to the prompt as a single parameter,
and the prompt splits its own arguments on spaces: write the channel after a
tilde and it arrives glued to the command name, so channel runs with nothing
to join and the phone sits on an empty prompt.
A push reaches only whoever is already on the channel — nothing keeps a copy of one — so the interface re-sends the current source every few seconds. That is what lets the code be scanned at any moment rather than only just after a save; a phone that arrives mid-session picks the piece up within a beat.
A blank that is never edited is deleted when the session ends, so opening and
closing the interface leaves the workspace exactly as it was. In the Aesthetic
Computer repository the piece is written to system/public/aesthetic.computer/ disks/; anywhere else it lands in the workspace root.
/runtime switches the language of the piece — mjs (JavaScript), lisp
(KidLisp), or lua (Processing, via L5) — and rewrites the blank. --runtime
picks it at launch, and processing, kidlisp, js and l5 are accepted as
names for the same three. Live pushes work for all three;
aesthetic.computer/@handle/<name> resolves .mjs and .lisp today, so a
Processing piece runs live but has no published route yet.
Processing pieces are written in Processing's vocabulary — setup and draw,
background, fill, circle — not Aesthetic Computer's paint. That is also
what keeps them running: a live push carries no file extension, so the client
recognises Lua by reading the source, and only source that opens with a --
comment and declares setup or draw is taken as Lua at all.
Account and publishing #
Easel reads the shared Aesthetic Computer sign-in at ~/.ac-token,
the same file ac-login and the AC desktop apps use. /login runs the
Authorization-Code + PKCE flow in your browser with a loopback callback and
writes that file; a sign-in or sign-out anywhere in the suite updates the
header live. Only the handle is shown.
/publish <file> [slug] puts a .mjs or .lisp piece live under your handle
at https://aesthetic.computer/@handle/slug, exactly like the web prompt's
publish command: it requests a presigned upload grant in your user bucket,
uploads the source, and reads the live file back before reporting the URL.
Writing a file under disks/ does not publish it, and the engine is told so.
Auto-publish does that on every save, so the session's URL is live the whole
time the piece is being worked on and the last edit is still there after the
terminal closes. It is on by default: the address on the rock is the piece's
published address, so a session that never publishes has nothing to point a
camera at. It coalesces — a publish runs once the saves stop,
never more than one at a time, and never twice for the same bytes — and it
flushes the pending save on exit. Off by default, since it writes to a public
route under your own handle: turn it on per session with the flag or
/autopublish, or for every session with EASEL_AUTOPUBLISH=1, which
--no-autopublish overrides. With it on the engine is told the piece is
already live and told not to ask you to publish.
aesthetic login
aesthetic whoami
aesthetic publish system/public/aesthetic.computer/disks/smiley.mjs
ac --runtime lisp
ac --backend codex
ac --autopublish
aesthetic doctor
npm test
Designing the furniture #
Easel does not draw all of itself. The QR, the live card of the piece and the
status stone are Slab menubar overlays parked on the terminal, and frame
filters Slab's own windows out of every capture — its usual job is reading the
machine underneath them. So a screenshot taken to judge the card's padding
shows the terminal where the card is.
frame <machine> --overlays opts out of that exclusion and widens a window
shot to a padded crop, so the menu bar an overlay is said to be flush against
is in the same picture.
easel/bin/design-loop.mjs is the whole cycle in one command: close the Easel
session, open a fresh one, wait for its overlays to land, photograph them.
Fresh because overlays are placed once, when a window appears — editing the
placement and reinstalling the menubar does not move what is already on screen,
so the only honest check is a session that has never seen the old numbers.
node easel/bin/design-loop.mjs # restart Easel, then shoot
node easel/bin/design-loop.mjs --shot # shoot what is already open
Edit an overlay, run slab/menubar-swift/install.sh, then run the loop.
Easel is proprietary. See LICENSE.
On Fish installations with existing ac or aesthetic functions, the
installer preserves them as ac-repo and aesthetic-platform.
The product boundary is recorded in
docs/local-contract.md.
Each complete piece update gets a local version (v1, v2, …). /versions
lists snapshots; /rollback vN restores one as a new version and sends it through
the usual live/publish path. Finish or interrupt the current turn and let uploads
finish first. History persists in ~/.local/share/easel/history/, keyed by the
piece's absolute file path; it is not yet shared between machines or accounts.
The AC backend streams text and completed write_piece checkpoints as they
arrive. It shows connecting, waiting, generating, composing, and writing states;
received kilobytes count stream bytes, not billed tokens. JavaScript checkpoints
are syntax-checked without executing them, so unfinished fragments keep the last
working preview. Other runtimes retain their own loader validation. This uses
ordered HTTPS streaming (SSE); a socket or UDP transport is not required for each
token to arrive immediately. Disconnects cancel an active response upstream.