agentgui 1.0.67 → 1.0.69

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 (55) hide show
  1. package/.prd +92 -0
  2. package/.prd-browser +607 -0
  3. package/CLAUDE.md +1559 -125
  4. package/browser-test-harness.js +371 -0
  5. package/browser-test.js +409 -0
  6. package/execute-tests.js +164 -0
  7. package/lib/claude-runner.js +41 -12
  8. package/lib/database-service.ts +252 -0
  9. package/lib/sync-service.ts +275 -0
  10. package/lib/types.ts +168 -0
  11. package/package.json +1 -1
  12. package/readme.md +586 -0
  13. package/run-e2e-test.sh +88 -0
  14. package/server.js +274 -8
  15. package/static/index.html +487 -180
  16. package/static/js/client.js +558 -0
  17. package/static/js/event-filter.js +311 -0
  18. package/static/js/event-processor.js +454 -0
  19. package/static/js/streaming-renderer.js +813 -0
  20. package/static/js/syntax-highlighter.js +271 -0
  21. package/static/js/ui-components.js +433 -0
  22. package/static/js/websocket-manager.js +482 -0
  23. package/static/templates/INDEX.html +465 -0
  24. package/static/templates/README.md +190 -0
  25. package/static/templates/agent-capabilities.html +56 -0
  26. package/static/templates/agent-metadata-panel.html +44 -0
  27. package/static/templates/agent-status-badge.html +30 -0
  28. package/static/templates/code-annotation-panel.html +155 -0
  29. package/static/templates/code-suggestion-panel.html +184 -0
  30. package/static/templates/command-header.html +77 -0
  31. package/static/templates/command-output-scrollable.html +118 -0
  32. package/static/templates/elapsed-time.html +54 -0
  33. package/static/templates/error-alert.html +106 -0
  34. package/static/templates/error-history-timeline.html +160 -0
  35. package/static/templates/error-recovery-options.html +109 -0
  36. package/static/templates/error-stack-trace.html +95 -0
  37. package/static/templates/error-summary.html +80 -0
  38. package/static/templates/event-counter.html +48 -0
  39. package/static/templates/execution-actions.html +97 -0
  40. package/static/templates/execution-progress-bar.html +80 -0
  41. package/static/templates/execution-stepper.html +120 -0
  42. package/static/templates/file-breadcrumb.html +118 -0
  43. package/static/templates/file-diff-viewer.html +121 -0
  44. package/static/templates/file-metadata.html +133 -0
  45. package/static/templates/file-read-panel.html +66 -0
  46. package/static/templates/file-write-panel.html +120 -0
  47. package/static/templates/git-branch-remote.html +107 -0
  48. package/static/templates/git-diff-list.html +101 -0
  49. package/static/templates/git-log-visualization.html +153 -0
  50. package/static/templates/git-status-panel.html +115 -0
  51. package/static/templates/quality-metrics-display.html +170 -0
  52. package/static/templates/terminal-output-panel.html +87 -0
  53. package/static/templates/test-results-display.html +144 -0
  54. package/test-browser.js +457 -0
  55. package/test-runner.js +182 -0
