Add an always-visible "debug" button to the topbar (next to the design-kit
shortcut) that toggles the debug HUD. Reflects the current state (lit in accent
when on) and stays in sync with the Ctrl+Shift+D keybind via the shared toggle
event.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A game-HUD-style debug overlay (FPS-counter for sessions) that surfaces the
daemon-inferred fields from #18 — activity, foreground, idle time, status — on
the active session, live off the existing events WS. Lets us validate the
inferred data before designing any "real" UI, and stays useful as a permanent
debugging tool.
- Off by default; toggled via Ctrl+Shift+D (persisted in localStorage) or `?hud`
in dev (debug.ts).
- Fixed bottom-right overlay, pointer-events:none so it never blocks the
terminal; activity value is color-coded (active/awaiting/idle).
- Generic: renders every non-client-owned Session field, so a new daemon-derived
field (#19 `detail`, later metrics) surfaces with no overlay change. Idle time
re-renders on a 1s ticker; values update on SessionUpdated.
- Pure consumer-side: no daemon/protocol/registry changes. The `?mock` fixture
seeds varied activity so the HUD can be reviewed without a daemon.
Closes#20
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an orthogonal activity dimension to sessions, inferred entirely from
structured tmux/`/proc` state — no PTY byte-stream parsing, no work on the
attach hot path.
Protocol: `ActivityState` enum (Active/Idle/AwaitingInput) + three additive,
serde-defaulted `Session` fields (`foreground`, `activity`, `last_activity_at`)
so the snapshot/diff flow stays forward-compatible with older daemons.
Detection (piggybacks the existing ~2s liveness tick, no new timer):
- foreground command via `tmux list-panes -F '#{pane_pid} #{pane_current_command}
#{pane_tty}'`;
- quiescence from a per-`PtyBridge` last-read `Instant`, bumped in `run_reader`;
- AwaitingInput via best-effort `/proc` walk to the deepest descendant: state
`S` + a non-timer `wchan` + an stdio fd on the pane tty. (`/proc/pid/syscall`
is ptrace-gated and unreadable for own sessions, so `wchan` distinguishes a
blocking read from a plain `sleep`.)
- combine: recent output → Active; else input-blocked → AwaitingInput; else
Idle. Emit `SessionUpdated` only on (activity, foreground) change — no diff
storms at steady state.
`maestro list` gains an ACTIVITY column and shows the live foreground. Pure
combine/parse logic is unit-tested; an e2e asserts `cat` → AwaitingInput and
`sleep` → Idle with correct foreground against real tmux.
Closes#18Closes#8
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Web: rename the per-row clone affordance to "Spawn similar" — it reopens the
revamped spawn form pre-seeded from the session's host, dir, command (matched
back to a preset by the form), and label, but with a freshly generated name
since it's a new session. Built entirely on the existing prefill machinery; the
custom event is renamed `catalog-spawn-similar`.
e2e (maestro-e2e): against real tmux + git, spawn into a git repo dir and assert
(1) the daemon's recent-dirs endpoint surfaces that dir with its command — what
#27's recents picker reads on the next form open — proving the durable
spawn-history write, and (2) gitroot infers the enclosing repo as the worktree
base_repo.
Closes#28
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a web app manifest, icons, and a runtime-caching service worker so the
UI can be installed on Android Chrome (and added to the iOS home screen).
- web/public/manifest.webmanifest: standalone display, theme/bg colors, icon set
- web/public/icon.svg + generated icon-192/512 and apple-touch-icon PNGs
- web/public/sw.js: caches the static shell for offline launch; never touches
/api (HTTP or WS), so live-daemon behavior is unchanged
- index.html: link manifest + apple-touch-icon, iOS standalone metas
- main.ts: register the service worker (production only)
Assets land in web/dist via Vite's public/ dir and are embedded by the
existing include_dir! in maestro-web, so no Rust changes are needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The worktree branch now defaults to the session name — including a clone's
prefilled name — and tracks it on reroll/name edit until the user types their
own branch. Moved the worktree section below the Name/Label fields so the
branch-derives-from-name relationship reads top-down.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework web/src/spawn.ts from raw text boxes into a low-friction, tile-first
flow: pick host tile → pick command tile → pick recent dir → Spawn, name
already filled.
- Command tiles from the daemon's advertised presets (GET /api/web/info, #26),
with built-in fallback; `custom` reveals the free-text command input.
- Host tiles replace the <select> (label hostname, sub daemon_id); most-
recently-spawned-on host sorts first + pre-selected; overflow behind "More…".
- Recent-dirs picker per host (GET /api/daemons/:id/recent-dirs, #26), rows show
dir + last command, re-queried on host change; picking a dir can pre-select
its last command. Persistent "Other directory…" drops to the raw input +
NAP-329 autocomplete; empty history prefills raw with home_dir.
- Worktree by branch alone: toggle infers base_repo via /gitroot from the
chosen dir (#26); non-repo dir disables the toggle with an inline hint;
WorktreeSpec.base_repo filled client-side (wire unchanged).
- Name field prefilled with a generated cyberpunk handle (names.ts mirrors the
maestro-names wordlists); ↻ rerolls.
Adds api.ts getWebInfo/recentDirs/gitRoot, the WebInfo/SpawnPreset/RecentDirs/
GitRoot types, and stubs the new endpoints in the `?mock&spawn` review fixture.
Closes#27
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Focus alone was the wrong signal: dismissing the soft keyboard (e.g. Android
back button) keeps the terminal focused, so the key bar lingered. Detect the
keyboard's actual visibility from the visual-viewport shrink — while an editable
element is focused, a height past a threshold below the focus-free baseline
means the keyboard is up — and gate the bar on a new `html.kb-open` class in
addition to the per-terminal focus class.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The special-key bar previously showed on every coarse-pointer device, even
when the terminal was unfocused and the soft keyboard was down — which looked
out of place. Gate it on the terminal textarea's focus/blur (the same signal
that drives the keyboard) via a `keybar-active` class, and re-fit on toggle
since showing/hiding the bar changes the terminal's height.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Android the soft keyboard overlaid the layout viewport and the browser
auto-scrolled the document on focus, dragging the shell (and its special-key
bar) to the top of the screen and forcing a manual scroll.
- index.html: interactive-widget=resizes-content so Chrome resizes the layout
viewport instead of overlaying the keyboard, killing the auto-scroll.
- viewport.ts: mirror visualViewport.offsetTop into --app-top for iOS / older
browsers that ignore interactive-widget and still scroll under the keyboard.
- styles.css: translateY(--app-top) re-pins #app to the visible region.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review feedback on the touch key bar:
- Pare to essentials: Esc, Tab, Ctrl, Alt + cursor keys (drop PgUp/PgDn
and the symbol keys).
- Two compact Termux-style rows in a 5-col grid; cursor keys as a
real-keyboard inverted-T with Home/End filling the top corners:
Esc Tab Home ↑ End
Ctrl Alt ← ↓ →
- Add Alt as a second sticky one-shot modifier; Ctrl+Alt compose, and
both promote cursor keys to the modified CSI form (\x1b[1;<n><final>).
- Explicit press-flash animation: the pointerdown preventDefault (kept so
the terminal stays focused) suppresses native :active feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On touch devices the soft keyboard omits the keys terminals depend on
(Esc, Tab, Ctrl, arrows, Home/End, PgUp/PgDn). Add a Termux-style
scrollable key bar to the attach view that injects the right xterm
sequences into the existing PTY input path.
- Ctrl is a one-shot sticky modifier: arm it and the next character —
tapped here or typed on the soft keyboard — is sent as its control
code, so Ctrl+C and friends work on mobile.
- Taps preventDefault on pointerdown so the terminal keeps focus and the
keyboard stays open.
- Bar is revealed only on coarse-pointer devices; a physical keyboard
already has these keys.
- Shell height now tracks the visual viewport (installViewportSizing) so
the bar lands above the soft keyboard instead of behind it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live-review iteration on the browser SPA, driven by crit feedback.
Sidebar / session tree
- Compact single-line leaf rows; status shown as a faint "alive" row wash
instead of per-row dots (exited rows dim + keep the "exited" badge).
- Host is now the first level of nesting (host → label path → session); the
inline @daemon is dropped from rows.
- Flush, borderless search input spanning the sidebar; drag-to-resize grip on
the sidebar edge (width persisted to localStorage); narrower default.
- Per-row clone action (opens the spawn modal prefilled); right-pointing
chevron when collapsed.
Chrome / layout
- Buttons swapped to a bracket terminal style ([ + Spawn ]); ghost/icon drop
the brackets. Old filled styles removed.
- Squared corners globally (radius tokens) and tighter, denser spacing.
- Session tally + daemon chips moved into the centered app header.
- Shorter control height; shared --bar-h aligns the search and attach bars.
Attach / terminal
- Useful attach title (name · command @ host) over a bare UUID.
- Robust fit: compute columns from the viewport's scrollbar-excluded
clientWidth (fixes mobile under-fit and desktop overflow); ResizeObserver +
font-load refit; full-bleed terminal on mobile.
Spawn / empty states
- Spawn modal accepts a prefill (clone); real "+ Spawn" buttons in welcome and
no-sessions empty states.
Dev
- New ?kit design-kit page (kit.ts/kit.css, DEV-only) showcasing components.
- Richer ?mock fixture with deeper label nesting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the full-screen route-swap SPA with a persistent two-pane layout:
- Global top bar (brand + Spawn + theme) above a sidebar (search + status +
session tree, highlights the active session) and a content pane (welcome or
the attached terminal). Selecting a row swaps only the content pane; the
sidebar and its store subscription persist.
- New `connections.ts` ConnectionPool: attach connections (terminal + WS) are
decoupled from the UI — navigating away parks a session (keeps it streaming)
rather than tearing it down, so switching back is instant with scrollback
intact. LRU-bounded (MAX_LIVE=6); explicit `⏻` disconnect distinct from nav.
- `attach.ts` → mountable `createAttachConnection`; `catalog.ts` exposes
`renderTopbar` + `renderSidebar`; `main.ts` is the shell + hash router.
- Responsive: desktop side-by-side; mobile single-pane (list ↔ terminal).
Single session viewed at a time (split panes deferred).
Typecheck + production build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite styles.css as an OKLCH design-system: cool-slate neutrals with a
single committed signal-green for "alive" (running status, primary action,
focus, selection). Monospace becomes the UI typeface; dark is default with a
full light theme via tokens, OS-following with a persisted header toggle.
- catalog: pulsing wordmark, status-bar tally (N sessions / M live) + daemon
chips, color-independent session status (pulsing dot for running, hollow
ring + "exited" badge for dead), reveal-on-hover kill, three teaching
empty states.
- attach: state pill (live/closed/error), framed dark xterm w/ green cursor.
- spawn: bottom-sheet/dialog form, mono inputs with green focus rings.
- a11y: AA contrast both themes, visible focus, prefers-reduced-motion paths,
status never color-only.
- theme.ts: dark/light toggle + persistence; mock.ts: DEV-only review fixture
(?mock / ?mock&spawn / ?theme=), tree-shaken from production builds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two web spawn QoL improvements plus the wire support they need:
- Command field now prefills `claude` instead of an empty box.
- Working directory prefills the selected daemon's home dir, updating on
daemon change unless the user has typed their own value.
The daemon resolves its `$HOME` via `dirs::home_dir()` at startup and
advertises it end-to-end: a new `home_dir` field on `DaemonOutbound::Hello`
flows through the registry's `register` into `DaemonInfo` (served on
`/api/daemons` and `/api/events`). The field is `#[serde(default)]`, so older
daemons that omit it deserialize to an empty string — backward compatible, and
the UI simply skips prefill when it's empty.
Hierarchical `/`-separated labels in the web sidebar already worked
(web/src/catalog.ts); no change there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build.rs now builds web/dist before include_dir! evaluates, so the
checked-in .gitkeep placeholder is no longer needed for a fresh checkout
to compile (verified by building with web/dist entirely absent). Ignore
dist/ wholesale and update the static_bundle doc comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CatalogStore is the TUI's view of the daemons-and-sessions tree, backed by
`WS /api/events` on the local daemon. The daemon's WS opens with a Snapshot
frame, so that's bootstrap; subsequent frames are deltas. No separate REST
round-trip needed.
Surface:
- `apply_event` — pure, used by tests directly and by the WS loop.
- `snapshot()` — cheap O(d+s) read returning sorted Vec views + monotonic
version for cheap render staleness checks.
- `wait_for_initial_snapshot` / `changed` — render-loop wakeups.
- `run_events_loop(ws_url)` — long-running task with exponential backoff
(1s → 30s, ±25% jitter). Reconnect re-sends a fresh Snapshot, which
reconciles state without server-side replay.
8 unit tests + 1 axum WS round-trip integration test passing.
New module `local_events::LocalEventBus` (broadcast<RegistryEvent>) with
two publishers:
* `spawn_session_publisher` — translates SessionManager's own session
events into the matching `RegistryEvent` variants (with daemon_id
filled in for the SessionRemoved case).
* `CatalogMirror::apply` — re-publishes peer events after applying them
to the local mirror, so subscribers see the post-apply state.
New `WS /api/events` route on the daemon: subscribes to the bus, sends
an initial `Snapshot` built from manager.snapshot() ∪ catalog state,
then forwards live deltas. `Lagged` follows the registry's convention
(client resyncs via REST).
Revert the REST-seed hack in web/src/events.ts — the WS endpoint now
exists, so `CatalogStore::start()` can go back to just `connect()`.
E2E: subscribe to a daemon's events WS, expect Snapshot then a
SessionAdded after POSTing /api/sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CatalogStore.start() previously connected the events WS only — fine when
loaded from the registry, broken when loaded from maestro-web (the daemon
has no /api/events endpoint yet, so the WS handshake 404s and the catalog
stays empty). Issue an initial resync() before connect(); the WS, when it
succeeds, takes over with live deltas. Daemon-side WS is filed as NAP-248.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Browser now opens ws://{host}/api/attach/:id directly — maestro-web
reverse-proxies it to the home daemon's broker, which short-circuits to
the local PTY (NAP-243) or, once NAP-247 lands, brokers to a peer over T2.
Drops the old getConnectInfo + daemon_url path: the browser no longer
knows or cares which daemon owns a session. ConnectInfo type and the
helper are gone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`web/vite.config.ts` was hard-coded to proxy `/api` at
`http://127.0.0.1:8080`, so pointing the dev UI at a remote registry meant
hand-editing the file. Read `MAESTRO_REGISTRY` from the environment via
`loadEnv` instead, with the local default preserved. Accept either the HTTP
base URL (`https://…`) or the daemon-facing URL (`wss://…/daemon`) — the
latter is rewritten to `https://…` and the `/daemon` suffix stripped, since
the proxy needs the HTTP origin. Flips `changeOrigin: true` for HTTPS
upstreams so the proxied Host matches the cert.
Documented under `web/README.md` § Dev.
The Dockerfile's `web-builder` stage (NAP-218) uses Node 20 + corepack.
Without a `packageManager` field, corepack defaulted to pnpm@latest,
which is now 11.3.0 — that release requires Node ≥22.13 and imports
`node:sqlite`, so it dies on Node 20 with
`ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite`. Dokploy builds were failing
in the `pnpm install` step.
Pin pnpm@9.15.4 (latest 9.x, fully Node-20 compatible). No code change
to the web bundle itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NAP-218. Browser UI for the prototype: catalog tree derived from
`metadata.label`, live updates over `WS /api/events`, search across
session name+label, spawn modal (with optional git worktree), and an
attach view that opens the daemon's per-session WS for xterm.js.
Framework choice: vanilla TypeScript. The interactivity surface is
small enough that pulling in a framework wouldn't pay back; xterm.js
does the heavy lifting. Easy to swap later without protocol impact.
Hash-based routing (`#/`, `#/attach/<id>`) keeps the registry's
static surface dumb — no SPA fallback needed; concurrent attaches
work by opening the same hash URL in another tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>