# 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 finished** — `CleanupFinishedWorktrees(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` factory analogous to existing editor patterns. ## SignalR Contract ### New `WorkerHub` methods ```csharp Task> GetWorktreesOverview(string? listId); Task SetWorktreeState(string taskId, WorktreeState newState); Task ForceRemoveWorktree(string taskId); ``` `CleanupFinishedWorktrees` already exists — extend its signature to accept an optional `listId`: ```csharp Task CleanupFinishedWorktrees(string? listId); // was: () ``` `MergeTask` is reused unchanged. ### DTOs ```csharp 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` ```csharp public sealed record ForceRemoveResult(bool Removed, string? Reason); public Task> GetOverviewAsync(string? listId, CancellationToken ct); public Task CleanupFinishedAsync(string? listId, CancellationToken ct); // signature extended public Task 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`