6.0 KiB
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.SelectionChangedworkaround. Carry per-file status + +adds/ −dels into the tree rows (from the parsedDiffFileViewModel).
- the Avalonia-12
- 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/ResolveMergeVmdelegates.LoadAsyncpulls the whole diff via GitService (GetCommitRangeDiffAsync|GetBranchDiffAsync|GetDiffAsync), parses withUnifiedDiffParser.Parse, buildsFileTree.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.LoadAsyncpullsGetPlanningAggregateAsync→Subtasks;SelectedSubtask→DisplayedDiff;IsCombinedModetoggle →BuildPlanningIntegrationBranchAsync(success → combined diff; conflict →CombinedWarningwith subtask + file count; null → hub-error warning).DisplayedDiff→ flattenedDiffLines(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, theTreeView.SelectionChanged→SelectedNodeworkaround, dir-row tap-to-expand.
Re-point the 3 doors → one viewer
MergeSectionViewModel:OpenDiffAsyncbuilds a Files-modeDiffViewerViewModel(+ ShowMergeModal/ResolveMergeVm) and calls a singleShowDiffViewerdelegate;ReviewCombinedDiffAsyncbuilds a Planning-mode one and calls the same delegate. ReplacesShowDiffModal+ShowPlanningDiffModalwith oneFunc<DiffViewerViewModel,Task> ShowDiffViewer; keepsShowMergeModal. (Resolve the VM via_services.)DetailsIslandView.axaml.cs: replace the twoShowDiffModal/ShowPlanningDiffModalwirings (→DiffModalView/PlanningDiffView) with oneShowDiffViewer(→DiffViewerView). KeepShowMergeModal.WorktreesOverviewModalViewModel:ShowDiffbuilds a Files-mode viewer (worktree path- base). Change
_diffVmFactoryfromFunc<WorktreeModalViewModel>toFunc<DiffViewerViewModel>;ShowDiffActionstaysAction<DiffViewerViewModel>.
- base). Change
WindowDialogService.cs:ShowDiffAction→new DiffViewerView+LoadAsync+ show.Program.cs: registerDiffViewerViewModel(transient) +Func<DiffViewerViewModel>; drop theWorktreeModalViewModelregistration.
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 Releaseclean (App);Ui.Tests+Localization.Testsgreen.- 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.