From 005128317f1229670a5a4f3700c84d911eed584b Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Wed, 14 Jan 2026 11:47:45 +0000 Subject: [PATCH 1/2] Fix dashboard agent list labeling --- src/dashboard/react-components/App.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dashboard/react-components/App.tsx b/src/dashboard/react-components/App.tsx index 6956b9355..3306555de 100644 --- a/src/dashboard/react-components/App.tsx +++ b/src/dashboard/react-components/App.tsx @@ -461,18 +461,20 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) { // Merge AI agents, human users, and local agents from linked daemons const combinedAgents = useMemo(() => { - const merged = [...(data?.agents ?? []), ...(data?.users ?? []), ...localAgents]; + const merged = [...(data?.agents ?? []), ...(data?.users ?? []), ...localAgents] + .filter((agent) => agent.name.toLowerCase() !== 'dashboard'); const byName = new Map(); for (const agent of merged) { const key = agent.name.toLowerCase(); const existing = byName.get(key); - // Local agents should preserve their isLocal flag when merging + // Prefer non-local agents when names collide to avoid cloud agents showing as local. if (existing) { + const keepNonLocal = !existing.isLocal && agent.isLocal; byName.set(key, { ...existing, ...agent, - isLocal: existing.isLocal || agent.isLocal, + isLocal: keepNonLocal ? false : Boolean(agent.isLocal), }); } else { byName.set(key, agent); From a332e60356aadfe5562e0e8bfb669d7088312586 Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Wed, 14 Jan 2026 12:45:30 +0000 Subject: [PATCH 2/2] Add tests for dashboard agent merge --- src/dashboard/lib/agent-merge.test.ts | 43 ++++++++++++++++++++++++++ src/dashboard/lib/agent-merge.ts | 35 +++++++++++++++++++++ src/dashboard/react-components/App.tsx | 27 ++++------------ 3 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 src/dashboard/lib/agent-merge.test.ts create mode 100644 src/dashboard/lib/agent-merge.ts diff --git a/src/dashboard/lib/agent-merge.test.ts b/src/dashboard/lib/agent-merge.test.ts new file mode 100644 index 000000000..a733dbf8f --- /dev/null +++ b/src/dashboard/lib/agent-merge.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import type { Agent } from '../types'; +import { mergeAgentsForDashboard } from './agent-merge.js'; + +describe('mergeAgentsForDashboard', () => { + it('filters out the Dashboard user', () => { + const agents: Agent[] = [ + { name: 'Dashboard', status: 'online' }, + { name: 'Lead', status: 'online' }, + ]; + + const merged = mergeAgentsForDashboard({ agents }); + + expect(merged.map((agent) => agent.name)).toEqual(['Lead']); + }); + + it('keeps cloud agents from being marked as local on name collision', () => { + const agents: Agent[] = [ + { name: 'Lead', status: 'online' }, + ]; + const localAgents: Agent[] = [ + { name: 'Lead', status: 'online', isLocal: true, daemonName: 'local-daemon' }, + ]; + + const merged = mergeAgentsForDashboard({ agents, localAgents }); + + expect(merged).toHaveLength(1); + expect(merged[0].name).toBe('Lead'); + expect(merged[0].isLocal).toBe(false); + }); + + it('preserves local agents when no cloud agent exists', () => { + const localAgents: Agent[] = [ + { name: 'Worker', status: 'online', isLocal: true, daemonName: 'local-daemon' }, + ]; + + const merged = mergeAgentsForDashboard({ localAgents }); + + expect(merged).toHaveLength(1); + expect(merged[0].name).toBe('Worker'); + expect(merged[0].isLocal).toBe(true); + }); +}); diff --git a/src/dashboard/lib/agent-merge.ts b/src/dashboard/lib/agent-merge.ts new file mode 100644 index 000000000..c46745205 --- /dev/null +++ b/src/dashboard/lib/agent-merge.ts @@ -0,0 +1,35 @@ +import type { Agent } from '../types'; + +export interface MergeAgentsInput { + agents?: Agent[]; + users?: Agent[]; + localAgents?: Agent[]; +} + +export function mergeAgentsForDashboard({ + agents = [], + users = [], + localAgents = [], +}: MergeAgentsInput): Agent[] { + const merged = [...agents, ...users, ...localAgents] + .filter((agent) => agent.name.toLowerCase() !== 'dashboard'); + const byName = new Map(); + + for (const agent of merged) { + const key = agent.name.toLowerCase(); + const existing = byName.get(key); + // Prefer non-local agents when names collide to avoid cloud agents showing as local. + if (existing) { + const keepNonLocal = !existing.isLocal && agent.isLocal; + byName.set(key, { + ...existing, + ...agent, + isLocal: keepNonLocal ? false : Boolean(agent.isLocal), + }); + } else { + byName.set(key, agent); + } + } + + return Array.from(byName.values()); +} diff --git a/src/dashboard/react-components/App.tsx b/src/dashboard/react-components/App.tsx index 3306555de..480bd32e2 100644 --- a/src/dashboard/react-components/App.tsx +++ b/src/dashboard/react-components/App.tsx @@ -45,6 +45,7 @@ import { useCloudSessionOptional } from './CloudSessionProvider'; import { WorkspaceProvider } from './WorkspaceContext'; import { api, convertApiDecision, setActiveWorkspaceId as setApiWorkspaceId } from '../lib/api'; import { cloudApi } from '../lib/cloudApi'; +import { mergeAgentsForDashboard } from '../lib/agent-merge'; import type { CurrentUser } from './MessageList'; /** @@ -461,27 +462,11 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) { // Merge AI agents, human users, and local agents from linked daemons const combinedAgents = useMemo(() => { - const merged = [...(data?.agents ?? []), ...(data?.users ?? []), ...localAgents] - .filter((agent) => agent.name.toLowerCase() !== 'dashboard'); - const byName = new Map(); - - for (const agent of merged) { - const key = agent.name.toLowerCase(); - const existing = byName.get(key); - // Prefer non-local agents when names collide to avoid cloud agents showing as local. - if (existing) { - const keepNonLocal = !existing.isLocal && agent.isLocal; - byName.set(key, { - ...existing, - ...agent, - isLocal: keepNonLocal ? false : Boolean(agent.isLocal), - }); - } else { - byName.set(key, agent); - } - } - - return Array.from(byName.values()); + return mergeAgentsForDashboard({ + agents: data?.agents, + users: data?.users, + localAgents, + }); }, [data?.agents, data?.users, localAgents]); // Mark a DM conversation as seen (used for unread badges)