The TUI read its leader from `[tui].leader`; the web used `DEFAULT_LEADER`, a module constant. Rebinding one left the other on `ctrl-a`, so the two escape hatches hanging off the leader (`leader ctrl-k`, `leader leader`) moved on one client and not the other. `GET /api/info` now carries `leader`. The web binds that when it arrives and rebuilds the leader-owning `CommandSet`, so the `?` overlay and the command bar render the configured chord; an absent or empty field — an older daemon, or an unreachable `/api/info` — leaves it on `ctrl-a`. Chose the existing `[tui].leader` served as-is over a new shared key: one knob, no precedence rules between two spellings, and no migration for operators who already set it. The name is a wart — it is the leader for every client now, not the TUI's — and `docs/COMMAND-BAR.md` and `docs/INSTALL.md` say so.
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.