agentgui 1.0.814 → 1.0.816

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.
@@ -1,12 +1,6 @@
1
- /**
2
- * Streaming Renderer Engine
3
- * Manages real-time event processing, batching, and DOM rendering
4
- * for Claude Code streaming execution display
5
- */
6
1
 
7
2
  class StreamingRenderer {
8
3
  constructor(config = {}) {
9
- // Configuration
10
4
  this.config = {
11
5
  batchSize: config.batchSize || 50,
12
6
  batchInterval: config.batchInterval || 16, // ~60fps
@@ -17,7 +11,6 @@ class StreamingRenderer {
17
11
  ...config
18
12
  };
19
13
 
20
- // State
21
14
  this.eventQueue = [];
22
15
  this.eventHistory = [];
23
16
  this.isProcessing = false;
@@ -34,12 +27,10 @@ class StreamingRenderer {
34
27
  avgProcessTime: 0
35
28
  };
36
29
 
37
- // DOM references
38
30
  this.outputContainer = null;
39
31
  this.scrollContainer = null;
40
32
  this.virtualScroller = null;
41
33
 
42
- // Event listeners
43
34
  this.listeners = {
44
35
  'event:queued': [],
45
36
  'event:dequeued': [],
@@ -50,14 +41,10 @@ class StreamingRenderer {
50
41
  'error:render': []
51
42
  };
52
43
 
53
- // Performance monitoring
54
44
  this.observer = null;
55
45
  this.resizeObserver = null;
56
46
  }
57
47
 
58
- /**
59
- * Initialize the renderer with DOM elements
60
- */
61
48
  init(outputContainerId, scrollContainerId = null) {
62
49
  this.outputContainer = document.getElementById(outputContainerId);
63
50
  this.scrollContainer = scrollContainerId ? document.getElementById(scrollContainerId) : this.outputContainer;
@@ -71,9 +58,6 @@ class StreamingRenderer {
71
58
  return this;
72
59
  }
73
60
 
74
- /**
75
- * Setup scroll optimization and auto-scroll
76
- */
77
61
  setupScrollOptimization() {
78
62
  if (!this.scrollContainer) return;
79
63
  this._userScrolledUp = false;
@@ -85,23 +69,17 @@ class StreamingRenderer {
85
69
  });
86
70
  }
87
71
 
88
- /**
89
- * Queue an event for batch processing
90
- */
91
72
  queueEvent(event) {
92
73
  if (!event || typeof event !== 'object') return false;
93
74
 
94
- // Add timestamp if not present
95
75
  if (!event.timestamp) {
96
76
  event.timestamp = Date.now();
97
77
  }
98
78
 
99
- // Deduplication
100
79
  if (this.isDuplicate(event)) {
101
80
  return false;
102
81
  }
103
82
 
104
- // Queue size check
105
83
  if (this.eventQueue.length >= this.config.maxQueueSize) {
106
84
  console.warn('Event queue overflow, dropping oldest events');
107
85
  this.eventQueue.shift();
@@ -110,7 +88,6 @@ class StreamingRenderer {
110
88
  this.eventQueue.push(event);
111
89
  this.eventHistory.push(event);
112
90
 
113
- // Trim history
114
91
  if (this.eventHistory.length > this.config.maxEventHistory) {
115
92
  this.eventHistory.shift();
116
93
  }
@@ -120,18 +97,12 @@ class StreamingRenderer {
120
97
  return true;
121
98
  }
122
99
 
123
- /**
124
- * Check if event is a duplicate
125
- * For streaming_progress events, use seq+sessionId for precise dedup
126
- * For other events, use type+id or type+sessionId
127
- */
128
100
  isDuplicate(event) {
129
101
  const key = this.getEventKey(event);
130
102
  if (!key) return false;
131
103
 
132
104
  const lastSeq = this.dedupMap.get(key);
133
105
 
134
- // For streaming_progress with seq, compare seq numbers directly
135
106
  if (event.type === 'streaming_progress' && event.seq !== undefined && lastSeq !== undefined) {
136
107
  if (event.seq <= lastSeq) {
137
108
  return true; // Same or older seq = duplicate
@@ -140,7 +111,6 @@ class StreamingRenderer {
140
111
  return false;
141
112
  }
142
113
 
143
- // For other events, use time-based dedup
144
114
  const now = Date.now();
145
115
  if (lastSeq && typeof lastSeq === 'number' && lastSeq > now - 500) {
146
116
  return true; // Recent duplicate
@@ -156,30 +126,20 @@ class StreamingRenderer {
156
126
  return false;
157
127
  }
158
128
 
159
- /**
160
- * Generate deduplication key for event
161
- * Use sessionId:seq for streaming_progress, fallback to type:id
162
- */
163
129
  getEventKey(event) {
164
130
  if (!event.type) return null;
165
- // For streaming events, use sessionId as primary key
166
131
  if (event.sessionId) {
167
132
  return `${event.sessionId}:${event.type}`;
168
133
  }
169
134
  return `${event.type}:${event.id || ''}`;
170
135
  }
171
136
 
172
- /**
173
- * Schedule batch processing
174
- */
175
137
  scheduleBatchProcess() {
176
138
  if (this.isProcessing || this.batchTimer) return;
177
139
 
178
140
  if (this.eventQueue.length >= this.config.batchSize) {
179
- // Process immediately if batch is full
180
141
  this.processBatch();
181
142
  } else {
182
- // Schedule for later
183
143
  this.batchTimer = setTimeout(() => {
184
144
  this.batchTimer = null;
185
145
  if (this.eventQueue.length > 0) {
@@ -189,9 +149,6 @@ class StreamingRenderer {
189
149
  }
190
150
  }
191
151
 
192
- /**
193
- * Process queued events as a batch
194
- */
195
152
  processBatch() {
196
153
  if (this.isProcessing) return;
197
154
  if (this.eventQueue.length === 0) return;
@@ -204,12 +161,10 @@ class StreamingRenderer {
204
161
  this.emit('batch:start', { batchSize, queueLength: this.eventQueue.length });
205
162
 
206
163
  try {
207
- // Process and render batch
208
164
  const renderStart = performance.now();
209
165
  this.renderBatch(batch);
210
166
  const renderTime = performance.now() - renderStart;
211
167
 
212
- // Update metrics
213
168
  this.performanceMetrics.totalBatches++;
214
169
  this.performanceMetrics.totalEvents += batchSize;
215
170
  this.performanceMetrics.avgBatchSize = this.performanceMetrics.totalEvents / this.performanceMetrics.totalBatches;
@@ -221,7 +176,6 @@ class StreamingRenderer {
221
176
  metrics: this.performanceMetrics
222
177
  });
223
178
 
224
- // Process more if queue is still full
225
179
  if (this.eventQueue.length >= this.config.batchSize) {
226
180
  this.isProcessing = false;
227
181
  setImmediate(() => this.processBatch());
@@ -241,9 +195,6 @@ class StreamingRenderer {
241
195
  this.performanceMetrics.avgProcessTime = this.performanceMetrics.avgProcessTime || processTime;
242
196
  }
243
197
 
244
- /**
245
- * Render a batch of events
246
- */
247
198
  renderBatch(batch) {
248
199
  if (!this.outputContainer) return;
249
200
 
@@ -251,7 +202,6 @@ class StreamingRenderer {
251
202
  const renderStart = performance.now();
252
203
 
253
204
  try {
254
- // Create document fragment for batch
255
205
  const fragment = document.createDocumentFragment();
256
206
  let nodeCount = 0;
257
207
 
@@ -267,16 +217,13 @@ class StreamingRenderer {
267
217
  }
268
218
  }
269
219
 
270
- // Append all at once (minimizes reflows)
271
220
  if (nodeCount > 0) {
272
221
  this.outputContainer.appendChild(fragment);
273
222
  this.domNodeCount += nodeCount;
274
223
 
275
- // Nest tool result blocks inside their corresponding tool use blocks
276
224
  this.nestToolResultsInToolUses();
277
225
  }
278
226
 
279
- // Auto-scroll to bottom
280
227
  this.autoScroll();
281
228
 
282
229
  const renderTime = performance.now() - renderStart;
@@ -293,14 +240,10 @@ class StreamingRenderer {
293
240
  }
294
241
  }
295
242
 
296
- /**
297
- * Render a single event to DOM element
298
- */
299
243
  renderEvent(event) {
300
244
  if (!event.type) return null;
301
245
 
302
246
  try {
303
- // Handle block rendering from streaming_progress events
304
247
  if (event.type === 'streaming_progress' && event.block) {
305
248
  return this.renderBlock(event.block, event);
306
249
  }
@@ -331,8 +274,6 @@ class StreamingRenderer {
331
274
  case 'code_block':
332
275
  return this.renderCode(event);
333
276
  case 'thinking_block':
334
- // Thinking blocks are now rendered immediately via handleStreamingProgress
335
- // Don't render them here to avoid duplicates
336
277
  return null;
337
278
  case 'tool_use':
338
279
  return this.renderToolUse(event);
@@ -355,9 +296,6 @@ class StreamingRenderer {
355
296
  }
356
297
  }
357
298
 
358
- /**
359
- * Render Claude message blocks with beautiful styling
360
- */
361
299
  renderBlock(block, context = {}, targetContainer = null) {
362
300
  if (!block || !block.type) return null;
363
301
 
@@ -400,9 +338,6 @@ class StreamingRenderer {
400
338
  }
401
339
  }
402
340
 
403
- /**
404
- * Render text block with semantic HTML
405
- */
406
341
  renderBlockText(block, context, targetContainer = null) {
407
342
  const text = block.text || '';
408
343
  const isHtml = this.containsHtmlTags(text);
@@ -457,9 +392,6 @@ class StreamingRenderer {
457
392
  return cleaned;
458
393
  }
459
394
 
460
- /**
461
- * Parse markdown and render links, code, bold, italic
462
- */
463
395
  parseAndRenderMarkdown(text) {
464
396
  const esc = this.escapeHtml.bind(this);
465
397
  const lines = text.split('\n');
@@ -525,9 +457,6 @@ class StreamingRenderer {
525
457
  .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="text-decoration:underline;opacity:0.85" target="_blank">$1</a>');
526
458
  }
527
459
 
528
- /**
529
- * Render code block with syntax highlighting
530
- */
531
460
  renderBlockCode(block, context) {
532
461
  const div = document.createElement('div');
533
462
  div.className = 'block-code';
@@ -567,9 +496,6 @@ class StreamingRenderer {
567
496
  return div;
568
497
  }
569
498
 
570
- /**
571
- * Render thinking block (expandable), signature-aware
572
- */
573
499
  renderBlockThinking(block, context) {
574
500
  const thinking = block.thinking || '';
575
501
  const hasSignature = !!block.signature;
@@ -596,9 +522,6 @@ class StreamingRenderer {
596
522
  return div;
597
523
  }
598
524
 
599
- /**
600
- * Get a tool-specific icon SVG string
601
- */
602
525
  getToolIcon(toolName) {
603
526
  const icons = {
604
527
  Read: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"/></svg>',
@@ -616,9 +539,6 @@ class StreamingRenderer {
616
539
  return icons[toolName] || '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10.666a1 1 0 11-1.64-1.118L9.687 10H5a1 1 0 01-.82-1.573l7-10.666a1 1 0 011.12-.373z" clip-rule="evenodd"/></svg>';
617
540
  }
618
541
 
619
- /**
620
- * Render a file path with icon, directory breadcrumb, and filename
621
- */
622
542
  renderFilePath(filePath) {
623
543
  if (!filePath) return '';
624
544
  const parts = pathSplit(filePath);
@@ -627,9 +547,6 @@ class StreamingRenderer {
627
547
  return `<div class="tool-param-file"><span class="file-icon">&#128196;</span>${dir ? `<span class="file-dir">${this.escapeHtml(dir)}/</span>` : ''}<span class="file-name">${this.escapeHtml(fileName)}</span></div>`;
628
548
  }
629
549
 
630
- /**
631
- * Render smart tool parameters based on tool type
632
- */
633
550
  renderSmartParams(toolName, input) {
634
551
  if (!input || Object.keys(input).length === 0) return '';
635
552
 
@@ -707,7 +624,6 @@ class StreamingRenderer {
707
624
  html += `<div style="margin-bottom:0.5rem;font-size:0.75rem;color:var(--color-text-secondary)"><span style="opacity:0.7">⏱️</span> Timeout: ${Math.round(input.timeout / 1000)}s</div>`;
708
625
  }
709
626
 
710
- // Render code with syntax highlighting
711
627
  if (input.code) {
712
628
  const codeLines = input.code.split('\n');
713
629
  const lineCount = codeLines.length;
@@ -717,7 +633,6 @@ class StreamingRenderer {
717
633
  html += `<div style="margin-top:0.5rem"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.25rem"><span style="font-size:0.7rem;font-weight:600;color:#0891b2;text-transform:uppercase">${this.escapeHtml(lang)}</span><span style="font-size:0.7rem;color:var(--color-text-secondary)">${lineCount} lines</span></div>${StreamingRenderer.renderCodeWithHighlight(displayCode, this.escapeHtml.bind(this), true)}${truncated ? `<div style="font-size:0.7rem;color:var(--color-text-secondary);text-align:center;padding:0.25rem">... ${lineCount - 50} more lines</div>` : ''}</div>`;
718
634
  }
719
635
 
720
- // Render commands (bash commands)
721
636
  if (input.commands) {
722
637
  const cmds = Array.isArray(input.commands) ? input.commands : [input.commands];
723
638
  cmds.forEach(cmd => {
@@ -734,9 +649,6 @@ class StreamingRenderer {
734
649
  }
735
650
  }
736
651
 
737
- /**
738
- * Render content preview with truncation
739
- */
740
652
  renderContentPreview(content, label) {
741
653
  const maxLen = 500;
742
654
  const truncated = content.length > maxLen;
@@ -748,16 +660,10 @@ class StreamingRenderer {
748
660
  return `<div class="tool-param-content-preview" style="margin-top:0.5rem"><div class="preview-header"><span>${this.escapeHtml(label)}</span><span style="font-weight:400">${lineCount} lines${truncated ? ' (truncated)' : ''}</span></div>${codeBody}${truncated ? '<div class="preview-truncated">... ' + (content.length - maxLen) + ' more characters</div>' : ''}</div>`;
749
661
  }
750
662
 
751
- /**
752
- * Render params as formatted JSON (default fallback for unknown tools)
753
- */
754
663
  renderJsonParams(input) {
755
664
  return `<div class="tool-params">${this.renderParametersBeautiful(input)}</div>`;
756
665
  }
757
666
 
758
- /**
759
- * Render tool use block with smart parameter display
760
- */
761
667
  getToolUseTitle(toolName, input) {
762
668
  const normalizedName = toolName.replace(/^mcp__.*?__/, '');
763
669
  if (normalizedName === 'Edit' && input.file_path) {
@@ -834,9 +740,6 @@ class StreamingRenderer {
834
740
  return details;
835
741
  }
836
742
 
837
- /**
838
- * Render content smartly - detect JSON, images, file lists, markdown
839
- */
840
743
  renderSmartContent(contentStr) {
841
744
  const trimmed = contentStr.trim();
842
745
 
@@ -874,9 +777,6 @@ class StreamingRenderer {
874
777
  return `<div style="padding:0.625rem 1rem;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;white-space:pre-wrap;word-break:break-all;line-height:1.5">${this.escapeHtml(trimmed)}</div>`;
875
778
  }
876
779
 
877
- /**
878
- * Render parsed JSON/object as formatted key-value display
879
- */
880
780
  renderParametersBeautiful(data, depth = 0) {
881
781
  if (data === null || data === undefined) return `<span style="color:var(--color-text-secondary);font-style:italic">null</span>`;
882
782
  if (typeof data === 'boolean') return `<span style="color:#d97706;font-weight:600">${data}</span>`;
@@ -900,7 +800,6 @@ class StreamingRenderer {
900
800
  if (Array.isArray(data)) {
901
801
  if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
902
802
  if (data.every(i => typeof i === 'string') && data.length <= 20) {
903
- // Render as an itemized list instead of inline badges
904
803
  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>`;
905
804
  }
906
805
  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>`;
@@ -915,9 +814,6 @@ class StreamingRenderer {
915
814
  return `<span>${this.escapeHtml(String(data))}</span>`;
916
815
  }
917
816
 
918
- /**
919
- * Static HTML version of smart content rendering for use in string templates
920
- */
921
817
  static renderSmartContentHTML(contentStr, escapeHtml, flat = false) {
922
818
  const trimmed = contentStr.trim();
923
819
  const esc = escapeHtml || window._escHtml;
@@ -926,12 +822,10 @@ class StreamingRenderer {
926
822
  return `<div style="padding:0.5rem"><img src="${esc(trimmed)}" style="max-width:100%;max-height:24rem;border-radius:0.375rem" loading="lazy"></div>`;
927
823
  }
928
824
 
929
- // Parse JSON and render as structured content
930
825
  if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
931
826
  try {
932
827
  const parsed = JSON.parse(trimmed);
933
828
 
934
- // Handle Claude content block arrays: [{type:"text", text:"..."}]
935
829
  if (Array.isArray(parsed) && parsed.length > 0 && parsed[0] && parsed[0].type === 'text') {
936
830
  const textParts = parsed.filter(b => b.type === 'text' && b.text);
937
831
  if (textParts.length > 0) {
@@ -940,7 +834,6 @@ class StreamingRenderer {
940
834
  }
941
835
  }
942
836
 
943
- // Handle Claude image content block arrays: [{type:"image", source:{type:"base64", data:"...", media_type:"..."}}]
944
837
  if (Array.isArray(parsed) && parsed.length > 0) {
945
838
  const imgParts = parsed.filter(b => b.type === 'image' && b.source && b.source.type === 'base64' && b.source.data);
946
839
  if (imgParts.length > 0) {
@@ -951,36 +844,27 @@ class StreamingRenderer {
951
844
  }
952
845
  }
953
846
 
954
- // For other JSON, render as itemized key-value structure
955
847
  return `<div style="padding:0.5rem 0.75rem">${StreamingRenderer.renderParamsHTML(parsed, 0, esc)}</div>`;
956
848
  } catch (e) {
957
- // Not valid JSON, might be code with braces
958
849
  }
959
850
  }
960
851
 
961
- // Check if this looks like `cat -n` output or grep with line numbers
962
852
  const lines = trimmed.split('\n');
963
853
  const isCatNOutput = lines.length > 1 && lines[0].match(/^\s*\d+→/);
964
854
  const isGrepOutput = lines.length > 1 && lines[0].match(/^\s*\d+-/);
965
855
 
966
856
  if (isCatNOutput || isGrepOutput) {
967
- // Strip line numbers and arrows/hyphens from output
968
857
  const cleanedLines = lines.map(line => {
969
- // Skip grep context separator lines
970
858
  if (line === '--') return null;
971
859
 
972
- // Handle both cat -n (→) and grep (-n) formats
973
- // Also handle grep with colon (:) for matching lines
974
860
  const match = line.match(/^\s*\d+[→\-:](.*)/);
975
861
  return match ? match[1] : line;
976
862
  }).filter(line => line !== null);
977
863
  const cleanedContent = cleanedLines.join('\n');
978
864
 
979
- // Try to detect and highlight code based on content patterns
980
865
  return StreamingRenderer.renderCodeWithHighlight(cleanedContent, esc, flat);
981
866
  }
982
867
 
983
- // Check for system reminder tags and format them specially
984
868
  const systemReminderPattern = /<system-reminder>([\s\S]*?)<\/system-reminder>/g;
985
869
  const systemReminders = [];
986
870
  let contentWithoutReminders = trimmed;
@@ -991,10 +875,8 @@ class StreamingRenderer {
991
875
  contentWithoutReminders = contentWithoutReminders.replace(reminderMatch[0], '');
992
876
  }
993
877
 
994
- // Clean up the content after removing reminders
995
878
  contentWithoutReminders = contentWithoutReminders.trim();
996
879
 
997
- // Check if this looks like a tool success message with formatted output
998
880
  const successPatterns = [
999
881
  /^Success\s+toolu_[\w]+$/m,
1000
882
  /^The file .* has been (updated|created|modified)/,
@@ -1010,12 +892,10 @@ class StreamingRenderer {
1010
892
  let successEndIndex = -1;
1011
893
  let codeStartIndex = -1;
1012
894
 
1013
- // Find the success message and where code starts
1014
895
  for (let i = 0; i < contentLines.length; i++) {
1015
896
  const line = contentLines[i];
1016
897
  if (line.match(/^Success\s+toolu_/)) {
1017
898
  successEndIndex = i;
1018
- // Look for the next non-empty line that contains code
1019
899
  for (let j = i + 1; j < contentLines.length; j++) {
1020
900
  if (contentLines[j].trim() && !contentLines[j].match(/^The file|^Here's the result/)) {
1021
901
  codeStartIndex = j;
@@ -1024,11 +904,8 @@ class StreamingRenderer {
1024
904
  }
1025
905
  break;
1026
906
  } else if (line.match(/^The file .* has been|^Applied \d+ edits? to|^Replaced|^Created|^Deleted/)) {
1027
- // For edit/write operations, code typically starts after the success message
1028
- // Look for "Here's the result" line or line numbers
1029
907
  for (let j = i + 1; j < contentLines.length; j++) {
1030
908
  if (contentLines[j].match(/^Here's the result|^\s*\d+→/)) {
1031
- // If it's "Here's the result", code starts on next line
1032
909
  if (contentLines[j].match(/^Here's the result/)) {
1033
910
  codeStartIndex = j + 1;
1034
911
  } else {
@@ -1036,13 +913,11 @@ class StreamingRenderer {
1036
913
  }
1037
914
  break;
1038
915
  } else if (contentLines[j].trim() && !contentLines[j].match(/^cat -n|^Running/)) {
1039
- // If we find non-empty content that's not a command, assume it's code
1040
916
  codeStartIndex = j;
1041
917
  break;
1042
918
  }
1043
919
  }
1044
920
  if (codeStartIndex === -1) {
1045
- // No line numbers found, treat next content as code
1046
921
  codeStartIndex = i + 2;
1047
922
  }
1048
923
  successEndIndex = codeStartIndex - 1;
@@ -1054,7 +929,6 @@ class StreamingRenderer {
1054
929
  const beforeCode = contentLines.slice(0, codeStartIndex).join('\n');
1055
930
  let codeContent = contentLines.slice(codeStartIndex).join('\n');
1056
931
 
1057
- // Check if code has line numbers and strip them
1058
932
  if (codeContent.match(/^\s*\d+→/m)) {
1059
933
  const codeLines = codeContent.split('\n');
1060
934
  codeContent = codeLines.map(line => {
@@ -1063,20 +937,16 @@ class StreamingRenderer {
1063
937
  }).join('\n');
1064
938
  }
1065
939
 
1066
- // Build the formatted output
1067
940
  let html = '';
1068
941
 
1069
- // Add success message
1070
942
  if (beforeCode.trim()) {
1071
943
  html += `<div style="color:var(--color-success);font-weight:600;margin-bottom:0.75rem;font-size:0.9rem">${esc(beforeCode.trim())}</div>`;
1072
944
  }
1073
945
 
1074
- // Add highlighted code
1075
946
  if (codeContent.trim()) {
1076
947
  html += StreamingRenderer.renderCodeWithHighlight(codeContent, esc, flat);
1077
948
  }
1078
949
 
1079
- // Add system reminders if any
1080
950
  if (systemReminders.length > 0) {
1081
951
  html += StreamingRenderer.renderSystemReminders(systemReminders, esc);
1082
952
  }
@@ -1085,13 +955,10 @@ class StreamingRenderer {
1085
955
  }
1086
956
  }
1087
957
 
1088
- // If there are system reminders but no success pattern, render them separately
1089
958
  if (systemReminders.length > 0) {
1090
959
  let html = '';
1091
960
 
1092
- // Render the main content
1093
961
  if (contentWithoutReminders) {
1094
- // Check if remaining content looks like code
1095
962
  if (StreamingRenderer.detectCodeContent(contentWithoutReminders)) {
1096
963
  html += StreamingRenderer.renderCodeWithHighlight(contentWithoutReminders, esc, flat);
1097
964
  } else {
@@ -1099,7 +966,6 @@ class StreamingRenderer {
1099
966
  }
1100
967
  }
1101
968
 
1102
- // Add system reminders
1103
969
  html += StreamingRenderer.renderSystemReminders(systemReminders, esc);
1104
970
  return html;
1105
971
  }
@@ -1119,7 +985,6 @@ class StreamingRenderer {
1119
985
  return `<div style="padding:0.625rem 1rem">${fileHtml}</div>`;
1120
986
  }
1121
987
 
1122
- // Check if this looks like code
1123
988
  const looksLikeCode = StreamingRenderer.detectCodeContent(trimmed);
1124
989
  if (looksLikeCode) {
1125
990
  return StreamingRenderer.renderCodeWithHighlight(trimmed, esc, flat);
@@ -1129,17 +994,12 @@ class StreamingRenderer {
1129
994
  return `<pre class="tool-result-pre">${esc(displayContent)}</pre>`;
1130
995
  }
1131
996
 
1132
- /**
1133
- * Render system reminders in a clean, formatted way
1134
- */
1135
997
  static renderSystemReminders(reminders, esc) {
1136
998
  if (!reminders || reminders.length === 0) return '';
1137
999
 
1138
1000
  const reminderHtml = reminders.map(reminder => {
1139
- // Parse reminder content for better formatting
1140
1001
  const lines = reminder.split('\n').filter(l => l.trim());
1141
1002
  const formattedLines = lines.map(line => {
1142
- // Make key points stand out
1143
1003
  if (line.includes('IMPORTANT:') || line.includes('WARNING:')) {
1144
1004
  return `<div style="font-weight:600;color:var(--color-warning);margin:0.25rem 0">${esc(line)}</div>`;
1145
1005
  }
@@ -1160,11 +1020,7 @@ class StreamingRenderer {
1160
1020
  `;
1161
1021
  }
1162
1022
 
1163
- /**
1164
- * Detect if content looks like code
1165
- */
1166
1023
  static detectCodeContent(content) {
1167
- // Common code patterns
1168
1024
  const codePatterns = [
1169
1025
  /^\s*(function|const|let|var|class|import|export|async|await)/m, // JavaScript
1170
1026
  /^\s*(def|class|import|from|if __name__|lambda|async def)/m, // Python
@@ -1178,9 +1034,6 @@ class StreamingRenderer {
1178
1034
  return codePatterns.some(pattern => pattern.test(content));
1179
1035
  }
1180
1036
 
1181
- /**
1182
- * Render code with basic syntax highlighting
1183
- */
1184
1037
  static renderCodeWithHighlight(code, esc, flat = false) {
1185
1038
  const preStyle = "background:var(--color-bg-code);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:var(--color-code-text);border:1px solid var(--color-code-border);margin:0";
1186
1039
  const codeHtml = `<pre style="${preStyle}"><code class="lazy-hl">${esc(code)}</code></pre>`;
@@ -1239,9 +1092,6 @@ class StreamingRenderer {
1239
1092
  return '';
1240
1093
  }
1241
1094
 
1242
- /**
1243
- * Static HTML version of parameter rendering
1244
- */
1245
1095
  static renderParamsHTML(data, depth, esc) {
1246
1096
  if (data === null || data === undefined) return `<span style="color:var(--color-text-secondary);font-style:italic">null</span>`;
1247
1097
  if (typeof data === 'boolean') return `<span style="color:#d97706;font-weight:600">${data}</span>`;
@@ -1269,7 +1119,6 @@ class StreamingRenderer {
1269
1119
  if (Array.isArray(data)) {
1270
1120
  if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
1271
1121
  if (data.every(i => typeof i === 'string') && data.length <= 20) {
1272
- // Render as an itemized list instead of inline badges
1273
1122
  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>`;
1274
1123
  }
1275
1124
  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>`;
@@ -1284,9 +1133,6 @@ class StreamingRenderer {
1284
1133
  return `<span>${esc(String(data))}</span>`;
1285
1134
  }
1286
1135
 
1287
- /**
1288
- * Render tool result as inline content to be merged into preceding tool_use block
1289
- */
1290
1136
  renderBlockToolResult(block, context) {
1291
1137
  const content = block.content || '';
1292
1138
  const toolName = block.tool_name || block.name || '';
@@ -1311,9 +1157,6 @@ class StreamingRenderer {
1311
1157
  return container;
1312
1158
  }
1313
1159
 
1314
- /**
1315
- * Render image block
1316
- */
1317
1160
  renderBlockImage(block, context) {
1318
1161
  const div = document.createElement('div');
1319
1162
  div.className = 'block-image';
@@ -1322,7 +1165,6 @@ class StreamingRenderer {
1322
1165
  let src = block.image || block.src || '';
1323
1166
  const alt = block.alt || 'Image';
1324
1167
 
1325
- // Handle base64 data
1326
1168
  if (block.data && block.media_type) {
1327
1169
  src = `data:${block.media_type};base64,${block.data}`;
1328
1170
  }
@@ -1335,9 +1177,6 @@ class StreamingRenderer {
1335
1177
  return div;
1336
1178
  }
1337
1179
 
1338
- /**
1339
- * Render bash command block
1340
- */
1341
1180
  renderBlockBash(block, context) {
1342
1181
  const div = document.createElement('div');
1343
1182
  div.className = 'block-bash';
@@ -1346,10 +1185,8 @@ class StreamingRenderer {
1346
1185
  const command = block.command || block.code || '';
1347
1186
  const output = block.output || '';
1348
1187
 
1349
- // For the command, use simple escaping
1350
1188
  let html = `<div class="bash-command"><span class="prompt">$</span><code>${this.escapeHtml(command)}</code></div>`;
1351
1189
 
1352
- // For output, check if it looks like code and use syntax highlighting
1353
1190
  if (output) {
1354
1191
  if (StreamingRenderer.detectCodeContent(output)) {
1355
1192
  html += StreamingRenderer.renderCodeWithHighlight(output, this.escapeHtml.bind(this), true);
@@ -1362,9 +1199,6 @@ class StreamingRenderer {
1362
1199
  return div;
1363
1200
  }
1364
1201
 
1365
- /**
1366
- * Render system event
1367
- */
1368
1202
  renderBlockSystem(block, context) {
1369
1203
  if (block.subtype === 'compact_boundary') return this.renderCompactBoundary(block);
1370
1204
  if (block.subtype === 'turn_duration') return this.renderTurnDuration(block);
@@ -1396,9 +1230,6 @@ class StreamingRenderer {
1396
1230
  return details;
1397
1231
  }
1398
1232
 
1399
- /**
1400
- * Render result block (execution summary)
1401
- */
1402
1233
  renderBlockResult(block, context) {
1403
1234
  const isError = block.is_error || false;
1404
1235
  const duration = block.duration_ms ? (block.duration_ms / 1000).toFixed(1) + 's' : '';
@@ -1447,9 +1278,6 @@ class StreamingRenderer {
1447
1278
  return details;
1448
1279
  }
1449
1280
 
1450
- /**
1451
- * Render tool status block (ACP in_progress/pending updates)
1452
- */
1453
1281
  renderBlockToolStatus(block, context) {
1454
1282
  const status = block.status || 'pending';
1455
1283
  const statusIcons = {
@@ -1491,9 +1319,6 @@ class StreamingRenderer {
1491
1319
  return div;
1492
1320
  }
1493
1321
 
1494
- /**
1495
- * Render usage block (ACP usage updates)
1496
- */
1497
1322
  renderBlockUsage(block, context) {
1498
1323
  const usage = block.usage || {};
1499
1324
  const used = usage.used || 0;
@@ -1513,9 +1338,6 @@ class StreamingRenderer {
1513
1338
  return div;
1514
1339
  }
1515
1340
 
1516
- /**
1517
- * Render plan block (ACP plan updates)
1518
- */
1519
1341
  renderBlockPlan(block, context) {
1520
1342
  const entries = block.entries || [];
1521
1343
  if (entries.length === 0) return null;
@@ -1573,15 +1395,11 @@ class StreamingRenderer {
1573
1395
  return div;
1574
1396
  }
1575
1397
 
1576
- /**
1577
- * Render generic block with formatted key-value pairs
1578
- */
1579
1398
  renderBlockGeneric(block, context) {
1580
1399
  const div = document.createElement('div');
1581
1400
  div.className = 'block-generic';
1582
1401
  div.classList.add(this._getBlockTypeClass('generic'));
1583
1402
 
1584
- // Show key-value pairs instead of raw JSON
1585
1403
  const fieldsHtml = Object.entries(block)
1586
1404
  .filter(([key]) => key !== 'type')
1587
1405
  .map(([key, value]) => {
@@ -1605,9 +1423,6 @@ class StreamingRenderer {
1605
1423
  return div;
1606
1424
  }
1607
1425
 
1608
- /**
1609
- * Render block error
1610
- */
1611
1426
  renderBlockError(block, error) {
1612
1427
  const div = document.createElement('div');
1613
1428
  div.className = 'block-error';
@@ -1628,9 +1443,6 @@ class StreamingRenderer {
1628
1443
  return div;
1629
1444
  }
1630
1445
 
1631
- /**
1632
- * Render streaming start event
1633
- */
1634
1446
  renderStreamingStart(event) {
1635
1447
  const div = document.createElement('div');
1636
1448
  div.className = 'event-streaming-start card mb-3 p-4 alert alert-info';
@@ -1653,16 +1465,11 @@ class StreamingRenderer {
1653
1465
  return div;
1654
1466
  }
1655
1467
 
1656
- /**
1657
- * Render streaming progress event
1658
- */
1659
1468
  renderStreamingProgress(event) {
1660
- // If there's a block in the progress event, render it beautifully
1661
1469
  if (event.block) {
1662
1470
  return this.renderBlock(event.block, event);
1663
1471
  }
1664
1472
 
1665
- // Fallback: simple progress indicator
1666
1473
  const div = document.createElement('div');
1667
1474
  div.className = 'event-streaming-progress mb-2 p-2';
1668
1475
  div.dataset.eventId = event.id || '';
@@ -1680,9 +1487,6 @@ class StreamingRenderer {
1680
1487
  return div;
1681
1488
  }
1682
1489
 
1683
- /**
1684
- * Render streaming complete event with metadata
1685
- */
1686
1490
  renderStreamingComplete(event) {
1687
1491
  const div = document.createElement('div');
1688
1492
  div.className = 'event-streaming-complete card mb-3 p-4 alert alert-success rounded-lg';
@@ -1716,9 +1520,6 @@ class StreamingRenderer {
1716
1520
  return div;
1717
1521
  }
1718
1522
 
1719
- /**
1720
- * Detect if content is a base64-encoded image
1721
- */
1722
1523
  detectBase64Image(content) {
1723
1524
  if (!content || typeof content !== 'string') return null;
1724
1525
  const trimmed = content.trim();
@@ -1736,9 +1537,6 @@ class StreamingRenderer {
1736
1537
  return null;
1737
1538
  }
1738
1539
 
1739
- /**
1740
- * Render file read event
1741
- */
1742
1540
  renderFileRead(event) {
1743
1541
  const fileName = event.path ? event.path.split('/').pop() : 'unknown';
1744
1542
  const details = document.createElement('details');
@@ -1789,9 +1587,6 @@ class StreamingRenderer {
1789
1587
  return details;
1790
1588
  }
1791
1589
 
1792
- /**
1793
- * Render file write event
1794
- */
1795
1590
  renderFileWrite(event) {
1796
1591
  const fileName = event.path ? event.path.split('/').pop() : 'unknown';
1797
1592
  const details = document.createElement('details');
@@ -1817,9 +1612,6 @@ class StreamingRenderer {
1817
1612
  return details;
1818
1613
  }
1819
1614
 
1820
- /**
1821
- * Render git status event
1822
- */
1823
1615
  renderGitStatus(event) {
1824
1616
  const div = document.createElement('div');
1825
1617
  div.className = 'event-git-status card mb-3 p-4';
@@ -1850,9 +1642,6 @@ class StreamingRenderer {
1850
1642
  return div;
1851
1643
  }
1852
1644
 
1853
- /**
1854
- * Render command execution event
1855
- */
1856
1645
  renderCommand(event) {
1857
1646
  const command = event.command || '';
1858
1647
  const output = event.output || '';
@@ -1888,9 +1677,6 @@ class StreamingRenderer {
1888
1677
  return details;
1889
1678
  }
1890
1679
 
1891
- /**
1892
- * Render error event
1893
- */
1894
1680
  renderError(event) {
1895
1681
  const message = event.message || event.error || 'Unknown error';
1896
1682
  const severity = event.severity || 'error';
@@ -1949,9 +1735,6 @@ class StreamingRenderer {
1949
1735
  return parts;
1950
1736
  }
1951
1737
 
1952
- /**
1953
- * Render text block event - for backward compatibility
1954
- */
1955
1738
  renderText(event) {
1956
1739
  const div = document.createElement('div');
1957
1740
  div.className = 'event-text mb-3';
@@ -1992,7 +1775,6 @@ class StreamingRenderer {
1992
1775
  });
1993
1776
  div.innerHTML = html;
1994
1777
 
1995
- // Add copy button functionality
1996
1778
  div.querySelectorAll('.copy-code-btn').forEach(btn => {
1997
1779
  btn.addEventListener('click', () => {
1998
1780
  const codeElement = btn.closest('.mb-3')?.querySelector('code');
@@ -2010,9 +1792,6 @@ class StreamingRenderer {
2010
1792
  return div;
2011
1793
  }
2012
1794
 
2013
- /**
2014
- * Render code block event
2015
- */
2016
1795
  renderCode(event) {
2017
1796
  const div = document.createElement('div');
2018
1797
  div.className = 'event-code mb-3';
@@ -2022,7 +1801,6 @@ class StreamingRenderer {
2022
1801
  const code = event.code || event.content || '';
2023
1802
  const language = event.language || 'plaintext';
2024
1803
 
2025
- // Render HTML code blocks as actual HTML elements
2026
1804
  if (language === 'html') {
2027
1805
  div.innerHTML = `
2028
1806
  <div class="html-rendered-container alert alert-info text-xs mb-2">
@@ -2044,9 +1822,6 @@ class StreamingRenderer {
2044
1822
  return div;
2045
1823
  }
2046
1824
 
2047
- /**
2048
- * Render thinking block event
2049
- */
2050
1825
  renderThinking(event) {
2051
1826
  const div = document.createElement('div');
2052
1827
  div.className = 'event-thinking mb-3 p-4 alert rounded';
@@ -2063,11 +1838,7 @@ class StreamingRenderer {
2063
1838
  return div;
2064
1839
  }
2065
1840
 
2066
- /**
2067
- * Render tool use event - for backward compatibility
2068
- */
2069
1841
  renderToolUse(event) {
2070
- // Use the new block-based renderer for consistency
2071
1842
  const block = {
2072
1843
  type: 'tool_use',
2073
1844
  name: event.toolName || event.tool || 'unknown',
@@ -2146,11 +1917,7 @@ class StreamingRenderer {
2146
1917
  return div;
2147
1918
  }
2148
1919
 
2149
- /**
2150
- * Render generic event with formatted key-value pairs
2151
- */
2152
1920
  renderGeneric(event) {
2153
- // Check if this is actually a file read with base64 image content
2154
1921
  if ((event.content?.source?.type === 'base64' || event.content?.type === 'base64') && event.path) {
2155
1922
  return this.renderFileRead(event);
2156
1923
  }
@@ -2162,7 +1929,6 @@ class StreamingRenderer {
2162
1929
 
2163
1930
  const time = new Date(event.timestamp).toLocaleTimeString();
2164
1931
 
2165
- // Format event data as key-value pairs
2166
1932
  const fieldsHtml = Object.entries(event)
2167
1933
  .filter(([key]) => !['type', 'timestamp'].includes(key))
2168
1934
  .map(([key, value]) => {
@@ -2190,9 +1956,6 @@ class StreamingRenderer {
2190
1956
  return div;
2191
1957
  }
2192
1958
 
2193
- /**
2194
- * Nest tool result blocks inside their corresponding tool use blocks
2195
- */
2196
1959
  nestToolResultsInToolUses() {
2197
1960
  if (!this.outputContainer) return;
2198
1961
 
@@ -2238,9 +2001,6 @@ class StreamingRenderer {
2238
2001
  });
2239
2002
  }
2240
2003
 
2241
- /**
2242
- * Auto-scroll to bottom of container
2243
- */
2244
2004
  mergeResultIntoToolUse(toolUseEl, block) {
2245
2005
  const content = block.content || '';
2246
2006
  const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
@@ -2302,23 +2062,14 @@ class StreamingRenderer {
2302
2062
  this._userScrolledUp = false;
2303
2063
  }
2304
2064
 
2305
- /**
2306
- * Update DOM node count for monitoring
2307
- */
2308
2065
  updateDOMNodeCount() {
2309
2066
  this.domNodeCount = this.outputContainer?.querySelectorAll('[data-event-id]').length || 0;
2310
2067
  }
2311
2068
 
2312
- /**
2313
- * HTML escape utility
2314
- */
2315
2069
  escapeHtml(text) {
2316
2070
  return window._escHtml(text);
2317
2071
  }
2318
2072
 
2319
- /**
2320
- * Format file size for display
2321
- */
2322
2073
  formatFileSize(bytes) {
2323
2074
  if (bytes === 0) return '0 B';
2324
2075
  const k = 1024;
@@ -2327,17 +2078,11 @@ class StreamingRenderer {
2327
2078
  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
2328
2079
  }
2329
2080
 
2330
- /**
2331
- * Truncate content for display
2332
- */
2333
2081
  truncateContent(content, maxLength = 200) {
2334
2082
  if (content.length <= maxLength) return content;
2335
2083
  return content.substring(0, maxLength) + '...';
2336
2084
  }
2337
2085
 
2338
- /**
2339
- * Clear all rendered events
2340
- */
2341
2086
  clear() {
2342
2087
  if (this.outputContainer) {
2343
2088
  this.outputContainer.innerHTML = '';
@@ -2348,9 +2093,6 @@ class StreamingRenderer {
2348
2093
  this.dedupMap.clear();
2349
2094
  }
2350
2095
 
2351
- /**
2352
- * Get performance metrics
2353
- */
2354
2096
  getMetrics() {
2355
2097
  return {
2356
2098
  ...this.performanceMetrics,
@@ -2361,9 +2103,6 @@ class StreamingRenderer {
2361
2103
  };
2362
2104
  }
2363
2105
 
2364
- /**
2365
- * Add event listener
2366
- */
2367
2106
  on(event, callback) {
2368
2107
  if (!this.listeners[event]) {
2369
2108
  this.listeners[event] = [];
@@ -2371,9 +2110,6 @@ class StreamingRenderer {
2371
2110
  this.listeners[event].push(callback);
2372
2111
  }
2373
2112
 
2374
- /**
2375
- * Emit event to listeners
2376
- */
2377
2113
  emit(event, data) {
2378
2114
  if (this.listeners[event]) {
2379
2115
  this.listeners[event].forEach(callback => {
@@ -2386,10 +2122,6 @@ class StreamingRenderer {
2386
2122
  }
2387
2123
  }
2388
2124
 
2389
- /**
2390
- * Render block header with lazy-loading placeholder for body
2391
- * Returns a <details> element with just the summary, body content deferred
2392
- */
2393
2125
  renderBlockHeader(block, context = {}) {
2394
2126
  if (!block || !block.type) return null;
2395
2127
 
@@ -2421,7 +2153,6 @@ class StreamingRenderer {
2421
2153
  details.open = block.type === 'success' || (block.type === 'tool_result' && !block.is_error);
2422
2154
  details.appendChild(summary);
2423
2155
 
2424
- // Attach lazy loader on first open
2425
2156
  details.addEventListener('toggle', async (e) => {
2426
2157
  if (details.open && details.getAttribute('data-lazy-load') === 'pending') {
2427
2158
  details.setAttribute('data-lazy-load', 'loading');
@@ -2441,9 +2172,6 @@ class StreamingRenderer {
2441
2172
  return details;
2442
2173
  }
2443
2174
 
2444
- /**
2445
- * Cleanup resources
2446
- */
2447
2175
  destroy() {
2448
2176
  if (this.observer) {
2449
2177
  this.observer.disconnect();
@@ -2459,7 +2187,6 @@ class StreamingRenderer {
2459
2187
  }
2460
2188
  }
2461
2189
 
2462
- // Export for use in browser
2463
2190
  if (typeof module !== 'undefined' && module.exports) {
2464
2191
  module.exports = StreamingRenderer;
2465
2192
  }