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
+177
View File
@@ -1421,6 +1421,183 @@ textarea {
border-radius: var(--r-2);
}
/* ---------- spawn form: tiles + recents picker (#27) ---------- */
.spawn-modal .field {
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.field-label {
font-size: var(--fs-13);
color: var(--ink);
}
/* One-click tiles: host row + command row. */
.tile-row {
display: flex;
flex-wrap: wrap;
gap: var(--sp-2);
}
.tile {
flex: 1 1 auto;
min-width: 96px;
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-start;
padding: var(--sp-2) var(--sp-3);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-2);
color: var(--ink);
font-family: inherit;
cursor: pointer;
text-align: left;
transition:
border-color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
}
.tile:hover {
border-color: var(--border-strong);
}
.tile.is-active {
border-color: var(--accent);
background: var(--accent-tint);
}
.tile-label {
font-size: var(--fs-14);
font-weight: 600;
}
.tile-sub {
font-size: var(--fs-12);
color: var(--muted);
font-family: var(--font-mono);
}
.tile-more {
flex: 0 0 auto;
min-width: 0;
justify-content: center;
align-items: center;
color: var(--muted);
}
/* Recent-dirs picker rows. */
.picker {
display: flex;
flex-direction: column;
gap: var(--sp-1);
max-height: 220px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.recent-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--sp-3);
padding: 6px var(--sp-3);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-2);
color: var(--ink);
font-family: inherit;
cursor: pointer;
text-align: left;
transition:
border-color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
}
.recent-row:hover {
border-color: var(--border-strong);
}
.recent-row.is-active {
border-color: var(--accent);
background: var(--accent-tint);
}
.recent-dir {
font-family: var(--font-mono);
font-size: var(--fs-14);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recent-cmd {
flex: 0 0 auto;
font-family: var(--font-mono);
font-size: var(--fs-12);
color: var(--muted);
}
.recent-other .recent-dir {
color: var(--muted);
font-family: inherit;
font-style: italic;
}
.picker-loading {
margin: 0;
padding: 6px var(--sp-3);
color: var(--faint);
font-size: var(--fs-13);
}
.raw-wrap {
display: flex;
}
.raw-input {
width: 100%;
}
/* Worktree-by-branch: inferred repo line + inline hint. */
.wt-hint {
margin: 0;
font-size: var(--fs-12);
color: var(--muted);
}
.wt-body {
display: flex;
flex-direction: column;
gap: var(--sp-3);
}
.wt-repo {
margin: 0;
font-family: var(--font-mono);
font-size: var(--fs-12);
color: var(--muted);
}
/* Generated-name field with a reroll button. */
.name-row {
display: flex;
gap: var(--sp-2);
align-items: center;
}
.name-row input {
flex: 1 1 auto;
}
.reroll {
flex: 0 0 auto;
}
/* ---------- directory autocomplete (NAP-329) ---------- */
.dir-ac {