docs(merge): spec + plan for Rider-style 3-pane merge editor
This commit is contained in:
92
docs/superpowers/plans/2026-06-19-rider-merge-editor.md
Normal file
92
docs/superpowers/plans/2026-06-19-rider-merge-editor.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Plan: Rider-style 3-pane merge editor
|
||||||
|
|
||||||
|
Spec: `docs/superpowers/specs/2026-06-19-rider-merge-editor-design.md`
|
||||||
|
|
||||||
|
TDD, one focused commit per task (Conventional Commits, `feat(merge): …`).
|
||||||
|
Build with `-c Release` per project (a running Worker locks `Debug`).
|
||||||
|
Run `ClaudeDo.Ui.Tests` (and `Localization.Tests` for Task 6). No real `claude` CLI in tests.
|
||||||
|
Stage ONLY the files each task touches, by explicit path (parallel sessions leave WIP).
|
||||||
|
Backend + seam stay unchanged. Implementer/reviewer subagents use **sonnet**.
|
||||||
|
|
||||||
|
## Task 1 — VM: active-file model + 3-pane reconstruction + readout
|
||||||
|
|
||||||
|
`ConflictResolverViewModel` / `ConflictModels.cs`, additive (seam untouched).
|
||||||
|
|
||||||
|
- Add `ActiveFile` (`MergeFile?`), `SelectFileCommand(MergeFile)`, default to first file
|
||||||
|
after load. Keep `Files`, `Current`/`CurrentIndex`/`Next`/`Previous` (focused conflict
|
||||||
|
for the header arrows), `CanContinue`, binary guard, planning routing — all unchanged.
|
||||||
|
- Add computed, per `ActiveFile`:
|
||||||
|
- `ActiveOursText` = concat(stable.Text | conflict.Ours)
|
||||||
|
- `ActiveTheirsText` = concat(stable.Text | conflict.Theirs)
|
||||||
|
- `ActiveResultText` = concat(stable.Text | conflict.Resolution ?? conflict.Ours)
|
||||||
|
- `ActiveConflicts` = ordered descriptors (block + segment index) for the view.
|
||||||
|
- `PositionText` → `"{conflicts} conflicts · {resolved} resolved"` for the active file;
|
||||||
|
keep `CanContinue` = every file resolved AND no binary.
|
||||||
|
- Switching files raises a change event the view listens to (reuse/extend
|
||||||
|
`CurrentChanged` → e.g. `ActiveFileChanged`).
|
||||||
|
- Tests (Ui.Tests): reconstruction text for ours/theirs/result (result seeds unresolved
|
||||||
|
with Ours); resolving a block updates `ActiveResultText` + readout; switching files
|
||||||
|
preserves each block's `Resolution`; `CanContinue` blocks until all files resolved;
|
||||||
|
binary file still blocks. Keep all existing tests green.
|
||||||
|
|
||||||
|
## Task 2 — View: 3-pane AXAML shell + document assembly + synced scroll
|
||||||
|
|
||||||
|
`Views/Conflicts/ConflictResolverView.axaml(.cs)`. Visual — verified by running.
|
||||||
|
|
||||||
|
- Replace AXAML: ModalShell host kept; header row (◀/▶ focus arrows bound to
|
||||||
|
Previous/Next, file switcher `ItemsControl`/`ComboBox` over `Files` bound to
|
||||||
|
`SelectFileCommand`, right-aligned `PositionText`); `Grid ColumnDefinitions="*,*,*"`
|
||||||
|
of three bordered panes with headers **Ours · current (merge target)** /
|
||||||
|
**Result** / **Theirs · incoming (task)** (drop Base); footer Continue
|
||||||
|
(`IsEnabled=CanContinue`) / Abort; binary banner (kept); `Escape`→Abort (kept).
|
||||||
|
- Code-behind: build three `TextDocument`s from `ActiveFile` segments, recording each
|
||||||
|
conflict's start line + line count per document; install TextMate per pane by file
|
||||||
|
extension; rebuild on `ActiveFileChanged`; Ours/Theirs `IsReadOnly=true`.
|
||||||
|
- Proportional synced vertical scroll across the three panes (re-entrancy guard).
|
||||||
|
- Push Result edits back to the active block `Resolution` (refined in Task 4).
|
||||||
|
|
||||||
|
## Task 3 — Result pane: read-only stable, editable conflicts
|
||||||
|
|
||||||
|
`ConflictResolverView.axaml.cs` + a small `IReadOnlySectionProvider` helper.
|
||||||
|
|
||||||
|
- Track each conflict's result span in a `TextSegmentCollection<…>` over the Result
|
||||||
|
document (anchors auto-adjust on edit).
|
||||||
|
- `IReadOnlySectionProvider`: `CanInsert` only strictly inside a conflict span;
|
||||||
|
`GetDeletableSegments` intersects with conflict spans only. Stable text becomes
|
||||||
|
immutable; conflict regions stay editable.
|
||||||
|
- Editing inside a conflict span writes the span text back to the block `Resolution`
|
||||||
|
and flips it resolved (updates readout + `CanContinue`).
|
||||||
|
|
||||||
|
## Task 4 — Color blocks (IBackgroundRenderer) + accept overlay
|
||||||
|
|
||||||
|
`ConflictResolverView.axaml.cs` + renderer/overlay helpers.
|
||||||
|
|
||||||
|
- `IBackgroundRenderer` per pane: unresolved conflict = red (Blood tint), resolved =
|
||||||
|
green/muted, Ours side = Moss tint, Theirs side = Accent tint — driven by recorded
|
||||||
|
spans + block `IsResolved`.
|
||||||
|
- Between-pane overlay Canvas (Ours|Result and Result|Theirs): `›` accept-ours / `‹`
|
||||||
|
accept-theirs + `✕` dismiss per conflict, positioned at the block's `TextView` visual
|
||||||
|
top, recomputed on scroll/resize. Click → `block.AcceptOurs/AcceptTheirs` and replace
|
||||||
|
the tracked Result span; resolved blocks recolor.
|
||||||
|
|
||||||
|
## Task 5 — Polish: readout, focus arrows scroll-to-conflict, resolved styling
|
||||||
|
|
||||||
|
- ◀/▶ arrows move `Current` and scroll all three panes to that conflict.
|
||||||
|
- `M conflicts · K resolved` live readout; Continue tooltip/hint when blocked.
|
||||||
|
- Resolved conflict recolors and drops its accept overlay; unresolved stays red.
|
||||||
|
(Fold into Task 4 if small.)
|
||||||
|
|
||||||
|
## Task 6 — Localization + tokens
|
||||||
|
|
||||||
|
- Add `conflictResolver.*` keys (pane headers, readout, accept tooltips, hints) to
|
||||||
|
`locales/en.json` AND `locales/de.json` (keep key parity).
|
||||||
|
- Add Tokens.axaml color tokens only if a needed conflict/resolved shade is missing.
|
||||||
|
- Run Localization.Tests (parity) + a quick scan for hard-coded strings in the view.
|
||||||
|
|
||||||
|
## Task 7 — Verify
|
||||||
|
|
||||||
|
- Build `ClaudeDo.App` + `ClaudeDo.Ui` `-c Release`; run `Ui.Tests` + `Localization.Tests`.
|
||||||
|
- Update `src/ClaudeDo.Ui/CLAUDE.md` (Planning/Conflicts paragraph → new 3-pane editor).
|
||||||
|
- **Visual verification gap (flag to Mika):** run the app, trigger a real conflict
|
||||||
|
(single-task approve + planning unit-merge) and confirm panes/colors/accept/scroll/
|
||||||
|
gating/binary render correctly — cannot be asserted in tests.
|
||||||
132
docs/superpowers/specs/2026-06-19-rider-merge-editor-design.md
Normal file
132
docs/superpowers/specs/2026-06-19-rider-merge-editor-design.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user