# 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 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` to `Func`; `ShowDiffAction` stays `Action`. - **`WindowDialogService.cs`**: `ShowDiffAction` → `new DiffViewerView` + `LoadAsync` + show. - **`Program.cs`**: register `DiffViewerViewModel` (transient) + `Func`; 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.