diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index 62a5176..307e630 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -406,6 +406,7 @@
"binaryHint": "Binärdateien können hier nicht zusammengeführt werden — brich ab und löse sie in deinem Editor:",
"prevConflict": "Vorheriger Konflikt (Umschalt+F8)",
"nextConflict": "Nächster Konflikt (F8)",
+ "conflictMap": "Konflikte in dieser Datei — Marker anklicken zum Springen",
"acceptOurs": "Main hinzufügen",
"acceptTheirs": "Incoming hinzufügen",
"removeOurs": "Main entfernen",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 0edc90d..430b947 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -406,6 +406,7 @@
"binaryHint": "Binary files can't be merged here — abort and resolve them in your editor:",
"prevConflict": "Previous conflict (Shift+F8)",
"nextConflict": "Next conflict (F8)",
+ "conflictMap": "Conflicts in this file — click a marker to jump",
"acceptOurs": "Add main",
"acceptTheirs": "Add incoming",
"removeOurs": "Remove main",
diff --git a/src/ClaudeDo.Ui/CLAUDE.md b/src/ClaudeDo.Ui/CLAUDE.md
index 7058a90..55a51bc 100644
--- a/src/ClaudeDo.Ui/CLAUDE.md
+++ b/src/ClaudeDo.Ui/CLAUDE.md
@@ -41,7 +41,7 @@ Design/ — Tokens.axaml (design tokens; merged before styles) + IslandStyle
- **NotesEditorViewModel** — day navigator + bullet CRUD for daily notes via `INotesApi`.
- **Modal VMs** — `SettingsModalViewModel` (four tabs: General, Worktrees, Files prompt-paths, Prime Claude incl. `DailyPrepMaxTasks` + prime-schedule rows), `ListSettingsModalViewModel` (name, working dir, commit type, per-list Model/SystemPrompt/AgentPath/MaxTurns with inherited-badge + reset, delete list), `RepoImportModalViewModel` (bulk-create lists from git repos found under chosen parents; already-wired repos disabled), `WeeklyReportModalViewModel` (range pickers default "since last standup weekday → today", cached per range, markdown via MarkdownView), `MergeModalViewModel` (single-task merge form, called from the diff modal), `WorktreesOverviewModalViewModel` (global/per-list worktree rows, batch merge + state ops), `UnfinishedPlanningModalViewModel` (Resume/FinalizeNow/Discard for a draft planning session), `WorkerConnectionModalViewModel` (offline help), `AboutModalViewModel`.
- **Diff stack** — `UnifiedDiffParser` (static; parses `git diff` output into `DiffFileViewModel`s, detecting added/deleted/renamed/binary files and per-line numbers; `Flatten` injects file-header rows for a combined single-pane view). `DiffModalViewModel` has two modes: live worktree (branch diff vs base, with a Merge action) and commit-range `base..head` (`FromCommitRange = true` — shows a merged task's diff after its worktree is gone; no merge action). The view renders a file list with binary/empty placeholders via `DiffLinesView`.
-- **Planning/Conflicts** — `PlanningDiffViewModel` (per-subtask diffs via `GetPlanningAggregateAsync`, toggle to combined integration-branch diff, conflict warnings), `ConflictResolverViewModel` (in-app **Rider-style 3-pane merge editor** for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — MAIN/ours (read-only) | editable Result | INCOMING/theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar); the between-pane gutter controls **toggle** each side in/out of the result — `›`/`‹` add MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a conflict can take main, incoming, both, or neither; a `FilesSummary` readout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`).
+- **Planning/Conflicts** — `PlanningDiffViewModel` (per-subtask diffs via `GetPlanningAggregateAsync`, toggle to combined integration-branch diff, conflict warnings), `ConflictResolverViewModel` (in-app **Rider-style 3-pane merge editor** for both single-task and planning unit-merge conflicts: single-task starts the conflict merge, parses each conflicted file into stable/conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`; exposes the active file's three reconstructed documents — `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText` (from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved conflicts with Ours) — plus `ActiveFile`/`SelectFileCommand` (multi-file switcher), `Current`/`Next`/`Previous` (focused-conflict nav), a per-active-file `PositionText` readout, per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on every file resolved + no binary; writes each file via `WriteConflictResolution`, continue/abort; **planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's mid-merge conflicts without re-starting the merge and routes continue/abort to `ContinuePlanningMerge`/`AbortPlanningMerge`, so a unit-merge conflict re-opens the editor per subtask via the `PlanningMergeConflict` broadcast). The view (`Views/Conflicts/ConflictResolverView`) shows the whole file in three **AvaloniaEdit** panes — MAIN/ours (read-only) | editable Result | INCOMING/theirs (read-only) — with TextMate highlighting by extension (theme `StyleInclude` in `App.axaml`); a code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across panes, an `IReadOnlySectionProvider` + `TextAnchor` regions keep only conflict spans editable in Result (edits flow back to the block); each unresolved conflict starts EMPTY (a thin marker bar); the between-pane gutter controls **toggle** each side in/out of the result — `›`/`‹` add MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a conflict can take main, incoming, both, or neither; a `FilesSummary` readout shows how many files still have conflicts, and the three panes share a proportional synced vertical scroll. A conflict overview ruler right of the Result pane (`ConflictMap`) maps every conflict in the file proportionally (click a tick to jump) — handy for long files. Conflict block tints live in `Tokens.axaml` (`Merge*TintBrush`). The editor is reached from review **Approve** on conflict and from the **Merge** button in the Diff window (a conflicting `MergeTask` hands off to the resolver via `RequestConflictResolution`).
## Services
diff --git a/src/ClaudeDo.Ui/Design/Tokens.axaml b/src/ClaudeDo.Ui/Design/Tokens.axaml
index d291080..95e965d 100644
--- a/src/ClaudeDo.Ui/Design/Tokens.axaml
+++ b/src/ClaudeDo.Ui/Design/Tokens.axaml
@@ -104,8 +104,9 @@
-
+
+
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/MergeSectionViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/MergeSectionViewModel.cs
index a3f6819..568f871 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/MergeSectionViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/MergeSectionViewModel.cs
@@ -156,6 +156,7 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
TaskTitle = TaskTitle ?? "",
ShowMergeModal = ShowMergeModal,
ResolveMergeVm = () => _services.GetRequiredService(),
+ RequestConflictResolution = RequestConflictResolution,
};
}
else if (CanDiffMergedRange)
diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
index 82050a3..b7a3f9d 100644
--- a/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
@@ -72,6 +72,7 @@ public sealed partial class DiffModalViewModel : ViewModelBase
public string TaskTitle { get; init; } = "";
public Func? ShowMergeModal { get; set; }
public Func? ResolveMergeVm { get; set; }
+ public Func? RequestConflictResolution { get; set; }
public ObservableCollection Files { get; } = new();
@@ -99,10 +100,11 @@ public sealed partial class DiffModalViewModel : ViewModelBase
{
if (TaskId is null || ShowMergeModal is null || ResolveMergeVm is null) return;
var vm = ResolveMergeVm();
+ vm.RequestConflictResolution = RequestConflictResolution;
await vm.InitializeAsync(TaskId, TaskTitle);
await ShowMergeModal(vm);
- // The diff is stale once the worktree has been merged away — close it too.
- if (vm.Merged) CloseAction?.Invoke();
+ // The diff is stale once the worktree merged away or a conflict opened the editor.
+ if (vm.Merged || vm.RoutedToResolver) CloseAction?.Invoke();
}
public async Task LoadAsync(CancellationToken ct = default)
diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs
index 79d7236..c9aa681 100644
--- a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs
@@ -28,10 +28,17 @@ public sealed partial class MergeModalViewModel : ViewModelBase
public Action? CloseAction { get; set; }
+ /// Set by the caller to hand a conflicting merge off to the in-app 3-pane editor
+ /// instead of dead-ending on the conflict message.
+ public Func? RequestConflictResolution { get; set; }
+
/// True once a merge has succeeded — lets the caller (e.g. the diff window)
/// close itself after this modal closes.
public bool Merged { get; private set; }
+ /// True once a conflict has been handed off to the resolver — also a cue to close the diff window.
+ public bool RoutedToResolver { get; private set; }
+
public MergeModalViewModel(IWorkerClient worker)
{
_worker = worker;
@@ -96,9 +103,21 @@ public sealed partial class MergeModalViewModel : ViewModelBase
});
break;
case "conflict":
- HasConflict = true;
- ConflictFiles = result.ConflictFiles;
- ErrorMessage = Loc.T("vm.merge.conflict");
+ // Hand off to the in-app 3-pane merge editor when wired (MergeTask aborted
+ // cleanly, so the resolver re-starts the merge leaving conflicts in the tree).
+ if (RequestConflictResolution is not null)
+ {
+ var branch = SelectedBranch!;
+ RoutedToResolver = true;
+ CloseAction?.Invoke();
+ await RequestConflictResolution(TaskId, branch);
+ }
+ else
+ {
+ HasConflict = true;
+ ConflictFiles = result.ConflictFiles;
+ ErrorMessage = Loc.T("vm.merge.conflict");
+ }
break;
case "blocked":
ErrorMessage = Loc.T("vm.merge.blocked", result.ErrorMessage ?? "");
diff --git a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml
index 6cc5302..157bd47 100644
--- a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml
+++ b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml
@@ -149,6 +149,9 @@
+
diff --git a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs
index 5f380bd..6bda13a 100644
--- a/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
@@ -251,6 +252,7 @@ public partial class ConflictResolverView : Window
private void PositionGutters()
{
ClearGutters();
+ PopulateConflictMap();
if (_vm?.ActiveFile is null) return;
var tv = ResultEditor.TextArea.TextView;
if (!tv.VisualLinesValid)
@@ -298,6 +300,47 @@ public partial class ConflictResolverView : Window
canvas.Children.Add(b);
}
+ // ── Conflict overview ruler (right of the result pane) ───────────────────
+
+ // A proportional map of every conflict in the active file so they're findable in
+ // long files without scrolling; ticks recolor by resolved state and jump on click.
+ private void PopulateConflictMap()
+ {
+ ConflictMap.Children.Clear();
+ if (_vm?.ActiveFile is null || _resultRegions.Count == 0) return;
+ var h = ConflictMap.Bounds.Height;
+ if (h <= 1) return;
+ var doc = ResultEditor.Document;
+ var totalLines = Math.Max(1, doc.LineCount);
+ var unresolved = BrushRes("MergeConflictEdgeBrush", Color.Parse("#80C87060"));
+ var resolved = BrushRes("MergeResolvedEdgeBrush", Color.Parse("#806FA86B"));
+
+ foreach (var region in _resultRegions)
+ {
+ var line = doc.GetLineByOffset(region.Start.Offset).LineNumber;
+ var y = (line - 1) / (double)totalLines * h;
+ var tick = new Rectangle
+ {
+ Width = 9,
+ Height = 4,
+ Fill = region.Block.IsResolved ? resolved : unresolved,
+ Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand),
+ };
+ Canvas.SetLeft(tick, 2);
+ Canvas.SetTop(tick, Math.Min(h - 4, Math.Max(0, y)));
+ var r = region;
+ tick.PointerPressed += (_, _) => JumpToRegion(r);
+ ConflictMap.Children.Add(tick);
+ }
+ }
+
+ private void JumpToRegion(ResultRegion region)
+ {
+ var line = ResultEditor.Document.GetLineByOffset(region.Start.Offset).LineNumber;
+ ResultEditor.ScrollToLine(line);
+ QueueGutters();
+ }
+
private static string Tr(string key) => ClaudeDo.Ui.Localization.Loc.T(key);
// ── Synced vertical scroll across the three panes ─────────────────────────
@@ -345,7 +388,7 @@ public partial class ConflictResolverView : Window
private void ApplyGrammar(string? path)
{
if (_registry is null || string.IsNullOrEmpty(path)) return;
- var ext = Path.GetExtension(path);
+ var ext = System.IO.Path.GetExtension(path);
if (string.IsNullOrEmpty(ext)) return;
var language = _registry.GetLanguageByExtension(ext);
if (language is null) return;