From 0aed6240b9d94d43abadd9b2cbbcb9f3622499db Mon Sep 17 00:00:00 2001 From: David Kaya Date: Wed, 1 Apr 2026 18:54:21 +0200 Subject: [PATCH] feat: render workflow diagnostics in activity panel Surface workflow-diagnostic session events (warnings and errors from Agent Framework workflows) in the turn-event log of the Activity Panel. - Add workflow-diagnostic case to formatTurnEventEntry with label/detail formatting that includes executor ID, subworkflow ID, exception type, and diagnostic message when present - Add AlertTriangle icon for diagnostic events with error/warning color - Add 5 tests covering executor-failed, workflow-warning, subworkflow-error, workflow-error, and missing diagnosticKind fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/renderer/components/ActivityPanel.tsx | 4 +- src/renderer/lib/sessionActivity.ts | 33 +++++++++- tests/renderer/sessionActivity.test.ts | 77 +++++++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/ActivityPanel.tsx b/src/renderer/components/ActivityPanel.tsx index 771a659..dad54f5 100644 --- a/src/renderer/components/ActivityPanel.tsx +++ b/src/renderer/components/ActivityPanel.tsx @@ -1,5 +1,5 @@ import { useMemo, type ReactNode } from 'react'; -import { Activity, ArrowRight, BarChart3, CheckCircle2, Clock, Cog, ShieldAlert, Sparkles, Users, Zap } from 'lucide-react'; +import { Activity, AlertTriangle, ArrowRight, BarChart3, CheckCircle2, Clock, Cog, ShieldAlert, Sparkles, Users, Zap } from 'lucide-react'; import { buildAgentActivityRows, @@ -190,6 +190,8 @@ function TurnEventIcon({ kind, phase, success }: { kind: SessionEventKind; phase return ; case 'session-compaction': return ; + case 'workflow-diagnostic': + return ; default: return ; } diff --git a/src/renderer/lib/sessionActivity.ts b/src/renderer/lib/sessionActivity.ts index 6a3f489..8111a74 100644 --- a/src/renderer/lib/sessionActivity.ts +++ b/src/renderer/lib/sessionActivity.ts @@ -1,6 +1,6 @@ import type { PatternDefinition } from '@shared/domain/pattern'; import type { SessionEventRecord } from '@shared/domain/event'; -import type { QuotaSnapshot } from '@shared/contracts/sidecar'; +import type { QuotaSnapshot, WorkflowDiagnosticKind, WorkflowDiagnosticSeverity } from '@shared/contracts/sidecar'; export interface AgentActivityState { agentId: string; @@ -260,6 +260,22 @@ function formatHookType(hookType: string | undefined): string { return hookTypeLabels[hookType] ?? hookType; } +const diagnosticLabels: Record = { + 'workflow-warning': 'Workflow warning', + 'workflow-error': 'Workflow error', + 'executor-failed': 'Executor failed', + 'subworkflow-warning': 'Subworkflow warning', + 'subworkflow-error': 'Subworkflow error', +}; + +function formatDiagnosticLabel( + kind: WorkflowDiagnosticKind | undefined, + severity: WorkflowDiagnosticSeverity | undefined, +): string { + if (kind) return diagnosticLabels[kind] ?? kind; + return severity === 'error' ? 'Workflow error' : 'Workflow warning'; +} + function formatTurnEventEntry(event: SessionEventRecord): TurnEventEntry | undefined { switch (event.kind) { case 'subagent': @@ -300,6 +316,21 @@ function formatTurnEventEntry(event: SessionEventRecord): TurnEventEntry | undef phase: event.compactionPhase, success: event.compactionSuccess, }; + case 'workflow-diagnostic': { + const label = formatDiagnosticLabel(event.diagnosticKind, event.diagnosticSeverity); + const detailParts: string[] = []; + if (event.executorId) detailParts.push(event.executorId); + if (event.subworkflowId) detailParts.push(event.subworkflowId); + if (event.exceptionType) detailParts.push(event.exceptionType); + if (event.diagnosticMessage) detailParts.push(event.diagnosticMessage); + return { + kind: event.kind, + occurredAt: event.occurredAt, + label, + detail: detailParts.length > 0 ? detailParts.join(' · ') : undefined, + success: event.diagnosticSeverity === 'error' ? false : undefined, + }; + } default: return undefined; } diff --git a/tests/renderer/sessionActivity.test.ts b/tests/renderer/sessionActivity.test.ts index 00aaef5..3be8a6b 100644 --- a/tests/renderer/sessionActivity.test.ts +++ b/tests/renderer/sessionActivity.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from 'bun:test'; import { applySessionEventActivity, applyAssistantUsageEvent, + applyTurnEventLog, buildAgentActivityRows, formatAgentActivityLabel, formatDuration, @@ -527,3 +528,79 @@ describe('usage formatting helpers', () => { expect(formatDuration(150_000)).toBe('2.5m'); }); }); + +describe('workflow diagnostic turn events', () => { + function makeDiagnosticEvent(overrides: Partial = {}): SessionEventRecord { + return { + sessionId: 'session-1', + kind: 'workflow-diagnostic', + occurredAt: '2026-03-23T00:00:00.000Z', + diagnosticSeverity: 'error', + diagnosticKind: 'executor-failed', + diagnosticMessage: 'Tool crashed.', + ...overrides, + }; + } + + test('formats executor-failed with full metadata', () => { + const result = applyTurnEventLog({}, makeDiagnosticEvent({ + executorId: 'Primary', + exceptionType: 'InvalidOperationException', + })); + const entries = result['session-1']!; + expect(entries).toHaveLength(1); + expect(entries[0].label).toBe('Executor failed'); + expect(entries[0].detail).toBe('Primary · InvalidOperationException · Tool crashed.'); + expect(entries[0].success).toBe(false); + }); + + test('formats workflow-warning with message only', () => { + const result = applyTurnEventLog({}, makeDiagnosticEvent({ + diagnosticSeverity: 'warning', + diagnosticKind: 'workflow-warning', + diagnosticMessage: 'Token budget is nearly exhausted.', + executorId: undefined, + exceptionType: undefined, + })); + const entries = result['session-1']!; + expect(entries[0].label).toBe('Workflow warning'); + expect(entries[0].detail).toBe('Token budget is nearly exhausted.'); + expect(entries[0].success).toBeUndefined(); + }); + + test('formats subworkflow-error with subworkflow ID', () => { + const result = applyTurnEventLog({}, makeDiagnosticEvent({ + diagnosticKind: 'subworkflow-error', + subworkflowId: 'subworkflow-review', + exceptionType: 'InvalidOperationException', + diagnosticMessage: 'Reviewer agent failed.', + })); + const entries = result['session-1']!; + expect(entries[0].label).toBe('Subworkflow error'); + expect(entries[0].detail).toBe('subworkflow-review · InvalidOperationException · Reviewer agent failed.'); + }); + + test('formats workflow-error without optional fields', () => { + const result = applyTurnEventLog({}, makeDiagnosticEvent({ + diagnosticKind: 'workflow-error', + diagnosticMessage: 'Workflow terminated unexpectedly.', + executorId: undefined, + subworkflowId: undefined, + exceptionType: undefined, + })); + const entries = result['session-1']!; + expect(entries[0].label).toBe('Workflow error'); + expect(entries[0].detail).toBe('Workflow terminated unexpectedly.'); + expect(entries[0].success).toBe(false); + }); + + test('falls back to severity when diagnosticKind is missing', () => { + const result = applyTurnEventLog({}, makeDiagnosticEvent({ + diagnosticKind: undefined, + diagnosticSeverity: 'warning', + diagnosticMessage: 'Something odd happened.', + })); + const entries = result['session-1']!; + expect(entries[0].label).toBe('Workflow warning'); + }); +});