refactor(merge): single IMergeCoordinator replaces the 5 conflict seams

The RequestConflictResolution Func was declared on 5 VMs and hand-threaded shell->details->merge-section->diff->merge-modal. Replaced with a DI-singleton IMergeCoordinator (MergeCoordinator holder; shell wires its Handler at composition, breaking the shell<->island cycle). Invokers (MergeModal, DetailsIsland, WorktreesOverview) depend on the interface; the two pass-through VMs (DiffModal, MergeSection) drop the seam entirely. No behavior change; conflict-seam + batch tests rewired to assert via the coordinator.
This commit is contained in:
Mika Kuns
2026-06-22 17:18:57 +02:00
parent 3f9f047955
commit 5be4b5c5fb
15 changed files with 79 additions and 75 deletions

View File

@@ -52,6 +52,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
private readonly IWorkerClient _worker;
private readonly IServiceProvider _services;
private readonly INotesApi _notesApi;
private readonly IMergeCoordinator _merge;
// ── Section view models ───────────────────────────────────────────────────
public AgentSettingsSectionViewModel AgentSettings { get; }
@@ -343,13 +344,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
public bool HasReviewFeedback => !string.IsNullOrWhiteSpace(ReviewFeedback);
// Kept for backwards-compat surface — delegates to Merge.RequestConflictResolution
public Func<string, string, System.Threading.Tasks.Task>? RequestConflictResolution
{
get => Merge.RequestConflictResolution;
set => Merge.RequestConflictResolution = value;
}
private static string StatusToStateKey(ClaudeDo.Data.Models.TaskStatus status) => status switch
{
ClaudeDo.Data.Models.TaskStatus.Queued => "queued",
@@ -404,12 +398,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
IDbContextFactory<ClaudeDoDbContext> dbFactory,
IWorkerClient worker,
IServiceProvider services,
INotesApi notesApi)
INotesApi notesApi,
IMergeCoordinator merge)
{
_dbFactory = dbFactory;
_worker = worker;
_services = services;
_notesApi = notesApi;
_merge = merge;
AgentSettings = new AgentSettingsSectionViewModel(worker);
Merge = new MergeSectionViewModel(worker, services);
@@ -1151,20 +1147,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
var hasChildren = Subtasks.Count > 0 || ChildOutcomes.Count > 0;
var result = await _worker.ApproveReviewAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
if (!hasChildren && result?.Status == "conflict")
{
if (Merge.RequestConflictResolution is not null)
{
await Merge.RequestConflictResolution(Task.Id, Merge.SelectedMergeTarget ?? "");
}
else
{
var (text, _, _) = MergePreviewPresenter.Describe(
new MergePreviewDto("conflict", result.ConflictFiles, 0));
Merge.MergePreviewText = text;
Merge.MergeIsClean = false;
Merge.MergeIsConflict = true;
}
}
await _merge.ResolveConflictAsync(Task.Id, Merge.SelectedMergeTarget ?? "");
}
catch (Exception ex)
{