From 7c312161bb9551a6617316bf3bb84961e3f41e1b Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 22 Apr 2026 13:16:46 +0200 Subject: [PATCH] feat(worker): add hub methods for list and task agent settings Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Worker/Hub/HubBroadcaster.cs | 3 + src/ClaudeDo.Worker/Hub/WorkerHub.cs | 70 +++++++++++++++++++ .../Hub/AgentSettingsHubTests.cs | 47 +++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs diff --git a/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs b/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs index 93aa7ef..2e01d17 100644 --- a/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs +++ b/src/ClaudeDo.Worker/Hub/HubBroadcaster.cs @@ -23,6 +23,9 @@ public sealed class HubBroadcaster public Task TaskUpdated(string taskId) => _hub.Clients.All.SendAsync("TaskUpdated", taskId); + public Task ListUpdated(string listId) => + _hub.Clients.All.SendAsync("ListUpdated", listId); + public Task RunCreated(string taskId, int runNumber, bool isRetry) => _hub.Clients.All.SendAsync("RunCreated", taskId, runNumber, isRetry); } diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 96f25e1..02c2e73 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -24,6 +24,10 @@ public record WorktreeCleanupDto(int Removed); public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks); public record MergeResultDto(string Status, IReadOnlyList ConflictFiles, string? ErrorMessage); public record MergeTargetsDto(string DefaultBranch, IReadOnlyList LocalBranches); +public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType); +public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath); +public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath); +public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath); public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub { @@ -205,4 +209,70 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub throw new HubException(ex.Message); } } + + public async Task UpdateList(UpdateListDto dto) + { + using var ctx = _dbFactory.CreateDbContext(); + var repo = new ListRepository(ctx); + var entity = await repo.GetByIdAsync(dto.Id); + if (entity is null) throw new HubException("list not found"); + + entity.Name = dto.Name; + entity.WorkingDir = string.IsNullOrWhiteSpace(dto.WorkingDir) ? null : dto.WorkingDir; + entity.DefaultCommitType = string.IsNullOrWhiteSpace(dto.DefaultCommitType) ? "chore" : dto.DefaultCommitType; + await repo.UpdateAsync(entity); + + await _broadcaster.ListUpdated(dto.Id); + } + + public async Task UpdateListConfig(UpdateListConfigDto dto) + { + using var ctx = _dbFactory.CreateDbContext(); + var repo = new ListRepository(ctx); + + var model = Nullify(dto.Model); + var systemPrompt = Nullify(dto.SystemPrompt); + var agentPath = Nullify(dto.AgentPath); + + if (model is null && systemPrompt is null && agentPath is null) + { + await repo.DeleteConfigAsync(dto.ListId); + } + else + { + await repo.SetConfigAsync(new ListConfigEntity + { + ListId = dto.ListId, + Model = model, + SystemPrompt = systemPrompt, + AgentPath = agentPath, + }); + } + + await _broadcaster.ListUpdated(dto.ListId); + } + + public async Task GetListConfig(string listId) + { + using var ctx = _dbFactory.CreateDbContext(); + var repo = new ListRepository(ctx); + var config = await repo.GetConfigAsync(listId); + if (config is null) return null; + return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath); + } + + public async Task UpdateTaskAgentSettings(UpdateTaskAgentSettingsDto dto) + { + using var ctx = _dbFactory.CreateDbContext(); + var repo = new TaskRepository(ctx); + await repo.UpdateAgentSettingsAsync( + dto.TaskId, + Nullify(dto.Model), + Nullify(dto.SystemPrompt), + Nullify(dto.AgentPath)); + + await _broadcaster.TaskUpdated(dto.TaskId); + } + + private static string? Nullify(string? s) => string.IsNullOrWhiteSpace(s) ? null : s; } diff --git a/tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs new file mode 100644 index 0000000..60cb54e --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Hub/AgentSettingsHubTests.cs @@ -0,0 +1,47 @@ +using ClaudeDo.Data.Models; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Tests.Infrastructure; +using Xunit; + +namespace ClaudeDo.Worker.Tests.Hub; + +public sealed class AgentSettingsHubTests : IDisposable +{ + private readonly DbFixture _db = new(); + private readonly ClaudeDo.Data.ClaudeDoDbContext _ctx; + private readonly ListRepository _repo; + + public AgentSettingsHubTests() + { + _ctx = _db.CreateContext(); + _repo = new ListRepository(_ctx); + } + + public void Dispose() + { + _ctx.Dispose(); + _db.Dispose(); + } + + [Fact] + public async Task UpdateListConfig_AllNull_DeletesRow() + { + var listId = Guid.NewGuid().ToString(); + await _repo.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); + await _repo.SetConfigAsync(new ListConfigEntity + { + ListId = listId, Model = "opus", SystemPrompt = null, AgentPath = null, + }); + + string? model = null, sp = null, ap = null; + if (model is null && sp is null && ap is null) + await _repo.DeleteConfigAsync(listId); + else + await _repo.SetConfigAsync(new ListConfigEntity + { + ListId = listId, Model = model, SystemPrompt = sp, AgentPath = ap, + }); + + Assert.Null(await _repo.GetConfigAsync(listId)); + } +}