feat: ship agent progress visibility
CI - Build & Test / Backend (.NET) (push) Failing after 31s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 19s
CI - Build & Test / Security Check (push) Successful in 4s

This commit is contained in:
2026-06-20 20:22:54 +02:00
parent 3dd745586b
commit adae7ba26d
9 changed files with 202 additions and 45 deletions
+1
View File
@@ -36,6 +36,7 @@ public interface ITaskService
Task<int> ResetStaleInProgressTasksAsync(TimeSpan staleThreshold, CancellationToken ct = default);
Task<IReadOnlyList<WorkTask>> GetChildTasksAsync(Guid parentId, CancellationToken ct = default);
Task<List<ActivityEvent>> GetTaskActivityAsync(Guid taskId, CancellationToken ct = default);
Task<DashboardTaskDto?> GetDashboardTaskByIdAsync(Guid id, CancellationToken ct = default);
// Agent Workflow Overview
Task<IReadOnlyList<WorkTask>> GetWaitingTasksAsync(CancellationToken ct = default);
+37 -16
View File
@@ -20,6 +20,15 @@ public sealed class TaskService(
public async Task<WorkTask?> GetByIdAsync(Guid id, CancellationToken ct = default)
=> await taskRepo.GetByIdAsync(id, ct);
public async Task<DashboardTaskDto?> GetDashboardTaskByIdAsync(Guid id, CancellationToken ct = default)
{
var task = await taskRepo.GetByIdAsync(id, ct);
if (task is null) return null;
var activity = await activityRepo.GetRecentForTasksAsync([task.Id], ct);
return MapToDtoWithActivity(task, activity);
}
public async Task<IReadOnlyList<WorkTask>> GetPendingApprovalAsync(CancellationToken ct = default)
=> await taskRepo.GetPendingApprovalAsync(ct);
@@ -162,35 +171,32 @@ public sealed class TaskService(
var agentTasks = all.Where(t => t.IsAgentTask).ToList();
var waitingForBao = agentTasks
var activity = await activityRepo.GetRecentForTasksAsync(agentTasks.Select(t => t.Id), ct);
List<DashboardTaskDto> map(IEnumerable<WorkTask> tasks)
=> tasks.Select(task => MapToDtoWithActivity(task, activity)).ToList();
var waitingForBao = map(agentTasks
.Where(t => string.Equals(t.ExpectedFrom, "bao", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase))
.Select(MapToDto)
.ToList();
!string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase)));
var waitingForIris = agentTasks
var waitingForIris = map(agentTasks
.Where(t => string.Equals(t.ExpectedFrom, "iris", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase))
.Select(MapToDto)
.ToList();
!string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase)));
var waitingForOthers = agentTasks
var waitingForOthers = map(agentTasks
.Where(t =>
{
var expected = (t.ExpectedFrom ?? "").ToLowerInvariant();
return expected != "bao" && expected != "iris" && !string.IsNullOrWhiteSpace(expected) &&
!string.Equals(t.State, "Done", StringComparison.OrdinalIgnoreCase);
})
.Select(MapToDto)
.ToList();
}));
var staleTasks = agentTasks
var staleTasks = map(agentTasks
.Where(t =>
(string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) ||
string.Equals(t.State, "Delegated", StringComparison.OrdinalIgnoreCase)) &&
t.UpdatedAt < threshold)
.Select(MapToDto)
.ToList();
t.UpdatedAt < threshold));
return new AgentWorkflowOverview(waitingForBao, waitingForIris, waitingForOthers,
staleTasks, staleThreshold);
@@ -533,6 +539,21 @@ public sealed class TaskService(
t.ParentTaskId, t.DueDate, t.CreatedAt, t.UpdatedAt,
t.IsAgentTask, t.ExpectedFrom);
private static DashboardTaskDto MapToDtoWithActivity(WorkTask t, IEnumerable<ActivityEvent> activity)
{
var last = activity
.Where(e => e.TaskId == t.Id)
.OrderByDescending(e => e.CreatedAt)
.FirstOrDefault();
return new DashboardTaskDto(
t.Id, t.Title, t.Detail, t.Source, t.State, t.Priority, t.AssignedTo,
t.ParentTaskId, t.DueDate, t.CreatedAt, t.UpdatedAt,
t.IsAgentTask, t.ExpectedFrom,
last?.Message,
last?.CreatedAt);
}
/// <summary>
/// Validates AssignedTo — only recognized agent values are accepted.
/// Returns null for invalid values.