@agent-relay/dashboard 2.0.81 → 2.0.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AgentList component
|
|
3
|
+
*
|
|
4
|
+
* Covers: collapse all/expand all behavior, solo agent rendering,
|
|
5
|
+
* and group display logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// @vitest-environment jsdom
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
12
|
+
import { AgentList } from './AgentList';
|
|
13
|
+
import type { Agent } from '../types';
|
|
14
|
+
|
|
15
|
+
function makeAgent(name: string, overrides: Partial<Agent> = {}): Agent {
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
status: 'online',
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('AgentList', () => {
|
|
24
|
+
describe('empty states', () => {
|
|
25
|
+
it('shows empty message when no agents', () => {
|
|
26
|
+
render(<AgentList agents={[]} />);
|
|
27
|
+
expect(screen.getByText('No agents connected')).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('shows search empty message when no agents match query', () => {
|
|
31
|
+
const agents = [makeAgent('backend-api')];
|
|
32
|
+
render(<AgentList agents={agents} searchQuery="zzz" />);
|
|
33
|
+
expect(screen.getByText(/No agents match/)).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('Collapse all / Expand all', () => {
|
|
38
|
+
const agents = [
|
|
39
|
+
makeAgent('backend-api'),
|
|
40
|
+
makeAgent('backend-db'),
|
|
41
|
+
makeAgent('frontend-ui'),
|
|
42
|
+
makeAgent('frontend-components'),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
it('shows agent count in header', () => {
|
|
46
|
+
render(<AgentList agents={agents} />);
|
|
47
|
+
expect(screen.getByText('4 agents')).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('shows Collapse all button by default', () => {
|
|
51
|
+
render(<AgentList agents={agents} />);
|
|
52
|
+
expect(screen.getByText('Collapse all')).toBeTruthy();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('hides all agent cards when Collapse all is clicked', () => {
|
|
56
|
+
render(<AgentList agents={agents} />);
|
|
57
|
+
|
|
58
|
+
// Groups should be visible initially
|
|
59
|
+
expect(screen.getByText('Backend')).toBeTruthy();
|
|
60
|
+
expect(screen.getByText('Frontend')).toBeTruthy();
|
|
61
|
+
|
|
62
|
+
fireEvent.click(screen.getByText('Collapse all'));
|
|
63
|
+
|
|
64
|
+
// After collapse, groups should be hidden entirely
|
|
65
|
+
expect(screen.queryByText('Backend')).toBeNull();
|
|
66
|
+
expect(screen.queryByText('Frontend')).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('shows Expand all button after collapsing', () => {
|
|
70
|
+
render(<AgentList agents={agents} />);
|
|
71
|
+
fireEvent.click(screen.getByText('Collapse all'));
|
|
72
|
+
expect(screen.getByText('Expand all')).toBeTruthy();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('still shows agent count when collapsed', () => {
|
|
76
|
+
render(<AgentList agents={agents} />);
|
|
77
|
+
fireEvent.click(screen.getByText('Collapse all'));
|
|
78
|
+
expect(screen.getByText('4 agents')).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('restores all groups when Expand all is clicked after collapse', () => {
|
|
82
|
+
render(<AgentList agents={agents} />);
|
|
83
|
+
|
|
84
|
+
fireEvent.click(screen.getByText('Collapse all'));
|
|
85
|
+
expect(screen.queryByText('Backend')).toBeNull();
|
|
86
|
+
|
|
87
|
+
fireEvent.click(screen.getByText('Expand all'));
|
|
88
|
+
expect(screen.getByText('Backend')).toBeTruthy();
|
|
89
|
+
expect(screen.getByText('Frontend')).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('hides pinned agents section when collapsed', () => {
|
|
93
|
+
render(
|
|
94
|
+
<AgentList
|
|
95
|
+
agents={agents}
|
|
96
|
+
pinnedAgents={['backend-api']}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText('Pinned')).toBeTruthy();
|
|
101
|
+
|
|
102
|
+
fireEvent.click(screen.getByText('Collapse all'));
|
|
103
|
+
expect(screen.queryByText('Pinned')).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('solo agent rendering', () => {
|
|
108
|
+
it('renders solo agent without group header', () => {
|
|
109
|
+
// "Lead" agent with prefix "lead" and only one agent in group
|
|
110
|
+
const agents = [
|
|
111
|
+
makeAgent('Lead'),
|
|
112
|
+
makeAgent('backend-api'),
|
|
113
|
+
makeAgent('backend-db'),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
render(<AgentList agents={agents} />);
|
|
117
|
+
|
|
118
|
+
// "Backend" should appear as a group header
|
|
119
|
+
expect(screen.getByText('Backend')).toBeTruthy();
|
|
120
|
+
// "Lead" should render as a standalone card (name appears in display + subtitle)
|
|
121
|
+
expect(screen.getAllByText('Lead').length).toBeGreaterThan(0);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('filters out system agents', () => {
|
|
126
|
+
it('filters out __setup__ agents', () => {
|
|
127
|
+
const agents = [
|
|
128
|
+
makeAgent('__setup__google'),
|
|
129
|
+
makeAgent('backend-api'),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
render(<AgentList agents={agents} />);
|
|
133
|
+
// Should only count the non-setup agent
|
|
134
|
+
expect(screen.getByText('1 agent')).toBeTruthy();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('filters out Dashboard agent', () => {
|
|
138
|
+
const agents = [
|
|
139
|
+
makeAgent('Dashboard'),
|
|
140
|
+
makeAgent('backend-api'),
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
render(<AgentList agents={agents} />);
|
|
144
|
+
expect(screen.getByText('1 agent')).toBeTruthy();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentList Component
|
|
3
|
+
*
|
|
4
|
+
* Displays agents grouped by their hierarchical prefix with
|
|
5
|
+
* collapsible groups and color coding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
|
9
|
+
import type { Agent } from '../types';
|
|
10
|
+
import { AgentCard } from './AgentCard';
|
|
11
|
+
import { groupAgents, getGroupStats, filterAgents, getAgentDisplayName, type AgentGroup } from '../lib/hierarchy';
|
|
12
|
+
import { STATUS_COLORS, getAgentColor, getAgentInitials } from '../lib/colors';
|
|
13
|
+
|
|
14
|
+
export interface AgentListProps {
|
|
15
|
+
agents: Agent[];
|
|
16
|
+
selectedAgent?: string;
|
|
17
|
+
searchQuery?: string;
|
|
18
|
+
/** Array of pinned agent names */
|
|
19
|
+
pinnedAgents?: string[];
|
|
20
|
+
/** Whether max pins has been reached */
|
|
21
|
+
isMaxPinned?: boolean;
|
|
22
|
+
onAgentSelect?: (agent: Agent) => void;
|
|
23
|
+
onAgentMessage?: (agent: Agent) => void;
|
|
24
|
+
onReleaseClick?: (agent: Agent) => void;
|
|
25
|
+
onLogsClick?: (agent: Agent) => void;
|
|
26
|
+
onProfileClick?: (agent: Agent) => void;
|
|
27
|
+
/** Handler for pin/unpin toggle */
|
|
28
|
+
onPinToggle?: (agent: Agent) => void;
|
|
29
|
+
compact?: boolean;
|
|
30
|
+
showGroupStats?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function AgentList({
|
|
34
|
+
agents,
|
|
35
|
+
selectedAgent,
|
|
36
|
+
searchQuery = '',
|
|
37
|
+
pinnedAgents = [],
|
|
38
|
+
isMaxPinned = false,
|
|
39
|
+
onAgentSelect,
|
|
40
|
+
onAgentMessage,
|
|
41
|
+
onReleaseClick,
|
|
42
|
+
onLogsClick,
|
|
43
|
+
onProfileClick,
|
|
44
|
+
onPinToggle,
|
|
45
|
+
compact = false,
|
|
46
|
+
showGroupStats = true,
|
|
47
|
+
}: AgentListProps) {
|
|
48
|
+
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
|
49
|
+
const [isPinnedExpanded, setIsPinnedExpanded] = useState(true);
|
|
50
|
+
const [isAllCollapsed, setIsAllCollapsed] = useState(false);
|
|
51
|
+
|
|
52
|
+
// Filter out setup agents (temporary agents for provider auth)
|
|
53
|
+
// and system agents like Dashboard (used for dashboard message sending)
|
|
54
|
+
// then apply search filtering
|
|
55
|
+
const filteredAgents = useMemo(() => {
|
|
56
|
+
const nonSystemAgents = agents.filter(a =>
|
|
57
|
+
!a.name.startsWith('__setup__') && a.name !== 'Dashboard'
|
|
58
|
+
);
|
|
59
|
+
return filterAgents(nonSystemAgents, searchQuery);
|
|
60
|
+
}, [agents, searchQuery]);
|
|
61
|
+
|
|
62
|
+
// Separate pinned and unpinned agents
|
|
63
|
+
const { pinnedAgentsList, unpinnedAgents } = useMemo(() => {
|
|
64
|
+
const pinned: Agent[] = [];
|
|
65
|
+
const unpinned: Agent[] = [];
|
|
66
|
+
for (const agent of filteredAgents) {
|
|
67
|
+
if (pinnedAgents.includes(agent.name)) {
|
|
68
|
+
pinned.push(agent);
|
|
69
|
+
} else {
|
|
70
|
+
unpinned.push(agent);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Sort pinned agents by their order in pinnedAgents array
|
|
74
|
+
pinned.sort((a, b) => pinnedAgents.indexOf(a.name) - pinnedAgents.indexOf(b.name));
|
|
75
|
+
return { pinnedAgentsList: pinned, unpinnedAgents: unpinned };
|
|
76
|
+
}, [filteredAgents, pinnedAgents]);
|
|
77
|
+
|
|
78
|
+
const groups = useMemo(
|
|
79
|
+
() => groupAgents(unpinnedAgents),
|
|
80
|
+
[unpinnedAgents]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Track if we've done initial expansion
|
|
84
|
+
const hasInitialized = useRef(false);
|
|
85
|
+
|
|
86
|
+
// Initialize all groups as expanded on first render
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!hasInitialized.current && groups.length > 0) {
|
|
89
|
+
setExpandedGroups(new Set(groups.map((g) => g.prefix)));
|
|
90
|
+
hasInitialized.current = true;
|
|
91
|
+
}
|
|
92
|
+
}, [groups]);
|
|
93
|
+
|
|
94
|
+
const toggleGroup = (prefix: string) => {
|
|
95
|
+
setExpandedGroups((prev) => {
|
|
96
|
+
const next = new Set(prev);
|
|
97
|
+
if (next.has(prefix)) {
|
|
98
|
+
next.delete(prefix);
|
|
99
|
+
} else {
|
|
100
|
+
next.add(prefix);
|
|
101
|
+
}
|
|
102
|
+
return next;
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const toggleAll = () => {
|
|
107
|
+
if (isAllCollapsed) {
|
|
108
|
+
// Expand: restore all groups and show everything
|
|
109
|
+
setIsAllCollapsed(false);
|
|
110
|
+
setExpandedGroups(new Set(groups.map((g) => g.prefix)));
|
|
111
|
+
setIsPinnedExpanded(true);
|
|
112
|
+
} else {
|
|
113
|
+
// Collapse: hide entire panel contents
|
|
114
|
+
setIsAllCollapsed(true);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (agents.length === 0) {
|
|
119
|
+
return (
|
|
120
|
+
<div className="flex flex-col items-center justify-center py-10 px-5 text-text-muted text-center">
|
|
121
|
+
<EmptyIcon />
|
|
122
|
+
<p>No agents connected</p>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (filteredAgents.length === 0) {
|
|
128
|
+
return (
|
|
129
|
+
<div className="flex flex-col items-center justify-center py-10 px-5 text-text-muted text-center">
|
|
130
|
+
<SearchIcon />
|
|
131
|
+
<p>No agents match "{searchQuery}"</p>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex flex-col gap-1">
|
|
138
|
+
<div className="flex justify-between items-center py-2 px-3 text-xs text-text-muted">
|
|
139
|
+
<span>{filteredAgents.length} {filteredAgents.length === 1 ? 'agent' : 'agents'}</span>
|
|
140
|
+
<button
|
|
141
|
+
className="bg-transparent border-none text-accent cursor-pointer text-xs hover:underline"
|
|
142
|
+
onClick={toggleAll}
|
|
143
|
+
>
|
|
144
|
+
{isAllCollapsed ? 'Expand all' : 'Collapse all'}
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{!isAllCollapsed && (
|
|
149
|
+
<>
|
|
150
|
+
{/* Pinned Agents Section */}
|
|
151
|
+
{pinnedAgentsList.length > 0 && (
|
|
152
|
+
<div className="mb-2">
|
|
153
|
+
<button
|
|
154
|
+
className="flex items-center gap-2 w-full py-2 px-3 bg-transparent border-none cursor-pointer text-sm text-left rounded transition-colors duration-200 relative hover:bg-amber-400/5"
|
|
155
|
+
onClick={() => setIsPinnedExpanded(!isPinnedExpanded)}
|
|
156
|
+
>
|
|
157
|
+
<div className="absolute left-0 top-1 bottom-1 w-[3px] rounded-sm bg-amber-400" />
|
|
158
|
+
<PinnedChevronIcon expanded={isPinnedExpanded} />
|
|
159
|
+
<PinHeaderIcon />
|
|
160
|
+
<span className="font-semibold text-amber-400">Pinned</span>
|
|
161
|
+
<span className="text-text-muted font-normal">({pinnedAgentsList.length})</span>
|
|
162
|
+
</button>
|
|
163
|
+
|
|
164
|
+
{isPinnedExpanded && (
|
|
165
|
+
<div className="py-1 pl-4 flex flex-col gap-1">
|
|
166
|
+
{pinnedAgentsList.map((agent) => (
|
|
167
|
+
<AgentCard
|
|
168
|
+
key={agent.name}
|
|
169
|
+
agent={agent}
|
|
170
|
+
isSelected={agent.name === selectedAgent}
|
|
171
|
+
compact={compact}
|
|
172
|
+
isPinned={true}
|
|
173
|
+
isMaxPinned={isMaxPinned}
|
|
174
|
+
onClick={onAgentSelect}
|
|
175
|
+
onMessageClick={onAgentMessage}
|
|
176
|
+
onReleaseClick={onReleaseClick}
|
|
177
|
+
onLogsClick={onLogsClick}
|
|
178
|
+
onProfileClick={onProfileClick}
|
|
179
|
+
onPinToggle={onPinToggle}
|
|
180
|
+
/>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{groups.map((group) => (
|
|
188
|
+
<AgentGroupComponent
|
|
189
|
+
key={group.prefix}
|
|
190
|
+
group={group}
|
|
191
|
+
isExpanded={expandedGroups.has(group.prefix)}
|
|
192
|
+
selectedAgent={selectedAgent}
|
|
193
|
+
compact={compact}
|
|
194
|
+
showStats={showGroupStats}
|
|
195
|
+
pinnedAgents={pinnedAgents}
|
|
196
|
+
isMaxPinned={isMaxPinned}
|
|
197
|
+
onToggle={() => toggleGroup(group.prefix)}
|
|
198
|
+
onAgentSelect={onAgentSelect}
|
|
199
|
+
onAgentMessage={onAgentMessage}
|
|
200
|
+
onReleaseClick={onReleaseClick}
|
|
201
|
+
onLogsClick={onLogsClick}
|
|
202
|
+
onProfileClick={onProfileClick}
|
|
203
|
+
onPinToggle={onPinToggle}
|
|
204
|
+
/>
|
|
205
|
+
))}
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface AgentGroupComponentProps {
|
|
213
|
+
group: AgentGroup;
|
|
214
|
+
isExpanded: boolean;
|
|
215
|
+
selectedAgent?: string;
|
|
216
|
+
compact?: boolean;
|
|
217
|
+
showStats?: boolean;
|
|
218
|
+
pinnedAgents?: string[];
|
|
219
|
+
isMaxPinned?: boolean;
|
|
220
|
+
onToggle: () => void;
|
|
221
|
+
onAgentSelect?: (agent: Agent) => void;
|
|
222
|
+
onAgentMessage?: (agent: Agent) => void;
|
|
223
|
+
onReleaseClick?: (agent: Agent) => void;
|
|
224
|
+
onLogsClick?: (agent: Agent) => void;
|
|
225
|
+
onProfileClick?: (agent: Agent) => void;
|
|
226
|
+
onPinToggle?: (agent: Agent) => void;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function AgentGroupComponent({
|
|
230
|
+
group,
|
|
231
|
+
isExpanded,
|
|
232
|
+
selectedAgent,
|
|
233
|
+
compact,
|
|
234
|
+
showStats,
|
|
235
|
+
pinnedAgents = [],
|
|
236
|
+
isMaxPinned = false,
|
|
237
|
+
onToggle,
|
|
238
|
+
onAgentSelect,
|
|
239
|
+
onAgentMessage,
|
|
240
|
+
onReleaseClick,
|
|
241
|
+
onLogsClick,
|
|
242
|
+
onProfileClick,
|
|
243
|
+
onPinToggle,
|
|
244
|
+
}: AgentGroupComponentProps) {
|
|
245
|
+
const stats = showStats ? getGroupStats(group.agents) : null;
|
|
246
|
+
|
|
247
|
+
// Check if this is a "solo" agent - single agent in group where name matches prefix
|
|
248
|
+
const isSoloAgent =
|
|
249
|
+
group.agents.length === 1 &&
|
|
250
|
+
group.agents[0].name.toLowerCase() === group.prefix.toLowerCase();
|
|
251
|
+
|
|
252
|
+
// For solo agents, render just the card without a group header
|
|
253
|
+
if (isSoloAgent) {
|
|
254
|
+
const agent = group.agents[0];
|
|
255
|
+
return (
|
|
256
|
+
<div className="mb-1 py-1 px-2">
|
|
257
|
+
<AgentCard
|
|
258
|
+
key={agent.name}
|
|
259
|
+
agent={agent}
|
|
260
|
+
isSelected={agent.name === selectedAgent}
|
|
261
|
+
compact={compact}
|
|
262
|
+
isPinned={pinnedAgents.includes(agent.name)}
|
|
263
|
+
isMaxPinned={isMaxPinned}
|
|
264
|
+
onClick={onAgentSelect}
|
|
265
|
+
onMessageClick={onAgentMessage}
|
|
266
|
+
onReleaseClick={onReleaseClick}
|
|
267
|
+
onLogsClick={onLogsClick}
|
|
268
|
+
onProfileClick={onProfileClick}
|
|
269
|
+
onPinToggle={onPinToggle}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<div className="mb-1">
|
|
277
|
+
<button
|
|
278
|
+
className="flex items-center gap-2 w-full py-2 px-3 bg-transparent border-none cursor-pointer text-sm text-left rounded transition-colors duration-200 relative hover:bg-[var(--group-light)]"
|
|
279
|
+
onClick={onToggle}
|
|
280
|
+
style={{
|
|
281
|
+
'--group-color': group.color.primary,
|
|
282
|
+
'--group-light': group.color.light,
|
|
283
|
+
} as React.CSSProperties}
|
|
284
|
+
>
|
|
285
|
+
<div
|
|
286
|
+
className="absolute left-0 top-1 bottom-1 w-[3px] rounded-sm"
|
|
287
|
+
style={{ backgroundColor: group.color.primary }}
|
|
288
|
+
/>
|
|
289
|
+
<ChevronIcon expanded={isExpanded} />
|
|
290
|
+
<span className="font-semibold text-text-primary">{group.displayName}</span>
|
|
291
|
+
<span className="text-text-muted font-normal">({group.agents.length})</span>
|
|
292
|
+
|
|
293
|
+
{showStats && stats && (
|
|
294
|
+
<div className="ml-auto flex gap-2">
|
|
295
|
+
{stats.online > 0 && (
|
|
296
|
+
<span className="flex items-center gap-1 text-xs text-text-muted" title={`${stats.online} agent${stats.online > 1 ? 's' : ''} online`}>
|
|
297
|
+
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: STATUS_COLORS.online }} />
|
|
298
|
+
{stats.online}
|
|
299
|
+
</span>
|
|
300
|
+
)}
|
|
301
|
+
{stats.needsAttention > 0 && (
|
|
302
|
+
<span className="flex items-center gap-1 text-xs text-text-muted" title={`${stats.needsAttention} agent${stats.needsAttention > 1 ? 's' : ''} need${stats.needsAttention === 1 ? 's' : ''} attention`}>
|
|
303
|
+
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: STATUS_COLORS.attention }} />
|
|
304
|
+
{stats.needsAttention}
|
|
305
|
+
</span>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</button>
|
|
310
|
+
|
|
311
|
+
{isExpanded && (
|
|
312
|
+
<div className="py-1 pl-4 flex flex-col gap-1">
|
|
313
|
+
{group.agents.map((agent) => (
|
|
314
|
+
<AgentCard
|
|
315
|
+
key={agent.name}
|
|
316
|
+
agent={agent}
|
|
317
|
+
isSelected={agent.name === selectedAgent}
|
|
318
|
+
compact={compact}
|
|
319
|
+
displayNameOverride={getAgentDisplayName(agent.name)}
|
|
320
|
+
isPinned={pinnedAgents.includes(agent.name)}
|
|
321
|
+
isMaxPinned={isMaxPinned}
|
|
322
|
+
onClick={onAgentSelect}
|
|
323
|
+
onMessageClick={onAgentMessage}
|
|
324
|
+
onReleaseClick={onReleaseClick}
|
|
325
|
+
onLogsClick={onLogsClick}
|
|
326
|
+
onProfileClick={onProfileClick}
|
|
327
|
+
onPinToggle={onPinToggle}
|
|
328
|
+
/>
|
|
329
|
+
))}
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function ChevronIcon({ expanded }: { expanded: boolean }) {
|
|
337
|
+
return (
|
|
338
|
+
<svg
|
|
339
|
+
className={`text-text-muted transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`}
|
|
340
|
+
width="16"
|
|
341
|
+
height="16"
|
|
342
|
+
viewBox="0 0 24 24"
|
|
343
|
+
fill="none"
|
|
344
|
+
stroke="currentColor"
|
|
345
|
+
strokeWidth="2"
|
|
346
|
+
>
|
|
347
|
+
<polyline points="9 18 15 12 9 6" />
|
|
348
|
+
</svg>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function EmptyIcon() {
|
|
353
|
+
return (
|
|
354
|
+
<svg
|
|
355
|
+
className="mb-3 opacity-50"
|
|
356
|
+
width="48"
|
|
357
|
+
height="48"
|
|
358
|
+
viewBox="0 0 24 24"
|
|
359
|
+
fill="none"
|
|
360
|
+
stroke="currentColor"
|
|
361
|
+
strokeWidth="1"
|
|
362
|
+
>
|
|
363
|
+
<circle cx="12" cy="8" r="5" />
|
|
364
|
+
<path d="M20 21a8 8 0 1 0-16 0" />
|
|
365
|
+
</svg>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function SearchIcon() {
|
|
370
|
+
return (
|
|
371
|
+
<svg
|
|
372
|
+
className="mb-3 opacity-50"
|
|
373
|
+
width="48"
|
|
374
|
+
height="48"
|
|
375
|
+
viewBox="0 0 24 24"
|
|
376
|
+
fill="none"
|
|
377
|
+
stroke="currentColor"
|
|
378
|
+
strokeWidth="1"
|
|
379
|
+
>
|
|
380
|
+
<circle cx="11" cy="11" r="8" />
|
|
381
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
382
|
+
</svg>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function PinnedChevronIcon({ expanded }: { expanded: boolean }) {
|
|
387
|
+
return (
|
|
388
|
+
<svg
|
|
389
|
+
className={`text-amber-400 transition-transform duration-200 ${expanded ? 'rotate-90' : ''}`}
|
|
390
|
+
width="16"
|
|
391
|
+
height="16"
|
|
392
|
+
viewBox="0 0 24 24"
|
|
393
|
+
fill="none"
|
|
394
|
+
stroke="currentColor"
|
|
395
|
+
strokeWidth="2"
|
|
396
|
+
>
|
|
397
|
+
<polyline points="9 18 15 12 9 6" />
|
|
398
|
+
</svg>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function PinHeaderIcon() {
|
|
403
|
+
return (
|
|
404
|
+
<svg
|
|
405
|
+
width="14"
|
|
406
|
+
height="14"
|
|
407
|
+
viewBox="0 0 24 24"
|
|
408
|
+
fill="currentColor"
|
|
409
|
+
stroke="currentColor"
|
|
410
|
+
strokeWidth="2"
|
|
411
|
+
strokeLinecap="round"
|
|
412
|
+
strokeLinejoin="round"
|
|
413
|
+
className="text-amber-400"
|
|
414
|
+
>
|
|
415
|
+
<path d="M12 17v5" />
|
|
416
|
+
<path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4.76Z" />
|
|
417
|
+
</svg>
|
|
418
|
+
);
|
|
419
|
+
}
|