diff --git a/src/ClaudeDo.Data/Git/ConflictMarkerParser.cs b/src/ClaudeDo.Data/Git/ConflictMarkerParser.cs
new file mode 100644
index 0000000..6238acb
--- /dev/null
+++ b/src/ClaudeDo.Data/Git/ConflictMarkerParser.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace ClaudeDo.Data.Git;
+
+///
+/// One piece of a conflicted file: either common ("stable") text both sides agree on,
+/// or a conflict region holding the two — or, with diff3 markers, three — competing versions.
+///
+public sealed record MergeSegment
+{
+ public bool IsConflict { get; init; }
+
+ /// Stable text (verbatim, line endings preserved) when is false.
+ public string Text { get; init; } = "";
+
+ /// "Ours" side (the target branch) when is true.
+ public string Ours { get; init; } = "";
+
+ /// Merge base, present only when the merge used diff3 conflict style; null otherwise.
+ public string? Base { get; init; }
+
+ /// "Theirs" side (the incoming branch) when is true.
+ public string Theirs { get; init; } = "";
+
+ public static MergeSegment Stable(string text) => new() { Text = text };
+
+ public static MergeSegment Conflict(string ours, string? @base, string theirs) =>
+ new() { IsConflict = true, Ours = ours, Base = @base, Theirs = theirs };
+}
+
+///
+/// Parses a conflicted file's text into ordered stable / conflict segments and reassembles it.
+/// Reads git conflict markers verbatim, so a file with no markers yields a single stable
+/// segment, and reassembling the stable text plus one chosen resolution per conflict
+/// round-trips the file exactly (line endings included).
+///
+public static class ConflictMarkerParser
+{
+ private const string OursMarker = "<<<<<<<";
+ private const string BaseMarker = "|||||||";
+ private const string SepMarker = "=======";
+ private const string TheirsMarker = ">>>>>>>";
+
+ public static IReadOnlyList Parse(string fileText)
+ {
+ var segments = new List();
+ var lines = SplitKeepLineEndings(fileText);
+ var stable = new StringBuilder();
+ var i = 0;
+
+ while (i < lines.Count)
+ {
+ if (!IsMarker(lines[i], OursMarker))
+ {
+ stable.Append(lines[i++]);
+ continue;
+ }
+
+ if (stable.Length > 0)
+ {
+ segments.Add(MergeSegment.Stable(stable.ToString()));
+ stable.Clear();
+ }
+
+ i++; // consume "<<<<<<<"
+ var ours = new StringBuilder();
+ while (i < lines.Count && !IsMarker(lines[i], BaseMarker) && !IsMarker(lines[i], SepMarker))
+ ours.Append(lines[i++]);
+
+ string? @base = null;
+ if (i < lines.Count && IsMarker(lines[i], BaseMarker))
+ {
+ i++; // consume "|||||||"
+ var baseText = new StringBuilder();
+ while (i < lines.Count && !IsMarker(lines[i], SepMarker))
+ baseText.Append(lines[i++]);
+ @base = baseText.ToString();
+ }
+
+ if (i < lines.Count && IsMarker(lines[i], SepMarker)) i++; // consume "======="
+
+ var theirs = new StringBuilder();
+ while (i < lines.Count && !IsMarker(lines[i], TheirsMarker))
+ theirs.Append(lines[i++]);
+
+ if (i < lines.Count && IsMarker(lines[i], TheirsMarker)) i++; // consume ">>>>>>>"
+
+ segments.Add(MergeSegment.Conflict(ours.ToString(), @base, theirs.ToString()));
+ }
+
+ if (stable.Length > 0)
+ segments.Add(MergeSegment.Stable(stable.ToString()));
+
+ return segments;
+ }
+
+ /// True when the file still contains an opening conflict marker.
+ public static bool HasConflicts(string fileText) =>
+ SplitKeepLineEndings(fileText).Any(l => IsMarker(l, OursMarker));
+
+ ///
+ /// Reassembles a file from its segments. Stable segments emit their text verbatim;
+ /// each conflict segment emits whatever returns for it.
+ ///
+ public static string Compose(
+ IEnumerable segments, Func resolveConflict) =>
+ string.Concat(segments.Select(s => s.IsConflict ? resolveConflict(s) : s.Text));
+
+ // A marker line starts with exactly the 7-char marker, then end-of-line or whitespace/label.
+ private static bool IsMarker(string line, string marker)
+ {
+ if (!line.StartsWith(marker, StringComparison.Ordinal)) return false;
+ if (line.Length == marker.Length) return true;
+ return line[marker.Length] is ' ' or '\t' or '\r' or '\n';
+ }
+
+ // Splits into physical lines, each retaining its trailing "\n" (and "\r" if present).
+ private static List SplitKeepLineEndings(string s)
+ {
+ var lines = new List();
+ var i = 0;
+ while (i < s.Length)
+ {
+ var nl = s.IndexOf('\n', i);
+ if (nl < 0) { lines.Add(s[i..]); break; }
+ lines.Add(s[i..(nl + 1)]);
+ i = nl + 1;
+ }
+ return lines;
+ }
+}
diff --git a/src/ClaudeDo.Data/Git/GitService.cs b/src/ClaudeDo.Data/Git/GitService.cs
index fc1edd5..b07c77c 100644
--- a/src/ClaudeDo.Data/Git/GitService.cs
+++ b/src/ClaudeDo.Data/Git/GitService.cs
@@ -252,8 +252,11 @@ public sealed class GitService
public async Task<(int ExitCode, string Stderr)> MergeNoFfAsync(
string repoDir, string sourceBranch, string message, CancellationToken ct = default)
{
+ // diff3 conflict style writes the merge base (|||||||) into conflict markers so the
+ // in-app resolver can show a true three-way view. It only enriches conflicted hunks;
+ // clean merges are unaffected.
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
- ["merge", "--no-ff", "-m", message, sourceBranch], ct);
+ ["-c", "merge.conflictStyle=diff3", "merge", "--no-ff", "-m", message, sourceBranch], ct);
return (exitCode, stderr);
}
diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
index 3b7590b..8e317d4 100644
--- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
+++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
@@ -55,6 +55,7 @@ public interface IWorkerClient : INotifyPropertyChanged
// ── Conflict resolution (worker hub side implemented by Layer C) ──
Task StartConflictMergeAsync(string taskId, string targetBranch);
Task GetMergeConflictsAsync(string taskId);
+ Task GetMergeConflictDocumentsAsync(string taskId);
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
Task ContinueConflictMergeAsync(string taskId);
Task AbortConflictMergeAsync(string taskId);
diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs
index 10b20ab..623d6a9 100644
--- a/src/ClaudeDo.Ui/Services/WorkerClient.cs
+++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs
@@ -275,6 +275,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public Task GetMergeConflictsAsync(string taskId)
=> _hub.InvokeAsync("GetMergeConflicts", taskId);
+ public Task GetMergeConflictDocumentsAsync(string taskId)
+ => _hub.InvokeAsync("GetMergeConflictDocuments", taskId);
+
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent)
=> _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent);
@@ -559,6 +562,9 @@ public record MergeTargetsDto(string DefaultBranch, IReadOnlyList LocalB
public record MergeConflictsDto(string TaskId, IReadOnlyList Files);
public record ConflictFileDto(string Path, IReadOnlyList Hunks);
public record ConflictHunkDto(string Ours, string Theirs, string? Base);
+public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList Files);
+public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList Segments);
+public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs
index 61f45fd..9979357 100644
--- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs
+++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs
@@ -61,6 +61,9 @@ public record MergeTargetsDto(string DefaultBranch, IReadOnlyList LocalB
public record MergeConflictsDto(string TaskId, IReadOnlyList Files);
public record ConflictFileDto(string Path, IReadOnlyList Hunks);
public record ConflictHunkDto(string Ours, string Theirs, string? Base);
+public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList Files);
+public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList Segments);
+public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
@@ -383,6 +386,18 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
new[] { new ConflictHunkDto(f.Ours, f.Theirs, f.Base) })).ToList());
});
+ public Task GetMergeConflictDocuments(string taskId)
+ => HubGuard(async () =>
+ {
+ var c = await _mergeService.GetConflictDocumentsAsync(taskId, CancellationToken.None);
+ return new MergeConflictDocumentsDto(
+ c.TaskId,
+ c.Files.Select(f => new ConflictDocumentDto(
+ f.Path, f.IsBinary,
+ f.Segments.Select(s => new MergeSegmentDto(
+ s.IsConflict, s.Text, s.Ours, s.Base, s.Theirs)).ToList())).ToList());
+ });
+
public Task WriteConflictResolution(string taskId, string path, string resolvedContent)
=> HubGuard(() => _mergeService.WriteResolutionAsync(
taskId, path, resolvedContent ?? "", CancellationToken.None));
diff --git a/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs b/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs
index 0d46ae1..6e3dfcf 100644
--- a/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs
+++ b/src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs
@@ -33,6 +33,15 @@ public sealed record ConflictFileContent(
string Theirs,
string? Base);
+public sealed record ConflictDocuments(
+ string TaskId,
+ IReadOnlyList Files);
+
+public sealed record ConflictDocumentContent(
+ string Path,
+ bool IsBinary,
+ IReadOnlyList Segments);
+
public sealed class TaskMergeService
{
public const string StatusMerged = "merged";
@@ -256,6 +265,45 @@ public sealed class TaskMergeService
return new MergeConflicts(taskId, result);
}
+ ///
+ /// Reads each conflicted working-tree file and parses its conflict markers into line-level
+ /// segments (with the diff3 merge base when present). Binary files are flagged and skipped.
+ ///
+ public async Task GetConflictDocumentsAsync(string taskId, CancellationToken ct)
+ {
+ var (_, list, _) = await LoadMergeContextAsync(taskId, ct);
+ if (string.IsNullOrWhiteSpace(list.WorkingDir))
+ throw new InvalidOperationException("list has no working directory");
+
+ var files = await _git.ListConflictedFilesAsync(list.WorkingDir, ct);
+ var result = new List(files.Count);
+ foreach (var path in files)
+ {
+ var full = Path.Combine(list.WorkingDir, path.Replace('/', Path.DirectorySeparatorChar));
+ string text;
+ try { text = await File.ReadAllTextAsync(full, ct); }
+ catch { text = ""; }
+
+ if (LooksBinary(text))
+ {
+ result.Add(new ConflictDocumentContent(path, true, Array.Empty()));
+ continue;
+ }
+
+ result.Add(new ConflictDocumentContent(path, false, ConflictMarkerParser.Parse(text)));
+ }
+ return new ConflictDocuments(taskId, result);
+ }
+
+ // A NUL byte in the head of the file is the conventional binary sniff.
+ private static bool LooksBinary(string text)
+ {
+ var n = Math.Min(text.Length, 8000);
+ for (var i = 0; i < n; i++)
+ if (text[i] == '\0') return true;
+ return false;
+ }
+
public async Task WriteResolutionAsync(string taskId, string path, string content, CancellationToken ct)
{
var (_, list, _) = await LoadMergeContextAsync(taskId, ct);
diff --git a/tests/ClaudeDo.Data.Tests/ConflictMarkerParserTests.cs b/tests/ClaudeDo.Data.Tests/ConflictMarkerParserTests.cs
new file mode 100644
index 0000000..149a888
--- /dev/null
+++ b/tests/ClaudeDo.Data.Tests/ConflictMarkerParserTests.cs
@@ -0,0 +1,123 @@
+using System.Linq;
+using ClaudeDo.Data.Git;
+using Xunit;
+
+namespace ClaudeDo.Data.Tests;
+
+public class ConflictMarkerParserTests
+{
+ [Fact]
+ public void NoMarkers_YieldsSingleStableSegment_AndRoundTrips()
+ {
+ const string text = "just\nsome\nplain\nlines\n";
+
+ var segments = ConflictMarkerParser.Parse(text);
+
+ var only = Assert.Single(segments);
+ Assert.False(only.IsConflict);
+ Assert.Equal(text, ConflictMarkerParser.Compose(segments, c => c.Ours));
+ Assert.False(ConflictMarkerParser.HasConflicts(text));
+ }
+
+ [Fact]
+ public void SimpleConflict_SplitsIntoStable_Conflict_Stable()
+ {
+ const string text =
+ "line1\n<<<<<<< HEAD\nours line\n=======\ntheirs line\n>>>>>>> branch\nline2\n";
+
+ var segments = ConflictMarkerParser.Parse(text);
+
+ Assert.Equal(3, segments.Count);
+ Assert.Equal("line1\n", segments[0].Text);
+ Assert.True(segments[1].IsConflict);
+ Assert.Equal("ours line\n", segments[1].Ours);
+ Assert.Equal("theirs line\n", segments[1].Theirs);
+ Assert.Null(segments[1].Base);
+ Assert.Equal("line2\n", segments[2].Text);
+ Assert.True(ConflictMarkerParser.HasConflicts(text));
+ }
+
+ [Fact]
+ public void AcceptingOurs_Or_Theirs_ProducesTheResolvedFile()
+ {
+ const string text =
+ "line1\n<<<<<<< HEAD\nours line\n=======\ntheirs line\n>>>>>>> branch\nline2\n";
+
+ var segments = ConflictMarkerParser.Parse(text);
+
+ Assert.Equal("line1\nours line\nline2\n",
+ ConflictMarkerParser.Compose(segments, c => c.Ours));
+ Assert.Equal("line1\ntheirs line\nline2\n",
+ ConflictMarkerParser.Compose(segments, c => c.Theirs));
+ // "Accept both" = ours followed by theirs.
+ Assert.Equal("line1\nours line\ntheirs line\nline2\n",
+ ConflictMarkerParser.Compose(segments, c => c.Ours + c.Theirs));
+ }
+
+ [Fact]
+ public void Diff3Style_CapturesTheMergeBase()
+ {
+ const string text =
+ "a\n<<<<<<< HEAD\nX\n||||||| base\nB\n=======\nY\n>>>>>>> branch\nz\n";
+
+ var segments = ConflictMarkerParser.Parse(text);
+
+ Assert.Equal(3, segments.Count);
+ Assert.Equal("X\n", segments[1].Ours);
+ Assert.Equal("B\n", segments[1].Base);
+ Assert.Equal("Y\n", segments[1].Theirs);
+ }
+
+ [Fact]
+ public void MultipleConflicts_AreEachCaptured()
+ {
+ const string text =
+ "<<<<<<< HEAD\nA\n=======\nB\n>>>>>>> br\nmid\n<<<<<<< HEAD\nC\n=======\nD\n>>>>>>> br\n";
+
+ var segments = ConflictMarkerParser.Parse(text);
+
+ Assert.Equal(3, segments.Count);
+ Assert.True(segments[0].IsConflict);
+ Assert.Equal("A\n", segments[0].Ours);
+ Assert.Equal("mid\n", segments[1].Text);
+ Assert.True(segments[2].IsConflict);
+ Assert.Equal("D\n", segments[2].Theirs);
+ }
+
+ [Fact]
+ public void CrlfLineEndings_ArePreserved()
+ {
+ const string text =
+ "a\r\n<<<<<<< HEAD\r\nX\r\n=======\r\nY\r\n>>>>>>> br\r\nb\r\n";
+
+ var segments = ConflictMarkerParser.Parse(text);
+
+ Assert.Equal("a\r\nX\r\nb\r\n", ConflictMarkerParser.Compose(segments, c => c.Ours));
+ }
+
+ [Fact]
+ public void ConflictAtEndOfFile_WithoutTrailingNewline_IsParsed()
+ {
+ const string text = "a\n<<<<<<< HEAD\nX\n=======\nY\n>>>>>>> br";
+
+ var segments = ConflictMarkerParser.Parse(text);
+
+ Assert.Equal(2, segments.Count);
+ Assert.Equal("a\n", segments[0].Text);
+ Assert.True(segments[1].IsConflict);
+ Assert.Equal("X\n", segments[1].Ours);
+ Assert.Equal("Y\n", segments[1].Theirs);
+ }
+
+ [Fact]
+ public void SevenEqualsInOrdinaryText_IsNotTreatedAsAConflict()
+ {
+ const string text = "title\n=======\nbody\n";
+
+ var segments = ConflictMarkerParser.Parse(text);
+
+ var only = Assert.Single(segments);
+ Assert.False(only.IsConflict);
+ Assert.Equal(text, ConflictMarkerParser.Compose(segments, c => c.Ours));
+ }
+}
diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
index 3cb7f2f..092e1f9 100644
--- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
+++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
@@ -66,6 +66,7 @@ public abstract class StubWorkerClient : IWorkerClient
public virtual Task CancelReviewAsync(string taskId) => Task.CompletedTask;
public virtual Task StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty(), null));
public virtual Task GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty()));
+ public virtual Task GetMergeConflictDocumentsAsync(string taskId) => Task.FromResult(new MergeConflictDocumentsDto(taskId, System.Array.Empty()));
public virtual Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
public virtual Task ContinueConflictMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty(), null));
public virtual Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
diff --git a/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
index 3b6dbe5..4c63d46 100644
--- a/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
@@ -571,6 +571,51 @@ public class TaskMergeServiceTests : IDisposable
Assert.False(await new GitService().IsMidMergeAsync(repo.RepoDir));
}
+ [Fact]
+ public async Task GetConflictDocumentsAsync_ParsesLineLevelHunksWithDiff3Base()
+ {
+ if (!GitRepoFixture.IsGitAvailable()) return;
+ var repo = NewRepo();
+ var db = NewDb();
+ var (list, task) = await SeedListAndTask(db, repo.RepoDir, TaskStatus.WaitingForReview);
+
+ // A committed README gives the conflict a common ancestor, so diff3 records a base.
+ File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "line1\nbase\nline3\n");
+ GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
+ GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "base readme");
+
+ var wtMgr = BuildWorktreeManager(db);
+ var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
+ _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
+ File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "README.md"), "line1\nWORKTREE\nline3\n");
+ await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
+
+ File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "line1\nMAIN\nline3\n");
+ GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
+ GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "main edit");
+
+ var (svc, _) = BuildService(db);
+ var target = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
+
+ var merge = await svc.MergeAsync(
+ task.Id, target, removeWorktree: false, "merge",
+ leaveConflictsInTree: true, CancellationToken.None);
+ Assert.Equal(TaskMergeService.StatusConflict, merge.Status);
+
+ var docs = await svc.GetConflictDocumentsAsync(task.Id, CancellationToken.None);
+
+ var file = Assert.Single(docs.Files);
+ Assert.EndsWith("README.md", file.Path.Replace('\\', '/'));
+ Assert.False(file.IsBinary);
+
+ var conflict = Assert.Single(file.Segments.Where(s => s.IsConflict).ToList());
+ Assert.Contains("MAIN", conflict.Ours); // ours = current branch (merge target)
+ Assert.Contains("WORKTREE", conflict.Theirs); // theirs = incoming worktree branch
+ Assert.NotNull(conflict.Base);
+ Assert.Contains("base", conflict.Base!); // diff3 merge base
+ Assert.Contains(file.Segments, s => !s.IsConflict && s.Text.Contains("line1"));
+ }
+
[Fact]
public async Task ApproveAndMergeAsync_NoWorktree_MarksDone()
{
diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
index 638cc06..96bbbe3 100644
--- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
@@ -53,6 +53,7 @@ sealed class FakeWorkerClient : IWorkerClient
public Task MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty(), null));
public Task StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty(), null));
public Task GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty()));
+ public Task GetMergeConflictDocumentsAsync(string taskId) => Task.FromResult(new MergeConflictDocumentsDto(taskId, System.Array.Empty()));
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
public Task ContinueConflictMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty(), null));
public Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;