fix(merge): pass the stash consent to the conflict resolver so it opens on a dirty target

The resolver restarts the merge via StartConflictMerge, which had no autostash.
After a stash-retry approve reported 'conflict', the restart hit the same dirty
target, got 'dirty_target' and the resolver silently never opened. Thread the
consent through the hub, client, coordinator and resolver; surface a refusal in
the footer instead of dropping it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-10-02 17:25:52 +02:00
co-authored by Claude Sonnet 5.5
parent ff7197c410
commit feb0afa33a
16 changed files with 173 additions and 20 deletions
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using Xunit;
@@ -124,4 +125,26 @@ public class IslandsShellViewModelExternalMergeTests
// click must degrade gracefully rather than throw, same as the existing UI-driven path.
await vm.OpenExternalMergeConflictCommand.ExecuteAsync(null);
}
private sealed class DirtyTargetWorker : StubWorkerClient
{
public override Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch, bool autostash = false) =>
Task.FromResult(new MergeResultDto("dirty_target", Array.Empty<string>(), "target working tree has uncommitted changes"));
}
[Fact]
public async Task RequestConflictResolution_ResolverRefused_ShowsWhyInTheFooter()
{
// Regression: a refusal (dirty target, ...) left the resolver closed with no explanation anywhere.
var vm = new IslandsShellViewModel
{
Dialogs = new RecordingDialogService(),
ConflictResolverFactory = id => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(new DirtyTargetWorker(), id),
};
await vm.RequestConflictResolutionAsync("t1", "main");
Assert.Equal(WorkerLogLevel.Error, vm.WorkerLogLevel);
Assert.Contains("uncommitted changes", vm.WorkerLogText);
}
}