@@ -0,0 +1,482 @@
1
+ /**
2
+ * WebSocket Manager
3
+ * Handles WebSocket connection, auto-reconnect, message buffering,
4
+ * and event distribution for streaming events
5
+ */
6
+
7
+ class WebSocketManager {
8
+ constructor(config = {}) {
9
+ // Configuration
10
+ this.config = {
11
+ url: config.url || this.getWebSocketURL(),
12
+ reconnectDelays: config.reconnectDelays || [1000, 2000, 4000, 8000, 16000],
13
+ maxReconnectDelay: config.maxReconnectDelay || 30000,
14
+ heartbeatInterval: config.heartbeatInterval || 30000,
15
+ messageTimeout: config.messageTimeout || 60000,
16
+ maxBufferedMessages: config.maxBufferedMessages || 1000,
17
+ ...config
18
+ };
19
+
20
+ // State
21
+ this.ws = null;
22
+ this.isConnected = false;
23
+ this.isConnecting = false;
24
+ this.reconnectCount = 0;
25
+ this.messageBuffer = [];
26
+ this.requestMap = new Map();
27
+ this.heartbeatTimer = null;
28
+ this.connectionState = 'disconnected';
29
+
30
+ // Statistics
31
+ this.stats = {
32
+ totalConnections: 0,
33
+ totalReconnects: 0,
34
+ totalMessagesSent: 0,
35
+ totalMessagesReceived: 0,
36
+ totalErrors: 0,
37
+ totalTimeouts: 0,
38
+ avgLatency: 0,
39
+ lastConnectedTime: null,
40
+ connectionDuration: 0
41
+ };
42
+
43
+ // Event listeners
44
+ this.listeners = {};
45
+ }
46
+
47
+ /**
48
+ * Get WebSocket URL from current window location
49
+ */
50
+ getWebSocketURL() {
51
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
52
+ const baseURL = window.__BASE_URL || '/gm';
53
+ return `${protocol}//${window.location.host}${baseURL}/ws`;
54
+ }
55
+
56
+ /**
57
+ * Connect to WebSocket server
58
+ */
59
+ async connect() {
60
+ if (this.isConnected || this.isConnecting) {
61
+ return this.ws;
62
+ }
63
+
64
+ this.isConnecting = true;
65
+ this.setConnectionState('connecting');
66
+
67
+ try {
68
+ console.log('WebSocket connecting to:', this.config.url);
69
+
70
+ this.ws = new WebSocket(this.config.url);
71
+
72
+ this.ws.onopen = () => this.onOpen();
73
+ this.ws.onmessage = (event) => this.onMessage(event);
74
+ this.ws.onerror = (error) => this.onError(error);
75
+ this.ws.onclose = () => this.onClose();
76
+
77
+ // Wait for connection with timeout
78
+ return await this.waitForConnection(this.config.messageTimeout);
79
+ } catch (error) {
80
+ console.error('WebSocket connection error:', error);
81
+ this.isConnecting = false;
82
+ this.stats.totalErrors++;
83
+ await this.scheduleReconnect();
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Wait for connection to establish
90
+ */
91
+ waitForConnection(timeout = 5000) {
92
+ return new Promise((resolve, reject) => {
93
+ const timer = setTimeout(() => {
94
+ reject(new Error('WebSocket connection timeout'));
95
+ }, timeout);
96
+
97
+ const checkConnection = () => {
98
+ if (this.isConnected) {
99
+ clearTimeout(timer);
100
+ resolve(this.ws);
101
+ } else if (this.ws?.readyState === WebSocket.OPEN) {
102
+ clearTimeout(timer);
103
+ resolve(this.ws);
104
+ } else {
105
+ setTimeout(checkConnection, 50);
106
+ }
107
+ };
108
+
109
+ checkConnection();
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Handle WebSocket open
115
+ */
116
+ onOpen() {
117
+ console.log('WebSocket connected');
118
+ this.isConnected = true;
119
+ this.isConnecting = false;
120
+ this.reconnectCount = 0;
121
+ this.stats.totalConnections++;
122
+ this.stats.lastConnectedTime = Date.now();
123
+ this.setConnectionState('connected');
124
+
125
+ // Flush buffered messages
126
+ this.flushMessageBuffer();
127
+
128
+ // Start heartbeat
129
+ this.startHeartbeat();
130
+
131
+ // Emit connected event
132
+ this.emit('connected', { timestamp: Date.now() });
133
+ }
134
+
135
+ /**
136
+ * Handle WebSocket message
137
+ */
138
+ onMessage(event) {
139
+ try {
140
+ const data = JSON.parse(event.data);
141
+ this.stats.totalMessagesReceived++;
142
+
143
+ // Handle pong response
144
+ if (data.type === 'pong') {
145
+ const requestId = data.requestId;
146
+ if (requestId && this.requestMap.has(requestId)) {
147
+ const request = this.requestMap.get(requestId);
148
+ request.resolve({ latency: Date.now() - request.sentTime });
149
+ this.requestMap.delete(requestId);
150
+ }
151
+ return;
152
+ }
153
+
154
+ // Route message to listeners
155
+ this.emit('message', data);
156
+
157
+ // Route by type
158
+ if (data.type) {
159
+ this.emit(`message:${data.type}`, data);
160
+ }
161
+ } catch (error) {
162
+ console.error('WebSocket message parse error:', error);
163
+ this.stats.totalErrors++;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Handle WebSocket error
169
+ */
170
+ onError(error) {
171
+ console.error('WebSocket error:', error);
172
+ this.stats.totalErrors++;
173
+ this.emit('error', { error, timestamp: Date.now() });
174
+ }
175
+
176
+ /**
177
+ * Handle WebSocket close
178
+ */
179
+ onClose() {
180
+ console.log('WebSocket disconnected');
181
+ this.isConnected = false;
182
+ this.isConnecting = false;
183
+ this.setConnectionState('disconnected');
184
+
185
+ // Stop heartbeat
186
+ if (this.heartbeatTimer) {
187
+ clearTimeout(this.heartbeatTimer);
188
+ }
189
+
190
+ // Update connection duration
191
+ if (this.stats.lastConnectedTime) {
192
+ this.stats.connectionDuration = Date.now() - this.stats.lastConnectedTime;
193
+ }
194
+
195
+ this.emit('disconnected', { timestamp: Date.now() });
196
+
197
+ // Attempt reconnect
198
+ if (!this.isManuallyDisconnected) {
199
+ this.scheduleReconnect();
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Schedule reconnection with exponential backoff
205
+ */
206
+ async scheduleReconnect() {
207
+ if (this.reconnectCount >= this.config.reconnectDelays.length) {
208
+ this.setConnectionState('reconnect_failed');
209
+ console.error('Max reconnection attempts reached');
210
+ this.emit('reconnect_failed', { attempts: this.reconnectCount });
211
+ return;
212
+ }
213
+
214
+ const delay = this.config.reconnectDelays[this.reconnectCount];
215
+ this.reconnectCount++;
216
+ this.stats.totalReconnects++;
217
+ this.setConnectionState('reconnecting');
218
+
219
+ console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectCount}/${this.config.reconnectDelays.length})`);
220
+
221
+ this.emit('reconnecting', { delay, attempt: this.reconnectCount });
222
+
223
+ return new Promise((resolve) => {
224
+ setTimeout(() => {
225
+ this.connect().catch((error) => {
226
+ console.error('Reconnection attempt failed:', error);
227
+ });
228
+ resolve();
229
+ }, delay);
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Start heartbeat/keepalive
235
+ */
236
+ startHeartbeat() {
237
+ if (this.heartbeatTimer) {
238
+ clearTimeout(this.heartbeatTimer);
239
+ }
240
+
241
+ this.heartbeatTimer = setInterval(() => {
242
+ if (this.isConnected) {
243
+ this.ping();
244
+ }
245
+ }, this.config.heartbeatInterval);
246
+ }
247
+
248
+ /**
249
+ * Send ping message
250
+ */
251
+ ping() {
252
+ const requestId = `ping-${Date.now()}-${Math.random()}`;
253
+ const request = {
254
+ sentTime: Date.now(),
255
+ resolve: null
256
+ };
257
+
258
+ const promise = new Promise((resolve) => {
259
+ request.resolve = resolve;
260
+ });
261
+
262
+ this.requestMap.set(requestId, request);
263
+
264
+ // Timeout if no response
265
+ setTimeout(() => {
266
+ if (this.requestMap.has(requestId)) {
267
+ this.stats.totalTimeouts++;
268
+ this.requestMap.delete(requestId);
269
+ this.emit('ping_timeout', { requestId });
270
+ }
271
+ }, 5000);
272
+
273
+ this.sendMessage({ type: 'ping', requestId });
274
+ return promise;
275
+ }
276
+
277
+ /**
278
+ * Send message through WebSocket
279
+ */
280
+ sendMessage(data) {
281
+ if (!data || typeof data !== 'object') {
282
+ throw new Error('Invalid message data');
283
+ }
284
+
285
+ if (!this.isConnected) {
286
+ // Buffer message if not connected
287
+ this.bufferMessage(data);
288
+ return false;
289
+ }
290
+
291
+ try {
292
+ this.ws.send(JSON.stringify(data));
293
+ this.stats.totalMessagesSent++;
294
+ return true;
295
+ } catch (error) {
296
+ console.error('WebSocket send error:', error);
297
+ this.stats.totalErrors++;
298
+ this.bufferMessage(data);
299
+ return false;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Buffer message for sending when connected
305
+ */
306
+ bufferMessage(data) {
307
+ if (this.messageBuffer.length >= this.config.maxBufferedMessages) {
308
+ console.warn('Message buffer full, dropping oldest message');
309
+ this.messageBuffer.shift();
310
+ }
311
+ this.messageBuffer.push(data);
312
+ this.emit('message_buffered', { bufferLength: this.messageBuffer.length });
313
+ }
314
+
315
+ /**
316
+ * Flush buffered messages
317
+ */
318
+ flushMessageBuffer() {
319
+ if (this.messageBuffer.length === 0) return;
320
+
321
+ console.log(`Flushing ${this.messageBuffer.length} buffered messages`);
322
+ const messages = [...this.messageBuffer];
323
+ this.messageBuffer = [];
324
+
325
+ for (const message of messages) {
326
+ try {
327
+ this.ws.send(JSON.stringify(message));
328
+ this.stats.totalMessagesSent++;
329
+ } catch (error) {
330
+ console.error('Error sending buffered message:', error);
331
+ this.bufferMessage(message);
332
+ }
333
+ }
334
+
335
+ this.emit('buffer_flushed', { count: messages.length });
336
+ }
337
+
338
+ /**
339
+ * Subscribe to streaming session
340
+ */
341
+ subscribeToSession(sessionId) {
342
+ return this.sendMessage({
343
+ type: 'subscribe',
344
+ sessionId,
345
+ timestamp: Date.now()
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Unsubscribe from streaming session
351
+ */
352
+ unsubscribeFromSession(sessionId) {
353
+ return this.sendMessage({
354
+ type: 'unsubscribe',
355
+ sessionId,
356
+ timestamp: Date.now()
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Request session history
362
+ */
363
+ requestSessionHistory(sessionId, limit = 1000, offset = 0) {
364
+ return new Promise((resolve, reject) => {
365
+ const requestId = `history-${Date.now()}-${Math.random()}`;
366
+
367
+ const timeout = setTimeout(() => {
368
+ this.requestMap.delete(requestId);
369
+ this.stats.totalTimeouts++;
370
+ reject(new Error('History request timeout'));
371
+ }, this.config.messageTimeout);
372
+
373
+ this.requestMap.set(requestId, {
374
+ type: 'history',
375
+ resolve: (data) => {
376
+ clearTimeout(timeout);
377
+ resolve(data);
378
+ },
379
+ reject
380
+ });
381
+
382
+ this.sendMessage({
383
+ type: 'request_history',
384
+ requestId,
385
+ sessionId,
386
+ limit,
387
+ offset,
388
+ timestamp: Date.now()
389
+ });
390
+ });
391
+ }
392
+
393
+ /**
394
+ * Set connection state
395
+ */
396
+ setConnectionState(state) {
397
+ this.connectionState = state;
398
+ this.emit('state_change', { state, timestamp: Date.now() });
399
+ }
400
+
401
+ /**
402
+ * Disconnect manually
403
+ */
404
+ disconnect() {
405
+ this.isManuallyDisconnected = true;
406
+ this.reconnectCount = 0;
407
+
408
+ if (this.heartbeatTimer) {
409
+ clearTimeout(this.heartbeatTimer);
410
+ }
411
+
412
+ if (this.ws) {
413
+ this.ws.close();
414
+ }
415
+
416
+ this.messageBuffer = [];
417
+ this.requestMap.clear();
418
+ this.setConnectionState('disconnected');
419
+ }
420
+
421
+ /**
422
+ * Get connection status
423
+ */
424
+ getStatus() {
425
+ return {
426
+ isConnected: this.isConnected,
427
+ isConnecting: this.isConnecting,
428
+ connectionState: this.connectionState,
429
+ reconnectCount: this.reconnectCount,
430
+ bufferLength: this.messageBuffer.length,
431
+ stats: { ...this.stats }
432
+ };
433
+ }
434
+
435
+ /**
436
+ * Add event listener
437
+ */
438
+ on(event, callback) {
439
+ if (!this.listeners[event]) {
440
+ this.listeners[event] = [];
441
+ }
442
+ this.listeners[event].push(callback);
443
+ }
444
+
445
+ /**
446
+ * Remove event listener
447
+ */
448
+ off(event, callback) {
449
+ if (!this.listeners[event]) return;
450
+ const index = this.listeners[event].indexOf(callback);
451
+ if (index > -1) {
452
+ this.listeners[event].splice(index, 1);
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Emit event
458
+ */
459
+ emit(event, data) {
460
+ if (!this.listeners[event]) return;
461
+ this.listeners[event].forEach((callback) => {
462
+ try {
463
+ callback(data);
464
+ } catch (error) {
465
+ console.error(`Listener error for event ${event}:`, error);
466
+ }
467
+ });
468
+ }
469
+
470
+ /**
471
+ * Cleanup resources
472
+ */
473
+ destroy() {
474
+ this.disconnect();
475
+ this.listeners = {};
476
+ }
477
+ }
478
+
479
+ // Export for use in browser
480
+ if (typeof module !== 'undefined' && module.exports) {
481
+ module.exports = WebSocketManager;
482
+ }