feat(dev): lab — parked-work view [worktrees 3/3] #136

Closed
opened 2026-06-08 00:30:20 +02:00 by dominik.polakovics · 1 comment

Parent

Cloonar/nixos#133 (full settled design).

What to build

A per-project Parked view so you can see and clean up parked worktrees/branches.

  • A collapsible <details> "Parked" strip on each project card (same idiom as the ⋯ menu), collapsed by default showing a cheap count (git for-each-ref refs/heads/lab refs/heads/afk + git worktree list). Keep it off the ~4s fragment poll.
  • A dedicated lazy endpoint computed on expand: per entry — branch name, dirty ● / clean ○, commits ahead of base, age, best-effort PR badge, worktree path (copyable). Shows both lab/ and afk/, tagged by kind.
  • Per-entry Discard: remove the worktree (if any) + delete the branch, behind the two-step confirm used by "Stop all"; show ahead/unpushed state so unpushed commits aren't nuked blindly. No Resume.
  • Note: discarding a parked afk/<N> deletes its claim branch → the issue becomes claimable again (a manual requeue; ADR-0013).

Mobile-first (single column, ~44px tap targets). Inline JS per ADR-0004 — verify with the ephemeral jsdom approach (no committed harness).

Acceptance criteria

  • Each project card shows a collapsed Parked count without adding cost to the ~4s poll.
  • Expanding lists both lab/ and afk/ parked entries with dirty/clean, ahead, age, PR badge, and worktree path.
  • Discard removes worktree + branch behind a two-step confirm and warns on unpushed commits.
  • Verified mobile-first; inline JS verified via jsdom.

Blocked by

Cloonar/nixos#134

## Parent Cloonar/nixos#133 (full settled design). ## What to build A per-project **Parked** view so you can see and clean up parked worktrees/branches. - A collapsible `<details>` "Parked" strip on each project card (same idiom as the ⋯ menu), collapsed by default showing a cheap count (`git for-each-ref refs/heads/lab refs/heads/afk` + `git worktree list`). Keep it **off** the ~4s fragment poll. - A dedicated **lazy** endpoint computed on expand: per entry — branch name, **dirty ● / clean ○**, commits **ahead** of base, age, best-effort **PR** badge, worktree path (copyable). Shows both `lab/` and `afk/`, tagged by kind. - Per-entry **Discard:** remove the worktree (if any) + delete the branch, behind the two-step confirm used by "Stop all"; show ahead/unpushed state so unpushed commits aren't nuked blindly. **No Resume.** - Note: discarding a parked `afk/<N>` deletes its claim branch → the issue becomes claimable again (a manual requeue; ADR-0013). Mobile-first (single column, ~44px tap targets). Inline JS per ADR-0004 — verify with the ephemeral jsdom approach (no committed harness). ## Acceptance criteria - [ ] Each project card shows a collapsed Parked count without adding cost to the ~4s poll. - [ ] Expanding lists both `lab/` and `afk/` parked entries with dirty/clean, ahead, age, PR badge, and worktree path. - [ ] Discard removes worktree + branch behind a two-step confirm and warns on unpushed commits. - [ ] Verified mobile-first; inline JS verified via jsdom. ## Blocked by Cloonar/nixos#134
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Add the per-project Parked view (worktrees slice 3/3): a collapsible strip on each project card listing parked lab/ and afk/ branches/worktrees, with a per-entry Discard.

Current behavior:
ADR-0017 slices 1–2 landed: every instance runs in its own worktree off origin/<default>, and one guarded-teardown rule keeps dirty/unmerged work while GC-ing clean/merged worktrees and branches. So dirty or clean-but-unmerged lab/<label> and afk/<N> branches/worktrees survive teardown ("parked") — a failed AFK run keeps its afk/<N> claim branch (ADR-0013), a dirty manual Stop keeps its worktree — but there is no UI to see or clean up that parked work. The reconciliation code already computes the exact parked set: managed (lab/+afk/) branches and worktrees minus those a live session owns (gatherRefs / ownedBranches).

