feat(maestro): tile-first web spawn form — host/command tiles, recents, worktree-by-branch, generated name (#27)
ci / rust (push) Successful in 4m29s

Rework web/src/spawn.ts from raw text boxes into a low-friction, tile-first
flow: pick host tile → pick command tile → pick recent dir → Spawn, name
already filled.

- Command tiles from the daemon's advertised presets (GET /api/web/info, #26),
  with built-in fallback; `custom` reveals the free-text command input.
- Host tiles replace the <select> (label hostname, sub daemon_id); most-
  recently-spawned-on host sorts first + pre-selected; overflow behind "More…".
- Recent-dirs picker per host (GET /api/daemons/:id/recent-dirs, #26), rows show
  dir + last command, re-queried on host change; picking a dir can pre-select
  its last command. Persistent "Other directory…" drops to the raw input +
  NAP-329 autocomplete; empty history prefills raw with home_dir.
- Worktree by branch alone: toggle infers base_repo via /gitroot from the
  chosen dir (#26); non-repo dir disables the toggle with an inline hint;
  WorktreeSpec.base_repo filled client-side (wire unchanged).
- Name field prefilled with a generated cyberpunk handle (names.ts mirrors the
  maestro-names wordlists); ↻ rerolls.

Adds api.ts getWebInfo/recentDirs/gitRoot, the WebInfo/SpawnPreset/RecentDirs/
GitRoot types, and stubs the new endpoints in the `?mock&spawn` review fixture.

Closes #27

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 21:22:25 +00:00
co-authored by Claude Opus 4.8
parent f38b948aa6
commit b3ef2e543e
8 changed files with 792 additions and 137 deletions
+18 -11
View File
@@ -273,7 +273,8 @@ export function renderTopbar(store: CatalogStore): HTMLElement {
spawnBtn.className = "btn btn-primary spawn-btn";
spawnBtn.textContent = "+ Spawn";
spawnBtn.addEventListener("click", () => {
openSpawnModal(store.snapshot().daemons);
const snap = store.snapshot();
openSpawnModal(snap.daemons, undefined, snap.sessions);
});
actions.appendChild(spawnBtn);
return header;
@@ -387,9 +388,10 @@ export function renderSidebar(store: CatalogStore): SidebarHandle {
treeHost.replaceChildren();
if (sessions.length === 0) {
treeHost.appendChild(
renderEmpty(state.daemons.size === 0, filter.trim().length > 0, () =>
openSpawnModal(store.snapshot().daemons),
),
renderEmpty(state.daemons.size === 0, filter.trim().length > 0, () => {
const snap = store.snapshot();
openSpawnModal(snap.daemons, undefined, snap.sessions);
}),
);
return;
}
@@ -406,13 +408,18 @@ export function renderSidebar(store: CatalogStore): SidebarHandle {
const cloneHandler = (ev: Event) => {
const s = (ev as CustomEvent<Session>).detail;
const rawName = typeof s.metadata?.name === "string" ? s.metadata.name : "";
openSpawnModal(store.snapshot().daemons, {
daemon_id: s.daemon_id,
cwd: s.cwd,
command: s.command,
name: rawName,
label: sessionLabel(s) ?? "",
});
const snap = store.snapshot();
openSpawnModal(
snap.daemons,
{
daemon_id: s.daemon_id,
cwd: s.cwd,
command: s.command,
name: rawName,
label: sessionLabel(s) ?? "",
},
snap.sessions,
);
};
document.addEventListener("catalog-clone", cloneHandler);