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>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
488c28a9d5
commit
2dbec543df
@@ -0,0 +1,49 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user