@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.
- package/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,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
|
+
}
|