feat(worker): add hub methods to set task status and tags freely
Adds ForceSetStatusAsync on ITaskStateService (no transition guards) plus SetTaskStatus / SetTaskTags / GetAllTags hub methods so the UI can edit a task's status and tags directly. PlanningHubTests ctor updated for the new ITaskStateService dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,9 @@ using ClaudeDo.Worker.Lifecycle;
|
|||||||
using ClaudeDo.Worker.Planning;
|
using ClaudeDo.Worker.Planning;
|
||||||
using ClaudeDo.Worker.Prime;
|
using ClaudeDo.Worker.Prime;
|
||||||
using ClaudeDo.Worker.Queue;
|
using ClaudeDo.Worker.Queue;
|
||||||
|
using ClaudeDo.Worker.State;
|
||||||
using ClaudeDo.Worker.Worktrees;
|
using ClaudeDo.Worker.Worktrees;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly PlanningMergeOrchestrator _planningMergeOrchestrator;
|
private readonly PlanningMergeOrchestrator _planningMergeOrchestrator;
|
||||||
private readonly PlanningChainCoordinator _planningChain;
|
private readonly PlanningChainCoordinator _planningChain;
|
||||||
private readonly IPrimeScheduleSignal _primeSignal;
|
private readonly IPrimeScheduleSignal _primeSignal;
|
||||||
|
private readonly ITaskStateService _state;
|
||||||
|
|
||||||
public WorkerHub(
|
public WorkerHub(
|
||||||
QueueService queue,
|
QueueService queue,
|
||||||
@@ -71,7 +74,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
PlanningAggregator planningAggregator,
|
PlanningAggregator planningAggregator,
|
||||||
PlanningMergeOrchestrator planningMergeOrchestrator,
|
PlanningMergeOrchestrator planningMergeOrchestrator,
|
||||||
PlanningChainCoordinator planningChain,
|
PlanningChainCoordinator planningChain,
|
||||||
IPrimeScheduleSignal primeSignal)
|
IPrimeScheduleSignal primeSignal,
|
||||||
|
ITaskStateService state)
|
||||||
{
|
{
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
_waker = waker;
|
_waker = waker;
|
||||||
@@ -88,6 +92,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_planningMergeOrchestrator = planningMergeOrchestrator;
|
_planningMergeOrchestrator = planningMergeOrchestrator;
|
||||||
_planningChain = planningChain;
|
_planningChain = planningChain;
|
||||||
_primeSignal = primeSignal;
|
_primeSignal = primeSignal;
|
||||||
|
_state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task QueuePlanningSubtasksAsync(string parentTaskId)
|
public async Task QueuePlanningSubtasksAsync(string parentTaskId)
|
||||||
@@ -318,6 +323,49 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath);
|
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetTaskStatus(string taskId, string status)
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse<TaskStatus>(status, ignoreCase: true, out var parsed))
|
||||||
|
throw new HubException($"unknown status: {status}");
|
||||||
|
var result = await _state.ForceSetStatusAsync(taskId, parsed, Context.ConnectionAborted);
|
||||||
|
if (!result.Ok) throw new HubException(result.Reason ?? "set status failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetTaskTags(string taskId, string[] tagNames)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await ctx.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
if (entity is null) throw new HubException("task not found");
|
||||||
|
|
||||||
|
var desired = (tagNames ?? Array.Empty<string>())
|
||||||
|
.Select(n => n?.Trim().ToLowerInvariant() ?? "")
|
||||||
|
.Where(n => n.Length > 0)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
foreach (var t in entity.Tags.Where(t => !desired.Contains(t.Name)).ToList())
|
||||||
|
entity.Tags.Remove(t);
|
||||||
|
|
||||||
|
var existingByName = await ctx.Tags
|
||||||
|
.Where(t => desired.Contains(t.Name))
|
||||||
|
.ToListAsync();
|
||||||
|
foreach (var name in desired)
|
||||||
|
{
|
||||||
|
if (entity.Tags.Any(t => t.Name == name)) continue;
|
||||||
|
var tag = existingByName.FirstOrDefault(t => t.Name == name)
|
||||||
|
?? new TagEntity { Name = name };
|
||||||
|
if (tag.Id == 0) ctx.Tags.Add(tag);
|
||||||
|
entity.Tags.Add(tag);
|
||||||
|
}
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetAllTags()
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
return await ctx.Tags.OrderBy(t => t.Name).Select(t => t.Name).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task UpdateTaskAgentSettings(UpdateTaskAgentSettingsDto dto)
|
public async Task UpdateTaskAgentSettings(UpdateTaskAgentSettingsDto dto)
|
||||||
{
|
{
|
||||||
using var ctx = _dbFactory.CreateDbContext();
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ public interface ITaskStateService
|
|||||||
Task<TransitionResult> CancelAsync(string taskId, DateTime finishedAt, CancellationToken ct);
|
Task<TransitionResult> CancelAsync(string taskId, DateTime finishedAt, CancellationToken ct);
|
||||||
Task<TransitionResult> ResetToIdleAsync(string taskId, CancellationToken ct);
|
Task<TransitionResult> ResetToIdleAsync(string taskId, CancellationToken ct);
|
||||||
|
|
||||||
|
Task<TransitionResult> ForceSetStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status, CancellationToken ct);
|
||||||
|
|
||||||
Task<TransitionResult> StartPlanningAsync(string parentId, CancellationToken ct);
|
Task<TransitionResult> StartPlanningAsync(string parentId, CancellationToken ct);
|
||||||
Task<TransitionResult> FinalizePlanningAsync(string parentId, CancellationToken ct);
|
Task<TransitionResult> FinalizePlanningAsync(string parentId, CancellationToken ct);
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,24 @@ public sealed class TaskStateService : ITaskStateService
|
|||||||
return new TransitionResult(true, null);
|
return new TransitionResult(true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unconditional status write — bypasses transition rules. Used by the UI's
|
||||||
|
// "set status freely" affordance; intentionally no guards (caller may strand
|
||||||
|
// the runner if used while a task is executing).
|
||||||
|
public async Task<TransitionResult> ForceSetStatusAsync(string taskId, TaskStatus status, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
var affected = await ctx.Tasks
|
||||||
|
.Where(t => t.Id == taskId)
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, status), ct);
|
||||||
|
|
||||||
|
if (affected == 0)
|
||||||
|
return new TransitionResult(false, "Task not found.");
|
||||||
|
|
||||||
|
if (status == TaskStatus.Queued) _waker.Wake();
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return new TransitionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TransitionResult> StartPlanningAsync(string parentId, CancellationToken ct)
|
public async Task<TransitionResult> StartPlanningAsync(string parentId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public sealed class PlanningHubTests : IDisposable
|
|||||||
{
|
{
|
||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||||
_planning, _launcher, null!, null!, null!, null!);
|
_planning, _launcher, null!, null!, null!, null!, null!);
|
||||||
hub.Clients = new FakeHubCallerClients(_proxy);
|
hub.Clients = new FakeHubCallerClients(_proxy);
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return hub;
|
return hub;
|
||||||
|
|||||||
Reference in New Issue
Block a user