agentgui 1.0.93 → 1.0.95
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 +1 -0
- package/database.js +43 -1
- package/lib/claude-runner.js +24 -22
- package/package.json +5 -3
- package/server.js +209 -146
- package/static/index.html +354 -94
- package/static/js/client.js +218 -144
- package/static/js/features.js +243 -0
- package/static/js/websocket-manager.js +1 -1
- package/static/styles.css +46 -4
package/server.js
CHANGED
|
@@ -4,13 +4,20 @@ import path from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { WebSocketServer } from 'ws';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
+
import { createRequire } from 'module';
|
|
7
8
|
import { queries } from './database.js';
|
|
8
9
|
import { runClaudeWithStreaming } from './lib/claude-runner.js';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const express = require('express');
|
|
13
|
+
const Busboy = require('busboy');
|
|
14
|
+
const fsbrowse = require('fsbrowse');
|
|
15
|
+
|
|
11
16
|
const SYSTEM_PROMPT = `Always write your responses in ripple-ui enhanced HTML. Avoid overriding light/dark mode CSS variables. Use all the benefits of HTML to express technical details with proper semantic markup, tables, code blocks, headings, and lists. Write clean, well-structured HTML that respects the existing design system.`;
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
const activeExecutions = new Map();
|
|
19
|
+
const messageQueues = new Map();
|
|
20
|
+
|
|
14
21
|
const debugLog = (msg) => {
|
|
15
22
|
const timestamp = new Date().toISOString();
|
|
16
23
|
console.error(`[${timestamp}] ${msg}`);
|
|
@@ -24,6 +31,70 @@ const watch = process.argv.includes('--no-watch') ? false : (process.argv.includ
|
|
|
24
31
|
const staticDir = path.join(__dirname, 'static');
|
|
25
32
|
if (!fs.existsSync(staticDir)) fs.mkdirSync(staticDir, { recursive: true });
|
|
26
33
|
|
|
34
|
+
// Express sub-app for fsbrowse file browser and file upload
|
|
35
|
+
const expressApp = express();
|
|
36
|
+
|
|
37
|
+
// File upload endpoint - copies dropped files to conversation workingDirectory
|
|
38
|
+
expressApp.post(BASE_URL + '/api/upload/:conversationId', (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const conv = queries.getConversation(req.params.conversationId);
|
|
41
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
42
|
+
if (!conv.workingDirectory) return res.status(400).json({ error: 'No working directory set for this conversation' });
|
|
43
|
+
|
|
44
|
+
const uploadDir = conv.workingDirectory;
|
|
45
|
+
if (!fs.existsSync(uploadDir)) {
|
|
46
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const bb = Busboy({ headers: req.headers });
|
|
50
|
+
const fileNames = [];
|
|
51
|
+
const writePromises = [];
|
|
52
|
+
|
|
53
|
+
bb.on('file', (fieldname, file, info) => {
|
|
54
|
+
const safeName = path.basename(info.filename);
|
|
55
|
+
const filePath = path.join(uploadDir, safeName);
|
|
56
|
+
fileNames.push(safeName);
|
|
57
|
+
const p = new Promise((resolve) => {
|
|
58
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
59
|
+
file.pipe(writeStream);
|
|
60
|
+
writeStream.on('finish', resolve);
|
|
61
|
+
writeStream.on('error', () => { file.resume(); resolve(); });
|
|
62
|
+
});
|
|
63
|
+
writePromises.push(p);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
bb.on('finish', () => {
|
|
67
|
+
Promise.all(writePromises).then(() => {
|
|
68
|
+
res.json({ ok: true, files: fileNames, count: fileNames.length });
|
|
69
|
+
}).catch(() => {
|
|
70
|
+
res.json({ ok: true, files: fileNames, count: fileNames.length });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
bb.on('error', (err) => {
|
|
75
|
+
res.status(500).json({ error: 'Upload failed: ' + err.message });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
req.pipe(bb);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
res.status(500).json({ error: err.message });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// fsbrowse file browser - mounted per conversation workingDirectory
|
|
85
|
+
// Route: /gm/files/:conversationId/*
|
|
86
|
+
expressApp.use(BASE_URL + '/files/:conversationId', (req, res, next) => {
|
|
87
|
+
const conv = queries.getConversation(req.params.conversationId);
|
|
88
|
+
if (!conv || !conv.workingDirectory) {
|
|
89
|
+
return res.status(404).json({ error: 'Conversation not found or no working directory' });
|
|
90
|
+
}
|
|
91
|
+
// Create a fresh fsbrowse router for this conversation's directory
|
|
92
|
+
const router = fsbrowse({ baseDir: conv.workingDirectory });
|
|
93
|
+
// Strip the conversationId param from the path before passing to fsbrowse
|
|
94
|
+
req.baseUrl = BASE_URL + '/files/' + req.params.conversationId;
|
|
95
|
+
router(req, res, next);
|
|
96
|
+
});
|
|
97
|
+
|
|
27
98
|
function discoverAgents() {
|
|
28
99
|
const agents = [];
|
|
29
100
|
const binaries = [
|
|
@@ -59,6 +130,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
59
130
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
60
131
|
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
|
|
61
132
|
|
|
133
|
+
// Route file upload and fsbrowse requests through Express sub-app
|
|
134
|
+
const pathOnly = req.url.split('?')[0];
|
|
135
|
+
if (pathOnly.startsWith(BASE_URL + '/api/upload/') || pathOnly.startsWith(BASE_URL + '/files/')) {
|
|
136
|
+
return expressApp(req, res);
|
|
137
|
+
}
|
|
138
|
+
|
|
62
139
|
if (req.url === '/') { res.writeHead(302, { Location: BASE_URL + '/' }); res.end(); return; }
|
|
63
140
|
|
|
64
141
|
if (!req.url.startsWith(BASE_URL + '/') && req.url !== BASE_URL) {
|
|
@@ -158,19 +235,30 @@ const server = http.createServer(async (req, res) => {
|
|
|
158
235
|
const agentId = body.agentId || 'claude-code';
|
|
159
236
|
const skipPermissions = body.skipPermissions || false;
|
|
160
237
|
|
|
161
|
-
debugLog(`[stream] Starting stream: conversationId=${conversationId}, agentId=${agentId}, skipPermissions=${skipPermissions}`);
|
|
162
|
-
|
|
163
|
-
// Create user message and session immediately
|
|
164
238
|
const userMessage = queries.createMessage(conversationId, 'user', prompt);
|
|
165
|
-
const session = queries.createSession(conversationId);
|
|
166
239
|
queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
|
|
240
|
+
|
|
241
|
+
broadcastSync({ type: 'message_created', conversationId, message: userMessage, timestamp: Date.now() });
|
|
242
|
+
|
|
243
|
+
if (activeExecutions.has(conversationId)) {
|
|
244
|
+
debugLog(`[stream] Conversation ${conversationId} is busy, queuing message`);
|
|
245
|
+
if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
|
|
246
|
+
messageQueues.get(conversationId).push({ content: prompt, agentId, skipPermissions, messageId: userMessage.id });
|
|
247
|
+
|
|
248
|
+
const queueLength = messageQueues.get(conversationId).length;
|
|
249
|
+
broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: userMessage.id, timestamp: Date.now() });
|
|
250
|
+
|
|
251
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
252
|
+
res.end(JSON.stringify({ message: userMessage, queued: true, queuePosition: queueLength }));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const session = queries.createSession(conversationId);
|
|
167
257
|
queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
|
|
168
258
|
|
|
169
|
-
// Send immediate response with session info
|
|
170
259
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
171
260
|
res.end(JSON.stringify({ message: userMessage, session, streamId: session.id }));
|
|
172
261
|
|
|
173
|
-
// Emit streaming start event
|
|
174
262
|
broadcastSync({
|
|
175
263
|
type: 'streaming_start',
|
|
176
264
|
sessionId: session.id,
|
|
@@ -180,7 +268,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
180
268
|
timestamp: Date.now()
|
|
181
269
|
});
|
|
182
270
|
|
|
183
|
-
// Fire-and-forget streaming with error handling
|
|
184
271
|
processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId, skipPermissions)
|
|
185
272
|
.catch(err => debugLog(`[stream] Uncaught error: ${err.message}`));
|
|
186
273
|
return;
|
|
@@ -374,58 +461,63 @@ function serveFile(filePath, res) {
|
|
|
374
461
|
|
|
375
462
|
async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, skipPermissions = false) {
|
|
376
463
|
const startTime = Date.now();
|
|
464
|
+
activeExecutions.set(conversationId, true);
|
|
465
|
+
queries.setIsStreaming(conversationId, true);
|
|
466
|
+
|
|
377
467
|
try {
|
|
378
|
-
debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}
|
|
468
|
+
debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
|
|
379
469
|
|
|
380
470
|
const conv = queries.getConversation(conversationId);
|
|
381
471
|
const cwd = conv?.workingDirectory || '/config';
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
debugLog(`[stream] Calling runClaudeWithStreaming with config: skipPermissions=${skipPermissions}`);
|
|
385
|
-
const config = {
|
|
386
|
-
skipPermissions,
|
|
387
|
-
verbose: true,
|
|
388
|
-
outputFormat: 'stream-json',
|
|
389
|
-
timeout: 1800000, // 30 minutes
|
|
390
|
-
print: true
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
// Prepend system prompt to user content
|
|
394
|
-
const promptWithSystem = `${SYSTEM_PROMPT}\n\n${content}`;
|
|
472
|
+
const resumeSessionId = conv?.claudeSessionId || null;
|
|
395
473
|
|
|
396
|
-
const outputs = await runClaudeWithStreaming(promptWithSystem, cwd, actualAgentId, config);
|
|
397
|
-
debugLog(`[stream] Claude returned ${outputs.length} streaming outputs`);
|
|
398
|
-
|
|
399
|
-
// Process streaming outputs similar to processMessage
|
|
400
|
-
// But emit WebSocket events for each block
|
|
401
474
|
let allBlocks = [];
|
|
402
|
-
let lastAssistantMessage = null;
|
|
403
475
|
let eventCount = 0;
|
|
404
476
|
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
477
|
+
const onEvent = (parsed) => {
|
|
478
|
+
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
479
|
+
for (const block of parsed.message.content) {
|
|
480
|
+
allBlocks.push(block);
|
|
481
|
+
eventCount++;
|
|
482
|
+
broadcastSync({
|
|
483
|
+
type: 'streaming_progress',
|
|
484
|
+
sessionId,
|
|
485
|
+
conversationId,
|
|
486
|
+
block,
|
|
487
|
+
blockIndex: allBlocks.length - 1,
|
|
488
|
+
timestamp: Date.now()
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
} else if (parsed.type === 'result' && parsed.result && allBlocks.length === 0) {
|
|
412
492
|
broadcastSync({
|
|
413
493
|
type: 'streaming_progress',
|
|
414
494
|
sessionId,
|
|
415
495
|
conversationId,
|
|
416
|
-
|
|
496
|
+
block: { type: 'text', text: parsed.result },
|
|
497
|
+
blockIndex: 0,
|
|
498
|
+
isResult: true,
|
|
417
499
|
timestamp: Date.now()
|
|
418
500
|
});
|
|
419
|
-
eventCount++;
|
|
420
|
-
} else if (output.type === 'tool_result' && output.result) {
|
|
421
|
-
debugLog(`[stream] Found tool result`);
|
|
422
|
-
allBlocks.push({
|
|
423
|
-
type: 'tool_result',
|
|
424
|
-
result: output.result,
|
|
425
|
-
tool_use_id: output.tool_use_id
|
|
426
|
-
});
|
|
427
|
-
eventCount++;
|
|
428
501
|
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const config = {
|
|
505
|
+
skipPermissions,
|
|
506
|
+
verbose: true,
|
|
507
|
+
outputFormat: 'stream-json',
|
|
508
|
+
timeout: 1800000,
|
|
509
|
+
print: true,
|
|
510
|
+
resumeSessionId,
|
|
511
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
512
|
+
onEvent
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(content, cwd, agentId || 'claude-code', config);
|
|
516
|
+
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
517
|
+
|
|
518
|
+
if (claudeSessionId && !conv?.claudeSessionId) {
|
|
519
|
+
queries.setClaudeSessionId(conversationId, claudeSessionId);
|
|
520
|
+
debugLog(`[stream] Stored claudeSessionId=${claudeSessionId}`);
|
|
429
521
|
}
|
|
430
522
|
|
|
431
523
|
let messageContent = null;
|
|
@@ -435,28 +527,20 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
435
527
|
blocks: allBlocks,
|
|
436
528
|
timestamp: Date.now()
|
|
437
529
|
});
|
|
438
|
-
debugLog(`[stream] Storing full execution with ${allBlocks.length} blocks`);
|
|
439
530
|
} else {
|
|
440
531
|
let textParts = [];
|
|
441
532
|
for (const output of outputs) {
|
|
442
|
-
if (
|
|
443
|
-
textParts.push(output);
|
|
444
|
-
} else if (output.text) {
|
|
445
|
-
textParts.push(output.text);
|
|
446
|
-
} else if (output.content?.text) {
|
|
447
|
-
textParts.push(output.content.text);
|
|
448
|
-
} else if (output.result) {
|
|
533
|
+
if (output.type === 'result' && output.result) {
|
|
449
534
|
textParts.push(String(output.result));
|
|
535
|
+
} else if (typeof output === 'string') {
|
|
536
|
+
textParts.push(output);
|
|
450
537
|
}
|
|
451
538
|
}
|
|
452
539
|
messageContent = textParts.join('\n').trim();
|
|
453
|
-
debugLog(`[stream] Storing text response: "${messageContent.substring(0, 100)}..."`);
|
|
454
540
|
}
|
|
455
541
|
|
|
456
542
|
if (messageContent) {
|
|
457
|
-
debugLog(`[stream] Creating assistant message`);
|
|
458
543
|
const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
|
|
459
|
-
debugLog(`[stream] Created message with id: ${assistantMessage.id}`);
|
|
460
544
|
broadcastSync({
|
|
461
545
|
type: 'streaming_complete',
|
|
462
546
|
sessionId,
|
|
@@ -465,30 +549,25 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
465
549
|
eventCount,
|
|
466
550
|
timestamp: Date.now()
|
|
467
551
|
});
|
|
468
|
-
|
|
469
|
-
|
|
552
|
+
broadcastSync({
|
|
553
|
+
type: 'message_created',
|
|
554
|
+
conversationId,
|
|
555
|
+
message: assistantMessage,
|
|
556
|
+
timestamp: Date.now()
|
|
557
|
+
});
|
|
470
558
|
}
|
|
471
559
|
|
|
472
|
-
debugLog(`[stream]
|
|
560
|
+
debugLog(`[stream] Completed: ${outputs.length} outputs, ${eventCount} events`);
|
|
473
561
|
} catch (error) {
|
|
474
562
|
const elapsed = Date.now() - startTime;
|
|
475
563
|
debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
|
|
476
564
|
|
|
477
|
-
// Mark session as incomplete for recovery
|
|
478
|
-
try {
|
|
479
|
-
const sessionStatus = error.message.includes('timeout') ? 'timeout' : 'error';
|
|
480
|
-
queries.markSessionIncomplete(sessionId, error.message);
|
|
481
|
-
debugLog(`[stream] Session ${sessionId} marked as incomplete (${sessionStatus})`);
|
|
482
|
-
} catch (err) {
|
|
483
|
-
debugLog(`[stream] Failed to mark session: ${err.message}`);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
565
|
broadcastSync({
|
|
487
566
|
type: 'streaming_error',
|
|
488
567
|
sessionId,
|
|
489
568
|
conversationId,
|
|
490
569
|
error: error.message,
|
|
491
|
-
recoverable: elapsed < 60000,
|
|
570
|
+
recoverable: elapsed < 60000,
|
|
492
571
|
timestamp: Date.now()
|
|
493
572
|
});
|
|
494
573
|
|
|
@@ -499,102 +578,91 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
499
578
|
message: errorMessage,
|
|
500
579
|
timestamp: Date.now()
|
|
501
580
|
});
|
|
581
|
+
} finally {
|
|
582
|
+
activeExecutions.delete(conversationId);
|
|
583
|
+
queries.setIsStreaming(conversationId, false);
|
|
584
|
+
drainMessageQueue(conversationId);
|
|
502
585
|
}
|
|
503
586
|
}
|
|
504
587
|
|
|
588
|
+
function drainMessageQueue(conversationId) {
|
|
589
|
+
const queue = messageQueues.get(conversationId);
|
|
590
|
+
if (!queue || queue.length === 0) return;
|
|
591
|
+
|
|
592
|
+
const next = queue.shift();
|
|
593
|
+
if (queue.length === 0) messageQueues.delete(conversationId);
|
|
594
|
+
|
|
595
|
+
debugLog(`[queue] Draining next message for ${conversationId}`);
|
|
596
|
+
|
|
597
|
+
const session = queries.createSession(conversationId);
|
|
598
|
+
queries.createEvent('session.created', { messageId: next.messageId, sessionId: session.id }, conversationId, session.id);
|
|
599
|
+
|
|
600
|
+
broadcastSync({
|
|
601
|
+
type: 'streaming_start',
|
|
602
|
+
sessionId: session.id,
|
|
603
|
+
conversationId,
|
|
604
|
+
messageId: next.messageId,
|
|
605
|
+
agentId: next.agentId,
|
|
606
|
+
timestamp: Date.now()
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
broadcastSync({
|
|
610
|
+
type: 'queue_status',
|
|
611
|
+
conversationId,
|
|
612
|
+
queueLength: queue?.length || 0,
|
|
613
|
+
timestamp: Date.now()
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
processMessageWithStreaming(conversationId, next.messageId, session.id, next.content, next.agentId, next.skipPermissions)
|
|
617
|
+
.catch(err => debugLog(`[queue] Error processing queued message: ${err.message}`));
|
|
618
|
+
}
|
|
619
|
+
|
|
505
620
|
async function processMessage(conversationId, messageId, content, agentId) {
|
|
506
621
|
try {
|
|
507
622
|
debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
|
|
508
623
|
|
|
509
624
|
const conv = queries.getConversation(conversationId);
|
|
510
625
|
const cwd = conv?.workingDirectory || '/config';
|
|
511
|
-
const
|
|
626
|
+
const resumeSessionId = conv?.claudeSessionId || null;
|
|
512
627
|
|
|
513
|
-
|
|
514
|
-
let contentStr = content;
|
|
515
|
-
if (typeof content === 'object') {
|
|
516
|
-
contentStr = JSON.stringify(content);
|
|
517
|
-
}
|
|
628
|
+
let contentStr = typeof content === 'object' ? JSON.stringify(content) : content;
|
|
518
629
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
debugLog(`[processMessage] Claude returned ${outputs.length} outputs`);
|
|
630
|
+
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(contentStr, cwd, agentId || 'claude-code', {
|
|
631
|
+
resumeSessionId,
|
|
632
|
+
systemPrompt: SYSTEM_PROMPT
|
|
633
|
+
});
|
|
524
634
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
635
|
+
if (claudeSessionId && !conv?.claudeSessionId) {
|
|
636
|
+
queries.setClaudeSessionId(conversationId, claudeSessionId);
|
|
637
|
+
}
|
|
528
638
|
|
|
639
|
+
let allBlocks = [];
|
|
529
640
|
for (const output of outputs) {
|
|
530
641
|
if (output.type === 'assistant' && output.message?.content) {
|
|
531
|
-
debugLog(`[processMessage] Found assistant message with ${output.message.content.length} content blocks`);
|
|
532
|
-
lastAssistantMessage = output.message;
|
|
533
642
|
allBlocks.push(...(output.message.content || []));
|
|
534
|
-
} else if (output.type === 'tool_result' && output.result) {
|
|
535
|
-
debugLog(`[processMessage] Found tool result: ${typeof output.result}`);
|
|
536
|
-
allBlocks.push({
|
|
537
|
-
type: 'tool_result',
|
|
538
|
-
result: output.result,
|
|
539
|
-
tool_use_id: output.tool_use_id
|
|
540
|
-
});
|
|
541
643
|
}
|
|
542
644
|
}
|
|
543
645
|
|
|
544
|
-
// Store full message structure if we have execution data, otherwise fallback to text
|
|
545
646
|
let messageContent = null;
|
|
546
|
-
|
|
547
647
|
if (allBlocks.length > 0) {
|
|
548
|
-
|
|
549
|
-
messageContent = JSON.stringify({
|
|
550
|
-
type: 'claude_execution',
|
|
551
|
-
blocks: allBlocks,
|
|
552
|
-
timestamp: Date.now()
|
|
553
|
-
});
|
|
554
|
-
debugLog(`[processMessage] Storing full execution with ${allBlocks.length} blocks`);
|
|
648
|
+
messageContent = JSON.stringify({ type: 'claude_execution', blocks: allBlocks, timestamp: Date.now() });
|
|
555
649
|
} else {
|
|
556
|
-
// Fallback: extract text for simple responses
|
|
557
650
|
let textParts = [];
|
|
558
651
|
for (const output of outputs) {
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
} else if (output.text) {
|
|
562
|
-
textParts.push(output.text);
|
|
563
|
-
} else if (output.content?.text) {
|
|
564
|
-
textParts.push(output.content.text);
|
|
565
|
-
} else if (output.result) {
|
|
566
|
-
textParts.push(String(output.result));
|
|
567
|
-
}
|
|
652
|
+
if (output.type === 'result' && output.result) textParts.push(String(output.result));
|
|
653
|
+
else if (typeof output === 'string') textParts.push(output);
|
|
568
654
|
}
|
|
569
655
|
messageContent = textParts.join('\n').trim();
|
|
570
|
-
debugLog(`[processMessage] Storing text response: "${messageContent.substring(0, 100)}..."`);
|
|
571
656
|
}
|
|
572
657
|
|
|
573
658
|
if (messageContent) {
|
|
574
|
-
debugLog(`[processMessage] Creating assistant message`);
|
|
575
659
|
const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
|
|
576
|
-
|
|
577
|
-
broadcastSync({
|
|
578
|
-
type: 'message_created',
|
|
579
|
-
conversationId,
|
|
580
|
-
message: assistantMessage,
|
|
581
|
-
timestamp: Date.now()
|
|
582
|
-
});
|
|
583
|
-
} else {
|
|
584
|
-
debugLog(`[processMessage] No response content extracted!`);
|
|
660
|
+
broadcastSync({ type: 'message_created', conversationId, message: assistantMessage, timestamp: Date.now() });
|
|
585
661
|
}
|
|
586
|
-
|
|
587
|
-
debugLog(`[processMessage] ✅ Completed: ${outputs.length} outputs received`);
|
|
588
662
|
} catch (error) {
|
|
589
663
|
debugLog(`[processMessage] Error: ${error.message}`);
|
|
590
|
-
debugLog(`[processMessage] Stack: ${error.stack}`);
|
|
591
664
|
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
|
|
592
|
-
broadcastSync({
|
|
593
|
-
type: 'message_created',
|
|
594
|
-
conversationId,
|
|
595
|
-
message: errorMessage,
|
|
596
|
-
timestamp: Date.now()
|
|
597
|
-
});
|
|
665
|
+
broadcastSync({ type: 'message_created', conversationId, message: errorMessage, timestamp: Date.now() });
|
|
598
666
|
}
|
|
599
667
|
}
|
|
600
668
|
|
|
@@ -624,11 +692,14 @@ wss.on('connection', (ws, req) => {
|
|
|
624
692
|
try {
|
|
625
693
|
const data = JSON.parse(msg);
|
|
626
694
|
if (data.type === 'subscribe') {
|
|
627
|
-
ws.subscriptions.add(data.sessionId);
|
|
628
|
-
|
|
695
|
+
if (data.sessionId) ws.subscriptions.add(data.sessionId);
|
|
696
|
+
if (data.conversationId) ws.subscriptions.add(`conv-${data.conversationId}`);
|
|
697
|
+
const subTarget = data.sessionId || data.conversationId;
|
|
698
|
+
debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
|
|
629
699
|
ws.send(JSON.stringify({
|
|
630
700
|
type: 'subscription_confirmed',
|
|
631
701
|
sessionId: data.sessionId,
|
|
702
|
+
conversationId: data.conversationId,
|
|
632
703
|
timestamp: Date.now()
|
|
633
704
|
}));
|
|
634
705
|
} else if (data.type === 'unsubscribe') {
|
|
@@ -666,25 +737,17 @@ wss.on('connection', (ws, req) => {
|
|
|
666
737
|
|
|
667
738
|
function broadcastSync(event) {
|
|
668
739
|
const data = JSON.stringify(event);
|
|
669
|
-
const isStreamingEvent = event.type && event.type.startsWith('streaming_');
|
|
670
|
-
const targetSessionId = event.sessionId || (event.conversationId && `conv-${event.conversationId}`);
|
|
671
740
|
|
|
672
741
|
for (const ws of syncClients) {
|
|
673
742
|
if (ws.readyState !== 1) continue;
|
|
674
743
|
|
|
675
744
|
let shouldSend = false;
|
|
676
745
|
|
|
677
|
-
if (
|
|
678
|
-
// Streaming events require sessionId subscription
|
|
679
|
-
shouldSend = ws.subscriptions && ws.subscriptions.has(targetSessionId);
|
|
680
|
-
} else if (event.sessionId) {
|
|
681
|
-
// Regular session events require sessionId subscription
|
|
682
|
-
shouldSend = ws.subscriptions && ws.subscriptions.has(event.sessionId);
|
|
683
|
-
} else if (event.type === 'message_created' || event.type === 'conversation_created') {
|
|
684
|
-
// Global events sent to all clients
|
|
746
|
+
if (event.sessionId && ws.subscriptions?.has(event.sessionId)) {
|
|
685
747
|
shouldSend = true;
|
|
686
|
-
} else {
|
|
687
|
-
|
|
748
|
+
} else if (event.conversationId && ws.subscriptions?.has(`conv-${event.conversationId}`)) {
|
|
749
|
+
shouldSend = true;
|
|
750
|
+
} else if (event.type === 'message_created' || event.type === 'conversation_created' || event.type === 'conversations_updated' || event.type === 'conversation_deleted' || event.type === 'queue_status') {
|
|
688
751
|
shouldSend = true;
|
|
689
752
|
}
|
|
690
753
|
|