@agent-relay/dashboard 2.0.80 → 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/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → 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,711 @@
1
+ /**
2
+ * ProjectList Component
3
+ *
4
+ * Displays projects with nested agents in a flat hierarchy.
5
+ * Each project is a collapsible section with its agents listed directly underneath.
6
+ */
7
+
8
+ import React, { useState, useMemo, useEffect } from 'react';
9
+ import type { Agent, Project } from '../types';
10
+ import { AgentCard } from './AgentCard';
11
+ import { STATUS_COLORS, getAgentColor } from '../lib/colors';
12
+
13
+ /**
14
+ * Gets the simple display name for an agent within a team group.
15
+ * Since the team header already shows context, just show the agent's role/name.
16
+ *
17
+ * Examples:
18
+ * - "Frontend-Lead" in team "Frontend" → "Lead"
19
+ * - "Lead" in team "Lead" → "Lead"
20
+ * - "backend-api" in team "backend" → "Api"
21
+ */
22
+ function stripTeamPrefix(agentName: string, teamName: string): string {
23
+ const lowerAgent = agentName.toLowerCase();
24
+ const lowerTeam = teamName.toLowerCase().replace(/-?team$/i, '');
25
+
26
+ // Pattern 1: Team prefix with dash separator (e.g., "frontend-lead" → "lead")
27
+ if (lowerTeam && lowerAgent.startsWith(lowerTeam + '-')) {
28
+ const stripped = agentName.substring(lowerTeam.length + 1);
29
+ return capitalizeWords(stripped);
30
+ }
31
+
32
+ // Pattern 2: Team prefix with underscore separator
33
+ if (lowerTeam && lowerAgent.startsWith(lowerTeam + '_')) {
34
+ const stripped = agentName.substring(lowerTeam.length + 1);
35
+ return capitalizeWords(stripped);
36
+ }
37
+
38
+ // Pattern 3: Last segment of hyphenated name (e.g., "LeadFrontend-Dev" → "Dev")
39
+ const parts = agentName.split('-');
40
+ if (parts.length > 1) {
41
+ return capitalizeWords(parts[parts.length - 1]);
42
+ }
43
+
44
+ // No prefix to strip, return capitalized original
45
+ return capitalizeWords(agentName);
46
+ }
47
+
48
+ /**
49
+ * Capitalizes words in a string (handles dash and underscore separators)
50
+ */
51
+ function capitalizeWords(str: string): string {
52
+ return str
53
+ .split(/[-_]/)
54
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
55
+ .join(' ');
56
+ }
57
+
58
+ export interface ProjectListProps {
59
+ projects: Project[];
60
+ localAgents?: Agent[];
61
+ /** Bridge-level agents like Architect that span all projects */
62
+ bridgeAgents?: Agent[];
63
+ currentProject?: string;
64
+ selectedAgent?: string;
65
+ searchQuery?: string;
66
+ /** Array of pinned agent names */
67
+ pinnedAgents?: string[];
68
+ /** Whether max pins has been reached */
69
+ isMaxPinned?: boolean;
70
+ onProjectSelect?: (project: Project) => void;
71
+ onAgentSelect?: (agent: Agent, project?: Project) => void;
72
+ onReleaseClick?: (agent: Agent) => void;
73
+ onLogsClick?: (agent: Agent) => void;
74
+ onProfileClick?: (agent: Agent) => void;
75
+ /** Handler for pin/unpin toggle */
76
+ onPinToggle?: (agent: Agent) => void;
77
+ compact?: boolean;
78
+ }
79
+
80
+ export function ProjectList({
81
+ projects,
82
+ localAgents = [],
83
+ bridgeAgents = [],
84
+ currentProject,
85
+ selectedAgent,
86
+ searchQuery = '',
87
+ pinnedAgents = [],
88
+ isMaxPinned = false,
89
+ onProjectSelect,
90
+ onAgentSelect,
91
+ onReleaseClick,
92
+ onLogsClick,
93
+ onProfileClick,
94
+ onPinToggle,
95
+ compact = false,
96
+ }: ProjectListProps) {
97
+ const [expandedProjects, setExpandedProjects] = useState<Set<string>>(
98
+ () => new Set(projects.map((p) => p.id))
99
+ );
100
+ const [isAllCollapsed, setIsAllCollapsed] = useState(false);
101
+
102
+ // Filter out system agents (setup agents and Dashboard) and human users
103
+ // These should not appear in the sidebar but can still send/receive messages
104
+ // Human users (isHuman or cli === 'dashboard') should appear in Direct Messages, not projects
105
+ const filterSystemAgents = (agents: Agent[]) =>
106
+ agents.filter(a =>
107
+ !a.name.startsWith('__setup__') &&
108
+ a.name !== 'Dashboard' &&
109
+ !a.isHuman &&
110
+ a.cli !== 'dashboard'
111
+ );
112
+
113
+ // Filter projects and agents based on search query
114
+ const filteredData = useMemo(() => {
115
+ const query = searchQuery.toLowerCase().trim();
116
+
117
+ // Always filter system agents first
118
+ const filteredLocalAgents = filterSystemAgents(localAgents);
119
+ const filteredBridgeAgents = filterSystemAgents(bridgeAgents);
120
+ const projectsWithFilteredAgents = projects.map(p => ({
121
+ ...p,
122
+ agents: filterSystemAgents(p.agents),
123
+ }));
124
+
125
+ if (!query) {
126
+ return { projects: projectsWithFilteredAgents, localAgents: filteredLocalAgents, bridgeAgents: filteredBridgeAgents };
127
+ }
128
+
129
+ // Filter local agents by search query (using pre-filtered agents)
130
+ const searchFilteredLocal = filteredLocalAgents.filter(
131
+ (a) =>
132
+ a.name.toLowerCase().includes(query) ||
133
+ a.currentTask?.toLowerCase().includes(query)
134
+ );
135
+
136
+ // Filter bridge agents by search query (using pre-filtered agents)
137
+ const searchFilteredBridge = filteredBridgeAgents.filter(
138
+ (a) =>
139
+ a.name.toLowerCase().includes(query) ||
140
+ a.currentTask?.toLowerCase().includes(query)
141
+ );
142
+
143
+ // Filter projects (show project if name matches OR any agent matches)
144
+ // Uses pre-filtered projects with system agents already removed
145
+ const searchFilteredProjects = projectsWithFilteredAgents
146
+ .map((project) => {
147
+ const projectNameMatches =
148
+ project.name?.toLowerCase().includes(query) ||
149
+ project.path.toLowerCase().includes(query);
150
+
151
+ const searchFilteredAgents = project.agents.filter(
152
+ (a) =>
153
+ a.name.toLowerCase().includes(query) ||
154
+ a.currentTask?.toLowerCase().includes(query)
155
+ );
156
+
157
+ // Include project if name matches or has matching agents
158
+ if (projectNameMatches || searchFilteredAgents.length > 0) {
159
+ return {
160
+ ...project,
161
+ agents: projectNameMatches ? project.agents : searchFilteredAgents,
162
+ };
163
+ }
164
+ return null;
165
+ })
166
+ .filter(Boolean) as Project[];
167
+
168
+ return { projects: searchFilteredProjects, localAgents: searchFilteredLocal, bridgeAgents: searchFilteredBridge };
169
+ }, [projects, localAgents, bridgeAgents, searchQuery]);
170
+
171
+ const toggleProject = (projectId: string) => {
172
+ setExpandedProjects((prev) => {
173
+ const next = new Set(prev);
174
+ if (next.has(projectId)) {
175
+ next.delete(projectId);
176
+ } else {
177
+ next.add(projectId);
178
+ }
179
+ return next;
180
+ });
181
+ };
182
+
183
+ const toggleAll = () => {
184
+ if (isAllCollapsed) {
185
+ // Expand: restore all projects
186
+ setIsAllCollapsed(false);
187
+ const allProjectIds = new Set(filteredData.projects.map((p) => p.id));
188
+ if (filteredData.localAgents.length > 0) {
189
+ allProjectIds.add('__local__');
190
+ }
191
+ setExpandedProjects(allProjectIds);
192
+ } else {
193
+ // Collapse: hide everything
194
+ setIsAllCollapsed(true);
195
+ }
196
+ };
197
+
198
+ const totalAgents =
199
+ filteredData.localAgents.length +
200
+ filteredData.bridgeAgents.length +
201
+ filteredData.projects.reduce((sum, p) => sum + p.agents.length, 0);
202
+
203
+ if (totalAgents === 0 && projects.length === 0 && localAgents.length === 0) {
204
+ return (
205
+ <div className="flex flex-col items-center justify-center py-10 px-5 text-text-muted text-center">
206
+ <EmptyIcon />
207
+ <p>No projects or agents</p>
208
+ </div>
209
+ );
210
+ }
211
+
212
+ if (totalAgents === 0 && searchQuery) {
213
+ return (
214
+ <div className="flex flex-col items-center justify-center py-10 px-5 text-text-muted text-center">
215
+ <SearchIcon />
216
+ <p>No results for "{searchQuery}"</p>
217
+ </div>
218
+ );
219
+ }
220
+
221
+ // Only show bridge section when in bridge mode (multiple projects)
222
+ const isInBridgeMode = filteredData.projects.length > 1;
223
+
224
+ return (
225
+ <div className="flex flex-col gap-1">
226
+ <div className="flex justify-between items-center py-2 px-3 text-xs text-text-muted">
227
+ <span>{totalAgents} {totalAgents === 1 ? 'agent' : 'agents'}</span>
228
+ <button
229
+ className="bg-transparent border-none text-accent cursor-pointer text-xs hover:underline"
230
+ onClick={toggleAll}
231
+ >
232
+ {isAllCollapsed ? 'Expand all' : 'Collapse all'}
233
+ </button>
234
+ </div>
235
+
236
+ {!isAllCollapsed && (
237
+ <>
238
+ {/* Bridge-level agents section (Architect, etc.) - only in bridge mode */}
239
+ {isInBridgeMode && filteredData.bridgeAgents.length > 0 && (
240
+ <BridgeSection
241
+ agents={filteredData.bridgeAgents}
242
+ selectedAgent={selectedAgent}
243
+ compact={compact}
244
+ pinnedAgents={pinnedAgents}
245
+ isMaxPinned={isMaxPinned}
246
+ onAgentSelect={(agent) => onAgentSelect?.(agent)}
247
+ onReleaseClick={onReleaseClick}
248
+ onLogsClick={onLogsClick}
249
+ onProfileClick={onProfileClick}
250
+ onPinToggle={onPinToggle}
251
+ />
252
+ )}
253
+
254
+ {/* Local agents section (current project) - only when not in bridge mode */}
255
+ {!isInBridgeMode && filteredData.localAgents.length > 0 && (
256
+ <ProjectSection
257
+ project={{
258
+ id: '__local__',
259
+ path: '',
260
+ name: 'Local',
261
+ agents: filteredData.localAgents,
262
+ }}
263
+ isExpanded={expandedProjects.has('__local__')}
264
+ isCurrentProject={true}
265
+ selectedAgent={selectedAgent}
266
+ compact={compact}
267
+ pinnedAgents={pinnedAgents}
268
+ isMaxPinned={isMaxPinned}
269
+ onToggle={() => toggleProject('__local__')}
270
+ onAgentSelect={(agent) => onAgentSelect?.(agent)}
271
+ onReleaseClick={onReleaseClick}
272
+ onLogsClick={onLogsClick}
273
+ onProfileClick={onProfileClick}
274
+ onPinToggle={onPinToggle}
275
+ />
276
+ )}
277
+
278
+ {/* Bridged projects */}
279
+ {filteredData.projects.map((project) => (
280
+ <ProjectSection
281
+ key={project.id}
282
+ project={project}
283
+ isExpanded={expandedProjects.has(project.id)}
284
+ isCurrentProject={project.id === currentProject}
285
+ selectedAgent={selectedAgent}
286
+ compact={compact}
287
+ isBridgeMode={isInBridgeMode}
288
+ pinnedAgents={pinnedAgents}
289
+ isMaxPinned={isMaxPinned}
290
+ onToggle={() => toggleProject(project.id)}
291
+ onProjectSelect={() => onProjectSelect?.(project)}
292
+ onAgentSelect={(agent) => onAgentSelect?.(agent, project)}
293
+ onReleaseClick={onReleaseClick}
294
+ onLogsClick={onLogsClick}
295
+ onProfileClick={onProfileClick}
296
+ onPinToggle={onPinToggle}
297
+ />
298
+ ))}
299
+ </>
300
+ )}
301
+ </div>
302
+ );
303
+ }
304
+
305
+ interface ProjectSectionProps {
306
+ project: Project;
307
+ isExpanded: boolean;
308
+ isCurrentProject: boolean;
309
+ selectedAgent?: string;
310
+ compact?: boolean;
311
+ /** Is this project part of a multi-project bridge setup */
312
+ isBridgeMode?: boolean;
313
+ pinnedAgents?: string[];
314
+ isMaxPinned?: boolean;
315
+ onToggle: () => void;
316
+ onProjectSelect?: () => void;
317
+ onAgentSelect?: (agent: Agent) => void;
318
+ onReleaseClick?: (agent: Agent) => void;
319
+ onLogsClick?: (agent: Agent) => void;
320
+ onProfileClick?: (agent: Agent) => void;
321
+ onPinToggle?: (agent: Agent) => void;
322
+ }
323
+
324
+ interface TeamGroup {
325
+ name: string;
326
+ agents: Agent[];
327
+ }
328
+
329
+ function ProjectSection({
330
+ project,
331
+ isExpanded,
332
+ isCurrentProject,
333
+ selectedAgent,
334
+ compact,
335
+ isBridgeMode = false,
336
+ pinnedAgents = [],
337
+ isMaxPinned = false,
338
+ onToggle,
339
+ onProjectSelect,
340
+ onAgentSelect,
341
+ onReleaseClick,
342
+ onLogsClick,
343
+ onProfileClick,
344
+ onPinToggle,
345
+ }: ProjectSectionProps) {
346
+ const [expandedTeams, setExpandedTeams] = useState<Set<string>>(new Set());
347
+
348
+ const stats = useMemo(() => {
349
+ let online = 0;
350
+ let needsAttention = 0;
351
+ for (const agent of project.agents) {
352
+ if (agent.status === 'online') online++;
353
+ if (agent.needsAttention) needsAttention++;
354
+ }
355
+ return { online, needsAttention, total: project.agents.length };
356
+ }, [project.agents]);
357
+
358
+ // Group agents by team (optional user-defined grouping)
359
+ const { teams, ungroupedAgents } = useMemo(() => {
360
+ const teamMap = new Map<string, Agent[]>();
361
+ const ungrouped: Agent[] = [];
362
+
363
+ for (const agent of project.agents) {
364
+ if (agent.team) {
365
+ const existing = teamMap.get(agent.team) || [];
366
+ existing.push(agent);
367
+ teamMap.set(agent.team, existing);
368
+ } else {
369
+ ungrouped.push(agent);
370
+ }
371
+ }
372
+
373
+ const teams: TeamGroup[] = Array.from(teamMap.entries())
374
+ .map(([name, agents]) => ({ name, agents }))
375
+ .sort((a, b) => a.name.localeCompare(b.name));
376
+
377
+ return { teams, ungroupedAgents: ungrouped };
378
+ }, [project.agents]);
379
+
380
+ const toggleTeam = (teamName: string) => {
381
+ setExpandedTeams((prev) => {
382
+ const next = new Set(prev);
383
+ if (next.has(teamName)) {
384
+ next.delete(teamName);
385
+ } else {
386
+ next.add(teamName);
387
+ }
388
+ return next;
389
+ });
390
+ };
391
+
392
+ // Auto-expand teams when project expands
393
+ useEffect(() => {
394
+ if (isExpanded && expandedTeams.size === 0 && teams.length > 0) {
395
+ setExpandedTeams(new Set(teams.map((t) => t.name)));
396
+ }
397
+ }, [isExpanded, teams, expandedTeams]);
398
+
399
+ const projectColor = getAgentColor(project.name || project.id);
400
+ const displayName = project.name || project.path.split('/').pop() || project.id;
401
+
402
+ return (
403
+ <div className="mb-1">
404
+ <button
405
+ className={`group flex items-center gap-2 w-full py-2.5 px-3 bg-none border-none cursor-pointer text-[13px] text-left rounded-md transition-colors duration-200 relative text-text-primary hover:bg-bg-hover ${
406
+ isCurrentProject ? 'bg-success-light' : ''
407
+ }`}
408
+ onClick={onToggle}
409
+ onDoubleClick={onProjectSelect}
410
+ style={{
411
+ '--project-color': projectColor.primary,
412
+ '--project-light': projectColor.light,
413
+ } as React.CSSProperties}
414
+ >
415
+ <div
416
+ className="absolute left-0 top-1 bottom-1 w-[3px] rounded-sm"
417
+ style={{ background: projectColor.primary }}
418
+ />
419
+ <ChevronIcon expanded={isExpanded} />
420
+ <FolderIcon color={projectColor.primary} />
421
+ <span className="font-semibold text-text-primary whitespace-nowrap overflow-hidden text-ellipsis">{displayName}</span>
422
+ <span className="text-text-muted font-normal flex-shrink-0">({stats.total})</span>
423
+
424
+ <div className="ml-auto flex gap-2 flex-shrink-0">
425
+ {stats.online > 0 && (
426
+ <span className="flex items-center gap-1 text-[11px] text-text-dim">
427
+ <span
428
+ className="w-1.5 h-1.5 rounded-full"
429
+ style={{ backgroundColor: STATUS_COLORS.online }}
430
+ />
431
+ {stats.online}
432
+ </span>
433
+ )}
434
+ {stats.needsAttention > 0 && (
435
+ <span className="flex items-center gap-1 text-[11px] text-text-dim">
436
+ <span
437
+ className="w-1.5 h-1.5 rounded-full"
438
+ style={{ backgroundColor: STATUS_COLORS.attention }}
439
+ />
440
+ {stats.needsAttention}
441
+ </span>
442
+ )}
443
+ </div>
444
+
445
+ {project.lead?.connected && (
446
+ <span className="text-[#ffd700] text-xs ml-1" title={`Lead: ${project.lead.name}`}>
447
+
448
+ </span>
449
+ )}
450
+
451
+ {/* Switch button - shown in bridge mode for non-current projects */}
452
+ {isBridgeMode && !isCurrentProject && onProjectSelect && (
453
+ <span
454
+ role="button"
455
+ tabIndex={0}
456
+ className="ml-2 py-1 px-2 text-[10px] font-medium bg-accent-cyan/20 text-accent-cyan rounded cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity hover:bg-accent-cyan/30"
457
+ onClick={(e) => {
458
+ e.stopPropagation();
459
+ onProjectSelect();
460
+ }}
461
+ onKeyDown={(e) => {
462
+ if (e.key === 'Enter' || e.key === ' ') {
463
+ e.preventDefault();
464
+ e.stopPropagation();
465
+ onProjectSelect();
466
+ }
467
+ }}
468
+ title="Switch to this project"
469
+ >
470
+ Switch
471
+ </span>
472
+ )}
473
+ </button>
474
+
475
+ {isExpanded && (
476
+ <div className="py-1 pl-5 flex flex-col gap-1">
477
+ {/* Team groups (optional user-defined) */}
478
+ {teams.map((team) => (
479
+ <div key={team.name} className="mb-0.5">
480
+ <button
481
+ className="flex items-center gap-1.5 w-full py-1.5 px-2 bg-none border-none cursor-pointer text-xs text-left rounded transition-colors duration-200 text-text-secondary hover:bg-bg-hover"
482
+ onClick={() => toggleTeam(team.name)}
483
+ >
484
+ <ChevronIcon expanded={expandedTeams.has(team.name)} />
485
+ <TeamIcon />
486
+ <span className="font-medium text-text-secondary">{team.name}</span>
487
+ <span className="text-text-muted font-normal">({team.agents.length})</span>
488
+ </button>
489
+ {expandedTeams.has(team.name) && (
490
+ <div className="py-0.5 pl-4 flex flex-col gap-1">
491
+ {team.agents.map((agent) => (
492
+ <AgentCard
493
+ key={agent.name}
494
+ agent={agent}
495
+ isSelected={agent.name === selectedAgent}
496
+ compact={compact}
497
+ displayNameOverride={stripTeamPrefix(agent.name, team.name)}
498
+ isPinned={pinnedAgents.includes(agent.name)}
499
+ isMaxPinned={isMaxPinned}
500
+ onClick={onAgentSelect}
501
+ onReleaseClick={onReleaseClick}
502
+ onLogsClick={onLogsClick}
503
+ onProfileClick={onProfileClick}
504
+ onPinToggle={onPinToggle}
505
+ />
506
+ ))}
507
+ </div>
508
+ )}
509
+ </div>
510
+ ))}
511
+
512
+ {/* Ungrouped agents (no team assigned) */}
513
+ {ungroupedAgents.map((agent) => (
514
+ <AgentCard
515
+ key={agent.name}
516
+ agent={agent}
517
+ isSelected={agent.name === selectedAgent}
518
+ compact={compact}
519
+ isPinned={pinnedAgents.includes(agent.name)}
520
+ isMaxPinned={isMaxPinned}
521
+ onClick={onAgentSelect}
522
+ onReleaseClick={onReleaseClick}
523
+ onLogsClick={onLogsClick}
524
+ onProfileClick={onProfileClick}
525
+ onPinToggle={onPinToggle}
526
+ />
527
+ ))}
528
+ </div>
529
+ )}
530
+ </div>
531
+ );
532
+ }
533
+
534
+ /**
535
+ * Bridge Section - displays bridge-level agents (Architect, etc.)
536
+ */
537
+ interface BridgeSectionProps {
538
+ agents: Agent[];
539
+ selectedAgent?: string;
540
+ compact?: boolean;
541
+ pinnedAgents?: string[];
542
+ isMaxPinned?: boolean;
543
+ onAgentSelect?: (agent: Agent) => void;
544
+ onReleaseClick?: (agent: Agent) => void;
545
+ onLogsClick?: (agent: Agent) => void;
546
+ onProfileClick?: (agent: Agent) => void;
547
+ onPinToggle?: (agent: Agent) => void;
548
+ }
549
+
550
+ function BridgeSection({
551
+ agents,
552
+ selectedAgent,
553
+ compact,
554
+ pinnedAgents = [],
555
+ isMaxPinned = false,
556
+ onAgentSelect,
557
+ onReleaseClick,
558
+ onLogsClick,
559
+ onProfileClick,
560
+ onPinToggle,
561
+ }: BridgeSectionProps) {
562
+ const [isExpanded, setIsExpanded] = useState(true);
563
+
564
+ return (
565
+ <div className="mb-2">
566
+ <button
567
+ className="flex items-center gap-2 w-full py-2.5 px-3 bg-none border-none cursor-pointer text-[13px] text-left rounded-md transition-colors duration-200 relative text-text-primary hover:bg-accent-purple/10"
568
+ onClick={() => setIsExpanded(!isExpanded)}
569
+ >
570
+ <div
571
+ className="absolute left-0 top-1 bottom-1 w-[3px] rounded-sm bg-accent-purple"
572
+ />
573
+ <ChevronIcon expanded={isExpanded} />
574
+ <BridgeIcon />
575
+ <span className="font-semibold text-text-primary">Bridge</span>
576
+ <span className="text-text-muted font-normal flex-shrink-0">({agents.length})</span>
577
+ </button>
578
+
579
+ {isExpanded && (
580
+ <div className="py-1 pl-5 flex flex-col gap-1">
581
+ {agents.map((agent) => (
582
+ <AgentCard
583
+ key={agent.name}
584
+ agent={agent}
585
+ isSelected={agent.name === selectedAgent}
586
+ compact={compact}
587
+ isPinned={pinnedAgents.includes(agent.name)}
588
+ isMaxPinned={isMaxPinned}
589
+ onClick={onAgentSelect}
590
+ onReleaseClick={onReleaseClick}
591
+ onLogsClick={onLogsClick}
592
+ onProfileClick={onProfileClick}
593
+ onPinToggle={onPinToggle}
594
+ />
595
+ ))}
596
+ </div>
597
+ )}
598
+ </div>
599
+ );
600
+ }
601
+
602
+ function BridgeIcon() {
603
+ return (
604
+ <svg
605
+ className="flex-shrink-0 text-accent-purple"
606
+ width="16"
607
+ height="16"
608
+ viewBox="0 0 24 24"
609
+ fill="none"
610
+ stroke="currentColor"
611
+ strokeWidth="2"
612
+ strokeLinecap="round"
613
+ strokeLinejoin="round"
614
+ >
615
+ <circle cx="12" cy="12" r="3" />
616
+ <circle cx="5" cy="5" r="2" />
617
+ <circle cx="19" cy="5" r="2" />
618
+ <circle cx="5" cy="19" r="2" />
619
+ <circle cx="19" cy="19" r="2" />
620
+ <line x1="9.5" y1="9.5" x2="6.5" y2="6.5" />
621
+ <line x1="14.5" y1="9.5" x2="17.5" y2="6.5" />
622
+ <line x1="9.5" y1="14.5" x2="6.5" y2="17.5" />
623
+ <line x1="14.5" y1="14.5" x2="17.5" y2="17.5" />
624
+ </svg>
625
+ );
626
+ }
627
+
628
+ function ChevronIcon({ expanded }: { expanded: boolean }) {
629
+ return (
630
+ <svg
631
+ className={`transition-transform duration-200 text-text-muted flex-shrink-0 ${expanded ? 'rotate-90' : ''}`}
632
+ width="16"
633
+ height="16"
634
+ viewBox="0 0 24 24"
635
+ fill="none"
636
+ stroke="currentColor"
637
+ strokeWidth="2"
638
+ >
639
+ <polyline points="9 18 15 12 9 6" />
640
+ </svg>
641
+ );
642
+ }
643
+
644
+ function FolderIcon({ color }: { color: string }) {
645
+ return (
646
+ <svg
647
+ className="flex-shrink-0"
648
+ style={{ color }}
649
+ width="16"
650
+ height="16"
651
+ viewBox="0 0 24 24"
652
+ fill="none"
653
+ stroke="currentColor"
654
+ strokeWidth="2"
655
+ >
656
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
657
+ </svg>
658
+ );
659
+ }
660
+
661
+ function TeamIcon() {
662
+ return (
663
+ <svg
664
+ className="text-text-muted flex-shrink-0"
665
+ width="14"
666
+ height="14"
667
+ viewBox="0 0 24 24"
668
+ fill="none"
669
+ stroke="currentColor"
670
+ strokeWidth="2"
671
+ >
672
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
673
+ <circle cx="9" cy="7" r="4" />
674
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
675
+ <path d="M16 3.13a4 4 0 0 1 0 7.75" />
676
+ </svg>
677
+ );
678
+ }
679
+
680
+ function EmptyIcon() {
681
+ return (
682
+ <svg
683
+ className="mb-3 opacity-50"
684
+ width="48"
685
+ height="48"
686
+ viewBox="0 0 24 24"
687
+ fill="none"
688
+ stroke="currentColor"
689
+ strokeWidth="1"
690
+ >
691
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
692
+ </svg>
693
+ );
694
+ }
695
+
696
+ function SearchIcon() {
697
+ return (
698
+ <svg
699
+ className="mb-3 opacity-50"
700
+ width="48"
701
+ height="48"
702
+ viewBox="0 0 24 24"
703
+ fill="none"
704
+ stroke="currentColor"
705
+ strokeWidth="1"
706
+ >
707
+ <circle cx="11" cy="11" r="8" />
708
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
709
+ </svg>
710
+ );
711
+ }