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
+23
View File
@@ -3,8 +3,11 @@ import type {
CreateSessionRequest,
DaemonInfo,
DirListing,
GitRoot,
RecentDirs,
Session,
Uuid,
WebInfo,
} from "./types.js";
async function jsonOrError<T>(res: Response): Promise<T> {
@@ -58,6 +61,26 @@ export async function listDir(daemonId: string, path: string): Promise<DirListin
return jsonOrError(await fetch(url, { credentials: "same-origin" }));
}
/** Identity + spawn presets of the daemon this web app is connected to (#26). */
export async function getWebInfo(): Promise<WebInfo> {
return jsonOrError(await fetch("/api/web/info", { credentials: "same-origin" }));
}
/** Recently-used working dirs on `daemonId`'s host, newest-first and deduped by
* dir (#26). Drives the spawn form's recents picker. */
export async function recentDirs(daemonId: string): Promise<RecentDirs> {
const url = `/api/daemons/${encodeURIComponent(daemonId)}/recent-dirs`;
return jsonOrError(await fetch(url, { credentials: "same-origin" }));
}
/** Probe the enclosing git repo of `path` on `daemonId`'s host (#26). `root` is
* absent when the path isn't inside a repo. `path` may be `~`/relative/absolute;
* the daemon resolves it against its home dir. */
export async function gitRoot(daemonId: string, path: string): Promise<GitRoot> {
const url = `/api/daemons/${encodeURIComponent(daemonId)}/gitroot?path=${encodeURIComponent(path)}`;
return jsonOrError(await fetch(url, { credentials: "same-origin" }));
}
export function eventsWsUrl(): string {
const proto = location.protocol === "https:" ? "wss:" : "ws:";
return `${proto}//${location.host}/api/events`;