Desired behavior:
Each project card gains a collapsible Parked strip (the native <details>/<summary> idiom the ⋯ menu uses — mobile-first, ~44px targets):

  • Collapsed (default): a cheap parked count from local-only git (for-each-ref over lab/+afk/ plus worktree list) — no network. Rendered only when count > 0. Cheap enough to live on the normal snapshot/poll path.
  • Expanded (lazy): per-entry detail fetched on demand from a dedicated per-project endpoint, separate from the /fragment poll, so the expensive work never runs on the ~4s poll. Each entry: branch name; kind (manual lab/ vs afk/<N>); dirty ● / clean ○ (when it has a worktree); commits ahead of origin/<default>; age; best-effort PR badge; unpushed warning; copyable worktree path (when one exists).
  • Per-entry Discard: behind the existing two-step data-confirm idiom. Discard is unguarded — force-removes the worktree (if any) and force-deletes the branch regardless of dirty/merged state — so it must NOT route through the guarded teardown. The entry surfaces ahead/unpushed state so the user sees what they're nuking. No Resume.
  • Discarding an afk/<N> entry deletes its claim branch, so the issue becomes claimable again (manual requeue; ADR-0013).

Settled design decisions (from triage grilling — do not re-litigate):

  1. Lazy body survives the poll via a client-owned subtree. The keyed-DOM morph reconciling #live recurses into every node, so a client-injected lazy body is wiped on the next poll. Extend the morph's existing "client-owned" concept from attributes (value/checked/selected/open) to subtrees: a node marked data-static has its own attributes patched but its children left untouched. The parked body carries data-static; fetched once on expand, re-fetched on each re-open. Chosen over re-fetching the body every poll.
  2. Refresh after Discard: the Discard action returns the refreshed view so the open strip updates at once; the collapsed count updates via the normal morph.
  3. Transient count/list skew is accepted. Body is frozen at expand-time while the count is live, so an external change can briefly make them disagree until re-open. Self-heals on re-open — do not add a count-change observer.
  4. Fail loud: a failing lazy endpoint shows the error in the strip, not a silent empty list.
  5. PR badge reuses Tracker.ListPulls (match branch against PR heads client-side, as the reaper does). It's a tea network call ⇒ lazy endpoint only, never the snapshot/count. Never-pushed lab/ branches never match (no badge) — correct.
  6. Enumeration reuses the reconciliation derivation (gatherRefs/ownedBranches): parked = managed branches/worktrees minus live-owned. Don't re-derive independently.

Key interfaces:

  • Git seam — add: commits-ahead-of-base (rev-list --count origin/<default>..<branch>), last-commit age, unpushed count (rev-list --count origin/<branch>..<branch>; for a never-pushed lab/ branch this equals ahead). Reuse Branches, Worktrees, WorktreeDirty, RemoveWorktree(force), DeleteBranch(-D).
  • Tracker.ListPulls(dir) — reused unchanged for the PR badge.
  • Reconciliation view (gatherRefs → managed worktrees, branch→worktree index, owned set) — reused to enumerate the parked set.
  • Guarded teardown (teardownGuarded/decideTeardown) — explicitly bypassed by Discard.
  • The #live morph (morphChildren/patchNode) — gains the data-static client-owned-subtree rule.
  • New per-project lazy route serving the parked-body fragment, distinct from /fragment; new Discard route (POST) with the branch passed as a form field (branch names contain /).

