A host reboot kills the tmux server, so `SessionManager::recover` iterated an
empty list and every session vanished from the API while its sidecar sat
untouched on disk. A second recovery pass now reads the sidecars tmux had
nothing to adopt and registers each as `SessionStatus::Dormant` — a record of
what was running, with no pane and no PTY bridge behind it.
`SessionStatus` deserializes leniently now, like `ActivityState`: an unknown
spelling reads as `Dead` rather than failing the whole `Session`.
Refs #162
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The TUI has always raised `KillConfirm` before `x` kills anything; the web
sent `DELETE /api/sessions/:id` on the first press and only prompted when the
daemon refused. The command bar puts both behind the same three keystrokes,
so the difference now reads as a bug (#160).
The web follows the TUI: a kill is destructive and unattended agents live in
these sessions. `killDialog.ts` grows a confirmation that shares the force
prompt's shell, names the blockers the card already knows about, and keeps the
focus on Cancel. The guarded refusal still opens its own dialog behind it.
There is no ctrl-k on a phone and the web UI is mobile-first, so the field
the operator already reaches for becomes the touch entry point: focusing or
tapping it dispatches OPEN_COMMAND_BAR_EVENT and the shell opens the bar with
an empty query. The field is readonly and blurs itself first, so no keyboard
rises over it and the bar can be closed without reopening it.
This replaces the inline substring filter — one search affordance in the
header, not two. buildTree loses its filter argument, and the tree's "No
matches" empty state goes with it; the bar's own no-results state took over.
The filter's name-and-directory matching is now covered against the bar.
The bar's phone-width sheet moves to the spawn modal's 599px break, with
thumb-sized rows and a field that clears the zoom-on-focus threshold.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tab on a highlighted session in the web command bar opens that session's
action page in the same overlay: kill, rename, spawn similar, still fuzzy
searchable and headed by the session's name. Escape, or backspace at an
empty query, returns to the result list with the previous query intact.
Each action raises the flow the card already has. The two that lived
inside the sidebar get an event apiece — the confirm-and-blockers kill and
the inline rename prompt — so the bar picks the action and nothing more;
spawn similar reuses the existing SPAWN_SIMILAR_EVENT.
One entry per command — stable id, plain-language label, key chord, run()
— replacing the ad-hoc keydown listeners in main.ts, catalog.ts, debug.ts
and attach.ts and the parallel SHORTCUTS constant in help.ts. The `?`
overlay now renders the registry, so a binding and its documentation
cannot disagree.
Keys are canonicalised the way the TUI's `InputKey::canonical` spells
them, and chords resolve like its `ChordState`: hold while the sequence
is a strict prefix, otherwise dispatch. That gives the web a leader,
`ctrl-a` by default like `[tui].leader`, held as a partial chord and
`preventDefault`ed so it stops being select-all — the escape hatch in
docs/COMMAND-BAR.md needs somewhere to land (#149). The leader stays out
of real form fields, where select-all is still what the operator wants.
Same keys, same effects: plain single-key bindings remain deaf inside
form fields and the terminal's hidden textarea, chorded ones do not.
Refs #148
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pane waited 150 ms on hover and 60 ms on a keyboard step before
following the sidebar highlight, so every preview paid for the fast walk
it might have been. Replace both with a leading-edge 120 ms gap: the
first move shows at once, a burst is coalesced to its start and its end.
The row's own hover no longer eases either — `background` was on a 120 ms
transition, which is what read as lag on the row itself.
The sidebar also rebuilt its whole tree on every `SessionUpdated`, a
couple of times a minute per live session, dropping `:hover` from the row
under the pointer. Refreshes that change nothing structural now patch the
activity rail and meta line into the standing rows instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A preview was distinguished only by the chip and a dashed ring, both easy to
miss, and the sidebar's hover reused the neutral surface — so a hovered row
said nothing about what the content pane was showing.
Adds a `--preview` token family on a hue of its own, deliberately not the
accent: the accent means "this is the one you picked", which a preview is not.
The previewed pane's titlebar, the chip, the throwaway tab, the previewed
sidebar row and hover all carry it. Palettes whose accent is already violet
(periwinkle, orchid) move the hue via `--preview-h`.
Moving the sidebar highlight (keyboard or hover) now shows that session in
the content pane without opening it: its own tab comes forward, or a
throwaway tab carries it. The layout, the saved layout and the daemon's
focus report are all left untouched, and the keyboard stays in the list.
Contract in docs/PREVIEW.md.
Spawn was `s`/`S` in the TUI and `n`/`N` in the web client; the TUI now
uses `n`/`N` too. The web sidebar gains `c`, `r` and `x` — collapse,
rename, kill — which the TUI already bound.
docs/SIDEBAR.md now carries the shared table, so a new binding has one
place to land on both clients.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The name field starts empty and the reroll is gone; the worktree branch keeps
a default by borrowing one handle from /api/names. A card whose name the
daemon generated renders dimmed until a first prompt settles it.
Refs #131
Tab now moves the keyboard from the session list to the pane (was
Escape); Escape moves it back from the pane to the list, intercepted via
xterm's attachCustomKeyEventHandler so it never reaches the shell/app.
Matches the TUI's `tab`/`esc` pair; Ctrl+Q was ruled out since browsers
reserve it and never deliver it to page JS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sidebar rows only navigated via location.hash + hashchange. Moving
pane focus by split, drag, or direct pane click never touched the
hash, so a later click back onto a session whose row href matched the
stale hash fired no hashchange and was silently ignored — the pane
had to be clicked directly to regain focus.
Row clicks now also dispatch a session-select event handled the same
way as a hash deep-link, independent of whether the hash text changes.
Rail now spans full row height flush to the left edge with square
corners, instead of a depth-shifted inset bar. Active state stays
solid green since colour alone already signals it; blink/flash stay
for awaiting-input/error.
Meta line fragment order was command last, after the blocked-work diff
stat, so the tool icon sat right of the diff count instead of left.
Reorder to duration/detail/command/diff-stat in both web and TUI
sidebars per docs/SIDEBAR.md, and update the contract doc + TUI tests
to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the inline activity dot with a coloured bar positioned inside
each row's own indent gutter, per the updated docs/SIDEBAR.md contract.
Active breathes, awaiting-input blinks, error flashes; idle stays
solid. prefers-reduced-motion leaves every state static.
Graphite replaces the signal-green accent as the default web palette;
data-palette on <html> selects ice-cyan, periwinkle, amber-phosphor,
ember or orchid (each with dark + light token sets), picked from a new
sidebar-footer dropdown and persisted in localStorage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sidebar footer gains a status dot beside the spawn button: green while the
daemon answers, amber while reconnecting, red while it is gone. Hovering it
shows the release the daemon is running — commit hash and commit date.
When the events socket drops, `DaemonLink` starts polling `GET /api/info`. Past
a 1200 ms grace window an overlay blocks the UI (and swallows the app's
shortcuts) until the daemon answers again. If it comes back on a different
commit the page reloads, so a tab never drives new server code from the bundle
that was just replaced; a plain restart just unlocks.
docs/RESTART.md is the contract.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draft panes are gone: an empty pane is a placeholder again, and every
spawn affordance opens a dialog around the shared spawn form. Escape,
Cancel, the ✕ and a backdrop click dismiss it; a successful submit puts
the session in a new tab (Spawn similar and the fast-forward fallback
keep placing it beside the pane they acted on).
Removes the per-leaf draft state, its persistence strip, and the form
reuse/teardown machinery the panes needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The kill button now sends the unforced DELETE immediately instead of
asking first. Only the daemon's guard refusal raises a prompt, and it is
a real dialog rather than confirm(), so it can offer the override the
daemon already implements.
The API error envelope is decoded into ApiRequestError, keeping the code
and the unsaved-work breakdown so the dialog names what is in the way.
The command fragment named the full command line, so a session spawned
with `claude --dangerously-skip-permissions` spent the whole line on a
flag. Cards now name the program only — its icon in the web client, its
bare name in the TUI — and keep the full command line in the tooltip.
Adds `maestro_protocol::display::command_program`, the Rust counterpart
of the web's `splitCommand`.
The header carried only the brand and a session tally; app actions had
already moved to the sidebar footer. The mobile ☰ it hosted moves to the
pane header (replacing the redundant back arrow) and to the welcome
state, so the session drawer stays reachable without a swipe. The DEV
`?kit` link moves to the sidebar footer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Spawn and Spawn similar open a draft pane behind the open sidebar on
mobile, so the tap read as dead. Extract the drawer into its own module
and close it on both spawn events as well as a session tap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Help, theme toggle, and Spawn now live pinned to the bottom of the
sidebar instead of the top-right app header, keeping them next to the
session list they act on.
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>
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.
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.
Remove the per-folder count badge (and the now-unused countLeaves) from the
sidebar tree — the tree structure already conveys grouping.
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>