Files
ClaudeDo/src/ClaudeDo.Worker/Lifecycle/RunningOperationRegistry.cs
T
mika kunsandClaude Sonnet 5.5 2dbec543df feat(mission-control): show running Worker operations in a Running now section
Registry of active verify/merge/rebase/worktree-creation operations in the Worker,
GetActiveOperations snapshot + OperationStarted/OperationEnded hub events, and a
Running now section in Mission Control with ticking elapsed time.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:08:43 +02:00

50 lines
2.0 KiB
C#

using System.Collections.Concurrent;
using ClaudeDo.Worker.Hub;
namespace ClaudeDo.Worker.Lifecycle;
/// <summary>
/// In-memory registry of Worker operations running right now (verify, merge, rebase, worktree
/// creation). <see cref="Begin"/> registers and broadcasts; disposing the returned scope removes
/// the entry, so a failed or thrown operation is always cleaned up by its caller's using/finally.
/// </summary>
public sealed class RunningOperationRegistry
{
private readonly ConcurrentDictionary<string, RunningOperationDto> _active = new();
private readonly HubBroadcaster? _broadcaster;
public RunningOperationRegistry(HubBroadcaster? broadcaster = null) => _broadcaster = broadcaster;
/// <summary>The operations running right now, oldest first.</summary>
public IReadOnlyList<RunningOperationDto> Snapshot() =>
_active.Values.OrderBy(o => o.StartedAt).ToList();
/// <summary>Registers an operation; dispose the result when it ends (idempotent).</summary>
public IDisposable Begin(string kind, string title, string? taskId = null, int? taskNumber = null, string? listId = null)
{
var op = new RunningOperationDto(Guid.NewGuid().ToString("N"), kind, taskId, taskNumber, listId, title, DateTime.UtcNow);
_active[op.Id] = op;
_ = Broadcast(_broadcaster?.OperationStarted(op));
return new Scope(this, op.Id);
}
// A broadcast failure must never fail the operation it describes.
private static async Task Broadcast(Task? send)
{
if (send is null) return;
try { await send; } catch { /* no clients / hub down */ }
}
private sealed class Scope(RunningOperationRegistry owner, string id) : IDisposable
{
private int _disposed;
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
owner._active.TryRemove(id, out _);
_ = Broadcast(owner._broadcaster?.OperationEnded(id));
}
}
}