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:
mika kuns
2026-04-29 10:39:44 +02:00
parent cfbe2fd7e3
commit 121e8cd476
4 changed files with 70 additions and 2 deletions

View File

@@ -7,7 +7,9 @@ using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Prime;
using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.State;
using ClaudeDo.Worker.Worktrees;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
@@ -55,6 +57,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
private readonly PlanningMergeOrchestrator _planningMergeOrchestrator;
private readonly PlanningChainCoordinator _planningChain;
private readonly IPrimeScheduleSignal _primeSignal;
private readonly ITaskStateService _state;
public WorkerHub(
QueueService queue,
@@ -71,7 +74,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
PlanningAggregator planningAggregator,
PlanningMergeOrchestrator planningMergeOrchestrator,
PlanningChainCoordinator planningChain,
IPrimeScheduleSignal primeSignal)
IPrimeScheduleSignal primeSignal,
ITaskStateService state)
{
_queue = queue;
_waker = waker;
@@ -88,6 +92,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
_planningMergeOrchestrator = planningMergeOrchestrator;
_planningChain = planningChain;
_primeSignal = primeSignal;
_state = state;
}
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);
}
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)
{
using var ctx = _dbFactory.CreateDbContext();