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'),