docs: add worktree overview modal spec and plan

This commit is contained in:
mika kuns
2026-05-19 09:27:19 +02:00
parent 5da69ee6aa
commit b944597af4
2 changed files with 1629 additions and 0 deletions

View File

@@ -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`