Files
ClaudeDo/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs
T
Mika Kuns 08d1797ead chore(claude-do): Mission Control: allgemeiner Session-Handoff an eine frische
## Ziel

Jede Mission-Control-Session (Ad-hoc UND Task-Session) soll ihre Arbeit an eine frische Session
uebergeben koennen: "gib das ab an eine neue Session". Die alte Kachel wird geschlossen, an
derselben Position oeffnet eine frische Session im selben cwd, die mit einem Brief startet, den
die abgebende Session selbst geschrieben hat.

Motivation: eine lange Session mit vollem/verrottetem Kontex

ClaudeDo-Task: 8b846deb-ad10-4243-9272-9aea2a01ec93
2026-09-28 11:57:37 +02:00

114 lines
6.2 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, string nextPhase) =>
_hub.Clients.All.SendAsync("HandoffRequested", taskId, survivingTaskIds, nextPhase);
/// <summary>A Mission Control session called handoff_session -- the UI replaces the tile whose
/// IdentityKey is paneId with a fresh session that starts from the brief.</summary>
public Task SessionHandoffRequested(string paneId, string briefPath) =>
_hub.Clients.All.SendAsync("SessionHandoffRequested", paneId, briefPath);
// 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<string> 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);
/// <summary>An agent wrote a note through MCP — the open notes editor reloads.</summary>
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:<taskId>"). 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<string> 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);
}