agentgui 1.0.121 → 1.0.123
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 +4 -4
- package/lib/claude-runner.js +814 -67
- package/package.json +1 -1
- package/server.js +38 -10
- package/static/index.html +12 -0
- package/static/js/client.js +85 -8
- package/static/js/streaming-renderer.js +339 -6
- package/WAVE6_FINAL_REPORT.md +0 -178
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -11,14 +11,7 @@ import { runClaudeWithStreaming } from './lib/claude-runner.js';
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const express = require('express');
|
|
13
13
|
const Busboy = require('busboy');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// Stub fsbrowse function for file browsing endpoint
|
|
17
|
-
const fsbrowse = (options) => {
|
|
18
|
-
return (req, res) => {
|
|
19
|
-
res.status(501).json({ error: 'File browsing not yet implemented' });
|
|
20
|
-
};
|
|
21
|
-
};
|
|
14
|
+
const fsbrowse = require('../fsbrowse');
|
|
22
15
|
|
|
23
16
|
const SYSTEM_PROMPT = `Always write your responses in ripple-ui enhanced HTML. Avoid overriding light/dark mode CSS variables. Use all the benefits of HTML to express technical details with proper semantic markup, tables, code blocks, headings, and lists. Write clean, well-structured HTML that respects the existing design system.`;
|
|
24
17
|
|
|
@@ -107,6 +100,17 @@ function discoverAgents() {
|
|
|
107
100
|
const binaries = [
|
|
108
101
|
{ cmd: 'claude', id: 'claude-code', name: 'Claude Code', icon: 'C' },
|
|
109
102
|
{ cmd: 'opencode', id: 'opencode', name: 'OpenCode', icon: 'O' },
|
|
103
|
+
{ cmd: 'gemini', id: 'gemini', name: 'Gemini CLI', icon: 'G' },
|
|
104
|
+
{ cmd: 'goose', id: 'goose', name: 'Goose', icon: 'g' },
|
|
105
|
+
{ cmd: 'openhands', id: 'openhands', name: 'OpenHands', icon: 'H' },
|
|
106
|
+
{ cmd: 'augment', id: 'augment', name: 'Augment Code', icon: 'A' },
|
|
107
|
+
{ cmd: 'cline', id: 'cline', name: 'Cline', icon: 'c' },
|
|
108
|
+
{ cmd: 'kimi', id: 'kimi', name: 'Kimi CLI', icon: 'K' },
|
|
109
|
+
{ cmd: 'qwen-code', id: 'qwen', name: 'Qwen Code', icon: 'Q' },
|
|
110
|
+
{ cmd: 'codex', id: 'codex', name: 'Codex CLI', icon: 'X' },
|
|
111
|
+
{ cmd: 'mistral-vibe', id: 'mistral', name: 'Mistral Vibe', icon: 'M' },
|
|
112
|
+
{ cmd: 'kiro', id: 'kiro', name: 'Kiro CLI', icon: 'k' },
|
|
113
|
+
{ cmd: 'fast-agent', id: 'fast-agent', name: 'fast-agent', icon: 'F' },
|
|
110
114
|
];
|
|
111
115
|
for (const bin of binaries) {
|
|
112
116
|
try {
|
|
@@ -176,8 +180,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
176
180
|
if (req.method === 'GET') {
|
|
177
181
|
const conv = queries.getConversation(convMatch[1]);
|
|
178
182
|
if (!conv) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); return; }
|
|
183
|
+
|
|
184
|
+
// Check both in-memory and database for active streaming status
|
|
185
|
+
const latestSession = queries.getLatestSession(convMatch[1]);
|
|
186
|
+
const isActivelyStreaming = activeExecutions.has(convMatch[1]) ||
|
|
187
|
+
(latestSession && latestSession.status === 'active');
|
|
188
|
+
|
|
179
189
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
180
|
-
res.end(JSON.stringify({
|
|
190
|
+
res.end(JSON.stringify({
|
|
191
|
+
conversation: conv,
|
|
192
|
+
isActivelyStreaming,
|
|
193
|
+
latestSession
|
|
194
|
+
}));
|
|
181
195
|
return;
|
|
182
196
|
}
|
|
183
197
|
|
|
@@ -422,7 +436,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
422
436
|
folderPath.replace('~', process.env.HOME || '/config') : folderPath;
|
|
423
437
|
const entries = fs.readdirSync(expandedPath, { withFileTypes: true });
|
|
424
438
|
const folders = entries
|
|
425
|
-
.filter(e => e.isDirectory())
|
|
439
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
426
440
|
.map(e => ({ name: e.name }))
|
|
427
441
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
428
442
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -660,6 +674,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
660
674
|
debugLog(`[stream] Stored claudeSessionId=${claudeSessionId}`);
|
|
661
675
|
}
|
|
662
676
|
|
|
677
|
+
// Mark session as complete
|
|
678
|
+
queries.updateSession(sessionId, {
|
|
679
|
+
status: 'complete',
|
|
680
|
+
response: JSON.stringify({ outputs, eventCount }),
|
|
681
|
+
completed_at: Date.now()
|
|
682
|
+
});
|
|
683
|
+
|
|
663
684
|
broadcastSync({
|
|
664
685
|
type: 'streaming_complete',
|
|
665
686
|
sessionId,
|
|
@@ -673,6 +694,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
673
694
|
const elapsed = Date.now() - startTime;
|
|
674
695
|
debugLog(`[stream] Error after ${elapsed}ms: ${error.message}`);
|
|
675
696
|
|
|
697
|
+
// Mark session as error
|
|
698
|
+
queries.updateSession(sessionId, {
|
|
699
|
+
status: 'error',
|
|
700
|
+
error: error.message,
|
|
701
|
+
completed_at: Date.now()
|
|
702
|
+
});
|
|
703
|
+
|
|
676
704
|
broadcastSync({
|
|
677
705
|
type: 'streaming_error',
|
|
678
706
|
sessionId,
|
package/static/index.html
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
--color-success: #10b981;
|
|
25
25
|
--color-error: #ef4444;
|
|
26
26
|
--color-warning: #f59e0b;
|
|
27
|
+
--color-info: #0891b2;
|
|
27
28
|
--sidebar-width: 300px;
|
|
28
29
|
--header-height: 52px;
|
|
29
30
|
--msg-max-width: 100%;
|
|
@@ -1652,6 +1653,17 @@
|
|
|
1652
1653
|
<select class="agent-selector" data-agent-selector title="Select agent">
|
|
1653
1654
|
<option value="claude-code">Claude Code</option>
|
|
1654
1655
|
<option value="opencode">OpenCode</option>
|
|
1656
|
+
<option value="gemini">Gemini CLI</option>
|
|
1657
|
+
<option value="goose">Goose</option>
|
|
1658
|
+
<option value="openhands">OpenHands</option>
|
|
1659
|
+
<option value="augment">Augment Code</option>
|
|
1660
|
+
<option value="cline">Cline</option>
|
|
1661
|
+
<option value="kimi">Kimi CLI</option>
|
|
1662
|
+
<option value="qwen">Qwen Code</option>
|
|
1663
|
+
<option value="codex">Codex CLI</option>
|
|
1664
|
+
<option value="mistral">Mistral Vibe</option>
|
|
1665
|
+
<option value="kiro">Kiro CLI</option>
|
|
1666
|
+
<option value="fast-agent">fast-agent</option>
|
|
1655
1667
|
</select>
|
|
1656
1668
|
<textarea
|
|
1657
1669
|
class="message-textarea"
|
package/static/js/client.js
CHANGED
|
@@ -845,6 +845,7 @@ class AgentGUIClient {
|
|
|
845
845
|
/**
|
|
846
846
|
* Poll for new chunks at regular intervals
|
|
847
847
|
* Uses exponential backoff on errors
|
|
848
|
+
* Also checks session status to detect completion
|
|
848
849
|
*/
|
|
849
850
|
async startChunkPolling(conversationId) {
|
|
850
851
|
if (!conversationId) return;
|
|
@@ -862,6 +863,32 @@ class AgentGUIClient {
|
|
|
862
863
|
if (!pollState.isPolling) return;
|
|
863
864
|
|
|
864
865
|
try {
|
|
866
|
+
// Check session status periodically
|
|
867
|
+
if (this.state.currentSession?.id) {
|
|
868
|
+
const sessionResponse = await fetch(`${window.__BASE_URL}/api/sessions/${this.state.currentSession.id}`);
|
|
869
|
+
if (sessionResponse.ok) {
|
|
870
|
+
const { session } = await sessionResponse.json();
|
|
871
|
+
if (session && (session.status === 'complete' || session.status === 'error')) {
|
|
872
|
+
// Session has finished, trigger appropriate handler
|
|
873
|
+
if (session.status === 'complete') {
|
|
874
|
+
this.handleStreamingComplete({
|
|
875
|
+
sessionId: session.id,
|
|
876
|
+
conversationId: conversationId,
|
|
877
|
+
timestamp: Date.now()
|
|
878
|
+
});
|
|
879
|
+
} else {
|
|
880
|
+
this.handleStreamingError({
|
|
881
|
+
sessionId: session.id,
|
|
882
|
+
conversationId: conversationId,
|
|
883
|
+
error: session.error || 'Unknown error',
|
|
884
|
+
timestamp: Date.now()
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
return; // Stop polling
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
865
892
|
const chunks = await this.fetchChunks(conversationId, pollState.lastFetchTimestamp);
|
|
866
893
|
|
|
867
894
|
if (chunks.length > 0) {
|
|
@@ -1072,8 +1099,11 @@ class AgentGUIClient {
|
|
|
1072
1099
|
|
|
1073
1100
|
async loadConversationMessages(conversationId) {
|
|
1074
1101
|
try {
|
|
1102
|
+
// Stop any existing polling when switching conversations
|
|
1103
|
+
this.stopChunkPolling();
|
|
1104
|
+
|
|
1075
1105
|
const convResponse = await fetch(window.__BASE_URL + `/api/conversations/${conversationId}`);
|
|
1076
|
-
const { conversation } = await convResponse.json();
|
|
1106
|
+
const { conversation, isActivelyStreaming, latestSession } = await convResponse.json();
|
|
1077
1107
|
this.state.currentConversation = conversation;
|
|
1078
1108
|
|
|
1079
1109
|
// Update URL with conversation ID
|
|
@@ -1083,6 +1113,9 @@ class AgentGUIClient {
|
|
|
1083
1113
|
this.wsManager.sendMessage({ type: 'subscribe', conversationId });
|
|
1084
1114
|
}
|
|
1085
1115
|
|
|
1116
|
+
// Check if there's an active streaming session that needs to be resumed
|
|
1117
|
+
const shouldResumeStreaming = isActivelyStreaming && latestSession && latestSession.status === 'active';
|
|
1118
|
+
|
|
1086
1119
|
// Try to fetch chunks first (Wave 3 architecture)
|
|
1087
1120
|
try {
|
|
1088
1121
|
const chunks = await this.fetchChunks(conversationId, 0);
|
|
@@ -1112,10 +1145,11 @@ class AgentGUIClient {
|
|
|
1112
1145
|
|
|
1113
1146
|
// Render each session's chunks
|
|
1114
1147
|
Object.entries(sessionChunks).forEach(([sessionId, sessionChunkList]) => {
|
|
1148
|
+
const isCurrentActiveSession = shouldResumeStreaming && latestSession && latestSession.id === sessionId;
|
|
1115
1149
|
const messageDiv = document.createElement('div');
|
|
1116
|
-
messageDiv.className =
|
|
1117
|
-
messageDiv.id = `message-${sessionId}`;
|
|
1118
|
-
messageDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks"></div>';
|
|
1150
|
+
messageDiv.className = `message message-assistant${isCurrentActiveSession ? ' streaming-message' : ''}`;
|
|
1151
|
+
messageDiv.id = isCurrentActiveSession ? `streaming-${sessionId}` : `message-${sessionId}`;
|
|
1152
|
+
messageDiv.innerHTML = '<div class="message-role">Assistant</div><div class="message-blocks streaming-blocks"></div>';
|
|
1119
1153
|
|
|
1120
1154
|
const blocksEl = messageDiv.querySelector('.message-blocks');
|
|
1121
1155
|
sessionChunkList.forEach(chunk => {
|
|
@@ -1127,10 +1161,22 @@ class AgentGUIClient {
|
|
|
1127
1161
|
}
|
|
1128
1162
|
});
|
|
1129
1163
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1164
|
+
// Add streaming indicator for active session
|
|
1165
|
+
if (isCurrentActiveSession) {
|
|
1166
|
+
const indicatorDiv = document.createElement('div');
|
|
1167
|
+
indicatorDiv.className = 'streaming-indicator';
|
|
1168
|
+
indicatorDiv.style = 'display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0;color:var(--color-text-secondary);font-size:0.875rem;';
|
|
1169
|
+
indicatorDiv.innerHTML = `
|
|
1170
|
+
<span class="animate-spin" style="display:inline-block;width:1rem;height:1rem;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;"></span>
|
|
1171
|
+
<span class="streaming-indicator-label">Processing...</span>
|
|
1172
|
+
`;
|
|
1173
|
+
messageDiv.appendChild(indicatorDiv);
|
|
1174
|
+
} else {
|
|
1175
|
+
const ts = document.createElement('div');
|
|
1176
|
+
ts.className = 'message-timestamp';
|
|
1177
|
+
ts.textContent = new Date(sessionChunkList[sessionChunkList.length - 1].created_at).toLocaleString();
|
|
1178
|
+
messageDiv.appendChild(ts);
|
|
1179
|
+
}
|
|
1134
1180
|
|
|
1135
1181
|
messagesEl.appendChild(messageDiv);
|
|
1136
1182
|
});
|
|
@@ -1143,6 +1189,37 @@ class AgentGUIClient {
|
|
|
1143
1189
|
}
|
|
1144
1190
|
}
|
|
1145
1191
|
|
|
1192
|
+
// Resume streaming if needed
|
|
1193
|
+
if (shouldResumeStreaming && latestSession) {
|
|
1194
|
+
console.log('Resuming live streaming for session:', latestSession.id);
|
|
1195
|
+
|
|
1196
|
+
// Set streaming state
|
|
1197
|
+
this.state.isStreaming = true;
|
|
1198
|
+
this.state.currentSession = {
|
|
1199
|
+
id: latestSession.id,
|
|
1200
|
+
conversationId: conversationId,
|
|
1201
|
+
agentId: conversation.agentType || 'claude-code',
|
|
1202
|
+
startTime: latestSession.created_at
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// Subscribe to WebSocket updates
|
|
1206
|
+
if (this.wsManager.isConnected) {
|
|
1207
|
+
this.wsManager.subscribeToSession(latestSession.id);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Get the timestamp of the last chunk to start polling from
|
|
1211
|
+
const lastChunkTime = chunks.length > 0
|
|
1212
|
+
? chunks[chunks.length - 1].created_at
|
|
1213
|
+
: Date.now();
|
|
1214
|
+
|
|
1215
|
+
// Start polling for new chunks
|
|
1216
|
+
this.chunkPollState.lastFetchTimestamp = lastChunkTime;
|
|
1217
|
+
this.startChunkPolling(conversationId);
|
|
1218
|
+
|
|
1219
|
+
// Disable controls while streaming
|
|
1220
|
+
this.disableControls();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1146
1223
|
// Restore scroll position after rendering
|
|
1147
1224
|
this.restoreScrollPosition(conversationId);
|
|
1148
1225
|
}
|
|
@@ -576,6 +576,50 @@ class StreamingRenderer {
|
|
|
576
576
|
case 'NotebookEdit':
|
|
577
577
|
return `<div class="tool-params">${this.renderFilePath(input.notebook_path)}${input.new_source ? this.renderContentPreview(input.new_source, 'Cell content') : ''}</div>`;
|
|
578
578
|
|
|
579
|
+
case 'dev__execute':
|
|
580
|
+
case 'dev_execute':
|
|
581
|
+
case 'execute': {
|
|
582
|
+
// Handle mcp__plugin_gm_dev__execute and similar dev execution tools
|
|
583
|
+
let html = '<div class="tool-params">';
|
|
584
|
+
|
|
585
|
+
// Show working directory if present
|
|
586
|
+
if (input.workingDirectory) {
|
|
587
|
+
html += `<div style="margin-bottom:0.5rem;font-size:0.75rem;color:var(--color-text-secondary)">
|
|
588
|
+
<span style="opacity:0.7">📁</span> ${this.escapeHtml(input.workingDirectory)}
|
|
589
|
+
</div>`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Show timeout if present
|
|
593
|
+
if (input.timeout) {
|
|
594
|
+
const seconds = Math.round(input.timeout / 1000);
|
|
595
|
+
html += `<div style="margin-bottom:0.5rem;font-size:0.75rem;color:var(--color-text-secondary)">
|
|
596
|
+
<span style="opacity:0.7">⏱️</span> Timeout: ${seconds}s
|
|
597
|
+
</div>`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Render code in a styled code block
|
|
601
|
+
if (input.code) {
|
|
602
|
+
const lines = input.code.split('\n');
|
|
603
|
+
const lineCount = lines.length;
|
|
604
|
+
const truncated = lineCount > 50;
|
|
605
|
+
const displayLines = truncated ? lines.slice(0, 50) : lines;
|
|
606
|
+
|
|
607
|
+
html += `<div style="margin-top:0.5rem">
|
|
608
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.25rem">
|
|
609
|
+
<span style="font-size:0.7rem;font-weight:600;color:#0891b2;text-transform:uppercase">JavaScript Code</span>
|
|
610
|
+
<span style="font-size:0.7rem;color:var(--color-text-secondary)">${lineCount} lines</span>
|
|
611
|
+
</div>
|
|
612
|
+
<div style="background:var(--color-bg-code);border-radius:0.375rem;padding:0.75rem;overflow-x:auto">
|
|
613
|
+
<pre style="margin:0;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;line-height:1.5;color:#d1d5db">${this.escapeHtml(displayLines.join('\n'))}</pre>
|
|
614
|
+
${truncated ? `<div style="margin-top:0.5rem;padding-top:0.5rem;border-top:1px solid var(--color-border);font-size:0.7rem;color:var(--color-text-secondary);text-align:center">... ${lineCount - 50} more lines truncated ...</div>` : ''}
|
|
615
|
+
</div>
|
|
616
|
+
</div>`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
html += '</div>';
|
|
620
|
+
return html;
|
|
621
|
+
}
|
|
622
|
+
|
|
579
623
|
default:
|
|
580
624
|
return this.renderJsonParams(input);
|
|
581
625
|
}
|
|
@@ -677,7 +721,8 @@ class StreamingRenderer {
|
|
|
677
721
|
if (Array.isArray(data)) {
|
|
678
722
|
if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
|
|
679
723
|
if (data.every(i => typeof i === 'string') && data.length <= 20) {
|
|
680
|
-
|
|
724
|
+
// Render as an itemized list instead of inline badges
|
|
725
|
+
return `<div style="display:flex;flex-direction:column;gap:0.125rem;${depth > 0 ? 'padding-left:1rem' : ''}">${data.map((i, idx) => `<div style="display:flex;align-items:center;gap:0.375rem"><span style="color:var(--color-text-secondary);font-size:0.65rem;opacity:0.5">•</span><span style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem">${this.escapeHtml(i)}</span></div>`).join('')}</div>`;
|
|
681
726
|
}
|
|
682
727
|
return `<div style="display:flex;flex-direction:column;gap:0.25rem;${depth > 0 ? 'padding-left:1rem' : ''}">${data.map((item, i) => `<div style="display:flex;gap:0.5rem;align-items:flex-start"><span style="color:var(--color-text-secondary);font-size:0.7rem;min-width:1.5rem;text-align:right;flex-shrink:0">${i}</span><div style="flex:1;min-width:0">${this.renderParametersBeautiful(item, depth + 1)}</div></div>`).join('')}</div>`;
|
|
683
728
|
}
|
|
@@ -702,14 +747,165 @@ class StreamingRenderer {
|
|
|
702
747
|
return `<div style="padding:0.5rem"><img src="${esc(trimmed)}" style="max-width:100%;max-height:24rem;border-radius:0.375rem" loading="lazy"></div>`;
|
|
703
748
|
}
|
|
704
749
|
|
|
750
|
+
// Instead of rendering JSON as parameters, check if it looks like code output
|
|
705
751
|
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
706
752
|
try {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
753
|
+
// Validate it's JSON, then render as highlighted code
|
|
754
|
+
JSON.parse(trimmed);
|
|
755
|
+
// Format JSON with proper indentation
|
|
756
|
+
const formatted = JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
757
|
+
return StreamingRenderer.renderCodeWithHighlight(formatted, esc);
|
|
758
|
+
} catch (e) {
|
|
759
|
+
// Not valid JSON, might be code with braces
|
|
760
|
+
}
|
|
710
761
|
}
|
|
711
762
|
|
|
763
|
+
// Check if this looks like `cat -n` output or grep with line numbers
|
|
712
764
|
const lines = trimmed.split('\n');
|
|
765
|
+
const isCatNOutput = lines.length > 1 && lines[0].match(/^\s*\d+→/);
|
|
766
|
+
const isGrepOutput = lines.length > 1 && lines[0].match(/^\s*\d+-/);
|
|
767
|
+
|
|
768
|
+
if (isCatNOutput || isGrepOutput) {
|
|
769
|
+
// Strip line numbers and arrows/hyphens from output
|
|
770
|
+
const cleanedLines = lines.map(line => {
|
|
771
|
+
// Skip grep context separator lines
|
|
772
|
+
if (line === '--') return null;
|
|
773
|
+
|
|
774
|
+
// Handle both cat -n (→) and grep (-n) formats
|
|
775
|
+
// Also handle grep with colon (:) for matching lines
|
|
776
|
+
const match = line.match(/^\s*\d+[→\-:](.*)/);
|
|
777
|
+
return match ? match[1] : line;
|
|
778
|
+
}).filter(line => line !== null);
|
|
779
|
+
const cleanedContent = cleanedLines.join('\n');
|
|
780
|
+
|
|
781
|
+
// Try to detect and highlight code based on content patterns
|
|
782
|
+
return StreamingRenderer.renderCodeWithHighlight(cleanedContent, esc);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Check for system reminder tags and format them specially
|
|
786
|
+
const systemReminderPattern = /<system-reminder>([\s\S]*?)<\/system-reminder>/g;
|
|
787
|
+
const systemReminders = [];
|
|
788
|
+
let contentWithoutReminders = trimmed;
|
|
789
|
+
|
|
790
|
+
let reminderMatch;
|
|
791
|
+
while ((reminderMatch = systemReminderPattern.exec(trimmed)) !== null) {
|
|
792
|
+
systemReminders.push(reminderMatch[1].trim());
|
|
793
|
+
contentWithoutReminders = contentWithoutReminders.replace(reminderMatch[0], '');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Clean up the content after removing reminders
|
|
797
|
+
contentWithoutReminders = contentWithoutReminders.trim();
|
|
798
|
+
|
|
799
|
+
// Check if this looks like a tool success message with formatted output
|
|
800
|
+
const successPatterns = [
|
|
801
|
+
/^Success\s+toolu_[\w]+$/m,
|
|
802
|
+
/^The file .* has been (updated|created|modified)/,
|
|
803
|
+
/^Here's the result of running `cat -n`/,
|
|
804
|
+
/^Applied \d+ edits? to/,
|
|
805
|
+
/^\w+ tool completed successfully/
|
|
806
|
+
];
|
|
807
|
+
|
|
808
|
+
const hasSuccessPattern = successPatterns.some(pattern => pattern.test(contentWithoutReminders));
|
|
809
|
+
|
|
810
|
+
if (hasSuccessPattern) {
|
|
811
|
+
const contentLines = contentWithoutReminders.split('\n');
|
|
812
|
+
let successEndIndex = -1;
|
|
813
|
+
let codeStartIndex = -1;
|
|
814
|
+
|
|
815
|
+
// Find the success message and where code starts
|
|
816
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
817
|
+
const line = contentLines[i];
|
|
818
|
+
if (line.match(/^Success\s+toolu_/)) {
|
|
819
|
+
successEndIndex = i;
|
|
820
|
+
// Look for the next non-empty line that contains code
|
|
821
|
+
for (let j = i + 1; j < contentLines.length; j++) {
|
|
822
|
+
if (contentLines[j].trim() && !contentLines[j].match(/^The file|^Here's the result/)) {
|
|
823
|
+
codeStartIndex = j;
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
break;
|
|
828
|
+
} else if (line.match(/^The file .* has been|^Applied \d+ edits? to|^Replaced|^Created|^Deleted/)) {
|
|
829
|
+
// For edit/write operations, code typically starts after the success message
|
|
830
|
+
// Look for "Here's the result" line or line numbers
|
|
831
|
+
for (let j = i + 1; j < contentLines.length; j++) {
|
|
832
|
+
if (contentLines[j].match(/^Here's the result|^\s*\d+→/)) {
|
|
833
|
+
// If it's "Here's the result", code starts on next line
|
|
834
|
+
if (contentLines[j].match(/^Here's the result/)) {
|
|
835
|
+
codeStartIndex = j + 1;
|
|
836
|
+
} else {
|
|
837
|
+
codeStartIndex = j;
|
|
838
|
+
}
|
|
839
|
+
break;
|
|
840
|
+
} else if (contentLines[j].trim() && !contentLines[j].match(/^cat -n|^Running/)) {
|
|
841
|
+
// If we find non-empty content that's not a command, assume it's code
|
|
842
|
+
codeStartIndex = j;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (codeStartIndex === -1) {
|
|
847
|
+
// No line numbers found, treat next content as code
|
|
848
|
+
codeStartIndex = i + 2;
|
|
849
|
+
}
|
|
850
|
+
successEndIndex = codeStartIndex - 1;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (codeStartIndex > 0 && codeStartIndex < contentLines.length) {
|
|
856
|
+
const beforeCode = contentLines.slice(0, codeStartIndex).join('\n');
|
|
857
|
+
let codeContent = contentLines.slice(codeStartIndex).join('\n');
|
|
858
|
+
|
|
859
|
+
// Check if code has line numbers and strip them
|
|
860
|
+
if (codeContent.match(/^\s*\d+→/m)) {
|
|
861
|
+
const codeLines = codeContent.split('\n');
|
|
862
|
+
codeContent = codeLines.map(line => {
|
|
863
|
+
const match = line.match(/^\s*\d+→(.*)/);
|
|
864
|
+
return match ? match[1] : line;
|
|
865
|
+
}).join('\n');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Build the formatted output
|
|
869
|
+
let html = '';
|
|
870
|
+
|
|
871
|
+
// Add success message
|
|
872
|
+
if (beforeCode.trim()) {
|
|
873
|
+
html += `<div style="color:var(--color-success);font-weight:600;margin-bottom:0.75rem;font-size:0.9rem">${esc(beforeCode.trim())}</div>`;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Add highlighted code
|
|
877
|
+
if (codeContent.trim()) {
|
|
878
|
+
html += StreamingRenderer.renderCodeWithHighlight(codeContent, esc);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Add system reminders if any
|
|
882
|
+
if (systemReminders.length > 0) {
|
|
883
|
+
html += StreamingRenderer.renderSystemReminders(systemReminders, esc);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return html;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// If there are system reminders but no success pattern, render them separately
|
|
891
|
+
if (systemReminders.length > 0) {
|
|
892
|
+
let html = '';
|
|
893
|
+
|
|
894
|
+
// Render the main content
|
|
895
|
+
if (contentWithoutReminders) {
|
|
896
|
+
// Check if remaining content looks like code
|
|
897
|
+
if (StreamingRenderer.detectCodeContent(contentWithoutReminders)) {
|
|
898
|
+
html += StreamingRenderer.renderCodeWithHighlight(contentWithoutReminders, esc);
|
|
899
|
+
} else {
|
|
900
|
+
html += `<pre class="tool-result-pre">${esc(contentWithoutReminders)}</pre>`;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Add system reminders
|
|
905
|
+
html += StreamingRenderer.renderSystemReminders(systemReminders, esc);
|
|
906
|
+
return html;
|
|
907
|
+
}
|
|
908
|
+
|
|
713
909
|
const allFilePaths = lines.length > 1 && lines.every(l => l.trim() === '' || l.trim().startsWith('/'));
|
|
714
910
|
if (allFilePaths && lines.filter(l => l.trim()).length > 0) {
|
|
715
911
|
const fileHtml = lines.filter(l => l.trim()).map(l => {
|
|
@@ -722,10 +918,146 @@ class StreamingRenderer {
|
|
|
722
918
|
return `<div style="padding:0.625rem 1rem">${fileHtml}</div>`;
|
|
723
919
|
}
|
|
724
920
|
|
|
921
|
+
// Check if this looks like code
|
|
922
|
+
const looksLikeCode = StreamingRenderer.detectCodeContent(trimmed);
|
|
923
|
+
if (looksLikeCode) {
|
|
924
|
+
return StreamingRenderer.renderCodeWithHighlight(trimmed, esc);
|
|
925
|
+
}
|
|
926
|
+
|
|
725
927
|
const displayContent = trimmed.length > 2000 ? trimmed.substring(0, 2000) + '\n... (truncated)' : trimmed;
|
|
726
928
|
return `<pre class="tool-result-pre">${esc(displayContent)}</pre>`;
|
|
727
929
|
}
|
|
728
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Render system reminders in a clean, formatted way
|
|
933
|
+
*/
|
|
934
|
+
static renderSystemReminders(reminders, esc) {
|
|
935
|
+
if (!reminders || reminders.length === 0) return '';
|
|
936
|
+
|
|
937
|
+
const reminderHtml = reminders.map(reminder => {
|
|
938
|
+
// Parse reminder content for better formatting
|
|
939
|
+
const lines = reminder.split('\n').filter(l => l.trim());
|
|
940
|
+
const formattedLines = lines.map(line => {
|
|
941
|
+
// Make key points stand out
|
|
942
|
+
if (line.includes('IMPORTANT:') || line.includes('WARNING:')) {
|
|
943
|
+
return `<div style="font-weight:600;color:var(--color-warning);margin:0.25rem 0">${esc(line)}</div>`;
|
|
944
|
+
}
|
|
945
|
+
return `<div style="margin:0.125rem 0">${esc(line)}</div>`;
|
|
946
|
+
}).join('');
|
|
947
|
+
|
|
948
|
+
return formattedLines;
|
|
949
|
+
}).join('');
|
|
950
|
+
|
|
951
|
+
return `
|
|
952
|
+
<div style="margin-top:1rem;padding:0.75rem;background:var(--color-bg-secondary);border-left:3px solid var(--color-info);border-radius:0.25rem;font-size:0.8rem;color:var(--color-text-secondary)">
|
|
953
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem">
|
|
954
|
+
<span style="color:var(--color-info)">ℹ</span>
|
|
955
|
+
<span style="font-weight:600;font-size:0.85rem;color:var(--color-text-primary)">System Reminder</span>
|
|
956
|
+
</div>
|
|
957
|
+
${reminderHtml}
|
|
958
|
+
</div>
|
|
959
|
+
`;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Detect if content looks like code
|
|
964
|
+
*/
|
|
965
|
+
static detectCodeContent(content) {
|
|
966
|
+
// Common code patterns
|
|
967
|
+
const codePatterns = [
|
|
968
|
+
/^\s*(function|const|let|var|class|import|export|async|await)/m, // JavaScript
|
|
969
|
+
/^\s*(def|class|import|from|if __name__|lambda|async def)/m, // Python
|
|
970
|
+
/^\s*(public|private|protected|class|interface|package|import)/m, // Java/TypeScript
|
|
971
|
+
/^\s*(<\?php|namespace|use|trait)/m, // PHP
|
|
972
|
+
/^\s*(#include|int main|void|struct|typedef)/m, // C/C++
|
|
973
|
+
/[{}\[\];()]/, // Brackets and semicolons
|
|
974
|
+
/=>|->|::/, // Arrow functions, pointers
|
|
975
|
+
];
|
|
976
|
+
|
|
977
|
+
return codePatterns.some(pattern => pattern.test(content));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Render code with basic syntax highlighting
|
|
982
|
+
*/
|
|
983
|
+
static renderCodeWithHighlight(code, esc) {
|
|
984
|
+
// Escape HTML first
|
|
985
|
+
let highlighted = esc(code);
|
|
986
|
+
|
|
987
|
+
// Detect if this is JSON and apply JSON-specific highlighting
|
|
988
|
+
const isJSON = (code.trim().startsWith('{') || code.trim().startsWith('[')) &&
|
|
989
|
+
code.includes('"') && (code.includes(':') || code.includes(','));
|
|
990
|
+
|
|
991
|
+
if (isJSON) {
|
|
992
|
+
// JSON-specific highlighting
|
|
993
|
+
const jsonHighlights = [
|
|
994
|
+
// Property names (keys) in quotes
|
|
995
|
+
{ pattern: /"([^"]+)"\s*:/g, replacement: '"<span style="color:#3b82f6;font-weight:600">$1</span>":' },
|
|
996
|
+
// String values
|
|
997
|
+
{ pattern: /:\s*"([^"]*)"/g, replacement: ': "<span style="color:#10b981">$1</span>"' },
|
|
998
|
+
// Numbers
|
|
999
|
+
{ pattern: /:\s*(\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, replacement: ': <span style="color:#f59e0b">$1</span>' },
|
|
1000
|
+
// Booleans and null
|
|
1001
|
+
{ pattern: /:\s*(true|false|null)/g, replacement: ': <span style="color:#ef4444">$1</span>' },
|
|
1002
|
+
// Array/object brackets
|
|
1003
|
+
{ pattern: /([\[\]{}])/g, replacement: '<span style="color:#6b7280;font-weight:600">$1</span>' },
|
|
1004
|
+
];
|
|
1005
|
+
|
|
1006
|
+
jsonHighlights.forEach(({ pattern, replacement }) => {
|
|
1007
|
+
highlighted = highlighted.replace(pattern, replacement);
|
|
1008
|
+
});
|
|
1009
|
+
} else {
|
|
1010
|
+
// General code syntax highlighting
|
|
1011
|
+
const highlights = [
|
|
1012
|
+
// Comments (do these first to avoid conflicts)
|
|
1013
|
+
{ pattern: /(\/\/[^\n]*)/g, replacement: '<span style="color:#6b7280;font-style:italic">$1</span>' },
|
|
1014
|
+
{ pattern: /(\/\*[\s\S]*?\*\/)/g, replacement: '<span style="color:#6b7280;font-style:italic">$1</span>' },
|
|
1015
|
+
{ pattern: /(#[^\n]*)/g, replacement: '<span style="color:#6b7280;font-style:italic">$1</span>' },
|
|
1016
|
+
|
|
1017
|
+
// Strings (improved to handle escaped quotes)
|
|
1018
|
+
{ pattern: /(["'])(?:[^\\]|\\.)*?\1/g, replacement: (match) => `<span style="color:#10b981">${match}</span>` },
|
|
1019
|
+
|
|
1020
|
+
// Template literals (backticks)
|
|
1021
|
+
{ pattern: /`([^`]*)`/g, replacement: '<span style="color:#10b981">`$1`</span>' },
|
|
1022
|
+
|
|
1023
|
+
// Keywords
|
|
1024
|
+
{ pattern: /\b(function|const|let|var|class|import|export|async|await|return|if|else|for|while|try|catch|throw|new|typeof|instanceof|this|super|switch|case|default|break|continue|do)\b/g,
|
|
1025
|
+
replacement: '<span style="color:#8b5cf6;font-weight:600">$1</span>' },
|
|
1026
|
+
{ pattern: /\b(def|class|import|from|return|if|elif|else|for|while|try|except|raise|with|as|lambda|pass|break|continue|yield|global|nonlocal)\b/g,
|
|
1027
|
+
replacement: '<span style="color:#8b5cf6;font-weight:600">$1</span>' },
|
|
1028
|
+
{ pattern: /\b(public|private|protected|static|final|abstract|interface|extends|implements|package|void|int|string|boolean|float|double|char)\b/g,
|
|
1029
|
+
replacement: '<span style="color:#8b5cf6;font-weight:600">$1</span>' },
|
|
1030
|
+
|
|
1031
|
+
// Type annotations
|
|
1032
|
+
{ pattern: /:\s*([A-Z][a-zA-Z0-9_]*)/g, replacement: ': <span style="color:#0891b2">$1</span>' },
|
|
1033
|
+
|
|
1034
|
+
// Numbers
|
|
1035
|
+
{ pattern: /\b(\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/g, replacement: '<span style="color:#f59e0b">$1</span>' },
|
|
1036
|
+
|
|
1037
|
+
// Booleans and null
|
|
1038
|
+
{ pattern: /\b(true|false|null|undefined|None|True|False|nil)\b/g, replacement: '<span style="color:#ef4444">$1</span>' },
|
|
1039
|
+
|
|
1040
|
+
// Function/method names (improved)
|
|
1041
|
+
{ pattern: /\b([a-zA-Z_][a-zA-Z0-9_]*)(?=\s*\()/g, replacement: '<span style="color:#3b82f6">$1</span>' },
|
|
1042
|
+
|
|
1043
|
+
// Operators
|
|
1044
|
+
{ pattern: /(===|!==|==|!=|<=|>=|&&|\|\||\+=|-=|\*=|\/=|%=|=>|->)/g, replacement: '<span style="color:#a855f7">$1</span>' },
|
|
1045
|
+
];
|
|
1046
|
+
|
|
1047
|
+
// Apply highlights
|
|
1048
|
+
highlights.forEach(({ pattern, replacement }) => {
|
|
1049
|
+
if (typeof replacement === 'function') {
|
|
1050
|
+
highlighted = highlighted.replace(pattern, replacement);
|
|
1051
|
+
} else {
|
|
1052
|
+
highlighted = highlighted.replace(pattern, replacement);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Use a dark theme that works well for code
|
|
1058
|
+
return `<pre style="background:#1e293b;padding:1rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.875rem;line-height:1.6;color:#e2e8f0;border:1px solid #334155;box-shadow:0 2px 4px rgba(0,0,0,0.1)">${highlighted}</pre>`;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
729
1061
|
/**
|
|
730
1062
|
* Static HTML version of parameter rendering
|
|
731
1063
|
*/
|
|
@@ -750,7 +1082,8 @@ class StreamingRenderer {
|
|
|
750
1082
|
if (Array.isArray(data)) {
|
|
751
1083
|
if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
|
|
752
1084
|
if (data.every(i => typeof i === 'string') && data.length <= 20) {
|
|
753
|
-
|
|
1085
|
+
// Render as an itemized list instead of inline badges
|
|
1086
|
+
return `<div style="display:flex;flex-direction:column;gap:0.125rem;${depth > 0 ? 'padding-left:1rem' : ''}">${data.map((i, idx) => `<div style="display:flex;align-items:center;gap:0.375rem"><span style="color:var(--color-text-secondary);font-size:0.65rem;opacity:0.5">•</span><span style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem">${esc(i)}</span></div>`).join('')}</div>`;
|
|
754
1087
|
}
|
|
755
1088
|
return `<div style="display:flex;flex-direction:column;gap:0.25rem;${depth > 0 ? 'padding-left:1rem' : ''}">${data.map((item, i) => `<div style="display:flex;gap:0.5rem;align-items:flex-start"><span style="color:var(--color-text-secondary);font-size:0.7rem;min-width:1.5rem;text-align:right;flex-shrink:0">${i}</span><div style="flex:1;min-width:0">${StreamingRenderer.renderParamsHTML(item, depth + 1, esc)}</div></div>`).join('')}</div>`;
|
|
756
1089
|
}
|
|
@@ -785,7 +1118,7 @@ class StreamingRenderer {
|
|
|
785
1118
|
<span class="status-label">${iconSvg} ${isError ? 'Error' : 'Success'}</span>
|
|
786
1119
|
${toolUseId ? `<span class="result-id">${this.escapeHtml(toolUseId)}</span>` : ''}
|
|
787
1120
|
</div>
|
|
788
|
-
${this.
|
|
1121
|
+
${StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this))}
|
|
789
1122
|
`;
|
|
790
1123
|
|
|
791
1124
|
return div;
|