docs: add worktree overview modal spec and plan
This commit is contained in:
1423
docs/superpowers/plans/2026-05-19-worktree-overview-modal.md
Normal file
1423
docs/superpowers/plans/2026-05-19-worktree-overview-modal.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
|||||||
|
# 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<WorktreesOverviewModalViewModel>` factory analogous to existing editor patterns.
|
||||||
|
|
||||||
|
## SignalR Contract
|
||||||
|
|
||||||
|
### New `WorkerHub` methods
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Task<CleanupResult> 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<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`
|
||||||
Reference in New Issue
Block a user