@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,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAgentLogs Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for streaming live PTY output from agents via WebSocket.
|
|
5
|
+
* Connects to the agent log streaming endpoint and provides real-time updates.
|
|
6
|
+
*
|
|
7
|
+
* Supports log replay on reconnect: tracks the last received timestamp and
|
|
8
|
+
* requests missed log entries from the server after reconnecting.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
12
|
+
import { useWorkspaceWsUrl } from '../WorkspaceContext';
|
|
13
|
+
|
|
14
|
+
export interface LogLine {
|
|
15
|
+
id: string;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
content: string;
|
|
18
|
+
type: 'stdout' | 'stderr' | 'system' | 'input';
|
|
19
|
+
agentName?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseAgentLogsOptions {
|
|
23
|
+
agentName: string;
|
|
24
|
+
/** Maximum number of lines to keep in buffer */
|
|
25
|
+
maxLines?: number;
|
|
26
|
+
/** Auto-connect on mount */
|
|
27
|
+
autoConnect?: boolean;
|
|
28
|
+
/** Enable reconnection on disconnect */
|
|
29
|
+
reconnect?: boolean;
|
|
30
|
+
/** Maximum reconnection attempts */
|
|
31
|
+
maxReconnectAttempts?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Connection quality state for UI indicators */
|
|
35
|
+
export type LogConnectionState = 'connected' | 'reconnecting' | 'disconnected';
|
|
36
|
+
|
|
37
|
+
export interface UseAgentLogsReturn {
|
|
38
|
+
logs: LogLine[];
|
|
39
|
+
isConnected: boolean;
|
|
40
|
+
isConnecting: boolean;
|
|
41
|
+
/** Granular connection quality: 'connected', 'reconnecting', or 'disconnected' */
|
|
42
|
+
connectionState: LogConnectionState;
|
|
43
|
+
error: Error | null;
|
|
44
|
+
connect: () => void;
|
|
45
|
+
disconnect: () => void;
|
|
46
|
+
clear: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate a unique ID for log lines
|
|
51
|
+
*/
|
|
52
|
+
let logIdCounter = 0;
|
|
53
|
+
function generateLogId(): string {
|
|
54
|
+
return `log-${Date.now()}-${++logIdCounter}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useAgentLogs(options: UseAgentLogsOptions): UseAgentLogsReturn {
|
|
58
|
+
const {
|
|
59
|
+
agentName,
|
|
60
|
+
maxLines = 5000,
|
|
61
|
+
autoConnect = true,
|
|
62
|
+
reconnect = true,
|
|
63
|
+
maxReconnectAttempts = Infinity,
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
const logStreamUrl = useWorkspaceWsUrl(`/ws/logs/${encodeURIComponent(agentName)}`);
|
|
67
|
+
|
|
68
|
+
const [logs, setLogs] = useState<LogLine[]>([]);
|
|
69
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
70
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
71
|
+
const [connectionState, setConnectionState] = useState<LogConnectionState>('disconnected');
|
|
72
|
+
const [error, setError] = useState<Error | null>(null);
|
|
73
|
+
|
|
74
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
75
|
+
const reconnectAttemptsRef = useRef(0);
|
|
76
|
+
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
77
|
+
const agentNameRef = useRef(agentName);
|
|
78
|
+
const shouldReconnectRef = useRef(true);
|
|
79
|
+
const isConnectingRef = useRef(false);
|
|
80
|
+
// Track manual close state per-WebSocket instance to avoid race conditions
|
|
81
|
+
// when React remounts quickly (e.g., StrictMode). Using WeakMap ensures
|
|
82
|
+
// each WebSocket tracks its own "was this a manual close" state.
|
|
83
|
+
const manualCloseMapRef = useRef(new WeakMap<WebSocket, boolean>());
|
|
84
|
+
// Track if we've successfully received data per-WebSocket instance
|
|
85
|
+
const hasReceivedDataMapRef = useRef(new WeakMap<WebSocket, boolean>());
|
|
86
|
+
|
|
87
|
+
// Replay support: track last received timestamp and known content for dedup
|
|
88
|
+
const lastTimestampRef = useRef<number | null>(null);
|
|
89
|
+
const hasConnectedBeforeRef = useRef(false);
|
|
90
|
+
// Track recent log content hashes for deduplication during replay
|
|
91
|
+
const recentLogHashesRef = useRef(new Set<string>());
|
|
92
|
+
|
|
93
|
+
// Keep agent name ref updated
|
|
94
|
+
agentNameRef.current = agentName;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate a simple hash for a log line to detect duplicates.
|
|
98
|
+
*/
|
|
99
|
+
const logHash = useCallback((content: string, timestamp: number): string => {
|
|
100
|
+
return `${timestamp}:${content.slice(0, 100)}`;
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const connect = useCallback(() => {
|
|
104
|
+
// Ensure reconnects are allowed for this session
|
|
105
|
+
shouldReconnectRef.current = true;
|
|
106
|
+
|
|
107
|
+
// Prevent multiple connections - use ref to avoid dependency on state
|
|
108
|
+
if (wsRef.current?.readyState === WebSocket.OPEN ||
|
|
109
|
+
wsRef.current?.readyState === WebSocket.CONNECTING ||
|
|
110
|
+
isConnectingRef.current) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Track reconnection state
|
|
115
|
+
if (hasConnectedBeforeRef.current) {
|
|
116
|
+
setConnectionState('reconnecting');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isConnectingRef.current = true;
|
|
120
|
+
setIsConnecting(true);
|
|
121
|
+
setError(null);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const ws = new WebSocket(logStreamUrl);
|
|
125
|
+
wsRef.current = ws;
|
|
126
|
+
// Initialize per-WebSocket state
|
|
127
|
+
manualCloseMapRef.current.set(ws, false);
|
|
128
|
+
hasReceivedDataMapRef.current.set(ws, false);
|
|
129
|
+
|
|
130
|
+
ws.onopen = () => {
|
|
131
|
+
isConnectingRef.current = false;
|
|
132
|
+
setIsConnected(true);
|
|
133
|
+
setIsConnecting(false);
|
|
134
|
+
setConnectionState('connected');
|
|
135
|
+
setError(null);
|
|
136
|
+
reconnectAttemptsRef.current = 0;
|
|
137
|
+
|
|
138
|
+
// On reconnect, request replay of missed log entries
|
|
139
|
+
if (hasConnectedBeforeRef.current && lastTimestampRef.current !== null) {
|
|
140
|
+
console.log(`[WS:Logs] Requesting replay from timestamp ${lastTimestampRef.current}`);
|
|
141
|
+
ws.send(JSON.stringify({
|
|
142
|
+
type: 'replay',
|
|
143
|
+
agent: agentNameRef.current,
|
|
144
|
+
lastTimestamp: lastTimestampRef.current,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
hasConnectedBeforeRef.current = true;
|
|
149
|
+
|
|
150
|
+
// Add system message for connection
|
|
151
|
+
setLogs((prev) => [
|
|
152
|
+
...prev,
|
|
153
|
+
{
|
|
154
|
+
id: generateLogId(),
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
content: `Connected to ${agentNameRef.current} log stream`,
|
|
157
|
+
type: 'system',
|
|
158
|
+
agentName: agentNameRef.current,
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
ws.onclose = (event) => {
|
|
164
|
+
// Read per-WebSocket state (isolated from other connections)
|
|
165
|
+
const wasManualClose = manualCloseMapRef.current.get(ws) ?? false;
|
|
166
|
+
const hadReceivedData = hasReceivedDataMapRef.current.get(ws) ?? false;
|
|
167
|
+
|
|
168
|
+
isConnectingRef.current = false;
|
|
169
|
+
setIsConnected(false);
|
|
170
|
+
setIsConnecting(false);
|
|
171
|
+
wsRef.current = null;
|
|
172
|
+
|
|
173
|
+
// Clear any pending reconnect when a close happens
|
|
174
|
+
if (reconnectTimeoutRef.current) {
|
|
175
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
176
|
+
reconnectTimeoutRef.current = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Skip logging/reconnecting for intentional disconnects (cleanup, user toggle)
|
|
180
|
+
if (wasManualClose) {
|
|
181
|
+
setConnectionState('disconnected');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Don't reconnect if agent was not found (custom close code 4404)
|
|
186
|
+
// This prevents infinite reconnect loops for non-existent agents
|
|
187
|
+
if (event.code === 4404) {
|
|
188
|
+
setConnectionState('disconnected');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add system message for disconnection, but only if:
|
|
193
|
+
// 1. The close was not clean (code 1006 or similar)
|
|
194
|
+
// 2. We had actually received data (to avoid false positives from transient connection issues)
|
|
195
|
+
// Code 1006 is very common and happens during normal operations (React remounts,
|
|
196
|
+
// network hiccups, etc.) - only show error if we had an established data stream
|
|
197
|
+
if (!event.wasClean && hadReceivedData) {
|
|
198
|
+
const willReconnect =
|
|
199
|
+
shouldReconnectRef.current &&
|
|
200
|
+
reconnect &&
|
|
201
|
+
reconnectAttemptsRef.current < maxReconnectAttempts;
|
|
202
|
+
|
|
203
|
+
setLogs((prev) => [
|
|
204
|
+
...prev,
|
|
205
|
+
{
|
|
206
|
+
id: generateLogId(),
|
|
207
|
+
timestamp: Date.now(),
|
|
208
|
+
content: willReconnect
|
|
209
|
+
? `Lost connection to log stream (code: ${event.code}). Reconnecting...`
|
|
210
|
+
: `Disconnected from log stream (code: ${event.code})`,
|
|
211
|
+
type: 'system',
|
|
212
|
+
agentName: agentNameRef.current,
|
|
213
|
+
},
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Schedule reconnect if enabled
|
|
218
|
+
if (
|
|
219
|
+
shouldReconnectRef.current &&
|
|
220
|
+
reconnect &&
|
|
221
|
+
reconnectAttemptsRef.current < maxReconnectAttempts
|
|
222
|
+
) {
|
|
223
|
+
setConnectionState('reconnecting');
|
|
224
|
+
const baseDelay = Math.min(
|
|
225
|
+
500 * Math.pow(2, reconnectAttemptsRef.current),
|
|
226
|
+
15000
|
|
227
|
+
);
|
|
228
|
+
// Add jitter to prevent thundering herd
|
|
229
|
+
const delay = Math.round(baseDelay * (0.5 + Math.random() * 0.5));
|
|
230
|
+
reconnectAttemptsRef.current++;
|
|
231
|
+
|
|
232
|
+
console.log(`[WS:Logs] Reconnecting (attempt ${reconnectAttemptsRef.current})...`);
|
|
233
|
+
|
|
234
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
235
|
+
connect();
|
|
236
|
+
}, delay);
|
|
237
|
+
} else {
|
|
238
|
+
setConnectionState('disconnected');
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
ws.onerror = () => {
|
|
243
|
+
isConnectingRef.current = false;
|
|
244
|
+
setError(new Error('WebSocket connection error'));
|
|
245
|
+
setIsConnecting(false);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
ws.onmessage = (event) => {
|
|
249
|
+
try {
|
|
250
|
+
const data = JSON.parse(event.data);
|
|
251
|
+
|
|
252
|
+
// Handle error messages from server
|
|
253
|
+
if (data.type === 'error') {
|
|
254
|
+
setError(new Error(data.error || `Failed to stream logs for ${data.agent || agentNameRef.current}`));
|
|
255
|
+
setLogs((prev) => [
|
|
256
|
+
...prev,
|
|
257
|
+
{
|
|
258
|
+
id: generateLogId(),
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
content: `Error: ${data.error || 'Unknown error'}`,
|
|
261
|
+
type: 'system',
|
|
262
|
+
agentName: data.agent || agentNameRef.current,
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle subscribed confirmation
|
|
269
|
+
if (data.type === 'subscribed') {
|
|
270
|
+
console.log(`[useAgentLogs] Subscribed to ${data.agent}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handle replay response: array of missed log entries
|
|
275
|
+
if (data.type === 'replay' && Array.isArray(data.entries)) {
|
|
276
|
+
hasReceivedDataMapRef.current.set(ws, true);
|
|
277
|
+
setLogs((prev) => {
|
|
278
|
+
const replayLines: LogLine[] = [];
|
|
279
|
+
for (const entry of data.entries) {
|
|
280
|
+
const content = entry.content || '';
|
|
281
|
+
const timestamp = entry.timestamp || Date.now();
|
|
282
|
+
const hash = logHash(content, timestamp);
|
|
283
|
+
|
|
284
|
+
// Skip duplicates
|
|
285
|
+
if (recentLogHashesRef.current.has(hash)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
recentLogHashesRef.current.add(hash);
|
|
289
|
+
|
|
290
|
+
replayLines.push({
|
|
291
|
+
id: generateLogId(),
|
|
292
|
+
timestamp,
|
|
293
|
+
content,
|
|
294
|
+
type: 'stdout' as const,
|
|
295
|
+
agentName: agentNameRef.current,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Update last timestamp
|
|
299
|
+
if (timestamp > (lastTimestampRef.current ?? 0)) {
|
|
300
|
+
lastTimestampRef.current = timestamp;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (replayLines.length === 0) return prev;
|
|
305
|
+
return [...prev, ...replayLines].slice(-maxLines);
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Handle history (initial log dump)
|
|
311
|
+
if (data.type === 'history' && Array.isArray(data.lines)) {
|
|
312
|
+
// Mark as having received data - connection is established
|
|
313
|
+
if (data.lines.length > 0) {
|
|
314
|
+
hasReceivedDataMapRef.current.set(ws, true);
|
|
315
|
+
}
|
|
316
|
+
setLogs((prev) => {
|
|
317
|
+
const historyLines: LogLine[] = data.lines.map((line: string) => {
|
|
318
|
+
const ts = Date.now();
|
|
319
|
+
const hash = logHash(line, ts);
|
|
320
|
+
recentLogHashesRef.current.add(hash);
|
|
321
|
+
lastTimestampRef.current = ts;
|
|
322
|
+
return {
|
|
323
|
+
id: generateLogId(),
|
|
324
|
+
timestamp: ts,
|
|
325
|
+
content: line,
|
|
326
|
+
type: 'stdout' as const,
|
|
327
|
+
agentName: data.agent || agentNameRef.current,
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
return [...prev, ...historyLines].slice(-maxLines);
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Handle different message formats - mark as having received data for all actual log messages
|
|
336
|
+
if (typeof data === 'string') {
|
|
337
|
+
// Simple string message
|
|
338
|
+
hasReceivedDataMapRef.current.set(ws, true);
|
|
339
|
+
const ts = Date.now();
|
|
340
|
+
lastTimestampRef.current = ts;
|
|
341
|
+
const hash = logHash(data, ts);
|
|
342
|
+
recentLogHashesRef.current.add(hash);
|
|
343
|
+
setLogs((prev) => {
|
|
344
|
+
const newLogs = [
|
|
345
|
+
...prev,
|
|
346
|
+
{
|
|
347
|
+
id: generateLogId(),
|
|
348
|
+
timestamp: ts,
|
|
349
|
+
content: data,
|
|
350
|
+
type: 'stdout' as const,
|
|
351
|
+
agentName: agentNameRef.current,
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
return newLogs.slice(-maxLines);
|
|
355
|
+
});
|
|
356
|
+
} else if (data.type === 'log' || data.type === 'output') {
|
|
357
|
+
// Structured log message
|
|
358
|
+
hasReceivedDataMapRef.current.set(ws, true);
|
|
359
|
+
const ts = data.timestamp || Date.now();
|
|
360
|
+
const content = data.content || data.data || data.message || '';
|
|
361
|
+
lastTimestampRef.current = ts;
|
|
362
|
+
const hash = logHash(content, ts);
|
|
363
|
+
recentLogHashesRef.current.add(hash);
|
|
364
|
+
setLogs((prev) => {
|
|
365
|
+
const logType: LogLine['type'] = data.stream === 'stderr' ? 'stderr' : 'stdout';
|
|
366
|
+
const newLogs: LogLine[] = [
|
|
367
|
+
...prev,
|
|
368
|
+
{
|
|
369
|
+
id: generateLogId(),
|
|
370
|
+
timestamp: ts,
|
|
371
|
+
content,
|
|
372
|
+
type: logType,
|
|
373
|
+
agentName: data.agentName || agentNameRef.current,
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
return newLogs.slice(-maxLines);
|
|
377
|
+
});
|
|
378
|
+
} else if (data.lines && Array.isArray(data.lines)) {
|
|
379
|
+
// Batch of lines
|
|
380
|
+
hasReceivedDataMapRef.current.set(ws, true);
|
|
381
|
+
setLogs((prev) => {
|
|
382
|
+
const newLines: LogLine[] = data.lines.map((line: string | { content: string; type?: string }) => {
|
|
383
|
+
const lineType: LogLine['type'] = (typeof line === 'object' && line.type === 'stderr') ? 'stderr' : 'stdout';
|
|
384
|
+
const content = typeof line === 'string' ? line : line.content;
|
|
385
|
+
const ts = Date.now();
|
|
386
|
+
lastTimestampRef.current = ts;
|
|
387
|
+
const hash = logHash(content, ts);
|
|
388
|
+
recentLogHashesRef.current.add(hash);
|
|
389
|
+
return {
|
|
390
|
+
id: generateLogId(),
|
|
391
|
+
timestamp: ts,
|
|
392
|
+
content,
|
|
393
|
+
type: lineType,
|
|
394
|
+
agentName: agentNameRef.current,
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
return [...prev, ...newLines].slice(-maxLines);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Keep the dedup set from growing unbounded
|
|
402
|
+
if (recentLogHashesRef.current.size > 2000) {
|
|
403
|
+
const entries = Array.from(recentLogHashesRef.current);
|
|
404
|
+
recentLogHashesRef.current = new Set(entries.slice(-1000));
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
// Handle plain text messages
|
|
408
|
+
if (typeof event.data === 'string') {
|
|
409
|
+
hasReceivedDataMapRef.current.set(ws, true);
|
|
410
|
+
const ts = Date.now();
|
|
411
|
+
lastTimestampRef.current = ts;
|
|
412
|
+
setLogs((prev) => {
|
|
413
|
+
const newLogs = [
|
|
414
|
+
...prev,
|
|
415
|
+
{
|
|
416
|
+
id: generateLogId(),
|
|
417
|
+
timestamp: ts,
|
|
418
|
+
content: event.data,
|
|
419
|
+
type: 'stdout' as const,
|
|
420
|
+
agentName: agentNameRef.current,
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
return newLogs.slice(-maxLines);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
} catch (e) {
|
|
429
|
+
isConnectingRef.current = false;
|
|
430
|
+
setError(e instanceof Error ? e : new Error('Failed to create WebSocket'));
|
|
431
|
+
setIsConnecting(false);
|
|
432
|
+
}
|
|
433
|
+
}, [logStreamUrl, maxLines, reconnect, maxReconnectAttempts, logHash]);
|
|
434
|
+
|
|
435
|
+
const disconnect = useCallback(() => {
|
|
436
|
+
// Prevent reconnection attempts after an intentional disconnect
|
|
437
|
+
shouldReconnectRef.current = false;
|
|
438
|
+
|
|
439
|
+
// Clear any pending reconnect
|
|
440
|
+
if (reconnectTimeoutRef.current) {
|
|
441
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
442
|
+
reconnectTimeoutRef.current = null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Mark this WebSocket as manually closed before closing it
|
|
446
|
+
// This prevents the false positive error message on close
|
|
447
|
+
if (wsRef.current) {
|
|
448
|
+
manualCloseMapRef.current.set(wsRef.current, true);
|
|
449
|
+
wsRef.current.close();
|
|
450
|
+
wsRef.current = null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
isConnectingRef.current = false;
|
|
454
|
+
setIsConnected(false);
|
|
455
|
+
setIsConnecting(false);
|
|
456
|
+
setConnectionState('disconnected');
|
|
457
|
+
}, []);
|
|
458
|
+
|
|
459
|
+
const clear = useCallback(() => {
|
|
460
|
+
setLogs([]);
|
|
461
|
+
recentLogHashesRef.current.clear();
|
|
462
|
+
}, []);
|
|
463
|
+
|
|
464
|
+
// Auto-connect on mount or agent change
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
if (autoConnect && agentName) {
|
|
467
|
+
connect();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return () => {
|
|
471
|
+
disconnect();
|
|
472
|
+
};
|
|
473
|
+
}, [agentName, autoConnect, connect, disconnect]);
|
|
474
|
+
|
|
475
|
+
// Visibility change listener: reconnect when tab becomes visible
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
const handleVisibilityChange = () => {
|
|
478
|
+
if (document.visibilityState === 'visible') {
|
|
479
|
+
// Check if connection is dead and reconnect
|
|
480
|
+
if (!wsRef.current || wsRef.current.readyState === WebSocket.CLOSED) {
|
|
481
|
+
console.log('[WS:Logs] Tab visible, reconnecting...');
|
|
482
|
+
reconnectAttemptsRef.current = 0; // Reset attempts for visibility-triggered reconnect
|
|
483
|
+
connect();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
489
|
+
return () => {
|
|
490
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
491
|
+
};
|
|
492
|
+
}, [connect]);
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
logs,
|
|
496
|
+
isConnected,
|
|
497
|
+
isConnecting,
|
|
498
|
+
connectionState,
|
|
499
|
+
error,
|
|
500
|
+
connect,
|
|
501
|
+
disconnect,
|
|
502
|
+
clear,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAgents Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for managing agent state with hierarchical grouping,
|
|
5
|
+
* filtering, and selection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
9
|
+
import type { Agent } from '../../types';
|
|
10
|
+
import {
|
|
11
|
+
groupAgents,
|
|
12
|
+
filterAgents,
|
|
13
|
+
sortAgentsByHierarchy,
|
|
14
|
+
getGroupStats,
|
|
15
|
+
type AgentGroup,
|
|
16
|
+
} from '../../lib/hierarchy';
|
|
17
|
+
import { getAgentColor, type ColorScheme } from '../../lib/colors';
|
|
18
|
+
|
|
19
|
+
export interface UseAgentsOptions {
|
|
20
|
+
agents: Agent[];
|
|
21
|
+
initialSelected?: string;
|
|
22
|
+
initialSearchQuery?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AgentWithColor extends Agent {
|
|
26
|
+
color: ColorScheme;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseAgentsReturn {
|
|
30
|
+
// Filtered and grouped agents
|
|
31
|
+
agents: Agent[];
|
|
32
|
+
groups: AgentGroup[];
|
|
33
|
+
sortedAgents: Agent[];
|
|
34
|
+
|
|
35
|
+
// Selection
|
|
36
|
+
selectedAgent: Agent | null;
|
|
37
|
+
selectAgent: (name: string | null) => void;
|
|
38
|
+
|
|
39
|
+
// Search/filter
|
|
40
|
+
searchQuery: string;
|
|
41
|
+
setSearchQuery: (query: string) => void;
|
|
42
|
+
|
|
43
|
+
// Stats
|
|
44
|
+
totalCount: number;
|
|
45
|
+
onlineCount: number;
|
|
46
|
+
needsAttentionCount: number;
|
|
47
|
+
|
|
48
|
+
// Utilities
|
|
49
|
+
getAgentByName: (name: string) => Agent | undefined;
|
|
50
|
+
getAgentWithColor: (agent: Agent) => AgentWithColor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useAgents({
|
|
54
|
+
agents,
|
|
55
|
+
initialSelected,
|
|
56
|
+
initialSearchQuery = '',
|
|
57
|
+
}: UseAgentsOptions): UseAgentsReturn {
|
|
58
|
+
const [selectedName, setSelectedName] = useState<string | null>(initialSelected ?? null);
|
|
59
|
+
const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
|
|
60
|
+
|
|
61
|
+
// Filter agents by search query
|
|
62
|
+
const filteredAgents = useMemo(
|
|
63
|
+
() => filterAgents(agents, searchQuery),
|
|
64
|
+
[agents, searchQuery]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Group agents by prefix
|
|
68
|
+
const groups = useMemo(
|
|
69
|
+
() => groupAgents(filteredAgents),
|
|
70
|
+
[filteredAgents]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Sort agents for flat list display
|
|
74
|
+
const sortedAgents = useMemo(
|
|
75
|
+
() => sortAgentsByHierarchy(filteredAgents),
|
|
76
|
+
[filteredAgents]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Get selected agent object
|
|
80
|
+
const selectedAgent = useMemo(
|
|
81
|
+
() => agents.find((a) => a.name === selectedName) ?? null,
|
|
82
|
+
[agents, selectedName]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Calculate stats
|
|
86
|
+
const stats = useMemo(() => {
|
|
87
|
+
const allStats = getGroupStats(agents);
|
|
88
|
+
return {
|
|
89
|
+
totalCount: allStats.total,
|
|
90
|
+
onlineCount: allStats.online,
|
|
91
|
+
needsAttentionCount: allStats.needsAttention,
|
|
92
|
+
};
|
|
93
|
+
}, [agents]);
|
|
94
|
+
|
|
95
|
+
// Selection handler
|
|
96
|
+
const selectAgent = useCallback((name: string | null) => {
|
|
97
|
+
setSelectedName(name);
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
// Get agent by name
|
|
101
|
+
const getAgentByName = useCallback(
|
|
102
|
+
(name: string) => agents.find((a) => a.name === name),
|
|
103
|
+
[agents]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Get agent with color scheme attached
|
|
107
|
+
const getAgentWithColor = useCallback(
|
|
108
|
+
(agent: Agent): AgentWithColor => ({
|
|
109
|
+
...agent,
|
|
110
|
+
color: getAgentColor(agent.name),
|
|
111
|
+
}),
|
|
112
|
+
[]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
agents: filteredAgents,
|
|
117
|
+
groups,
|
|
118
|
+
sortedAgents,
|
|
119
|
+
selectedAgent,
|
|
120
|
+
selectAgent,
|
|
121
|
+
searchQuery,
|
|
122
|
+
setSearchQuery,
|
|
123
|
+
...stats,
|
|
124
|
+
getAgentByName,
|
|
125
|
+
getAgentWithColor,
|
|
126
|
+
};
|
|
127
|
+
}
|