The one that loses data: the project picker lists linked worktrees as pickable rows, so `base_repo` was easily a worktree rather than the main checkout, and the new placement then nested the tree inside it. Git hides a registered worktree's contents from the outer `git status`, so the outer tree read clean and removing it deleted the inner one's uncommitted work — while that session could still be running. Placement now resolves to the main checkout via `--git-common-dir`, and removal refuses outright when `worktree list` reports anything registered underneath. Also: - The `worktree_root` override dropped the old UUID without replacing it, so two repos spawning the same branch name targeted one directory and the second failed. The repo basename now sits between root and slug. - Project staleness read `.git/HEAD`'s mtime, which only moves on checkout. A repo committed to daily from one long-lived branch reported its last branch switch and got dimmed. Prefer `logs/HEAD`. - The projects cache TTL (5s) was shorter than its own background refresh (60s), so nearly every request rescanned all of $HOME under the scanner lock — and draft panes re-request on unrelated re-renders. Now 180s. - onSuccess resolved the leaf against the active tab only, so switching tabs mid-POST orphaned the session. Searches every tab.
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.