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-26 16:11:48 +02:00
parent 3f9f047955
commit 5be4b5c5fb
15 changed files with 79 additions and 75 deletions
@@ -9,6 +9,7 @@ namespace ClaudeDo.Ui.ViewModels.Modals;
public sealed partial class MergeModalViewModel : ViewModelBase
{
private readonly IWorkerClient _worker;
private readonly IMergeCoordinator _merge;
public string TaskId { get; set; } = "";
public string TaskTitle { get; set; } = "";
@@ -28,10 +29,6 @@ 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<string, string, Task>? 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; }
@@ -39,9 +36,10 @@ public sealed partial class MergeModalViewModel : ViewModelBase
/// 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)
public MergeModalViewModel(IWorkerClient worker, IMergeCoordinator merge)
{
_worker = worker;
_merge = merge;
}
public async Task InitializeAsync(string taskId, string taskTitle)
@@ -103,21 +101,11 @@ public sealed partial class MergeModalViewModel : ViewModelBase
});
break;
case "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");
}
// MergeTask aborted cleanly; hand the conflict to the in-app 3-pane editor,
// which re-starts the merge leaving conflicts in the tree.
RoutedToResolver = true;
CloseAction?.Invoke();
await _merge.ResolveConflictAsync(TaskId, SelectedBranch!);
break;
case "blocked":
ErrorMessage = Loc.T("vm.merge.blocked", result.ErrorMessage ?? "");