# 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 `MergeSegment`s: *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 `TextDocument`s 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.