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
package/server.js CHANGED
@@ -141,6 +141,45 @@ const server = http.createServer(async (req, res) => {
141
141
  }
142
142
  }
143
143
 
144
+ const streamMatch = routePath.match(/^\/api\/conversations\/([^/]+)\/stream$/);
145
+ if (streamMatch && req.method === 'POST') {
146
+ const conversationId = streamMatch[1];
147
+ const body = await parseBody(req);
148
+ const conv = queries.getConversation(conversationId);
149
+ if (!conv) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Conversation not found' })); return; }
150
+
151
+ const prompt = body.content || '';
152
+ const agentId = body.agentId || 'claude-code';
153
+ const skipPermissions = body.skipPermissions || false;
154
+
155
+ debugLog(`[stream] Starting stream: conversationId=${conversationId}, agentId=${agentId}, skipPermissions=${skipPermissions}`);
156
+
157
+ // Create user message and session immediately
158
+ const userMessage = queries.createMessage(conversationId, 'user', prompt);
159
+ const session = queries.createSession(conversationId);
160
+ queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
161
+ queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
162
+
163
+ // Send immediate response with session info
164
+ res.writeHead(200, { 'Content-Type': 'application/json' });
165
+ res.end(JSON.stringify({ message: userMessage, session, streamId: session.id }));
166
+
167
+ // Emit streaming start event
168
+ broadcastSync({
169
+ type: 'streaming_start',
170
+ sessionId: session.id,
171
+ conversationId,
172
+ messageId: userMessage.id,
173
+ agentId,
174
+ timestamp: Date.now()
175
+ });
176
+
177
+ // Fire-and-forget streaming with error handling
178
+ processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId, skipPermissions)
179
+ .catch(err => debugLog(`[stream] Uncaught error: ${err.message}`));
180
+ return;
181
+ }
182
+
144
183
  const messageMatch = routePath.match(/^\/api\/conversations\/([^/]+)\/messages\/([^/]+)$/);
145
184
  if (messageMatch && req.method === 'GET') {
146
185
  const msg = queries.getMessage(messageMatch[2]);
@@ -174,6 +213,46 @@ const server = http.createServer(async (req, res) => {
174
213
  return;
175
214
  }
176
215
 
216
+ const executionMatch = routePath.match(/^\/api\/sessions\/([^/]+)\/execution$/);
217
+ if (executionMatch && req.method === 'GET') {
218
+ const sessionId = executionMatch[1];
219
+ const url = new URL(req.url, 'http://localhost');
220
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '1000'), 5000);
221
+ const offset = Math.max(parseInt(url.searchParams.get('offset') || '0'), 0);
222
+ const filterType = url.searchParams.get('filterType');
223
+
224
+ try {
225
+ // Retrieve execution history from database
226
+ // This would normally query execution_events table
227
+ // For now, return proper response structure
228
+ const executionData = {
229
+ sessionId,
230
+ events: [],
231
+ total: 0,
232
+ limit,
233
+ offset,
234
+ hasMore: false,
235
+ metadata: {
236
+ status: 'pending',
237
+ startTime: Date.now(),
238
+ duration: 0,
239
+ eventCount: 0
240
+ }
241
+ };
242
+
243
+ if (filterType) {
244
+ executionData.events = executionData.events.filter(e => e.type === filterType);
245
+ }
246
+
247
+ res.writeHead(200, { 'Content-Type': 'application/json' });
248
+ res.end(JSON.stringify(executionData));
249
+ } catch (err) {
250
+ res.writeHead(400, { 'Content-Type': 'application/json' });
251
+ res.end(JSON.stringify({ error: err.message }));
252
+ }
253
+ return;
254
+ }
255
+
177
256
  if (routePath === '/api/agents' && req.method === 'GET') {
178
257
  res.writeHead(200, { 'Content-Type': 'application/json' });
179
258
  res.end(JSON.stringify({ agents: discoveredAgents }));
@@ -287,6 +366,132 @@ function serveFile(filePath, res) {
287
366
  });
288
367
  }
289
368
 
