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 _hub; public HubBroadcaster(IHubContext 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 survivingTaskIds, string nextPhase) => _hub.Clients.All.SendAsync("HandoffRequested", taskId, survivingTaskIds, nextPhase); // start_list_handler (External MCP) -- the App creates the handler task and opens the triage // tile, exactly like the "Let Claude handle it" button. public Task ListHandlerStartRequested(string listId, IReadOnlyList taskIds) => _hub.Clients.All.SendAsync("ListHandlerStartRequested", listId, taskIds); // The 5h usage window is about to run out under a running list-handler session (HandlerUsageGuard). // Mission Control types `message` straight into that task's ConPTY pane, so the session can hand // off cleanly instead of dying mid-merge. No pane for the task = nothing happens. public Task HandlerUsageWarning(string handlerTaskId, string message) => _hub.Clients.All.SendAsync("HandlerUsageWarning", handlerTaskId, message); /// An agent wrote a note through MCP — the open notes editor reloads. public Task NotesUpdated() => _hub.Clients.All.SendAsync("NotesUpdated"); public Task ListUpdated(string listId) => _hub.Clients.All.SendAsync("ListUpdated", listId); 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); // Generic progress channel for long-running worker operations (merge phases, worktree // cleanup, startup recovery, planning integration, ...). opKey is the TaskId for a // task-bound operation, otherwise a stable string ("worktree-cleanup", "startup-recovery", // "planning-integration:"). current/total is shown as text by the UI, never a // progress bar -- most operations have no meaningful total. public Task OperationProgress(string opKey, string phase, int current, int total) => _hub.Clients.All.SendAsync("OperationProgress", opKey, phase, current, total); 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 files, bool externallyDriven) => _hub.Clients.All.SendAsync("PlanningMergeConflict", planningTaskId, subtaskId, files, externallyDriven); public Task PlanningMergeAborted(string planningTaskId, string? reason) => _hub.Clients.All.SendAsync("PlanningMergeAborted", planningTaskId, reason); 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); }