Two new effects: ProbeGitRoot resolves the draft target's base repo via
/api/gitroot (seq-guarded, a miss unticks the toggle) and LoadBranches
fills the existing-branch picker from /api/branches, both applied back
to the tab that requested them. Branch loads triggered by a successful
probe are served inline so perform() stays recursion-free.
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.
Worktree on by default over a git repository with Enter/Space to spawn
in the checkout itself; base repo inferred from picked projects or a
guarded /api/gitroot probe for raw paths (a miss unticks, web parity);
new-branch name tracking the generated session name until touched;
Left/Right switches to picking an existing branch from GET /api/branches,
with held branches skipped and rendered dimmed beside their holder.
No cleanup option: cleanup is unconditional daemon-side (#56).
The spawn modal is no longer one of the three surfaces: the Components
section now describes the draft pane, and the stale ≥600px
bottom-sheet→dialog breakpoint note is dropped.
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.
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.
- 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.
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.
forward_to_session dropped Paste(_) on the floor, so pastes into a
session vanished once terminal.rs enabled bracketed paste. Split the
arm: Paste forwards the payload verbatim (the remote readline
reassembles it), only Mouse events have no wire encoding.
Action::OpenSpawnModal becomes NewDraftPane: pressing s turns the empty
pane into an inline draft form (or opens a new tab when the pane shows a
session). The form is a pane-level widget with its own field focus; a
successful submit replaces the draft with the new session in place,
matching the web client's draft panes. SpawnModal and OverlayKind::Spawn
are removed; kill-confirm and help overlays stay.
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.