Files
ClaudeDo/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs
T
mika kuns 5d1d2d89d0 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.
2026-08-06 12:04:52 +02:00

90 lines
4.4 KiB
C#

using ClaudeDo.Data.Models;
using ClaudeDo.Worker.Prime;
using ClaudeDo.Worker.Refine;
using Microsoft.AspNetCore.SignalR;
namespace ClaudeDo.Worker.Hub;
public sealed class HubBroadcaster : IPrimeBroadcaster, IRefineBroadcaster
{
private readonly IHubContext<WorkerHub> _hub;
public HubBroadcaster(IHubContext<WorkerHub> hub) => _hub = hub;
public Task TaskStarted(string slot, string taskId, DateTime startedAt) =>
_hub.Clients.All.SendAsync("TaskStarted", slot, taskId, startedAt);
public Task TaskFinished(string slot, string taskId, string status, DateTime finishedAt) =>
_hub.Clients.All.SendAsync("TaskFinished", slot, taskId, status, finishedAt);
public Task TaskMessage(string taskId, string ndjsonLine) =>
_hub.Clients.All.SendAsync("TaskMessage", taskId, ndjsonLine);
public Task WorktreeUpdated(string taskId) =>
_hub.Clients.All.SendAsync("WorktreeUpdated", taskId);
public Task TaskUpdated(string taskId) =>
_hub.Clients.All.SendAsync("TaskUpdated", taskId);
public Task TaskQuestionAsked(string taskId, string questionId, string question) =>
_hub.Clients.All.SendAsync("TaskQuestionAsked", taskId, questionId, question);
public Task TaskQuestionResolved(string taskId, string questionId) =>
_hub.Clients.All.SendAsync("TaskQuestionResolved", taskId, questionId);
// A running list-handler session called handoff_list_handler at the end of Phase 2 -- the UI
// opens a second ConPTY tile for the same handler task id to carry out Phases 3-5.
public Task HandoffRequested(string taskId, IReadOnlyList<string> survivingTaskIds) =>
_hub.Clients.All.SendAsync("HandoffRequested", taskId, survivingTaskIds);
public Task ListUpdated(string listId) =>
_hub.Clients.All.SendAsync("ListUpdated", listId);
public Task RunCreated(string taskId, int runNumber, bool isRetry) =>
_hub.Clients.All.SendAsync("RunCreated", taskId, runNumber, isRetry);
public Task UsageUpdated(UsageSnapshotDto snapshot) =>
_hub.Clients.All.SendAsync("UsageUpdated", snapshot);
public Task WorkerLog(string message, WorkerLogLevel level, DateTime timestampUtc) =>
_hub.Clients.All.SendAsync("WorkerLog", message, level, timestampUtc);
public Task PlanningMergeStarted(string planningTaskId, string targetBranch) =>
_hub.Clients.All.SendAsync("PlanningMergeStarted", planningTaskId, targetBranch);
public Task PlanningSubtaskMerged(string planningTaskId, string subtaskId) =>
_hub.Clients.All.SendAsync("PlanningSubtaskMerged", planningTaskId, subtaskId);
public Task PlanningMergeConflict(
string planningTaskId, string subtaskId, IReadOnlyList<string> files, bool externallyDriven) =>
_hub.Clients.All.SendAsync("PlanningMergeConflict", planningTaskId, subtaskId, files, externallyDriven);
public Task PlanningMergeAborted(string planningTaskId) =>
_hub.Clients.All.SendAsync("PlanningMergeAborted", planningTaskId);
public Task PlanningCompleted(string planningTaskId) =>
_hub.Clients.All.SendAsync("PlanningCompleted", planningTaskId);
public Task PrimeFired(Guid scheduleId, bool success, string message, DateTimeOffset firedAt) =>
_hub.Clients.All.SendAsync("PrimeFired", scheduleId, success, message, firedAt);
Task IPrimeBroadcaster.PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt) =>
PrimeFired(scheduleId, success, message, firedAt);
public Task PrepStarted() => _hub.Clients.All.SendAsync("PrepStarted");
public Task PrepLine(string line) => _hub.Clients.All.SendAsync("PrepLine", line);
public Task PrepFinished(bool success) => _hub.Clients.All.SendAsync("PrepFinished", success);
Task IPrimeBroadcaster.PrepStartedAsync() => PrepStarted();
Task IPrimeBroadcaster.PrepLineAsync(string line) => PrepLine(line);
Task IPrimeBroadcaster.PrepFinishedAsync(bool success) => PrepFinished(success);
public Task RefineStarted(string taskId) => _hub.Clients.All.SendAsync("RefineStarted", taskId);
public Task RefineFinished(string taskId, bool success, string? error) =>
_hub.Clients.All.SendAsync("RefineFinished", taskId, success, error);
Task IRefineBroadcaster.RefineStartedAsync(string taskId) => RefineStarted(taskId);
Task IRefineBroadcaster.RefineFinishedAsync(string taskId, bool success, string? error) =>
RefineFinished(taskId, success, error);
}