// Retained-worktrees view (#84): every worktree the daemon is holding that no // session uses, listed from `GET /api/worktrees` with a reclaim action. // // Wording is shared with the CLI (`maestro worktrees list`) and the TUI // overlay: same reason labels, same in-the-way phrasing via // `formatInTheWay` (the mirror of `InTheWay`'s Display impl). Reclaim reads // as what it is — destructive — naming what will be lost before it happens // and saying out loud that the branch survives (docs/WORKTREES.md). import { escapeHtml } from "./html.js"; import { abbreviateCwd, abbreviateFolderName, formatInTheWay, reasonLabel, type RetainedWorktree, type Uuid, } from "./types.js"; export interface WorktreesDeps { /** `GET /api/worktrees`. */ load(): Promise; /** `DELETE /api/worktrees/:id`. */ reclaim(id: Uuid): Promise; /** The daemon's home dir, for `~` abbreviation of paths. */ homeDir(): string | undefined; } export interface WorktreesViewHandle { el: HTMLElement; /** Re-fetch the store and re-render. Safe to call repeatedly. */ refresh(): Promise; dispose(): void; } /** Oldest-retained first, matching `maestro worktrees list`'s ordering. */ function sorted(items: RetainedWorktree[]): RetainedWorktree[] { return [...items].sort( (a, b) => Date.parse(a.retained_at) - Date.parse(b.retained_at), ); } export function createWorktreesView(deps: WorktreesDeps): WorktreesViewHandle { const el = document.createElement("section"); el.className = "worktrees"; el.innerHTML = `

retained worktrees

Kept when a session ended with unsaved work. Reclaiming removes the directory only; branches are never deleted.

`; const listHost = el.querySelector(".wt-list")!; const confirmEl = el.querySelector(".wt-confirm")!; let pendingId: string | null = null; const render = (items: RetainedWorktree[]) => { listHost.replaceChildren(); if (items.length === 0) { // A drained store is the normal state — say so plainly, not like // something went wrong. const empty = document.createElement("div"); empty.className = "empty"; empty.innerHTML = `
no retained worktrees

Nothing is being held. Worktrees kept after a force-kill or an unsaved exit show up here.

`; listHost.appendChild(empty); return; } const home = deps.homeDir(); for (const w of sorted(items)) { listHost.appendChild(renderRow(w, home)); } }; const renderRow = (w: RetainedWorktree, home: string | undefined): HTMLElement => { const row = document.createElement("div"); row.className = "wt-row"; const clear = formatInTheWay(w.in_the_way ?? {}) === "no unsaved work"; row.innerHTML = ` ${escapeHtml(w.branch)} ${abbreviateFolderName(abbreviateCwd(w.path, home))} ${escapeHtml(reasonLabel(w.reason))} ${escapeHtml(formatInTheWay(w.in_the_way ?? {}))} `; row.querySelector(".wt-reclaim-btn")!.addEventListener("click", () => { showConfirm(w); }); return row; }; const showConfirm = (w: RetainedWorktree) => { pendingId = w.id; // Mirrors the TUI's ReclaimConfirm line for line: what goes away, what is // in the way right now, and that the branch survives reclaim. confirmEl.innerHTML = `

Reclaim worktree ${escapeHtml(w.branch)}?

Currently in the way: ${escapeHtml(formatInTheWay(w.in_the_way ?? {}))}

The directory is removed; the branch is never deleted.

`; confirmEl.hidden = false; confirmEl.querySelector(".wt-confirm-go")!.focus(); }; const hideConfirm = () => { pendingId = null; confirmEl.hidden = true; confirmEl.replaceChildren(); }; confirmEl.addEventListener("click", async (e) => { const target = e.target as HTMLElement; if (target.closest(".wt-confirm-cancel")) { hideConfirm(); return; } if (!target.closest(".wt-confirm-go") || !pendingId) return; const id = pendingId; hideConfirm(); try { await deps.reclaim(id); } catch (err) { alert(`Failed to reclaim: ${(err as Error).message}`); } await refresh(); }); const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && !confirmEl.hidden) hideConfirm(); }; document.addEventListener("keydown", onKey); const refresh = async () => { try { render(await deps.load()); } catch (err) { listHost.replaceChildren(); const fail = document.createElement("div"); fail.className = "empty"; fail.innerHTML = `
Couldn't load retained worktrees

`; fail.querySelector(".empty-hint")!.textContent = (err as Error).message; listHost.appendChild(fail); } }; return { el, refresh, dispose() { document.removeEventListener("keydown", onKey); el.remove(); }, }; }