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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.121",
3
+ "version": "1.0.123",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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
- // const fsbrowse = require('fsbrowse');
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({ conversation: conv }));
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"
@@ -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 = 'message message-assistant';
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
- const ts = document.createElement('div');
1131
- ts.className = 'message-timestamp';
1132
- ts.textContent = new Date(sessionChunkList[sessionChunkList.length - 1].created_at).toLocaleString();
1133
- messageDiv.appendChild(ts);
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
- return `<div style="display:flex;flex-wrap:wrap;gap:0.25rem">${data.map(i => `<span style="display:inline-block;padding:0.125rem 0.5rem;background:var(--color-bg-secondary);border-radius:1rem;font-size:0.7rem;font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(i)}</span>`).join('')}</div>`;
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
- const parsed = JSON.parse(trimmed);
708
- return `<div style="padding:0.625rem 1rem">${StreamingRenderer.renderParamsHTML(parsed, 0, esc)}</div>`;
709
- } catch (e) {}
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
- return `<div style="display:flex;flex-wrap:wrap;gap:0.25rem">${data.map(i => `<span style="display:inline-block;padding:0.125rem 0.5rem;background:var(--color-bg-secondary);border-radius:1rem;font-size:0.7rem;font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${esc(i)}</span>`).join('')}</div>`;
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.renderSmartContent(contentStr)}
1121
+ ${StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this))}
789
1122
  `;
790
1123
 
791
1124
  return div;