agentgui 1.0.152 → 1.0.154
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/database.js +25 -0
- package/lib/claude-runner.js +23 -2
- package/package.json +1 -1
- package/server.js +82 -11
- package/static/index.html +95 -33
- package/static/js/client.js +200 -127
- package/static/js/conversations.js +63 -15
- package/static/js/event-processor.js +1 -3
- package/static/js/streaming-renderer.js +130 -158
- package/static/js/syntax-highlighter.js +1 -3
- package/static/js/ui-components.js +1 -3
package/static/js/client.js
CHANGED
|
@@ -30,6 +30,10 @@ class AgentGUIClient {
|
|
|
30
30
|
agents: []
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
// Conversation DOM cache: store rendered DOM + scroll position per conversationId
|
|
34
|
+
this.conversationCache = new Map();
|
|
35
|
+
this.MAX_CACHE_SIZE = 10;
|
|
36
|
+
|
|
33
37
|
// Event handlers
|
|
34
38
|
this.eventHandlers = {};
|
|
35
39
|
|
|
@@ -368,6 +372,12 @@ class AgentGUIClient {
|
|
|
368
372
|
case 'queue_status':
|
|
369
373
|
this.handleQueueStatus(data);
|
|
370
374
|
break;
|
|
375
|
+
case 'rate_limit_hit':
|
|
376
|
+
this.handleRateLimitHit(data);
|
|
377
|
+
break;
|
|
378
|
+
case 'rate_limit_clear':
|
|
379
|
+
this.handleRateLimitClear(data);
|
|
380
|
+
break;
|
|
371
381
|
default:
|
|
372
382
|
break;
|
|
373
383
|
}
|
|
@@ -446,30 +456,42 @@ class AgentGUIClient {
|
|
|
446
456
|
if (!sessionGroups[c.sessionId]) { sessionGroups[c.sessionId] = []; sessionOrder.push(c.sessionId); }
|
|
447
457
|
sessionGroups[c.sessionId].push(c);
|
|
448
458
|
});
|
|
459
|
+
const priorFrag = document.createDocumentFragment();
|
|
449
460
|
let ui = 0;
|
|
450
461
|
sessionOrder.forEach(sid => {
|
|
451
462
|
const sList = sessionGroups[sid];
|
|
452
463
|
const sStart = sList[0].created_at;
|
|
453
464
|
while (ui < userMsgs.length && userMsgs[ui].created_at <= sStart) {
|
|
454
465
|
const m = userMsgs[ui++];
|
|
455
|
-
|
|
466
|
+
const uDiv = document.createElement('div');
|
|
467
|
+
uDiv.className = 'message message-user';
|
|
468
|
+
uDiv.setAttribute('data-msg-id', m.id);
|
|
469
|
+
uDiv.innerHTML = `<div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
|
|
470
|
+
priorFrag.appendChild(uDiv);
|
|
456
471
|
}
|
|
457
472
|
const mDiv = document.createElement('div');
|
|
458
473
|
mDiv.className = 'message message-assistant';
|
|
459
474
|
mDiv.id = `message-${sid}`;
|
|
460
475
|
mDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
|
|
461
476
|
const bEl = mDiv.querySelector('.message-blocks');
|
|
462
|
-
|
|
477
|
+
const bFrag = document.createDocumentFragment();
|
|
478
|
+
sList.forEach(chunk => { if (chunk.block?.type) { const el = this.renderer.renderBlock(chunk.block, chunk); if (el) bFrag.appendChild(el); } });
|
|
479
|
+
bEl.appendChild(bFrag);
|
|
463
480
|
const ts = document.createElement('div'); ts.className = 'message-timestamp'; ts.textContent = new Date(sList[sList.length - 1].created_at).toLocaleString();
|
|
464
481
|
mDiv.appendChild(ts);
|
|
465
|
-
|
|
482
|
+
priorFrag.appendChild(mDiv);
|
|
466
483
|
});
|
|
467
484
|
while (ui < userMsgs.length) {
|
|
468
485
|
const m = userMsgs[ui++];
|
|
469
|
-
|
|
486
|
+
const uDiv = document.createElement('div');
|
|
487
|
+
uDiv.className = 'message message-user';
|
|
488
|
+
uDiv.setAttribute('data-msg-id', m.id);
|
|
489
|
+
uDiv.innerHTML = `<div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div>`;
|
|
490
|
+
priorFrag.appendChild(uDiv);
|
|
470
491
|
}
|
|
492
|
+
messagesEl.appendChild(priorFrag);
|
|
471
493
|
} else {
|
|
472
|
-
messagesEl.
|
|
494
|
+
messagesEl.appendChild(this.renderMessagesFragment(fullData.messages || []));
|
|
473
495
|
}
|
|
474
496
|
}
|
|
475
497
|
} catch (e) {
|
|
@@ -587,8 +609,8 @@ class AgentGUIClient {
|
|
|
587
609
|
console.log('Streaming completed:', data);
|
|
588
610
|
|
|
589
611
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
612
|
+
if (conversationId) this.invalidateCache(conversationId);
|
|
590
613
|
|
|
591
|
-
// If this event is for a conversation we are NOT currently viewing, just track state
|
|
592
614
|
if (conversationId && this.state.currentConversation?.id !== conversationId) {
|
|
593
615
|
console.log('Streaming completed for non-active conversation:', conversationId);
|
|
594
616
|
this.emit('streaming:complete', data);
|
|
@@ -683,6 +705,37 @@ class AgentGUIClient {
|
|
|
683
705
|
}
|
|
684
706
|
}
|
|
685
707
|
|
|
708
|
+
handleRateLimitHit(data) {
|
|
709
|
+
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
710
|
+
this.state.isStreaming = false;
|
|
711
|
+
this.stopChunkPolling();
|
|
712
|
+
|
|
713
|
+
const sessionId = data.sessionId || this.state.currentSession?.id;
|
|
714
|
+
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
715
|
+
if (streamingEl) {
|
|
716
|
+
const indicator = streamingEl.querySelector('.streaming-indicator');
|
|
717
|
+
if (indicator) {
|
|
718
|
+
const retrySeconds = Math.ceil((data.retryAfterMs || 60000) / 1000);
|
|
719
|
+
indicator.innerHTML = `<span style="color:var(--color-warning);">Rate limited. Retrying in ${retrySeconds}s...</span>`;
|
|
720
|
+
let remaining = retrySeconds;
|
|
721
|
+
const countdownTimer = setInterval(() => {
|
|
722
|
+
remaining--;
|
|
723
|
+
if (remaining <= 0) {
|
|
724
|
+
clearInterval(countdownTimer);
|
|
725
|
+
indicator.innerHTML = '<span style="color:var(--color-info);">Restarting...</span>';
|
|
726
|
+
} else {
|
|
727
|
+
indicator.innerHTML = `<span style="color:var(--color-warning);">Rate limited. Retrying in ${remaining}s...</span>`;
|
|
728
|
+
}
|
|
729
|
+
}, 1000);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
handleRateLimitClear(data) {
|
|
735
|
+
if (data.conversationId !== this.state.currentConversation?.id) return;
|
|
736
|
+
this.enableControls();
|
|
737
|
+
}
|
|
738
|
+
|
|
686
739
|
isHtmlContent(text) {
|
|
687
740
|
const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
|
|
688
741
|
return htmlPattern.test(text);
|
|
@@ -803,14 +856,11 @@ class AgentGUIClient {
|
|
|
803
856
|
inputHtml = `<div class="folded-tool-body"><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></div>`;
|
|
804
857
|
}
|
|
805
858
|
const tn = block.name || 'unknown';
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
} else {
|
|
812
|
-
html += `<div class="streaming-block-tool-use"><div class="tool-use-header"><span class="tool-use-icon">⚙</span> <span class="tool-use-name">${this.escapeHtml(tn)}</span></div>${inputHtml}</div>`;
|
|
813
|
-
}
|
|
859
|
+
const hasRenderer = typeof StreamingRenderer !== 'undefined';
|
|
860
|
+
const dName = hasRenderer ? StreamingRenderer.getToolDisplayName(tn) : tn;
|
|
861
|
+
const tTitle = hasRenderer && block.input ? StreamingRenderer.getToolTitle(tn, block.input) : '';
|
|
862
|
+
const iconHtml = hasRenderer && this.renderer ? `<span class="folded-tool-icon">${this.renderer.getToolIcon(tn)}</span>` : '';
|
|
863
|
+
html += `<details class="folded-tool"><summary class="folded-tool-bar">${iconHtml}<span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}</details>`;
|
|
814
864
|
} else if (block.type === 'tool_result') {
|
|
815
865
|
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
816
866
|
const smartHtml = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.renderSmartContentHTML(content, this.escapeHtml.bind(this)) : `<pre class="tool-result-pre">${this.escapeHtml(content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content)}</pre>`;
|
|
@@ -989,9 +1039,7 @@ class AgentGUIClient {
|
|
|
989
1039
|
pollState.backoffDelay = 150;
|
|
990
1040
|
const lastChunk = chunks[chunks.length - 1];
|
|
991
1041
|
pollState.lastFetchTimestamp = lastChunk.created_at;
|
|
992
|
-
chunks.
|
|
993
|
-
if (chunk.block && chunk.block.type) this.renderChunk(chunk);
|
|
994
|
-
});
|
|
1042
|
+
this.renderChunkBatch(chunks.filter(c => c.block && c.block.type));
|
|
995
1043
|
} else {
|
|
996
1044
|
pollState.backoffDelay = Math.min(pollState.backoffDelay + 50, 500);
|
|
997
1045
|
}
|
|
@@ -1035,23 +1083,44 @@ class AgentGUIClient {
|
|
|
1035
1083
|
*/
|
|
1036
1084
|
renderChunk(chunk) {
|
|
1037
1085
|
if (!chunk || !chunk.block) return;
|
|
1038
|
-
|
|
1039
|
-
const sessionId = chunk.sessionId;
|
|
1040
|
-
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
1086
|
+
const streamingEl = document.getElementById(`streaming-${chunk.sessionId}`);
|
|
1041
1087
|
if (!streamingEl) return;
|
|
1042
|
-
|
|
1043
1088
|
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
1044
1089
|
if (!blocksEl) return;
|
|
1045
|
-
|
|
1046
|
-
const block = chunk.block;
|
|
1047
|
-
const element = this.renderer.renderBlock(block, chunk);
|
|
1048
|
-
|
|
1090
|
+
const element = this.renderer.renderBlock(chunk.block, chunk);
|
|
1049
1091
|
if (element) {
|
|
1050
1092
|
blocksEl.appendChild(element);
|
|
1051
1093
|
this.scrollToBottom();
|
|
1052
1094
|
}
|
|
1053
1095
|
}
|
|
1054
1096
|
|
|
1097
|
+
renderChunkBatch(chunks) {
|
|
1098
|
+
if (!chunks.length) return;
|
|
1099
|
+
const groups = {};
|
|
1100
|
+
for (const chunk of chunks) {
|
|
1101
|
+
const sid = chunk.sessionId;
|
|
1102
|
+
if (!groups[sid]) groups[sid] = [];
|
|
1103
|
+
groups[sid].push(chunk);
|
|
1104
|
+
}
|
|
1105
|
+
let appended = false;
|
|
1106
|
+
for (const sid of Object.keys(groups)) {
|
|
1107
|
+
const streamingEl = document.getElementById(`streaming-${sid}`);
|
|
1108
|
+
if (!streamingEl) continue;
|
|
1109
|
+
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
1110
|
+
if (!blocksEl) continue;
|
|
1111
|
+
const frag = document.createDocumentFragment();
|
|
1112
|
+
for (const chunk of groups[sid]) {
|
|
1113
|
+
const el = this.renderer.renderBlock(chunk.block, chunk);
|
|
1114
|
+
if (el) frag.appendChild(el);
|
|
1115
|
+
}
|
|
1116
|
+
if (frag.childNodes.length) {
|
|
1117
|
+
blocksEl.appendChild(frag);
|
|
1118
|
+
appended = true;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (appended) this.scrollToBottom();
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1055
1124
|
/**
|
|
1056
1125
|
* Load agents
|
|
1057
1126
|
*/
|
|
@@ -1174,11 +1243,34 @@ class AgentGUIClient {
|
|
|
1174
1243
|
}
|
|
1175
1244
|
}
|
|
1176
1245
|
|
|
1246
|
+
cacheCurrentConversation() {
|
|
1247
|
+
const convId = this.state.currentConversation?.id;
|
|
1248
|
+
if (!convId) return;
|
|
1249
|
+
const outputEl = document.getElementById('output');
|
|
1250
|
+
if (!outputEl || !outputEl.firstChild) return;
|
|
1251
|
+
if (this.state.isStreaming) return;
|
|
1252
|
+
|
|
1253
|
+
this.saveScrollPosition(convId);
|
|
1254
|
+
const clone = outputEl.cloneNode(true);
|
|
1255
|
+
this.conversationCache.set(convId, {
|
|
1256
|
+
dom: clone,
|
|
1257
|
+
conversation: this.state.currentConversation,
|
|
1258
|
+
timestamp: Date.now()
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
if (this.conversationCache.size > this.MAX_CACHE_SIZE) {
|
|
1262
|
+
const oldest = this.conversationCache.keys().next().value;
|
|
1263
|
+
this.conversationCache.delete(oldest);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
invalidateCache(conversationId) {
|
|
1268
|
+
this.conversationCache.delete(conversationId);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1177
1271
|
async loadConversationMessages(conversationId) {
|
|
1178
1272
|
try {
|
|
1179
|
-
|
|
1180
|
-
this.saveScrollPosition(this.state.currentConversation.id);
|
|
1181
|
-
}
|
|
1273
|
+
this.cacheCurrentConversation();
|
|
1182
1274
|
this.stopChunkPolling();
|
|
1183
1275
|
if (this.state.isStreaming && this.state.currentConversation?.id !== conversationId) {
|
|
1184
1276
|
this.state.isStreaming = false;
|
|
@@ -1190,9 +1282,27 @@ class AgentGUIClient {
|
|
|
1190
1282
|
this.wsManager.sendMessage({ type: 'subscribe', conversationId });
|
|
1191
1283
|
}
|
|
1192
1284
|
|
|
1285
|
+
const cached = this.conversationCache.get(conversationId);
|
|
1286
|
+
if (cached && (Date.now() - cached.timestamp) < 120000) {
|
|
1287
|
+
const outputEl = document.getElementById('output');
|
|
1288
|
+
if (outputEl) {
|
|
1289
|
+
outputEl.innerHTML = '';
|
|
1290
|
+
while (cached.dom.firstChild) {
|
|
1291
|
+
outputEl.appendChild(cached.dom.firstChild);
|
|
1292
|
+
}
|
|
1293
|
+
this.state.currentConversation = cached.conversation;
|
|
1294
|
+
this.conversationCache.delete(conversationId);
|
|
1295
|
+
this.restoreScrollPosition(conversationId);
|
|
1296
|
+
this.enableControls();
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
this.conversationCache.delete(conversationId);
|
|
1302
|
+
|
|
1193
1303
|
const resp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full`);
|
|
1194
1304
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
1195
|
-
const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, messages: allMessages } = await resp.json();
|
|
1305
|
+
const { conversation, isActivelyStreaming, latestSession, chunks: rawChunks, totalChunks, messages: allMessages } = await resp.json();
|
|
1196
1306
|
|
|
1197
1307
|
this.state.currentConversation = conversation;
|
|
1198
1308
|
|
|
@@ -1201,6 +1311,7 @@ class AgentGUIClient {
|
|
|
1201
1311
|
block: typeof chunk.data === 'string' ? JSON.parse(chunk.data) : chunk.data
|
|
1202
1312
|
}));
|
|
1203
1313
|
const userMessages = (allMessages || []).filter(m => m.role === 'user');
|
|
1314
|
+
const hasMoreChunks = totalChunks && chunks.length < totalChunks;
|
|
1204
1315
|
|
|
1205
1316
|
const shouldResumeStreaming = isActivelyStreaming && latestSession &&
|
|
1206
1317
|
(latestSession.status === 'active' || latestSession.status === 'pending');
|
|
@@ -1217,6 +1328,29 @@ class AgentGUIClient {
|
|
|
1217
1328
|
`;
|
|
1218
1329
|
|
|
1219
1330
|
const messagesEl = outputEl.querySelector('.conversation-messages');
|
|
1331
|
+
|
|
1332
|
+
if (hasMoreChunks) {
|
|
1333
|
+
const loadMoreBtn = document.createElement('button');
|
|
1334
|
+
loadMoreBtn.className = 'btn btn-secondary';
|
|
1335
|
+
loadMoreBtn.style.cssText = 'width:100%;margin-bottom:1rem;padding:0.5rem;font-size:0.8rem;';
|
|
1336
|
+
loadMoreBtn.textContent = `Load earlier messages (${totalChunks - chunks.length} more chunks)`;
|
|
1337
|
+
loadMoreBtn.addEventListener('click', async () => {
|
|
1338
|
+
loadMoreBtn.disabled = true;
|
|
1339
|
+
loadMoreBtn.textContent = 'Loading...';
|
|
1340
|
+
try {
|
|
1341
|
+
const fullResp = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}/full?allChunks=1`);
|
|
1342
|
+
if (fullResp.ok) {
|
|
1343
|
+
this.invalidateCache(conversationId);
|
|
1344
|
+
await this.loadConversationMessages(conversationId);
|
|
1345
|
+
}
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
loadMoreBtn.textContent = 'Failed to load. Try again.';
|
|
1348
|
+
loadMoreBtn.disabled = false;
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
messagesEl.appendChild(loadMoreBtn);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1220
1354
|
if (chunks.length > 0) {
|
|
1221
1355
|
const sessionOrder = [];
|
|
1222
1356
|
const sessionChunks = {};
|
|
@@ -1228,7 +1362,16 @@ class AgentGUIClient {
|
|
|
1228
1362
|
sessionChunks[chunk.sessionId].push(chunk);
|
|
1229
1363
|
});
|
|
1230
1364
|
|
|
1365
|
+
const frag = document.createDocumentFragment();
|
|
1231
1366
|
let userMsgIdx = 0;
|
|
1367
|
+
|
|
1368
|
+
if (hasMoreChunks && sessionOrder.length > 0) {
|
|
1369
|
+
const firstChunkTime = chunks[0].created_at;
|
|
1370
|
+
while (userMsgIdx < userMessages.length && userMessages[userMsgIdx].created_at < firstChunkTime) {
|
|
1371
|
+
userMsgIdx++;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1232
1375
|
sessionOrder.forEach((sessionId) => {
|
|
1233
1376
|
const sessionChunkList = sessionChunks[sessionId];
|
|
1234
1377
|
const sessionStart = sessionChunkList[0].created_at;
|
|
@@ -1243,7 +1386,7 @@ class AgentGUIClient {
|
|
|
1243
1386
|
${this.renderMessageContent(msg.content)}
|
|
1244
1387
|
<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
|
|
1245
1388
|
`;
|
|
1246
|
-
|
|
1389
|
+
frag.appendChild(userDiv);
|
|
1247
1390
|
userMsgIdx++;
|
|
1248
1391
|
}
|
|
1249
1392
|
|
|
@@ -1254,12 +1397,14 @@ class AgentGUIClient {
|
|
|
1254
1397
|
messageDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
|
|
1255
1398
|
|
|
1256
1399
|
const blocksEl = messageDiv.querySelector('.message-blocks');
|
|
1400
|
+
const blockFrag = document.createDocumentFragment();
|
|
1257
1401
|
sessionChunkList.forEach(chunk => {
|
|
1258
1402
|
if (chunk.block && chunk.block.type) {
|
|
1259
1403
|
const element = this.renderer.renderBlock(chunk.block, chunk);
|
|
1260
|
-
if (element)
|
|
1404
|
+
if (element) blockFrag.appendChild(element);
|
|
1261
1405
|
}
|
|
1262
1406
|
});
|
|
1407
|
+
blocksEl.appendChild(blockFrag);
|
|
1263
1408
|
|
|
1264
1409
|
if (isCurrentActiveSession) {
|
|
1265
1410
|
const indicatorDiv = document.createElement('div');
|
|
@@ -1277,7 +1422,7 @@ class AgentGUIClient {
|
|
|
1277
1422
|
messageDiv.appendChild(ts);
|
|
1278
1423
|
}
|
|
1279
1424
|
|
|
1280
|
-
|
|
1425
|
+
frag.appendChild(messageDiv);
|
|
1281
1426
|
});
|
|
1282
1427
|
|
|
1283
1428
|
while (userMsgIdx < userMessages.length) {
|
|
@@ -1290,11 +1435,12 @@ class AgentGUIClient {
|
|
|
1290
1435
|
${this.renderMessageContent(msg.content)}
|
|
1291
1436
|
<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
|
|
1292
1437
|
`;
|
|
1293
|
-
|
|
1438
|
+
frag.appendChild(userDiv);
|
|
1294
1439
|
userMsgIdx++;
|
|
1295
1440
|
}
|
|
1441
|
+
messagesEl.appendChild(frag);
|
|
1296
1442
|
} else {
|
|
1297
|
-
messagesEl.
|
|
1443
|
+
messagesEl.appendChild(this.renderMessagesFragment(allMessages || []));
|
|
1298
1444
|
}
|
|
1299
1445
|
|
|
1300
1446
|
if (shouldResumeStreaming && latestSession) {
|
|
@@ -1330,109 +1476,36 @@ class AgentGUIClient {
|
|
|
1330
1476
|
}
|
|
1331
1477
|
}
|
|
1332
1478
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1479
|
+
renderMessagesFragment(messages) {
|
|
1480
|
+
const frag = document.createDocumentFragment();
|
|
1481
|
+
if (messages.length === 0) {
|
|
1482
|
+
const p = document.createElement('p');
|
|
1483
|
+
p.className = 'text-secondary';
|
|
1484
|
+
p.textContent = 'No messages in this conversation yet';
|
|
1485
|
+
frag.appendChild(p);
|
|
1486
|
+
return frag;
|
|
1487
|
+
}
|
|
1488
|
+
for (const msg of messages) {
|
|
1489
|
+
const div = document.createElement('div');
|
|
1490
|
+
div.className = `message message-${msg.role}`;
|
|
1491
|
+
div.innerHTML = `<div class="message-role">${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}</div>${this.renderMessageContent(msg.content)}<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>`;
|
|
1492
|
+
frag.appendChild(div);
|
|
1493
|
+
}
|
|
1494
|
+
return frag;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1336
1497
|
renderMessages(messages) {
|
|
1337
1498
|
if (messages.length === 0) {
|
|
1338
1499
|
return '<p class="text-secondary">No messages in this conversation yet</p>';
|
|
1339
1500
|
}
|
|
1340
|
-
|
|
1341
|
-
return messages.map(msg => {
|
|
1342
|
-
let contentHtml = '';
|
|
1343
|
-
|
|
1344
|
-
if (typeof msg.content === 'string') {
|
|
1345
|
-
if (this.isHtmlContent(msg.content)) {
|
|
1346
|
-
contentHtml = `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${this.sanitizeHtml(msg.content)}</div></div>`;
|
|
1347
|
-
} else {
|
|
1348
|
-
contentHtml = `<div class="message-text">${this.escapeHtml(msg.content)}</div>`;
|
|
1349
|
-
}
|
|
1350
|
-
} else if (msg.content && typeof msg.content === 'object' && msg.content.type === 'claude_execution') {
|
|
1351
|
-
contentHtml = '<div class="message-blocks">';
|
|
1352
|
-
if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
|
|
1353
|
-
msg.content.blocks.forEach(block => {
|
|
1354
|
-
if (block.type === 'text') {
|
|
1355
|
-
const parts = this.parseMarkdownCodeBlocks(block.text);
|
|
1356
|
-
parts.forEach(part => {
|
|
1357
|
-
if (part.type === 'html') {
|
|
1358
|
-
contentHtml += `<div class="message-text"><div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">${part.content}</div></div>`;
|
|
1359
|
-
} else if (part.type === 'text') {
|
|
1360
|
-
contentHtml += `<div class="message-text">${this.escapeHtml(part.content)}</div>`;
|
|
1361
|
-
} else if (part.type === 'code') {
|
|
1362
|
-
contentHtml += this.renderCodeBlock(part.language, part.code);
|
|
1363
|
-
}
|
|
1364
|
-
});
|
|
1365
|
-
} else if (block.type === 'code_block') {
|
|
1366
|
-
// Render HTML code blocks as actual HTML elements
|
|
1367
|
-
if (block.language === 'html') {
|
|
1368
|
-
contentHtml += `
|
|
1369
|
-
<div class="message-code">
|
|
1370
|
-
<div class="html-rendered-label mb-2 p-2 bg-blue-50 dark:bg-blue-900 rounded border border-blue-200 dark:border-blue-700 text-xs text-blue-700 dark:text-blue-300">
|
|
1371
|
-
Rendered HTML
|
|
1372
|
-
</div>
|
|
1373
|
-
<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
|
|
1374
|
-
${block.code}
|
|
1375
|
-
</div>
|
|
1376
|
-
</div>
|
|
1377
|
-
`;
|
|
1378
|
-
} else {
|
|
1379
|
-
const cBlkLineCount = block.code.split('\n').length;
|
|
1380
|
-
contentHtml += `<div class="message-code"><details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(block.language || 'code')} - ${cBlkLineCount} line${cBlkLineCount !== 1 ? 's' : ''}</summary><pre style="margin:0;border-radius:0 0 0.375rem 0.375rem">${this.escapeHtml(block.code)}</pre></details></div>`;
|
|
1381
|
-
}
|
|
1382
|
-
} else if (block.type === 'tool_use') {
|
|
1383
|
-
let inputHtml = '';
|
|
1384
|
-
if (block.input && Object.keys(block.input).length > 0) {
|
|
1385
|
-
const inputStr = JSON.stringify(block.input, null, 2);
|
|
1386
|
-
inputHtml = `<div class="folded-tool-body"><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></div>`;
|
|
1387
|
-
}
|
|
1388
|
-
const tn2 = block.name || 'unknown';
|
|
1389
|
-
const foldable2 = tn2.startsWith('mcp__') || tn2 === 'Edit';
|
|
1390
|
-
if (foldable2) {
|
|
1391
|
-
const dName2 = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.getToolDisplayName(tn2) : tn2;
|
|
1392
|
-
const tTitle2 = typeof StreamingRenderer !== 'undefined' && block.input ? StreamingRenderer.getToolTitle(tn2, block.input) : '';
|
|
1393
|
-
contentHtml += `<details class="streaming-block-tool-use folded-tool"><summary class="folded-tool-bar"><span class="folded-tool-name">${this.escapeHtml(dName2)}</span>${tTitle2 ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle2)}</span>` : ''}</summary>${inputHtml}</details>`;
|
|
1394
|
-
} else {
|
|
1395
|
-
contentHtml += `<div class="streaming-block-tool-use"><div class="tool-use-header"><span class="tool-use-icon">⚙</span> <span class="tool-use-name">${this.escapeHtml(tn2)}</span></div>${inputHtml}</div>`;
|
|
1396
|
-
}
|
|
1397
|
-
} else if (block.type === 'tool_result') {
|
|
1398
|
-
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
1399
|
-
const smartHtml = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.renderSmartContentHTML(content, this.escapeHtml.bind(this)) : `<pre class="tool-result-pre">${this.escapeHtml(content.length > 2000 ? content.substring(0, 2000) + '\n... (truncated)' : content)}</pre>`;
|
|
1400
|
-
contentHtml += `<div class="streaming-block-tool-result${block.is_error ? ' tool-result-error' : ''}"><div class="tool-result-header">${block.is_error ? '<span class="tool-result-error-badge">Error</span>' : '<span class="tool-result-ok-badge">Result</span>'}</div>${smartHtml}</div>`;
|
|
1401
|
-
}
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
contentHtml += '</div>';
|
|
1405
|
-
} else {
|
|
1406
|
-
// Fallback for non-array msg.content: format as key-value pairs
|
|
1407
|
-
if (typeof msg.content === 'object' && msg.content !== null) {
|
|
1408
|
-
const fieldsHtml = Object.entries(msg.content)
|
|
1409
|
-
.map(([key, value]) => {
|
|
1410
|
-
let displayValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
1411
|
-
if (displayValue.length > 150) displayValue = displayValue.substring(0, 150) + '...';
|
|
1412
|
-
return `<div style="font-size:0.8rem;margin-bottom:0.375rem"><span style="font-weight:600">${this.escapeHtml(key)}:</span> <code style="background:var(--color-bg-secondary);padding:0.125rem 0.25rem;border-radius:0.25rem">${this.escapeHtml(displayValue)}</code></div>`;
|
|
1413
|
-
}).join('');
|
|
1414
|
-
contentHtml = `<div class="message-text" style="background:var(--color-bg-secondary);padding:0.75rem;border-radius:0.375rem">${fieldsHtml}</div>`;
|
|
1415
|
-
} else {
|
|
1416
|
-
contentHtml = `<div class="message-text">${this.escapeHtml(String(msg.content))}</div>`;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
return `
|
|
1421
|
-
<div class="message message-${msg.role}">
|
|
1422
|
-
<div class="message-role">${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}</div>
|
|
1423
|
-
${contentHtml}
|
|
1424
|
-
<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
|
|
1425
|
-
</div>
|
|
1426
|
-
`;
|
|
1427
|
-
}).join('');
|
|
1501
|
+
return messages.map(msg => `<div class="message message-${msg.role}"><div class="message-role">${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}</div>${this.renderMessageContent(msg.content)}<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div></div>`).join('');
|
|
1428
1502
|
}
|
|
1429
1503
|
|
|
1430
1504
|
/**
|
|
1431
1505
|
* Escape HTML to prevent XSS
|
|
1432
1506
|
*/
|
|
1433
1507
|
escapeHtml(text) {
|
|
1434
|
-
|
|
1435
|
-
return text.replace(/[&<>"']/g, c => map[c]);
|
|
1508
|
+
return window._escHtml(text);
|
|
1436
1509
|
}
|
|
1437
1510
|
|
|
1438
1511
|
/**
|
|
@@ -31,6 +31,7 @@ class ConversationManager {
|
|
|
31
31
|
|
|
32
32
|
async init() {
|
|
33
33
|
this.newBtn?.addEventListener('click', () => this.openFolderBrowser());
|
|
34
|
+
this.setupDelegatedListeners();
|
|
34
35
|
this.loadConversations();
|
|
35
36
|
this.setupWebSocketListener();
|
|
36
37
|
this.setupFolderBrowser();
|
|
@@ -38,6 +39,23 @@ class ConversationManager {
|
|
|
38
39
|
setInterval(() => this.loadConversations(), 30000);
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
setupDelegatedListeners() {
|
|
43
|
+
this.listEl.addEventListener('click', (e) => {
|
|
44
|
+
const deleteBtn = e.target.closest('[data-delete-conv]');
|
|
45
|
+
if (deleteBtn) {
|
|
46
|
+
e.stopPropagation();
|
|
47
|
+
const convId = deleteBtn.dataset.deleteConv;
|
|
48
|
+
const conv = this.conversations.find(c => c.id === convId);
|
|
49
|
+
this.confirmDelete(convId, conv?.title || 'Untitled');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const item = e.target.closest('[data-conv-id]');
|
|
53
|
+
if (item) {
|
|
54
|
+
this.select(item.dataset.convId);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
setupFolderBrowser() {
|
|
42
60
|
this.folderBrowser.modal = document.getElementById('folderBrowserModal');
|
|
43
61
|
this.folderBrowser.listEl = document.getElementById('folderList');
|
|
@@ -222,17 +240,56 @@ class ConversationManager {
|
|
|
222
240
|
return;
|
|
223
241
|
}
|
|
224
242
|
|
|
225
|
-
this.listEl.innerHTML = '';
|
|
226
243
|
this.emptyEl.style.display = 'none';
|
|
227
244
|
|
|
228
245
|
const sorted = [...this.conversations].sort((a, b) =>
|
|
229
246
|
new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
|
|
230
247
|
);
|
|
231
248
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
249
|
+
const existingMap = {};
|
|
250
|
+
for (const child of Array.from(this.listEl.children)) {
|
|
251
|
+
const cid = child.dataset.convId;
|
|
252
|
+
if (cid) existingMap[cid] = child;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const frag = document.createDocumentFragment();
|
|
256
|
+
for (const conv of sorted) {
|
|
257
|
+
const existing = existingMap[conv.id];
|
|
258
|
+
if (existing) {
|
|
259
|
+
this.updateConversationItem(existing, conv);
|
|
260
|
+
delete existingMap[conv.id];
|
|
261
|
+
frag.appendChild(existing);
|
|
262
|
+
} else {
|
|
263
|
+
frag.appendChild(this.createConversationItem(conv));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const orphan of Object.values(existingMap)) orphan.remove();
|
|
268
|
+
this.listEl.appendChild(frag);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
updateConversationItem(el, conv) {
|
|
272
|
+
const isActive = conv.id === this.activeId;
|
|
273
|
+
el.classList.toggle('active', isActive);
|
|
274
|
+
|
|
275
|
+
const isStreaming = conv.isStreaming === 1 || conv.isStreaming === true || this.streamingConversations?.has(conv.id);
|
|
276
|
+
const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
|
|
277
|
+
const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
|
|
278
|
+
const agent = conv.agentType || 'unknown';
|
|
279
|
+
const wd = conv.workingDirectory ? conv.workingDirectory.split('/').pop() : '';
|
|
280
|
+
const metaParts = [agent, timestamp];
|
|
281
|
+
if (wd) metaParts.push(wd);
|
|
282
|
+
|
|
283
|
+
const titleEl = el.querySelector('.conversation-item-title');
|
|
284
|
+
if (titleEl) {
|
|
285
|
+
const badgeHtml = isStreaming
|
|
286
|
+
? '<span class="conversation-streaming-badge" title="Streaming in progress"><span class="streaming-dot"></span></span>'
|
|
287
|
+
: '';
|
|
288
|
+
titleEl.innerHTML = `${badgeHtml}${this.escapeHtml(title)}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const metaEl = el.querySelector('.conversation-item-meta');
|
|
292
|
+
if (metaEl) metaEl.textContent = metaParts.join(' \u2022 ');
|
|
236
293
|
}
|
|
237
294
|
|
|
238
295
|
createConversationItem(conv) {
|
|
@@ -267,14 +324,6 @@ class ConversationManager {
|
|
|
267
324
|
</button>
|
|
268
325
|
`;
|
|
269
326
|
|
|
270
|
-
// Handle delete button click
|
|
271
|
-
const deleteBtn = li.querySelector('[data-delete-conv]');
|
|
272
|
-
deleteBtn.addEventListener('click', (e) => {
|
|
273
|
-
e.stopPropagation();
|
|
274
|
-
this.confirmDelete(conv.id, title);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
li.addEventListener('click', () => this.select(conv.id));
|
|
278
327
|
return li;
|
|
279
328
|
}
|
|
280
329
|
|
|
@@ -368,8 +417,7 @@ class ConversationManager {
|
|
|
368
417
|
}
|
|
369
418
|
|
|
370
419
|
escapeHtml(text) {
|
|
371
|
-
|
|
372
|
-
return text.replace(/[&<>"']/g, c => map[c]);
|
|
420
|
+
return window._escHtml(text);
|
|
373
421
|
}
|
|
374
422
|
}
|
|
375
423
|
|