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.
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
// Fast-forward spawn (#67): one keystroke, every default resolved, no form.
|
|
// Pure resolution only — the caller (main.ts) owns the fetches and the POST,
|
|
// and falls back to a seeded draft pane when anything is missing.
|
|
|
|
import type { CreateSessionRequest, Project, SpawnPreset } from "./types.js";
|
|
import { sortProjects } from "./projectList.js";
|
|
|
|
/** What one fast-forward keystroke resolved. Anything absent means "open a
|
|
* draft pre-filled with the rest instead of spawning". */
|
|
export interface FastForwardResolution {
|
|
project?: Project;
|
|
command?: string;
|
|
name?: string;
|
|
}
|
|
|
|
/** The defaults: most recent project (fresh-first, same ordering the picker
|
|
* shows), its last command falling back to the first non-custom preset, and
|
|
* a freshly generated name when the daemon supplied one. */
|
|
export function resolveFastForward(
|
|
projects: Project[],
|
|
presets: SpawnPreset[],
|
|
name?: string,
|
|
): FastForwardResolution {
|
|
const project = sortProjects(projects, new Date())[0];
|
|
const lastCommand = project?.last_command?.trim();
|
|
const command =
|
|
lastCommand || presets.find((p) => p.command !== "")?.command || undefined;
|
|
return { project, command, name: name || undefined };
|
|
}
|
|
|
|
/** The immediate spawn request — worktree on, new branch named after the
|
|
* generated session name — or `null` when any default is missing. */
|
|
export function buildFastForwardRequest(
|
|
res: FastForwardResolution,
|
|
): CreateSessionRequest | null {
|
|
const { project, command, name } = res;
|
|
if (!project || !command?.trim() || !name) return null;
|
|
return {
|
|
cwd: project.path,
|
|
command,
|
|
metadata: { name },
|
|
worktree: {
|
|
base_repo: project.base_repo ?? project.path,
|
|
branch: name,
|
|
cleanup_on_kill: true,
|
|
create_branch: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Seed for the fallback draft pane: whatever did resolve goes into the
|
|
* form; the rest stays with its own defaults. */
|
|
export function fastForwardSeed(res: FastForwardResolution) {
|
|
return {
|
|
cwd: res.project?.path ?? "",
|
|
command: res.command ?? "",
|
|
name: res.name ?? "",
|
|
// Left undefined rather than false when nothing resolved: the form
|
|
// defaults the toggle on, and `false` would stick, so the fallback draft
|
|
// would disagree with both a normally-opened draft and the TUI's fast path
|
|
// (#67 review).
|
|
worktreeEnabled: res.project ? true : undefined,
|
|
worktreeBranch:
|
|
res.project && res.name ? res.name : undefined,
|
|
};
|
|
}
|