The name field starts empty and the reroll is gone; the worktree branch keeps
a default by borrowing one handle from /api/names. A card whose name the
daemon generated renders dimmed until a first prompt settles it.
Refs #131
The picker now leads the form in both clients and takes focus on open, so
Tab walks from it into the command tiles. Opening the form also preselects
the project of the session you were last on — a worktree session seeds its
base repository — so 'n' then Enter spawns another session there.
Draft panes are gone: an empty pane is a placeholder again, and every
spawn affordance opens a dialog around the shared spawn form. Escape,
Cancel, the ✕ and a backdrop click dismiss it; a successful submit puts
the session in a new tab (Spawn similar and the fast-forward fallback
keep placing it beside the pane they acted on).
Removes the per-leaf draft state, its persistence strip, and the form
reuse/teardown machinery the panes needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Session handles are one word from a single 218-entry list — instruments,
forms, notation and theory, tempo and dynamics — replacing the
adjective-noun pair. A batch is shuffled, so it repeats only once the
wordlist runs out.
Project boxes show the parent directory instead of the full path: the
last segment is already the name line above it. The last command is gone
from the box, and picking a project no longer pre-selects it — the
directory and what runs in it are independent choices. Both changes land
in the TUI picker too, which mirrors the web form's rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Spawn similar dropped the session's cwd into the free-text directory hatch,
so the project picker showed nothing selected. For a worktree session that
cwd was the daemon-made checkout, and the worktree toggle defaulted on, so
the clone asked for a worktree of a worktree.
It now seeds the picker: the cloned project's box selected, the same
worktree decision, base repo plus '+ new' when the source had a worktree.
A seeded path that matches no project falls back to the raw input showing
that path instead of an empty picker.
The worktree step ("+ new" plus the repository's existing worktrees) now
lives inside the collapsed "Advanced options" section in both the web form
and the TUI draft pane, so a plain spawn is command, project, name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Matches the TUI's WorktreeToggle, whose Enter also toggles rather than
submitting — otherwise the two clients would disagree on this one
field despite #97 requiring the same rule everywhere else.
The new-branch name input and the existing-branch picker both had a
literal <label>Branch</label>, on top of "New branch"/"Existing branch"
mode radios — reading the form gave no clue what the field wanted.
Kept the mode radios (they name the choice, not the field) and gave
each field its own label: "New branch name" for the text input,
"Pick a branch" for the select.
A project-box or worktree-choice picker button that isn't yet the
active pick takes the Enter to choose it instead; the next Enter, once
it's active, submits. The project filter resolves to the top filtered
project rather than sending whatever cwd was last set. The raw-cwd
autocomplete's own Enter (accept the highlight) keeps working via
e.defaultPrevented.
Closed by default behind a disclosure reusing the sidebar folder-header
affordance (catalog.ts), since most spawns take the default target and
the section was the largest block in the form. The collapsed header
states whether a worktree will be created, so #91's off-by-default for
a worktree row stays visible without expanding.
Fixes a latent [hidden] + display:flex bug for the new .wt-collapsible
element (author CSS beats the UA [hidden] rule at equal specificity);
.wt-body has the same bug pre-existing and is out of scope here.
The working-directory picker was a flat list mixing repositories with
their worktrees, each row repeating the project name as an abbreviated
path. It is now two steps: a grid of project boxes carrying the full
unabbreviated path, then — only once a project is picked — that
repository's worktrees with "+ new" as the default.
"+ new" creates a worktree; picking an existing one runs in it and
leaves the create-worktree toggle off, since the daemon never adopts
(docs/WORKTREES.md). That is #91's rule, now an explicit choice rather
than an inference from which row was clicked.
Also fixes `.raw-wrap[hidden]`: `display: flex` outranked the UA sheet's
`[hidden]`, so the free-text cwd input showed even when hidden.
The interaction rules are listed in spawnForm.ts for the TUI mirror (#96)
to follow.
Refs #95
Typing in the raw cwd field fired one /api/gitroot request per
keystroke (#74). Every keystroke now resets a 250ms window shared with
the TUI draft (GITROOT_PROBE_DEBOUNCE_MS there); probeToken still owns
ordering so a late answer can't overwrite a newer input.
A tile clicked, or a custom command typed, during the /api/info round trip
was discarded: applyInitialCommand() ran unconditionally a second time once
presets refined, re-selecting the first non-custom preset with no seed to
protect the operator's pick. Guard it with a commandTouched flag, mirroring
the nameTouched pattern already used for the name-batch race.
Fixes#76.
Picking a project row that is itself a linked git worktree used to
default 'Create git worktree' on, so spawning asked for a second
worktree of an already-checked-out branch and git refused. Both clients
now default the toggle off there: the session just runs in that existing
worktree with no worktree record (not adoption). Turning it back on by
hand still tries, per the issue's non-goal.
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.
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.
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.
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.
- 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
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.
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.