The browser app had no tests and no way to run any: package.json shipped dev/build/preview/typecheck, and CI installed Node and pnpm but never ran anything with them. Its densest logic was module-private, so even with a runner none of it was reachable. Extracts four pure modules out of DOM-heavy ones — tree.ts (directory grouping), keys.ts (terminal escape encoding), paths.ts (completion arithmetic), html.ts (one escaper, replacing four copies). Extraction rather than `export` because attach.ts imports xterm's CSS at module level, which a node test cannot load. CatalogStore takes an injectable socket factory, scheduler and refetch, so the reducer and the backoff schedule run without a server. snapshot() now returns a copy: it returned the live object while events mutated it in place, so listeners could not diff and every event rebuilt the whole tree. 36 tests across 6 files, and a `web` CI job running typecheck, test and build. vitest is pinned to 2.x — 4.x requires vite >= 6 and this is vite 5. 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.