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.
- package/.prd +92 -0
- package/.prd-browser +607 -0
- package/CLAUDE.md +1559 -125
- package/browser-test-harness.js +371 -0
- package/browser-test.js +409 -0
- package/execute-tests.js +164 -0
- package/lib/claude-runner.js +41 -12
- package/lib/database-service.ts +252 -0
- package/lib/sync-service.ts +275 -0
- package/lib/types.ts +168 -0
- package/package.json +1 -1
- package/readme.md +586 -0
- package/run-e2e-test.sh +88 -0
- package/server.js +274 -8
- package/static/index.html +487 -180
- package/static/js/client.js +558 -0
- package/static/js/event-filter.js +311 -0
- package/static/js/event-processor.js +454 -0
- package/static/js/streaming-renderer.js +813 -0
- package/static/js/syntax-highlighter.js +271 -0
- package/static/js/ui-components.js +433 -0
- package/static/js/websocket-manager.js +482 -0
- package/static/templates/INDEX.html +465 -0
- package/static/templates/README.md +190 -0
- package/static/templates/agent-capabilities.html +56 -0
- package/static/templates/agent-metadata-panel.html +44 -0
- package/static/templates/agent-status-badge.html +30 -0
- package/static/templates/code-annotation-panel.html +155 -0
- package/static/templates/code-suggestion-panel.html +184 -0
- package/static/templates/command-header.html +77 -0
- package/static/templates/command-output-scrollable.html +118 -0
- package/static/templates/elapsed-time.html +54 -0
- package/static/templates/error-alert.html +106 -0
- package/static/templates/error-history-timeline.html +160 -0
- package/static/templates/error-recovery-options.html +109 -0
- package/static/templates/error-stack-trace.html +95 -0
- package/static/templates/error-summary.html +80 -0
- package/static/templates/event-counter.html +48 -0
- package/static/templates/execution-actions.html +97 -0
- package/static/templates/execution-progress-bar.html +80 -0
- package/static/templates/execution-stepper.html +120 -0
- package/static/templates/file-breadcrumb.html +118 -0
- package/static/templates/file-diff-viewer.html +121 -0
- package/static/templates/file-metadata.html +133 -0
- package/static/templates/file-read-panel.html +66 -0
- package/static/templates/file-write-panel.html +120 -0
- package/static/templates/git-branch-remote.html +107 -0
- package/static/templates/git-diff-list.html +101 -0
- package/static/templates/git-log-visualization.html +153 -0
- package/static/templates/git-status-panel.html +115 -0
- package/static/templates/quality-metrics-display.html +170 -0
- package/static/templates/terminal-output-panel.html +87 -0
- package/static/templates/test-results-display.html +144 -0
- package/test-browser.js +457 -0
- 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
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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);
|