Files
ClaudeDo/docs/superpowers/specs/2026-05-19-worktree-overview-modal-design.md
2026-05-19 09:27:19 +02:00

9.1 KiB
Raw Blame History

Worktree Overview Modal — Design

Status: Approved Date: 2026-05-19

Problem

Worktree management is becoming hard to oversee. The current UI only exposes per-task worktree actions (merge / keep / discard) from TaskDetailView, plus two global maintenance buttons (CleanupFinishedWorktrees, ResetAllWorktrees). There is no view that shows all existing worktrees at a glance with their state, age, branch, and diff stat. Stale or "phantom" worktrees (DB row but missing directory, or vice versa) have no targeted recovery path.

Goals

  • A modal that lists every worktree row from the DB, joined with task + list metadata.
  • Two entry points: filtered to one list (List context menu), and global grouped by list (Help menu).
  • Quick per-row actions hidden behind a right-click context menu.
  • Targeted force-remove for stuck / phantom worktrees.
  • Manual refresh only; no live SignalR subscription needed.

Non-Goals

  • No auto-refresh / live updates from SignalR events.
  • No UI tests (the project has none for the Ui project).
  • No changes to WorktreeManager, TaskRunner, or the existing per-worktree file-tree modal (WorktreeModalView) — it gets reused as the "Show diff" target.

UI

New view pair

WorktreesOverviewModalView + WorktreesOverviewModalViewModel, parallel to existing WorktreeModalView (which shows the file tree inside one worktree).

Layout

┌─ Worktrees [List "Foo"]  or  Worktrees (all) ───────────────┐
│ [ Refresh ]  [ Cleanup finished ]                            │
│                                                              │
│ ▼ List Foo                       (global mode only)          │
│   Title          Branch       State    +/-    Age            │
│   Fix login bug  claudedo/ab… Active   +42-7  3h ago         │
│   Add API …      claudedo/cd… Merged   +8 -0  1d ago         │
│ ▼ List Bar                                                   │
│   …                                                          │
└──────────────────────────────────────────────────────────────┘
  • DataGrid (or ItemsControl with Grid template) for rows.
  • List-filtered mode: no group headers, just the table.
  • Global mode: Expander per list with list name as header (default expanded).
  • State as a colored badge — new WorktreeStateColorConverter analogous to StatusColorConverter:
    • Active=Blue, Merged=Green, Discarded=Gray, Kept=Orange.
  • Right-click on a row opens a MenuFlyout with all actions.
  • Phantom rows (PathExistsOnDisk == false) get a small warning icon in the Path tooltip area.

Default sort

State (Active first), then CreatedAt descending. Same inside each list group in global mode.

Per-row context menu

Item Enabled when Behavior
Show diff always Opens existing WorktreeModalView with WorktreePath set
Open in Explorer PathExistsOnDisk == true Process.Start("explorer.exe", path)
Jump to task always Closes modal, selects list + task in main window
Merge State == Active Calls existing MergeTask hub method
Discard State == Active SetWorktreeState(taskId, Discarded)
Keep State == Active SetWorktreeState(taskId, Kept)
Copy branch always Clipboard
Copy path always Clipboard
—————— (separator)
Force remove Task.Status != Running Confirmation dialog → ForceRemoveWorktree(taskId) (red label)

Bulk buttons (toolbar)

  • Refresh — re-runs GetWorktreesOverview.
  • Cleanup finishedCleanupFinishedWorktrees(listId); in list-filtered mode acts on that list, in global mode on all.

Entry points

  • List context menu → "Worktrees anzeigen…" → opens modal in filtered mode (listId = the list).
  • Help menu → "Worktrees" → opens modal in global mode (listId = null).

MainWindowViewModel gets OpenWorktreesOverviewCommand(listId) and OpenWorktreesOverviewGlobalCommand(), both using a DI Func<WorktreesOverviewModalViewModel> factory analogous to existing editor patterns.

SignalR Contract

New WorkerHub methods

Task<IReadOnlyList<WorktreeOverviewDto>> GetWorktreesOverview(string? listId);
Task<bool> SetWorktreeState(string taskId, WorktreeState newState);
Task<ForceRemoveResultDto> ForceRemoveWorktree(string taskId);

CleanupFinishedWorktrees already exists — extend its signature to accept an optional listId:

Task<CleanupResult> CleanupFinishedWorktrees(string? listId);   // was: ()

