agentgui 1.0.155 → 1.0.156
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/package.json +1 -1
- package/server.js +39 -68
- package/static/js/client.js +49 -25
- package/static/js/conversations.js +4 -3
- package/static/js/websocket-manager.js +23 -3
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -27,7 +27,6 @@ const messageQueues = new Map();
|
|
|
27
27
|
const rateLimitState = new Map();
|
|
28
28
|
const STUCK_AGENT_THRESHOLD_MS = 600000;
|
|
29
29
|
const NO_PID_GRACE_PERIOD_MS = 60000;
|
|
30
|
-
const STALE_SESSION_MIN_AGE_MS = 30000;
|
|
31
30
|
const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 60000;
|
|
32
31
|
|
|
33
32
|
const debugLog = (msg) => {
|
|
@@ -231,10 +230,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
231
230
|
const conv = queries.getConversation(convMatch[1]);
|
|
232
231
|
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
233
232
|
|
|
234
|
-
// Check both in-memory and database for active streaming status
|
|
235
233
|
const latestSession = queries.getLatestSession(convMatch[1]);
|
|
236
|
-
const isActivelyStreaming = activeExecutions.has(convMatch[1])
|
|
237
|
-
(latestSession && latestSession.status === 'active');
|
|
234
|
+
const isActivelyStreaming = activeExecutions.has(convMatch[1]);
|
|
238
235
|
|
|
239
236
|
sendJSON(req, res, 200, {
|
|
240
237
|
conversation: conv,
|
|
@@ -279,17 +276,41 @@ const server = http.createServer(async (req, res) => {
|
|
|
279
276
|
const conv = queries.getConversation(conversationId);
|
|
280
277
|
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
281
278
|
const body = await parseBody(req);
|
|
279
|
+
const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
|
|
282
280
|
const idempotencyKey = body.idempotencyKey || null;
|
|
283
281
|
const message = queries.createMessage(conversationId, 'user', body.content, idempotencyKey);
|
|
284
282
|
queries.createEvent('message.created', { role: 'user', messageId: message.id }, conversationId);
|
|
285
283
|
broadcastSync({ type: 'message_created', conversationId, message, timestamp: Date.now() });
|
|
284
|
+
|
|
285
|
+
if (activeExecutions.has(conversationId)) {
|
|
286
|
+
if (!messageQueues.has(conversationId)) messageQueues.set(conversationId, []);
|
|
287
|
+
messageQueues.get(conversationId).push({ content: body.content, agentId, messageId: message.id });
|
|
288
|
+
const queueLength = messageQueues.get(conversationId).length;
|
|
289
|
+
broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: message.id, timestamp: Date.now() });
|
|
290
|
+
sendJSON(req, res, 200, { message, queued: true, queuePosition: queueLength, idempotencyKey });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
286
294
|
const session = queries.createSession(conversationId);
|
|
287
295
|
queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, conversationId, session.id);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
296
|
+
|
|
297
|
+
activeExecutions.set(conversationId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
|
|
298
|
+
queries.setIsStreaming(conversationId, true);
|
|
299
|
+
|
|
300
|
+
broadcastSync({
|
|
301
|
+
type: 'streaming_start',
|
|
302
|
+
sessionId: session.id,
|
|
303
|
+
conversationId,
|
|
304
|
+
messageId: message.id,
|
|
305
|
+
agentId,
|
|
306
|
+
timestamp: Date.now()
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
sendJSON(req, res, 201, { message, session, idempotencyKey });
|
|
310
|
+
|
|
311
|
+
processMessageWithStreaming(conversationId, message.id, session.id, body.content, agentId)
|
|
312
|
+
.catch(err => debugLog(`[messages] Uncaught error: ${err.message}`));
|
|
313
|
+
return;
|
|
293
314
|
}
|
|
294
315
|
}
|
|
295
316
|
|
|
@@ -323,7 +344,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
323
344
|
const session = queries.createSession(conversationId);
|
|
324
345
|
queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
|
|
325
346
|
|
|
326
|
-
|
|
347
|
+
activeExecutions.set(conversationId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
|
|
348
|
+
queries.setIsStreaming(conversationId, true);
|
|
327
349
|
|
|
328
350
|
broadcastSync({
|
|
329
351
|
type: 'streaming_start',
|
|
@@ -334,6 +356,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
334
356
|
timestamp: Date.now()
|
|
335
357
|
});
|
|
336
358
|
|
|
359
|
+
sendJSON(req, res, 200, { message: userMessage, session, streamId: session.id });
|
|
360
|
+
|
|
337
361
|
processMessageWithStreaming(conversationId, userMessage.id, session.id, prompt, agentId)
|
|
338
362
|
.catch(err => debugLog(`[stream] Uncaught error: ${err.message}`));
|
|
339
363
|
return;
|
|
@@ -362,8 +386,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
362
386
|
const conv = queries.getConversation(conversationId);
|
|
363
387
|
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
364
388
|
const latestSession = queries.getLatestSession(conversationId);
|
|
365
|
-
const isActivelyStreaming = activeExecutions.has(conversationId)
|
|
366
|
-
(latestSession && latestSession.status === 'active');
|
|
389
|
+
const isActivelyStreaming = activeExecutions.has(conversationId);
|
|
367
390
|
|
|
368
391
|
const url = new URL(req.url, 'http://localhost');
|
|
369
392
|
const chunkLimit = Math.min(parseInt(url.searchParams.get('chunkLimit') || '500'), 5000);
|
|
@@ -896,8 +919,6 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
896
919
|
});
|
|
897
920
|
|
|
898
921
|
batcher.drain();
|
|
899
|
-
activeExecutions.delete(conversationId);
|
|
900
|
-
queries.setIsStreaming(conversationId, false);
|
|
901
922
|
|
|
902
923
|
setTimeout(() => {
|
|
903
924
|
rateLimitState.delete(conversationId);
|
|
@@ -987,54 +1008,6 @@ function drainMessageQueue(conversationId) {
|
|
|
987
1008
|
.catch(err => debugLog(`[queue] Error processing queued message: ${err.message}`));
|
|
988
1009
|
}
|
|
989
1010
|
|
|
990
|
-
async function processMessage(conversationId, messageId, content, agentId) {
|
|
991
|
-
try {
|
|
992
|
-
debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
|
|
993
|
-
|
|
994
|
-
const conv = queries.getConversation(conversationId);
|
|
995
|
-
const cwd = conv?.workingDirectory || STARTUP_CWD;
|
|
996
|
-
const resumeSessionId = conv?.claudeSessionId || null;
|
|
997
|
-
|
|
998
|
-
let contentStr = typeof content === 'object' ? JSON.stringify(content) : content;
|
|
999
|
-
|
|
1000
|
-
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(contentStr, cwd, agentId || 'claude-code', {
|
|
1001
|
-
resumeSessionId,
|
|
1002
|
-
systemPrompt: SYSTEM_PROMPT
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
if (claudeSessionId) {
|
|
1006
|
-
queries.setClaudeSessionId(conversationId, claudeSessionId);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
let allBlocks = [];
|
|
1010
|
-
for (const output of outputs) {
|
|
1011
|
-
if (output.type === 'assistant' && output.message?.content) {
|
|
1012
|
-
allBlocks.push(...(output.message.content || []));
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
let messageContent = null;
|
|
1017
|
-
if (allBlocks.length > 0) {
|
|
1018
|
-
messageContent = JSON.stringify({ type: 'claude_execution', blocks: allBlocks, timestamp: Date.now() });
|
|
1019
|
-
} else {
|
|
1020
|
-
let textParts = [];
|
|
1021
|
-
for (const output of outputs) {
|
|
1022
|
-
if (output.type === 'result' && output.result) textParts.push(String(output.result));
|
|
1023
|
-
else if (typeof output === 'string') textParts.push(output);
|
|
1024
|
-
}
|
|
1025
|
-
messageContent = textParts.join('\n').trim();
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
if (messageContent) {
|
|
1029
|
-
const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
|
|
1030
|
-
broadcastSync({ type: 'message_created', conversationId, message: assistantMessage, timestamp: Date.now() });
|
|
1031
|
-
}
|
|
1032
|
-
} catch (error) {
|
|
1033
|
-
debugLog(`[processMessage] Error: ${error.message}`);
|
|
1034
|
-
const errorMessage = queries.createMessage(conversationId, 'assistant', `Error: ${error.message}`);
|
|
1035
|
-
broadcastSync({ type: 'message_created', conversationId, message: errorMessage, timestamp: Date.now() });
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
1011
|
|
|
1039
1012
|
const wss = new WebSocketServer({
|
|
1040
1013
|
server,
|
|
@@ -1128,10 +1101,10 @@ wss.on('connection', (ws, req) => {
|
|
|
1128
1101
|
});
|
|
1129
1102
|
|
|
1130
1103
|
const BROADCAST_TYPES = new Set([
|
|
1131
|
-
'message_created', 'conversation_created', '
|
|
1132
|
-
'
|
|
1133
|
-
'
|
|
1134
|
-
'rate_limit_clear'
|
|
1104
|
+
'message_created', 'conversation_created', 'conversation_updated',
|
|
1105
|
+
'conversations_updated', 'conversation_deleted', 'queue_status',
|
|
1106
|
+
'streaming_start', 'streaming_complete', 'streaming_error',
|
|
1107
|
+
'rate_limit_hit', 'rate_limit_clear'
|
|
1135
1108
|
]);
|
|
1136
1109
|
|
|
1137
1110
|
const wsBatchQueues = new Map();
|
|
@@ -1233,8 +1206,6 @@ function recoverStaleSessions() {
|
|
|
1233
1206
|
|
|
1234
1207
|
for (const session of staleSessions) {
|
|
1235
1208
|
if (activeExecutions.has(session.conversationId)) continue;
|
|
1236
|
-
const sessionAge = now - session.started_at;
|
|
1237
|
-
if (sessionAge < STALE_SESSION_MIN_AGE_MS) continue;
|
|
1238
1209
|
|
|
1239
1210
|
queries.updateSession(session.id, {
|
|
1240
1211
|
status: 'error',
|
package/static/js/client.js
CHANGED
|
@@ -24,7 +24,7 @@ class AgentGUIClient {
|
|
|
24
24
|
isInitialized: false,
|
|
25
25
|
currentSession: null,
|
|
26
26
|
currentConversation: null,
|
|
27
|
-
|
|
27
|
+
streamingConversations: new Map(),
|
|
28
28
|
sessionEvents: [],
|
|
29
29
|
conversations: [],
|
|
30
30
|
agents: []
|
|
@@ -405,11 +405,12 @@ class AgentGUIClient {
|
|
|
405
405
|
// just track the state but do not modify the DOM or start polling
|
|
406
406
|
if (this.state.currentConversation?.id !== data.conversationId) {
|
|
407
407
|
console.log('Streaming started for non-active conversation:', data.conversationId);
|
|
408
|
+
this.state.streamingConversations.set(data.conversationId, true);
|
|
408
409
|
this.emit('streaming:start', data);
|
|
409
410
|
return;
|
|
410
411
|
}
|
|
411
412
|
|
|
412
|
-
this.state.
|
|
413
|
+
this.state.streamingConversations.set(data.conversationId, true);
|
|
413
414
|
this.state.currentSession = {
|
|
414
415
|
id: data.sessionId,
|
|
415
416
|
conversationId: data.conversationId,
|
|
@@ -583,11 +584,12 @@ class AgentGUIClient {
|
|
|
583
584
|
// If this event is for a conversation we are NOT currently viewing, just track state
|
|
584
585
|
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
585
586
|
console.log('Streaming error for non-active conversation:', conversationId);
|
|
587
|
+
this.state.streamingConversations.delete(conversationId);
|
|
586
588
|
this.emit('streaming:error', data);
|
|
587
589
|
return;
|
|
588
590
|
}
|
|
589
591
|
|
|
590
|
-
this.state.
|
|
592
|
+
this.state.streamingConversations.delete(conversationId);
|
|
591
593
|
|
|
592
594
|
// Stop polling for chunks
|
|
593
595
|
this.stopChunkPolling();
|
|
@@ -613,11 +615,12 @@ class AgentGUIClient {
|
|
|
613
615
|
|
|
614
616
|
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
615
617
|
console.log('Streaming completed for non-active conversation:', conversationId);
|
|
618
|
+
this.state.streamingConversations.delete(conversationId);
|
|
616
619
|
this.emit('streaming:complete', data);
|
|
617
620
|
return;
|
|
618
621
|
}
|
|
619
622
|
|
|
620
|
-
this.state.
|
|
623
|
+
this.state.streamingConversations.delete(conversationId);
|
|
621
624
|
|
|
622
625
|
// Stop polling for chunks
|
|
623
626
|
this.stopChunkPolling();
|
|
@@ -662,7 +665,7 @@ class AgentGUIClient {
|
|
|
662
665
|
return;
|
|
663
666
|
}
|
|
664
667
|
|
|
665
|
-
if (data.message.role === 'assistant' && this.state.
|
|
668
|
+
if (data.message.role === 'assistant' && this.state.streamingConversations.has(data.conversationId)) {
|
|
666
669
|
this.emit('message:created', data);
|
|
667
670
|
return;
|
|
668
671
|
}
|
|
@@ -707,15 +710,21 @@ class AgentGUIClient {
|
|
|
707
710
|
|
|
708
711
|
handleRateLimitHit(data) {
|
|
709
712
|
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
710
|
-
this.state.
|
|
713
|
+
this.state.streamingConversations.delete(data.conversationId);
|
|
711
714
|
this.stopChunkPolling();
|
|
715
|
+
this.enableControls();
|
|
716
|
+
|
|
717
|
+
const cooldownMs = data.retryAfterMs || 60000;
|
|
718
|
+
this._rateLimitSafetyTimer = setTimeout(() => {
|
|
719
|
+
this.enableControls();
|
|
720
|
+
}, cooldownMs + 10000);
|
|
712
721
|
|
|
713
722
|
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
714
723
|
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
715
724
|
if (streamingEl) {
|
|
716
725
|
const indicator = streamingEl.querySelector('.streaming-indicator');
|
|
717
726
|
if (indicator) {
|
|
718
|
-
const retrySeconds = Math.ceil(
|
|
727
|
+
const retrySeconds = Math.ceil(cooldownMs / 1000);
|
|
719
728
|
indicator.innerHTML = `<span style="color:var(--color-warning);">Rate limited. Retrying in ${retrySeconds}s...</span>`;
|
|
720
729
|
let remaining = retrySeconds;
|
|
721
730
|
const countdownTimer = setInterval(() => {
|
|
@@ -733,6 +742,10 @@ class AgentGUIClient {
|
|
|
733
742
|
|
|
734
743
|
handleRateLimitClear(data) {
|
|
735
744
|
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
745
|
+
if (this._rateLimitSafetyTimer) {
|
|
746
|
+
clearTimeout(this._rateLimitSafetyTimer);
|
|
747
|
+
this._rateLimitSafetyTimer = null;
|
|
748
|
+
}
|
|
736
749
|
this.enableControls();
|
|
737
750
|
}
|
|
738
751
|
|
|
@@ -1012,35 +1025,46 @@ class AgentGUIClient {
|
|
|
1012
1025
|
pollState.lastFetchTimestamp = Date.now();
|
|
1013
1026
|
pollState.backoffDelay = 150;
|
|
1014
1027
|
pollState.sessionCheckCounter = 0;
|
|
1028
|
+
pollState.emptyPollCount = 0;
|
|
1029
|
+
|
|
1030
|
+
const checkSessionStatus = async () => {
|
|
1031
|
+
if (!this.state.currentSession?.id) return false;
|
|
1032
|
+
const sessionResponse = await fetch(`${window.__BASE_URL}/api/sessions/${this.state.currentSession.id}`);
|
|
1033
|
+
if (!sessionResponse.ok) return false;
|
|
1034
|
+
const { session } = await sessionResponse.json();
|
|
1035
|
+
if (session && (session.status === 'complete' || session.status === 'error')) {
|
|
1036
|
+
if (session.status === 'complete') {
|
|
1037
|
+
this.handleStreamingComplete({ sessionId: session.id, conversationId, timestamp: Date.now() });
|
|
1038
|
+
} else {
|
|
1039
|
+
this.handleStreamingError({ sessionId: session.id, conversationId, error: session.error || 'Unknown error', timestamp: Date.now() });
|
|
1040
|
+
}
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
return false;
|
|
1044
|
+
};
|
|
1015
1045
|
|
|
1016
1046
|
const pollOnce = async () => {
|
|
1017
1047
|
if (!pollState.isPolling) return;
|
|
1018
1048
|
|
|
1019
1049
|
try {
|
|
1020
1050
|
pollState.sessionCheckCounter++;
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (session.status === 'complete') {
|
|
1027
|
-
this.handleStreamingComplete({ sessionId: session.id, conversationId, timestamp: Date.now() });
|
|
1028
|
-
} else {
|
|
1029
|
-
this.handleStreamingError({ sessionId: session.id, conversationId, error: session.error || 'Unknown error', timestamp: Date.now() });
|
|
1030
|
-
}
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1051
|
+
const shouldCheckSession = pollState.sessionCheckCounter % 3 === 0 || pollState.emptyPollCount >= 3;
|
|
1052
|
+
if (shouldCheckSession) {
|
|
1053
|
+
const done = await checkSessionStatus();
|
|
1054
|
+
if (done) return;
|
|
1055
|
+
if (pollState.emptyPollCount >= 3) pollState.emptyPollCount = 0;
|
|
1034
1056
|
}
|
|
1035
1057
|
|
|
1036
1058
|
const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
|
|
1037
1059
|
|
|
1038
1060
|
if (chunks.length > 0) {
|
|
1039
1061
|
pollState.backoffDelay = 150;
|
|
1062
|
+
pollState.emptyPollCount = 0;
|
|
1040
1063
|
const lastChunk = chunks[chunks.length - 1];
|
|
1041
1064
|
pollState.lastFetchTimestamp = lastChunk.created_at;
|
|
1042
1065
|
this.renderChunkBatch(chunks.filter(c => c.block && c.block.type));
|
|
1043
1066
|
} else {
|
|
1067
|
+
pollState.emptyPollCount++;
|
|
1044
1068
|
pollState.backoffDelay = Math.min(pollState.backoffDelay + 50, 500);
|
|
1045
1069
|
}
|
|
1046
1070
|
|
|
@@ -1248,7 +1272,7 @@ class AgentGUIClient {
|
|
|
1248
1272
|
if (!convId) return;
|
|
1249
1273
|
const outputEl = document.getElementById('output');
|
|
1250
1274
|
if (!outputEl || !outputEl.firstChild) return;
|
|
1251
|
-
if (this.state.
|
|
1275
|
+
if (this.state.streamingConversations.has(convId)) return;
|
|
1252
1276
|
|
|
1253
1277
|
this.saveScrollPosition(convId);
|
|
1254
1278
|
const clone = outputEl.cloneNode(true);
|
|
@@ -1272,8 +1296,7 @@ class AgentGUIClient {
|
|
|
1272
1296
|
try {
|
|
1273
1297
|
this.cacheCurrentConversation();
|
|
1274
1298
|
this.stopChunkPolling();
|
|
1275
|
-
if (this.state.
|
|
1276
|
-
this.state.isStreaming = false;
|
|
1299
|
+
if (this.state.currentConversation?.id !== conversationId) {
|
|
1277
1300
|
this.state.currentSession = null;
|
|
1278
1301
|
}
|
|
1279
1302
|
|
|
@@ -1313,7 +1336,8 @@ class AgentGUIClient {
|
|
|
1313
1336
|
const userMessages = (allMessages || []).filter(m => m.role === 'user');
|
|
1314
1337
|
const hasMoreChunks = totalChunks && chunks.length < totalChunks;
|
|
1315
1338
|
|
|
1316
|
-
const
|
|
1339
|
+
const clientKnowsStreaming = this.state.streamingConversations.has(conversationId);
|
|
1340
|
+
const shouldResumeStreaming = (isActivelyStreaming || clientKnowsStreaming) && latestSession &&
|
|
1317
1341
|
(latestSession.status === 'active' || latestSession.status === 'pending');
|
|
1318
1342
|
|
|
1319
1343
|
const outputEl = document.getElementById('output');
|
|
@@ -1444,7 +1468,7 @@ class AgentGUIClient {
|
|
|
1444
1468
|
}
|
|
1445
1469
|
|
|
1446
1470
|
if (shouldResumeStreaming && latestSession) {
|
|
1447
|
-
this.state.
|
|
1471
|
+
this.state.streamingConversations.set(conversationId, true);
|
|
1448
1472
|
this.state.currentSession = {
|
|
1449
1473
|
id: latestSession.id,
|
|
1450
1474
|
conversationId: conversationId,
|
|
@@ -218,10 +218,11 @@ class ConversationManager {
|
|
|
218
218
|
const data = await res.json();
|
|
219
219
|
this.conversations = data.conversations || [];
|
|
220
220
|
|
|
221
|
-
// Seed streaming state from database isStreaming flag
|
|
222
221
|
for (const conv of this.conversations) {
|
|
223
222
|
if (conv.isStreaming === 1 || conv.isStreaming === true) {
|
|
224
223
|
this.streamingConversations.add(conv.id);
|
|
224
|
+
} else {
|
|
225
|
+
this.streamingConversations.delete(conv.id);
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -272,7 +273,7 @@ class ConversationManager {
|
|
|
272
273
|
const isActive = conv.id === this.activeId;
|
|
273
274
|
el.classList.toggle('active', isActive);
|
|
274
275
|
|
|
275
|
-
const isStreaming =
|
|
276
|
+
const isStreaming = this.streamingConversations.has(conv.id);
|
|
276
277
|
const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
|
|
277
278
|
const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
|
|
278
279
|
const agent = conv.agentType || 'unknown';
|
|
@@ -298,7 +299,7 @@ class ConversationManager {
|
|
|
298
299
|
li.dataset.convId = conv.id;
|
|
299
300
|
if (conv.id === this.activeId) li.classList.add('active');
|
|
300
301
|
|
|
301
|
-
const isStreaming =
|
|
302
|
+
const isStreaming = this.streamingConversations.has(conv.id);
|
|
302
303
|
|
|
303
304
|
const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
|
|
304
305
|
const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
|
|
@@ -26,6 +26,7 @@ class WebSocketManager {
|
|
|
26
26
|
this.requestMap = new Map();
|
|
27
27
|
this.heartbeatTimer = null;
|
|
28
28
|
this.connectionState = 'disconnected';
|
|
29
|
+
this.activeSubscriptions = new Set();
|
|
29
30
|
|
|
30
31
|
// Statistics
|
|
31
32
|
this.stats = {
|
|
@@ -124,11 +125,10 @@ class WebSocketManager {
|
|
|
124
125
|
|
|
125
126
|
// Flush buffered messages
|
|
126
127
|
this.flushMessageBuffer();
|
|
128
|
+
this.resubscribeAll();
|
|
127
129
|
|
|
128
|
-
// Start heartbeat
|
|
129
130
|
this.startHeartbeat();
|
|
130
131
|
|
|
131
|
-
// Emit connected event
|
|
132
132
|
this.emit('connected', { timestamp: Date.now() });
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -271,8 +271,15 @@ class WebSocketManager {
|
|
|
271
271
|
throw new Error('Invalid message data');
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
if (data.type === 'subscribe') {
|
|
275
|
+
const key = data.sessionId ? `session:${data.sessionId}` : `conv:${data.conversationId}`;
|
|
276
|
+
this.activeSubscriptions.add(key);
|
|
277
|
+
} else if (data.type === 'unsubscribe') {
|
|
278
|
+
const key = data.sessionId ? `session:${data.sessionId}` : `conv:${data.conversationId}`;
|
|
279
|
+
this.activeSubscriptions.delete(key);
|
|
280
|
+
}
|
|
281
|
+
|
|
274
282
|
if (!this.isConnected) {
|
|
275
|
-
// Buffer message if not connected
|
|
276
283
|
this.bufferMessage(data);
|
|
277
284
|
return false;
|
|
278
285
|
}
|
|
@@ -335,6 +342,19 @@ class WebSocketManager {
|
|
|
335
342
|
});
|
|
336
343
|
}
|
|
337
344
|
|
|
345
|
+
resubscribeAll() {
|
|
346
|
+
for (const key of this.activeSubscriptions) {
|
|
347
|
+
const [type, id] = key.split(':');
|
|
348
|
+
const msg = { type: 'subscribe', timestamp: Date.now() };
|
|
349
|
+
if (type === 'session') msg.sessionId = id;
|
|
350
|
+
else msg.conversationId = id;
|
|
351
|
+
try {
|
|
352
|
+
this.ws.send(JSON.stringify(msg));
|
|
353
|
+
this.stats.totalMessagesSent++;
|
|
354
|
+
} catch (_) {}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
338
358
|
/**
|
|
339
359
|
* Unsubscribe from streaming session
|
|
340
360
|
*/
|