On the alternate screen there's no scrollback, but apps like Claude Code and tmux enable mouse tracking. When term.modes.mouseTrackingMode is active, turn finger drags into wheel events so xterm encodes them as mouse-wheel reports the app reads — scrolling its own content. Normal buffer still walks scrollback. 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.