One entry per command — stable id, plain-language label, key chord, run() — replacing the ad-hoc keydown listeners in main.ts, catalog.ts, debug.ts and attach.ts and the parallel SHORTCUTS constant in help.ts. The `?` overlay now renders the registry, so a binding and its documentation cannot disagree. Keys are canonicalised the way the TUI's `InputKey::canonical` spells them, and chords resolve like its `ChordState`: hold while the sequence is a strict prefix, otherwise dispatch. That gives the web a leader, `ctrl-a` by default like `[tui].leader`, held as a partial chord and `preventDefault`ed so it stops being select-all — the escape hatch in docs/COMMAND-BAR.md needs somewhere to land (#149). The leader stays out of real form fields, where select-all is still what the operator wants. Same keys, same effects: plain single-key bindings remain deaf inside form fields and the terminal's hidden textarea, chorded ones do not. Refs #148 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Maestro web UI
Browser UI for Maestro. The daemon embeds this bundle and serves it off its own
listener, so the page talks back to the same origin over /api/* — 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
multiview.ts # tabs + split panes
spawnForm.ts # shared spawn form (preset tiles, project picker, worktree)
spawnModal.ts # dialog chrome around the spawn form
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 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/. Under its default web feature
the daemon embeds that directory at compile time and serves it from /,
alongside the /api/* routes. See
crates/maestro-daemon/src/static_bundle.rs.