MergeTask is reused unchanged.

DTOs

public sealed record WorktreeOverviewDto(
    string TaskId,
    string TaskTitle,
    TaskStatus TaskStatus,
    string ListId,
    string ListName,
    string Path,
    string BranchName,
    WorktreeState State,
    string? DiffStat,
    DateTime CreatedAt,
    bool PathExistsOnDisk);

public sealed record ForceRemoveResultDto(bool Removed, string? Reason);

Broadcasts

After successful SetWorktreeState and ForceRemoveWorktree, fire HubBroadcaster.WorktreeUpdated(taskId) so TaskDetailView (if open) refreshes. CleanupFinishedWorktrees already broadcasts; keep behavior, optionally batch.

WorkerClient (UI)

Add wrapper methods for the four new/changed hub calls.

Backend Changes

WorktreeMaintenanceService

public sealed record ForceRemoveResult(bool Removed, string? Reason);

public Task<IReadOnlyList<WorktreeOverviewRow>> GetOverviewAsync(string? listId, CancellationToken ct);
public Task<CleanupResult> CleanupFinishedAsync(string? listId, CancellationToken ct);   // signature extended
public Task<ForceRemoveResult> ForceRemoveAsync(string taskId, CancellationToken ct);
  • GetOverviewAsync — joins worktrees × tasks × lists (AsNoTracking), maps to DTO including PathExistsOnDisk = Directory.Exists(path).
  • CleanupFinishedAsync(listId) — same join as today but also filters t.ListId == listId when not null.
  • ForceRemoveAsync — refactors existing TryRemoveAsync(row, force: true, …) into a single-row entry point shared with ResetAllAsync. Refuses when the task is currently Running, returning ForceRemoveResult(false, "task is currently running"). Otherwise removes the worktree directory, prunes, deletes the branch, deletes the DB row.

WorktreeRepository

SetStateAsync(string taskId, WorktreeState newState, CancellationToken ct) already documented in CLAUDE.md. If absent, add it; if present, just expose it via the hub.

Unchanged

WorktreeManager, TaskRunner, WorktreeModalView, all existing merge / cleanup flows.

Data Flow

  1. User opens modal → WorkerClient.GetWorktreesOverviewAsync(listId) → bind rows.
  2. Refresh button → same call.
  3. Per-row action → corresponding hub call → on success, update the affected row locally (no full reload).
  4. Bulk Cleanup → hub call → full reload.

Force-Remove Semantics

Initial state Result
Active, task not Running Worktree dir removed, branch deleted, DB row deleted. Task remains in current status (Done/Failed/Idle).
Active, task Running Refused with reason "task is currently running".
Merged / Discarded / Kept Same removal path.
Phantom (dir missing) DB row deleted, branch best-effort deleted.

Testing

New tests in tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs (real SQLite, real git):

  1. GetOverviewAsync_returns_all_when_listId_null
  2. GetOverviewAsync_filters_by_listId
  3. GetOverviewAsync_flags_PathExistsOnDisk_false_for_phantom_row
  4. CleanupFinishedAsync_filters_by_listId
  5. ForceRemoveAsync_removes_active_worktree (happy path incl. branch delete)
  6. ForceRemoveAsync_blocked_when_task_running
  7. ForceRemoveAsync_removes_phantom_row

UI verification (manual):

  • Open from list context menu → only that list's rows.
  • Open from Help menu → all lists grouped, default expanded.
  • Force-remove an Active worktree → row vanishes, DB row gone, branch deleted.
  • Force-remove while task Running → toast / dialog with reason, row unchanged.
  • Cleanup finished in filtered mode → only finished rows of the selected list disappear.
  • "Show diff" reuses existing WorktreeModalView.

Files Touched

New:

  • src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs
  • src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml
  • src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml.cs
  • src/ClaudeDo.Ui/Converters/WorktreeStateColorConverter.cs
  • src/ClaudeDo.Worker/Worktrees/WorktreeOverviewDto.cs (or extend an existing DTOs file)

Modified:

  • src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs
  • src/ClaudeDo.Worker/Hub/WorkerHub.cs
  • src/ClaudeDo.Ui/Services/WorkerClient.cs
  • src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs
  • src/ClaudeDo.Ui/Views/MainWindow.axaml (Help menu entry, list context menu entry)
  • src/ClaudeDo.App/Program.cs (DI registration of new VM)
  • tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs