@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,424 @@
1
+ /**
2
+ * useOrchestrator Hook
3
+ *
4
+ * Connects to the daemon orchestrator for workspace and agent management.
5
+ * Provides real-time updates via WebSocket.
6
+ */
7
+
8
+ import { useState, useEffect, useCallback, useRef } from 'react';
9
+ import type { Workspace } from '../WorkspaceSelector';
10
+
11
+ export interface OrchestratorAgent {
12
+ id: string;
13
+ name: string;
14
+ workspaceId: string;
15
+ provider: string;
16
+ status: 'running' | 'idle' | 'crashed' | 'restarting' | 'stopped';
17
+ pid?: number;
18
+ task?: string;
19
+ cwd?: string;
20
+ spawnedAt: Date;
21
+ restartCount: number;
22
+ }
23
+
24
+ export interface OrchestratorEvent {
25
+ type: string;
26
+ workspaceId?: string;
27
+ agentId?: string;
28
+ data: unknown;
29
+ timestamp: Date;
30
+ }
31
+
32
+ export interface UseOrchestratorOptions {
33
+ /** Orchestrator API URL (default: http://localhost:3456) */
34
+ apiUrl?: string;
35
+ /** Enable orchestrator connection (default: false - orchestrator is optional) */
36
+ enabled?: boolean;
37
+ }
38
+
39
+ export interface UseOrchestratorResult {
40
+ /** All workspaces */
41
+ workspaces: Workspace[];
42
+ /** Currently active workspace ID */
43
+ activeWorkspaceId?: string;
44
+ /** Active workspace agents */
45
+ agents: OrchestratorAgent[];
46
+ /** Connection status */
47
+ isConnected: boolean;
48
+ /** Loading state */
49
+ isLoading: boolean;
50
+ /** Error state */
51
+ error: Error | null;
52
+ /** Switch to a workspace */
53
+ switchWorkspace: (workspaceId: string) => Promise<void>;
54
+ /** Add a new workspace */
55
+ addWorkspace: (path: string, name?: string) => Promise<Workspace>;
56
+ /** Remove a workspace */
57
+ removeWorkspace: (workspaceId: string) => Promise<void>;
58
+ /** Spawn an agent */
59
+ spawnAgent: (name: string, task?: string, provider?: string, cwd?: string) => Promise<OrchestratorAgent>;
60
+ /** Stop an agent */
61
+ stopAgent: (agentName: string) => Promise<void>;
62
+ /** Refresh data */
63
+ refresh: () => Promise<void>;
64
+ }
65
+
66
+ export function useOrchestrator(options: UseOrchestratorOptions = {}): UseOrchestratorResult {
67
+ const { apiUrl = 'http://localhost:3456', enabled = false } = options;
68
+
69
+ const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
70
+ const [activeWorkspaceId, setActiveWorkspaceId] = useState<string>();
71
+ const [agents, setAgents] = useState<OrchestratorAgent[]>([]);
72
+ const [isConnected, setIsConnected] = useState(false);
73
+ const [isLoading, setIsLoading] = useState(enabled);
74
+ const [error, setError] = useState<Error | null>(null);
75
+
76
+ const wsRef = useRef<WebSocket | null>(null);
77
+ const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
78
+
79
+ // Convert API URL to WebSocket URL
80
+ const wsUrl = apiUrl.replace(/^http/, 'ws');
81
+
82
+ // Fetch initial data - only if enabled
83
+ const fetchData = useCallback(async () => {
84
+ if (!enabled) {
85
+ return;
86
+ }
87
+
88
+ try {
89
+ setIsLoading(true);
90
+ setError(null);
91
+
92
+ const response = await fetch(`${apiUrl}/workspaces`);
93
+ if (!response.ok) {
94
+ throw new Error(`Failed to fetch workspaces: ${response.statusText}`);
95
+ }
96
+
97
+ const data = await response.json();
98
+ setWorkspaces(
99
+ data.workspaces.map((w: Workspace) => ({
100
+ ...w,
101
+ lastActiveAt: new Date(w.lastActiveAt),
102
+ }))
103
+ );
104
+ setActiveWorkspaceId(data.activeWorkspaceId);
105
+
106
+ // Fetch agents for active workspace
107
+ if (data.activeWorkspaceId) {
108
+ const agentsResponse = await fetch(`${apiUrl}/workspaces/${data.activeWorkspaceId}/agents`);
109
+ if (agentsResponse.ok) {
110
+ const agentsData = await agentsResponse.json();
111
+ const wsId = agentsData.workspaceId || data.activeWorkspaceId;
112
+ setAgents(
113
+ agentsData.agents.map((a: OrchestratorAgent) => ({
114
+ ...a,
115
+ workspaceId: a.workspaceId || wsId,
116
+ spawnedAt: new Date(a.spawnedAt),
117
+ }))
118
+ );
119
+ }
120
+ }
121
+ } catch (err) {
122
+ setError(err instanceof Error ? err : new Error(String(err)));
123
+ } finally {
124
+ setIsLoading(false);
125
+ }
126
+ }, [apiUrl, enabled]);
127
+
128
+ // Track reconnect attempts for the orchestrator
129
+ const reconnectAttemptsRef = useRef(0);
130
+
131
+ // WebSocket connection - only connect if enabled
132
+ useEffect(() => {
133
+ // Skip connection if orchestrator is not enabled
134
+ if (!enabled) {
135
+ return;
136
+ }
137
+
138
+ const connect = () => {
139
+ try {
140
+ const ws = new WebSocket(wsUrl);
141
+
142
+ ws.onopen = () => {
143
+ setIsConnected(true);
144
+ setError(null);
145
+ reconnectAttemptsRef.current = 0;
146
+ };
147
+
148
+ ws.onmessage = (event) => {
149
+ try {
150
+ const message = JSON.parse(event.data);
151
+
152
+ if (message.type === 'init') {
153
+ // Initial state from server
154
+ setWorkspaces(
155
+ message.data.workspaces.map((w: Workspace) => ({
156
+ ...w,
157
+ lastActiveAt: new Date(w.lastActiveAt),
158
+ }))
159
+ );
160
+ setActiveWorkspaceId(message.data.activeWorkspaceId);
161
+ const initWsId = message.data.activeWorkspaceId;
162
+ setAgents(
163
+ message.data.agents?.map((a: OrchestratorAgent) => ({
164
+ ...a,
165
+ workspaceId: a.workspaceId || initWsId,
166
+ spawnedAt: new Date(a.spawnedAt),
167
+ })) || []
168
+ );
169
+ setIsLoading(false);
170
+ } else if (message.type === 'event') {
171
+ handleEvent(message.data as OrchestratorEvent);
172
+ }
173
+ } catch (err) {
174
+ console.error('Failed to parse WebSocket message:', err);
175
+ }
176
+ };
177
+
178
+ ws.onclose = () => {
179
+ setIsConnected(false);
180
+ wsRef.current = null;
181
+
182
+ // Reconnect with backoff and jitter
183
+ const baseDelay = Math.min(
184
+ 500 * Math.pow(2, reconnectAttemptsRef.current),
185
+ 15000
186
+ );
187
+ // Add jitter to prevent thundering herd
188
+ const delay = Math.round(baseDelay * (0.5 + Math.random() * 0.5));
189
+ reconnectAttemptsRef.current++;
190
+
191
+ console.log(`[WS:Orchestrator] Reconnecting (attempt ${reconnectAttemptsRef.current})...`);
192
+
193
+ reconnectTimeoutRef.current = setTimeout(connect, delay);
194
+ };
195
+
196
+ ws.onerror = (err) => {
197
+ console.error('WebSocket error:', err);
198
+ ws.close();
199
+ };
200
+
201
+ wsRef.current = ws;
202
+ } catch (err) {
203
+ setError(err instanceof Error ? err : new Error(String(err)));
204
+ // Retry connection with backoff and jitter
205
+ const baseDelay = Math.min(
206
+ 500 * Math.pow(2, reconnectAttemptsRef.current),
207
+ 15000
208
+ );
209
+ const delay = Math.round(baseDelay * (0.5 + Math.random() * 0.5));
210
+ reconnectAttemptsRef.current++;
211
+ reconnectTimeoutRef.current = setTimeout(connect, delay);
212
+ }
213
+ };
214
+
215
+ // Start with HTTP fetch, then upgrade to WebSocket
216
+ fetchData().then(connect);
217
+
218
+ // Visibility change listener: reconnect when tab becomes visible
219
+ const handleVisibilityChange = () => {
220
+ if (document.visibilityState === 'visible') {
221
+ if (!wsRef.current || wsRef.current.readyState === WebSocket.CLOSED) {
222
+ console.log('[WS:Orchestrator] Tab visible, reconnecting...');
223
+ reconnectAttemptsRef.current = 0;
224
+ connect();
225
+ }
226
+ }
227
+ };
228
+
229
+ document.addEventListener('visibilitychange', handleVisibilityChange);
230
+
231
+ return () => {
232
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
233
+ if (reconnectTimeoutRef.current) {
234
+ clearTimeout(reconnectTimeoutRef.current);
235
+ }
236
+ if (wsRef.current) {
237
+ wsRef.current.close();
238
+ }
239
+ };
240
+ }, [wsUrl, fetchData, enabled]);
241
+
242
+ // Handle real-time events
243
+ const handleEvent = useCallback((event: OrchestratorEvent) => {
244
+ switch (event.type) {
245
+ case 'workspace:added':
246
+ setWorkspaces((prev) => [...prev, event.data as Workspace]);
247
+ break;
248
+
249
+ case 'workspace:removed':
250
+ setWorkspaces((prev) => prev.filter((w) => w.id !== event.workspaceId));
251
+ break;
252
+
253
+ case 'workspace:updated':
254
+ setWorkspaces((prev) =>
255
+ prev.map((w) =>
256
+ w.id === event.workspaceId ? { ...w, ...(event.data as Partial<Workspace>) } : w
257
+ )
258
+ );
259
+ break;
260
+
261
+ case 'workspace:switched':
262
+ setActiveWorkspaceId((event.data as { currentId: string }).currentId);
263
+ break;
264
+
265
+ case 'agent:spawned': {
266
+ const spawnedAgent = event.data as OrchestratorAgent;
267
+ setAgents((prev) => [...prev, {
268
+ ...spawnedAgent,
269
+ workspaceId: spawnedAgent.workspaceId || event.workspaceId || activeWorkspaceId || '',
270
+ }]);
271
+ break;
272
+ }
273
+
274
+ case 'agent:stopped':
275
+ case 'agent:crashed':
276
+ setAgents((prev) => prev.filter((a) => a.name !== (event.data as { name: string }).name));
277
+ break;
278
+
279
+ case 'agent:restarted':
280
+ setAgents((prev) =>
281
+ prev.map((a) =>
282
+ a.name === (event.data as { name: string }).name
283
+ ? { ...a, status: 'running' as const, restartCount: a.restartCount + 1 }
284
+ : a
285
+ )
286
+ );
287
+ break;
288
+ }
289
+ }, [activeWorkspaceId]);
290
+
291
+ // Switch workspace
292
+ const switchWorkspace = useCallback(
293
+ async (workspaceId: string) => {
294
+ const response = await fetch(`${apiUrl}/workspaces/${workspaceId}/switch`, {
295
+ method: 'POST',
296
+ });
297
+
298
+ if (!response.ok) {
299
+ throw new Error(`Failed to switch workspace: ${response.statusText}`);
300
+ }
301
+
302
+ // Fetch agents for new workspace
303
+ const agentsResponse = await fetch(`${apiUrl}/workspaces/${workspaceId}/agents`);
304
+ if (agentsResponse.ok) {
305
+ const agentsData = await agentsResponse.json();
306
+ setAgents(
307
+ agentsData.agents.map((a: OrchestratorAgent) => ({
308
+ ...a,
309
+ spawnedAt: new Date(a.spawnedAt),
310
+ }))
311
+ );
312
+ }
313
+ },
314
+ [apiUrl]
315
+ );
316
+
317
+ // Add workspace
318
+ const addWorkspace = useCallback(
319
+ async (path: string, name?: string): Promise<Workspace> => {
320
+ const response = await fetch(`${apiUrl}/workspaces`, {
321
+ method: 'POST',
322
+ headers: { 'Content-Type': 'application/json' },
323
+ body: JSON.stringify({ path, name }),
324
+ });
325
+
326
+ if (!response.ok) {
327
+ const error = await response.json();
328
+ throw new Error(error.error || 'Failed to add workspace');
329
+ }
330
+
331
+ return response.json();
332
+ },
333
+ [apiUrl]
334
+ );
335
+
336
+ // Remove workspace
337
+ const removeWorkspace = useCallback(
338
+ async (workspaceId: string) => {
339
+ const response = await fetch(`${apiUrl}/workspaces/${workspaceId}`, {
340
+ method: 'DELETE',
341
+ });
342
+
343
+ if (!response.ok) {
344
+ throw new Error(`Failed to remove workspace: ${response.statusText}`);
345
+ }
346
+ },
347
+ [apiUrl]
348
+ );
349
+
350
+ // Spawn agent
351
+ const spawnAgent = useCallback(
352
+ async (name: string, task?: string, provider?: string, cwd?: string): Promise<OrchestratorAgent> => {
353
+ if (!activeWorkspaceId) {
354
+ throw new Error('No active workspace');
355
+ }
356
+
357
+ const response = await fetch(`${apiUrl}/workspaces/${activeWorkspaceId}/agents`, {
358
+ method: 'POST',
359
+ headers: { 'Content-Type': 'application/json' },
360
+ body: JSON.stringify({ name, task, provider, cwd }),
361
+ });
362
+
363
+ if (!response.ok) {
364
+ const error = await response.json();
365
+ throw new Error(error.error || 'Failed to spawn agent');
366
+ }
367
+
368
+ const result = await response.json();
369
+
370
+ // Refetch agents to get cwd data from /api/spawned
371
+ const agentsResponse = await fetch(`${apiUrl}/workspaces/${activeWorkspaceId}/agents`);
372
+ if (agentsResponse.ok) {
373
+ const agentsData = await agentsResponse.json();
374
+ const refetchWsId = agentsData.workspaceId || activeWorkspaceId;
375
+ setAgents(
376
+ agentsData.agents.map((a: OrchestratorAgent) => ({
377
+ ...a,
378
+ workspaceId: a.workspaceId || refetchWsId,
379
+ spawnedAt: new Date(a.spawnedAt),
380
+ }))
381
+ );
382
+ }
383
+
384
+ return result;
385
+ },
386
+ [apiUrl, activeWorkspaceId]
387
+ );
388
+
389
+ // Stop agent
390
+ const stopAgent = useCallback(
391
+ async (agentName: string) => {
392
+ if (!activeWorkspaceId) {
393
+ throw new Error('No active workspace');
394
+ }
395
+
396
+ const response = await fetch(
397
+ `${apiUrl}/workspaces/${activeWorkspaceId}/agents/${agentName}`,
398
+ {
399
+ method: 'DELETE',
400
+ }
401
+ );
402
+
403
+ if (!response.ok) {
404
+ throw new Error(`Failed to stop agent: ${response.statusText}`);
405
+ }
406
+ },
407
+ [apiUrl, activeWorkspaceId]
408
+ );
409
+
410
+ return {
411
+ workspaces,
412
+ activeWorkspaceId,
413
+ agents,
414
+ isConnected,
415
+ isLoading,
416
+ error,
417
+ switchWorkspace,
418
+ addWorkspace,
419
+ removeWorkspace,
420
+ spawnAgent,
421
+ stopAgent,
422
+ refresh: fetchData,
423
+ };
424
+ }