Files
ClaudeDo/docs/superpowers/specs/2026-06-19-rider-merge-editor-design.md

7.7 KiB
Raw Blame History

Rider-style 3-pane merge editor (conflict resolver redesign)

Date: 2026-06-19

Goal

Replace ClaudeDo's current conflict resolver (3 read-only columns Base|Ours|Theirs, one conflict at a time, accept buttons + editable result below) with a JetBrains Rider-style 3-pane merge editor:

  • LEFT = Ours (read-only) · current branch / merge target
  • MIDDLE = Result (editable) · the merged file being assembled
  • RIGHT = Theirs (read-only) · incoming task branch

Whole file per pane (not one conflict at a time), color-coded conflict blocks, inline per-hunk accept controls ( accept a side into the result, dismiss), a M conflicts · K resolved readout, synced scrolling, Continue gated until every conflict is resolved, Abort, and a binary-file guard. Visual reference: the attached "Merge Revisions" screenshot.

Background

  • Avalonia 12 desktop app; the conflict editor already uses AvaloniaEdit 12.0.0
    • AvaloniaEdit.TextMate (theme StyleInclude in src/ClaudeDo.App/App.axaml).
  • Backend is kept unchanged. WorkerHub.GetMergeConflictDocuments(taskId) returns each conflicted file as ordered MergeSegments: stable text (git's already auto-merged content) interleaved with conflict blocks carrying Ours/Base/Theirs. StartConflictMerge / WriteConflictResolution / Continue[Planning]ConflictMerge / Abort[Planning]ConflictMerge and their IWorkerClient mirrors stay as-is. ConflictMarkerParser (Data) already produces the segments. ours = merge target (current branch); theirs = incoming task branch. Merges are LOCAL-only (no push).
  • Seam kept unchanged so single-task AND planning conflict paths keep working: IslandsShellViewModel.ConflictResolverFactory + ShowConflictResolver (wired in MainWindow.axaml.cs), VM ctor (IWorkerClient, taskId), OpenAsync(targetBranch), OpenForPlanningAsync(parentId, subtaskId), CloseRequested. The planning-path WIP currently uncommitted in the tree (OpenForPlanningAsync, _conflictTaskId, LoadDocumentsAsync) is part of this seam and is preserved.

Key insight: the segments already line the panes up

Because every conflicted file is split into stable (identical on both sides, git auto-merged) and conflict (divergent) segments, reconstructing three documents —

  • Ours = Σ over segments of (stable.Text | conflict.Ours)
  • Theirs = Σ over segments of (stable.Text | conflict.Theirs)
  • Result = Σ over segments of (stable.Text | conflict.Resolution ?? conflict.Ours)

— yields three documents that are byte-identical in their stable regions and differ only inside conflict blocks. So the panes align line-for-line for free, and a real client-side 3-way diff is not needed for the core feature.

Decisions

  • Data source = segment-based (no backend change, no DiffPlex). The worker already applied git's auto-merge; only conflicts remain actionable. The screenshot's "N changes" (non-conflicting hunks shown as separately flippable) are already merged and have nothing to accept, so the readout is M conflicts · K resolved. True "N changes" parity (raw :1/:2/:3 blobs + DiffPlex 3-way) is an explicit later add-on that does not touch the seam — see Out of scope / fast-follow.
  • One file at a time + file switcher. Like Rider's title bar ("Merge Revisions for …file"). When more than one file conflicts, a compact switcher selects the active file; Continue still requires all files resolved. (Replaces today's cross-file flattened one-at-a-time navigation as the primary model.)
  • Result-pane editing model. The middle document is the merged file. Stable text is read-only via IReadOnlySectionProvider; only conflict regions are editable. Each conflict's result span is tracked in a TextSegmentCollection (anchors auto-adjust on edit). Accepting (ours)/(theirs) replaces that span; editing inside it or accepting flips the block to resolved. Unresolved regions are seeded with the Ours text and painted red until acted on.
  • Accept controls = overlay between panes (not an AvaloniaEdit margin). A thin Canvas overlay between Ours|Result and Result|Theirs hosts / (and ) per conflict, positioned at each block's visual Y (recomputed on scroll/resize). This matches the screenshot's between-pane gutters and avoids the lack of a built-in right-side margin.
  • Synced scroll = proportional (Green). Mirror each pane's vertical scroll offset to the other two with a re-entrancy guard. Aligned/virtual-space scroll + bezier connector curves are a deferred stretch.
  • Seam + existing VM tests preserved. Keep MergeConflictBlock with its AcceptOurs/Theirs/Both/Base commands and MergeFile.Compose; keep Current/CurrentIndex/Next/Previous repurposed as the focused-conflict the top arrows jump to. New state (active file, readout) is additive.

Architecture

ViewModel (ConflictResolverViewModel, ConflictModels.cs)

Unchanged seam: ctor, OpenAsync, OpenForPlanningAsync, CloseRequested, Continue/Abort (incl. planning routing), CanContinue gating, binary guard.

Additive:

  • ActiveFile (MergeFile) + the switcher list (Files) + SelectFileCommand.
  • Per-active-file reconstruction exposed for the view and for tests: ActiveOursText, ActiveTheirsText, ActiveResultText (result seeds unresolved = Ours), plus an ordered list of conflict descriptors (the block + its segment index) so the view can compute offsets/spans as it assembles each document.
  • Readout PositionText"{M} conflicts · {K} resolved" (active file and/or total); CanContinue stays "all files resolved AND no binary".
  • On switching files, block Resolution persists (state lives on MergeConflictBlock), so progress survives navigation; the view rebuilds documents from the active file.

View (Views/Conflicts/ConflictResolverView.axaml + .cs)

  • AXAML: ModalShell host (kept), header (prev/next arrows, file switcher, readout), Grid of three bordered panes with headers, two between-pane overlay Canvases, footer (Continue/Abort), binary banner, Escape→Abort. Drop the Base column.
  • Code-behind builds three TextDocuments from ActiveFile's segments, recording each conflict's line span per document; installs TextMate by file extension on all three; rebuilds on file switch; pushes result-pane edits back into the active block's Resolution and flips resolved.
  • IReadOnlySectionProvider on the Result TextArea (stable = read-only, conflicts = editable) backed by a TextSegmentCollection of the conflict result-spans.
  • One IBackgroundRenderer per pane painting unresolved-conflict (red), resolved (green/muted), and ours/theirs side tints, driven by the recorded spans + block state.
  • Overlay accept controls positioned at each block's TextView visual top; click → block.AcceptOurs/AcceptTheirs and the code-behind replaces the tracked result span.
  • Proportional synced vertical scroll across the three panes.

Localization / tokens

  • New conflictResolver.* keys (pane headers, readout, accept tooltips) in en.json + de.json (parity enforced by Localization.Tests).
  • Block colors from Tokens.axaml (reuse Blood/Moss/Accent tints; add tokens only if a needed shade is missing).

Out of scope / fast-follow (not in this plan)

  • Raw 3-way diff "N changes" parity (Option B): a new worker method returning raw :1/:2/:3 blobs per conflicted file + DiffPlex client-side 3-way diff so non-conflicting changes also appear as accept-able hunks. Seam-preserving; later.
  • Intra-conflict word/line highlighting (Rider's "Highlight words") via a line transformer.
  • Bezier connector curves + aligned / virtual-space synced scroll (Red stretch).
  • No DB migration, no backend/seam changes, no push.