@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.
Files changed (244) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
  3. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
  6. package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
  7. package/out/about.html +2 -2
  8. package/out/about.txt +1 -1
  9. package/out/app/onboarding.html +1 -1
  10. package/out/app/onboarding.txt +1 -1
  11. package/out/app.html +1 -1
  12. package/out/app.txt +2 -2
  13. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  14. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  15. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  16. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  17. package/out/blog.html +2 -2
  18. package/out/blog.txt +1 -1
  19. package/out/careers.html +2 -2
  20. package/out/careers.txt +1 -1
  21. package/out/changelog.html +2 -2
  22. package/out/changelog.txt +1 -1
  23. package/out/cloud/link.html +1 -1
  24. package/out/cloud/link.txt +2 -2
  25. package/out/complete-profile.html +2 -2
  26. package/out/complete-profile.txt +1 -1
  27. package/out/connect-repos.html +1 -1
  28. package/out/connect-repos.txt +1 -1
  29. package/out/contact.html +2 -2
  30. package/out/contact.txt +1 -1
  31. package/out/docs.html +2 -2
  32. package/out/docs.txt +1 -1
  33. package/out/history.html +1 -1
  34. package/out/history.txt +2 -2
  35. package/out/index.html +1 -1
  36. package/out/index.txt +2 -2
  37. package/out/login.html +2 -2
  38. package/out/login.txt +1 -1
  39. package/out/metrics.html +1 -1
  40. package/out/metrics.txt +2 -2
  41. package/out/pricing.html +2 -2
  42. package/out/pricing.txt +1 -1
  43. package/out/privacy.html +2 -2
  44. package/out/privacy.txt +1 -1
  45. package/out/providers/setup/claude.html +1 -1
  46. package/out/providers/setup/claude.txt +1 -1
  47. package/out/providers/setup/codex.html +1 -1
  48. package/out/providers/setup/codex.txt +1 -1
  49. package/out/providers/setup/cursor.html +1 -1
  50. package/out/providers/setup/cursor.txt +1 -1
  51. package/out/providers.html +1 -1
  52. package/out/providers.txt +1 -1
  53. package/out/security.html +2 -2
  54. package/out/security.txt +1 -1
  55. package/out/signup.html +2 -2
  56. package/out/signup.txt +1 -1
  57. package/out/terms.html +2 -2
  58. package/out/terms.txt +1 -1
  59. package/package.json +7 -1
  60. package/src/app/about/page.tsx +7 -0
  61. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
  62. package/src/app/app/[[...slug]]/page.tsx +23 -0
  63. package/src/app/app/onboarding/page.tsx +394 -0
  64. package/src/app/apple-icon.png +0 -0
  65. package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
  66. package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
  67. package/src/app/blog/page.tsx +15 -0
  68. package/src/app/careers/page.tsx +7 -0
  69. package/src/app/changelog/page.tsx +7 -0
  70. package/src/app/cloud/link/page.tsx +464 -0
  71. package/src/app/complete-profile/page.tsx +204 -0
  72. package/src/app/connect-repos/page.tsx +410 -0
  73. package/src/app/contact/page.tsx +7 -0
  74. package/src/app/docs/page.tsx +7 -0
  75. package/src/app/favicon.png +0 -0
  76. package/src/app/globals.css +200 -0
  77. package/src/app/history/page.tsx +658 -0
  78. package/src/app/layout.tsx +25 -0
  79. package/src/app/login/page.tsx +424 -0
  80. package/src/app/metrics/page.tsx +781 -0
  81. package/src/app/page.tsx +59 -0
  82. package/src/app/pricing/page.tsx +7 -0
  83. package/src/app/privacy/page.tsx +7 -0
  84. package/src/app/providers/page.tsx +193 -0
  85. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
  86. package/src/app/providers/setup/[provider]/constants.ts +35 -0
  87. package/src/app/providers/setup/[provider]/page.tsx +42 -0
  88. package/src/app/security/page.tsx +7 -0
  89. package/src/app/signup/page.tsx +533 -0
  90. package/src/app/terms/page.tsx +7 -0
  91. package/src/components/ActivityFeed.tsx +216 -0
  92. package/src/components/AddWorkspaceModal.tsx +170 -0
  93. package/src/components/AgentCard.test.tsx +134 -0
  94. package/src/components/AgentCard.tsx +585 -0
  95. package/src/components/AgentList.test.tsx +147 -0
  96. package/src/components/AgentList.tsx +419 -0
  97. package/src/components/AgentLogPreview.tsx +173 -0
  98. package/src/components/AgentProfilePanel.tsx +569 -0
  99. package/src/components/App.tsx +3424 -0
  100. package/src/components/BillingPanel.tsx +922 -0
  101. package/src/components/BillingResult.tsx +447 -0
  102. package/src/components/BroadcastComposer.tsx +690 -0
  103. package/src/components/ChannelAdminPanel.tsx +773 -0
  104. package/src/components/ChannelBrowser.tsx +385 -0
  105. package/src/components/ChannelChat.tsx +261 -0
  106. package/src/components/ChannelSidebar.tsx +399 -0
  107. package/src/components/CloudSessionProvider.tsx +130 -0
  108. package/src/components/CommandPalette.tsx +815 -0
  109. package/src/components/ConfirmationDialog.tsx +133 -0
  110. package/src/components/ConversationHistory.tsx +518 -0
  111. package/src/components/CoordinatorPanel.tsx +956 -0
  112. package/src/components/DecisionQueue.tsx +717 -0
  113. package/src/components/DirectMessageView.tsx +164 -0
  114. package/src/components/FileAutocomplete.tsx +368 -0
  115. package/src/components/FleetOverview.tsx +278 -0
  116. package/src/components/LogViewer.tsx +310 -0
  117. package/src/components/LogViewerPanel.tsx +482 -0
  118. package/src/components/Logo.tsx +284 -0
  119. package/src/components/MentionAutocomplete.tsx +384 -0
  120. package/src/components/MessageComposer.tsx +473 -0
  121. package/src/components/MessageList.tsx +725 -0
  122. package/src/components/MessageSenderName.tsx +91 -0
  123. package/src/components/MessageStatusIndicator.tsx +142 -0
  124. package/src/components/NewConversationModal.tsx +400 -0
  125. package/src/components/NotificationToast.tsx +488 -0
  126. package/src/components/OnlineUsersIndicator.tsx +164 -0
  127. package/src/components/Pagination.tsx +124 -0
  128. package/src/components/PricingPlans.tsx +386 -0
  129. package/src/components/ProjectList.tsx +711 -0
  130. package/src/components/ProviderAuthFlow.tsx +343 -0
  131. package/src/components/ProviderConnectionList.tsx +375 -0
  132. package/src/components/ProvisioningProgress.tsx +730 -0
  133. package/src/components/ReactionChips.tsx +70 -0
  134. package/src/components/ReactionPicker.tsx +121 -0
  135. package/src/components/RepoAccessPanel.tsx +787 -0
  136. package/src/components/RepositoriesPanel.tsx +901 -0
  137. package/src/components/ServerCard.tsx +202 -0
  138. package/src/components/SessionExpiredModal.tsx +128 -0
  139. package/src/components/SpawnModal.test.tsx +190 -0
  140. package/src/components/SpawnModal.tsx +1001 -0
  141. package/src/components/TaskAssignmentUI.tsx +375 -0
  142. package/src/components/TerminalProviderSetup.tsx +517 -0
  143. package/src/components/ThemeProvider.tsx +159 -0
  144. package/src/components/ThinkingIndicator.tsx +231 -0
  145. package/src/components/ThreadList.tsx +198 -0
  146. package/src/components/ThreadPanel.tsx +405 -0
  147. package/src/components/TrajectoryViewer.tsx +698 -0
  148. package/src/components/TypingIndicator.tsx +69 -0
  149. package/src/components/UsageBanner.tsx +231 -0
  150. package/src/components/UserProfilePanel.tsx +233 -0
  151. package/src/components/WorkspaceContext.tsx +95 -0
  152. package/src/components/WorkspaceSelector.tsx +234 -0
  153. package/src/components/WorkspaceStatusIndicator.tsx +396 -0
  154. package/src/components/XTermInteractive.tsx +516 -0
  155. package/src/components/XTermLogViewer.tsx +719 -0
  156. package/src/components/channels/ChannelDialogs.tsx +1411 -0
  157. package/src/components/channels/ChannelHeader.tsx +317 -0
  158. package/src/components/channels/ChannelMessageList.tsx +463 -0
  159. package/src/components/channels/ChannelViewV1.tsx +146 -0
  160. package/src/components/channels/MessageInput.tsx +302 -0
  161. package/src/components/channels/SearchInput.tsx +172 -0
  162. package/src/components/channels/SearchResults.tsx +336 -0
  163. package/src/components/channels/api.test.ts +1527 -0
  164. package/src/components/channels/api.ts +703 -0
  165. package/src/components/channels/index.ts +76 -0
  166. package/src/components/channels/mockApi.ts +344 -0
  167. package/src/components/channels/types.ts +566 -0
  168. package/src/components/hooks/index.ts +58 -0
  169. package/src/components/hooks/useAgentLogs.ts +504 -0
  170. package/src/components/hooks/useAgents.ts +127 -0
  171. package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
  172. package/src/components/hooks/useBroadcastDedup.ts +86 -0
  173. package/src/components/hooks/useChannelAdmin.ts +329 -0
  174. package/src/components/hooks/useChannelBrowser.ts +239 -0
  175. package/src/components/hooks/useChannelCommands.ts +138 -0
  176. package/src/components/hooks/useChannels.ts +367 -0
  177. package/src/components/hooks/useDebounce.ts +29 -0
  178. package/src/components/hooks/useDirectMessage.test.ts +952 -0
  179. package/src/components/hooks/useDirectMessage.ts +141 -0
  180. package/src/components/hooks/useMessages.ts +310 -0
  181. package/src/components/hooks/useOrchestrator.test.ts +165 -0
  182. package/src/components/hooks/useOrchestrator.ts +424 -0
  183. package/src/components/hooks/usePinnedAgents.test.ts +356 -0
  184. package/src/components/hooks/usePinnedAgents.ts +140 -0
  185. package/src/components/hooks/usePresence.test.ts +245 -0
  186. package/src/components/hooks/usePresence.ts +377 -0
  187. package/src/components/hooks/useRecentRepos.ts +130 -0
  188. package/src/components/hooks/useSession.ts +209 -0
  189. package/src/components/hooks/useThread.ts +138 -0
  190. package/src/components/hooks/useTrajectory.ts +265 -0
  191. package/src/components/hooks/useWebSocket.ts +290 -0
  192. package/src/components/hooks/useWorkspaceMembers.ts +132 -0
  193. package/src/components/hooks/useWorkspaceRepos.ts +73 -0
  194. package/src/components/hooks/useWorkspaceStatus.ts +237 -0
  195. package/src/components/index.ts +81 -0
  196. package/src/components/layout/Header.tsx +311 -0
  197. package/src/components/layout/RepoContextHeader.tsx +361 -0
  198. package/src/components/layout/Sidebar.archive.test.tsx +126 -0
  199. package/src/components/layout/Sidebar.test.tsx +691 -0
  200. package/src/components/layout/Sidebar.tsx +900 -0
  201. package/src/components/layout/index.ts +7 -0
  202. package/src/components/settings/BillingSettingsPanel.tsx +564 -0
  203. package/src/components/settings/SettingsPage.tsx +683 -0
  204. package/src/components/settings/TeamSettingsPanel.tsx +560 -0
  205. package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
  206. package/src/components/settings/index.ts +11 -0
  207. package/src/components/settings/types.ts +79 -0
  208. package/src/components/utils/messageFormatting.test.tsx +331 -0
  209. package/src/components/utils/messageFormatting.tsx +597 -0
  210. package/src/index.ts +63 -0
  211. package/src/landing/AboutPage.tsx +77 -0
  212. package/src/landing/BlogContent.tsx +187 -0
  213. package/src/landing/BlogPage.tsx +47 -0
  214. package/src/landing/CareersPage.tsx +53 -0
  215. package/src/landing/ChangelogPage.tsx +33 -0
  216. package/src/landing/ContactPage.tsx +41 -0
  217. package/src/landing/DocsPage.tsx +43 -0
  218. package/src/landing/LandingPage.tsx +702 -0
  219. package/src/landing/PricingPage.tsx +549 -0
  220. package/src/landing/PrivacyPage.tsx +117 -0
  221. package/src/landing/SecurityPage.tsx +42 -0
  222. package/src/landing/StaticPage.tsx +165 -0
  223. package/src/landing/TermsPage.tsx +125 -0
  224. package/src/landing/blogData.ts +312 -0
  225. package/src/landing/index.ts +18 -0
  226. package/src/landing/styles.css +3673 -0
  227. package/src/lib/agent-merge.test.ts +43 -0
  228. package/src/lib/agent-merge.ts +35 -0
  229. package/src/lib/api.ts +1294 -0
  230. package/src/lib/cloudApi.ts +893 -0
  231. package/src/lib/colors.test.ts +175 -0
  232. package/src/lib/colors.ts +218 -0
  233. package/src/lib/config.ts +109 -0
  234. package/src/lib/hierarchy.ts +242 -0
  235. package/src/lib/stuckDetection.ts +142 -0
  236. package/src/lib/useUrlRouting.ts +190 -0
  237. package/src/types/index.ts +317 -0
  238. package/src/types/threading.ts +7 -0
  239. package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
  240. package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
  241. /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
  243. /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
  244. /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
+ }