Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/dashboard/lib/agent-merge.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
35 changes: 35 additions & 0 deletions src/dashboard/lib/agent-merge.ts
Original file line number Diff line number Diff line change
@@ -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<string, Agent>();

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());
}
25 changes: 6 additions & 19 deletions src/dashboard/react-components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -461,25 +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];
const byName = new Map<string, Agent>();

for (const agent of merged) {
const key = agent.name.toLowerCase();
const existing = byName.get(key);
// Local agents should preserve their isLocal flag when merging
if (existing) {
byName.set(key, {
...existing,
...agent,
isLocal: existing.isLocal || 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)
Expand Down
Loading