112 lines
6.0 KiB
Markdown
112 lines
6.0 KiB
Markdown
# 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.
|