Six findings from reviewing the accumulated blitz/spawn-v2 diff: - loadRecents restored the seeded cwd but never emitted it, so the leaf's draft kept cwd:"" and the next render reseeded the form blank. - stripDrafts did not recurse, so every half-filled draft in a split tab was written to localStorage; the old test passed only because hydrate discards draft on the way back in. - showRawCwd and selectPreset grabbed focus from async init paths, so a draft pane stole the caret seconds after mounting. - .mv-draft inherited no layout once the form left .modal-body. - onSuccess mounted the new session onto a leaf that may have been closed while the POST was in flight, stranding it in no pane.
maestro-web
Browser UI for Maestro. Talks to a single local daemon over its /api/*
HTTP/WS surface — session catalog, spawn/kill, and terminal I/O.
Framework choice: vanilla TypeScript
No frontend framework. The UI is small enough that the cost of pulling in Preact / Solid / Leptos doesn't pay back. xterm.js does all the heavy rendering; everything else is a session tree and a spawn form, both trivial in plain DOM.
This decision is non-architectural — swap in a framework later without touching the daemon if needed.
Layout
src/
main.ts # entry point, hash-route dispatch
api.ts # daemon HTTP API client
events.ts # `/api/events` WebSocket subscriber
catalog.ts # session tree view (groups by working directory)
attach.ts # xterm.js + attach WS view
spawn.ts # spawn-session modal
theme.ts # dark/light theme toggle + persistence
types.ts # mirror of `maestro-protocol` JSON shapes
styles.css # design-system tokens + component styles
mock.ts # DEV-only review fixture (see Design)
Design
The visual system (OKLCH token palette, monospace-first typography, dark+light
theming, motion, components) is documented in ../DESIGN.md;
the tokens live in the :root block of src/styles.css. Strategic intent
(register, brand, principles) is in ../PRODUCT.md. Theme
follows the OS by default and can be pinned via the header toggle (persisted in
localStorage).
mock.ts is a DEV-only fixture (stripped from production builds): visit the dev
server with ?mock to seed a representative catalog without a running daemon,
?mock&spawn to open the spawn modal, and ?theme=light|dark to pin a theme.
Routing is hash-based:
#/→ catalog#/attach/<session-id>→ terminal
This keeps the static handler dumb (no SPA fallback needed). Concurrent
attaches: open the same #/attach/<id> URL in another tab.
Dev
pnpm install
pnpm dev # http://localhost:5173, proxies /api → http://127.0.0.1:7070
pnpm build # → dist/, embedded into the maestro-web binary at cargo build time
The Vite dev server proxies /api and the WebSockets to a local daemon.
Default is http://127.0.0.1:7070; point it elsewhere by exporting
MAESTRO_DAEMON_URL before pnpm dev:
MAESTRO_DAEMON_URL=http://laptop.lan:7070 pnpm dev
Production
pnpm build writes the bundle to web/dist/. The maestro-web crate embeds
that directory at compile time and serves it from /, reverse-proxying /api/*
to the daemon. See crates/maestro-web/src/static_bundle.rs.