Commit Graph
673 Commits
Author SHA1 Message Date
Miguel PalhasandClaude Opus 5 6596104789 refactor(maestro): delete the config projections and the dead knobs
ci / web (pull_request) Successful in 28s
ci / rust (pull_request) Successful in 1m49s
ci / web (push) Successful in 39s
ci / rust (push) Successful in 2m48s
maestro-cli/src/config.rs was two pass-throughs, a newtype error, a
two-field projection of MaestroConfigFile, and one four-line or_else with
a single caller. `resolve_daemon_url` moves onto MaestroConfigFile, next
to the schema it reads, and the module goes; its tests move with it.

`[web].dev_proxy_target` is deleted end to end — schema, env var,
normalisation, three tests, README line, and the startup warning that
admitted the server ignores it.

MaestroConfigFile::save had no callers outside its own tests. Deleting it
also removes every skip_serializing_if, the five is_empty impls, and the
Serialize derives that existed only to make its output tidy.

BroadcastTuning was a struct that was never instantiated: all three uses
were `::DEFAULT.field` in const position and there is no [broadcast]
section. Now two plain consts.

$MAESTRO_CONFIG was honoured twice, by resolve_config_path and by clap's
`env =`. Every binary already gets it through clap, so resolve_config_path
and CONFIG_PATH_ENV go and the TUI falls back to default_config_path like
everything else.

Also collapses three layers of 1:1 forwarding: the umbrella's cli_dispatch
shim, and four near-identical 8-line functions in cli/dispatch.rs that
differed only in which run_* they called.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:39:01 +01:00
Miguel PalhasandClaude Opus 5 b20651c33e fix(maestro): close three holes in the wire contract
ci / web (pull_request) Successful in 27s
ci / rust (pull_request) Successful in 1m40s
ci / web (push) Successful in 35s
ci / rust (push) Successful in 2m23s
Each was a typed field that type-checked on both sides and carried
nothing across.

SessionStatus::Dead was never constructed by the daemon, yet the TUI, CLI
and SPA all render it and DESIGN.md specifies the exited badge. Sessions
were removed on death, never marked. They are now marked Dead and kept
until the operator dismisses them, with the PtyBridge sealed rather than
dropped: attached clients still get a Close frame (NAP-226), and the
replay buffer survives so a later attach can read what a crashed agent
printed. Previously that output was destroyed. Dead sessions are
read-only and do not survive a daemon restart.

CreateSessionRequest.env was accepted, stored, echoed back and dropped —
new_session_piped had no env parameter. It now reaches tmux as sorted
`-e KEY=VAL` flags. A key containing `=` or NUL is rejected, since tmux
would parse it as a different assignment than the caller wrote.

PROTOCOL_VERSION was written by the daemon and read by nobody. Adds
`maestro_protocol::is_compatible`, compared on the major component only;
`maestro list` shows the daemon version and the TUI warns on mismatch.

Also stops mapping every non-404 session error to 502: an invalid env key
is a client mistake and now answers 400.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:27:51 +01:00
Miguel PalhasandClaude Opus 5 031c80a40b test(maestro): give the SPA a test runner and a reachable pure core
ci / rust (pull_request) Successful in 2m28s
ci / web (pull_request) Successful in 23s
ci / web (push) Successful in 26s
ci / rust (push) Successful in 1m38s
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>
2026-08-22 14:08:14 +01:00
Miguel PalhasandClaude Opus 5 db44650a17 refactor(maestro): collapse the daemon's double event fan-out
ci / rust (pull_request) Successful in 3m18s
ci / rust (push) Successful in 2m5s
A background task, a second broadcast channel and a whole module existed
to remap SessionEvent onto CatalogEvent one variant at a time.
SessionManager now emits CatalogEvent directly.

Deletes local_events.rs, the publisher task, the LocalEventBus channel,
enum SessionEvent, LocalApiState::events, and the duplicate capacity
constant — events_capacity was declared twice for one logical stream.

Lag semantics improve. There were two lag points: the publisher falling
behind the manager, which broadcast one Lagged to every client, and each
WS subscriber falling behind the bus. Only the per-subscriber one is left,
which pump_events already handled — a slow client now makes only itself
resync.

