The CLI, TUI and end-to-end harness each carried their own client: three reqwest builders with three timeout policies, three copies of the success-or-body check, and route strings at every call site. Two of them discarded the daemon's structured error and reported only a status code. Deletes `maestro-cli/src/http.rs`, both `ws_base` copies in the TUI, the CLI's partial `DaemonInfo` (it dropped `version` and `presets`), and the e2e harness's `check_ok`. `maestro-web` gains a protocol dependency and stops building the error envelope with a format string and reading it back as untyped JSON; the daemon and the proxy now register routes from the same constants. `maestro_web::router` returns `Result` — an unusable daemon URL fails at startup instead of at first connect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.