fix: scope turn-activity panel metrics to per-agent events in sequential workflows

In sequential workflows, multiple collapsible turn-activity panels shared
the same SessionRunRecord and displayed identical aggregate metrics (tool
calls, approvals, etc.) instead of per-agent counts.

Root cause: ChatPane grouped thinking messages correctly per agent, but
passed the full unfiltered run object to every TurnActivityPanel, so
summarizeActivity() counted all events from the entire run for each panel.

Changes:
- Extract filterEventsByAgent() and summarizeActivity() to
  runTimelineFormatting.ts as pure, testable helpers
- Add agentNames (derived from thinking message authors) and isLastRunPanel
  to the DisplayItem turn-activity variant in ChatPane
- TurnActivityPanel now filters run.events through filterEventsByAgent()
  so each panel only counts events belonging to its agents
- Git summary / discard actions only render on the last panel of a run
- Per-agent timing derived from scoped events instead of run-level start

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-07 12:25:01 +02:00
co-authored by Copilot
parent 778c3b4a5b
commit 0a5fa81111
4 changed files with 292 additions and 40 deletions
+65
View File
@@ -168,3 +168,68 @@ export function truncateContent(content: string | undefined, maxLength = 80): st
export function findLatestRun(runs: readonly SessionRunRecord[]): SessionRunRecord | undefined {
return runs.length > 0 ? runs[0] : undefined;
}
/* ── Agent-scoped event filtering ──────────────────────────── */
/** Run-level events that are not scoped to any specific agent. */
const RUN_LEVEL_EVENT_KINDS = new Set<RunTimelineEventKind>([
'run-started',
'run-completed',
'run-cancelled',
'run-failed',
]);
/**
* Filter run events to only those belonging to the given agents.
* When `agentNames` is undefined or empty (single-agent run), all events pass through.
* Run-level events (start/complete/fail) are excluded from agent-scoped panels
* because they represent the entire run, not a specific agent's work.
*/
export function filterEventsByAgent(
events: readonly RunTimelineEventRecord[],
agentNames: ReadonlySet<string> | undefined,
): RunTimelineEventRecord[] {
if (!agentNames || agentNames.size === 0) return events.slice();
return events.filter((e) => {
if (RUN_LEVEL_EVENT_KINDS.has(e.kind)) return false;
if (e.kind === 'handoff') {
return (e.sourceAgentName != null && agentNames.has(e.sourceAgentName))
|| (e.targetAgentName != null && agentNames.has(e.targetAgentName));
}
if (e.agentName) return agentNames.has(e.agentName);
return false;
});
}
/** Counts of different activity types within a set of run timeline events. */
export interface ActivitySummary {
thinkingSteps: number;
toolCalls: number;
handoffs: number;
approvals: number;
hasError: boolean;
}
/**
* Summarize activity from thinking messages and (already-filtered) run events.
*/
export function summarizeActivity(
thinkingMessages: ReadonlyArray<{ content: string }>,
events: readonly RunTimelineEventRecord[],
): ActivitySummary {
const thinkingSteps = thinkingMessages.filter((m) => m.content).length;
let toolCalls = 0;
let handoffs = 0;
let approvals = 0;
let hasError = false;
for (const e of events) {
if (e.kind === 'tool-call') toolCalls++;
else if (e.kind === 'handoff') handoffs++;
else if (e.kind === 'approval') approvals++;
else if (e.kind === 'run-failed') hasError = true;
}
return { thinkingSteps, toolCalls, handoffs, approvals, hasError };
}