Acceptance criteria:

  • A card with parked work shows a collapsed Parked count; a project with none shows no strip; the count adds no network call to the poll.
  • Expanding fetches a lazy per-project endpoint (off the /fragment poll) listing both lab/ and afk/ entries with dirty/clean (when worktree present), commits-ahead, age, best-effort PR badge, unpushed warning, copyable worktree path.
  • The expanded body survives ≥1 background poll without being wiped or snapping shut (the data-static rule), verified via the ephemeral jsdom harness (ADR-0004 — no committed JS).
  • Per-entry Discard force-removes the worktree (if any) and deletes the branch behind the two-step confirm, regardless of dirty/merged, and does not go through the guarded teardown.
  • Discarding an afk/<N> entry removes the claim branch so the issue re-enters the claimable set.
  • A failing lazy endpoint shows an error in the strip, not an empty list.
  • Go unit tests cover parked enumeration (live-owned exclusion; lab/+afk/ entries; bare branch vs branch-with-worktree), the new git helpers, and Discard-is-unguarded; the template is asserted to stamp data-static on the parked body. go test ./..., go vet ./..., go build ./... pass locally (pre-commit is eval-only — it does not build/run Go).
  • Mobile-first: single column, ~44px tap targets, reusing the .menu/.btn idioms.
  • default.nix version date string bumped.

