refactor(diff): single DiffViewer replaces DiffModal + WorktreeModal + PlanningDiff

This commit is contained in:
Mika Kuns
2026-06-23 09:30:37 +02:00
parent 4022bd7197
commit 167d2fec6a
28 changed files with 923 additions and 1120 deletions

View File

@@ -0,0 +1,111 @@
# Phase 5 — DiffViewer (A1 + B2)
Date: 2026-06-23
Umbrella: `docs/superpowers/plans/2026-06-19-feature-unification-plan.md`
Design: `docs/superpowers/specs/2026-06-19-feature-unification-design.md` (A1, B2)
## Goal
One diff component replaces the three parallel read-only diff windows:
`DiffModalViewModel`/View, `WorktreeModalViewModel`/View, `PlanningDiffViewModel`/View.
**Merge editor (`ConflictResolverViewModel`) is untouched** — per the design's hard
decision; the viewer only *opens* it on conflict via the existing Merge flow.
All three are already master-detail: **left nav pane + right `DiffLinesView`**. They
differ only in left-pane content, chrome, and data source — so they collapse into one
shell with a source mode.
## Decisions (Mika, 2026-06-23)
- **File nav = file-tree** (folder-grouped), not a flat list. Port `WorktreeModal`'s tree
+ the Avalonia-12 `TreeView.SelectionChanged` workaround. Carry per-file status + +adds/
dels into the tree rows (from the parsed `DiffFileViewModel`).
- Planning keeps its **subtask-list + combined-mode toggle**; the branch source keeps its
**Merge** button.
## Target
### Shared types → `ViewModels/Modals/DiffModels.cs` (new, same namespace)
Move out of the to-be-deleted VMs so `UnifiedDiffParser`/`DiffLinesView` keep compiling:
`DiffLineKind`, `DiffFileStatus`, `DiffLineViewModel`, `DiffFileViewModel` (from
`DiffModalViewModel.cs`), `SubtaskDiffRow` (from `PlanningDiffViewModel.cs`). Add new
`DiffTreeNodeViewModel` (dir/file node; file leaves hold their `DiffFileViewModel`).
### `DiffViewerViewModel` (`ViewModels/Modals/DiffViewerViewModel.cs`, new)
ctor `(GitService git, IWorkerClient worker)`. A `DiffViewerMode { Files, Planning }`.
- **File sources** (replaces DiffModal + WorktreeModal): config props `WorktreePath`,
`BaseRef`, `HeadCommit`, `FromCommitRange`, `TaskId`, `TaskTitle` + `ShowMergeModal`/
`ResolveMergeVm` delegates. `LoadAsync` pulls the whole diff via GitService
(`GetCommitRangeDiffAsync` | `GetBranchDiffAsync` | `GetDiffAsync`), parses with
`UnifiedDiffParser.Parse`, builds `FileTree`. `SelectedNode` (leaf) → `SelectedFile`
(header + binary/empty placeholders + `Lines`). Commit-range null-guard → "no longer
available" (preserve DiffModal behavior). `MergeCommand` (CanMerge = TaskId +
delegates) opens the MergeModal, closes on merged/routed (verbatim from DiffModal).
- **Planning source** (replaces PlanningDiff): config `PlanningTaskId`, `TargetBranch`.
`LoadAsync` pulls `GetPlanningAggregateAsync``Subtasks`; `SelectedSubtask`
`DisplayedDiff`; `IsCombinedMode` toggle → `BuildPlanningIntegrationBranchAsync`
(success → combined diff; conflict → `CombinedWarning` with subtask + file count;
null → hub-error warning). `DisplayedDiff` → flattened `DiffLines` (right pane).
- Shared: `StatusMessage`, `CloseAction`, `CloseCommand`.
### `DiffViewerView` (`Views/Modals/DiffViewerView.axaml` + `.cs`, new)
`ModalShell`-based window. Left pane: `TreeView` (Files mode) or subtask `ListBox`
(Planning mode), toggled by mode. Right pane: the DiffModal file pane (header + binary/
empty/no-changes placeholders + `DiffLinesView Lines="SelectedFile.Lines"`) in Files mode,
or `DiffLinesView Lines="DiffLines"` in Planning mode. Toolbar: combined toggle + warning
+ loading (Planning). Footer: Merge button (Files mode, CanMerge). Code-behind: `CloseAction`,
the `TreeView.SelectionChanged``SelectedNode` workaround, dir-row tap-to-expand.
### Re-point the 3 doors → one viewer
- **`MergeSectionViewModel`**: `OpenDiffAsync` builds a Files-mode `DiffViewerViewModel`
(+ ShowMergeModal/ResolveMergeVm) and calls a single `ShowDiffViewer` delegate;
`ReviewCombinedDiffAsync` builds a Planning-mode one and calls the *same* delegate.
Replaces `ShowDiffModal` + `ShowPlanningDiffModal` with one `Func<DiffViewerViewModel,Task>
ShowDiffViewer`; keeps `ShowMergeModal`. (Resolve the VM via `_services`.)
- **`DetailsIslandView.axaml.cs`**: replace the two `ShowDiffModal`/`ShowPlanningDiffModal`
wirings (→ `DiffModalView`/`PlanningDiffView`) with one `ShowDiffViewer` (→ `DiffViewerView`).
Keep `ShowMergeModal`.
- **`WorktreesOverviewModalViewModel`**: `ShowDiff` builds a Files-mode viewer (worktree path
+ base). Change `_diffVmFactory` from `Func<WorktreeModalViewModel>` to
`Func<DiffViewerViewModel>`; `ShowDiffAction` stays `Action<DiffViewerViewModel>`.
- **`WindowDialogService.cs`**: `ShowDiffAction``new DiffViewerView` + `LoadAsync` + show.
- **`Program.cs`**: register `DiffViewerViewModel` (transient) + `Func<DiffViewerViewModel>`;
drop the `WorktreeModalViewModel` registration.
### Delete
`DiffModalViewModel.cs`, `WorktreeModalViewModel.cs`, `PlanningDiffViewModel.cs`,
`DiffModalView.axaml(.cs)`, `WorktreeModalView.axaml(.cs)`, `PlanningDiffView.axaml(.cs)`.
### Localization
Reuse existing keys in the merged view (`modals.diff.*` for the file pane, `planning.diff.*`
for the planning toolbar). Prune clearly-orphaned `modals.worktree.*` if trivial; keep en/de
parity.
## Tests
Replace `DiffModalViewModelTests` + `PlanningDiffViewModelTests` with
`DiffViewerViewModelTests` preserving the behaviors: commit-range null-guard → unavailable;
planning init populates + selects first; subtask select → DisplayedDiff; combined toggle
success/conflict/null. `WorktreesOverviewBatchMergeTests` compiles unchanged (`() => null!`
satisfies the new Func type). `UnifiedDiffParserTests` unchanged.
## Acceptance
- `dotnet build -c Release` clean (App); `Ui.Tests` + `Localization.Tests` green.
- One viewer reached from all 3 doors; old VMs/views deleted; merge editor untouched.
- Visual gap flagged: Details "Open Diff" (dirty + post-merge commit-range), Worktrees-
Overview "Show Diff" (tree), Details "Review Combined Diff" (subtasks + combined toggle),
and the Merge button still opens the merge form / resolver on conflict.
## Commit
`refactor(diff): single DiffViewer replaces DiffModal + WorktreeModal + PlanningDiff`.
Stage by path (exclude concurrent peers' files). Then Phase 3 (WorktreeActions) follows as
its own slice, reusing this viewer.