A Structured session's adapter hands back an ACP session id from
session/new; a respawn will pass it to session/load (#135). This is a
new field rather than #161's agent_session_id: that one is the Claude
Code conversation id the activity hook reports and `claude --resume`
takes, minted by Claude Code itself. The ACP id is minted by the
adapter, only exists for Structured sessions, and is what the adapter
takes back. Overloading one field with two ids from two processes
would leave a respawn unable to tell which kind it holds.
Adds `SessionView { Terminal, Structured }` to maestro-protocol, a
defaulted `view` field on `Session` and `CreateSessionRequest`, and
sidecar round-trip tests for both a legacy payload with no `view` key
and a `Structured` session. Nothing reads the field yet; every other
struct literal across the workspace picks up `Default::default()`
(Terminal) to keep compiling.
Gitea #133.
The TUI read its leader from `[tui].leader`; the web used `DEFAULT_LEADER`,
a module constant. Rebinding one left the other on `ctrl-a`, so the two
escape hatches hanging off the leader (`leader ctrl-k`, `leader leader`)
moved on one client and not the other.
`GET /api/info` now carries `leader`. The web binds that when it arrives and
rebuilds the leader-owning `CommandSet`, so the `?` overlay and the command
bar render the configured chord; an absent or empty field — an older daemon,
or an unreachable `/api/info` — leaves it on `ctrl-a`.
Chose the existing `[tui].leader` served as-is over a new shared key: one
knob, no precedence rules between two spellings, and no migration for
operators who already set it. The name is a wart — it is the leader for every
client now, not the TUI's — and `docs/COMMAND-BAR.md` and `docs/INSTALL.md`
say so.
ctrl-a is consumed as the leader on every press, so it never reaches the
shell and readline's beginning-of-line was gone with no way back — unlike
Ctrl-K, which already has an escape hatch. Fix it the way tmux does:
pressing the leader twice sends the literal leader byte to the focused
pane and clears the pending chord, on both web and TUI, built from the
configured leader so a rebound leader carries it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ctrl-k now opens the command bar in both modes (#153), taking readline's
kill-to-end-of-line away from every pane. Leader then ctrl-k — ctrl-a
ctrl-k by default — sends the literal 0x0B byte to the focused pane
instead, built from the configured leader so rebinding it rebinds the
chord too.
Chording a second binding off the leader exposed a gap in ChordState:
a key bound both alone and as a chord prefix (ctrl-a alone, and ctrl-a
ctrl-k) would get stuck waiting on the next keystroke, discarding its
own meaning the moment that keystroke didn't continue the chord. Feed
now resolves the held prefix to its own binding when that happens and
hands the breaking key back via take_replay for the caller to process
fresh — otherwise, pressing leader then any other key would never
leave Insert mode again.
`tab` on a highlighted session opens its action page inside the command
bar: kill, rename, spawn similar, still fuzzy-searchable, with the
overlay titled after the session. Escape or backspace at an empty query
returns to the result list with the previous query intact.
Each action raises the flow the TUI already has — `KillConfirm`, the
rename prompt pre-filled, and the spawn form, now seeded from that
session's directory and command rather than the focused one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`>` narrows to commands from the live keymap for the current mode, matched
on the action's help_label only — the chord is displayed, never matched, so
results cannot depend on a user's rebinds. Rows dedupe by action, not by
chord: the default map binds both `enter` and `tab` to AttachHighlighted,
and AttachHighlighted and ToggleAttachHighlighted share a label because they
are the same match arm.
`~` narrows to recent dirs plus the project list. Enter takes the
fast-forward path pinned to that directory, with no recency fallback, so a
directory outside the project scan opens the pre-filled form rather than
spawning elsewhere.
Both fall out of parsing the query line, as in web/src/commandBar.ts: a
sigil is only a sigil in first position, and backspacing past one is just
the query becoming mixed again.
Opens from Normal and Insert mode as an ordinary keymap entry, so it is
rebindable and the help overlay picks it up from the live keymap. An
empty query lists sessions most recently focused first, matching the web
bar's `orderSessionsForBar`; typing ranks through `matcher.rs` with the
matched ranges highlighted, grouped under kind headers.
Enter attaches through the same path the sidebar's attach uses, Escape
closes and leaves focus and mode where they were.
Ports the docs/COMMAND-BAR.md scoring algorithm (dynamic program over
query/text positions, word-boundary bonuses, whole-field adjustment,
four-level tie-break) to crates/maestro-tui, matching web/src/matcher.ts.
Fixtures F1-F12 from the doc are ported as tests, asserting order,
scores, field and ranges.
The web list still said "open in the focused pane" with a 1px ring, the last
edge left in it, and the TUI said it with the ● glyph alone. Both now use a
background.
Web: `--accent-fill`, the same 30% recipe `--preview-tint` uses, so the two
tiers differ by hue rather than by strength. The row rules are also rewritten
as one ordered run of equal-specificity `:where()` selectors, which is what
`docs/SIDEBAR.md` claims the ladder is — `.is-open` moves in from the bottom
of the multi-view block, where source order had been deciding its precedence
by accident.
TUI: `palette.pane_row_bg()`, blended partway from the card surface to the
selection surface, so the sidebar reads unfilled / half filled / filled. It
borrows the selection surface's foregrounds, since those are the ones picked
for a filled row, and the cursor still wins on its own row. Blended rather
than shipped as a third `Surface` — that would be eight more colours in each
of seventeen presets — with palette tests asserting it stays a visible step
from both surfaces and stays legible in every preset.
Add Session::last_focused_at, shipped on the wire like every other
field. Both POST /api/sessions/:id/focus (focused: true) and opening
/api/attach/:id stamp it and emit SessionUpdated, so a client that
only attaches sorts the same as one that reports focus explicitly.
Losing focus does not clear it, and a never-focused session has None,
which is what lets the command bar's empty-query ordering
(docs/COMMAND-BAR.md) sort it after every session that has focused.
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>
Insert mode bound `esc` to LeaveInsert, so full-screen apps in a session
(vim, Claude Code) never saw Escape — the only way out of Insert is now
the leader, which rebinding still follows.
A key bound to `Noop` in Insert mode is also forwarded to the pty now.
`Noop` documents itself as the way to suppress a default binding, and in
Insert mode an unbound key belongs to the session, so this is the escape
hatch for taking any other default back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar highlight now shows its session in the pane after a 120ms
debounce, attaching if nothing already is, without the tab taking it on
and without reporting focus to the daemon. Preview-only attaches stay
warm behind the current one, bounded at three.
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 TUI repainted on every 33ms tick whether or not anything had
changed — measured at ~725 bytes/s of escape sequences with nothing
attached and nothing happening. Over a remote terminal that is constant
traffic for no visible change.
Output was also unbuffered: `CrosstermBackend::new(std::io::stdout())`
issues one `write!` per cell run, 3551 of them for a 200x50 repaint,
each taking the global stdout lock. A BufWriter collapses a frame into
one flush (measured: 3551 writes -> 2).
Frames are now keyed on the catalog version, the active pane's screen
revision, an input epoch and the terminal size, and skipped when the key
is unchanged. Session cards carry an advancing duration, so a frame
still never stands for longer than a second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The TUI arm of the umbrella binary hardcoded http://127.0.0.1:7070 instead
of going through MaestroConfigFile::resolve_daemon_url, which every CLI
subcommand uses. On a host whose [daemon].bind_addr is not the default port,
`maestro list` worked and `maestro tui` could not connect.
RunConfig.daemon_url is now the override rather than the final URL; the TUI
resolves it against the config file it already loads for its UI settings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The name field starts empty behind a hint, ^R reroll is gone, and the
fetched handle seeds the worktree branch instead. Fast-forward spawns with
no name at all. A card whose name the daemon generated renders muted.
Refs #131
A session's rail glows AwaitingInput even after the operator has already
looked at it. Clients now report pane focus (POST /:id/focus); the daemon
clears the glow to Idle after 5s of continuous focus and keeps it cleared
until the session does new work, so a fresh Active cycle is required to
glow again.
Normal mode only exists while the sidebar is focused (Insert mode always
pairs with Focus::Pane), so bare `q` and `tab` there never reach a pty:
`q` quits, `tab` focuses the highlighted pane (same as Enter). `esc` is
added as a second, fixed way out of Insert alongside the leader.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
N picked the globally freshest project by git-mtime, ignoring the session
the operator was actually on. Plain n's spawn form already seeds from the
focused session (spawnSimilarProjectSeed); fast-forward now matches that,
falling back to recency only when nothing is focused.
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>
Leads the existing indent gutter on both the name row and the meta
row with a one-column block glyph in the state's palette role,
replacing the meta line's inline dot. Folder headers gain a matching
blank column so labels stay aligned under session names.
Replaces the army-green dark/light palettes with neutral graphite and
adds ice-cyan, periwinkle, amber-phosphor, ember and orchid presets.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The picker now leads the form in both clients and takes focus on open, so
Tab walks from it into the command tiles. Opening the form also preselects
the project of the session you were last on — a worktree session seeds its
base repository — so 'n' then Enter spawns another session there.
The status bar gains the TUI's half of the same contract the web client got:
a coloured dot plus the release the daemon is running, pinned right so a narrow
terminal truncates the key hints rather than the release.
`DaemonLink` mirrors web/src/daemonLink.ts — the events loop reports its
connects and disconnects, a probe task polls `GET /api/info` while the daemon
is unreachable, and past the grace window a modal covers the frame and locks
the keyboard (quit excepted) until it answers. A daemon that comes back on a
new commit cannot be reloaded into a running TUI binary, so the status bar says
`updated: <hash> (restart tui)` instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`s` (OpenSpawnModal, was NewDraftPane) puts the form up as a centered
overlay instead of taking over the pane. The attached session stays
attached behind it and comes back untouched on Escape; the modal owns
every key while it is up, so `x` and `j` typed into a field no longer
reach the keymap.
Tab::pane is a session id again and PaneRef is gone. The form lives on
App next to the rename buffer, so its loads no longer address a tab
index that could not identify them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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`.
Session handles are one word from a single 218-entry list — instruments,
forms, notation and theory, tempo and dynamics — replacing the
adjective-noun pair. A batch is shuffled, so it repeats only once the
wordlist runs out.
Project boxes show the parent directory instead of the full path: the
last segment is already the name line above it. The last command is gone
from the box, and picking a project no longer pre-selects it — the
directory and what runs in it are independent choices. Both changes land
in the TUI picker too, which mirrors the web form's rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Group rows are not something to focus; the cards under them are one row
away. A collapsed group still stops the walk — its header is the only row
it has, so skipping it would leave the group unreachable.
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.
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>
The worktree step ("+ new" plus the repository's existing worktrees) now
lives inside the collapsed "Advanced options" section in both the web form
and the TUI draft pane, so a plain spawn is command, project, name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`r` opens a prompt pre-filled with the session's own name — empty when it
has none, so Enter cannot pin the command as the name. Enter saves, Escape
cancels, and an emptied field clears the name.
Refs #125
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Solarized (dark/light), Catppuccin Mocha, GitHub (dark/light),
Tokyo Night, Gruvbox, Nord, Dracula. Colours auto-tuned from the
canonical seeds to clear the palette contrast floors; tests now
assert every RGB preset, not just dark/light.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>