From 5bce7dbb0540472b3b1b3912f97fdfdb86f68071 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 17 Feb 2026 16:12:40 -0600 Subject: [PATCH] feat(web): show latest orchestrator event in task progress widget --- .../components/widgets/TaskProgressWidget.tsx | 33 +++++++++++- .../__tests__/TaskProgressWidget.test.tsx | 54 +++++++++++++++++++ docs/tasks.md | 1 + 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/widgets/TaskProgressWidget.tsx b/apps/web/src/components/widgets/TaskProgressWidget.tsx index 63f8b1e..f03a9b7 100644 --- a/apps/web/src/components/widgets/TaskProgressWidget.tsx +++ b/apps/web/src/components/widgets/TaskProgressWidget.tsx @@ -27,6 +27,13 @@ interface QueueStats { delayed: number; } +interface RecentOrchestratorEvent { + type: string; + timestamp: string; + taskId?: string; + agentId?: string; +} + function getElapsedTime(spawnedAt: string, completedAt?: string): string { const start = new Date(spawnedAt).getTime(); const end = completedAt ? new Date(completedAt).getTime() : Date.now(); @@ -103,6 +110,7 @@ function getAgentTypeLabel(agentType: string): string { export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element { const [tasks, setTasks] = useState([]); const [queueStats, setQueueStats] = useState(null); + const [recentEvents, setRecentEvents] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [isQueuePaused, setIsQueuePaused] = useState(false); @@ -135,6 +143,17 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R } }, []); + const fetchRecentEvents = useCallback(async (): Promise => { + try { + const res = await fetch("/api/orchestrator/events/recent?limit=5"); + if (!res.ok) throw new Error(`HTTP ${String(res.status)}`); + const data = (await res.json()) as { events: RecentOrchestratorEvent[] }; + setRecentEvents(data.events); + } catch { + // Optional enhancement path; do not fail widget if recent-events endpoint is unavailable. + } + }, []); + const setQueueState = useCallback( async (action: "pause" | "resume"): Promise => { setIsActionPending(true); @@ -157,10 +176,12 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R useEffect(() => { void fetchTasks(); void fetchQueueStats(); + void fetchRecentEvents(); const interval = setInterval(() => { void fetchTasks(); void fetchQueueStats(); + void fetchRecentEvents(); }, 15000); const eventSource = @@ -169,6 +190,7 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R eventSource.onmessage = (): void => { void fetchTasks(); void fetchQueueStats(); + void fetchRecentEvents(); }; eventSource.onerror = (): void => { // Polling remains the resilience path. @@ -179,7 +201,9 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R clearInterval(interval); eventSource?.close(); }; - }, [fetchTasks, fetchQueueStats]); + }, [fetchTasks, fetchQueueStats, fetchRecentEvents]); + + const latestEvent = recentEvents.length > 0 ? recentEvents[recentEvents.length - 1] : null; const stats = { total: tasks.length, @@ -226,6 +250,13 @@ export function TaskProgressWidget({ id: _id, config: _config }: WidgetProps): R + {latestEvent && ( +
+ Latest: {latestEvent.type} + {latestEvent.taskId ? ` · ${latestEvent.taskId}` : ""} +
+ )} + {/* Summary stats */}
diff --git a/apps/web/src/components/widgets/__tests__/TaskProgressWidget.test.tsx b/apps/web/src/components/widgets/__tests__/TaskProgressWidget.test.tsx index 47fd72d..a6b038e 100644 --- a/apps/web/src/components/widgets/__tests__/TaskProgressWidget.test.tsx +++ b/apps/web/src/components/widgets/__tests__/TaskProgressWidget.test.tsx @@ -242,4 +242,58 @@ describe("TaskProgressWidget", (): void => { expect(taskElements[1]?.textContent).toBe("COMPLETED-TASK"); }); }); + + it("should display latest orchestrator event when available", async (): Promise => { + mockFetch.mockImplementation((input: RequestInfo | URL) => { + let url = ""; + if (typeof input === "string") { + url = input; + } else if (input instanceof URL) { + url = input.toString(); + } else { + url = input.url; + } + if (url.includes("/api/orchestrator/agents")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve([]), + } as unknown as Response); + } + if (url.includes("/api/orchestrator/queue/stats")) { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + pending: 0, + active: 0, + completed: 0, + failed: 0, + delayed: 0, + }), + } as unknown as Response); + } + if (url.includes("/api/orchestrator/events/recent")) { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + events: [ + { + type: "task.executing", + timestamp: new Date().toISOString(), + taskId: "TASK-123", + }, + ], + }), + } as unknown as Response); + } + return Promise.reject(new Error("Unknown endpoint")); + }); + + render(); + + await waitFor(() => { + expect(screen.getByText(/Latest: task.executing · TASK-123/i)).toBeInTheDocument(); + }); + }); }); diff --git a/docs/tasks.md b/docs/tasks.md index 63cb9f1..c24131d 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -386,3 +386,4 @@ | ORCH-OBS-007 | done | Add `OrchestratorEventsWidget` for live/recent orchestration visibility with Matrix signal hints | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-002 | | orch | 2026-02-17T16:55Z | 2026-02-17T17:03Z | 12K | 9K | | ORCH-OBS-008 | done | Integrate new widget into HUD/WidgetRegistry and extend widget regression coverage | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-007 | | orch | 2026-02-17T17:03Z | 2026-02-17T17:08Z | 10K | 7K | | ORCH-OBS-009 | done | Seed default/reset local HUD layout with orchestration widgets so visibility works out-of-box | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-008 | | orch | 2026-02-17T17:10Z | 2026-02-17T17:14Z | 8K | 6K | +| ORCH-OBS-010 | done | Enrich `TaskProgressWidget` with latest recent-event context from `/api/orchestrator/events/recent` | #411 | web | feature/mosaic-stack-finalization | ORCH-OBS-009 | | orch | 2026-02-17T17:15Z | 2026-02-17T17:20Z | 8K | 6K |