import { escapeHtml } from "./html.js";
import {
createSession,
fetchBranches,
fetchNames,
getDaemonInfo,
projects as fetchProjects,
gitRoot,
} from "./api.js";
import { attachDirAutocomplete } from "./dirAutocomplete.js";
import { parentDir } from "./paths.js";
import { filterProjects, groupProjects, isStale, sortProjects } from "./projectList.js";
import type { ProjectGroup } from "./projectList.js";
import { abbreviateCwd } from "./types.js";
import type { BranchInfo, CreateSessionRequest, Project, Session, SpawnPreset } from "./types.js";
/** Where a worktree's branch comes from: freshly created (default) or picked
* from the repository's existing branches (#61). */
export type WorktreeBranchMode = "new" | "existing";
/** Seed values for the spawn form — used by "clone" to open a draft with the
* same cwd, command, and name as an existing session, and by draft panes to
* restore their own half-filled state after a re-render. */
export interface SpawnFormSeed {
command?: string;
cwd?: string;
name?: string;
worktreeEnabled?: boolean;
worktreeBranch?: string;
/** Whether the operator typed the branch themselves, rather than it having
* been auto-synced from the session name. Carried separately because the
* value alone cannot distinguish the two. */
worktreeBranchEdited?: boolean;
worktreeBranchMode?: WorktreeBranchMode;
cwdRawVisible?: boolean;
}
/** The form's current values, reported on every change so a caller (the draft
* pane) can hang them off its layout leaf. */
export interface SpawnFormValues {
command: string;
cwd: string;
name: string;
worktreeEnabled: boolean;
worktreeBranch: string;
worktreeBranchEdited: boolean;
/** "new" creates the branch; "existing" checks out a picked one (#61). */
worktreeBranchMode: WorktreeBranchMode;
/** Whether the cwd lives in the raw text input (vs a picked recent row). */
cwdRawVisible?: boolean;
}
export function buildSpawnRequest(
values: SpawnFormValues,
baseRepo: string | null,
): { req: CreateSessionRequest } | { error: string } {
if (!values.cwd) return { error: "Pick a working directory." };
if (!values.command) return { error: "Pick a command (or fill the custom command)." };
const metadata: Record = {};
const name = values.name.trim();
if (name) metadata.name = name;
const req: CreateSessionRequest = {
cwd: values.cwd,
command: values.command,
metadata,
};
if (values.worktreeEnabled) {
const mode = values.worktreeBranchMode ?? "new";
const branch = values.worktreeBranch.trim();
if (!baseRepo) return { error: "Worktree needs a directory inside a git repository." };
if (!branch) {
return { error: mode === "existing" ? "Pick an existing branch." : "Worktree requires a branch name." };
}
req.worktree = {
base_repo: baseRepo,
branch,
// Existing-mode picks must check out as-is; the daemon creates
// anything else with `worktree add -b` (#56/#61).
create_branch: mode !== "existing",
};
}
return { req };
}
/** Built-ins used when the daemon's `/api/info` presets can't be fetched
* (older daemon, transient error). Mirrors `maestro_config::builtin_presets`.
* Exported for the fast-forward spawn's preset fallback (#67). */
export const FALLBACK_PRESETS: SpawnPreset[] = [
{ id: "claude", label: "Claude", command: "claude" },
{ id: "opencode", label: "opencode", command: "opencode" },
{ id: "zsh", label: "Shell", command: "zsh" },
{ id: "custom", label: "Custom…", command: "" },
];
/** Quiet period after raw-path typing before `/api/gitroot` fires (#74).
* Must stay equal to `GITROOT_PROBE_DEBOUNCE_MS` in
* crates/maestro-tui/src/draft.rs so both clients debounce alike. */
const GITROOT_PROBE_DEBOUNCE_MS = 250;
export interface SpawnFormHandle {
el: HTMLFormElement;
/** Remove global listeners (dir autocomplete) — call when unmounting. */
destroy(): void;
}
export interface SpawnFormOptions {
seed?: SpawnFormSeed;
/** Extra class(es) for the