7.7 KiB
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(themeStyleIncludeinsrc/ClaudeDo.App/App.axaml).
- Backend is kept unchanged.
WorkerHub.GetMergeConflictDocuments(taskId)returns each conflicted file as orderedMergeSegments: stable text (git's already auto-merged content) interleaved with conflict blocks carryingOurs/Base/Theirs.StartConflictMerge/WriteConflictResolution/Continue[Planning]ConflictMerge/Abort[Planning]ConflictMergeand theirIWorkerClientmirrors 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 inMainWindow.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/:3blobs + 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 aTextSegmentCollection(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
MergeConflictBlockwith itsAcceptOurs/Theirs/Both/Basecommands andMergeFile.Compose; keepCurrent/CurrentIndex/Next/Previousrepurposed 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);CanContinuestays "all files resolved AND no binary". - On switching files, block
Resolutionpersists (state lives onMergeConflictBlock), 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),
Gridof 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 fromActiveFile'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'sResolutionand flips resolved. IReadOnlySectionProvideron the ResultTextArea(stable = read-only, conflicts = editable) backed by aTextSegmentCollectionof the conflict result-spans.- One
IBackgroundRendererper 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
TextViewvisual top; click →block.AcceptOurs/AcceptTheirsand 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) inen.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/:3blobs 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.