The browser app had no tests and no way to run any: package.json shipped
dev/build/preview/typecheck, and CI installed Node and pnpm but never ran
anything with them. Its densest logic was module-private, so even with a
runner none of it was reachable.
Extracts four pure modules out of DOM-heavy ones — tree.ts (directory
grouping), keys.ts (terminal escape encoding), paths.ts (completion
arithmetic), html.ts (one escaper, replacing four copies). Extraction
rather than `export` because attach.ts imports xterm's CSS at module
level, which a node test cannot load.
CatalogStore takes an injectable socket factory, scheduler and refetch, so
the reducer and the backoff schedule run without a server. snapshot() now
returns a copy: it returned the live object while events mutated it in
place, so listeners could not diff and every event rebuilt the whole tree.
36 tests across 6 files, and a `web` CI job running typecheck, test and
build. vitest is pinned to 2.x — 4.x requires vite >= 6 and this is vite 5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Session tree nests by working directory only; the daemon chips in the
header become a session tally, and the spawn form loses its host picker.
API calls move from /api/daemons/:id/* to /api/dirs, /api/recent-dirs,
/api/gitroot, and /api/info.
The working-directory picker showed raw absolute paths (`/home/naps62/...`),
unlike the sidebar tree which collapses home to `~` and shrinks leading
segments to initials. Reuse the sidebar's abbreviation so both read the same:
`/home/naps62/git.naps.pt/naps62/maestro` → `~/g/n/maestro`, with the full path
kept in the row tooltip and submitted verbatim.
- types.ts: hoist `abbreviateFolderName` (+ a local `escapeHtml`) here so both
the sidebar and spawn dialog share one implementation (catalog.ts imports
spawn.ts, so the helper can't live in catalog without a cycle).
- catalog.ts: import the shared helper instead of its own copy.
- spawn.ts: abbreviate each recent-dir row via `abbreviateFolderName(abbreviateCwd(...))`.
- styles.css: dim the collapsed stem in the picker too (`.recent-dir .folder-stem`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the freeform `metadata.label` session grouping with nesting derived
from each session's working directory. The daemon's activity watcher already
polls each tmux pane every tick, so it now also captures
`#{pane_current_path}` and surfaces it as a new `Session.current_cwd`
(serde-default, back-compat); `cwd` stays the spawn dir for worktree
inference, spawn-similar, and recents.
Web catalog builds the tree from `current_cwd ?? cwd`, collapses the home
dir to `~`, and folds single-child folder chains so `~/git/maestro` reads as
one node. CLI swaps its LABEL column for an abbreviated DIR; the spawn forms
(web + TUI) and `--label` flag are removed.
The pane format stays space-delimited (parser splits the first three tokens
and keeps the remainder as the path) so a cwd containing spaces survives.
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>
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>
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>
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>