From 0572e974aa331957e88c2fd0e2e2e0639eaa6ace Mon Sep 17 00:00:00 2001 From: Douglas Date: Thu, 13 Aug 2026 12:08:02 +0100 Subject: [PATCH] Fix session panel agent grouping --- .../SidePanel/components/ActivityPanel.tsx | 39 ++++-- .../sections/buildProjectSessionPanelData.ts | 21 +++- .../buildProjectSessionPanelData.test.ts | 111 ++++++++++++++++++ 3 files changed, 158 insertions(+), 13 deletions(-) diff --git a/src/components/Session/SidePanel/components/ActivityPanel.tsx b/src/components/Session/SidePanel/components/ActivityPanel.tsx index 01db4df9..c1d0e301 100644 --- a/src/components/Session/SidePanel/components/ActivityPanel.tsx +++ b/src/components/Session/SidePanel/components/ActivityPanel.tsx @@ -105,12 +105,14 @@ function SectionList({ children }: { children: ReactNode }) { ); } -function AgentsSection({ +function AgentCategorySection({ + title, items, scope, headerAction, onSelect, }: { + title: string; items: SessionAgentItem[]; scope: SessionPanelScope; headerAction?: ReactNode; @@ -134,9 +136,11 @@ function AgentsSection({ onClick={() => onSelect(item)} > {item.name || - t('layout.session-panel-remote-subagent', { - defaultValue: 'Remote subagent', - })} + (item.subagent + ? t('layout.session-panel-remote-subagent', { + defaultValue: 'Remote subagent', + }) + : t('agents.agent', { defaultValue: 'Agent' }))} )} /> @@ -144,7 +148,7 @@ function AgentsSection({ return ( } headerAction={headerAction} defaultOpen={false} @@ -451,6 +455,14 @@ export function SessionActivityPanel({ () => buildProjectSessionPanelData(scopedRuns, skills, connectors), [connectors, scopedRuns, skills] ); + const agents = useMemo( + () => panelData.agents.filter((agent) => !agent.subagent), + [panelData.agents] + ); + const subagents = useMemo( + () => panelData.agents.filter((agent) => agent.subagent), + [panelData.agents] + ); const projectFiles = useProjectOutputFiles( projectId, activeTask, @@ -562,14 +574,25 @@ export function SessionActivityPanel({
- {panelData.agents.length > 0 ? ( - 0 ? ( + ) : null} + {subagents.length > 0 ? ( + + ) : null} {panelData.progress.length > 0 ? ( (); + const isInternalAgent = (name: string) => + normalizeContextKey(name) === 'questionconfirmagent'; const put = ( run: ProjectSessionRun, identity: string, @@ -284,19 +286,24 @@ function collectAgents( for (const node of run.nodes) { if (node.kind !== 'activity') continue; if (node.activityType === 'agent') { - const text = `${node.eventType} ${node.title}`.toLowerCase(); + const name = node.agentName || node.title; + if (isInternalAgent(name)) continue; + const text = `${node.eventType} ${node.title} ${name}`.toLowerCase(); const subagent = /sub.?agent|remote/.test(text); put( run, - node.agentId || node.agentName || node.title, - node.agentName || node.title, + // Agent UUIDs are process instances and change on every Run. The + // stable semantic name identifies the logical agent in the panel. + name || node.agentId || 'agent', + name, node.detail || '', subagent ); } else if (node.agentId || node.agentName) { + if (isInternalAgent(node.agentName || '')) continue; put( run, - node.agentId || node.agentName || 'agent', + node.agentName || node.agentId || 'agent', node.agentName || '', '', false @@ -310,7 +317,11 @@ function collectAgents( if (!run) continue; const callText = `${call.toolkitName} ${call.method}`.toLowerCase(); const subagent = /sub.?agent|remote/.test(callText); - const identity = call.agentName || (subagent ? 'remote-subagent' : 'agent'); + // Typed tool lifecycle events may not carry agent identity. They are not + // independent agents and must not create a phantom "Remote subagent" row. + if (!call.agentName && !subagent) continue; + if (isInternalAgent(call.agentName)) continue; + const identity = call.agentName || 'remote-subagent'; put(run, identity, call.agentName, '', subagent); const key = `${subagent ? 'subagent' : 'agent'}:${normalizeContextKey( identity diff --git a/test/unit/components/buildProjectSessionPanelData.test.ts b/test/unit/components/buildProjectSessionPanelData.test.ts index a6a5b612..a49877db 100644 --- a/test/unit/components/buildProjectSessionPanelData.test.ts +++ b/test/unit/components/buildProjectSessionPanelData.test.ts @@ -67,6 +67,30 @@ function toolNode( }; } +function agentNode( + runId: string, + eventId: string, + sequence: number, + agentId: string, + agentName: string, + eventType = 'legacy.create_agent', + title = agentName +): ChatActivityNode { + return { + ...baseNode(runId, eventId, sequence), + eventType, + legacyStep: eventType.startsWith('legacy.') + ? eventType.replace('legacy.', '') + : null, + kind: 'activity', + activityType: 'agent', + status: 'running', + title, + agentId, + agentName, + }; +} + function todoActivity( eventId: string, sequence: number, @@ -119,6 +143,93 @@ function makeRun( } describe('buildProjectSessionPanelData', () => { + it('deduplicates logical agents across Runs and skips anonymous tool frames', () => { + const oldRun = makeRun('run-old', false, [ + agentNode( + 'run-old', + 'question-confirm', + 1, + 'confirm-agent-id', + 'question_confirm_agent' + ), + agentNode( + 'run-old', + 'single-agent-old', + 2, + 'single-agent-instance-a', + 'single_agent' + ), + ]); + const currentRun = makeRun('run-current', true, [ + agentNode( + 'run-current', + 'single-agent-current', + 1, + 'single-agent-instance-b', + 'single_agent' + ), + { + ...toolNode( + 'run-current', + 'named-tool-frame', + 2, + 'running', + 'Registering agent' + ), + agentId: undefined, + agentName: 'single_agent', + }, + { + ...toolNode( + 'run-current', + 'anonymous-canonical-tool', + 3, + 'running', + 'Tool without agent identity' + ), + agentId: undefined, + agentName: undefined, + }, + ]); + + const data = buildProjectSessionPanelData([oldRun, currentRun], []); + + expect(data.agents).toMatchObject([ + { + id: 'agent:singleagent', + name: 'single_agent', + historical: false, + subagent: false, + }, + ]); + }); + + it('classifies remote delegated agents separately from primary agents', () => { + const run = makeRun('run-current', true, [ + agentNode( + 'run-current', + 'primary-agent', + 1, + 'primary-instance', + 'single_agent' + ), + agentNode( + 'run-current', + 'remote-agent', + 2, + 'remote-instance', + 'research_helper', + 'agent.remote_started', + 'Remote subagent research_helper' + ), + ]); + + expect(buildProjectSessionPanelData([run], []).agents).toMatchObject([ + { name: 'single_agent', type: 'agent', subagent: false }, + { name: 'research_helper', type: 'subagent', subagent: true }, + ]); + }); + it('pairs semantic tool lifecycle events by durable call id', () => { const run = makeRun('run-1', true, [ toolNode('run-1', 'tool-start', 1, 'running', 'Searching', 'call-1'),