The cleanup guard plus deterministic paths had created a trap: one untracked file an agent left behind — a log, a .env, a stray artifact — made the guard refuse removal, the directory stayed, and every later spawn of that branch hit PathExists. The branch was burned permanently and the product offered no way to clear it. A leftover tree registered for the same branch is now adopted, so the operator lands back in their own unsaved work. A tree a *live* session holds is still rejected, since two sessions must not share a checkout. Also: - Fast-forward still awaited /api/info, /api/projects and /api/names inline, the exact freeze the draft loaders were restructured to avoid. It posts to the same channel now. - Draft updates were routed by tab index, which shifts when a tab closes: an in-flight load could be dropped or land on the wrong pane. Drafts carry an id and are found by it. - extend_names left the name it had just displayed at the head of the queue, so the next reroll produced it again and the key looked dead. - The fast-forward fallback discarded info.home_dir, so its draft showed unabbreviated paths and seeded "Other directory…" with / — disagreeing with the normal draft entry point. - Web fast-forward mounted via openSession, which replaces the focused pane: pressing it while attached evicted that session from the layout. It reuses an empty pane or splits, as the TUI does. - branchEdited was inferred from the seeded branch value, but in "new" mode that value is the auto-synced session name — so any form rebuild froze the branch against later rerolls. - resolveBaseRepo ignored the worktree checkbox, rebuilding the fieldset expanded under an unticked box and probing gitroot for a worktree the operator had turned off. - short_hash used DefaultHasher, whose algorithm is not stable across Rust releases, in a directory name — a toolchain bump would orphan every worktree under the override root. Now FNV-1a.
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
multiview.ts # tabs + split panes; draft panes host the spawn form
spawnForm.ts # shared spawn form (preset tiles, project picker, worktree)
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 a draft pane, 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.