Adds the first daemon-level tests for WS /api/events: the Snapshot-then-
deltas contract, and that a late subscriber gets a Snapshot of current
state. Both were described only in a comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 13:37:15 +01:00
Miguel PalhasandClaude Opus 5 f01771568b refactor(maestro): make the TUI dispatch a reducer over effects
ci / rust (pull_request) Successful in 1m57s
ci / rust (push) Successful in 1m54s
dispatch was `async fn -> ()` with reqwest calls and task spawns inlined,
so it could not report intent and the spawn, kill and attach arms had no
test. It now returns Vec<Effect> and App::run performs them.

Two bugs the old shape produced, both fixed and covered:

- The toast overlay could not be dismissed. handle_overlay_input returned
  Pass for Toast(_) and nothing cleared it, so "spawn failed: ..." stayed
  until another overlay replaced it. It now clears and still returns Pass,
  so the keystroke does its normal job instead of being eaten.
- Closing a tab leaked its emulator. Only submit_kill released one, so a
  closed tab's WebSocket task and vt100 parser outlived it forever.
  close_tab now reports the session it dropped — and holds onto it when
  another tab still shows the same one.

AppState::overlay is private behind show_overlay/dismiss_overlay/toast.
Four sites poked the field directly; that is why the toast bug existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 13:08:08 +01:00
Miguel PalhasandClaude Opus 5 63898cef25 feat(maestro): put a Panes seam behind Tmux
ci / rust (pull_request) Successful in 1m45s
ci / rust (push) Successful in 1m38s
SessionManager held a concrete Tmux, so its decisions — spawn rollback
ordering, liveness diffing, activity change-detection — could only be
reached by forking a real tmux server. Inner now holds an Arc<dyn Panes>;
Tmux is the production adapter, FakePanes the one tests drive.

Arc<dyn Panes> rather than a type parameter: a generic would spread
through LocalApiState, attach and cli, and one vtable hop is free next to
a fork+exec.

Adds tests/session_decisions.rs — six cases, no subprocess, 0.24s.
Replaces the chmod-0o500 rollback test, which detected root and returned
early, so on CI it asserted nothing; the failure is now forced by a
regular file sitting where the sidecar directory's parent must be, which
no uid can work around. It also asserts the rollback *order*, not just
the end state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:44:13 +01:00
Miguel PalhasandClaude Opus 5 fc53d74850 fix(maestro): recognise tmux 3.7's "server exited unexpectedly"
Killing the last session shuts the server down, and tmux >= 3.7 reports
that to the still-connected client with a phrasing `is_no_server_error`
did not match. `list_sessions` then returned Err, and `run_liveness_pass`
logs a warning and returns — so the pass reaped nothing.

Silent by construction: nothing asserted the phrasing set, and the only
symptom was an integration test that looked flaky. Both now covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:37:54 +01:00
Miguel PalhasandClaude Opus 5 ef3d314296 refactor(maestro): finish moving the e2e harness off raw reqwest
ci / rust (pull_request) Successful in 3m11s
ci / rust (push) Successful in 1m27s
Restores the harness's deliberate 5s per-request budget, which the shared
client's 30s default had silently replaced, and converts the last two
hand-rolled daemon calls in e2e.rs (recent-dirs, gitroot). Drops reqwest
from maestro-e2e entirely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:22:35 +01:00
Miguel PalhasandClaude Opus 5 148f97fa71 chore(maestro): drop the unused tokio dep from the client feature
ci / rust (pull_request) Successful in 5m29s
reqwest brings its own runtime requirement; maestro-protocol never names
tokio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:17:46 +01:00
Miguel PalhasandClaude Opus 5 39cfa50d68 fix(maestro): keep the TUI's two request budgets separate
ci / rust (pull_request) Successful in 5m9s
Collapsing the health probe and the app client into one lost both
policies: the probe's 500ms was a total request timeout, not a connect
timeout, and the app's calls had 5s rather than the client default 30s.
A daemon that accepts TCP and then stalls held the terminal for 30s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:16:30 +01:00
Miguel PalhasandClaude Opus 5 02356f84ab refactor(maestro): route every client through the shared DaemonClient
ci / rust (pull_request) Successful in 3m0s
The CLI, TUI and end-to-end harness each carried their own client: three
reqwest builders with three timeout policies, three copies of the
success-or-body check, and route strings at every call site. Two of them
discarded the daemon's structured error and reported only a status code.

Deletes `maestro-cli/src/http.rs`, both `ws_base` copies in the TUI, the
CLI's partial `DaemonInfo` (it dropped `version` and `presets`), and the
e2e harness's `check_ok`. `maestro-web` gains a protocol dependency and
stops building the error envelope with a format string and reading it back
as untyped JSON; the daemon and the proxy now register routes from the same
constants.

