From c9cc2fd8e725d30b8656259461c63bbe0337b0fd Mon Sep 17 00:00:00 2001 From: David Kaya Date: Wed, 25 Mar 2026 22:17:33 +0100 Subject: [PATCH] fix: clear stale agent activity on cancel Clear only still-active side-panel agent states when a run ends as cancelled or error, while preserving completed agent statuses. Add a renderer regression test for cancelled runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/renderer/lib/sessionActivity.ts | 40 +++++++++++++++++++++ tests/renderer/sessionActivity.test.ts | 49 ++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/renderer/lib/sessionActivity.ts b/src/renderer/lib/sessionActivity.ts index 282ff74..434d99b 100644 --- a/src/renderer/lib/sessionActivity.ts +++ b/src/renderer/lib/sessionActivity.ts @@ -48,6 +48,14 @@ export function applySessionEventActivity( } } + if ( + event.kind === 'run-updated' + && event.run + && (event.run.status === 'cancelled' || event.run.status === 'error') + ) { + return clearActiveSessionActivity(current, event.sessionId); + } + return current; } @@ -137,6 +145,38 @@ function removeSessionActivity( return next; } +function clearActiveSessionActivity( + current: SessionActivityMap, + sessionId: string, +): SessionActivityMap { + const sessionActivity = current[sessionId]; + if (!sessionActivity) { + return current; + } + + let changed = false; + const nextSessionActivity = Object.fromEntries( + Object.entries(sessionActivity).filter(([, activity]) => { + const keep = !isAgentActivityActive(activity); + changed ||= !keep; + return keep; + }), + ); + + if (!changed) { + return current; + } + + if (Object.keys(nextSessionActivity).length === 0) { + return removeSessionActivity(current, sessionId); + } + + return { + ...current, + [sessionId]: nextSessionActivity, + }; +} + function resolveAgentKey(event: SessionEventRecord): string | undefined { return event.agentId?.trim() || event.agentName?.trim(); } diff --git a/tests/renderer/sessionActivity.test.ts b/tests/renderer/sessionActivity.test.ts index 0b4e3ca..abc3b66 100644 --- a/tests/renderer/sessionActivity.test.ts +++ b/tests/renderer/sessionActivity.test.ts @@ -179,6 +179,55 @@ describe('session activity helpers', () => { ).toEqual(current); }); + test('clears only active agent states when a run is cancelled', () => { + const current: SessionActivityMap = { + 'session-1': { + architect: { + agentId: 'architect', + agentName: 'Architect', + activityType: 'completed', + }, + reviewer: { + agentId: 'reviewer', + agentName: 'Reviewer', + activityType: 'thinking', + }, + }, + }; + + expect( + applySessionEventActivity(current, { + sessionId: 'session-1', + kind: 'run-updated', + occurredAt: '2026-03-23T00:00:01.000Z', + run: { + id: 'run-1', + requestId: 'turn-1', + projectId: 'project-1', + projectPath: 'C:\\workspace\\project', + workspaceKind: 'project', + patternId: 'pattern-1', + patternName: 'Pattern', + patternMode: 'single', + triggerMessageId: 'msg-1', + startedAt: '2026-03-23T00:00:00.000Z', + completedAt: '2026-03-23T00:00:01.000Z', + status: 'cancelled', + agents: [], + events: [], + }, + }), + ).toEqual({ + 'session-1': { + architect: { + agentId: 'architect', + agentName: 'Architect', + activityType: 'completed', + }, + }, + }); + }); + test('builds rows for all agents with sensible defaults', () => { expect(buildAgentActivityRows(undefined, agents)).toEqual([ {