refactor: extract interfaces to Interfaces folders and consolidate filters
Move interface declarations into per-area Interfaces/ subfolders, merge the small task-list filter classes into StatusFilter/SmartFlagFilter, and simplify related services, converters and hub DTO handling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
10
src/ClaudeDo.Worker/External/ConfigMcpTools.cs
vendored
10
src/ClaudeDo.Worker/External/ConfigMcpTools.cs
vendored
@@ -36,9 +36,9 @@ public sealed class ConfigMcpTools
|
||||
_ = await _lists.GetByIdAsync(listId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"List {listId} not found.");
|
||||
|
||||
var m = Nullify(model);
|
||||
var sp = Nullify(systemPrompt);
|
||||
var ap = Nullify(agentPath);
|
||||
var m = model.NullIfBlank();
|
||||
var sp = systemPrompt.NullIfBlank();
|
||||
var ap = agentPath.NullIfBlank();
|
||||
|
||||
if (m is null && sp is null && ap is null)
|
||||
await _lists.DeleteConfigAsync(listId, cancellationToken);
|
||||
@@ -58,9 +58,7 @@ public sealed class ConfigMcpTools
|
||||
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
|
||||
await _tasks.UpdateAgentSettingsAsync(taskId, Nullify(model), Nullify(systemPrompt), Nullify(agentPath), cancellationToken);
|
||||
await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), cancellationToken);
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
}
|
||||
|
||||
private static string? Nullify(string? s) => string.IsNullOrWhiteSpace(s) ? null : s;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,22 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
_state = state;
|
||||
}
|
||||
|
||||
// Maps the two exceptions service methods throw into client-facing HubExceptions:
|
||||
// KeyNotFoundException -> notFoundMessage, InvalidOperationException -> its own message.
|
||||
private static async Task HubGuard(Func<Task> action, string notFoundMessage = "task not found")
|
||||
{
|
||||
try { await action(); }
|
||||
catch (KeyNotFoundException) { throw new HubException(notFoundMessage); }
|
||||
catch (InvalidOperationException ex) { throw new HubException(ex.Message); }
|
||||
}
|
||||
|
||||
private static async Task<T> HubGuard<T>(Func<Task<T>> action, string notFoundMessage = "task not found")
|
||||
{
|
||||
try { return await action(); }
|
||||
catch (KeyNotFoundException) { throw new HubException(notFoundMessage); }
|
||||
catch (InvalidOperationException ex) { throw new HubException(ex.Message); }
|
||||
}
|
||||
|
||||
public async Task QueuePlanningSubtasksAsync(string parentTaskId)
|
||||
{
|
||||
try
|
||||
@@ -157,37 +173,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> ContinueTask(string taskId, string followUpPrompt)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _queue.ContinueTask(taskId, followUpPrompt);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw new HubException(ex.Message);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
throw new HubException("task not found");
|
||||
}
|
||||
}
|
||||
public Task<string> ContinueTask(string taskId, string followUpPrompt)
|
||||
=> HubGuard(() => _queue.ContinueTask(taskId, followUpPrompt));
|
||||
|
||||
public async Task ResetTask(string taskId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _resetService.ResetAsync(taskId, CancellationToken.None);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw new HubException(ex.Message);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
throw new HubException("task not found");
|
||||
}
|
||||
}
|
||||
public Task ResetTask(string taskId)
|
||||
=> HubGuard(() => _resetService.ResetAsync(taskId, CancellationToken.None));
|
||||
|
||||
public bool CancelTask(string taskId) => _queue.CancelTask(taskId);
|
||||
|
||||
@@ -285,10 +275,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
return new ForceRemoveResultDto(result.Removed, result.Reason);
|
||||
}
|
||||
|
||||
public async Task<MergeResultDto> MergeTask(
|
||||
public Task<MergeResultDto> MergeTask(
|
||||
string taskId, string targetBranch, bool removeWorktree, string commitMessage)
|
||||
{
|
||||
try
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var r = await _mergeService.MergeAsync(
|
||||
taskId,
|
||||
@@ -297,33 +286,14 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
string.IsNullOrWhiteSpace(commitMessage) ? "Merge task" : commitMessage,
|
||||
CancellationToken.None);
|
||||
return new MergeResultDto(r.Status, r.ConflictFiles, r.ErrorMessage);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
throw new HubException("task not found");
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw new HubException(ex.Message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public async Task<MergeTargetsDto> GetMergeTargets(string taskId)
|
||||
{
|
||||
try
|
||||
public Task<MergeTargetsDto> GetMergeTargets(string taskId)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var t = await _mergeService.GetTargetsAsync(taskId, CancellationToken.None);
|
||||
return new MergeTargetsDto(t.DefaultBranch, t.LocalBranches);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
throw new HubException("task not found");
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw new HubException(ex.Message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public async Task UpdateList(UpdateListDto dto)
|
||||
{
|
||||
@@ -345,9 +315,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
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);
|
||||
var model = dto.Model.NullIfBlank();
|
||||
var systemPrompt = dto.SystemPrompt.NullIfBlank();
|
||||
var agentPath = dto.AgentPath.NullIfBlank();
|
||||
|
||||
if (model is null && systemPrompt is null && agentPath is null)
|
||||
{
|
||||
@@ -394,9 +364,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
var repo = new TaskRepository(ctx);
|
||||
await repo.UpdateAgentSettingsAsync(
|
||||
dto.TaskId,
|
||||
Nullify(dto.Model),
|
||||
Nullify(dto.SystemPrompt),
|
||||
Nullify(dto.AgentPath));
|
||||
dto.Model.NullIfBlank(),
|
||||
dto.SystemPrompt.NullIfBlank(),
|
||||
dto.AgentPath.NullIfBlank());
|
||||
|
||||
await _broadcaster.TaskUpdated(dto.TaskId);
|
||||
}
|
||||
@@ -449,21 +419,16 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
public Task<int> GetPendingDraftCountAsync(string taskId)
|
||||
=> _planning.GetPendingDraftCountAsync(taskId, Context.ConnectionAborted);
|
||||
|
||||
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregate(string planningTaskId)
|
||||
{
|
||||
try
|
||||
public Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregate(string planningTaskId)
|
||||
=> HubGuard<IReadOnlyList<SubtaskDiffDto>>(async () =>
|
||||
{
|
||||
var diffs = await _planningAggregator.GetAggregatedDiffAsync(planningTaskId, CancellationToken.None);
|
||||
return diffs.Select(d => new SubtaskDiffDto(
|
||||
d.SubtaskId, d.Title, d.BranchName, d.BaseCommit, d.HeadCommit, d.DiffStat, d.UnifiedDiff)).ToList();
|
||||
}
|
||||
catch (KeyNotFoundException) { throw new HubException("planning task not found"); }
|
||||
catch (InvalidOperationException ex) { throw new HubException(ex.Message); }
|
||||
}
|
||||
}, "planning task not found");
|
||||
|
||||
public async Task<CombinedDiffResultDto> BuildPlanningIntegrationBranch(string planningTaskId, string targetBranch)
|
||||
{
|
||||
try
|
||||
public Task<CombinedDiffResultDto> BuildPlanningIntegrationBranch(string planningTaskId, string targetBranch)
|
||||
=> HubGuard(async () =>
|
||||
{
|
||||
var result = await _planningAggregator.BuildIntegrationBranchAsync(
|
||||
planningTaskId, targetBranch ?? "", CancellationToken.None);
|
||||
@@ -475,17 +440,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
false, null, null, f.Value.FirstConflictSubtaskId, f.Value.ConflictedFiles),
|
||||
_ => throw new InvalidOperationException("unknown result type"),
|
||||
};
|
||||
}
|
||||
catch (KeyNotFoundException) { throw new HubException("planning task not found"); }
|
||||
catch (InvalidOperationException ex) { throw new HubException(ex.Message); }
|
||||
}
|
||||
}, "planning task not found");
|
||||
|
||||
public async Task MergeAllPlanning(string planningTaskId, string targetBranch)
|
||||
{
|
||||
try { await _planningMergeOrchestrator.StartAsync(planningTaskId, targetBranch ?? "", CancellationToken.None); }
|
||||
catch (KeyNotFoundException) { throw new HubException("planning task not found"); }
|
||||
catch (InvalidOperationException ex) { throw new HubException(ex.Message); }
|
||||
}
|
||||
public Task MergeAllPlanning(string planningTaskId, string targetBranch)
|
||||
=> HubGuard(() => _planningMergeOrchestrator.StartAsync(planningTaskId, targetBranch ?? "", CancellationToken.None),
|
||||
"planning task not found");
|
||||
|
||||
public async Task ContinuePlanningMerge(string planningTaskId)
|
||||
{
|
||||
@@ -537,6 +496,4 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
await new PrimeScheduleRepository(ctx).DeleteAsync(id);
|
||||
_primeSignal.Signal();
|
||||
}
|
||||
|
||||
private static string? Nullify(string? s) => string.IsNullOrWhiteSpace(s) ? null : s;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,27 @@ public sealed class TaskMergeService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private async Task<(TaskEntity Task, ListEntity List, WorktreeEntity? Worktree)> LoadMergeContextAsync(
|
||||
string taskId, CancellationToken ct)
|
||||
{
|
||||
using var ctx = _dbFactory.CreateDbContext();
|
||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
|
||||
?? throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||
var list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
|
||||
?? throw new InvalidOperationException("List not found.");
|
||||
var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, ct);
|
||||
return (task, list, wt);
|
||||
}
|
||||
|
||||
private async Task MarkWorktreeMergedAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
await new WorktreeRepository(ctx).SetStateAsync(taskId, WorktreeState.Merged, ct);
|
||||
}
|
||||
await _broadcaster.WorktreeUpdated(taskId);
|
||||
}
|
||||
|
||||
public async Task<MergeResult> MergeAsync(
|
||||
string taskId,
|
||||
string targetBranch,
|
||||
@@ -49,18 +70,7 @@ public sealed class TaskMergeService
|
||||
bool leaveConflictsInTree,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TaskEntity task;
|
||||
ListEntity list;
|
||||
WorktreeEntity? wt;
|
||||
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
|
||||
?? throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||
list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
|
||||
?? throw new InvalidOperationException("List not found.");
|
||||
wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, ct);
|
||||
}
|
||||
var (task, list, wt) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
if (task.Status == TaskStatus.Running)
|
||||
return Blocked("task is running");
|
||||
@@ -134,11 +144,7 @@ public sealed class TaskMergeService
|
||||
}
|
||||
}
|
||||
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
await new WorktreeRepository(ctx).SetStateAsync(taskId, WorktreeState.Merged, ct);
|
||||
}
|
||||
await _broadcaster.WorktreeUpdated(taskId);
|
||||
await MarkWorktreeMergedAsync(taskId, ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Merged task {TaskId} branch {Branch} into {Target} (remove worktree: {Remove})",
|
||||
@@ -158,18 +164,7 @@ public sealed class TaskMergeService
|
||||
|
||||
public async Task<MergeResult> ContinueMergeAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
TaskEntity task;
|
||||
ListEntity list;
|
||||
WorktreeEntity? wt;
|
||||
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
|
||||
?? throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||
list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
|
||||
?? throw new InvalidOperationException("List not found.");
|
||||
wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, ct);
|
||||
}
|
||||
var (_, list, wt) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
if (wt is null) return Blocked("task has no worktree");
|
||||
if (wt.State != WorktreeState.Active) return Blocked($"worktree state is {wt.State}");
|
||||
@@ -186,11 +181,7 @@ public sealed class TaskMergeService
|
||||
try { await _git.CommitAsync(list.WorkingDir, $"Merge branch '{wt.BranchName}'", ct); }
|
||||
catch (Exception ex) { return Blocked($"commit failed: {ex.Message}"); }
|
||||
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
await new WorktreeRepository(ctx).SetStateAsync(taskId, WorktreeState.Merged, ct);
|
||||
}
|
||||
await _broadcaster.WorktreeUpdated(taskId);
|
||||
await MarkWorktreeMergedAsync(taskId, ct);
|
||||
_logger.LogInformation("Continued merge of task {TaskId} branch {Branch}", taskId, wt.BranchName);
|
||||
|
||||
return new MergeResult(StatusMerged, Array.Empty<string>(), null);
|
||||
@@ -198,17 +189,7 @@ public sealed class TaskMergeService
|
||||
|
||||
public async Task<MergeResult> AbortMergeAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
ListEntity list;
|
||||
WorktreeEntity? wt;
|
||||
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
|
||||
?? throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||
list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
|
||||
?? throw new InvalidOperationException("List not found.");
|
||||
wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(taskId, ct);
|
||||
}
|
||||
var (_, list, wt) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
if (wt is null) return Blocked("task has no worktree");
|
||||
if (wt.State != WorktreeState.Active) return Blocked($"worktree state is {wt.State}");
|
||||
@@ -225,15 +206,7 @@ public sealed class TaskMergeService
|
||||
|
||||
public async Task<MergeTargets> GetTargetsAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
TaskEntity task;
|
||||
ListEntity list;
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
task = await new TaskRepository(ctx).GetByIdAsync(taskId, ct)
|
||||
?? throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||
list = await new ListRepository(ctx).GetByIdAsync(task.ListId, ct)
|
||||
?? throw new InvalidOperationException("List not found.");
|
||||
}
|
||||
var (_, list, _) = await LoadMergeContextAsync(taskId, ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(list.WorkingDir))
|
||||
return new MergeTargets("", Array.Empty<string>());
|
||||
|
||||
@@ -30,40 +30,35 @@ public sealed class OverrideSlotService
|
||||
{
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var exists = await taskRepo.GetByIdAsync(taskId);
|
||||
var exists = await new TaskRepository(context).GetByIdAsync(taskId);
|
||||
if (exists is null)
|
||||
throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_slot is not null)
|
||||
throw new InvalidOperationException("override slot busy");
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
_slot = new QueueSlotState { TaskId = taskId, StartedAt = DateTime.UtcNow, Cts = cts };
|
||||
|
||||
_ = RunInSlotAsync(taskId, cts.Token).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
_logger.LogError(t.Exception, "RunInSlotAsync failed for task {TaskId}", taskId);
|
||||
lock (_lock) { _slot = null; }
|
||||
cts.Dispose();
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
StartInSlot(taskId, ct => RunInSlotAsync(taskId, ct), "RunInSlotAsync failed for task {TaskId}");
|
||||
}
|
||||
|
||||
public async Task<string> ContinueTask(string taskId, string followUpPrompt)
|
||||
{
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var taskRepo = new TaskRepository(context);
|
||||
var task = await taskRepo.GetByIdAsync(taskId)
|
||||
?? throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||
using (var context = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var task = await new TaskRepository(context).GetByIdAsync(taskId)
|
||||
?? throw new KeyNotFoundException($"Task '{taskId}' not found.");
|
||||
|
||||
if (task.Status == Data.Models.TaskStatus.Running)
|
||||
throw new InvalidOperationException("task is already running");
|
||||
if (task.Status == Data.Models.TaskStatus.Running)
|
||||
throw new InvalidOperationException("task is already running");
|
||||
}
|
||||
|
||||
StartInSlot(taskId, ct => RunContinueInSlotAsync(taskId, followUpPrompt, ct),
|
||||
"RunContinueInSlotAsync failed for task {TaskId}");
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
// Claims the single override slot under lock, runs <work> in the background,
|
||||
// and releases the slot when it completes. Throws if the slot is already busy.
|
||||
private void StartInSlot(string taskId, Func<CancellationToken, Task> work, string faultMessage)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_slot is not null)
|
||||
@@ -72,16 +67,14 @@ public sealed class OverrideSlotService
|
||||
var cts = new CancellationTokenSource();
|
||||
_slot = new QueueSlotState { TaskId = taskId, StartedAt = DateTime.UtcNow, Cts = cts };
|
||||
|
||||
_ = RunContinueInSlotAsync(taskId, followUpPrompt, cts.Token).ContinueWith(t =>
|
||||
_ = work(cts.Token).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
_logger.LogError(t.Exception, "RunContinueInSlotAsync failed for task {TaskId}", taskId);
|
||||
_logger.LogError(t.Exception, faultMessage, taskId);
|
||||
lock (_lock) { _slot = null; }
|
||||
cts.Dispose();
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public bool TryCancel(string taskId)
|
||||
|
||||
7
src/ClaudeDo.Worker/StringExtensions.cs
Normal file
7
src/ClaudeDo.Worker/StringExtensions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ClaudeDo.Worker;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
/// <summary>Returns null for null/whitespace input, otherwise the original string.</summary>
|
||||
public static string? NullIfBlank(this string? s) => string.IsNullOrWhiteSpace(s) ? null : s;
|
||||
}
|
||||
Reference in New Issue
Block a user