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/database.js
CHANGED
|
@@ -1031,6 +1031,11 @@ export const queries = {
|
|
|
1031
1031
|
});
|
|
1032
1032
|
},
|
|
1033
1033
|
|
|
1034
|
+
getConversationChunkCount(conversationId) {
|
|
1035
|
+
const stmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
|
|
1036
|
+
return stmt.get(conversationId).count;
|
|
1037
|
+
},
|
|
1038
|
+
|
|
1034
1039
|
getConversationChunks(conversationId) {
|
|
1035
1040
|
const stmt = prep(
|
|
1036
1041
|
`SELECT id, sessionId, conversationId, sequence, type, data, created_at
|
|
@@ -1049,6 +1054,26 @@ export const queries = {
|
|
|
1049
1054
|
});
|
|
1050
1055
|
},
|
|
1051
1056
|
|
|
1057
|
+
getRecentConversationChunks(conversationId, limit) {
|
|
1058
|
+
const stmt = prep(
|
|
1059
|
+
`SELECT id, sessionId, conversationId, sequence, type, data, created_at
|
|
1060
|
+
FROM chunks WHERE conversationId = ?
|
|
1061
|
+
ORDER BY created_at DESC LIMIT ?`
|
|
1062
|
+
);
|
|
1063
|
+
const rows = stmt.all(conversationId, limit);
|
|
1064
|
+
rows.reverse();
|
|
1065
|
+
return rows.map(row => {
|
|
1066
|
+
try {
|
|
1067
|
+
return {
|
|
1068
|
+
...row,
|
|
1069
|
+
data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
|
|
1070
|
+
};
|
|
1071
|
+
} catch (e) {
|
|
1072
|
+
return row;
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
},
|
|
1076
|
+
|
|
1052
1077
|
getChunksSince(sessionId, timestamp) {
|
|
1053
1078
|
const stmt = prep(
|
|
1054
1079
|
`SELECT id, sessionId, conversationId, sequence, type, data, created_at
|
package/lib/claude-runner.js
CHANGED
|
@@ -46,7 +46,8 @@ class AgentRunner {
|
|
|
46
46
|
const {
|
|
47
47
|
timeout = 300000,
|
|
48
48
|
onEvent = null,
|
|
49
|
-
onError = null
|
|
49
|
+
onError = null,
|
|
50
|
+
onRateLimit = null
|
|
50
51
|
} = config;
|
|
51
52
|
|
|
52
53
|
const args = this.buildArgs(prompt, config);
|
|
@@ -60,6 +61,8 @@ class AgentRunner {
|
|
|
60
61
|
const outputs = [];
|
|
61
62
|
let timedOut = false;
|
|
62
63
|
let sessionId = null;
|
|
64
|
+
let rateLimited = false;
|
|
65
|
+
let retryAfterSec = 60;
|
|
63
66
|
|
|
64
67
|
const timeoutHandle = setTimeout(() => {
|
|
65
68
|
timedOut = true;
|
|
@@ -103,6 +106,14 @@ class AgentRunner {
|
|
|
103
106
|
proc.stderr.on('data', (chunk) => {
|
|
104
107
|
const errorText = chunk.toString();
|
|
105
108
|
console.error(`[${this.id}] stderr:`, errorText);
|
|
109
|
+
|
|
110
|
+
const rateLimitMatch = errorText.match(/rate.?limit|429|too many requests|overloaded|throttl/i);
|
|
111
|
+
if (rateLimitMatch) {
|
|
112
|
+
rateLimited = true;
|
|
113
|
+
const retryMatch = errorText.match(/retry.?after[:\s]+(\d+)/i);
|
|
114
|
+
if (retryMatch) retryAfterSec = parseInt(retryMatch[1], 10) || 60;
|
|
115
|
+
}
|
|
116
|
+
|
|
106
117
|
if (onError) {
|
|
107
118
|
try { onError(errorText); } catch (e) {}
|
|
108
119
|
}
|
|
@@ -112,7 +123,17 @@ class AgentRunner {
|
|
|
112
123
|
clearTimeout(timeoutHandle);
|
|
113
124
|
if (timedOut) return;
|
|
114
125
|
|
|
115
|
-
|
|
126
|
+
if (rateLimited) {
|
|
127
|
+
const err = new Error(`Rate limited - retry after ${retryAfterSec}s`);
|
|
128
|
+
err.rateLimited = true;
|
|
129
|
+
err.retryAfterSec = retryAfterSec;
|
|
130
|
+
if (onRateLimit) {
|
|
131
|
+
try { onRateLimit({ retryAfterSec }); } catch (e) {}
|
|
132
|
+
}
|
|
133
|
+
reject(err);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
116
137
|
const success = code === 0 || (outputs.length > 0 && this.allowNonZeroExit);
|
|
117
138
|
|
|
118
139
|
if (success) {
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -24,9 +24,11 @@ const SYSTEM_PROMPT = `Write all responses as clean semantic HTML. Use tags like
|
|
|
24
24
|
|
|
25
25
|
const activeExecutions = new Map();
|
|
26
26
|
const messageQueues = new Map();
|
|
27
|
+
const rateLimitState = new Map();
|
|
27
28
|
const STUCK_AGENT_THRESHOLD_MS = 600000;
|
|
28
29
|
const NO_PID_GRACE_PERIOD_MS = 60000;
|
|
29
30
|
const STALE_SESSION_MIN_AGE_MS = 30000;
|
|
31
|
+
const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 60000;
|
|
30
32
|
|
|
31
33
|
const debugLog = (msg) => {
|
|
32
34
|
const timestamp = new Date().toISOString();
|
|
@@ -298,8 +300,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
298
300
|
const conv = queries.getConversation(conversationId);
|
|
299
301
|
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
300
302
|
|
|
301
|
-
const prompt = body.content || '';
|
|
302
|
-
const agentId = body.agentId || 'claude-code';
|
|
303
|
+
const prompt = body.content || body.message || '';
|
|
304
|
+
const agentId = body.agentId || conv.agentType || conv.agentId || 'claude-code';
|
|
303
305
|
|
|
304
306
|
const userMessage = queries.createMessage(conversationId, 'user', prompt);
|
|
305
307
|
queries.createEvent('message.created', { role: 'user', messageId: userMessage.id }, conversationId);
|
|
@@ -362,14 +364,28 @@ const server = http.createServer(async (req, res) => {
|
|
|
362
364
|
const latestSession = queries.getLatestSession(conversationId);
|
|
363
365
|
const isActivelyStreaming = activeExecutions.has(conversationId) ||
|
|
364
366
|
(latestSession && latestSession.status === 'active');
|
|
365
|
-
|
|
367
|
+
|
|
368
|
+
const url = new URL(req.url, 'http://localhost');
|
|
369
|
+
const chunkLimit = Math.min(parseInt(url.searchParams.get('chunkLimit') || '500'), 5000);
|
|
370
|
+
const allChunks = url.searchParams.get('allChunks') === '1';
|
|
371
|
+
|
|
372
|
+
const totalChunks = queries.getConversationChunkCount(conversationId);
|
|
373
|
+
let chunks;
|
|
374
|
+
if (allChunks || totalChunks <= chunkLimit) {
|
|
375
|
+
chunks = queries.getConversationChunks(conversationId);
|
|
376
|
+
} else {
|
|
377
|
+
chunks = queries.getRecentConversationChunks(conversationId, chunkLimit);
|
|
378
|
+
}
|
|
366
379
|
const msgResult = queries.getPaginatedMessages(conversationId, 100, 0);
|
|
380
|
+
const rateLimitInfo = rateLimitState.get(conversationId) || null;
|
|
367
381
|
sendJSON(req, res, 200, {
|
|
368
382
|
conversation: conv,
|
|
369
383
|
isActivelyStreaming,
|
|
370
384
|
latestSession,
|
|
371
385
|
chunks,
|
|
372
|
-
|
|
386
|
+
totalChunks,
|
|
387
|
+
messages: msgResult.messages,
|
|
388
|
+
rateLimitInfo
|
|
373
389
|
});
|
|
374
390
|
return;
|
|
375
391
|
}
|
|
@@ -702,6 +718,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
702
718
|
activeExecutions.set(conversationId, { pid: null, startTime, sessionId, lastActivity: startTime });
|
|
703
719
|
queries.setIsStreaming(conversationId, true);
|
|
704
720
|
queries.updateSession(sessionId, { status: 'active' });
|
|
721
|
+
const batcher = createChunkBatcher();
|
|
705
722
|
|
|
706
723
|
try {
|
|
707
724
|
debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
|
|
@@ -713,7 +730,6 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
713
730
|
let allBlocks = [];
|
|
714
731
|
let eventCount = 0;
|
|
715
732
|
let currentSequence = queries.getMaxSequence(sessionId) ?? -1;
|
|
716
|
-
const batcher = createChunkBatcher();
|
|
717
733
|
|
|
718
734
|
const onEvent = (parsed) => {
|
|
719
735
|
eventCount++;
|
|
@@ -826,12 +842,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
826
842
|
};
|
|
827
843
|
|
|
828
844
|
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(content, cwd, agentId || 'claude-code', config);
|
|
845
|
+
activeExecutions.delete(conversationId);
|
|
829
846
|
batcher.drain();
|
|
830
847
|
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
831
848
|
|
|
832
|
-
if (claudeSessionId
|
|
849
|
+
if (claudeSessionId) {
|
|
833
850
|
queries.setClaudeSessionId(conversationId, claudeSessionId);
|
|
834
|
-
debugLog(`[stream]
|
|
851
|
+
debugLog(`[stream] Updated claudeSessionId=${claudeSessionId}`);
|
|
835
852
|
}
|
|
836
853
|
|
|
837
854
|
// Mark session as complete
|
|
@@ -854,13 +871,47 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
854
871
|
const elapsed = Date.now() - startTime;
|
|
855
872
|
debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
|
|
856
873
|
|
|
857
|
-
|
|
874
|
+
const isRateLimit = error.rateLimited ||
|
|
875
|
+
/rate.?limit|429|too many requests|overloaded|throttl/i.test(error.message);
|
|
876
|
+
|
|
858
877
|
queries.updateSession(sessionId, {
|
|
859
878
|
status: 'error',
|
|
860
879
|
error: error.message,
|
|
861
880
|
completed_at: Date.now()
|
|
862
881
|
});
|
|
863
882
|
|
|
883
|
+
if (isRateLimit) {
|
|
884
|
+
const cooldownMs = (error.retryAfterSec || 60) * 1000;
|
|
885
|
+
const retryAt = Date.now() + cooldownMs;
|
|
886
|
+
rateLimitState.set(conversationId, { retryAt, cooldownMs });
|
|
887
|
+
debugLog(`[rate-limit] Conv ${conversationId} hit rate limit, retry in ${cooldownMs}ms`);
|
|
888
|
+
|
|
889
|
+
broadcastSync({
|
|
890
|
+
type: 'rate_limit_hit',
|
|
891
|
+
sessionId,
|
|
892
|
+
conversationId,
|
|
893
|
+
retryAfterMs: cooldownMs,
|
|
894
|
+
retryAt,
|
|
895
|
+
timestamp: Date.now()
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
batcher.drain();
|
|
899
|
+
activeExecutions.delete(conversationId);
|
|
900
|
+
queries.setIsStreaming(conversationId, false);
|
|
901
|
+
|
|
902
|
+
setTimeout(() => {
|
|
903
|
+
rateLimitState.delete(conversationId);
|
|
904
|
+
debugLog(`[rate-limit] Conv ${conversationId} cooldown expired, restarting`);
|
|
905
|
+
broadcastSync({
|
|
906
|
+
type: 'rate_limit_clear',
|
|
907
|
+
conversationId,
|
|
908
|
+
timestamp: Date.now()
|
|
909
|
+
});
|
|
910
|
+
scheduleRetry(conversationId, messageId, content, agentId);
|
|
911
|
+
}, cooldownMs);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
864
915
|
broadcastSync({
|
|
865
916
|
type: 'streaming_error',
|
|
866
917
|
sessionId,
|
|
@@ -881,10 +932,29 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
881
932
|
batcher.drain();
|
|
882
933
|
activeExecutions.delete(conversationId);
|
|
883
934
|
queries.setIsStreaming(conversationId, false);
|
|
884
|
-
|
|
935
|
+
if (!rateLimitState.has(conversationId)) {
|
|
936
|
+
drainMessageQueue(conversationId);
|
|
937
|
+
}
|
|
885
938
|
}
|
|
886
939
|
}
|
|
887
940
|
|
|
941
|
+
function scheduleRetry(conversationId, messageId, content, agentId) {
|
|
942
|
+
const newSession = queries.createSession(conversationId);
|
|
943
|
+
queries.createEvent('session.created', { messageId, sessionId: newSession.id, retryReason: 'rate_limit' }, conversationId, newSession.id);
|
|
944
|
+
|
|
945
|
+
broadcastSync({
|
|
946
|
+
type: 'streaming_start',
|
|
947
|
+
sessionId: newSession.id,
|
|
948
|
+
conversationId,
|
|
949
|
+
messageId,
|
|
950
|
+
agentId,
|
|
951
|
+
timestamp: Date.now()
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
processMessageWithStreaming(conversationId, messageId, newSession.id, content, agentId)
|
|
955
|
+
.catch(err => debugLog(`[retry] Error: ${err.message}`));
|
|
956
|
+
}
|
|
957
|
+
|
|
888
958
|
function drainMessageQueue(conversationId) {
|
|
889
959
|
const queue = messageQueues.get(conversationId);
|
|
890
960
|
if (!queue || queue.length === 0) return;
|
|
@@ -932,7 +1002,7 @@ async function processMessage(conversationId, messageId, content, agentId) {
|
|
|
932
1002
|
systemPrompt: SYSTEM_PROMPT
|
|
933
1003
|
});
|
|
934
1004
|
|
|
935
|
-
if (claudeSessionId
|
|
1005
|
+
if (claudeSessionId) {
|
|
936
1006
|
queries.setClaudeSessionId(conversationId, claudeSessionId);
|
|
937
1007
|
}
|
|
938
1008
|
|
|
@@ -1060,7 +1130,8 @@ wss.on('connection', (ws, req) => {
|
|
|
1060
1130
|
const BROADCAST_TYPES = new Set([
|
|
1061
1131
|
'message_created', 'conversation_created', 'conversations_updated',
|
|
1062
1132
|
'conversation_deleted', 'queue_status', 'streaming_start',
|
|
1063
|
-
'streaming_complete', 'streaming_error'
|
|
1133
|
+
'streaming_complete', 'streaming_error', 'rate_limit_hit',
|
|
1134
|
+
'rate_limit_clear'
|
|
1064
1135
|
]);
|
|
1065
1136
|
|
|
1066
1137
|
const wsBatchQueues = new Map();
|
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>
|