diff --git a/web/src/spawnForm.ts b/web/src/spawnForm.ts index c18b2db..714796b 100644 --- a/web/src/spawnForm.ts +++ b/web/src/spawnForm.ts @@ -147,33 +147,38 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle {
- + +
@@ -192,6 +197,9 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { const cwdRawWrap = form.querySelector(".cwd-raw-wrap") as HTMLElement; const cwdRaw = form.querySelector(".cwd-raw") as HTMLInputElement; const wtEnabled = form.querySelector("input[name=worktree_enabled]") as HTMLInputElement; + const wtToggle = form.querySelector(".wt-toggle") as HTMLButtonElement; + const wtCollapsible = form.querySelector(".wt-collapsible") as HTMLElement; + const wtSummary = form.querySelector(".wt-summary") as HTMLElement; const wtHint = form.querySelector(".wt-hint") as HTMLElement; const wtBody = form.querySelector(".wt-body") as HTMLElement; const wtRepo = form.querySelector(".wt-repo") as HTMLElement; @@ -464,6 +472,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { // off. Turning it back on by hand still tries (and fails with git's // own message), which is deliberate (#91 non-goal). wtEnabled.checked = !project.is_worktree; + updateWtSummary(); if (project.is_worktree) { inferredBaseRepo = null; wtBody.hidden = true; @@ -569,6 +578,18 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { return p; } + // ---- worktree: disclosure (#93), reuses the sidebar's folder-header --- + let advancedCollapsed = true; + function setAdvancedCollapsed(collapsed: boolean): void { + advancedCollapsed = collapsed; + wtToggle.setAttribute("aria-expanded", String(!collapsed)); + wtCollapsible.hidden = collapsed; + } + function updateWtSummary(): void { + wtSummary.textContent = wtEnabled.checked ? "creates a worktree" : "no worktree"; + } + wtToggle.addEventListener("click", () => setAdvancedCollapsed(!advancedCollapsed)); + // ---- worktree (branch-only; base_repo inferred via scan or /gitroot) -- // The project scan already reports repo membership, so a picked project // resolves its base repo locally (#61); only the free-text escape hatch @@ -649,6 +670,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { // Publish the untick too, or the next render seeds the box checked with // no inferred repo and submitting fails. wtEnabled.checked = false; + updateWtSummary(); emit(); wtBody.hidden = true; wtHint.hidden = false; @@ -722,6 +744,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { branchSelect.addEventListener("change", () => emit()); wtEnabled.addEventListener("change", () => { + updateWtSummary(); emit(); if (wtEnabled.checked) { resolveBaseRepo(); @@ -735,6 +758,7 @@ export function createSpawnForm(opts: SpawnFormOptions): SpawnFormHandle { // The toggle defaults ON (#61): the operator almost always wants a // worktree, and every non-repo target unticks it via the gitroot probe. wtEnabled.checked = prefill.worktreeEnabled ?? true; + updateWtSummary(); if (prefill.worktreeBranch) branchInput.value = prefill.worktreeBranch; setBranchMode(branchMode); diff --git a/web/src/spawnFormWorktree.test.ts b/web/src/spawnFormWorktree.test.ts index bb04b96..ff5ca1e 100644 --- a/web/src/spawnFormWorktree.test.ts +++ b/web/src/spawnFormWorktree.test.ts @@ -180,6 +180,59 @@ describe("spawnForm worktree defaults and branch picker (#61)", () => { expect(modes.at(-1)).toBe("existing"); }); + it("collapses the advanced (worktree) section by default (#93)", async () => { + const form = await mount(); + const toggle = form.querySelector(".wt-toggle")!; + expect(toggle.getAttribute("aria-expanded")).toBe("false"); + expect(form.querySelector(".wt-collapsible")!.hidden).toBe(true); + }); + + it("states in the collapsed header that a worktree will be created by default (#93)", async () => { + const form = await mount(); + expect(form.querySelector(".wt-summary")!.textContent).toMatch(/creates? a worktree/i); + }); + + it("states in the collapsed header that no worktree will be created for a worktree row (#91, #93)", async () => { + urls = stubFetch({ + projects: [ + { + path: "/home/user/wt-91", + name: "wt-91", + is_worktree: true, + base_repo: "/home/user/maestro", + last_activity: "2026-08-20T10:00:00Z", + }, + ], + }); + handle = createSpawnForm({ onSuccess() {} }); + document.body.appendChild(handle.el); + for (let i = 0; i < 40; i++) await Promise.resolve(); + + handle.el + .querySelectorAll(".project-row") + .forEach((row) => row.dataset.cwd === "/home/user/wt-91" && row.click()); + await drain(); + + expect(handle.el.querySelector(".wt-summary")!.textContent).toMatch(/no worktree/i); + // Still collapsed — picking a project must not force it open. + expect(handle.el.querySelector(".wt-toggle")!.getAttribute("aria-expanded")).toBe( + "false", + ); + }); + + it("expanding the advanced section does not reorder the fields above it (#93)", async () => { + const form = await mount(); + const before = [...form.querySelectorAll(".field")]; + + form.querySelector(".wt-toggle")!.click(); + + expect(form.querySelector(".wt-toggle")!.getAttribute("aria-expanded")).toBe( + "true", + ); + expect(form.querySelector(".wt-collapsible")!.hidden).toBe(false); + expect([...form.querySelectorAll(".field")]).toStrictEqual(before); + }); + it("does not repopulate a destroyed form when the branch list lands late", async () => { const gate: { release?: () => void } = {}; urls = stubFetch({ gateBranches: (r) => (gate.release = r) }); diff --git a/web/src/styles.css b/web/src/styles.css index afe24c3..5aef4a8 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1286,18 +1286,33 @@ textarea { .worktree { border: 1px solid var(--border); border-radius: var(--r-2); - padding: var(--sp-3); + padding: 0 var(--sp-3); margin: 0; display: flex; flex-direction: column; - gap: var(--sp-3); background: color-mix(in oklab, var(--surface-2) 50%, transparent); } -.worktree legend { - padding: 0 6px; - color: var(--ink); - font-size: var(--fs-13); +.worktree .wt-toggle { + margin: 0 calc(var(--sp-3) * -1); + padding-left: var(--sp-3); + width: calc(100% + var(--sp-3) * 2); +} + +.wt-summary { + color: var(--muted); + font-size: var(--fs-12); +} + +.wt-collapsible { + display: flex; + flex-direction: column; + gap: var(--sp-3); + padding-bottom: var(--sp-3); +} + +.wt-collapsible[hidden] { + display: none; } .form-error {