agentgui 1.0.151 → 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.
- package/database.js +112 -66
- package/lib/claude-runner.js +23 -2
- package/package.json +1 -1
- package/server.js +326 -129
- package/static/index.html +95 -33
- package/static/js/client.js +129 -128
- 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/websocket-manager.js +16 -27
- package/static/styles.css +0 -1989
package/static/index.html
CHANGED
|
@@ -527,39 +527,6 @@
|
|
|
527
527
|
font-size: 0.75rem;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
.streaming-block-tool-use {
|
|
531
|
-
margin: 0.25rem 0;
|
|
532
|
-
border-left: 3px solid #06b6d4;
|
|
533
|
-
background: rgba(6,182,212,0.06);
|
|
534
|
-
border-radius: 0 0.375rem 0.375rem 0;
|
|
535
|
-
overflow: hidden;
|
|
536
|
-
}
|
|
537
|
-
.streaming-block-tool-use.folded-tool {
|
|
538
|
-
border-left: none;
|
|
539
|
-
border-radius: 0.375rem;
|
|
540
|
-
margin: 0.125rem 0;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
.tool-use-header {
|
|
544
|
-
padding: 0.5rem 0.75rem;
|
|
545
|
-
font-weight: 600;
|
|
546
|
-
font-size: 0.85rem;
|
|
547
|
-
display: flex;
|
|
548
|
-
align-items: center;
|
|
549
|
-
gap: 0.375rem;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
.tool-use-icon {
|
|
553
|
-
font-size: 0.9rem;
|
|
554
|
-
opacity: 0.7;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
.tool-use-name {
|
|
558
|
-
color: #0891b2;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
html.dark .tool-use-name { color: #22d3ee; }
|
|
562
|
-
|
|
563
530
|
.tool-input-details {
|
|
564
531
|
}
|
|
565
532
|
|
|
@@ -1102,6 +1069,12 @@
|
|
|
1102
1069
|
opacity: 0.3;
|
|
1103
1070
|
}
|
|
1104
1071
|
|
|
1072
|
+
.conversation-messages { contain: content; }
|
|
1073
|
+
.streaming-blocks { contain: content; }
|
|
1074
|
+
.sidebar-list { contain: strict; content-visibility: auto; }
|
|
1075
|
+
.message { contain: layout style; content-visibility: auto; contain-intrinsic-size: auto 120px; }
|
|
1076
|
+
#output-scroll { will-change: transform; }
|
|
1077
|
+
|
|
1105
1078
|
.voice-block .voice-result-stats {
|
|
1106
1079
|
font-size: 0.8rem;
|
|
1107
1080
|
color: var(--color-text-secondary);
|
|
@@ -1585,6 +1558,90 @@
|
|
|
1585
1558
|
font-size: 0.7rem;
|
|
1586
1559
|
}
|
|
1587
1560
|
|
|
1561
|
+
/* --- Error variant of folded-tool --- */
|
|
1562
|
+
.folded-tool.folded-tool-error {
|
|
1563
|
+
background: #fef2f2;
|
|
1564
|
+
border-color: #fecaca;
|
|
1565
|
+
}
|
|
1566
|
+
html.dark .folded-tool.folded-tool-error {
|
|
1567
|
+
background: #1c0f0f;
|
|
1568
|
+
border-color: #7f1d1d;
|
|
1569
|
+
}
|
|
1570
|
+
.folded-tool-error > .folded-tool-bar {
|
|
1571
|
+
background: #fee2e2;
|
|
1572
|
+
}
|
|
1573
|
+
html.dark .folded-tool-error > .folded-tool-bar {
|
|
1574
|
+
background: #2c1010;
|
|
1575
|
+
}
|
|
1576
|
+
.folded-tool-error > .folded-tool-bar::before { color: #ef4444; }
|
|
1577
|
+
html.dark .folded-tool-error > .folded-tool-bar::before { color: #f87171; }
|
|
1578
|
+
.folded-tool-error > .folded-tool-bar:hover { background: #fecaca; }
|
|
1579
|
+
html.dark .folded-tool-error > .folded-tool-bar:hover { background: #451a1a; }
|
|
1580
|
+
.folded-tool-error .folded-tool-icon { color: #ef4444; }
|
|
1581
|
+
html.dark .folded-tool-error .folded-tool-icon { color: #f87171; }
|
|
1582
|
+
.folded-tool-error .folded-tool-name { color: #991b1b; }
|
|
1583
|
+
html.dark .folded-tool-error .folded-tool-name { color: #fca5a5; }
|
|
1584
|
+
.folded-tool-error .folded-tool-desc { color: #b91c1c; }
|
|
1585
|
+
html.dark .folded-tool-error .folded-tool-desc { color: #f87171; }
|
|
1586
|
+
.folded-tool-error > .folded-tool-body { border-top-color: #fecaca; }
|
|
1587
|
+
html.dark .folded-tool-error > .folded-tool-body { border-top-color: #7f1d1d; }
|
|
1588
|
+
|
|
1589
|
+
/* --- Collapsible Code Summary --- */
|
|
1590
|
+
.collapsible-code {
|
|
1591
|
+
margin: 0.25rem 0;
|
|
1592
|
+
border-radius: 0.375rem;
|
|
1593
|
+
overflow: hidden;
|
|
1594
|
+
background: #1e293b;
|
|
1595
|
+
border: 1px solid #334155;
|
|
1596
|
+
}
|
|
1597
|
+
.collapsible-code-summary {
|
|
1598
|
+
display: flex;
|
|
1599
|
+
align-items: center;
|
|
1600
|
+
gap: 0.5rem;
|
|
1601
|
+
padding: 0.3rem 0.75rem;
|
|
1602
|
+
cursor: pointer;
|
|
1603
|
+
user-select: none;
|
|
1604
|
+
list-style: none;
|
|
1605
|
+
font-size: 0.75rem;
|
|
1606
|
+
line-height: 1.3;
|
|
1607
|
+
background: #1f2937;
|
|
1608
|
+
color: #9ca3af;
|
|
1609
|
+
font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
|
|
1610
|
+
transition: background 0.15s;
|
|
1611
|
+
}
|
|
1612
|
+
.collapsible-code-summary::-webkit-details-marker { display: none; }
|
|
1613
|
+
.collapsible-code-summary::marker { display: none; content: ''; }
|
|
1614
|
+
.collapsible-code-summary::before {
|
|
1615
|
+
content: '\25b6';
|
|
1616
|
+
font-size: 0.5rem;
|
|
1617
|
+
margin-right: 0.125rem;
|
|
1618
|
+
display: inline-block;
|
|
1619
|
+
transition: transform 0.15s;
|
|
1620
|
+
color: #6b7280;
|
|
1621
|
+
flex-shrink: 0;
|
|
1622
|
+
}
|
|
1623
|
+
.collapsible-code[open] > .collapsible-code-summary::before { transform: rotate(90deg); }
|
|
1624
|
+
.collapsible-code-summary:hover { background: #374151; }
|
|
1625
|
+
.collapsible-code-summary .copy-code-btn {
|
|
1626
|
+
margin-left: auto;
|
|
1627
|
+
background: none;
|
|
1628
|
+
border: none;
|
|
1629
|
+
color: #6b7280;
|
|
1630
|
+
cursor: pointer;
|
|
1631
|
+
padding: 0.125rem;
|
|
1632
|
+
border-radius: 0.25rem;
|
|
1633
|
+
display: flex;
|
|
1634
|
+
align-items: center;
|
|
1635
|
+
transition: all 0.15s;
|
|
1636
|
+
}
|
|
1637
|
+
.collapsible-code-summary .copy-code-btn:hover { color: #e5e7eb; background: #4b5563; }
|
|
1638
|
+
.collapsible-code-summary .copy-code-btn svg { width: 0.875rem; height: 0.875rem; }
|
|
1639
|
+
.collapsible-code-label {
|
|
1640
|
+
font-size: 0.7rem;
|
|
1641
|
+
text-transform: uppercase;
|
|
1642
|
+
letter-spacing: 0.05em;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1588
1645
|
/* --- Tool Result Block --- */
|
|
1589
1646
|
.block-tool-result {
|
|
1590
1647
|
margin-bottom: 0.25rem;
|
|
@@ -2077,6 +2134,11 @@
|
|
|
2077
2134
|
</div>
|
|
2078
2135
|
</div>
|
|
2079
2136
|
|
|
2137
|
+
<script>
|
|
2138
|
+
var _escHtmlMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
2139
|
+
var _escHtmlRe = /[&<>"']/g;
|
|
2140
|
+
window._escHtml = function(t) { return t.replace(_escHtmlRe, function(c) { return _escHtmlMap[c]; }); };
|
|
2141
|
+
</script>
|
|
2080
2142
|
<script defer src="/gm/js/event-processor.js"></script>
|
|
2081
2143
|
<script defer src="/gm/js/streaming-renderer.js"></script>
|
|
2082
2144
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
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) {
|
|
@@ -544,12 +566,13 @@ class AgentGUIClient {
|
|
|
544
566
|
}
|
|
545
567
|
|
|
546
568
|
scrollToBottom() {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
569
|
+
if (this._scrollRafPending) return;
|
|
570
|
+
this._scrollRafPending = true;
|
|
571
|
+
requestAnimationFrame(() => {
|
|
572
|
+
this._scrollRafPending = false;
|
|
573
|
+
const scrollContainer = document.getElementById('output-scroll');
|
|
574
|
+
if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
575
|
+
});
|
|
553
576
|
}
|
|
554
577
|
|
|
555
578
|
handleStreamingError(data) {
|
|
@@ -682,6 +705,37 @@ class AgentGUIClient {
|
|
|
682
705
|
}
|
|
683
706
|
}
|
|
684
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
|
+
|
|
685
739
|
isHtmlContent(text) {
|
|
686
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;
|
|
687
741
|
return htmlPattern.test(text);
|
|
@@ -802,14 +856,11 @@ class AgentGUIClient {
|
|
|
802
856
|
inputHtml = `<div class="folded-tool-body"><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></div>`;
|
|
803
857
|
}
|
|
804
858
|
const tn = block.name || 'unknown';
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
} else {
|
|
811
|
-
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>`;
|
|
812
|
-
}
|
|
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>`;
|
|
813
864
|
} else if (block.type === 'tool_result') {
|
|
814
865
|
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
815
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>`;
|
|
@@ -988,9 +1039,7 @@ class AgentGUIClient {
|
|
|
988
1039
|
pollState.backoffDelay = 150;
|
|
989
1040
|
const lastChunk = chunks[chunks.length - 1];
|
|
990
1041
|
pollState.lastFetchTimestamp = lastChunk.created_at;
|
|
991
|
-
chunks.
|
|
992
|
-
if (chunk.block && chunk.block.type) this.renderChunk(chunk);
|
|
993
|
-
});
|
|
1042
|
+
this.renderChunkBatch(chunks.filter(c => c.block && c.block.type));
|
|
994
1043
|
} else {
|
|
995
1044
|
pollState.backoffDelay = Math.min(pollState.backoffDelay + 50, 500);
|
|
996
1045
|
}
|
|
@@ -1034,23 +1083,44 @@ class AgentGUIClient {
|
|
|
1034
1083
|
*/
|
|
1035
1084
|
renderChunk(chunk) {
|
|
1036
1085
|
if (!chunk || !chunk.block) return;
|
|
1037
|
-
|
|
1038
|
-
const sessionId = chunk.sessionId;
|
|
1039
|
-
const streamingEl = document.getElementById(`streaming-${sessionId}`);
|
|
1086
|
+
const streamingEl = document.getElementById(`streaming-${chunk.sessionId}`);
|
|
1040
1087
|
if (!streamingEl) return;
|
|
1041
|
-
|
|
1042
1088
|
const blocksEl = streamingEl.querySelector('.streaming-blocks');
|
|
1043
1089
|
if (!blocksEl) return;
|
|
1044
|
-
|
|
1045
|
-
const block = chunk.block;
|
|
1046
|
-
const element = this.renderer.renderBlock(block, chunk);
|
|
1047
|
-
|
|
1090
|
+
const element = this.renderer.renderBlock(chunk.block, chunk);
|
|
1048
1091
|
if (element) {
|
|
1049
1092
|
blocksEl.appendChild(element);
|
|
1050
1093
|
this.scrollToBottom();
|
|
1051
1094
|
}
|
|
1052
1095
|
}
|
|
1053
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
|
+
|
|
1054
1124
|
/**
|
|
1055
1125
|
* Load agents
|
|
1056
1126
|
*/
|
|
@@ -1227,6 +1297,7 @@ class AgentGUIClient {
|
|
|
1227
1297
|
sessionChunks[chunk.sessionId].push(chunk);
|
|
1228
1298
|
});
|
|
1229
1299
|
|
|
1300
|
+
const frag = document.createDocumentFragment();
|
|
1230
1301
|
let userMsgIdx = 0;
|
|
1231
1302
|
sessionOrder.forEach((sessionId) => {
|
|
1232
1303
|
const sessionChunkList = sessionChunks[sessionId];
|
|
@@ -1242,7 +1313,7 @@ class AgentGUIClient {
|
|
|
1242
1313
|
${this.renderMessageContent(msg.content)}
|
|
1243
1314
|
<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
|
|
1244
1315
|
`;
|
|
1245
|
-
|
|
1316
|
+
frag.appendChild(userDiv);
|
|
1246
1317
|
userMsgIdx++;
|
|
1247
1318
|
}
|
|
1248
1319
|
|
|
@@ -1253,12 +1324,14 @@ class AgentGUIClient {
|
|
|
1253
1324
|
messageDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
|
|
1254
1325
|
|
|
1255
1326
|
const blocksEl = messageDiv.querySelector('.message-blocks');
|
|
1327
|
+
const blockFrag = document.createDocumentFragment();
|
|
1256
1328
|
sessionChunkList.forEach(chunk => {
|
|
1257
1329
|
if (chunk.block && chunk.block.type) {
|
|
1258
1330
|
const element = this.renderer.renderBlock(chunk.block, chunk);
|
|
1259
|
-
if (element)
|
|
1331
|
+
if (element) blockFrag.appendChild(element);
|
|
1260
1332
|
}
|
|
1261
1333
|
});
|
|
1334
|
+
blocksEl.appendChild(blockFrag);
|
|
1262
1335
|
|
|
1263
1336
|
if (isCurrentActiveSession) {
|
|
1264
1337
|
const indicatorDiv = document.createElement('div');
|
|
@@ -1276,7 +1349,7 @@ class AgentGUIClient {
|
|
|
1276
1349
|
messageDiv.appendChild(ts);
|
|
1277
1350
|
}
|
|
1278
1351
|
|
|
1279
|
-
|
|
1352
|
+
frag.appendChild(messageDiv);
|
|
1280
1353
|
});
|
|
1281
1354
|
|
|
1282
1355
|
while (userMsgIdx < userMessages.length) {
|
|
@@ -1289,11 +1362,12 @@ class AgentGUIClient {
|
|
|
1289
1362
|
${this.renderMessageContent(msg.content)}
|
|
1290
1363
|
<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
|
|
1291
1364
|
`;
|
|
1292
|
-
|
|
1365
|
+
frag.appendChild(userDiv);
|
|
1293
1366
|
userMsgIdx++;
|
|
1294
1367
|
}
|
|
1368
|
+
messagesEl.appendChild(frag);
|
|
1295
1369
|
} else {
|
|
1296
|
-
messagesEl.
|
|
1370
|
+
messagesEl.appendChild(this.renderMessagesFragment(allMessages || []));
|
|
1297
1371
|
}
|
|
1298
1372
|
|
|
1299
1373
|
if (shouldResumeStreaming && latestSession) {
|
|
@@ -1329,109 +1403,36 @@ class AgentGUIClient {
|
|
|
1329
1403
|
}
|
|
1330
1404
|
}
|
|
1331
1405
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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
|
+
|
|
1335
1424
|
renderMessages(messages) {
|
|
1336
1425
|
if (messages.length === 0) {
|
|
1337
1426
|
return '<p class="text-secondary">No messages in this conversation yet</p>';
|
|
1338
1427
|
}
|
|
1339
|
-
|
|
1340
|
-
return messages.map(msg => {
|
|
1341
|
-
let contentHtml = '';
|
|
1342
|
-
|
|
1343
|
-
if (typeof msg.content === 'string') {
|
|
1344
|
-
if (this.isHtmlContent(msg.content)) {
|
|
1345
|
-
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>`;
|
|
1346
|
-
} else {
|
|
1347
|
-
contentHtml = `<div class="message-text">${this.escapeHtml(msg.content)}</div>`;
|
|
1348
|
-
}
|
|
1349
|
-
} else if (msg.content && typeof msg.content === 'object' && msg.content.type === 'claude_execution') {
|
|
1350
|
-
contentHtml = '<div class="message-blocks">';
|
|
1351
|
-
if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
|
|
1352
|
-
msg.content.blocks.forEach(block => {
|
|
1353
|
-
if (block.type === 'text') {
|
|
1354
|
-
const parts = this.parseMarkdownCodeBlocks(block.text);
|
|
1355
|
-
parts.forEach(part => {
|
|
1356
|
-
if (part.type === 'html') {
|
|
1357
|
-
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>`;
|
|
1358
|
-
} else if (part.type === 'text') {
|
|
1359
|
-
contentHtml += `<div class="message-text">${this.escapeHtml(part.content)}</div>`;
|
|
1360
|
-
} else if (part.type === 'code') {
|
|
1361
|
-
contentHtml += this.renderCodeBlock(part.language, part.code);
|
|
1362
|
-
}
|
|
1363
|
-
});
|
|
1364
|
-
} else if (block.type === 'code_block') {
|
|
1365
|
-
// Render HTML code blocks as actual HTML elements
|
|
1366
|
-
if (block.language === 'html') {
|
|
1367
|
-
contentHtml += `
|
|
1368
|
-
<div class="message-code">
|
|
1369
|
-
<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">
|
|
1370
|
-
Rendered HTML
|
|
1371
|
-
</div>
|
|
1372
|
-
<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
|
|
1373
|
-
${block.code}
|
|
1374
|
-
</div>
|
|
1375
|
-
</div>
|
|
1376
|
-
`;
|
|
1377
|
-
} else {
|
|
1378
|
-
const cBlkLineCount = block.code.split('\n').length;
|
|
1379
|
-
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>`;
|
|
1380
|
-
}
|
|
1381
|
-
} else if (block.type === 'tool_use') {
|
|
1382
|
-
let inputHtml = '';
|
|
1383
|
-
if (block.input && Object.keys(block.input).length > 0) {
|
|
1384
|
-
const inputStr = JSON.stringify(block.input, null, 2);
|
|
1385
|
-
inputHtml = `<div class="folded-tool-body"><pre class="tool-input-pre">${this.escapeHtml(inputStr)}</pre></div>`;
|
|
1386
|
-
}
|
|
1387
|
-
const tn2 = block.name || 'unknown';
|
|
1388
|
-
const foldable2 = tn2.startsWith('mcp__') || tn2 === 'Edit';
|
|
1389
|
-
if (foldable2) {
|
|
1390
|
-
const dName2 = typeof StreamingRenderer !== 'undefined' ? StreamingRenderer.getToolDisplayName(tn2) : tn2;
|
|
1391
|
-
const tTitle2 = typeof StreamingRenderer !== 'undefined' && block.input ? StreamingRenderer.getToolTitle(tn2, block.input) : '';
|
|
1392
|
-
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>`;
|
|
1393
|
-
} else {
|
|
1394
|
-
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>`;
|
|
1395
|
-
}
|
|
1396
|
-
} else if (block.type === 'tool_result') {
|
|
1397
|
-
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
1398
|
-
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>`;
|
|
1399
|
-
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>`;
|
|
1400
|
-
}
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
contentHtml += '</div>';
|
|
1404
|
-
} else {
|
|
1405
|
-
// Fallback for non-array msg.content: format as key-value pairs
|
|
1406
|
-
if (typeof msg.content === 'object' && msg.content !== null) {
|
|
1407
|
-
const fieldsHtml = Object.entries(msg.content)
|
|
1408
|
-
.map(([key, value]) => {
|
|
1409
|
-
let displayValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
1410
|
-
if (displayValue.length > 150) displayValue = displayValue.substring(0, 150) + '...';
|
|
1411
|
-
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>`;
|
|
1412
|
-
}).join('');
|
|
1413
|
-
contentHtml = `<div class="message-text" style="background:var(--color-bg-secondary);padding:0.75rem;border-radius:0.375rem">${fieldsHtml}</div>`;
|
|
1414
|
-
} else {
|
|
1415
|
-
contentHtml = `<div class="message-text">${this.escapeHtml(String(msg.content))}</div>`;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
return `
|
|
1420
|
-
<div class="message message-${msg.role}">
|
|
1421
|
-
<div class="message-role">${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}</div>
|
|
1422
|
-
${contentHtml}
|
|
1423
|
-
<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>
|
|
1424
|
-
</div>
|
|
1425
|
-
`;
|
|
1426
|
-
}).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('');
|
|
1427
1429
|
}
|
|
1428
1430
|
|
|
1429
1431
|
/**
|
|
1430
1432
|
* Escape HTML to prevent XSS
|
|
1431
1433
|
*/
|
|
1432
1434
|
escapeHtml(text) {
|
|
1433
|
-
|
|
1434
|
-
return text.replace(/[&<>"']/g, c => map[c]);
|
|
1435
|
+
return window._escHtml(text);
|
|
1435
1436
|
}
|
|
1436
1437
|
|
|
1437
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
|
-
|
|
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
|
|