fix(ui): suppress auto-open conflict resolver during MCP-driven merges

review_task/continue_merge on a planning parent always leaves conflicts in
the tree, and the UI auto-opened the resolver on every PlanningMergeConflict
broadcast regardless of who started the merge -- so a running Claude session
resolving a unit-merge conflict could race a human editing the same shared
checkout in a resolver window neither of them asked for.

PlanningMergeOrchestrator.StartAsync now takes an externallyDriven flag (set
by ExternalMcpService's MCP-driven review_task path, left false for the UI's
ApproveReview) that rides along on the PlanningMergeConflict broadcast. The
UI only auto-opens the resolver when it's false; otherwise it shows a
persistent banner with a manual "Open resolver" button, cleared on
PlanningMergeAborted/PlanningCompleted. A new GetActiveExternalConflictsAsync
query (checked against GitService.IsMidMergeAsync rather than the in-memory
flag alone) lets the UI resync the banner on reconnect instead of trusting a
one-shot broadcast that isn't replayed after a restart.

The childless single-task conflict path was checked and needed no change --
it only broadcasts the generic TaskUpdated, never PlanningMergeConflict.
This commit is contained in:
mika kuns
2026-08-06 12:04:52 +02:00
parent 8247a749a0
commit 5d1d2d89d0
15 changed files with 379 additions and 15 deletions
+12
View File
@@ -76,6 +76,7 @@ public record WorktreeOverviewDto(
bool PathExistsOnDisk);
public record ForceRemoveResultDto(bool Removed, string? Reason);
public record PlanningMergeConflictStateDto(string PlanningTaskId, string SubtaskId);
public record PendingQuestionDto(string TaskId, string QuestionId, string Question);
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
@@ -916,6 +917,17 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
catch (InvalidOperationException ex) { throw new HubException(ex.Message); }
}
/// <summary>Unit merges currently paused on a conflict that an MCP session (not the UI)
/// started. The UI calls this on (re)connect to recover the "don't auto-open the resolver"
/// banner after a restart, since the one-shot PlanningMergeConflict broadcast isn't replayed.</summary>
public async Task<List<PlanningMergeConflictStateDto>> GetActiveExternalPlanningMergeConflicts()
{
var conflicts = await _planningMergeOrchestrator.GetActiveExternalConflictsAsync(CancellationToken.None);
return conflicts
.Select(c => new PlanningMergeConflictStateDto(c.PlanningTaskId, c.SubtaskId))
.ToList();
}
public async Task<List<PrimeScheduleDto>> ListPrimeSchedules()
{
using var ctx = _dbFactory.CreateDbContext();