Files
ClaudeDo/docs/superpowers/plans/2026-06-19-unify-diff-viewer.md

6.0 KiB
Raw Blame History

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 GetPlanningAggregateAsyncSubtasks; SelectedSubtaskDisplayedDiff; 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.SelectionChangedSelectedNode 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: ShowDiffActionnew 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.