`maestro_web::router` returns `Result` — an unusable daemon URL fails at
startup instead of at first connect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:11:43 +01:00
Miguel PalhasandClaude Opus 5 01a1073943 feat(maestro): own the /api surface in maestro-protocol
The crate held the wire types but not the wire surface, so every consumer
re-derived routes, URL construction and the error envelope.

Adds `routes` (path constants, axum patterns, and a single fallible
`ws_base`), the `ApiError` envelope, `DaemonInfo`, and a `DaemonClient`
behind an optional `client` feature. `SpawnPreset` moves here because it
rides `GET /api/info`; `maestro-config` re-exports it so the on-disk
schema and the payload stay one type.

`ws_base` now rejects a base with no recognised scheme. The five previous
copies disagreed — the CLI errored, the web proxy trusted it, the TUI
passed it through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:11:37 +01:00
Miguel Palhas c5d7a6bf80 Merge remote-tracking branch 'origin/main' into nix
ci / rust (push) Successful in 3m7s
2026-08-22 10:56:21 +01:00
Miguel PalhasandClaude Opus 5 401afbc812 feat(maestro): add a nix flake and Home Manager module
Ships `maestro`, `maestro-web` and the SPA bundle as flake packages, plus a
`services.maestro` Home Manager module that runs the daemon and the web UI as
systemd user services.

`maestro-web`'s build.rs now honours MAESTRO_WEB_DIST and hands the resolved
bundle path to `include_dir!` through MAESTRO_WEB_BUNDLE_DIR. Without it the
Nix build would have to run `pnpm install` mid-compile, which the sandbox has
no network for. Unset, the pnpm path is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 10:53:40 +01:00
Miguel PalhasandClaude Opus 5 66498415f1 docs(maestro): delete superpowers specs and plans
ci / rust (push) Successful in 2m55s
They described the registry/peer-to-peer design and were kept only for
their superseded banners. CLAUDE.md is the current reference. Repoints
the doc comments that cited them, and fixes the maestro-web README,
which still named MAESTRO_REGISTRY as the Vite proxy override
(MAESTRO_DAEMON_URL since the refactor).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 10:49:39 +01:00
Miguel PalhasandClaude Opus 5 69586a4da5 refactor(maestro): rename brokerStreamUrl to attachStreamUrl
There is no broker: the SPA opens the daemon's /api/attach/:id directly
through the maestro-web proxy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 10:49:39 +01:00
Miguel PalhasandClaude Opus 5 8ff5046ff3 refactor(maestro): one default-on-missing config loader
load() and load_or_default() differed only in whether a missing file was
an error. Nothing wants the error: every field has a default. Keeps
load(), drops the MissingFile variant, whose message pointed at the
`maestro daemon configure` subcommand the single-host refactor deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 10:49:39 +01:00
Miguel PalhasandClaude Opus 5 bf7def5cf1 chore(maestro): drop dead maestro-names crate
Nothing in the workspace depended on it — session handles are generated
client-side in web/src/names.ts, which carried its own copy of the
wordlists. Also drops two unused deps: rust-embed (no crate used it;
maestro-web embeds via include_dir) and urlencoding in maestro-cli.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 10:49:30 +01:00
Miguel Palhas c856333e4a docs(maestro): describe the single-host architecture
ci / rust (push) Successful in 2m40s
Rewrites CLAUDE.md as the current-architecture reference, drops the
registry from INSTALL/PRODUCT/DESIGN and the web README, deletes the
registry Dockerfile, and marks the dated design specs superseded where
they describe the registry, JWT auth, or cross-host attach.

