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
4 changes: 2 additions & 2 deletions workspace/frontend/components/layout/nav-agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { AgentAvatar } from '@/components/agents/agent-avatar';
import { AgentRosterHints } from '@/components/agents/agent-roster-hints'; // v1.1 M1
import { cn } from '@/lib/utils';
import { agentLabel } from '@/lib/helpers';
import { ownedSidebarAgents } from '@/lib/sidebar-agents';
import { sidebarAgents } from '@/lib/sidebar-agents';
import { agentAvailability, availabilityDotClass, availabilityLabel } from '@/lib/collab'; // v1.1 M3
import { useWorkspace } from '@/lib/workspace-context';
import { dmSessionId, myAddress } from '@/lib/dm';
Expand All @@ -37,7 +37,7 @@ export function NavAgents({ onNavigate }: { onNavigate?: () => void }) {
const t = useT();
const [open, setOpen] = useState(true);

const recentAgents = useMemo(() => ownedSidebarAgents(agents, currentUser.id), [agents, currentUser.id]);
const recentAgents = useMemo(() => sidebarAgents(agents, currentUser.id), [agents, currentUser.id]);
const onlineCount = recentAgents.filter((a) => a.status === 'online').length;

if (recentAgents.length === 0 && onlineUsers.length === 0) return null;
Expand Down
7 changes: 4 additions & 3 deletions workspace/frontend/components/layout/nav-rail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ vi.mock('./layout-context', () => ({ useLayout: () => ({
viewMode: 'threads', openView: state.openView, isRailExpanded: true, railDragWidth: null,
}), RAIL_WIDTH_COLLAPSED: 48, RAIL_WIDTH_EXPANDED: 180 }));
vi.mock('@/lib/workspace-context', () => ({ useWorkspace: () => ({
unreadThreadsTotal: state.unreadThreadsTotal, workspace: { name: 'Test' }, agents: [{ agentName: 'shared', ownerEmail: 'maya@example.com', status: 'online' }],
unreadThreadsTotal: state.unreadThreadsTotal, workspace: { name: 'Test' }, agents: [{ agentName: 'shared', ownerEmail: 'maya@example.com', status: 'online', canStartThread: true }],
currentUser: { id: 'sam@example.com' }, tasks: [], onlineUsers: [], pendingApprovalsByAgent: {}, unreadDmCounts: {},
}) }));
vi.mock('@/lib/i18n', () => ({ useT: () => (key: string) => key }));
Expand All @@ -32,13 +32,14 @@ vi.mock('@/components/campaign/campaign-sidebar-card', () => ({ CampaignSidebarC
vi.stubGlobal('React', React);
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);

it('shows workspace navigation to a member with no owned agents without adding shared agents to My agents', async () => {
it('shows shared agents alongside workspace navigation for a member with no owned agents', async () => {
const host = document.createElement('div'); document.body.append(host);
const root = createRoot(host);
try {
await act(async () => root.render(React.createElement(NavRail)));
for (const label of ['views.files', 'views.knowledge', 'views.issues', 'views.inbox', 'collab.viewAgents']) expect(host.textContent).toContain(label);
expect(host.textContent).not.toContain('shared');
expect(host.textContent).toContain('shared');
expect(host.textContent).toContain('nav.agentsWithCount');
expect(host.textContent).not.toContain('views.tasks');
const services = [...host.querySelectorAll('button')].find(b => b.textContent === 'collab.viewAgents')!;
await act(async () => services.click());
Expand Down
4 changes: 2 additions & 2 deletions workspace/frontend/components/layout/nav-rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { AgentAvatar } from '@/components/agents/agent-avatar';
import { AgentRosterHints } from '@/components/agents/agent-roster-hints'; // v1.1 M1
import { cn } from '@/lib/utils';
import { agentLabel } from '@/lib/helpers';
import { ownedSidebarAgents } from '@/lib/sidebar-agents';
import { sidebarAgents } from '@/lib/sidebar-agents';
import { agentPresence } from '@/lib/approvals';
import { useWorkspace } from '@/lib/workspace-context';
import { dmSessionId, myAddress } from '@/lib/dm';
Expand Down Expand Up @@ -204,7 +204,7 @@ export function NavRail() {
const t = useT();
const [agentsOpen, setAgentsOpen] = React.useState(true);

const recentAgents = ownedSidebarAgents(agents, currentUser.id);
const recentAgents = sidebarAgents(agents, currentUser.id);
// Yumi (built-in) still appears in the roster, but does not satisfy the
// "connect your first agent" call to action.
const hasAgents = recentAgents.filter((a) => !a.builtin).length > 0;
Expand Down
2 changes: 1 addition & 1 deletion workspace/frontend/lib/i18n/messages/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export const messages = {
noMembersFound: 'No people found',
thisComputer: 'This Computer',
collaboration: 'Collaboration',
agentsWithCount: 'My agents ({online}/{total})',
agentsWithCount: 'Agents ({online}/{total})',
onlineWithCount: 'Online ({count})',
you: '{name} (you)',
workspaceFallback: 'Workspace',
Expand Down
2 changes: 1 addition & 1 deletion workspace/frontend/lib/i18n/messages/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export const messages: Messages = {
noMembersFound: '未找到成员',
thisComputer: '此电脑',
collaboration: '协作',
agentsWithCount: '我的智能体({online}/{total})',
agentsWithCount: '智能体({online}/{total})',
onlineWithCount: '在线({count})',
you: '{name}(你)',
workspaceFallback: '工作区',
Expand Down
29 changes: 23 additions & 6 deletions workspace/frontend/lib/sidebar-agents.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import { expect, it } from 'vitest';
import { ownedSidebarAgents } from './sidebar-agents';
import { sidebarAgents } from './sidebar-agents';
import type { WorkspaceAgent } from './types';
it('shows only owned agents including offline ones, not shared or unclaimed agents', () => {

it('includes owned and directly usable shared agents, including offline agents', () => {
const agents = [
{ agentName: 'mine', ownerEmail: ' ME@EXAMPLE.COM ', status: 'offline' },
{ agentName: 'shared', ownerEmail: 'other@example.com', status: 'online', visibility: 'workspace' },
{ agentName: 'unclaimed', ownerEmail: null, status: 'online' },
{ agentName: 'shared-person', ownerEmail: 'other@example.com', status: 'online', canStartThread: true },
{ agentName: 'shared-group', ownerEmail: 'other@example.com', status: 'offline', canStartThread: true },
{ agentName: 'shared-everyone', ownerEmail: null, status: 'online', canStartThread: true },
{ agentName: 'private', ownerEmail: 'other@example.com', status: 'online', canStartThread: false },
{ agentName: 'thread-only', ownerEmail: 'other@example.com', status: 'online', isShared: true, canStartThread: false },
{ agentName: 'legacy-team-flag', ownerEmail: null, status: 'online', visibility: 'team' },
{ agentName: 'removed', ownerEmail: 'me@example.com', status: 'removed', canStartThread: true },
] as WorkspaceAgent[];
expect(ownedSidebarAgents(agents, 'me@example.com').map((a) => a.agentName)).toEqual(['mine']);
expect(ownedSidebarAgents(agents, '')).toEqual([]);
expect(sidebarAgents(agents, 'me@example.com').map((a) => a.agentName))
.toEqual(['mine', 'shared-person', 'shared-group', 'shared-everyone']);
expect(sidebarAgents(agents, '')).toEqual([]);
});

it('removes a shared agent when its access is revoked without removing owned agents', () => {
const agents = [
{ agentName: 'mine', ownerEmail: 'me@example.com', status: 'online', canStartThread: true },
{ agentName: 'shared', ownerEmail: 'other@example.com', status: 'online', canStartThread: true },
] as WorkspaceAgent[];
expect(sidebarAgents(agents, 'me@example.com')).toHaveLength(2);
expect(sidebarAgents(agents.map((agent) => ({ ...agent, canStartThread: false })), 'me@example.com')
.map((agent) => agent.agentName)).toEqual(['mine']);
});
11 changes: 7 additions & 4 deletions workspace/frontend/lib/sidebar-agents.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { WorkspaceAgent } from './types';

/** The personal roster never includes other people's or unclaimed agents. */
export function ownedSidebarAgents(agents: WorkspaceAgent[], email: string) {
const owner = email.trim().toLowerCase();
return owner ? agents.filter((agent) => agent.ownerEmail?.trim().toLowerCase() === owner && agent.status !== 'removed') : [];
/** Owned agents and agents the server permits this viewer to contact directly. */
export function sidebarAgents(agents: WorkspaceAgent[], email: string) {
const viewer = email.trim().toLowerCase();
if (!viewer) return [];
return agents.filter((agent) => agent.status !== 'removed' && (
agent.ownerEmail?.trim().toLowerCase() === viewer || agent.canStartThread === true
));
}
Loading