369
+ async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, skipPermissions = false) {
370
+ const startTime = Date.now();
371
+ try {
372
+ debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}, agentId=${agentId}, skipPermissions=${skipPermissions}`);
373
+
374
+ const cwd = '/config';
375
+ const actualAgentId = agentId || 'claude-code';
376
+
377
+ debugLog(`[stream] Calling runClaudeWithStreaming with config: skipPermissions=${skipPermissions}`);
378
+ const config = {
379
+ skipPermissions,
380
+ verbose: true,
381
+ outputFormat: 'stream-json',
382
+ timeout: 1800000, // 30 minutes
383
+ print: true
384
+ };
385
+
386
+ const outputs = await runClaudeWithStreaming(content, cwd, actualAgentId, config);
387
+ debugLog(`[stream] Claude returned ${outputs.length} streaming outputs`);
388
+
389
+ // Process streaming outputs similar to processMessage
390
+ // But emit WebSocket events for each block
391
+ let allBlocks = [];
392
+ let lastAssistantMessage = null;
393
+ let eventCount = 0;
394
+
395
+ for (const output of outputs) {
396
+ if (output.type === 'assistant' && output.message?.content) {
397
+ debugLog(`[stream] Found assistant message with ${output.message.content.length} content blocks`);
398
+ lastAssistantMessage = output.message;
399
+ allBlocks.push(...(output.message.content || []));
400
+
401
+ // Emit progress event for each block
402
+ broadcastSync({
403
+ type: 'streaming_progress',
404
+ sessionId,
405
+ conversationId,
406
+ blockCount: allBlocks.length,
407
+ timestamp: Date.now()
408
+ });
409
+ eventCount++;
410
+ } else if (output.type === 'tool_result' && output.result) {
411
+ debugLog(`[stream] Found tool result`);
412
+ allBlocks.push({
413
+ type: 'tool_result',
414
+ result: output.result,
415
+ tool_use_id: output.tool_use_id
416
+ });
417
+ eventCount++;
418
+ }
419
+ }
420
+
421
+ let messageContent = null;
422
+ if (allBlocks.length > 0) {
423
+ messageContent = JSON.stringify({
424
+ type: 'claude_execution',
425
+ blocks: allBlocks,
426
+ timestamp: Date.now()
427
+ });
428
+ debugLog(`[stream] Storing full execution with ${allBlocks.length} blocks`);
429
+ } else {
430
+ let textParts = [];
431
+ for (const output of outputs) {
432
+ if (typeof output === 'string') {
433
+ textParts.push(output);
434
+ } else if (output.text) {
435
+ textParts.push(output.text);
436
+ } else if (output.content?.text) {
437
+ textParts.push(output.content.text);
438
+ } else if (output.result) {
439
+ textParts.push(String(output.result));
440
+ }
441
+ }
442
+ messageContent = textParts.join('\n').trim();
443
+ debugLog(`[stream] Storing text response: "${messageContent.substring(0, 100)}..."`);
444
+ }
445
+
446
+ if (messageContent) {
447
+ debugLog(`[stream] Creating assistant message`);
448
+ const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
449
+ debugLog(`[stream] Created message with id: ${assistantMessage.id}`);
450
+ broadcastSync({
451
+ type: 'streaming_complete',
452
+ sessionId,
453
+ conversationId,
454
+ messageId: assistantMessage.id,
455
+ eventCount,
456
+ timestamp: Date.now()
457
+ });
458
+ } else {
459
+ debugLog(`[stream] No response content extracted!`);
460
+ }
461
+
462
+ debugLog(`[stream] ✅ Completed: ${outputs.length} outputs received, ${eventCount} events emitted`);
463
+ } catch (error) {
464
+ const elapsed = Date.now() - startTime;
465
+ debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
466
+
467
+ // Mark session as incomplete for recovery
468
+ try {
469
+ const sessionStatus = error.message.includes('timeout') ? 'timeout' : 'error';
470
+ queries.markSessionIncomplete(sessionId, error.message);
471
+ debugLog(`[stream] Session ${sessionId} marked as incomplete (${sessionStatus})`);
472
+ } catch (err) {
473
+ debugLog(`[stream] Failed to mark session: ${err.message}`);
474
+ }
475
+
476
+ broadcastSync({
477
+ type: 'streaming_error',
478
+ sessionId,
479
+ conversationId,
480
+ error: error.message,
481
+ recoverable: elapsed < 60000, // Retryable if failed within 1 minute
482
+ timestamp: Date.now()
483
+ });
484
+
485
+ const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
486
+ broadcastSync({
487
+ type: 'message_created',
488
+ conversationId,
489
+ message: errorMessage,
490
+ timestamp: Date.now()
491
+ });
492
+ }
493
+ }
494
+
290
495
  async function processMessage(conversationId, messageId, content, agentId) {
291
496
  try {
292
497
  debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
@@ -401,11 +606,34 @@ wss.on('connection', (ws, req) => {
401
606
  const data = JSON.parse(msg);
402
607
  if (data.type === 'subscribe') {
403
608
  ws.subscriptions.add(data.sessionId);
609
+ debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${data.sessionId}`);
610
+ ws.send(JSON.stringify({
611
+ type: 'subscription_confirmed',
612
+ sessionId: data.sessionId,
613
+ timestamp: Date.now()
614
+ }));
404
615
  } else if (data.type === 'unsubscribe') {
405
616
  ws.subscriptions.delete(data.sessionId);
617
+ debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId}`);
618
+ } else if (data.type === 'get_subscriptions') {
619
+ ws.send(JSON.stringify({
620
+ type: 'subscriptions',
621
+ subscriptions: Array.from(ws.subscriptions),
622
+ timestamp: Date.now()
623
+ }));
624
+ } else if (data.type === 'ping') {
625
+ ws.send(JSON.stringify({
626
+ type: 'pong',
627
+ timestamp: Date.now()
628
+ }));
406
629
  }
407
630
  } catch (e) {
408
631
  console.error('WebSocket message parse error:', e.message);
632
+ ws.send(JSON.stringify({
633
+ type: 'error',
634
+ error: 'Invalid message format',
635
+ timestamp: Date.now()
636
+ }));
409
637
  }
410
638
  });
411
639
 
@@ -419,15 +647,29 @@ wss.on('connection', (ws, req) => {
419
647
 
420
648
  function broadcastSync(event) {
421
649
  const data = JSON.stringify(event);
650
+ const isStreamingEvent = event.type && event.type.startsWith('streaming_');
651
+ const targetSessionId = event.sessionId || (event.conversationId && `conv-${event.conversationId}`);
652
+
422
653
  for (const ws of syncClients) {
423
- if (ws.readyState === 1) {
424
- // CRITICAL: Only send if client subscribed to this session
425
- if (event.sessionId) {
426
- if (!ws.subscriptions || !ws.subscriptions.has(event.sessionId)) {
427
- continue;
428
- }
429
- }
430
- // Send immediately - no buffering
654
+ if (ws.readyState !== 1) continue;
655
+
656
+ let shouldSend = false;
657
+
658
+ if (isStreamingEvent && targetSessionId) {
659
+ // Streaming events require sessionId subscription
660
+ shouldSend = ws.subscriptions && ws.subscriptions.has(targetSessionId);
661
+ } else if (event.sessionId) {
662
+ // Regular session events require sessionId subscription
663
+ shouldSend = ws.subscriptions && ws.subscriptions.has(event.sessionId);
664
+ } else if (event.type === 'message_created' || event.type === 'conversation_created') {
665
+ // Global events sent to all clients
666
+ shouldSend = true;
667
+ } else {
668
+ // Default: send to all connected clients
669
+ shouldSend = true;
670
+ }
671
+
672
+ if (shouldSend) {
431
673
  ws.send(data);
432
674
  }
433
675
  }
@@ -507,4 +749,28 @@ function performAutoImport() {
507
749
  }
508
750
  }
509
751
 
752
+ function performRecovery() {
753
+ try {
754
+ // Cleanup orphaned sessions (older than 7 days)
755
+ const cleanedUp = queries.cleanupOrphanedSessions(7);
756
+ if (cleanedUp > 0) {
757
+ debugLog(`[RECOVERY] Cleaned up ${cleanedUp} orphaned sessions`);
758
+ }
759
+
760
+ // Mark sessions incomplete if they've been processing too long (>2 hours)
761
+ const longRunning = queries.getSessionsProcessingLongerThan(120);
762
+ if (longRunning.length > 0) {
763
+ for (const session of longRunning) {
764
+ queries.markSessionIncomplete(session.id, 'Timeout: processing exceeded 2 hours');
765
+ }
766
+ debugLog(`[RECOVERY] Marked ${longRunning.length} long-running sessions as incomplete`);
767
+ }
768
+ } catch (err) {
769
+ console.error('[RECOVERY] Error:', err.message);
770
+ }
771
+ }
772
+
773
+ // Run recovery every 5 minutes
774
+ setInterval(performRecovery, 300000);
775
+
510
776
  server.listen(PORT, onServerReady);