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,558 @@
1
+ /**
2
+ * AgentGUI Client
3
+ * Main application orchestrator that integrates WebSocket, event processing,
4
+ * and streaming renderer for real-time Claude Code execution visualization
5
+ */
6
+
7
+ class AgentGUIClient {
8
+ constructor(config = {}) {
9
+ this.config = {
10
+ containerId: config.containerId || 'app',
11
+ outputContainerId: config.outputContainerId || 'output',
12
+ scrollContainerId: config.scrollContainerId || 'output-scroll',
13
+ autoConnect: config.autoConnect !== false,
14
+ ...config
15
+ };
16
+
17
+ // Initialize components
18
+ this.renderer = new StreamingRenderer(config.renderer || {});
19
+ this.wsManager = new WebSocketManager(config.websocket || {});
20
+ this.eventProcessor = new EventProcessor(config.eventProcessor || {});
21
+
22
+ // Application state
23
+ this.state = {
24
+ isInitialized: false,
25
+ currentSession: null,
26
+ currentConversation: null,
27
+ isStreaming: false,
28
+ sessionEvents: [],
29
+ conversations: [],
30
+ agents: []
31
+ };
32
+
33
+ // Event handlers
34
+ this.eventHandlers = {};
35
+
36
+ // UI state
37
+ this.ui = {
38
+ statusIndicator: null,
39
+ messageInput: null,
40
+ sendButton: null,
41
+ agentSelector: null
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Initialize the client
47
+ */
48
+ async init() {
49
+ try {
50
+ console.log('Initializing AgentGUI client');
51
+
52
+ // Initialize renderer
53
+ this.renderer.init(this.config.outputContainerId, this.config.scrollContainerId);
54
+
55
+ // Setup event listeners
56
+ this.setupWebSocketListeners();
57
+ this.setupRendererListeners();
58
+
59
+ // Load initial data
60
+ await this.loadAgents();
61
+ await this.loadConversations();
62
+
63
+ // Setup UI elements
64
+ this.setupUI();
65
+
66
+ // Connect WebSocket
67
+ if (this.config.autoConnect) {
68
+ await this.connectWebSocket();
69
+ }
70
+
71
+ this.state.isInitialized = true;
72
+ this.emit('initialized');
73
+
74
+ console.log('AgentGUI client initialized');
75
+ return this;
76
+ } catch (error) {
77
+ console.error('Client initialization error:', error);
78
+ this.showError('Failed to initialize client: ' + error.message);
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Setup WebSocket event listeners
85
+ */
86
+ setupWebSocketListeners() {
87
+ this.wsManager.on('connected', () => {
88
+ console.log('WebSocket connected');
89
+ this.updateConnectionStatus('connected');
90
+ this.emit('ws:connected');
91
+ });
92
+
93
+ this.wsManager.on('disconnected', () => {
94
+ console.log('WebSocket disconnected');
95
+ this.updateConnectionStatus('disconnected');
96
+ this.emit('ws:disconnected');
97
+ });
98
+
99
+ this.wsManager.on('reconnecting', (data) => {
100
+ console.log('WebSocket reconnecting:', data);
101
+ this.updateConnectionStatus('reconnecting');
102
+ });
103
+
104
+ this.wsManager.on('message', (data) => {
105
+ this.handleWebSocketMessage(data);
106
+ });
107
+
108
+ this.wsManager.on('error', (data) => {
109
+ console.error('WebSocket error:', data);
110
+ this.showError('Connection error: ' + (data.error?.message || 'unknown'));
111
+ });
112
+
113
+ this.wsManager.on('reconnect_failed', (data) => {
114
+ console.error('WebSocket reconnection failed:', data);
115
+ this.updateConnectionStatus('error');
116
+ this.showError('Failed to reconnect to server after ' + data.attempts + ' attempts');
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Setup renderer event listeners
122
+ */
123
+ setupRendererListeners() {
124
+ this.renderer.on('batch:complete', (data) => {
125
+ console.log('Batch rendered:', data);
126
+ this.updateMetrics(data.metrics);
127
+ });
128
+
129
+ this.renderer.on('error:render', (data) => {
130
+ console.error('Render error:', data.error);
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Setup UI elements
136
+ */
137
+ setupUI() {
138
+ const container = document.getElementById(this.config.containerId);
139
+ if (!container) {
140
+ throw new Error(`Container not found: ${this.config.containerId}`);
141
+ }
142
+
143
+ // Get references to key UI elements
144
+ this.ui.statusIndicator = document.querySelector('[data-status-indicator]');
145
+ this.ui.messageInput = document.querySelector('[data-message-input]');
146
+ this.ui.sendButton = document.querySelector('[data-send-button]');
147
+ this.ui.agentSelector = document.querySelector('[data-agent-selector]');
148
+
149
+ // Setup event listeners
150
+ if (this.ui.sendButton) {
151
+ this.ui.sendButton.addEventListener('click', () => this.startExecution());
152
+ }
153
+
154
+ if (this.ui.messageInput) {
155
+ this.ui.messageInput.addEventListener('keydown', (e) => {
156
+ if (e.key === 'Enter' && e.ctrlKey) {
157
+ this.startExecution();
158
+ }
159
+ });
160
+ }
161
+
162
+ // Setup theme toggle
163
+ const themeToggle = document.querySelector('[data-theme-toggle]');
164
+ if (themeToggle) {
165
+ themeToggle.addEventListener('click', () => this.toggleTheme());
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Connect to WebSocket
171
+ */
172
+ async connectWebSocket() {
173
+ try {
174
+ await this.wsManager.connect();
175
+ this.updateConnectionStatus('connected');
176
+ } catch (error) {
177
+ console.error('WebSocket connection failed:', error);
178
+ this.updateConnectionStatus('error');
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Handle incoming WebSocket message
185
+ */
186
+ handleWebSocketMessage(data) {
187
+ try {
188
+ // Route by message type
189
+ switch (data.type) {
190
+ case 'streaming_start':
191
+ this.handleStreamingStart(data);
192
+ break;
193
+
194
+ case 'streaming_progress':
195
+ this.queueEvent(data);
196
+ break;
197
+
198
+ case 'streaming_complete':
199
+ this.handleStreamingComplete(data);
200
+ break;
201
+
202
+ case 'file_read':
203
+ case 'file_write':
204
+ case 'command_execute':
205
+ case 'git_status':
206
+ case 'error':
207
+ case 'text_block':
208
+ case 'code_block':
209
+ case 'thinking_block':
210
+ case 'tool_use':
211
+ this.queueEvent(data);
212
+ break;
213
+
214
+ case 'conversation_created':
215
+ this.handleConversationCreated(data);
216
+ break;
217
+
218
+ case 'message_created':
219
+ this.handleMessageCreated(data);
220
+ break;
221
+
222
+ default:
223
+ console.log('Unhandled message type:', data.type);
224
+ }
225
+ } catch (error) {
226
+ console.error('Message handling error:', error);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Queue event for rendering
232
+ */
233
+ queueEvent(data) {
234
+ try {
235
+ // Process event
236
+ const processed = this.eventProcessor.processEvent(data);
237
+ if (!processed) return;
238
+
239
+ // Queue for rendering
240
+ this.renderer.queueEvent(processed);
241
+
242
+ // Track session events
243
+ if (data.sessionId && this.state.currentSession?.id === data.sessionId) {
244
+ this.state.sessionEvents.push(processed);
245
+ }
246
+ } catch (error) {
247
+ console.error('Event queuing error:', error);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Handle streaming start
253
+ */
254
+ handleStreamingStart(data) {
255
+ console.log('Streaming started:', data);
256
+ this.state.isStreaming = true;
257
+ this.state.currentSession = {
258
+ id: data.sessionId,
259
+ conversationId: data.conversationId,
260
+ agentId: data.agentId,
261
+ startTime: Date.now()
262
+ };
263
+ this.state.sessionEvents = [];
264
+ this.renderer.clear();
265
+
266
+ this.renderer.queueEvent({
267
+ type: 'streaming_start',
268
+ sessionId: data.sessionId,
269
+ conversationId: data.conversationId,
270
+ agentId: data.agentId,
271
+ timestamp: data.timestamp || Date.now()
272
+ });
273
+
274
+ this.disableControls();
275
+ this.emit('streaming:start', data);
276
+ }
277
+
278
+ /**
279
+ * Handle streaming complete
280
+ */
281
+ handleStreamingComplete(data) {
282
+ console.log('Streaming completed:', data);
283
+ this.state.isStreaming = false;
284
+
285
+ const duration = data.duration || (Date.now() - (this.state.currentSession?.startTime || Date.now()));
286
+
287
+ this.renderer.queueEvent({
288
+ type: 'streaming_complete',
289
+ sessionId: data.sessionId,
290
+ duration,
291
+ timestamp: data.timestamp || Date.now()
292
+ });
293
+
294
+ this.enableControls();
295
+ this.emit('streaming:complete', {
296
+ ...data,
297
+ duration,
298
+ eventCount: this.state.sessionEvents.length
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Handle conversation created
304
+ */
305
+ handleConversationCreated(data) {
306
+ if (data.conversation) {
307
+ this.state.conversations.push(data.conversation);
308
+ this.emit('conversation:created', data.conversation);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Handle message created
314
+ */
315
+ handleMessageCreated(data) {
316
+ this.emit('message:created', data);
317
+ }
318
+
319
+ /**
320
+ * Start execution
321
+ */
322
+ async startExecution() {
323
+ if (this.state.isStreaming) {
324
+ this.showError('Streaming already in progress');
325
+ return;
326
+ }
327
+
328
+ const prompt = this.ui.messageInput?.value || '';
329
+ const agentId = this.ui.agentSelector?.value || 'claude-code';
330
+
331
+ if (!prompt.trim()) {
332
+ this.showError('Please enter a prompt');
333
+ return;
334
+ }
335
+
336
+ try {
337
+ this.disableControls();
338
+
339
+ const response = await fetch(window.__BASE_URL + '/api/conversations', {
340
+ method: 'POST',
341
+ headers: { 'Content-Type': 'application/json' },
342
+ body: JSON.stringify({
343
+ agentId,
344
+ title: prompt.substring(0, 50)
345
+ })
346
+ });
347
+
348
+ const { conversation } = await response.json();
349
+ this.state.currentConversation = conversation;
350
+
351
+ // Start streaming
352
+ await this.streamToConversation(conversation.id, prompt, agentId);
353
+ } catch (error) {
354
+ console.error('Execution error:', error);
355
+ this.showError('Failed to start execution: ' + error.message);
356
+ this.enableControls();
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Stream execution to conversation
362
+ */
363
+ async streamToConversation(conversationId, prompt, agentId) {
364
+ try {
365
+ const response = await fetch(`${window.__BASE_URL}/api/conversations/${conversationId}/stream`, {
366
+ method: 'POST',
367
+ headers: { 'Content-Type': 'application/json' },
368
+ body: JSON.stringify({
369
+ content: prompt,
370
+ agentId,
371
+ skipPermissions: false
372
+ })
373
+ });
374
+
375
+ if (!response.ok) {
376
+ throw new Error(`HTTP ${response.status}`);
377
+ }
378
+
379
+ const { session, streamId } = await response.json();
380
+
381
+ // Subscribe to session events via WebSocket
382
+ if (this.wsManager.isConnected) {
383
+ this.wsManager.subscribeToSession(session.id);
384
+ }
385
+
386
+ this.emit('execution:started', { session, streamId });
387
+ } catch (error) {
388
+ console.error('Stream execution error:', error);
389
+ this.showError('Failed to stream execution: ' + error.message);
390
+ this.enableControls();
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Load agents
396
+ */
397
+ async loadAgents() {
398
+ try {
399
+ const response = await fetch(window.__BASE_URL + '/api/agents');
400
+ const { agents } = await response.json();
401
+ this.state.agents = agents;
402
+
403
+ // Populate agent selector
404
+ if (this.ui.agentSelector) {
405
+ this.ui.agentSelector.innerHTML = agents
406
+ .map(agent => `<option value="${agent.id}">${agent.name}</option>`)
407
+ .join('');
408
+ }
409
+
410
+ return agents;
411
+ } catch (error) {
412
+ console.error('Failed to load agents:', error);
413
+ return [];
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Load conversations
419
+ */
420
+ async loadConversations() {
421
+ try {
422
+ const response = await fetch(window.__BASE_URL + '/api/conversations');
423
+ const { conversations } = await response.json();
424
+ this.state.conversations = conversations;
425
+ return conversations;
426
+ } catch (error) {
427
+ console.error('Failed to load conversations:', error);
428
+ return [];
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Update connection status UI
434
+ */
435
+ updateConnectionStatus(status) {
436
+ if (this.ui.statusIndicator) {
437
+ this.ui.statusIndicator.dataset.status = status;
438
+ this.ui.statusIndicator.textContent = status.charAt(0).toUpperCase() + status.slice(1);
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Update metrics display
444
+ */
445
+ updateMetrics(metrics) {
446
+ const metricsDisplay = document.querySelector('[data-metrics]');
447
+ if (metricsDisplay && metrics) {
448
+ metricsDisplay.textContent = `Batches: ${metrics.totalBatches} | Events: ${metrics.totalEvents} | Avg render: ${metrics.avgRenderTime.toFixed(2)}ms`;
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Disable UI controls during streaming
454
+ */
455
+ disableControls() {
456
+ if (this.ui.sendButton) this.ui.sendButton.disabled = true;
457
+ if (this.ui.messageInput) this.ui.messageInput.disabled = true;
458
+ if (this.ui.agentSelector) this.ui.agentSelector.disabled = true;
459
+ }
460
+
461
+ /**
462
+ * Enable UI controls
463
+ */
464
+ enableControls() {
465
+ if (this.ui.sendButton) this.ui.sendButton.disabled = false;
466
+ if (this.ui.messageInput) this.ui.messageInput.disabled = false;
467
+ if (this.ui.agentSelector) this.ui.agentSelector.disabled = false;
468
+ }
469
+
470
+ /**
471
+ * Toggle theme
472
+ */
473
+ toggleTheme() {
474
+ const isDark = document.documentElement.classList.toggle('dark');
475
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
476
+ }
477
+
478
+ /**
479
+ * Show error message
480
+ */
481
+ showError(message) {
482
+ console.error(message);
483
+ // Could display in a toast or alert
484
+ alert(message);
485
+ }
486
+
487
+ /**
488
+ * Add event listener
489
+ */
490
+ on(event, callback) {
491
+ if (!this.eventHandlers[event]) {
492
+ this.eventHandlers[event] = [];
493
+ }
494
+ this.eventHandlers[event].push(callback);
495
+ }
496
+
497
+ /**
498
+ * Emit event
499
+ */
500
+ emit(event, data) {
501
+ if (this.eventHandlers[event]) {
502
+ this.eventHandlers[event].forEach(callback => {
503
+ try {
504
+ callback(data);
505
+ } catch (error) {
506
+ console.error(`Event handler error for ${event}:`, error);
507
+ }
508
+ });
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Get application state
514
+ */
515
+ getState() {
516
+ return { ...this.state };
517
+ }
518
+
519
+ /**
520
+ * Get metrics
521
+ */
522
+ getMetrics() {
523
+ return {
524
+ renderer: this.renderer.getMetrics(),
525
+ websocket: this.wsManager.getStatus(),
526
+ eventProcessor: this.eventProcessor.getStats(),
527
+ state: this.state
528
+ };
529
+ }
530
+
531
+ /**
532
+ * Cleanup resources
533
+ */
534
+ destroy() {
535
+ this.renderer.destroy();
536
+ this.wsManager.destroy();
537
+ this.eventHandlers = {};
538
+ }
539
+ }
540
+
541
+ // Global instance
542
+ let agentGUIClient = null;
543
+
544
+ // Initialize on DOM ready
545
+ document.addEventListener('DOMContentLoaded', async () => {
546
+ try {
547
+ agentGUIClient = new AgentGUIClient();
548
+ await agentGUIClient.init();
549
+ console.log('AgentGUI ready');
550
+ } catch (error) {
551
+ console.error('Failed to initialize AgentGUI:', error);
552
+ }
553
+ });
554
+
555
+ // Export for testing
556
+ if (typeof module !== 'undefined' && module.exports) {
557
+ module.exports = AgentGUIClient;
558
+ }