Ctrl+Shift+H moves the keyboard between the session list and the pane;
j/k walk the cards, Enter opens, Escape hands the pane back. The walk
skips expanded folder headers and stops on collapsed ones, matching the
TUI.
Line 2 of a session card spelled out its blocked work ("1 uncommitted
file") and its command ("claude"). Both now render as marks: the
breakdown as diff notation, and the command's leading program as its
tool's icon in the web client.
The probe carries the line delta of the tracked changes — staged and
unstaged together, against HEAD — so `git add` never makes the counter
drop. Untracked files stay a file count; counting their lines means
reading every one off disk. A tracked change with no line delta falls
back to `~N` rather than claiming `+0 -0`.
The TUI keeps its command as text: a terminal cannot be assumed to have
a font carrying tool glyphs.
The sidebar row dot, the pane top strip and the mobile pill dot all
painted a hashed per-session colour; the focused pane added an outer
accent glow on top of its ring. Removes the colour concept entirely
along with the glow, leaving the flat accent ring as the only focus mark.
`?` toggles a dialog listing every web binding, mirroring the TUI's help
overlay. A `?` button in the header covers pointer-only clients, and the
welcome screen points at the key.
One registry (SHORTCUTS) drives the rendering, so a binding added without
an entry reads as missing. main.ts and the overlay now share one
isTypingTarget guard instead of two copies.
Removes the retained-worktree store end to end: the daemon's state-dir
store, GET/DELETE /api/worktrees, `maestro worktrees list|reclaim`, the
TUI overlay behind `w`, and the web `#/worktrees` view and header link.
The kill guard stays. A forced kill and a dirty natural exit now leave
the worktree on disk untouched instead of parking it in a store, so
removing one is an ordinary `git worktree remove`. A pristine tree is
still removed on exit. DELETE /api/sessions/:id always answers 204.
The header's debug button also goes; Ctrl+Shift+D and `?hud` still
toggle the HUD.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Right-clicking a card opens a custom context menu (rename, spawn similar,
kill). Rename swaps the name for an inline field; Enter commits, Escape and
blur cancel. The tree stops rebuilding while the field is open, so a catalog
delta cannot replace what is being typed into.
Refs #125
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closed by default behind a disclosure reusing the sidebar folder-header
affordance (catalog.ts), since most spawns take the default target and
the section was the largest block in the form. The collapsed header
states whether a worktree will be created, so #91's off-by-default for
a worktree row stays visible without expanding.
Fixes a latent [hidden] + display:flex bug for the new .wt-collapsible
element (author CSS beats the UA [hidden] rule at equal specificity);
.wt-body has the same bug pre-existing and is out of scope here.
The working-directory picker was a flat list mixing repositories with
their worktrees, each row repeating the project name as an abbreviated
path. It is now two steps: a grid of project boxes carrying the full
unabbreviated path, then — only once a project is picked — that
repository's worktrees with "+ new" as the default.
"+ new" creates a worktree; picking an existing one runs in it and
leaves the create-worktree toggle off, since the daemon never adopts
(docs/WORKTREES.md). That is #91's rule, now an explicit choice rather
than an inference from which row was clicked.
Also fixes `.raw-wrap[hidden]`: `display: flex` outranked the UA sheet's
`[hidden]`, so the free-text cwd input showed even when hidden.
The interaction rules are listed in spawnForm.ts for the TUI mirror (#96)
to follow.
Refs #95
The form filled the full pane width, so on a 4K display every input
stretched thousands of pixels wide. Bound it to 60ch (in line with the
48ch/42ch precedents already in the type scale) and centre it with
margin auto; panes narrower than the cap still fill edge to edge.
The draft pane covers every field the modal had; the modal was a second
code path for the same job. The header Spawn button, sidebar empty state,
welcome button, and ?mock&spawn now open a draft pane via
MultiView.openDraft — reusing an empty focused pane, else splitting it.
"Spawn similar" seeds a draft with the session's cwd + command.
Modal-only CSS (backdrop, sheet/dialog chrome, keyframes) is deleted;
.modal-body form styling folds into .spawn-form, which also picks up the
select styling the worktree branch picker was borrowing from the modal.
- toggle defaults on for any target the project scan reports (repo or
worktree); only the free-text escape hatch pays for /api/gitroot
- branch mode control switches between creating a new branch (named
after the session name, as before) and picking an existing one from
GET /api/branches; branches held by another worktree stay visible but
disabled, labelled with that worktree's path
- emits create_branch: false on the wire for existing-mode picks
- branch-list load follows the form's ready/destroyed discipline so a
late response cannot corrupt a rebuilt draft
Six findings from reviewing the accumulated blitz/spawn-v2 diff:
- loadRecents restored the seeded cwd but never emitted it, so the leaf's
draft kept cwd:"" and the next render reseeded the form blank.
- stripDrafts did not recurse, so every half-filled draft in a split tab
was written to localStorage; the old test passed only because hydrate
discards draft on the way back in.
- showRawCwd and selectPreset grabbed focus from async init paths, so a
draft pane stole the caret seconds after mounting.
- .mv-draft inherited no layout once the form left .modal-body.
- onSuccess mounted the new session onto a leaf that may have been closed
while the POST was in flight, stranding it in no pane.
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 sidebar-drawer toggle now lives left of the logo in the global header
instead of floating over the content area / tab bar. Removes the in-canvas
fab and tab-bar menu button and their styles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xterm's native touch scrolling crawled a line at a time and stalled after a
few. Map finger drags on the viewport to term.scrollLines (skipped on the alt
screen, which the app owns) and claim vertical pans via touch-action: pan-y.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On phones (single tab, one pane at a time):
- drop the pane border/padding/focus-ring so the session fills the screen
- collapse the redundant tab bar; float a ☰ to keep the sidebar drawer reachable
- disable pull-to-refresh (overscroll-behavior) so terminal scroll wins
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Sidebar UI tweaks from a live crit review:
- Statusbar: drop the redundant global "N sessions / N live" tally; each
daemon chip now carries its own green per-host session-count badge
(tabular-nums, dimmed when 0).
- Session tree: deep folder paths collapse leading segments to initials and
keep the trailing dir name (~/git.naps.pt/naps62/maestro -> ~/g/n/maestro),
with the stem dimmed and the full path in the tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Below 860px the split tree projects to a one-pane-at-a-time pill strip (no
splits/drag/resize — desktop only); the tree is untouched so widening restores
the splits. Only the visible pane's session stays live; the rest park. The
sidebar becomes an off-canvas drawer toggled by a ☰ in the tab bar, with a
backdrop; tapping a session opens it and closes the drawer. Re-renders on the
breakpoint crossing without losing sessions.
Closes#35. Part of epic #37.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render the content pane as tabs of nested split trees, each session leaf a live
attach mounted through the multi-mount pool:
- #30 arbitrarily-nested H/V splits with mouse-resizable dividers; per-pane
⏻/▥/▤/✕ tools; closing a pane collapses its parent split.
- #31 tabs, each holding its own tree; add / switch / close.
- #32 drag-to-dock: drag a pane title or a sidebar session onto a pane for a
5-zone dock (edges split, centre swaps/replaces); a sidebar drag moves an
already-open session.
- #33 focused pane at full strength, others dim; sidebar shows focused (loud)
vs open-elsewhere (quiet) with a stable per-session colour mapping rows to
panes.
Opening a session fills the focused pane (single-pane parity by default);
multi-pane is opt-in via split / drag / tabs. Panes auto-close when their
session ends (guarded against eviction and intentional close). The steady
"connected" label is hidden as noise.
Closes#30, #31, #32, #33. Part of epic #37.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>