Dogfood-driven layout fixes:
- `j`/`k` now walk the same daemon-then-sessions order the sidebar emits
(`visible_sidebar_order`), so the highlight follows what the user sees
instead of jumping by Uuid order across daemons.
- Tab bar collapses from 3 rows of box border to 1 row of plain text +
1 row of horizontal rule. The rule doubles as the pane's focus indicator
(cyan top border when `focus = Pane`, default otherwise). Saves ~4 rows
above the pane before any content shows.
- Sidebar drops the full box border in favour of a single right-edge
vertical rule (also cyan when focused). The "daemons" header now lives
inline as a bold first line instead of in the block title.
Visible content area grows by ~2 cols + ~4 rows on the typical layout
without losing focus / panel-boundary affordances.
Workspace clippy + 85 unit tests passing.
Three dogfood-driven UX fixes:
1. `AppState::attach_to_focused` now sets `mode = Mode::Insert` along with
`focus = Focus::Pane`, so pressing Enter on a sidebar row drops the user
straight into typing the remote shell. The leader (default `ctrl-a`)
returns to Normal — same surface as tmux's prefix key.
2. `Keymap::entries(mode)` exposes the live binding table; `App::dispatch
(Action::ShowHelp)` (and `App::new`) rebuild `HelpOverlay::lines` from
it. Users see the actual Normal + Insert bindings + the leader hint,
not just "? to close".
3. Visible blinking cursor in both surfaces that accept text:
- `Pane::render` calls `Frame::set_cursor_position` at the vt100 cursor
position when the pane is focused (and the screen hasn't hidden the
cursor itself). Terminals do the blink.
- `SpawnModal::render` places the cursor at the end of whichever text
field is currently focused (cwd / command / label). Daemon dropdown
and the Submit button still rely on the REVERSED style.
Workspace clippy + 85 unit tests passing.
First-use UX fix: the TUI rendered the daemon list but felt frozen because
mode and focus were invisible, and the sidebar had no nav keys, so
`Enter`/`AttachHighlighted` had nothing to target.
- `render::draw` now reserves the bottom row for a status bar that always
shows `[NORMAL]`/`[INSERT]` (cyan/yellow chip), the focused panel
(sidebar / pane), and the most useful key hints (`j/k: nav • enter:
attach • s: spawn • ?: help • ^Wq: quit`). Status bar is never
eclipsed by overlays.
- `Sidebar::render_with_focus` and the pane block now draw their border
in cyan when focused, in the default colour otherwise.
- New `Action::SidebarNext` / `SidebarPrev`, bound to `j`/`k` and
`down`/`up` in the shipped Normal-mode defaults. Dispatch walks the
catalog's session list, calling `AppState::move_sidebar_highlight`
(wraps both ends).
Workspace clippy + 85 unit tests + 3 integration tests green.
- `RunConfig` grows a `config_path: Option<PathBuf>` field, plumbed from the
umbrella binary's global `--config` flag through to `MaestroConfigFile`
loading. Explicit `--config <path>` wins over `$MAESTRO_CONFIG` over the
platform default; a missing file still falls back to shipped defaults.
- `docs/INSTALL.md` gains a "terminal UI" section: that `maestro tui` ships
in the default feature set, how to opt out (`--no-default-features
--features cli,daemon`), the `MAESTRO_DAEMON_URL` override, and a worked
example of `[tui]` / `[tui.keymap.normal]` config pointing at the
full action vocabulary in the design spec.
- Adds a thin-build line for `--features cli` (CLI-only client) and lists
`tui` in the default-feature set.
- CLAUDE.md repo-layout block lists the new `maestro-tui` crate.
Closes the NAP-275 v1 epic.
Stitches the pieces from NAP-276..280 into a working app:
- `app.rs` — `App` owns config, catalog, emulators, state, modal, chord
resolver, region map, last-sent resize cache, and the reqwest client.
`run()` races crossterm events, catalog change notifies, and a 30 Hz
heartbeat. `redraw()` builds `RenderInputs`, calls `render::draw`,
refreshes `RegionMap`, then runs per-session min-size resize propagation
to the daemon.
- `dispatch(Action)` — Action → state mutation. `AttachHighlighted` spawns
a real `SessionTask` if none exists for the session. `OpenSpawnModal`
rebuilds the modal against the current daemon list. `KillHighlighted`
opens a confirm overlay.
- `handle_overlay_input` — intercepts keystrokes while spawn modal / kill
confirm / help is open. Spawn `Submit` fires `POST /api/sessions`.
Kill `y` fires `DELETE /api/sessions/:id` and drops the local handle.
- Insert-mode passthrough: characters (with ctrl/alt mapping), arrows
(CSI sequences), Enter/Tab/Backspace/Esc become binary bytes on the
active session's WS sink.
- `lib::run` — installs the panic hook, runs a 3×500ms `/api/web/info`
probe, loads `[tui]` from `MaestroConfigFile`, spawns
`CatalogStore::run_events_loop`, waits up to 2s for the bootstrap
Snapshot, enters the terminal guard, and drives `App::run`.
90 tests passing (5 new app dispatch unit tests + the existing widget +
catalog + session_task + emulators + pure-logic suites).
Adds the full visual layer:
- `state.rs` — `AppState` (tabs/focus/mode/sidebar visibility/overlay).
v1 keeps each tab to one optional session; the split tree is v1.5.
- `terminal.rs` — `TerminalGuard` (raw mode + alt screen + mouse capture
with RAII restore) and an idempotent panic hook that restores the
terminal before re-panicking.
- `sidebar.rs` — daemon → session tree with ●/○/✗ glyphs. Pulls a label
out of `metadata.label` (the protocol's shape), falls back to the
command.
- `tabs.rs` — top-row TabBar with active highlight.
- `pane.rs` — `vt100::Screen` → ratatui `Buffer` cell-by-cell, with full
fg/bg/bold/italic/underline/reverse translation. `PaneStatus` round-trips
from `SessionState`.
- `overlays.rs` — SpawnModal (5-field form with rotation), KillConfirm,
HelpOverlay, Toast. All centered popups with `Clear` underlay.
- `render.rs` — `draw(f, RenderInputs)` composes everything; returns a
`RegionMap` whose `region_at(col, row)` resolves a mouse coordinate to
a tagged Region (modal eclipses the rest).
22 new unit tests bringing the crate total to 80 passing. Tests use
content-based assertions over `ratatui::backend::TestBackend` output rather
than file-based goldens — less brittle to whitespace shifts while still
covering the visible content.
Per-attached-session task:
- Owns one WS to `WS /api/attach/:id` and a `vt100::Parser`.
- Binary frames → `parser.process(bytes)`. Text frames → `ClientControl::Ping`
is replied to as Pong; other text frames are ignored in v1.
- `SessionInput::Bytes(b)` becomes a binary frame; `Resize { cols, rows }`
resizes the parser AND sends `ClientControl::Resize` on the wire.
- WS drop → `Reconnecting` with exponential backoff (1s → 30s, ±25% jitter).
Parser screen is **not** reset across reconnects, so the user keeps seeing
the last-known frame instead of a blank.
- `SessionHandle::end()` transitions the task to `Ended` and stops it.
SessionEmulators:
- `(SessionId → SessionHandle)` registry.
- Owns a `ResizeTracker` whose `set_pane_size(sid, pane_id, (cols, rows))`
and `session_size(sid) -> Option<(cols, rows)>` compute the per-dimension
minimum across all panes bound to the same session — the value the
dispatcher feeds to the daemon.
Tests:
- 7 unit tests for `ResizeTracker` covering per-dim min, cross-session
isolation, and pane/session drop semantics.
- 2 unit tests for `SessionState` transitions.
- 1 axum-WS round-trip integration test asserting `"hello"` lands in the
vt100 screen within 500ms.
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.
`TuiConfig::load(&TuiSection)` builds the runtime config:
1. Apply shipped default Normal-mode bindings (cover every v1 action).
2. Insert mode binds the leader → `LeaveInsert` (built from the leader spec
so rebinding leader moves the Insert escape automatically).
3. Apply user overrides from `[tui.keymap.normal]` / `[tui.keymap.insert]`
over the defaults, with the same validation path — bad TOML entries
surface as `BadKeymap { mode, input, action, source }`.
Also adjusts the existing `MaestroConfigFile` round-trip test to include the
new `tui` field.
8 new unit tests passing (49 total for tui + config).
Two-level `(mode → input → action)` lookup. `set` validates each whitespace-
separated chord token via `InputKey::parse` and the action name via
`Action::parse`, so a bad TOML entry surfaces a precise diagnostic at TUI
startup instead of silently dropping a binding.
`known_bindings(mode)` feeds `ChordState` so a key only holds when it strictly
prefixes a real binding.
8 unit tests passing.
- `InputKey` — hashable variant covering chars-with-modifiers, specials, and
mouse events (kind + tagged region). `canonical()`/`parse()` round-trip the
TOML form (`ctrl-w h`, `mouse:click(pane)`).
- `canonicalise()` — `crossterm::Event` → `InputKey`, with a region-resolver
callback supplied by the renderer (NAP-280 will wire `RegionMap` to it).
- `ChordState` — small state machine that holds a key when it strictly
prefixes a known binding, resolves otherwise. Drops stale held keys past
`timeout`.
11 unit tests passing.
Closed list of TUI verbs. The string form (`EnterInsert`, `FocusPane(Left)`,
`GotoTab(3)`) is what users write in `[tui.keymap.normal]`; the enum is what
the event loop matches on.
Parser surfaces `Unknown`, `BadArg`, and `Malformed` diagnostics so a bad
keymap entry is rejected at load time with a specific reason. Display round-
trips so the canonical form is stable.
9 unit tests passing.
Schema-only — adds `TuiSection` with `leader`, `chord_timeout_ms`, and per-mode
keymap override maps; pulls crossterm into the workspace deps so the next
commit's input layer can consume it.
Field set matches `docs/superpowers/specs/2026-05-28-maestro-tui-design.md` §3.9.
New `crates/maestro-tui/` library behind a `tui` cargo feature on the umbrella
binary (default-on). Ships a clap-derive `TuiArgs` and a `run()` stub that
returns `NotImplemented` — exercises the full dispatch path so phases 2+ can
fill in the catalog, session liveness, widgets, and event loop without
touching scaffolding.
Acceptance:
- `cargo build -p maestro --features tui` succeeds
- `maestro tui` prints "maestro: not implemented" and exits 1
- `cargo test -p maestro-tui` passes (2 clap-parse tests)
- `cargo fmt --check` + `cargo clippy --all-targets --features tui -- -D warnings` clean
Phase 1 of the Maestro TUI epic (NAP-275); plan in
`docs/superpowers/plans/2026-05-28-maestro-tui.md`.