agentgui 1.0.152 → 1.0.153

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.
@@ -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
- messagesEl.insertAdjacentHTML('beforeend', `<div class="message message-user" data-msg-id="${m.id}"><div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div></div>`);
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
- sList.forEach(chunk => { if (chunk.block?.type) { const el = this.renderer.renderBlock(chunk.block, chunk); if (el) bEl.appendChild(el); } });
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
- messagesEl.appendChild(mDiv);
482
+ priorFrag.appendChild(mDiv);
466
483
  });
467
484
  while (ui < userMsgs.length) {
468
485
  const m = userMsgs[ui++];
469
- messagesEl.insertAdjacentHTML('beforeend', `<div class="message message-user" data-msg-id="${m.id}"><div class="message-role">User</div>${this.renderMessageContent(m.content)}<div class="message-timestamp">${new Date(m.created_at).toLocaleString()}</div></div>`);
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.innerHTML = this.renderMessages(fullData.messages || []);
494
+ messagesEl.appendChild(this.renderMessagesFragment(fullData.messages || []));
473
495
  }
474
496
  }
475
497
  } catch (e) {
@@ -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 foldable = tn.startsWith('mcp__') || tn === 'Edit';
807
- if (foldable) {
808
- const dName = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.getToolDisplayName(tn) : tn;
809
- const tTitle = typeof StreamingRenderer !== 'undefined' && block.input ? StreamingRenderer.getToolTitle(tn, block.input) : '';
810
- html += `<details class="streaming-block-tool-use folded-tool"><summary class="folded-tool-bar"><span class="folded-tool-name">${this.escapeHtml(dName)}</span>${tTitle ? `<span class="folded-tool-desc">${this.escapeHtml(tTitle)}</span>` : ''}</summary>${inputHtml}</details>`;
811
- } else {
812
- html += `<div class="streaming-block-tool-use"><div class="tool-use-header"><span class="tool-use-icon">&#9881;</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.forEach(chunk => {
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
  */
@@ -1228,6 +1297,7 @@ class AgentGUIClient {
1228
1297
  sessionChunks[chunk.sessionId].push(chunk);
1229
1298
  });
1230
1299
 
1300
+ const frag = document.createDocumentFragment();
1231
1301
  let userMsgIdx = 0;
1232
1302
  sessionOrder.forEach((sessionId) => {
1233
1303
  const sessionChunkList = sessionChunks[sessionId];
@@ -1243,7 +1313,7 @@ class AgentGUIClient {
1243
1313
  ${this.renderMessageContent(msg.content)}
1244
1314
  <div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
1245
1315
  `;
1246
- messagesEl.appendChild(userDiv);
1316
+ frag.appendChild(userDiv);
1247
1317
  userMsgIdx++;
1248
1318
  }
1249
1319
 
@@ -1254,12 +1324,14 @@ class AgentGUIClient {
1254
1324
  messageDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
1255
1325
 
1256
1326
  const blocksEl = messageDiv.querySelector('.message-blocks');
1327
+ const blockFrag = document.createDocumentFragment();
1257
1328
  sessionChunkList.forEach(chunk => {
1258
1329
  if (chunk.block && chunk.block.type) {
1259
1330
  const element = this.renderer.renderBlock(chunk.block, chunk);
1260
- if (element) blocksEl.appendChild(element);
1331
+ if (element) blockFrag.appendChild(element);
1261
1332
  }
1262
1333
  });
1334
+ blocksEl.appendChild(blockFrag);
1263
1335
 
1264
1336
  if (isCurrentActiveSession) {
1265
1337
  const indicatorDiv = document.createElement('div');
@@ -1277,7 +1349,7 @@ class AgentGUIClient {
1277
1349
  messageDiv.appendChild(ts);
1278
1350
  }
1279
1351
 
1280
- messagesEl.appendChild(messageDiv);
1352
+ frag.appendChild(messageDiv);
1281
1353
  });
1282
1354
 
1283
1355
  while (userMsgIdx < userMessages.length) {
@@ -1290,11 +1362,12 @@ class AgentGUIClient {
1290
1362
  ${this.renderMessageContent(msg.content)}
1291
1363
  <div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
1292
1364
  `;
1293
- messagesEl.appendChild(userDiv);
1365
+ frag.appendChild(userDiv);
1294
1366
  userMsgIdx++;
1295
1367
  }
1368
+ messagesEl.appendChild(frag);
1296
1369
  } else {
1297
- messagesEl.innerHTML = this.renderMessages(allMessages || []);
1370
+ messagesEl.appendChild(this.renderMessagesFragment(allMessages || []));
1298
1371
  }
1299
1372
 
1300
1373
  if (shouldResumeStreaming && latestSession) {
@@ -1330,109 +1403,36 @@ class AgentGUIClient {
1330
1403
  }
1331
1404
  }
1332
1405
 
1333
- /**
1334
- * Render messages for display
1335
- */
1406
+ renderMessagesFragment(messages) {
1407
+ const frag = document.createDocumentFragment();
1408
+ if (messages.length === 0) {
1409
+ const p = document.createElement('p');
1410
+ p.className = 'text-secondary';
1411
+ p.textContent = 'No messages in this conversation yet';
1412
+ frag.appendChild(p);
1413
+ return frag;
1414
+ }
1415
+ for (const msg of messages) {
1416
+ const div = document.createElement('div');
1417
+ div.className = `message message-${msg.role}`;
1418
+ 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>`;
1419
+ frag.appendChild(div);
1420
+ }
1421
+ return frag;
1422
+ }
1423
+
1336
1424
  renderMessages(messages) {
1337
1425
  if (messages.length === 0) {
1338
1426
  return '<p class="text-secondary">No messages in this conversation yet</p>';
1339
1427
  }
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">&#9881;</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('');
1428
+ 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
1429
  }
1429
1430
 
1430
1431
  /**
1431
1432
  * Escape HTML to prevent XSS
1432
1433
  */
1433
1434
  escapeHtml(text) {
1434
- const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
1435
- return text.replace(/[&<>"']/g, c => map[c]);
1435
+ return window._escHtml(text);
1436
1436
  }
1437
1437
 
1438
1438
  /**
@@ -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
- sorted.forEach(conv => {
233
- const item = this.createConversationItem(conv);
234
- this.listEl.appendChild(item);
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
- const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
372
- return text.replace(/[&<>"']/g, c => map[c]);
420
+ return window._escHtml(text);
373
421
  }
374
422
  }
375
423
 
@@ -413,9 +413,7 @@ class EventProcessor {
413
413
  * HTML escape utility
414
414
  */
415
415
  escapeHtml(text) {
416
- const div = document.createElement('div');
417
- div.textContent = text;
418
- return div.innerHTML;
416
+ return window._escHtml(text);
419
417
  }
420
418
 
421
419
  /**