Live-review iteration on the browser SPA, driven by crit feedback. Sidebar / session tree - Compact single-line leaf rows; status shown as a faint "alive" row wash instead of per-row dots (exited rows dim + keep the "exited" badge). - Host is now the first level of nesting (host → label path → session); the inline @daemon is dropped from rows. - Flush, borderless search input spanning the sidebar; drag-to-resize grip on the sidebar edge (width persisted to localStorage); narrower default. - Per-row clone action (opens the spawn modal prefilled); right-pointing chevron when collapsed. Chrome / layout - Buttons swapped to a bracket terminal style ([ + Spawn ]); ghost/icon drop the brackets. Old filled styles removed. - Squared corners globally (radius tokens) and tighter, denser spacing. - Session tally + daemon chips moved into the centered app header. - Shorter control height; shared --bar-h aligns the search and attach bars. Attach / terminal - Useful attach title (name · command @ host) over a bare UUID. - Robust fit: compute columns from the viewport's scrollbar-excluded clientWidth (fixes mobile under-fit and desktop overflow); ResizeObserver + font-load refit; full-bleed terminal on mobile. Spawn / empty states - Spawn modal accepts a prefill (clone); real "+ Spawn" buttons in welcome and no-sessions empty states. Dev - New ?kit design-kit page (kit.ts/kit.css, DEV-only) showcasing components. - Richer ?mock fixture with deeper label nesting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
maestro-web
Browser UI for Maestro. Connects to the registry HTTP/WS API for the catalog and to individual daemons (via direct WebSocket) for 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 registry or daemon if needed.
Layout
src/
main.ts # entry point, hash-route dispatch
api.ts # registry HTTP API client
events.ts # `/api/events` WebSocket subscriber
catalog.ts # session tree view (groups by metadata.label)
attach.ts # xterm.js + daemon WS attach 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 registry,
?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 registry's 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:8080
pnpm build # → dist/, embedded into the registry binary at cargo build time
The Vite dev server proxies /api and the events WebSocket to a
maestro-registry. Default is http://127.0.0.1:8080; point it elsewhere by
exporting MAESTRO_REGISTRY before pnpm dev:
MAESTRO_REGISTRY=https://maestro-registry.example.com pnpm dev
# Daemon-facing URL also works — `wss://...` is rewritten to `https://...`
# and a trailing `/daemon` is stripped.
MAESTRO_REGISTRY=wss://maestro-registry.example.com/daemon pnpm dev
Direct daemon WS connections (for attach) bypass the proxy and use the URL
returned by GET /api/sessions/:id/connect-info.
Production
pnpm build writes the bundle to web/dist/. The Rust registry crate
embeds that directory via rust-embed at compile time and serves it from
/. See crates/maestro-registry/src/web_assets.rs.