The installer loses its configure step — every config field now has a
default, so a daemon starts with no config file at all.
2026-08-22 10:26:49 +01:00
Miguel Palhas de097cf17f test(maestro): rework e2e harness for a single daemon
Drops the registry subprocess and the T2 broker scenarios; the harness
spawns one daemon and the client talks straight to its /api/* surface.
2026-08-22 10:21:27 +01:00
Miguel Palhas 1bb464be4b refactor(maestro): flatten web UI to a single host
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.
2026-08-22 10:14:10 +01:00
Miguel Palhas 6790f41652 refactor(maestro): point CLI and TUI at the local daemon only
CLI drops the registry fallback, cookie auth, set-registry, and the
--daemon selector; every command hits [cli].daemon_url. TUI drops the
daemon tree from the sidebar and the daemon field from the spawn modal.
GET /api/web/info becomes GET /api/info.
2026-08-22 10:07:12 +01:00
Miguel Palhas d006f4a7bd refactor(maestro): drop registry, p2p, and auth from daemon core
Maestro is now single-host: one daemon owns the machine's sessions and
clients talk to it directly. Removes the registry crate, the daemon's
registry client/broker/catalog mirror, JWT attach auth, cookie auth, and
the perimeter (T2) transport config.

The daemon API is now unauthenticated on its bind address, same trust
model as tmux. Session and CreateSessionRequest lose daemon_id;
RegistryEvent becomes CatalogEvent with no daemon variants; attach moves
to WS /api/attach/:id with no token.
2026-08-22 10:00:24 +01:00
naps62-yoloandClaude Opus 4.8 d6d433770b feat(maestro): swipe to open/close the sidebar drawer on mobile
ci / rust (push) Failing after 1m50s
Horizontal swipes now drive the off-canvas drawer (right opens, left closes)
instead of triggering browser back/forward. Gated to mobile and to clearly
horizontal gestures so vertical terminal scrolling is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:55:04 +00:00
naps62-yoloandClaude Opus 4.8 7d0dc6597a feat(maestro): move mobile ☰ to the main header
ci / rust (push) Failing after 1m45s
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>
2026-06-24 16:14:28 +00:00
naps62-yoloandClaude Opus 4.8 58b732d400 refactor(maestro): drop folder session counters in sidebar
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>
2026-06-24 16:14:28 +00:00
naps62-yoloandClaude Opus 4.8 347b6bdc2a feat(maestro): forward touch scroll to mouse-tracking apps
ci / rust (push) Failing after 1m40s
On the alternate screen there's no scrollback, but apps like Claude Code and
tmux enable mouse tracking. When term.modes.mouseTrackingMode is active, turn
finger drags into wheel events so xterm encodes them as mouse-wheel reports the
app reads — scrolling its own content. Normal buffer still walks scrollback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:05:57 +00:00
naps62-yoloandClaude Opus 4.8 a1b6c85f8d fix(maestro): reliable touch scrollback on mobile
ci / rust (push) Failing after 1m49s
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>
2026-06-24 15:59:49 +00:00
naps62-yoloandClaude Opus 4.8 3b69c98cca fix(maestro): render ☰ on mobile empty state
ci / rust (push) Failing after 1m48s
The welcome/empty-tab branch returned before renderMobile, so with the tab
bar collapsed there was no ☰ — leaving mobile users stuck with no way to
open the sidebar drawer. Float the same ☰ over the welcome view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:55:25 +00:00
naps62-yoloandClaude Opus 4.8 9286eb09ba feat(maestro): tighten mobile session UX
ci / rust (push) Failing after 1m42s
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>
2026-06-24 15:53:26 +00:00
naps62-yoloandClaude Opus 4.8 57d417f7a0 feat(maestro): abbreviate paths in spawn dialog recent-dir picker
ci / rust (push) Failing after 1m43s
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>
2026-06-24 15:41:52 +00:00
naps62-yoloandClaude Opus 4.8 70c592e884 fix(maestro): surface cross-host attach failures instead of silent close
ci / rust (push) Failing after 2m11s
Cross-host (T2-brokered) attach fails when no shared perimeter is
configured: the daemon rejects the upgrade with a structured 502
(no_perimeter_candidate / no_local_perimeter / perimeter_mismatch /
peer_unreachable), but maestro-web's WS proxy had already upgraded the
browser socket and merely dropped it on the upstream failure — the
attach pane flashed and vanished with no reason shown.

- maestro-web proxy: on a failed upstream handshake, relay the daemon's
  error message to the browser as an application close frame (4502 +
  reason) instead of a bare drop; parse the structured JSON error body.
- web attach.ts: on a 4000+ close with a reason, show it in the pane
  header and terminal body and keep the pane up, rather than auto-closing.
- daemon: WARN at startup when no perimeter_id/private_addr is configured,
  since cross-host attach is then unavailable.
- docs/INSTALL.md: document the required matching perimeter_id + per-host
  private_addr for cross-host attach.

Closes #39

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:08:03 +00:00
naps62-yoloandClaude Opus 4.8 b67591695e feat(maestro): per-host session counts + abbreviated folder paths
ci / rust (push) Successful in 4m54s
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>
2026-06-22 09:56:15 +00:00
naps62-yoloandClaude Opus 4.8 851006cd2c feat(maestro): mobile multi-view — tabs-only projection + sidebar drawer
ci / rust (push) Successful in 4m48s
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>
2026-06-22 09:30:28 +00:00
naps62-yoloandClaude Opus 4.8 d272210ecd feat(maestro): persist multi-view layout per device
ci / rust (push) Successful in 5m21s
Save the layout tree (tabs + splits + focus + ratios) to localStorage,
debounced, on every structural/focus/resize change; restore it on boot. Load
rebuilds with fresh node ids and defensively drops malformed data — a corrupt or
versioned-out blob falls back to a single empty tab. Sessions that no longer
exist when restored aren't pruned eagerly (the session list isn't known at
boot); their panes attach, the broker closes the dead socket, and the pane
auto-closes.

Closes #34. Part of epic #37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:24:57 +00:00
naps62-yoloandClaude Opus 4.8 159b7ecd6e feat(maestro): multi-view web UI — splits, tabs, drag-dock, highlights
ci / rust (push) Successful in 5m46s
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>
2026-06-22 09:20:57 +00:00
naps62-yolo 5a7e3c1b09 Merge remote-tracking branch 'origin/main' into 29-layout-tree-multi-mount-pool
ci / rust (push) Successful in 5m47s
2026-06-21 21:48:50 +00:00
naps62-yoloandClaude Opus 4.8 3ef323f354 fix(maestro): 404 missing assets + no-cache shell so redeploys don't blank
ci / rust (push) Successful in 5m50s
The SPA fallback returned index.html (HTTP 200, text/html) for ANY unknown
path, including missing /assets/*. index.html was also served with no
Cache-Control, so browsers cached the shell. After a redeploy rotated Vite's
content hashes, a cached shell requested its old /assets/index-OLDHASH.js,
the server answered with the HTML shell, and the browser tried to execute
HTML as JS -> blank page (seen on Android Chrome while the installed PWA,
with its own warm cache, kept working).

- Missing paths that name a file (have an extension) now 404 instead of
  falling through to index.html; only extensionless SPA routes fall back.
- index.html / sw.js / manifest / icons: Cache-Control: no-cache.
- Content-hashed assets/: public, max-age=31536000, immutable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:46:30 +00:00
naps62-yoloandClaude Opus 4.8 2999e888a8 refactor(maestro): multi-mount ConnectionPool; route web through layout tree
Rework the pool from single-active to a mounted-set model: several connections
can be visible at once (one per leaf), sessions no longer shown are parked
(still streaming) and bounded by an LRU buffer that never evicts visible panes.
Keep activate/parkActive/active() as compat shims and an onFocusChange/
onRequestClose surface; the HUD keeps working via active() -> focused().

main.ts now derives the single-pane view from a one-leaf layout tree via
setVisible/setFocus instead of calling activate directly — no behaviour change,
but the multi-mount path is now exercised end to end.

Refs #29 (epic #37).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 21:19:09 +00:00
naps62-yoloandClaude Opus 4.8 e58f5da1fb feat(maestro): layout-tree model for multi-view
Source-of-truth state for what's on screen: tabs of binary split trees whose
leaves are sessions, plus leaves/findLeaf/activeTab/emptyLayout helpers. Splits
and tabs UI build on this in later issues; #29 only renders a single leaf.

Refs #29 (epic #37).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 21:19:09 +00:00
naps62-yolo 6bb45ea8e4 Merge branch 'labels'
ci / rust (push) Successful in 4m49s
2026-06-21 21:12:48 +00:00
naps62-yolo 1311b6c2da Merge remote-tracking branch 'origin/main' into labels 2026-06-21 21:12:00 +00:00
naps62-yoloandClaude Opus 4.8 458c81094b feat(maestro): nest sessions by live working directory, drop labels
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>
2026-06-21 21:11:23 +00:00
naps62-yoloandClaude Opus 4.8 6167559fea fix(maestro): run spawned commands through a login+interactive shell
ci / rust (push) Failing after 1m46s
Presets spawn bare commands (`claude`, `opencode`); tmux runs them via
`$SHELL -c` non-interactively, so `~/.zshrc` — where the user's PATH
additions (`~/.local/bin`, `~/.opencode/bin`, …) live — is never sourced.
Under the stripped systemd-user PATH `maestrod` runs with, those binaries
aren't found → "command not found". `zsh` worked only because it's in the
system PATH.

Wrap each session command in `exec $SHELL -lic '<cmd>'` so an interactive
login shell sources the user's rc and restores their PATH before exec'ing
the real process. `exec` keeps the process tree flat so pane-command
inference is unaffected. Falls back to the bare command when $SHELL is unset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:36:21 +00:00
naps62-yoloandClaude Opus 4.8 5602cf5fc8 feat(maestro): navbar debug toggle for the HUD (#20)
ci / rust (push) Failing after 1m48s
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>
2026-06-21 15:24:36 +00:00
naps62-yoloandClaude Opus 4.8 f3c4784acc feat(maestro): debug HUD overlay for inferred session metrics (#20)
ci / rust (push) Failing after 1m49s
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>
2026-06-21 14:55:41 +00:00
naps62-yoloandClaude Opus 4.8 fd80ae152a feat(maestro): infer session activity state + foreground (Active/Idle/AwaitingInput) (#18)
ci / rust (push) Failing after 2m15s
Add an orthogonal activity dimension to sessions, inferred entirely from
structured tmux/`/proc` state — no PTY byte-stream parsing, no work on the
attach hot path.

Protocol: `ActivityState` enum (Active/Idle/AwaitingInput) + three additive,
serde-defaulted `Session` fields (`foreground`, `activity`, `last_activity_at`)
so the snapshot/diff flow stays forward-compatible with older daemons.

Detection (piggybacks the existing ~2s liveness tick, no new timer):
- foreground command via `tmux list-panes -F '#{pane_pid} #{pane_current_command}
  #{pane_tty}'`;
- quiescence from a per-`PtyBridge` last-read `Instant`, bumped in `run_reader`;
- AwaitingInput via best-effort `/proc` walk to the deepest descendant: state
  `S` + a non-timer `wchan` + an stdio fd on the pane tty. (`/proc/pid/syscall`
  is ptrace-gated and unreadable for own sessions, so `wchan` distinguishes a
  blocking read from a plain `sleep`.)
- combine: recent output → Active; else input-blocked → AwaitingInput; else
  Idle. Emit `SessionUpdated` only on (activity, foreground) change — no diff
  storms at steady state.

`maestro list` gains an ACTIVITY column and shows the live foreground. Pure
combine/parse logic is unit-tested; an e2e asserts `cat` → AwaitingInput and
`sleep` → Idle with correct foreground against real tmux.

Closes #18
Closes #8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:51:08 +00:00
naps62-yoloandClaude Opus 4.8 c8bf240624 feat(maestro): "Spawn similar" catalog action + spawn-history/gitroot e2e (#28)
ci / rust (push) Successful in 4m15s
Web: rename the per-row clone affordance to "Spawn similar" — it reopens the
revamped spawn form pre-seeded from the session's host, dir, command (matched
back to a preset by the form), and label, but with a freshly generated name
since it's a new session. Built entirely on the existing prefill machinery; the
custom event is renamed `catalog-spawn-similar`.

e2e (maestro-e2e): against real tmux + git, spawn into a git repo dir and assert
(1) the daemon's recent-dirs endpoint surfaces that dir with its command — what
#27's recents picker reads on the next form open — proving the durable
spawn-history write, and (2) gitroot infers the enclosing repo as the worktree
base_repo.

Closes #28

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:05:48 +00:00
naps62-yoloandClaude Opus 4.8 eef96548b8 feat(maestro): make web UI an installable PWA
ci / rust (push) Successful in 4m15s
Add a web app manifest, icons, and a runtime-caching service worker so the
UI can be installed on Android Chrome (and added to the iOS home screen).

- web/public/manifest.webmanifest: standalone display, theme/bg colors, icon set
- web/public/icon.svg + generated icon-192/512 and apple-touch-icon PNGs
- web/public/sw.js: caches the static shell for offline launch; never touches
  /api (HTTP or WS), so live-daemon behavior is unchanged
- index.html: link manifest + apple-touch-icon, iOS standalone metas
- main.ts: register the service worker (production only)

Assets land in web/dist via Vite's public/ dir and are embedded by the
existing include_dir! in maestro-web, so no Rust changes are needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:38:44 +00:00
naps62-yoloandClaude Opus 4.8 658ec32444 feat(maestro): seed worktree branch from session name; move worktree below name (#27)
ci / rust (push) Successful in 4m35s
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>
2026-06-20 21:26:01 +00:00