Commit Graph
68 Commits
Author SHA1 Message Date
Miguel Palhas 73e9ecfc85 fix(maestro): cap and centre the spawn form's width
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.
2026-08-24 14:48:19 +01:00
Miguel Palhas 9c6e06ec4f fix(maestro): reuse empty draft pane and keep its seed (#72)
openDraft's reuse check was `!target.draft`, dead after first render:
createSpawnForm auto-syncs its picked defaults into the leaf's draft
once its project load settles, so every empty pane has a non-null
draft a second later. `n` and Spawn always split instead of reusing
the pane on screen.

Reuse now checks only `!target.sessionId`, matching the codebase's own
definition of "empty" pane elsewhere. Reusing also drops the leaf's
stale form instance so a new seed is read by the rebuilt form instead
of being ignored by the old one.
2026-08-24 14:39:51 +01:00
Miguel Palhas d4dcfe7f00 feat(maestro): show blocked work on session rows and warn on kill in the web UI (#83) 2026-08-24 13:19:12 +01:00
Miguel Palhas 31702e03e3 refactor(maestro): drop cleanup_on_kill from the wire and the CLI
Nothing has honoured WorktreeSpec.cleanup_on_kill since #56, and after
#79 and #81 survival is decided by what is in a worktree, not by what
the spawn asked for. A field that lies is worse than a missing one.

Removes the field from the protocol, the --cleanup-on-kill flag from
the CLI, and the tests asserting its default.
2026-08-24 12:08:41 +01:00
Miguel Palhas 6f698df9f4 feat(maestro): retained-worktrees view with reclaim (#84) 2026-08-24 11:44:22 +01:00
Miguel Palhas 8e8540460f feat(maestro): web wire types + client calls for retained worktrees 2026-08-24 11:44:22 +01:00
Miguel Palhas bbbc24a4aa fix(maestro): fifth-review findings, incl. the guarded-worktree dead end
The cleanup guard plus deterministic paths had created a trap: one
untracked file an agent left behind — a log, a .env, a stray artifact —
made the guard refuse removal, the directory stayed, and every later
spawn of that branch hit PathExists. The branch was burned permanently
and the product offered no way to clear it. A leftover tree registered
for the same branch is now adopted, so the operator lands back in their
own unsaved work. A tree a *live* session holds is still rejected, since
two sessions must not share a checkout.

Also:

- Fast-forward still awaited /api/info, /api/projects and /api/names
  inline, the exact freeze the draft loaders were restructured to avoid.
  It posts to the same channel now.
- Draft updates were routed by tab index, which shifts when a tab closes:
  an in-flight load could be dropped or land on the wrong pane. Drafts
  carry an id and are found by it.
- extend_names left the name it had just displayed at the head of the
  queue, so the next reroll produced it again and the key looked dead.
- The fast-forward fallback discarded info.home_dir, so its draft showed
  unabbreviated paths and seeded "Other directory…" with / — disagreeing
  with the normal draft entry point.
- Web fast-forward mounted via openSession, which replaces the focused
  pane: pressing it while attached evicted that session from the layout.
  It reuses an empty pane or splits, as the TUI does.
- branchEdited was inferred from the seeded branch value, but in "new"
  mode that value is the auto-synced session name — so any form rebuild
  froze the branch against later rerolls.
- resolveBaseRepo ignored the worktree checkbox, rebuilding the fieldset
  expanded under an unticked box and probing gitroot for a worktree the
  operator had turned off.
- short_hash used DefaultHasher, whose algorithm is not stable across
  Rust releases, in a directory name — a toolchain bump would orphan
  every worktree under the override root. Now FNV-1a.
2026-08-24 00:28:50 +01:00
Miguel Palhas e9368bb242 fix(maestro): fourth-review findings across daemon, tui and web
The existing-branch picker could never succeed in either client: both
treated the main checkout's HEAD as available, and since that is usually
`main` it became the default pick — git then refuses the second checkout.
A TUI test had baked the bug in by asserting the selection lands on the
`is_head` entry. Both pickers now treat "checked out anywhere" as held.

Worktree paths were still not injective. Flattening `/` and `.` to `-`
made `feat/x`, `feat.x` and `feat-x` share one directory, and the
override root keyed on the repo basename alone, so `~/a/maestro` and
`~/b/maestro` collided. A branch name is already a valid relative path —
git stores it as refs/heads/feat/example — so it is used verbatim and
nests under `worktrees/feat/`; the override root gains a digest of the
full repo path. Git forbids holding both `feat` and `feat/x`, so the
nesting cannot collide for valid refs.

Also:

- Recovered sessions dropped their worktree association, so after a
  restart a kill cleaned up nothing and the leftover directory blocked
  that branch forever. It is persisted on Session now.
- The nesting guard compared git's resolved paths against an unresolved
  one, so it silently passed on any symlinked repo path — and that guard
  is what stands between a kill and an inner tree's uncommitted work.
- Draft loads ran on the TUI event loop: /api/projects on a cold cache
  walks all of $HOME, freezing keys, frames and attached panes for up to
  the request timeout. They are detached tasks posting to a channel now.
- Fast-forward in the web client created the session and never mounted
  it, so `N` looked like a dead key while the TUI attached.
- The fast-forward fallback seed forced the worktree toggle off, against
  both a normal draft and the TUI.
- The projects background refresh honoured the TTL, so two of every three
  ticks were no-ops and requests paid for the rescan instead.
2026-08-24 00:10:35 +01:00
Miguel Palhas 586c243056 feat(maestro): bind N to fast-forward spawn in the web client (#67) 2026-08-23 23:39:30 +01:00
Miguel Palhas 1647e64aed feat(maestro): fast-forward spawn resolution for the web client (#67) 2026-08-23 23:37:57 +01:00
Miguel Palhas 525dc9a04a fix(maestro): reuse draft forms across renders (#68)
render() destroyed and rebuilt every draft form, so an unrelated
session exiting tore down a draft mid-edit: focus, caret and
selection were lost and /api/info, /api/projects, /api/names were
refetched for a form already on screen.

Key draft forms by leaf id; rebuild only the pane DOM around them
and destroy a form when its leaf is removed or filled by a session.
2026-08-23 22:43:42 +01:00
Miguel Palhas 5c3f9cab9f feat(maestro): replace spawn modal with draft panes (#62)
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.
2026-08-23 22:36:48 +01:00
Miguel Palhas 26b0fa5e8a merge: worktree on by default, new-or-existing branch picker (#61) 2026-08-23 21:36:40 +01:00
Miguel Palhas 8b3c5d73b6 fix(maestro): third-review findings — nested worktrees, staleness, placement
The one that loses data: the project picker lists linked worktrees as
pickable rows, so `base_repo` was easily a worktree rather than the main
checkout, and the new placement then nested the tree inside it. Git hides
a registered worktree's contents from the outer `git status`, so the
outer tree read clean and removing it deleted the inner one's uncommitted
work — while that session could still be running. Placement now resolves
to the main checkout via `--git-common-dir`, and removal refuses outright
when `worktree list` reports anything registered underneath.

Also:

- The `worktree_root` override dropped the old UUID without replacing it,
  so two repos spawning the same branch name targeted one directory and
  the second failed. The repo basename now sits between root and slug.
- Project staleness read `.git/HEAD`'s mtime, which only moves on
  checkout. A repo committed to daily from one long-lived branch reported
  its last branch switch and got dimmed. Prefer `logs/HEAD`.
- The projects cache TTL (5s) was shorter than its own background refresh
  (60s), so nearly every request rescanned all of $HOME under the scanner
  lock — and draft panes re-request on unrelated re-renders. Now 180s.
- onSuccess resolved the leaf against the active tab only, so switching
  tabs mid-POST orphaned the session. Searches every tab.
2026-08-23 21:36:29 +01:00
Miguel Palhas ca4f7cb1d5 feat(maestro): worktree on by default with new-or-existing branch picker (#61)
- 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
2026-08-23 21:34:35 +01:00
Miguel Palhas 818e0cc2db feat(maestro): spawn form rerolls from a fetched name batch (#53) 2026-08-23 21:15:33 +01:00
Miguel Palhas b9829bc56f feat(maestro): project list replaces recents picker in spawn form (#59) 2026-08-23 21:06:10 +01:00
Miguel Palhas 9f582263b8 feat(maestro): project list sort/group/filter helpers (#59) 2026-08-23 21:02:16 +01:00
Miguel Palhas 8376122dfe fix(maestro): second-review findings across daemon, tui and web
Ten findings from reviewing the accumulated blitz/spawn-v2 diff. The two
that could wedge a repo:

- The cleanup guard refused any worktree whose commits were on no other
  ref, which is every ordinary unpushed branch. Paths are deterministic
  now, so the leftover tree blocked the next spawn and each branch became
  a one-shot. `worktree remove` keeps refs/heads/<branch>, so committed
  work is never at risk: guard on an unclean working tree alone.
- A worktree whose directory had vanished failed the git probe and was
  reported as unsaved work, stranding the registration forever. Prune it
  instead, and prune before `worktree add` tests the target path.

Also:

- The CLI hardcoded create_branch: true, so the daemon's existing-branch
  path was unreachable. Added --existing-branch.
- Pastes were forwarded without their DECSET 2004 markers, so a remote
  shell ran a multi-line paste line by line.
- spawnForm emitted before its seed was restored and after destroy(), so
  a rebuilt draft blanked its cwd and a dead form clobbered the live one.
- showRawCwd and the probe's worktree untick never reached the leaf.
- A late autocomplete fetch reopened the popup under a blurred input.
- The "remove worktree when killed" checkbox promised something the
  daemon no longer honours; removed.
2026-08-23 20:54:21 +01:00
Miguel Palhas ca585447ff fix(maestro): dir autocomplete dismissal and key handling
Escape closes the popup, refocuses the input and stops propagation so it
can't also discard a surrounding draft pane. Tab commits the highlighted
entry and lets focus move to the next field. Blur / clicking outside
dismisses, and a dismissed popup stays closed until the operator types
again.
2026-08-23 20:38:55 +01:00
Miguel Palhas 6f8de8de7c fix(maestro): draft-pane regressions from the integration review
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.
2026-08-23 20:12:28 +01:00
Miguel Palhas 790d1e2075 feat(maestro): empty panes render an inline spawn draft form (#57) 2026-08-23 19:52:18 +01:00
Miguel Palhas cbfd5eee12 refactor(maestro): extract the spawn form so it can live outside the modal 2026-08-23 19:52:18 +01:00
Miguel Palhas 10cb47c50b feat(maestro): hang draft state off layout leaves, never persisted (#57) 2026-08-23 19:52:18 +01:00
Miguel PalhasandClaude Opus 5 f31377c34f fix(maestro): three defects the review flagged and left open
ci / web (pull_request) Successful in 28s
ci / rust (pull_request) Successful in 1m58s
ci / web (push) Successful in 34s
ci / rust (push) Successful in 2m44s
SPA attach had no reconnect at all. Any dropped frame closed the socket,
`onClosed` fired, and the pane was destroyed permanently — the TUI
reconnects forever on the same endpoint. Session death also reached the UI
on two uncoordinated channels: the row from the events socket, the pane
from `ws.onclose`, with nothing reconciling them, so either could happen
without the other.

The catalog is now the authority. `shouldRetryClose` decides from session
status: Running means the link dropped, so retry with 500ms-to-10s
backoff; Dead means the PTY stream was sealed and nothing more is coming;
absent means it is gone. Application closes (4000+, a relayed daemon
rejection) and clean closes are never retried. The shell also closes any
pane whose session has left the catalog. The policy lives in reconnect.ts
so it is tested without a DOM.

kill_session checked `has()` before `kill()`, which only opened a window
for the liveness watcher to reap in between — turning a 404 into a 502.
`kill` reports the miss itself.

with_spawn_history logged and carried on when `Arc::get_mut` failed,
producing a daemon whose recent-dirs picker is permanently empty with
nothing to show for it. It panics now: that is a construction-order
mistake, at startup, in code that runs once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 16:31:54 +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 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 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 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 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
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-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-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 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