feat(merge): unify planning conflicts onto the resolver + 3-pane VM foundation

Route planning unit-merge conflicts through ConflictResolverViewModel
(OpenForPlanningAsync) and delete the old ConflictResolutionViewModel dialog.
Add active-file 3-pane reconstruction (MergeFile OursText/TheirsText/ResultText,
ActiveFile, SelectFileCommand, active-file readout) as the VM foundation for the
Rider-style editor. Seam preserved; Ui.Tests 128/128.
This commit is contained in:
Mika Kuns
2026-06-19 09:58:32 +02:00
parent 983c177c9a
commit 378a92c156
10 changed files with 288 additions and 337 deletions

View File

@@ -41,9 +41,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
public Func<MergeModalViewModel> ResolveMergeVm => _mergeVmFactory;
// Set by MainWindow to open the conflict resolution dialog.
public Func<ConflictResolutionViewModel, Task>? ShowConflictDialog { get; set; }
// Layer C seam: composition root sets the factory; MainWindow sets the dialog opener.
// The integrator connects Layer A/B's RequestConflictResolution(taskId, target) to this method.
public Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>? ConflictResolverFactory { get; set; }
@@ -146,44 +143,17 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
private void OnPlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> conflictedFiles)
{
// Already on UI thread (WorkerClient dispatches via Dispatcher.UIThread.Post).
_ = OpenConflictDialogAsync(planningTaskId, subtaskId, conflictedFiles);
// A unit-merge conflict resolves in the same in-app 3-way editor as a single-task merge.
_ = OpenPlanningConflictAsync(planningTaskId, subtaskId);
}
private async Task OpenConflictDialogAsync(string planningTaskId, string subtaskId, IReadOnlyList<string> conflictedFiles)
private async Task OpenPlanningConflictAsync(string planningTaskId, string subtaskId)
{
if (ShowConflictDialog == null || _dbFactory == null) return;
string subtaskTitle = subtaskId;
// The conflict lives in the list's working dir (the repo being merged into),
// not the subtask worktree. VS Code must open this folder to show the merge UI.
string repoDirectory = System.Environment.CurrentDirectory;
string targetBranch = Worker?.LastApproveTarget ?? "main";
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var entity = await ctx.Tasks
.Include(t => t.List)
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == subtaskId);
if (entity != null)
{
subtaskTitle = entity.Title;
if (entity.List?.WorkingDir is { } dir && !string.IsNullOrWhiteSpace(dir))
repoDirectory = dir;
}
}
catch { /* Non-fatal: fall back to subtaskId and cwd */ }
var vm = new ConflictResolutionViewModel(
Worker!,
planningTaskId,
subtaskTitle,
targetBranch,
conflictedFiles,
repoDirectory);
await ShowConflictDialog(vm);
if (ConflictResolverFactory is null || ShowConflictResolver is null) return;
var vm = ConflictResolverFactory(subtaskId);
var hasConflicts = await vm.OpenForPlanningAsync(planningTaskId, subtaskId);
if (hasConflicts)
await ShowConflictResolver(vm);
}
// For tests only — does NOT wire up events.