Out of scope:

  • Resume / durable-workspace re-entry.
  • Any change to the guarded-teardown rule, the AFK reaper, or the startup/runtime sweeps — Discard is a new, separate, unguarded action.
  • Merge-back affordances (work lands via the normal PR flow).
  • A new ADR — ADR-0017 already documents this as slice 3; add a "Parked view" entry to CONTEXT.md instead.
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Add the per-project **Parked** view (worktrees slice 3/3): a collapsible strip on each project card listing parked `lab/` and `afk/` branches/worktrees, with a per-entry Discard. **Current behavior:** ADR-0017 slices 1–2 landed: every instance runs in its own worktree off `origin/<default>`, and one guarded-teardown rule keeps dirty/unmerged work while GC-ing clean/merged worktrees and branches. So dirty or clean-but-unmerged `lab/<label>` and `afk/<N>` branches/worktrees survive teardown ("parked") — a failed AFK run keeps its `afk/<N>` claim branch (ADR-0013), a dirty manual Stop keeps its worktree — but there is **no UI to see or clean up** that parked work. The reconciliation code already computes the exact parked set: managed (`lab/`+`afk/`) branches and worktrees minus those a live session owns (`gatherRefs` / `ownedBranches`). **Desired behavior:** Each project card gains a collapsible **Parked** strip (the native `<details>`/`<summary>` idiom the ⋯ menu uses — mobile-first, ~44px targets): - **Collapsed (default):** a cheap parked **count** from local-only git (`for-each-ref` over `lab/`+`afk/` plus `worktree list`) — no network. Rendered only when count > 0. Cheap enough to live on the normal snapshot/poll path. - **Expanded (lazy):** per-entry detail fetched on demand from a dedicated per-project endpoint, separate from the `/fragment` poll, so the expensive work never runs on the ~4s poll. Each entry: branch name; kind (manual `lab/` vs `afk/<N>`); **dirty ● / clean ○** (when it has a worktree); commits **ahead** of `origin/<default>`; **age**; best-effort **PR** badge; **unpushed** warning; copyable **worktree path** (when one exists). - **Per-entry Discard:** behind the existing two-step `data-confirm` idiom. Discard is **unguarded** — force-removes the worktree (if any) and force-deletes the branch *regardless* of dirty/merged state — so it must NOT route through the guarded teardown. The entry surfaces ahead/unpushed state so the user sees what they're nuking. No Resume. - Discarding an `afk/<N>` entry deletes its claim branch, so the issue becomes claimable again (manual requeue; ADR-0013). **Settled design decisions (from triage grilling — do not re-litigate):** 1. **Lazy body survives the poll via a client-owned subtree.** The keyed-DOM morph reconciling `#live` recurses into every node, so a client-injected lazy body is wiped on the next poll. Extend the morph's existing "client-owned" concept from attributes (`value`/`checked`/`selected`/`open`) to subtrees: a node marked `data-static` has its own attributes patched but its **children left untouched**. The parked body carries `data-static`; fetched once on expand, re-fetched on each re-open. Chosen over re-fetching the body every poll. 2. **Refresh after Discard:** the Discard action returns the refreshed view so the open strip updates at once; the collapsed count updates via the normal morph. 3. **Transient count/list skew is accepted.** Body is frozen at expand-time while the count is live, so an external change can briefly make them disagree until re-open. Self-heals on re-open — do **not** add a count-change observer. 4. **Fail loud:** a failing lazy endpoint shows the error in the strip, not a silent empty list. 5. **PR badge reuses `Tracker.ListPulls`** (match branch against PR heads client-side, as the reaper does). It's a `tea` network call ⇒ lazy endpoint only, never the snapshot/count. Never-pushed `lab/` branches never match (no badge) — correct. 6. **Enumeration reuses the reconciliation derivation** (`gatherRefs`/`ownedBranches`): parked = managed branches/worktrees minus live-owned. Don't re-derive independently. **Key interfaces:** - **`Git` seam** — add: commits-ahead-of-base (`rev-list --count origin/<default>..<branch>`), last-commit age, unpushed count (`rev-list --count origin/<branch>..<branch>`; for a never-pushed `lab/` branch this equals ahead). Reuse `Branches`, `Worktrees`, `WorktreeDirty`, `RemoveWorktree`(force), `DeleteBranch`(-D). - **`Tracker.ListPulls(dir)`** — reused unchanged for the PR badge. - **Reconciliation view** (`gatherRefs` → managed worktrees, branch→worktree index, owned set) — reused to enumerate the parked set. - **Guarded teardown** (`teardownGuarded`/`decideTeardown`) — explicitly **bypassed** by Discard. - **The `#live` morph** (`morphChildren`/`patchNode`) — gains the `data-static` client-owned-subtree rule. - **New per-project lazy route** serving the parked-body fragment, distinct from `/fragment`; **new Discard route** (POST) with the branch passed as a **form field** (branch names contain `/`). **Acceptance criteria:** - [ ] A card with parked work shows a collapsed **Parked** count; a project with none shows no strip; the count adds no network call to the poll. - [ ] Expanding fetches a lazy per-project endpoint (off the `/fragment` poll) listing both `lab/` and `afk/` entries with dirty/clean (when worktree present), commits-ahead, age, best-effort PR badge, unpushed warning, copyable worktree path. - [ ] The expanded body survives ≥1 background poll without being wiped or snapping shut (the `data-static` rule), verified via the ephemeral jsdom harness (ADR-0004 — no committed JS). - [ ] Per-entry **Discard** force-removes the worktree (if any) and deletes the branch behind the two-step confirm, **regardless** of dirty/merged, and does not go through the guarded teardown. - [ ] Discarding an `afk/<N>` entry removes the claim branch so the issue re-enters the claimable set. - [ ] A failing lazy endpoint shows an error in the strip, not an empty list. - [ ] Go unit tests cover parked enumeration (live-owned exclusion; lab/+afk/ entries; bare branch vs branch-with-worktree), the new git helpers, and Discard-is-unguarded; the template is asserted to stamp `data-static` on the parked body. `go test ./...`, `go vet ./...`, `go build ./...` pass locally (pre-commit is eval-only — it does not build/run Go). - [ ] Mobile-first: single column, ~44px tap targets, reusing the `.menu`/`.btn` idioms. - [ ] `default.nix` version date string bumped. **Out of scope:** - **Resume** / durable-workspace re-entry. - Any change to the guarded-teardown rule, the AFK reaper, or the startup/runtime sweeps — Discard is a new, separate, unguarded action. - Merge-back affordances (work lands via the normal PR flow). - A new ADR — ADR-0017 already documents this as slice 3; add a "Parked view" entry to CONTEXT.md instead.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos#136
No description provided.