agentgui 1.0.274 → 1.0.275

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.
Files changed (69) hide show
  1. package/CLAUDE.md +280 -280
  2. package/IPFS_DOWNLOADER.md +277 -277
  3. package/TASK_2C_COMPLETION.md +334 -334
  4. package/bin/gmgui.cjs +54 -54
  5. package/build-portable.js +3 -42
  6. package/database.js +1422 -1406
  7. package/lib/claude-runner.js +1130 -1130
  8. package/lib/ipfs-downloader.js +459 -459
  9. package/lib/speech.js +152 -152
  10. package/package.json +1 -1
  11. package/readme.md +76 -76
  12. package/server.js +3787 -3794
  13. package/setup-npm-token.sh +68 -68
  14. package/static/app.js +773 -773
  15. package/static/event-rendering-showcase.html +708 -708
  16. package/static/index.html +3178 -3180
  17. package/static/js/agent-auth.js +298 -298
  18. package/static/js/audio-recorder-processor.js +18 -18
  19. package/static/js/client.js +2656 -2656
  20. package/static/js/conversations.js +583 -583
  21. package/static/js/dialogs.js +267 -267
  22. package/static/js/event-consolidator.js +101 -101
  23. package/static/js/event-filter.js +311 -311
  24. package/static/js/event-processor.js +452 -452
  25. package/static/js/features.js +413 -413
  26. package/static/js/kalman-filter.js +67 -67
  27. package/static/js/progress-dialog.js +130 -130
  28. package/static/js/script-runner.js +219 -219
  29. package/static/js/streaming-renderer.js +2123 -2120
  30. package/static/js/syntax-highlighter.js +269 -269
  31. package/static/js/tts-websocket-handler.js +152 -152
  32. package/static/js/ui-components.js +431 -431
  33. package/static/js/voice.js +849 -849
  34. package/static/js/websocket-manager.js +596 -596
  35. package/static/templates/INDEX.html +465 -465
  36. package/static/templates/README.md +190 -190
  37. package/static/templates/agent-capabilities.html +56 -56
  38. package/static/templates/agent-metadata-panel.html +44 -44
  39. package/static/templates/agent-status-badge.html +30 -30
  40. package/static/templates/code-annotation-panel.html +155 -155
  41. package/static/templates/code-suggestion-panel.html +184 -184
  42. package/static/templates/command-header.html +77 -77
  43. package/static/templates/command-output-scrollable.html +118 -118
  44. package/static/templates/elapsed-time.html +54 -54
  45. package/static/templates/error-alert.html +106 -106
  46. package/static/templates/error-history-timeline.html +160 -160
  47. package/static/templates/error-recovery-options.html +109 -109
  48. package/static/templates/error-stack-trace.html +95 -95
  49. package/static/templates/error-summary.html +80 -80
  50. package/static/templates/event-counter.html +48 -48
  51. package/static/templates/execution-actions.html +97 -97
  52. package/static/templates/execution-progress-bar.html +80 -80
  53. package/static/templates/execution-stepper.html +120 -120
  54. package/static/templates/file-breadcrumb.html +118 -118
  55. package/static/templates/file-diff-viewer.html +121 -121
  56. package/static/templates/file-metadata.html +133 -133
  57. package/static/templates/file-read-panel.html +66 -66
  58. package/static/templates/file-write-panel.html +120 -120
  59. package/static/templates/git-branch-remote.html +107 -107
  60. package/static/templates/git-diff-list.html +101 -101
  61. package/static/templates/git-log-visualization.html +153 -153
  62. package/static/templates/git-status-panel.html +115 -115
  63. package/static/templates/quality-metrics-display.html +170 -170
  64. package/static/templates/terminal-output-panel.html +87 -87
  65. package/static/templates/test-results-display.html +144 -144
  66. package/static/theme.js +72 -72
  67. package/test-download-progress.js +223 -223
  68. package/test-websocket-broadcast.js +147 -147
  69. package/tests/ipfs-downloader.test.js +370 -370
@@ -1,2120 +1,2123 @@
1
- /**
2
- * Streaming Renderer Engine
3
- * Manages real-time event processing, batching, and DOM rendering
4
- * for Claude Code streaming execution display
5
- */
6
-
7
- function pathSplit(p) {
8
- return p.split(/[\/\\]/).filter(Boolean);
9
- }
10
-
11
- function pathBasename(p) {
12
- const parts = pathSplit(p);
13
- return parts.length ? parts.pop() : '';
14
- }
15
-
16
- class StreamingRenderer {
17
- constructor(config = {}) {
18
- // Configuration
19
- this.config = {
20
- batchSize: config.batchSize || 50,
21
- batchInterval: config.batchInterval || 16, // ~60fps
22
- maxQueueSize: config.maxQueueSize || 10000,
23
- maxEventHistory: config.maxEventHistory || 1000,
24
- virtualScrollThreshold: config.virtualScrollThreshold || 500,
25
- debounceDelay: config.debounceDelay || 100,
26
- ...config
27
- };
28
-
29
- // State
30
- this.eventQueue = [];
31
- this.eventHistory = [];
32
- this.isProcessing = false;
33
- this.batchTimer = null;
34
- this.dedupMap = new Map();
35
- this.renderCache = new Map();
36
- this.domNodeCount = 0;
37
- this.lastRenderTime = 0;
38
- this.performanceMetrics = {
39
- totalEvents: 0,
40
- totalBatches: 0,
41
- avgBatchSize: 0,
42
- avgRenderTime: 0,
43
- avgProcessTime: 0
44
- };
45
-
46
- // DOM references
47
- this.outputContainer = null;
48
- this.scrollContainer = null;
49
- this.virtualScroller = null;
50
-
51
- // Event listeners
52
- this.listeners = {
53
- 'event:queued': [],
54
- 'event:dequeued': [],
55
- 'batch:start': [],
56
- 'batch:complete': [],
57
- 'render:start': [],
58
- 'render:complete': [],
59
- 'error:render': []
60
- };
61
-
62
- // Performance monitoring
63
- this.observer = null;
64
- this.resizeObserver = null;
65
- }
66
-
67
- /**
68
- * Initialize the renderer with DOM elements
69
- */
70
- init(outputContainerId, scrollContainerId = null) {
71
- this.outputContainer = document.getElementById(outputContainerId);
72
- this.scrollContainer = scrollContainerId ? document.getElementById(scrollContainerId) : this.outputContainer;
73
-
74
- if (!this.outputContainer) {
75
- throw new Error(`Output container not found: ${outputContainerId}`);
76
- }
77
-
78
- this.setupDOMObserver();
79
- this.setupResizeObserver();
80
- this.setupScrollOptimization();
81
- StreamingRenderer._setupGlobalLazyHL();
82
- return this;
83
- }
84
-
85
- /**
86
- * Setup DOM mutation observer for external changes
87
- */
88
- setupDOMObserver() {
89
- }
90
-
91
- /**
92
- * Setup resize observer for viewport changes
93
- */
94
- setupResizeObserver() {
95
- }
96
-
97
- /**
98
- * Setup scroll optimization and auto-scroll
99
- */
100
- setupScrollOptimization() {
101
- if (!this.scrollContainer) return;
102
- this._userScrolledUp = false;
103
- this.scrollContainer.addEventListener('scroll', () => {
104
- if (this._programmaticScroll) return;
105
- const sc = this.scrollContainer;
106
- const distFromBottom = sc.scrollHeight - sc.scrollTop - sc.clientHeight;
107
- this._userScrolledUp = distFromBottom > 80;
108
- });
109
- }
110
-
111
- /**
112
- * Queue an event for batch processing
113
- */
114
- queueEvent(event) {
115
- if (!event || typeof event !== 'object') return false;
116
-
117
- // Add timestamp if not present
118
- if (!event.timestamp) {
119
- event.timestamp = Date.now();
120
- }
121
-
122
- // Deduplication
123
- if (this.isDuplicate(event)) {
124
- return false;
125
- }
126
-
127
- // Queue size check
128
- if (this.eventQueue.length >= this.config.maxQueueSize) {
129
- console.warn('Event queue overflow, dropping oldest events');
130
- this.eventQueue.shift();
131
- }
132
-
133
- this.eventQueue.push(event);
134
- this.eventHistory.push(event);
135
-
136
- // Trim history
137
- if (this.eventHistory.length > this.config.maxEventHistory) {
138
- this.eventHistory.shift();
139
- }
140
-
141
- this.emit('event:queued', { event, queueLength: this.eventQueue.length });
142
- this.scheduleBatchProcess();
143
- return true;
144
- }
145
-
146
- /**
147
- * Check if event is a duplicate
148
- */
149
- isDuplicate(event) {
150
- const key = this.getEventKey(event);
151
- if (!key) return false;
152
-
153
- const lastTime = this.dedupMap.get(key);
154
- const now = Date.now();
155
-
156
- if (lastTime && (now - lastTime) < 100) {
157
- return true;
158
- }
159
-
160
- this.dedupMap.set(key, now);
161
- if (this.dedupMap.size > 5000) {
162
- const cutoff = now - 1000;
163
- for (const [k, t] of this.dedupMap) {
164
- if (t < cutoff) this.dedupMap.delete(k);
165
- }
166
- }
167
- return false;
168
- }
169
-
170
- /**
171
- * Generate deduplication key for event
172
- */
173
- getEventKey(event) {
174
- if (!event.type) return null;
175
- return `${event.type}:${event.id || event.sessionId || ''}`;
176
- }
177
-
178
- /**
179
- * Schedule batch processing
180
- */
181
- scheduleBatchProcess() {
182
- if (this.isProcessing || this.batchTimer) return;
183
-
184
- if (this.eventQueue.length >= this.config.batchSize) {
185
- // Process immediately if batch is full
186
- this.processBatch();
187
- } else {
188
- // Schedule for later
189
- this.batchTimer = setTimeout(() => {
190
- this.batchTimer = null;
191
- if (this.eventQueue.length > 0) {
192
- this.processBatch();
193
- }
194
- }, this.config.batchInterval);
195
- }
196
- }
197
-
198
- /**
199
- * Process queued events as a batch
200
- */
201
- processBatch() {
202
- if (this.isProcessing) return;
203
- if (this.eventQueue.length === 0) return;
204
-
205
- this.isProcessing = true;
206
- const processStart = performance.now();
207
- const batchSize = Math.min(this.eventQueue.length, this.config.batchSize);
208
- const batch = this.eventQueue.splice(0, batchSize);
209
-
210
- this.emit('batch:start', { batchSize, queueLength: this.eventQueue.length });
211
-
212
- try {
213
- // Process and render batch
214
- const renderStart = performance.now();
215
- this.renderBatch(batch);
216
- const renderTime = performance.now() - renderStart;
217
-
218
- // Update metrics
219
- this.performanceMetrics.totalBatches++;
220
- this.performanceMetrics.totalEvents += batchSize;
221
- this.performanceMetrics.avgBatchSize = this.performanceMetrics.totalEvents / this.performanceMetrics.totalBatches;
222
- this.performanceMetrics.avgRenderTime = (this.performanceMetrics.avgRenderTime * (this.performanceMetrics.totalBatches - 1) + renderTime) / this.performanceMetrics.totalBatches;
223
-
224
- this.emit('batch:complete', {
225
- batchSize,
226
- renderTime,
227
- metrics: this.performanceMetrics
228
- });
229
-
230
- // Process more if queue is still full
231
- if (this.eventQueue.length >= this.config.batchSize) {
232
- this.isProcessing = false;
233
- setImmediate(() => this.processBatch());
234
- } else {
235
- this.isProcessing = false;
236
- if (this.eventQueue.length > 0) {
237
- this.scheduleBatchProcess();
238
- }
239
- }
240
- } catch (error) {
241
- console.error('Batch processing error:', error);
242
- this.isProcessing = false;
243
- this.emit('error:render', { error, batch });
244
- }
245
-
246
- const processTime = performance.now() - processStart;
247
- this.performanceMetrics.avgProcessTime = this.performanceMetrics.avgProcessTime || processTime;
248
- }
249
-
250
- /**
251
- * Render a batch of events
252
- */
253
- renderBatch(batch) {
254
- if (!this.outputContainer) return;
255
-
256
- this.emit('render:start', { eventCount: batch.length });
257
- const renderStart = performance.now();
258
-
259
- try {
260
- // Create document fragment for batch
261
- const fragment = document.createDocumentFragment();
262
- let nodeCount = 0;
263
-
264
- for (const event of batch) {
265
- try {
266
- const element = this.renderEvent(event);
267
- if (element) {
268
- fragment.appendChild(element);
269
- nodeCount++;
270
- }
271
- } catch (error) {
272
- console.error('Event render error:', error, event);
273
- }
274
- }
275
-
276
- // Append all at once (minimizes reflows)
277
- if (nodeCount > 0) {
278
- this.outputContainer.appendChild(fragment);
279
- this.domNodeCount += nodeCount;
280
- }
281
-
282
- // Auto-scroll to bottom
283
- this.autoScroll();
284
-
285
- const renderTime = performance.now() - renderStart;
286
- this.lastRenderTime = renderTime;
287
-
288
- this.emit('render:complete', {
289
- eventCount: batch.length,
290
- nodeCount,
291
- renderTime
292
- });
293
- } catch (error) {
294
- console.error('Batch render error:', error);
295
- this.emit('error:render', { error, batch });
296
- }
297
- }
298
-
299
- /**
300
- * Render a single event to DOM element
301
- */
302
- renderEvent(event) {
303
- if (!event.type) return null;
304
-
305
- try {
306
- // Handle block rendering from streaming_progress events
307
- if (event.type === 'streaming_progress' && event.block) {
308
- return this.renderBlock(event.block, event);
309
- }
310
-
311
- if (event.type === 'streaming_error' && event.isPrematureEnd) {
312
- return this.renderBlockPremature({ type: 'premature', error: event.error, exitCode: event.exitCode });
313
- }
314
-
315
- switch (event.type) {
316
- case 'streaming_start':
317
- return this.renderStreamingStart(event);
318
- case 'streaming_progress':
319
- return this.renderStreamingProgress(event);
320
- case 'streaming_complete':
321
- return this.renderStreamingComplete(event);
322
- case 'file_read':
323
- return this.renderFileRead(event);
324
- case 'file_write':
325
- return this.renderFileWrite(event);
326
- case 'git_status':
327
- return this.renderGitStatus(event);
328
- case 'command_execute':
329
- return this.renderCommand(event);
330
- case 'error':
331
- return this.renderError(event);
332
- case 'text_block':
333
- return this.renderText(event);
334
- case 'code_block':
335
- return this.renderCode(event);
336
- case 'thinking_block':
337
- return this.renderThinking(event);
338
- case 'tool_use':
339
- return this.renderToolUse(event);
340
- default:
341
- return this.renderGeneric(event);
342
- }
343
- } catch (error) {
344
- console.error('Event render error:', error, event);
345
- return this.renderError({ message: error.message, event });
346
- }
347
- }
348
-
349
- /**
350
- * Render Claude message blocks with beautiful styling
351
- */
352
- renderBlock(block, context = {}, targetContainer = null) {
353
- if (!block || !block.type) return null;
354
-
355
- try {
356
- switch (block.type) {
357
- case 'text':
358
- return this.renderBlockText(block, context, targetContainer);
359
- case 'code':
360
- return this.renderBlockCode(block, context);
361
- case 'thinking':
362
- return this.renderBlockThinking(block, context);
363
- case 'tool_use':
364
- return this.renderBlockToolUse(block, context);
365
- case 'tool_result':
366
- return this.renderBlockToolResult(block, context);
367
- case 'image':
368
- return this.renderBlockImage(block, context);
369
- case 'bash':
370
- return this.renderBlockBash(block, context);
371
- case 'system':
372
- return this.renderBlockSystem(block, context);
373
- case 'result':
374
- return this.renderBlockResult(block, context);
375
- case 'tool_status':
376
- return this.renderBlockToolStatus(block, context);
377
- case 'usage':
378
- return this.renderBlockUsage(block, context);
379
- case 'plan':
380
- return this.renderBlockPlan(block, context);
381
- case 'premature':
382
- return this.renderBlockPremature(block, context);
383
- default:
384
- return this.renderBlockGeneric(block, context);
385
- }
386
- } catch (error) {
387
- console.error('Block render error:', error, block);
388
- return this.renderBlockError(block, error);
389
- }
390
- }
391
-
392
- /**
393
- * Render text block with semantic HTML
394
- */
395
- renderBlockText(block, context, targetContainer = null) {
396
- const text = block.text || '';
397
- const isHtml = this.containsHtmlTags(text);
398
- const cached = this.renderCache.get(text);
399
- const html = cached || (isHtml ? this.sanitizeHtml(text) : this.parseAndRenderMarkdown(text));
400
-
401
- if (!cached && this.renderCache.size < 2000) {
402
- this.renderCache.set(text, html);
403
- }
404
-
405
- const container = targetContainer || this.outputContainer;
406
- const lastChild = container && container.lastElementChild;
407
- if (lastChild && lastChild.classList.contains('block-text') && !isHtml && !lastChild.classList.contains('html-content')) {
408
- lastChild.innerHTML += html;
409
- return null;
410
- }
411
-
412
- const div = document.createElement('div');
413
- div.className = 'block-text';
414
- if (isHtml) div.classList.add('html-content');
415
- div.innerHTML = html;
416
- div.classList.add(this._getBlockTypeClass('text'));
417
- return div;
418
- }
419
-
420
- _getBlockTypeClass(blockType) {
421
- const validTypes = ['text','tool_use','tool_result','code','thinking','bash','system','result','error','image','plan','usage','premature','tool_status','generic'];
422
- return validTypes.includes(blockType) ? `block-type-${blockType}` : 'block-type-generic';
423
- }
424
-
425
- _getToolColorClass(toolName) {
426
- const n = (toolName || '').replace(/^mcp__[^_]+__/, '').toLowerCase();
427
- const map = { read: 'read', write: 'write', edit: 'edit', bash: 'bash', glob: 'glob', grep: 'grep', webfetch: 'web', websearch: 'web', todowrite: 'todo', task: 'task', notebookedit: 'edit' };
428
- return `tool-color-${map[n] || 'default'}`;
429
- }
430
-
431
- containsHtmlTags(text) {
432
- const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
433
- return htmlPattern.test(text);
434
- }
435
-
436
- sanitizeHtml(html) {
437
- const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
438
- let cleaned = html.replace(dangerous, '');
439
- cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
440
- cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
441
- cleaned = cleaned.replace(/javascript\s*:/gi, '');
442
- return cleaned;
443
- }
444
-
445
- /**
446
- * Parse markdown and render links, code, bold, italic
447
- */
448
- parseAndRenderMarkdown(text) {
449
- let html = this.escapeHtml(text);
450
-
451
- // Render markdown bold: **text** -> <strong>text</strong>
452
- html = html.replace(/\*\*([^*]+)\*\*/g, '<strong class="font-semibold text-gray-900 dark:text-gray-100">$1</strong>');
453
-
454
- // Render markdown italic: *text* or _text_
455
- html = html.replace(/\*([^*]+)\*/g, '<em class="italic text-gray-700 dark:text-gray-300">$1</em>');
456
- html = html.replace(/_([^_]+)_/g, '<em class="italic text-gray-700 dark:text-gray-300">$1</em>');
457
-
458
- // Render inline code: `code`
459
- html = html.replace(/`([^`]+)`/g, '<code class="inline-code bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm font-mono text-red-600 dark:text-red-400">$1</code>');
460
-
461
- // Render markdown links: [text](url)
462
- html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-blue-600 dark:text-blue-400 hover:underline" target="_blank">$1</a>');
463
-
464
- // Convert line breaks
465
- html = html.replace(/\n/g, '<br>');
466
-
467
- return html;
468
- }
469
-
470
- /**
471
- * Render code block with syntax highlighting
472
- */
473
- renderBlockCode(block, context) {
474
- const div = document.createElement('div');
475
- div.className = 'block-code';
476
- div.classList.add(this._getBlockTypeClass('code'));
477
-
478
- const code = block.code || '';
479
- const language = (block.language || 'plaintext').toLowerCase();
480
- const lineCount = code.split('\n').length;
481
-
482
- const header = document.createElement('div');
483
- header.className = 'code-block-header';
484
- header.innerHTML = `
485
- <span class="collapsible-code-label">${this.escapeHtml(language)} - ${lineCount} line${lineCount !== 1 ? 's' : ''}</span>
486
- <button class="copy-code-btn" title="Copy code">
487
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
488
- </button>
489
- `;
490
-
491
- const copyBtn = header.querySelector('.copy-code-btn');
492
- copyBtn.addEventListener('click', (e) => {
493
- e.preventDefault();
494
- e.stopPropagation();
495
- navigator.clipboard.writeText(code).then(() => {
496
- const orig = copyBtn.innerHTML;
497
- copyBtn.innerHTML = '<svg viewBox="0 0 20 20" fill="#34d399"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
498
- setTimeout(() => { copyBtn.innerHTML = orig; }, 2000);
499
- });
500
- });
501
-
502
- const preStyle = "background:#1e293b;padding:1rem;border-radius:0 0 0.375rem 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;border-top:none;margin:0";
503
- const codeContainer = document.createElement('div');
504
- codeContainer.innerHTML = `<pre style="${preStyle}"><code class="lazy-hl">${this.escapeHtml(code)}</code></pre>`;
505
-
506
- div.appendChild(header);
507
- div.appendChild(codeContainer);
508
-
509
- return div;
510
- }
511
-
512
- /**
513
- * Render thinking block (expandable)
514
- */
515
- renderBlockThinking(block, context) {
516
- const div = document.createElement('div');
517
- div.className = 'block-thinking';
518
- div.classList.add(this._getBlockTypeClass('thinking'));
519
-
520
- const thinking = block.thinking || '';
521
- div.innerHTML = `
522
- <details open>
523
- <summary>
524
- <svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
525
- <span>Thinking Process</span>
526
- </summary>
527
- <div class="thinking-content">${this.escapeHtml(thinking)}</div>
528
- </details>
529
- `;
530
-
531
- return div;
532
- }
533
-
534
- /**
535
- * Get a tool-specific icon SVG string
536
- */
537
- getToolIcon(toolName) {
538
- const icons = {
539
- 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>',
540
- Write: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>',
541
- Edit: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"/></svg>',
542
- Bash: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3.293 1.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L7.586 10 5.293 7.707a1 1 0 010-1.414zM11 12a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg>',
543
- Glob: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/></svg>',
544
- Grep: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>',
545
- WebFetch: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 012 2v1a2 2 0 01-2 2 2 2 0 01-2 2v.5a6.003 6.003 0 01-6.668-7.473z" clip-rule="evenodd"/></svg>',
546
- WebSearch: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>',
547
- TodoWrite: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 011 1v3.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 111.414-1.414L6 11.586V8a1 1 0 011-1z" clip-rule="evenodd"/></svg>',
548
- Task: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg>',
549
- NotebookEdit: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z"/></svg>'
550
- };
551
- 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>';
552
- }
553
-
554
- /**
555
- * Render a file path with icon, directory breadcrumb, and filename
556
- */
557
- renderFilePath(filePath) {
558
- if (!filePath) return '';
559
- const parts = pathSplit(filePath);
560
- const fileName = parts.pop();
561
- const dir = parts.join('/');
562
- 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>`;
563
- }
564
-
565
- /**
566
- * Render smart tool parameters based on tool type
567
- */
568
- renderSmartParams(toolName, input) {
569
- if (!input || Object.keys(input).length === 0) return '';
570
-
571
- const normalizedName = toolName.replace(/^mcp__[^_]+__/, '');
572
-
573
- switch (normalizedName) {
574
- case 'Read':
575
- return `<div class="tool-params">${this.renderFilePath(input.file_path)}${input.offset ? `<div style="margin-top:0.375rem;font-size:0.75rem;color:var(--color-text-secondary)">Lines ${input.offset}${input.limit ? '–' + (input.offset + input.limit) : '+'}</div>` : ''}</div>`;
576
-
577
- case 'Write':
578
- return `<div class="tool-params">${this.renderFilePath(input.file_path)}${input.content ? this.renderContentPreview(input.content, 'Content') : ''}</div>`;
579
-
580
- case 'Edit': {
581
- let html = `<div class="tool-params">${this.renderFilePath(input.file_path)}`;
582
- if (input.old_string || input.new_string) {
583
- html += `<div class="tool-param-diff" style="margin-top:0.5rem">`;
584
- if (input.old_string) {
585
- html += `<div class="diff-header">Remove</div><div class="diff-old">${this.escapeHtml(this.truncateContent(input.old_string, 500))}</div>`;
586
- }
587
- if (input.new_string) {
588
- html += `<div class="diff-header">Add</div><div class="diff-new">${this.escapeHtml(this.truncateContent(input.new_string, 500))}</div>`;
589
- }
590
- html += '</div>';
591
- }
592
- return html + '</div>';
593
- }
594
-
595
- case 'Bash': {
596
- const cmd = input.command || input.commands || '';
597
- const cmdText = typeof cmd === 'string' ? cmd : JSON.stringify(cmd);
598
- let html = `<div class="tool-params"><div class="tool-param-command"><span class="prompt-char">$</span><span class="command-text">${this.escapeHtml(cmdText)}</span></div>`;
599
- if (input.description) html += `<div style="margin-top:0.375rem;font-size:0.75rem;color:var(--color-text-secondary)">${this.escapeHtml(input.description)}</div>`;
600
- return html + '</div>';
601
- }
602
-
603
- case 'Glob':
604
- return `<div class="tool-params"><div class="tool-param-query"><span class="query-icon">&#128193;</span><code style="font-size:0.85rem">${this.escapeHtml(input.pattern || '')}</code></div>${input.path ? `<div style="margin-top:0.25rem;font-size:0.75rem;color:var(--color-text-secondary)">in ${this.escapeHtml(input.path)}</div>` : ''}</div>`;
605
-
606
- case 'Grep':
607
- return `<div class="tool-params"><div class="tool-param-query"><span class="query-icon">&#128269;</span><code style="font-size:0.85rem">${this.escapeHtml(input.pattern || '')}</code></div>${input.path ? `<div style="margin-top:0.25rem;font-size:0.75rem;color:var(--color-text-secondary)">in ${this.escapeHtml(input.path)}</div>` : ''}${input.glob ? `<div style="margin-top:0.125rem;font-size:0.7rem;color:var(--color-text-secondary)">files: ${this.escapeHtml(input.glob)}</div>` : ''}</div>`;
608
-
609
- case 'WebFetch':
610
- return `<div class="tool-params"><div class="tool-param-url"><span class="url-icon">&#127760;</span>${this.escapeHtml(input.url || '')}</div>${input.prompt ? `<div style="margin-top:0.375rem;font-size:0.8rem;color:var(--color-text-secondary)">${this.escapeHtml(this.truncateContent(input.prompt, 150))}</div>` : ''}</div>`;
611
-
612
- case 'WebSearch':
613
- return `<div class="tool-params"><div class="tool-param-query"><span class="query-icon">&#128269;</span><strong style="font-size:0.85rem">${this.escapeHtml(input.query || '')}</strong></div></div>`;
614
-
615
- case 'TodoWrite':
616
- if (input.todos && Array.isArray(input.todos)) {
617
- const statusIcons = { completed: '&#9989;', in_progress: '&#9881;', pending: '&#9744;' };
618
- const items = input.todos.map(t => `<div class="todo-item"><span class="todo-status">${statusIcons[t.status] || '&#9744;'}</span><span class="todo-text">${this.escapeHtml(t.content || '')}</span></div>`).join('');
619
- return `<div class="tool-params"><div class="tool-param-todos">${items}</div></div>`;
620
- }
621
- return this.renderJsonParams(input);
622
-
623
- case 'Task':
624
- return `<div class="tool-params">${input.description ? `<div style="font-weight:600;font-size:0.85rem;margin-bottom:0.375rem">${this.escapeHtml(input.description)}</div>` : ''}${input.prompt ? `<div style="font-size:0.8rem;color:var(--color-text-secondary);max-height:100px;overflow-y:auto;white-space:pre-wrap;word-break:break-word">${this.escapeHtml(this.truncateContent(input.prompt, 300))}</div>` : ''}${input.subagent_type ? `<div style="margin-top:0.375rem;font-size:0.7rem"><code style="background:var(--color-bg-secondary);padding:0.125rem 0.375rem;border-radius:0.25rem">${this.escapeHtml(input.subagent_type)}</code></div>` : ''}</div>`;
625
-
626
- case 'NotebookEdit':
627
- return `<div class="tool-params">${this.renderFilePath(input.notebook_path)}${input.new_source ? this.renderContentPreview(input.new_source, 'Cell content') : ''}</div>`;
628
-
629
- case 'dev__execute':
630
- case 'dev_execute':
631
- case 'execute': {
632
- let html = '<div class="tool-params">';
633
-
634
- if (input.workingDirectory) {
635
- html += `<div style="margin-bottom:0.5rem;font-size:0.75rem;color:var(--color-text-secondary)"><span style="opacity:0.7">📁</span> ${this.escapeHtml(input.workingDirectory)}</div>`;
636
- }
637
-
638
- if (input.timeout) {
639
- 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>`;
640
- }
641
-
642
- // Render code with syntax highlighting
643
- if (input.code) {
644
- const codeLines = input.code.split('\n');
645
- const lineCount = codeLines.length;
646
- const truncated = lineCount > 50;
647
- const displayCode = truncated ? codeLines.slice(0, 50).join('\n') : input.code;
648
- const lang = input.runtime || 'javascript';
649
- 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>`;
650
- }
651
-
652
- // Render commands (bash commands)
653
- if (input.commands) {
654
- const cmds = Array.isArray(input.commands) ? input.commands : [input.commands];
655
- cmds.forEach(cmd => {
656
- html += `<div style="margin-top:0.375rem"><div class="tool-param-command"><span class="prompt-char">$</span><span class="command-text">${this.escapeHtml(typeof cmd === 'string' ? cmd : JSON.stringify(cmd))}</span></div></div>`;
657
- });
658
- }
659
-
660
- html += '</div>';
661
- return html;
662
- }
663
-
664
- default:
665
- return this.renderJsonParams(input);
666
- }
667
- }
668
-
669
- /**
670
- * Render content preview with truncation
671
- */
672
- renderContentPreview(content, label) {
673
- const maxLen = 500;
674
- const truncated = content.length > maxLen;
675
- const displayContent = truncated ? content.substring(0, maxLen) : content;
676
- const lineCount = content.split('\n').length;
677
- const codeBody = StreamingRenderer.detectCodeContent(displayContent)
678
- ? StreamingRenderer.renderCodeWithHighlight(displayContent, this.escapeHtml.bind(this), true)
679
- : `<div class="preview-body">${this.escapeHtml(displayContent)}</div>`;
680
- 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>`;
681
- }
682
-
683
- /**
684
- * Render params as formatted JSON (default fallback for unknown tools)
685
- */
686
- renderJsonParams(input) {
687
- return `<div class="tool-params">${this.renderParametersBeautiful(input)}</div>`;
688
- }
689
-
690
- /**
691
- * Render tool use block with smart parameter display
692
- */
693
- getToolUseTitle(toolName, input) {
694
- const normalizedName = toolName.replace(/^mcp__[^_]+__/, '');
695
- if (normalizedName === 'Edit' && input.file_path) {
696
- const parts = pathSplit(input.file_path);
697
- const fileName = parts.pop();
698
- const dir = parts.slice(-2).join('/');
699
- return dir ? `${dir}/${fileName}` : fileName;
700
- }
701
- if (normalizedName === 'Read' && input.file_path) {
702
- return pathBasename(input.file_path);
703
- }
704
- if (normalizedName === 'Write' && input.file_path) {
705
- return pathBasename(input.file_path);
706
- }
707
- if (normalizedName === 'Bash' || normalizedName === 'bash') {
708
- const cmd = input.command || input.commands || '';
709
- const cmdText = typeof cmd === 'string' ? cmd : JSON.stringify(cmd);
710
- return cmdText.length > 60 ? cmdText.substring(0, 57) + '...' : cmdText;
711
- }
712
- if (normalizedName === 'Glob' && input.pattern) return input.pattern;
713
- if (normalizedName === 'Grep' && input.pattern) return input.pattern;
714
- if (normalizedName === 'WebFetch' && input.url) {
715
- try { return new URL(input.url).hostname; } catch (e) { return input.url.substring(0, 40); }
716
- }
717
- if (normalizedName === 'WebSearch' && input.query) return input.query.substring(0, 50);
718
- if (input.file_path) return pathBasename(input.file_path);
719
- if (input.command) {
720
- const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command);
721
- return c.length > 50 ? c.substring(0, 47) + '...' : c;
722
- }
723
- if (input.query) return input.query.substring(0, 50);
724
- return '';
725
- }
726
-
727
- getToolUseDisplayName(toolName) {
728
- const normalized = toolName.replace(/^mcp__[^_]+__/, '');
729
- const knownTools = ['Read','Write','Edit','Bash','Glob','Grep','WebFetch','WebSearch','TodoWrite','Task','NotebookEdit'];
730
- if (knownTools.includes(normalized)) return normalized;
731
- if (toolName.startsWith('mcp__')) {
732
- const parts = toolName.split('__');
733
- return parts.length >= 3 ? parts[2] : parts[parts.length - 1];
734
- }
735
- return normalized || toolName;
736
- }
737
-
738
- renderBlockToolUse(block, context) {
739
- const toolName = block.name || 'unknown';
740
- const input = block.input || {};
741
-
742
- const details = document.createElement('details');
743
- details.className = 'block-tool-use folded-tool';
744
- if (block.id) details.dataset.toolUseId = block.id;
745
- details.classList.add(this._getBlockTypeClass('tool_use'));
746
- details.classList.add(this._getToolColorClass(toolName));
747
- const summary = document.createElement('summary');
748
- summary.className = 'folded-tool-bar';
749
- const displayName = this.getToolUseDisplayName(toolName);
750
- const titleInfo = this.getToolUseTitle(toolName, input);
751
- summary.innerHTML = `
752
- <span class="folded-tool-icon">${this.getToolIcon(toolName)}</span>
753
- <span class="folded-tool-name">${this.escapeHtml(displayName)}</span>
754
- ${titleInfo ? `<span class="folded-tool-desc">${this.escapeHtml(titleInfo)}</span>` : ''}
755
- `;
756
- details.appendChild(summary);
757
- if (Object.keys(input).length > 0) {
758
- const paramsDiv = document.createElement('div');
759
- paramsDiv.className = 'folded-tool-body';
760
- paramsDiv.innerHTML = this.renderSmartParams(toolName, input);
761
- details.appendChild(paramsDiv);
762
- }
763
- return details;
764
- }
765
-
766
- /**
767
- * Render content smartly - detect JSON, images, file lists, markdown
768
- */
769
- renderSmartContent(contentStr) {
770
- const trimmed = contentStr.trim();
771
-
772
- if (trimmed.startsWith('data:image/')) {
773
- return `<div style="padding:0.5rem"><img src="${this.escapeHtml(trimmed)}" style="max-width:100%;max-height:24rem;border-radius:0.375rem" loading="lazy"></div>`;
774
- }
775
-
776
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
777
- try {
778
- const parsed = JSON.parse(trimmed);
779
- return `<div style="padding:0.625rem 1rem">${this.renderParametersBeautiful(parsed)}</div>`;
780
- } catch (e) {}
781
- }
782
-
783
- const lines = trimmed.split('\n');
784
- const allFilePaths = lines.length > 1 && lines.every(l => {
785
- const t = l.trim();
786
- return t === '' || t.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(t);
787
- });
788
- if (allFilePaths && lines.filter(l => l.trim()).length > 0) {
789
- const fileHtml = lines.filter(l => l.trim()).map(l => {
790
- const p = l.trim();
791
- const parts = pathSplit(p);
792
- const name = parts.pop();
793
- const dir = parts.join('/');
794
- return `<div style="display:flex;align-items:center;gap:0.375rem;padding:0.1875rem 0;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${this.escapeHtml(dir)}/</span><span style="font-weight:600">${this.escapeHtml(name)}</span></div>`;
795
- }).join('');
796
- return `<div style="padding:0.625rem 1rem">${fileHtml}</div>`;
797
- }
798
-
799
- if (trimmed.length > 1500) {
800
- return `<div class="result-body collapsed" 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><button class="expand-btn" onclick="this.previousElementSibling.classList.toggle('collapsed');this.textContent=this.textContent==='Show more'?'Show less':'Show more'">Show more</button>`;
801
- }
802
-
803
- 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>`;
804
- }
805
-
806
- /**
807
- * Render parsed JSON/object as formatted key-value display
808
- */
809
- renderParametersBeautiful(data, depth = 0) {
810
- if (data === null || data === undefined) return `<span style="color:var(--color-text-secondary);font-style:italic">null</span>`;
811
- if (typeof data === 'boolean') return `<span style="color:#d97706;font-weight:600">${data}</span>`;
812
- if (typeof data === 'number') return `<span style="color:#7c3aed;font-weight:600">${data}</span>`;
813
-
814
- if (typeof data === 'string') {
815
- if (data.length > 200 && StreamingRenderer.detectCodeContent(data)) {
816
- const displayData = data.length > 1000 ? data.substring(0, 1000) : data;
817
- const suffix = data.length > 1000 ? `<div style="font-size:0.7rem;color:var(--color-text-secondary);text-align:center;padding:0.25rem">... ${data.length - 1000} more characters</div>` : '';
818
- return `<div style="max-height:200px;overflow-y:auto">${StreamingRenderer.renderCodeWithHighlight(displayData, this.escapeHtml.bind(this), true)}${suffix}</div>`;
819
- }
820
- if (data.length > 500) {
821
- const lines = data.split('\n').length;
822
- return `<div style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;background:var(--color-bg-code);color:#d1d5db;padding:0.5rem;border-radius:0.375rem;line-height:1.5">${this.escapeHtml(data.substring(0, 1000))}${data.length > 1000 ? '\n... (' + (data.length - 1000) + ' more chars, ' + lines + ' lines)' : ''}</div>`;
823
- }
824
- const looksLikePath = data.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(data);
825
- if (looksLikePath && !data.includes(' ') && data.includes('.')) return this.renderFilePath(data);
826
- return `<span style="color:var(--color-text-primary)">${this.escapeHtml(data)}</span>`;
827
- }
828
-
829
- if (Array.isArray(data)) {
830
- if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
831
- if (data.every(i => typeof i === 'string') && data.length <= 20) {
832
- // Render as an itemized list instead of inline badges
833
- 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>`;
834
- }
835
- 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>`;
836
- }
837
-
838
- if (typeof data === 'object') {
839
- const entries = Object.entries(data);
840
- if (entries.length === 0) return `<span style="color:var(--color-text-secondary)">{}</span>`;
841
- return `<div style="display:flex;flex-direction:column;gap:0.375rem;${depth > 0 ? 'padding-left:1rem' : ''}">${entries.map(([k, v]) => `<div style="display:flex;gap:0.5rem;align-items:flex-start"><span style="font-weight:600;font-size:0.75rem;color:#0891b2;flex-shrink:0;min-width:fit-content;font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(k)}</span><div style="flex:1;min-width:0;font-size:0.8rem">${this.renderParametersBeautiful(v, depth + 1)}</div></div>`).join('')}</div>`;
842
- }
843
-
844
- return `<span>${this.escapeHtml(String(data))}</span>`;
845
- }
846
-
847
- /**
848
- * Static HTML version of smart content rendering for use in string templates
849
- */
850
- static renderSmartContentHTML(contentStr, escapeHtml, flat = false) {
851
- const trimmed = contentStr.trim();
852
- const esc = escapeHtml || window._escHtml;
853
-
854
- if (trimmed.startsWith('data:image/')) {
855
- return `<div style="padding:0.5rem"><img src="${esc(trimmed)}" style="max-width:100%;max-height:24rem;border-radius:0.375rem" loading="lazy"></div>`;
856
- }
857
-
858
- // Parse JSON and render as structured content
859
- if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
860
- try {
861
- const parsed = JSON.parse(trimmed);
862
-
863
- // Handle Claude content block arrays: [{type:"text", text:"..."}]
864
- if (Array.isArray(parsed) && parsed.length > 0 && parsed[0] && parsed[0].type === 'text') {
865
- const textParts = parsed.filter(b => b.type === 'text' && b.text);
866
- if (textParts.length > 0) {
867
- const combined = textParts.map(b => b.text).join('\n');
868
- return StreamingRenderer.renderSmartContentHTML(combined, esc, flat);
869
- }
870
- }
871
-
872
- // For other JSON, render as itemized key-value structure
873
- return `<div style="padding:0.5rem 0.75rem">${StreamingRenderer.renderParamsHTML(parsed, 0, esc)}</div>`;
874
- } catch (e) {
875
- // Not valid JSON, might be code with braces
876
- }
877
- }
878
-
879
- // Check if this looks like `cat -n` output or grep with line numbers
880
- const lines = trimmed.split('\n');
881
- const isCatNOutput = lines.length > 1 && lines[0].match(/^\s*\d+→/);
882
- const isGrepOutput = lines.length > 1 && lines[0].match(/^\s*\d+-/);
883
-
884
- if (isCatNOutput || isGrepOutput) {
885
- // Strip line numbers and arrows/hyphens from output
886
- const cleanedLines = lines.map(line => {
887
- // Skip grep context separator lines
888
- if (line === '--') return null;
889
-
890
- // Handle both cat -n (→) and grep (-n) formats
891
- // Also handle grep with colon (:) for matching lines
892
- const match = line.match(/^\s*\d+[→\-:](.*)/);
893
- return match ? match[1] : line;
894
- }).filter(line => line !== null);
895
- const cleanedContent = cleanedLines.join('\n');
896
-
897
- // Try to detect and highlight code based on content patterns
898
- return StreamingRenderer.renderCodeWithHighlight(cleanedContent, esc, flat);
899
- }
900
-
901
- // Check for system reminder tags and format them specially
902
- const systemReminderPattern = /<system-reminder>([\s\S]*?)<\/system-reminder>/g;
903
- const systemReminders = [];
904
- let contentWithoutReminders = trimmed;
905
-
906
- let reminderMatch;
907
- while ((reminderMatch = systemReminderPattern.exec(trimmed)) !== null) {
908
- systemReminders.push(reminderMatch[1].trim());
909
- contentWithoutReminders = contentWithoutReminders.replace(reminderMatch[0], '');
910
- }
911
-
912
- // Clean up the content after removing reminders
913
- contentWithoutReminders = contentWithoutReminders.trim();
914
-
915
- // Check if this looks like a tool success message with formatted output
916
- const successPatterns = [
917
- /^Success\s+toolu_[\w]+$/m,
918
- /^The file .* has been (updated|created|modified)/,
919
- /^Here's the result of running `cat -n`/,
920
- /^Applied \d+ edits? to/,
921
- /^\w+ tool completed successfully/
922
- ];
923
-
924
- const hasSuccessPattern = successPatterns.some(pattern => pattern.test(contentWithoutReminders));
925
-
926
- if (hasSuccessPattern) {
927
- const contentLines = contentWithoutReminders.split('\n');
928
- let successEndIndex = -1;
929
- let codeStartIndex = -1;
930
-
931
- // Find the success message and where code starts
932
- for (let i = 0; i < contentLines.length; i++) {
933
- const line = contentLines[i];
934
- if (line.match(/^Success\s+toolu_/)) {
935
- successEndIndex = i;
936
- // Look for the next non-empty line that contains code
937
- for (let j = i + 1; j < contentLines.length; j++) {
938
- if (contentLines[j].trim() && !contentLines[j].match(/^The file|^Here's the result/)) {
939
- codeStartIndex = j;
940
- break;
941
- }
942
- }
943
- break;
944
- } else if (line.match(/^The file .* has been|^Applied \d+ edits? to|^Replaced|^Created|^Deleted/)) {
945
- // For edit/write operations, code typically starts after the success message
946
- // Look for "Here's the result" line or line numbers
947
- for (let j = i + 1; j < contentLines.length; j++) {
948
- if (contentLines[j].match(/^Here's the result|^\s*\d+→/)) {
949
- // If it's "Here's the result", code starts on next line
950
- if (contentLines[j].match(/^Here's the result/)) {
951
- codeStartIndex = j + 1;
952
- } else {
953
- codeStartIndex = j;
954
- }
955
- break;
956
- } else if (contentLines[j].trim() && !contentLines[j].match(/^cat -n|^Running/)) {
957
- // If we find non-empty content that's not a command, assume it's code
958
- codeStartIndex = j;
959
- break;
960
- }
961
- }
962
- if (codeStartIndex === -1) {
963
- // No line numbers found, treat next content as code
964
- codeStartIndex = i + 2;
965
- }
966
- successEndIndex = codeStartIndex - 1;
967
- break;
968
- }
969
- }
970
-
971
- if (codeStartIndex > 0 && codeStartIndex < contentLines.length) {
972
- const beforeCode = contentLines.slice(0, codeStartIndex).join('\n');
973
- let codeContent = contentLines.slice(codeStartIndex).join('\n');
974
-
975
- // Check if code has line numbers and strip them
976
- if (codeContent.match(/^\s*\d+→/m)) {
977
- const codeLines = codeContent.split('\n');
978
- codeContent = codeLines.map(line => {
979
- const match = line.match(/^\s*\d+→(.*)/);
980
- return match ? match[1] : line;
981
- }).join('\n');
982
- }
983
-
984
- // Build the formatted output
985
- let html = '';
986
-
987
- // Add success message
988
- if (beforeCode.trim()) {
989
- html += `<div style="color:var(--color-success);font-weight:600;margin-bottom:0.75rem;font-size:0.9rem">${esc(beforeCode.trim())}</div>`;
990
- }
991
-
992
- // Add highlighted code
993
- if (codeContent.trim()) {
994
- html += StreamingRenderer.renderCodeWithHighlight(codeContent, esc, flat);
995
- }
996
-
997
- // Add system reminders if any
998
- if (systemReminders.length > 0) {
999
- html += StreamingRenderer.renderSystemReminders(systemReminders, esc);
1000
- }
1001
-
1002
- return html;
1003
- }
1004
- }
1005
-
1006
- // If there are system reminders but no success pattern, render them separately
1007
- if (systemReminders.length > 0) {
1008
- let html = '';
1009
-
1010
- // Render the main content
1011
- if (contentWithoutReminders) {
1012
- // Check if remaining content looks like code
1013
- if (StreamingRenderer.detectCodeContent(contentWithoutReminders)) {
1014
- html += StreamingRenderer.renderCodeWithHighlight(contentWithoutReminders, esc, flat);
1015
- } else {
1016
- html += `<pre class="tool-result-pre">${esc(contentWithoutReminders)}</pre>`;
1017
- }
1018
- }
1019
-
1020
- // Add system reminders
1021
- html += StreamingRenderer.renderSystemReminders(systemReminders, esc);
1022
- return html;
1023
- }
1024
-
1025
- const allFilePaths = lines.length > 1 && lines.every(l => {
1026
- const t = l.trim();
1027
- return t === '' || t.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(t);
1028
- });
1029
- if (allFilePaths && lines.filter(l => l.trim()).length > 0) {
1030
- const fileHtml = lines.filter(l => l.trim()).map(l => {
1031
- const p = l.trim();
1032
- const parts = pathSplit(p);
1033
- const name = parts.pop();
1034
- const dir = parts.join('/');
1035
- return `<div style="display:flex;align-items:center;gap:0.375rem;padding:0.1875rem 0;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${esc(dir)}/</span><span style="font-weight:600">${esc(name)}</span></div>`;
1036
- }).join('');
1037
- return `<div style="padding:0.625rem 1rem">${fileHtml}</div>`;
1038
- }
1039
-
1040
- // Check if this looks like code
1041
- const looksLikeCode = StreamingRenderer.detectCodeContent(trimmed);
1042
- if (looksLikeCode) {
1043
- return StreamingRenderer.renderCodeWithHighlight(trimmed, esc, flat);
1044
- }
1045
-
1046
- const displayContent = trimmed.length > 2000 ? trimmed.substring(0, 2000) + '\n... (truncated)' : trimmed;
1047
- return `<pre class="tool-result-pre">${esc(displayContent)}</pre>`;
1048
- }
1049
-
1050
- /**
1051
- * Render system reminders in a clean, formatted way
1052
- */
1053
- static renderSystemReminders(reminders, esc) {
1054
- if (!reminders || reminders.length === 0) return '';
1055
-
1056
- const reminderHtml = reminders.map(reminder => {
1057
- // Parse reminder content for better formatting
1058
- const lines = reminder.split('\n').filter(l => l.trim());
1059
- const formattedLines = lines.map(line => {
1060
- // Make key points stand out
1061
- if (line.includes('IMPORTANT:') || line.includes('WARNING:')) {
1062
- return `<div style="font-weight:600;color:var(--color-warning);margin:0.25rem 0">${esc(line)}</div>`;
1063
- }
1064
- return `<div style="margin:0.125rem 0">${esc(line)}</div>`;
1065
- }).join('');
1066
-
1067
- return formattedLines;
1068
- }).join('');
1069
-
1070
- return `
1071
- <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)">
1072
- <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem">
1073
- <span style="color:var(--color-info)">ℹ</span>
1074
- <span style="font-weight:600;font-size:0.85rem;color:var(--color-text-primary)">System Reminder</span>
1075
- </div>
1076
- ${reminderHtml}
1077
- </div>
1078
- `;
1079
- }
1080
-
1081
- /**
1082
- * Detect if content looks like code
1083
- */
1084
- static detectCodeContent(content) {
1085
- // Common code patterns
1086
- const codePatterns = [
1087
- /^\s*(function|const|let|var|class|import|export|async|await)/m, // JavaScript
1088
- /^\s*(def|class|import|from|if __name__|lambda|async def)/m, // Python
1089
- /^\s*(public|private|protected|class|interface|package|import)/m, // Java/TypeScript
1090
- /^\s*(<\?php|namespace|use|trait)/m, // PHP
1091
- /^\s*(#include|int main|void|struct|typedef)/m, // C/C++
1092
- /[{}\[\];()]/, // Brackets and semicolons
1093
- /=>|->|::/, // Arrow functions, pointers
1094
- ];
1095
-
1096
- return codePatterns.some(pattern => pattern.test(content));
1097
- }
1098
-
1099
- /**
1100
- * Render code with basic syntax highlighting
1101
- */
1102
- static renderCodeWithHighlight(code, esc, flat = false) {
1103
- const preStyle = "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;margin:0";
1104
- const codeHtml = `<pre style="${preStyle}"><code class="lazy-hl">${esc(code)}</code></pre>`;
1105
- if (flat) return codeHtml;
1106
- const lineCount = code.split('\n').length;
1107
- const summaryLabel = `code - ${lineCount} line${lineCount !== 1 ? 's' : ''}`;
1108
- return `<details class="collapsible-code"><summary class="collapsible-code-summary">${summaryLabel}</summary>${codeHtml}</details>`;
1109
- }
1110
-
1111
- static _setupGlobalLazyHL() {
1112
- if (StreamingRenderer._lazyHLSetup) return;
1113
- StreamingRenderer._lazyHLSetup = true;
1114
- const root = document.getElementById('output-scroll') || document.body;
1115
- root.addEventListener('toggle', (e) => {
1116
- const details = e.target;
1117
- if (!details.open || details.tagName !== 'DETAILS') return;
1118
- const codeEls = details.querySelectorAll('code.lazy-hl');
1119
- if (codeEls.length === 0) return;
1120
- if (typeof hljs === 'undefined') return;
1121
- for (const el of codeEls) {
1122
- try {
1123
- const raw = el.textContent;
1124
- const result = hljs.highlightAuto(raw);
1125
- el.classList.remove('lazy-hl');
1126
- el.classList.add('hljs');
1127
- el.innerHTML = result.value;
1128
- } catch (_) {}
1129
- }
1130
- }, true);
1131
- }
1132
-
1133
- static getToolDisplayName(toolName) {
1134
- const normalized = toolName.replace(/^mcp__[^_]+__/, '');
1135
- const knownTools = ['Read','Write','Edit','Bash','Glob','Grep','WebFetch','WebSearch','TodoWrite','Task','NotebookEdit'];
1136
- if (knownTools.includes(normalized)) return normalized;
1137
- if (toolName.startsWith('mcp__')) {
1138
- const parts = toolName.split('__');
1139
- return parts.length >= 3 ? parts[2] : parts[parts.length - 1];
1140
- }
1141
- return normalized || toolName;
1142
- }
1143
-
1144
- static getToolTitle(toolName, input) {
1145
- const n = toolName.replace(/^mcp__[^_]+__/, '');
1146
- if (n === 'Edit' && input.file_path) { const p = pathSplit(input.file_path); const f = p.pop(); const d = p.slice(-2).join('/'); return d ? d+'/'+f : f; }
1147
- if (n === 'Read' && input.file_path) return pathBasename(input.file_path);
1148
- if (n === 'Write' && input.file_path) return pathBasename(input.file_path);
1149
- if ((n === 'Bash' || n === 'bash') && (input.command || input.commands)) { const c = typeof (input.command||input.commands) === 'string' ? (input.command||input.commands) : JSON.stringify(input.command||input.commands); return c.length > 60 ? c.substring(0,57)+'...' : c; }
1150
- if (n === 'Glob' && input.pattern) return input.pattern;
1151
- if (n === 'Grep' && input.pattern) return input.pattern;
1152
- if (n === 'WebFetch' && input.url) { try { return new URL(input.url).hostname; } catch(e) { return input.url.substring(0,40); } }
1153
- if (n === 'WebSearch' && input.query) return input.query.substring(0,50);
1154
- if (input.file_path) return pathBasename(input.file_path);
1155
- if (input.command) { const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command); return c.length > 50 ? c.substring(0,47)+'...' : c; }
1156
- if (input.query) return input.query.substring(0,50);
1157
- return '';
1158
- }
1159
-
1160
- /**
1161
- * Static HTML version of parameter rendering
1162
- */
1163
- static renderParamsHTML(data, depth, esc) {
1164
- if (data === null || data === undefined) return `<span style="color:var(--color-text-secondary);font-style:italic">null</span>`;
1165
- if (typeof data === 'boolean') return `<span style="color:#d97706;font-weight:600">${data}</span>`;
1166
- if (typeof data === 'number') return `<span style="color:#7c3aed;font-weight:600">${data}</span>`;
1167
-
1168
- if (typeof data === 'string') {
1169
- if (data.length > 200 && StreamingRenderer.detectCodeContent(data)) {
1170
- const displayData = data.length > 1000 ? data.substring(0, 1000) : data;
1171
- const suffix = data.length > 1000 ? `<div style="font-size:0.7rem;color:var(--color-text-secondary);text-align:center;padding:0.25rem">... ${data.length - 1000} more characters</div>` : '';
1172
- return `<div style="max-height:200px;overflow-y:auto">${StreamingRenderer.renderCodeWithHighlight(displayData, esc, true)}${suffix}</div>`;
1173
- }
1174
- if (data.length > 500) {
1175
- return `<div style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;background:var(--color-bg-code);color:#d1d5db;padding:0.5rem;border-radius:0.375rem;line-height:1.5">${esc(data.substring(0, 1000))}${data.length > 1000 ? '\n... (' + (data.length - 1000) + ' more chars)' : ''}</div>`;
1176
- }
1177
- const looksLikePath = /^[A-Za-z]:[\\\/]/.test(data) || data.startsWith('/');
1178
- if (looksLikePath && !data.includes(' ') && data.includes('.')) {
1179
- const parts = pathSplit(data);
1180
- const name = parts.pop();
1181
- const dir = parts.join('/');
1182
- return `<div style="display:flex;align-items:center;gap:0.375rem;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.8rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${esc(dir)}/</span><span style="font-weight:600">${esc(name)}</span></div>`;
1183
- }
1184
- return `<span style="color:var(--color-text-primary)">${esc(data)}</span>`;
1185
- }
1186
-
1187
- if (Array.isArray(data)) {
1188
- if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
1189
- if (data.every(i => typeof i === 'string') && data.length <= 20) {
1190
- // Render as an itemized list instead of inline badges
1191
- 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>`;
1192
- }
1193
- 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>`;
1194
- }
1195
-
1196
- if (typeof data === 'object') {
1197
- const entries = Object.entries(data);
1198
- if (entries.length === 0) return `<span style="color:var(--color-text-secondary)">{}</span>`;
1199
- return `<div style="display:flex;flex-direction:column;gap:0.375rem;${depth > 0 ? 'padding-left:1rem' : ''}">${entries.map(([k, v]) => `<div style="display:flex;gap:0.5rem;align-items:flex-start"><span style="font-weight:600;font-size:0.75rem;color:#0891b2;flex-shrink:0;min-width:fit-content;font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${esc(k)}</span><div style="flex:1;min-width:0;font-size:0.8rem">${StreamingRenderer.renderParamsHTML(v, depth + 1, esc)}</div></div>`).join('')}</div>`;
1200
- }
1201
-
1202
- return `<span>${esc(String(data))}</span>`;
1203
- }
1204
-
1205
- /**
1206
- * Render tool result as inline content to be merged into preceding tool_use block
1207
- */
1208
- renderBlockToolResult(block, context) {
1209
- const isError = block.is_error || false;
1210
- const content = block.content || '';
1211
- const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
1212
- const parentIsOpen = context.parentIsOpen !== undefined ? context.parentIsOpen : true;
1213
-
1214
- const wrapper = document.createElement('div');
1215
- wrapper.className = 'tool-result-inline' + (isError ? ' tool-result-error' : ' tool-result-success');
1216
- wrapper.dataset.eventType = 'tool_result';
1217
- if (block.tool_use_id) wrapper.dataset.toolUseId = block.tool_use_id;
1218
- wrapper.classList.add(this._getBlockTypeClass('tool_result'));
1219
-
1220
- const header = document.createElement('div');
1221
- header.className = 'tool-result-status';
1222
- const iconSvg = isError
1223
- ? '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'
1224
- : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
1225
- header.innerHTML = `
1226
- <span class="folded-tool-icon">${iconSvg}</span>
1227
- <span class="folded-tool-name">${isError ? 'Error' : 'Success'}</span>
1228
- `;
1229
- wrapper.appendChild(header);
1230
-
1231
- const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this), true);
1232
- const body = document.createElement('div');
1233
- body.className = 'folded-tool-body';
1234
- if (!parentIsOpen) {
1235
- body.style.display = 'none';
1236
- }
1237
- body.innerHTML = renderedContent;
1238
- wrapper.appendChild(body);
1239
-
1240
- return wrapper;
1241
- }
1242
-
1243
- /**
1244
- * Render image block
1245
- */
1246
- renderBlockImage(block, context) {
1247
- const div = document.createElement('div');
1248
- div.className = 'block-image';
1249
- div.classList.add(this._getBlockTypeClass('image'));
1250
-
1251
- let src = block.image || block.src || '';
1252
- const alt = block.alt || 'Image';
1253
-
1254
- // Handle base64 data
1255
- if (block.data && block.media_type) {
1256
- src = `data:${block.media_type};base64,${block.data}`;
1257
- }
1258
-
1259
- div.innerHTML = `
1260
- <img src="${this.escapeHtml(src)}" alt="${this.escapeHtml(alt)}" loading="lazy">
1261
- ${block.alt ? `<div class="image-caption">${this.escapeHtml(alt)}</div>` : ''}
1262
- `;
1263
-
1264
- return div;
1265
- }
1266
-
1267
- /**
1268
- * Render bash command block
1269
- */
1270
- renderBlockBash(block, context) {
1271
- const div = document.createElement('div');
1272
- div.className = 'block-bash';
1273
- div.classList.add(this._getBlockTypeClass('bash'));
1274
-
1275
- const command = block.command || block.code || '';
1276
- const output = block.output || '';
1277
-
1278
- // For the command, use simple escaping
1279
- let html = `<div class="bash-command"><span class="prompt">$</span><code>${this.escapeHtml(command)}</code></div>`;
1280
-
1281
- // For output, check if it looks like code and use syntax highlighting
1282
- if (output) {
1283
- if (StreamingRenderer.detectCodeContent(output)) {
1284
- html += StreamingRenderer.renderCodeWithHighlight(output, this.escapeHtml.bind(this), true);
1285
- } else {
1286
- html += `<pre class="bash-output"><code>${this.escapeHtml(output)}</code></pre>`;
1287
- }
1288
- }
1289
-
1290
- div.innerHTML = html;
1291
- return div;
1292
- }
1293
-
1294
- /**
1295
- * Render system event
1296
- */
1297
- renderBlockSystem(block, context) {
1298
- const details = document.createElement('details');
1299
- details.className = 'folded-tool folded-tool-info';
1300
- details.dataset.eventType = 'system';
1301
- details.classList.add(this._getBlockTypeClass('system'));
1302
- const desc = block.model ? this.escapeHtml(block.model) : 'Session';
1303
- const summary = document.createElement('summary');
1304
- summary.className = 'folded-tool-bar';
1305
- summary.innerHTML = `
1306
- <span class="folded-tool-icon"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg></span>
1307
- <span class="folded-tool-name">Session</span>
1308
- <span class="folded-tool-desc">${desc}</span>
1309
- `;
1310
- details.appendChild(summary);
1311
- const body = document.createElement('div');
1312
- body.className = 'folded-tool-body block-system';
1313
- body.innerHTML = `
1314
- <div class="system-body">
1315
- ${block.model ? `<div class="sys-field"><span class="sys-label">Model</span><span class="sys-value"><code>${this.escapeHtml(block.model)}</code></span></div>` : ''}
1316
- ${block.cwd ? `<div class="sys-field"><span class="sys-label">Directory</span><span class="sys-value"><code>${this.escapeHtml(block.cwd)}</code></span></div>` : ''}
1317
- ${block.session_id ? `<div class="sys-field"><span class="sys-label">Session</span><span class="sys-value"><code>${this.escapeHtml(block.session_id)}</code></span></div>` : ''}
1318
- ${block.tools && Array.isArray(block.tools) ? `<div class="sys-field" style="flex-direction:column;gap:0.375rem"><span class="sys-label">Tools (${block.tools.length})</span><div class="tools-list">${block.tools.map(t => `<span class="tool-badge">${this.escapeHtml(t)}</span>`).join('')}</div></div>` : ''}
1319
- </div>
1320
- `;
1321
- details.appendChild(body);
1322
- return details;
1323
- }
1324
-
1325
- /**
1326
- * Render result block (execution summary)
1327
- */
1328
- renderBlockResult(block, context) {
1329
- const isError = block.is_error || false;
1330
- const duration = block.duration_ms ? (block.duration_ms / 1000).toFixed(1) + 's' : '';
1331
- const cost = block.total_cost_usd ? '$' + block.total_cost_usd.toFixed(4) : '';
1332
- const turns = block.num_turns || '';
1333
- const statsDesc = [duration, cost, turns ? turns + ' turns' : ''].filter(Boolean).join(' / ');
1334
-
1335
- const details = document.createElement('details');
1336
- details.className = isError ? 'folded-tool folded-tool-error' : 'folded-tool';
1337
- details.dataset.eventType = 'result';
1338
- details.classList.add(this._getBlockTypeClass(isError ? 'error' : 'result'));
1339
-
1340
- const iconSvg = isError
1341
- ? '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'
1342
- : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
1343
-
1344
- const summary = document.createElement('summary');
1345
- summary.className = 'folded-tool-bar';
1346
- summary.innerHTML = `
1347
- <span class="folded-tool-icon">${iconSvg}</span>
1348
- <span class="folded-tool-name">${isError ? 'Failed' : 'Complete'}</span>
1349
- <span class="folded-tool-desc">${this.escapeHtml(statsDesc)}</span>
1350
- `;
1351
- details.appendChild(summary);
1352
-
1353
- if (block.result || duration || cost || turns) {
1354
- const body = document.createElement('div');
1355
- body.className = 'folded-tool-body';
1356
- let bodyHtml = '';
1357
- if (duration || cost || turns) {
1358
- bodyHtml += `<div class="block-result"><div class="result-stats">
1359
- ${duration ? `<div class="result-stat"><span class="stat-icon">&#9202;</span><span class="stat-value">${this.escapeHtml(duration)}</span><span class="stat-label">duration</span></div>` : ''}
1360
- ${cost ? `<div class="result-stat"><span class="stat-icon">&#128176;</span><span class="stat-value">${this.escapeHtml(cost)}</span><span class="stat-label">cost</span></div>` : ''}
1361
- ${turns ? `<div class="result-stat"><span class="stat-icon">&#128260;</span><span class="stat-value">${this.escapeHtml(String(turns))}</span><span class="stat-label">turns</span></div>` : ''}
1362
- </div></div>`;
1363
- }
1364
- if (block.result) {
1365
- const r = typeof block.result === 'string' ? block.result : JSON.stringify(block.result, null, 2);
1366
- const rendered = this.containsHtmlTags(r) ? '<div class="html-content">' + this.sanitizeHtml(r) + '</div>' : `<div style="font-size:0.8rem;white-space:pre-wrap;word-break:break-word;line-height:1.5">${this.escapeHtml(r)}</div>`;
1367
- bodyHtml += rendered;
1368
- }
1369
- body.innerHTML = bodyHtml;
1370
- details.appendChild(body);
1371
- }
1372
-
1373
- return details;
1374
- }
1375
-
1376
- /**
1377
- * Render tool status block (ACP in_progress/pending updates)
1378
- */
1379
- renderBlockToolStatus(block, context) {
1380
- const status = block.status || 'pending';
1381
- const statusIcons = {
1382
- pending: '<svg viewBox="0 0 20 20" fill="currentColor" style="color:var(--color-text-secondary)"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>',
1383
- in_progress: '<svg viewBox="0 0 20 20" fill="currentColor" class="animate-spin" style="color:var(--color-info)"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>'
1384
- };
1385
- const statusLabels = {
1386
- pending: 'Pending',
1387
- in_progress: 'Running...'
1388
- };
1389
-
1390
- const div = document.createElement('div');
1391
- div.className = 'block-tool-status';
1392
- div.dataset.toolUseId = block.tool_use_id || '';
1393
- div.classList.add(this._getBlockTypeClass('tool_status'));
1394
- div.innerHTML = `
1395
- <div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--color-text-secondary)">
1396
- ${statusIcons[status] || statusIcons.pending}
1397
- <span>${statusLabels[status] || status}</span>
1398
- </div>
1399
- `;
1400
- return div;
1401
- }
1402
-
1403
- /**
1404
- * Render usage block (ACP usage updates)
1405
- */
1406
- renderBlockUsage(block, context) {
1407
- const usage = block.usage || {};
1408
- const used = usage.used || 0;
1409
- const size = usage.size || 0;
1410
- const cost = usage.cost ? '$' + usage.cost.toFixed(4) : '';
1411
-
1412
- const div = document.createElement('div');
1413
- div.className = 'block-usage';
1414
- div.classList.add(this._getBlockTypeClass('usage'));
1415
- div.innerHTML = `
1416
- <div style="display:flex;gap:1rem;padding:0.25rem 0.5rem;font-size:0.7rem;color:var(--color-text-secondary);background:var(--color-bg-secondary);border-radius:0.25rem">
1417
- ${used ? `<span><strong>Used:</strong> ${used.toLocaleString()}</span>` : ''}
1418
- ${size ? `<span><strong>Context:</strong> ${size.toLocaleString()}</span>` : ''}
1419
- ${cost ? `<span><strong>Cost:</strong> ${cost}</span>` : ''}
1420
- </div>
1421
- `;
1422
- return div;
1423
- }
1424
-
1425
- /**
1426
- * Render plan block (ACP plan updates)
1427
- */
1428
- renderBlockPlan(block, context) {
1429
- const entries = block.entries || [];
1430
- if (entries.length === 0) return null;
1431
-
1432
- const priorityColors = {
1433
- high: '#ef4444',
1434
- medium: '#f59e0b',
1435
- low: '#6b7280'
1436
- };
1437
- const statusIcons = {
1438
- pending: '',
1439
- in_progress: '◐',
1440
- completed: '●'
1441
- };
1442
-
1443
- const div = document.createElement('div');
1444
- div.className = 'block-plan';
1445
- div.classList.add(this._getBlockTypeClass('plan'));
1446
- div.innerHTML = `
1447
- <details class="folded-tool folded-tool-info">
1448
- <summary class="folded-tool-bar">
1449
- <span class="folded-tool-icon"><svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg></span>
1450
- <span class="folded-tool-name">Plan</span>
1451
- <span class="folded-tool-desc">${entries.length} tasks</span>
1452
- </summary>
1453
- <div class="folded-tool-body">
1454
- <div style="display:flex;flex-direction:column;gap:0.375rem">
1455
- ${entries.map(e => `
1456
- <div style="display:flex;align-items:center;gap:0.5rem;font-size:0.8rem">
1457
- <span style="color:${priorityColors[e.priority] || priorityColors.low}">${statusIcons[e.status] || statusIcons.pending}</span>
1458
- <span style="${e.status === 'completed' ? 'text-decoration:line-through;opacity:0.6' : ''}">${this.escapeHtml(e.content || '')}</span>
1459
- </div>
1460
- `).join('')}
1461
- </div>
1462
- </div>
1463
- </details>
1464
- `;
1465
- return div;
1466
- }
1467
-
1468
- renderBlockPremature(block, context) {
1469
- const div = document.createElement('div');
1470
- div.className = 'folded-tool folded-tool-error block-premature';
1471
- div.classList.add(this._getBlockTypeClass('premature'));
1472
- const code = block.exitCode != null ? ` (exit ${block.exitCode})` : '';
1473
- const stderrDisplay = block.stderrText ? `<div class="folded-tool-content" style="margin-top:8px;padding:8px;background:rgba(0,0,0,0.05);border-radius:4px;font-family:monospace;font-size:0.9em;white-space:pre-wrap;">${this.escapeHtml(block.stderrText)}</div>` : '';
1474
- div.innerHTML = `
1475
- <div class="folded-tool-bar" style="background:rgba(245,158,11,0.1)">
1476
- <span class="folded-tool-icon" style="color:#f59e0b"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg></span>
1477
- <span class="folded-tool-name" style="color:#f59e0b">ACP Ended Prematurely${this.escapeHtml(code)}</span>
1478
- <span class="folded-tool-desc">${this.escapeHtml(block.error || 'Process exited without output')}</span>
1479
- </div>
1480
- ${stderrDisplay}
1481
- `;
1482
- return div;
1483
- }
1484
-
1485
- /**
1486
- * Render generic block with formatted key-value pairs
1487
- */
1488
- renderBlockGeneric(block, context) {
1489
- const div = document.createElement('div');
1490
- div.className = 'block-generic';
1491
- div.classList.add(this._getBlockTypeClass('generic'));
1492
-
1493
- // Show key-value pairs instead of raw JSON
1494
- const fieldsHtml = Object.entries(block)
1495
- .filter(([key]) => key !== 'type')
1496
- .map(([key, value]) => {
1497
- let displayValue;
1498
- if (typeof value === 'string') {
1499
- displayValue = value.length > 200 ? value.substring(0, 200) + '...' : value;
1500
- } else if (typeof value === 'number' || typeof value === 'boolean') {
1501
- displayValue = String(value);
1502
- } else {
1503
- displayValue = JSON.stringify(value, null, 2);
1504
- if (displayValue.length > 200) displayValue = displayValue.substring(0, 200) + '...';
1505
- }
1506
- return `<div class="generic-field"><span class="field-key">${this.escapeHtml(key)}:</span><span class="field-value">${this.escapeHtml(displayValue)}</span></div>`;
1507
- }).join('');
1508
-
1509
- div.innerHTML = `
1510
- <div class="generic-type">${this.escapeHtml(block.type)}</div>
1511
- <div class="generic-fields">${fieldsHtml}</div>
1512
- `;
1513
-
1514
- return div;
1515
- }
1516
-
1517
- /**
1518
- * Render block error
1519
- */
1520
- renderBlockError(block, error) {
1521
- const div = document.createElement('div');
1522
- div.className = 'block-error';
1523
- div.classList.add(this._getBlockTypeClass('error'));
1524
-
1525
- div.innerHTML = `
1526
- <div style="display:flex;align-items:flex-start;gap:0.625rem">
1527
- <svg viewBox="0 0 20 20" fill="currentColor" style="color:#ef4444;flex-shrink:0;margin-top:0.125rem">
1528
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
1529
- </svg>
1530
- <div>
1531
- <div style="font-weight:600;color:#991b1b">Render Error</div>
1532
- <div style="font-size:0.85rem;color:#7f1d1d;margin-top:0.25rem">${this.escapeHtml(error.message)}</div>
1533
- </div>
1534
- </div>
1535
- `;
1536
-
1537
- return div;
1538
- }
1539
-
1540
- /**
1541
- * Render streaming start event
1542
- */
1543
- renderStreamingStart(event) {
1544
- const div = document.createElement('div');
1545
- div.className = 'event-streaming-start card mb-3 p-4 bg-blue-50 dark:bg-blue-900';
1546
- div.dataset.eventId = event.id || event.sessionId || '';
1547
- div.dataset.eventType = 'streaming_start';
1548
-
1549
- const time = new Date(event.timestamp).toLocaleTimeString();
1550
- div.innerHTML = `
1551
- <div class="flex items-center gap-2">
1552
- <svg class="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1553
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" opacity="0.25"></circle>
1554
- <path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
1555
- </svg>
1556
- <div class="flex-1">
1557
- <h4 class="font-semibold text-blue-900 dark:text-blue-200">Streaming Started</h4>
1558
- <p class="text-sm text-blue-700 dark:text-blue-300">Agent: ${this.escapeHtml(event.agentId || 'unknown')} • ${time}</p>
1559
- </div>
1560
- </div>
1561
- `;
1562
- return div;
1563
- }
1564
-
1565
- /**
1566
- * Render streaming progress event
1567
- */
1568
- renderStreamingProgress(event) {
1569
- // If there's a block in the progress event, render it beautifully
1570
- if (event.block) {
1571
- return this.renderBlock(event.block, event);
1572
- }
1573
-
1574
- // Fallback: simple progress indicator
1575
- const div = document.createElement('div');
1576
- div.className = 'event-streaming-progress mb-2 p-2';
1577
- div.dataset.eventId = event.id || '';
1578
- div.dataset.eventType = 'streaming_progress';
1579
-
1580
- const percentage = event.progress || 0;
1581
- div.innerHTML = `
1582
- <div class="flex items-center gap-2 text-sm">
1583
- <span class="text-secondary">${percentage}%</span>
1584
- <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
1585
- <div class="bg-blue-500 h-full transition-all" style="width: ${percentage}%"></div>
1586
- </div>
1587
- </div>
1588
- `;
1589
- return div;
1590
- }
1591
-
1592
- /**
1593
- * Render streaming complete event with metadata
1594
- */
1595
- renderStreamingComplete(event) {
1596
- const div = document.createElement('div');
1597
- div.className = 'event-streaming-complete card mb-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950 dark:to-emerald-950 border border-green-200 dark:border-green-800 rounded-lg';
1598
- div.dataset.eventId = event.id || event.sessionId || '';
1599
- div.dataset.eventType = 'streaming_complete';
1600
-
1601
- const time = new Date(event.timestamp).toLocaleTimeString();
1602
- const eventCount = event.eventCount || 0;
1603
-
1604
- div.innerHTML = `
1605
- <div class="flex items-start gap-3">
1606
- <div class="flex-shrink-0 mt-0.5">
1607
- <svg class="w-6 h-6 text-green-600 dark:text-green-400 animate-bounce" fill="currentColor" viewBox="0 0 20 20">
1608
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
1609
- </svg>
1610
- </div>
1611
- <div class="flex-1">
1612
- <h4 class="font-bold text-lg text-green-900 dark:text-green-200">✨ Execution Complete</h4>
1613
- <div class="mt-2 grid grid-cols-2 gap-3 text-sm">
1614
- <div>
1615
- <span class="text-green-700 dark:text-green-400 font-semibold">${eventCount}</span>
1616
- <span class="text-green-600 dark:text-green-500">events processed</span>
1617
- </div>
1618
- <div class="text-right">
1619
- <span class="text-green-600 dark:text-green-500">${time}</span>
1620
- </div>
1621
- </div>
1622
- </div>
1623
- </div>
1624
- `;
1625
- return div;
1626
- }
1627
-
1628
- /**
1629
- * Render file read event
1630
- */
1631
- renderFileRead(event) {
1632
- const fileName = event.path ? event.path.split('/').pop() : 'unknown';
1633
- const details = document.createElement('details');
1634
- details.className = 'block-tool-use folded-tool';
1635
- details.classList.add(this._getBlockTypeClass('tool_use'));
1636
- details.classList.add(this._getToolColorClass('Read'));
1637
- details.dataset.eventId = event.id || '';
1638
- details.dataset.eventType = 'file_read';
1639
- const summary = document.createElement('summary');
1640
- summary.className = 'folded-tool-bar';
1641
- summary.innerHTML = `
1642
- <span class="folded-tool-icon">${this.getToolIcon('Read')}</span>
1643
- <span class="folded-tool-name">Read</span>
1644
- <span class="folded-tool-desc">${this.escapeHtml(fileName)}</span>
1645
- `;
1646
- details.appendChild(summary);
1647
- if (event.path || event.content) {
1648
- const body = document.createElement('div');
1649
- body.className = 'folded-tool-body';
1650
- let html = '';
1651
- if (event.path) html += this.renderFilePath(event.path);
1652
- if (event.content) {
1653
- html += `<pre style="background:#1e293b;padding:0.75rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;line-height:1.5;color:#e2e8f0;margin:0.5rem 0 0 0"><code class="lazy-hl">${this.escapeHtml(this.truncateContent(event.content, 2000))}</code></pre>`;
1654
- }
1655
- body.innerHTML = html;
1656
- details.appendChild(body);
1657
- }
1658
- return details;
1659
- }
1660
-
1661
- /**
1662
- * Render file write event
1663
- */
1664
- renderFileWrite(event) {
1665
- const fileName = event.path ? event.path.split('/').pop() : 'unknown';
1666
- const details = document.createElement('details');
1667
- details.className = 'block-tool-use folded-tool';
1668
- details.classList.add(this._getBlockTypeClass('tool_use'));
1669
- details.classList.add(this._getToolColorClass('Write'));
1670
- details.dataset.eventId = event.id || '';
1671
- details.dataset.eventType = 'file_write';
1672
- const summary = document.createElement('summary');
1673
- summary.className = 'folded-tool-bar';
1674
- summary.innerHTML = `
1675
- <span class="folded-tool-icon">${this.getToolIcon('Write')}</span>
1676
- <span class="folded-tool-name">Write</span>
1677
- <span class="folded-tool-desc">${this.escapeHtml(fileName)}</span>
1678
- `;
1679
- details.appendChild(summary);
1680
- if (event.path) {
1681
- const body = document.createElement('div');
1682
- body.className = 'folded-tool-body';
1683
- body.innerHTML = this.renderFilePath(event.path);
1684
- details.appendChild(body);
1685
- }
1686
- return details;
1687
- }
1688
-
1689
- /**
1690
- * Render git status event
1691
- */
1692
- renderGitStatus(event) {
1693
- const div = document.createElement('div');
1694
- div.className = 'event-git-status card mb-3 p-4';
1695
- div.dataset.eventId = event.id || '';
1696
- div.dataset.eventType = 'git_status';
1697
-
1698
- const branch = event.branch || 'unknown';
1699
- const changes = event.changes || {};
1700
- const total = (changes.added || 0) + (changes.modified || 0) + (changes.deleted || 0);
1701
-
1702
- div.innerHTML = `
1703
- <div class="flex items-center gap-3 mb-2">
1704
- <svg class="w-4 h-4 text-orange-600 dark:text-orange-400" fill="currentColor" viewBox="0 0 20 20">
1705
- <path fill-rule="evenodd" d="M9.243 3.03a1 1 0 01.727 1.155L9.53 6h2.94l.56-2.243a1 1 0 111.94.486L14.53 6H17a1 1 0 110 2h-2.97l-.5 2H17a1 1 0 110 2h-3.03l-.56 2.243a1 1 0 11-1.94-.486L12.47 14H9.53l-.56 2.243a1 1 0 11-1.94-.486L7.47 14H4a1 1 0 110-2h3.03l.5-2H4a1 1 0 110-2h2.97l.56-2.243a1 1 0 011.155-.727zM9.03 8l.5 2h2.94l-.5-2H9.03z" clip-rule="evenodd"></path>
1706
- </svg>
1707
- <div class="flex-1">
1708
- <h4 class="font-semibold text-sm">Git Status</h4>
1709
- <p class="text-xs text-secondary">Branch: ${this.escapeHtml(branch)}</p>
1710
- </div>
1711
- </div>
1712
- <div class="flex gap-4 text-xs">
1713
- ${changes.added ? `<span class="text-green-600 dark:text-green-400">+${changes.added}</span>` : ''}
1714
- ${changes.modified ? `<span class="text-blue-600 dark:text-blue-400">~${changes.modified}</span>` : ''}
1715
- ${changes.deleted ? `<span class="text-red-600 dark:text-red-400">-${changes.deleted}</span>` : ''}
1716
- ${total === 0 ? '<span class="text-secondary">no changes</span>' : ''}
1717
- </div>
1718
- `;
1719
- return div;
1720
- }
1721
-
1722
- /**
1723
- * Render command execution event
1724
- */
1725
- renderCommand(event) {
1726
- const command = event.command || '';
1727
- const output = event.output || '';
1728
- const exitCode = event.exitCode !== undefined ? event.exitCode : null;
1729
- const cmdPreview = command.length > 60 ? command.substring(0, 57) + '...' : command;
1730
-
1731
- const details = document.createElement('details');
1732
- details.className = 'block-tool-use folded-tool';
1733
- details.classList.add(this._getBlockTypeClass('tool_use'));
1734
- details.classList.add(this._getToolColorClass('Bash'));
1735
- details.dataset.eventId = event.id || '';
1736
- details.dataset.eventType = 'command_execute';
1737
- const summary = document.createElement('summary');
1738
- summary.className = 'folded-tool-bar';
1739
- summary.innerHTML = `
1740
- <span class="folded-tool-icon">${this.getToolIcon('Bash')}</span>
1741
- <span class="folded-tool-name">Bash</span>
1742
- <span class="folded-tool-desc">${this.escapeHtml(cmdPreview)}</span>
1743
- `;
1744
- details.appendChild(summary);
1745
-
1746
- const body = document.createElement('div');
1747
- body.className = 'folded-tool-body';
1748
- let html = `<div class="tool-param-command"><span class="prompt-char">$</span><span class="command-text">${this.escapeHtml(command)}</span></div>`;
1749
- if (output) {
1750
- html += `<pre style="background:#1e293b;padding:0.75rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;line-height:1.5;color:#e2e8f0;margin:0.5rem 0 0 0"><code class="lazy-hl">${this.escapeHtml(this.truncateContent(output, 2000))}</code></pre>`;
1751
- }
1752
- if (exitCode !== null && exitCode !== 0) {
1753
- html += `<div style="margin-top:0.375rem;font-size:0.75rem;color:#ef4444;font-weight:600">Exit code: ${exitCode}</div>`;
1754
- }
1755
- body.innerHTML = html;
1756
- details.appendChild(body);
1757
- return details;
1758
- }
1759
-
1760
- /**
1761
- * Render error event
1762
- */
1763
- renderError(event) {
1764
- const message = event.message || event.error || 'Unknown error';
1765
- const severity = event.severity || 'error';
1766
- const msgPreview = message.length > 80 ? message.substring(0, 77) + '...' : message;
1767
-
1768
- const details = document.createElement('details');
1769
- details.className = 'folded-tool folded-tool-error permanently-expanded';
1770
- details.setAttribute('open', '');
1771
- details.dataset.eventId = event.id || '';
1772
- details.dataset.eventType = 'error';
1773
- const summary = document.createElement('summary');
1774
- summary.className = 'folded-tool-bar';
1775
- summary.innerHTML = `
1776
- <span class="folded-tool-icon"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg></span>
1777
- <span class="folded-tool-name">Error</span>
1778
- <span class="folded-tool-desc">${this.escapeHtml(msgPreview)}</span>
1779
- `;
1780
- details.appendChild(summary);
1781
-
1782
- const body = document.createElement('div');
1783
- body.className = 'folded-tool-body';
1784
- body.innerHTML = `<div style="font-size:0.8rem;white-space:pre-wrap;word-break:break-word;line-height:1.5">${this.escapeHtml(message)}</div>`;
1785
- details.appendChild(body);
1786
- return details;
1787
- }
1788
-
1789
- isHtmlContent(text) {
1790
- const openTag = /<(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])\b[^>]*>/i;
1791
- const closeTag = /<\/(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])>/i;
1792
- return openTag.test(text) && closeTag.test(text);
1793
- }
1794
-
1795
- parseMarkdownCodeBlocks(text) {
1796
- const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
1797
- const parts = [];
1798
- let lastIndex = 0;
1799
- let match;
1800
-
1801
- while ((match = codeBlockRegex.exec(text)) !== null) {
1802
- if (match.index > lastIndex) {
1803
- const segment = text.substring(lastIndex, match.index);
1804
- parts.push({ type: this.isHtmlContent(segment) ? 'html' : 'text', content: segment });
1805
- }
1806
- parts.push({ type: 'code', language: match[1] || 'plain', code: match[2] });
1807
- lastIndex = codeBlockRegex.lastIndex;
1808
- }
1809
-
1810
- if (lastIndex < text.length) {
1811
- const segment = text.substring(lastIndex);
1812
- parts.push({ type: this.isHtmlContent(segment) ? 'html' : 'text', content: segment });
1813
- }
1814
-
1815
- if (parts.length === 0) {
1816
- return [{ type: this.isHtmlContent(text) ? 'html' : 'text', content: text }];
1817
- }
1818
-
1819
- return parts;
1820
- }
1821
-
1822
- /**
1823
- * Render text block event - for backward compatibility
1824
- */
1825
- renderText(event) {
1826
- const div = document.createElement('div');
1827
- div.className = 'event-text mb-3';
1828
- div.dataset.eventId = event.id || '';
1829
- div.dataset.eventType = 'text_block';
1830
-
1831
- const text = event.text || event.content || '';
1832
- const parts = this.parseMarkdownCodeBlocks(text);
1833
- let html = '';
1834
- parts.forEach(part => {
1835
- if (part.type === 'html') {
1836
- html += `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto mb-3">${part.content}</div>`;
1837
- } else if (part.type === 'text') {
1838
- html += `<div class="p-4 bg-white dark:bg-gray-950 rounded-lg border border-gray-200 dark:border-gray-800 mb-3 leading-relaxed text-sm">${this.parseAndRenderMarkdown(part.content)}</div>`;
1839
- } else if (part.type === 'code') {
1840
- if (part.language.toLowerCase() === 'html') {
1841
- html += `<div class="html-rendered-container mb-3 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-800">
1842
- <div class="html-rendered-label px-4 py-2 bg-blue-100 dark:bg-blue-900 text-xs font-semibold text-blue-900 dark:text-blue-200">Rendered HTML</div>
1843
- <div class="html-content bg-white dark:bg-gray-800 p-4 overflow-x-auto">${part.code}</div>
1844
- </div>`;
1845
- } else {
1846
- const partLineCount = part.code.split('\n').length;
1847
- html += `<div class="mb-3 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-800">
1848
- <details class="collapsible-code">
1849
- <summary class="collapsible-code-summary">
1850
- <span>${this.escapeHtml(part.language)} - ${partLineCount} line${partLineCount !== 1 ? 's' : ''}</span>
1851
- <button class="copy-code-btn text-gray-400 hover:text-gray-200 transition-colors p-1 rounded hover:bg-gray-800" title="Copy code" onclick="event.preventDefault();event.stopPropagation();navigator.clipboard.writeText(this.closest('.collapsible-code').querySelector('code').textContent)">
1852
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1853
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
1854
- </svg>
1855
- </button>
1856
- </summary>
1857
- <pre class="bg-gray-900 text-gray-100 p-4 overflow-x-auto" style="margin:0;border-radius:0 0 0.375rem 0.375rem"><code class="language-${this.escapeHtml(part.language)}">${this.escapeHtml(part.code)}</code></pre>
1858
- </details>
1859
- </div>`;
1860
- }
1861
- }
1862
- });
1863
- div.innerHTML = html;
1864
-
1865
- // Add copy button functionality
1866
- div.querySelectorAll('.copy-code-btn').forEach(btn => {
1867
- btn.addEventListener('click', () => {
1868
- const codeElement = btn.closest('.mb-3')?.querySelector('code');
1869
- if (codeElement) {
1870
- const code = codeElement.textContent;
1871
- navigator.clipboard.writeText(code).then(() => {
1872
- const originalText = btn.innerHTML;
1873
- btn.innerHTML = '<svg class="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>';
1874
- setTimeout(() => { btn.innerHTML = originalText; }, 2000);
1875
- });
1876
- }
1877
- });
1878
- });
1879
-
1880
- return div;
1881
- }
1882
-
1883
- /**
1884
- * Render code block event
1885
- */
1886
- renderCode(event) {
1887
- const div = document.createElement('div');
1888
- div.className = 'event-code mb-3';
1889
- div.dataset.eventId = event.id || '';
1890
- div.dataset.eventType = 'code_block';
1891
-
1892
- const code = event.code || event.content || '';
1893
- const language = event.language || 'plaintext';
1894
-
1895
- // Render HTML code blocks as actual HTML elements
1896
- if (language === 'html') {
1897
- div.innerHTML = `
1898
- <div class="html-rendered-container mb-2 p-2 bg-blue-50 dark:bg-blue-900 rounded border border-blue-200 dark:border-blue-700 text-xs text-blue-700 dark:text-blue-300">
1899
- Rendered HTML
1900
- </div>
1901
- <div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
1902
- ${code}
1903
- </div>
1904
- `;
1905
- } else {
1906
- const codeLineCount = code.split('\n').length;
1907
- div.innerHTML = `
1908
- <details class="collapsible-code">
1909
- <summary class="collapsible-code-summary">${this.escapeHtml(language)} - ${codeLineCount} line${codeLineCount !== 1 ? 's' : ''}</summary>
1910
- <pre class="bg-gray-900 text-gray-100 p-4 overflow-x-auto" style="margin:0;border-radius:0 0 0.375rem 0.375rem"><code class="language-${this.escapeHtml(language)}">${this.escapeHtml(code)}</code></pre>
1911
- </details>
1912
- `;
1913
- }
1914
- return div;
1915
- }
1916
-
1917
- /**
1918
- * Render thinking block event
1919
- */
1920
- renderThinking(event) {
1921
- const div = document.createElement('div');
1922
- div.className = 'event-thinking mb-3 p-4 bg-purple-50 dark:bg-purple-900 rounded';
1923
- div.dataset.eventId = event.id || '';
1924
- div.dataset.eventType = 'thinking_block';
1925
-
1926
- const text = event.thinking || event.content || '';
1927
- div.innerHTML = `
1928
- <details>
1929
- <summary class="cursor-pointer font-semibold text-purple-900 dark:text-purple-200">Thinking</summary>
1930
- <p class="mt-3 text-sm text-purple-800 dark:text-purple-300 whitespace-pre-wrap">${this.escapeHtml(text)}</p>
1931
- </details>
1932
- `;
1933
- return div;
1934
- }
1935
-
1936
- /**
1937
- * Render tool use event - for backward compatibility
1938
- */
1939
- renderToolUse(event) {
1940
- // Use the new block-based renderer for consistency
1941
- const block = {
1942
- type: 'tool_use',
1943
- name: event.toolName || event.tool || 'unknown',
1944
- input: event.input || {}
1945
- };
1946
- const div = this.renderBlockToolUse(block, event);
1947
- div.className = 'event-tool-use mb-3';
1948
- div.dataset.eventId = event.id || '';
1949
- div.dataset.eventType = 'tool_use';
1950
- return div;
1951
- }
1952
-
1953
- /**
1954
- * Render generic event with formatted key-value pairs
1955
- */
1956
- renderGeneric(event) {
1957
- const div = document.createElement('div');
1958
- div.className = 'event-generic mb-3 p-3 bg-gray-100 dark:bg-gray-800 rounded text-sm';
1959
- div.dataset.eventId = event.id || '';
1960
- div.dataset.eventType = event.type;
1961
-
1962
- const time = new Date(event.timestamp).toLocaleTimeString();
1963
-
1964
- // Format event data as key-value pairs
1965
- const fieldsHtml = Object.entries(event)
1966
- .filter(([key]) => !['type', 'timestamp'].includes(key))
1967
- .map(([key, value]) => {
1968
- let displayValue;
1969
- if (typeof value === 'string') {
1970
- displayValue = value.length > 100 ? value.substring(0, 100) + '...' : value;
1971
- } else if (typeof value === 'number' || typeof value === 'boolean') {
1972
- displayValue = String(value);
1973
- } else if (value === null) {
1974
- displayValue = 'null';
1975
- } else {
1976
- displayValue = JSON.stringify(value);
1977
- if (displayValue.length > 100) displayValue = displayValue.substring(0, 100) + '...';
1978
- }
1979
- return `<div style="font-size:0.75rem;margin-bottom:0.25rem"><span style="font-weight:600;color:var(--color-text-secondary)">${this.escapeHtml(key)}:</span> <span style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(displayValue)}</span></div>`;
1980
- }).join('');
1981
-
1982
- div.innerHTML = `
1983
- <div style="display:flex;justify-content:space-between;margin-bottom:0.5rem">
1984
- <span style="font-weight:600;color:var(--color-text-primary)">${this.escapeHtml(event.type)}</span>
1985
- <span style="font-size:0.75rem;color:var(--color-text-secondary)">${time}</span>
1986
- </div>
1987
- <div>${fieldsHtml || '<span style="color:var(--color-text-secondary);font-size:0.75rem">No additional data</span>'}</div>
1988
- `;
1989
- return div;
1990
- }
1991
-
1992
- /**
1993
- * Auto-scroll to bottom of container
1994
- */
1995
- autoScroll() {
1996
- if (this._scrollRafPending || this._userScrolledUp) return;
1997
- this._scrollRafPending = true;
1998
- requestAnimationFrame(() => {
1999
- this._scrollRafPending = false;
2000
- if (this.scrollContainer) {
2001
- this._programmaticScroll = true;
2002
- try { this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight; } catch (_) {}
2003
- this._programmaticScroll = false;
2004
- }
2005
- });
2006
- }
2007
-
2008
- resetScrollState() {
2009
- this._userScrolledUp = false;
2010
- }
2011
-
2012
- updateVirtualScroll() {
2013
- }
2014
-
2015
- /**
2016
- * Update DOM node count for monitoring
2017
- */
2018
- updateDOMNodeCount() {
2019
- this.domNodeCount = this.outputContainer?.querySelectorAll('[data-event-id]').length || 0;
2020
- }
2021
-
2022
- /**
2023
- * HTML escape utility
2024
- */
2025
- escapeHtml(text) {
2026
- return window._escHtml(text);
2027
- }
2028
-
2029
- /**
2030
- * Format file size for display
2031
- */
2032
- formatFileSize(bytes) {
2033
- if (bytes === 0) return '0 B';
2034
- const k = 1024;
2035
- const sizes = ['B', 'KB', 'MB', 'GB'];
2036
- const i = Math.floor(Math.log(bytes) / Math.log(k));
2037
- return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
2038
- }
2039
-
2040
- /**
2041
- * Truncate content for display
2042
- */
2043
- truncateContent(content, maxLength = 200) {
2044
- if (content.length <= maxLength) return content;
2045
- return content.substring(0, maxLength) + '...';
2046
- }
2047
-
2048
- /**
2049
- * Clear all rendered events
2050
- */
2051
- clear() {
2052
- if (this.outputContainer) {
2053
- this.outputContainer.innerHTML = '';
2054
- }
2055
- this.eventQueue = [];
2056
- this.eventHistory = [];
2057
- this.domNodeCount = 0;
2058
- this.dedupMap.clear();
2059
- }
2060
-
2061
- /**
2062
- * Get performance metrics
2063
- */
2064
- getMetrics() {
2065
- return {
2066
- ...this.performanceMetrics,
2067
- domNodeCount: this.domNodeCount,
2068
- queueLength: this.eventQueue.length,
2069
- historyLength: this.eventHistory.length,
2070
- lastRenderTime: this.lastRenderTime
2071
- };
2072
- }
2073
-
2074
- /**
2075
- * Add event listener
2076
- */
2077
- on(event, callback) {
2078
- if (!this.listeners[event]) {
2079
- this.listeners[event] = [];
2080
- }
2081
- this.listeners[event].push(callback);
2082
- }
2083
-
2084
- /**
2085
- * Emit event to listeners
2086
- */
2087
- emit(event, data) {
2088
- if (this.listeners[event]) {
2089
- this.listeners[event].forEach(callback => {
2090
- try {
2091
- callback(data);
2092
- } catch (e) {
2093
- console.error('Listener error:', e);
2094
- }
2095
- });
2096
- }
2097
- }
2098
-
2099
- /**
2100
- * Cleanup resources
2101
- */
2102
- destroy() {
2103
- if (this.observer) {
2104
- this.observer.disconnect();
2105
- }
2106
- if (this.resizeObserver) {
2107
- this.resizeObserver.disconnect();
2108
- }
2109
- if (this.batchTimer) {
2110
- clearTimeout(this.batchTimer);
2111
- }
2112
- this.listeners = {};
2113
- this.clear();
2114
- }
2115
- }
2116
-
2117
- // Export for use in browser
2118
- if (typeof module !== 'undefined' && module.exports) {
2119
- module.exports = StreamingRenderer;
2120
- }
1
+ /**
2
+ * Streaming Renderer Engine
3
+ * Manages real-time event processing, batching, and DOM rendering
4
+ * for Claude Code streaming execution display
5
+ */
6
+
7
+ function pathSplit(p) {
8
+ return p.split(/[\/\\]/).filter(Boolean);
9
+ }
10
+
11
+ function pathBasename(p) {
12
+ const parts = pathSplit(p);
13
+ return parts.length ? parts.pop() : '';
14
+ }
15
+
16
+ class StreamingRenderer {
17
+ constructor(config = {}) {
18
+ // Configuration
19
+ this.config = {
20
+ batchSize: config.batchSize || 50,
21
+ batchInterval: config.batchInterval || 16, // ~60fps
22
+ maxQueueSize: config.maxQueueSize || 10000,
23
+ maxEventHistory: config.maxEventHistory || 1000,
24
+ virtualScrollThreshold: config.virtualScrollThreshold || 500,
25
+ debounceDelay: config.debounceDelay || 100,
26
+ ...config
27
+ };
28
+
29
+ // State
30
+ this.eventQueue = [];
31
+ this.eventHistory = [];
32
+ this.isProcessing = false;
33
+ this.batchTimer = null;
34
+ this.dedupMap = new Map();
35
+ this.renderCache = new Map();
36
+ this.domNodeCount = 0;
37
+ this.lastRenderTime = 0;
38
+ this.performanceMetrics = {
39
+ totalEvents: 0,
40
+ totalBatches: 0,
41
+ avgBatchSize: 0,
42
+ avgRenderTime: 0,
43
+ avgProcessTime: 0
44
+ };
45
+
46
+ // DOM references
47
+ this.outputContainer = null;
48
+ this.scrollContainer = null;
49
+ this.virtualScroller = null;
50
+
51
+ // Event listeners
52
+ this.listeners = {
53
+ 'event:queued': [],
54
+ 'event:dequeued': [],
55
+ 'batch:start': [],
56
+ 'batch:complete': [],
57
+ 'render:start': [],
58
+ 'render:complete': [],
59
+ 'error:render': []
60
+ };
61
+
62
+ // Performance monitoring
63
+ this.observer = null;
64
+ this.resizeObserver = null;
65
+ }
66
+
67
+ /**
68
+ * Initialize the renderer with DOM elements
69
+ */
70
+ init(outputContainerId, scrollContainerId = null) {
71
+ this.outputContainer = document.getElementById(outputContainerId);
72
+ this.scrollContainer = scrollContainerId ? document.getElementById(scrollContainerId) : this.outputContainer;
73
+
74
+ if (!this.outputContainer) {
75
+ throw new Error(`Output container not found: ${outputContainerId}`);
76
+ }
77
+
78
+ this.setupDOMObserver();
79
+ this.setupResizeObserver();
80
+ this.setupScrollOptimization();
81
+ StreamingRenderer._setupGlobalLazyHL();
82
+ return this;
83
+ }
84
+
85
+ /**
86
+ * Setup DOM mutation observer for external changes
87
+ */
88
+ setupDOMObserver() {
89
+ }
90
+
91
+ /**
92
+ * Setup resize observer for viewport changes
93
+ */
94
+ setupResizeObserver() {
95
+ }
96
+
97
+ /**
98
+ * Setup scroll optimization and auto-scroll
99
+ */
100
+ setupScrollOptimization() {
101
+ if (!this.scrollContainer) return;
102
+ this._userScrolledUp = false;
103
+ this.scrollContainer.addEventListener('scroll', () => {
104
+ if (this._programmaticScroll) return;
105
+ const sc = this.scrollContainer;
106
+ const distFromBottom = sc.scrollHeight - sc.scrollTop - sc.clientHeight;
107
+ this._userScrolledUp = distFromBottom > 80;
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Queue an event for batch processing
113
+ */
114
+ queueEvent(event) {
115
+ if (!event || typeof event !== 'object') return false;
116
+
117
+ // Add timestamp if not present
118
+ if (!event.timestamp) {
119
+ event.timestamp = Date.now();
120
+ }
121
+
122
+ // Deduplication
123
+ if (this.isDuplicate(event)) {
124
+ return false;
125
+ }
126
+
127
+ // Queue size check
128
+ if (this.eventQueue.length >= this.config.maxQueueSize) {
129
+ console.warn('Event queue overflow, dropping oldest events');
130
+ this.eventQueue.shift();
131
+ }
132
+
133
+ this.eventQueue.push(event);
134
+ this.eventHistory.push(event);
135
+
136
+ // Trim history
137
+ if (this.eventHistory.length > this.config.maxEventHistory) {
138
+ this.eventHistory.shift();
139
+ }
140
+
141
+ this.emit('event:queued', { event, queueLength: this.eventQueue.length });
142
+ this.scheduleBatchProcess();
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Check if event is a duplicate
148
+ */
149
+ isDuplicate(event) {
150
+ const key = this.getEventKey(event);
151
+ if (!key) return false;
152
+
153
+ const lastTime = this.dedupMap.get(key);
154
+ const now = Date.now();
155
+
156
+ if (lastTime && (now - lastTime) < 100) {
157
+ return true;
158
+ }
159
+
160
+ this.dedupMap.set(key, now);
161
+ if (this.dedupMap.size > 5000) {
162
+ const cutoff = now - 1000;
163
+ for (const [k, t] of this.dedupMap) {
164
+ if (t < cutoff) this.dedupMap.delete(k);
165
+ }
166
+ }
167
+ return false;
168
+ }
169
+
170
+ /**
171
+ * Generate deduplication key for event
172
+ */
173
+ getEventKey(event) {
174
+ if (!event.type) return null;
175
+ return `${event.type}:${event.id || event.sessionId || ''}`;
176
+ }
177
+
178
+ /**
179
+ * Schedule batch processing
180
+ */
181
+ scheduleBatchProcess() {
182
+ if (this.isProcessing || this.batchTimer) return;
183
+
184
+ if (this.eventQueue.length >= this.config.batchSize) {
185
+ // Process immediately if batch is full
186
+ this.processBatch();
187
+ } else {
188
+ // Schedule for later
189
+ this.batchTimer = setTimeout(() => {
190
+ this.batchTimer = null;
191
+ if (this.eventQueue.length > 0) {
192
+ this.processBatch();
193
+ }
194
+ }, this.config.batchInterval);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Process queued events as a batch
200
+ */
201
+ processBatch() {
202
+ if (this.isProcessing) return;
203
+ if (this.eventQueue.length === 0) return;
204
+
205
+ this.isProcessing = true;
206
+ const processStart = performance.now();
207
+ const batchSize = Math.min(this.eventQueue.length, this.config.batchSize);
208
+ const batch = this.eventQueue.splice(0, batchSize);
209
+
210
+ this.emit('batch:start', { batchSize, queueLength: this.eventQueue.length });
211
+
212
+ try {
213
+ // Process and render batch
214
+ const renderStart = performance.now();
215
+ this.renderBatch(batch);
216
+ const renderTime = performance.now() - renderStart;
217
+
218
+ // Update metrics
219
+ this.performanceMetrics.totalBatches++;
220
+ this.performanceMetrics.totalEvents += batchSize;
221
+ this.performanceMetrics.avgBatchSize = this.performanceMetrics.totalEvents / this.performanceMetrics.totalBatches;
222
+ this.performanceMetrics.avgRenderTime = (this.performanceMetrics.avgRenderTime * (this.performanceMetrics.totalBatches - 1) + renderTime) / this.performanceMetrics.totalBatches;
223
+
224
+ this.emit('batch:complete', {
225
+ batchSize,
226
+ renderTime,
227
+ metrics: this.performanceMetrics
228
+ });
229
+
230
+ // Process more if queue is still full
231
+ if (this.eventQueue.length >= this.config.batchSize) {
232
+ this.isProcessing = false;
233
+ setImmediate(() => this.processBatch());
234
+ } else {
235
+ this.isProcessing = false;
236
+ if (this.eventQueue.length > 0) {
237
+ this.scheduleBatchProcess();
238
+ }
239
+ }
240
+ } catch (error) {
241
+ console.error('Batch processing error:', error);
242
+ this.isProcessing = false;
243
+ this.emit('error:render', { error, batch });
244
+ }
245
+
246
+ const processTime = performance.now() - processStart;
247
+ this.performanceMetrics.avgProcessTime = this.performanceMetrics.avgProcessTime || processTime;
248
+ }
249
+
250
+ /**
251
+ * Render a batch of events
252
+ */
253
+ renderBatch(batch) {
254
+ if (!this.outputContainer) return;
255
+
256
+ this.emit('render:start', { eventCount: batch.length });
257
+ const renderStart = performance.now();
258
+
259
+ try {
260
+ // Create document fragment for batch
261
+ const fragment = document.createDocumentFragment();
262
+ let nodeCount = 0;
263
+
264
+ for (const event of batch) {
265
+ try {
266
+ const element = this.renderEvent(event);
267
+ if (element) {
268
+ fragment.appendChild(element);
269
+ nodeCount++;
270
+ }
271
+ } catch (error) {
272
+ console.error('Event render error:', error, event);
273
+ }
274
+ }
275
+
276
+ // Append all at once (minimizes reflows)
277
+ if (nodeCount > 0) {
278
+ this.outputContainer.appendChild(fragment);
279
+ this.domNodeCount += nodeCount;
280
+ }
281
+
282
+ // Auto-scroll to bottom
283
+ this.autoScroll();
284
+
285
+ const renderTime = performance.now() - renderStart;
286
+ this.lastRenderTime = renderTime;
287
+
288
+ this.emit('render:complete', {
289
+ eventCount: batch.length,
290
+ nodeCount,
291
+ renderTime
292
+ });
293
+ } catch (error) {
294
+ console.error('Batch render error:', error);
295
+ this.emit('error:render', { error, batch });
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Render a single event to DOM element
301
+ */
302
+ renderEvent(event) {
303
+ if (!event.type) return null;
304
+
305
+ try {
306
+ // Handle block rendering from streaming_progress events
307
+ if (event.type === 'streaming_progress' && event.block) {
308
+ return this.renderBlock(event.block, event);
309
+ }
310
+
311
+ if (event.type === 'streaming_error' && event.isPrematureEnd) {
312
+ return this.renderBlockPremature({ type: 'premature', error: event.error, exitCode: event.exitCode });
313
+ }
314
+
315
+ switch (event.type) {
316
+ case 'streaming_start':
317
+ return this.renderStreamingStart(event);
318
+ case 'streaming_progress':
319
+ return this.renderStreamingProgress(event);
320
+ case 'streaming_complete':
321
+ return this.renderStreamingComplete(event);
322
+ case 'file_read':
323
+ return this.renderFileRead(event);
324
+ case 'file_write':
325
+ return this.renderFileWrite(event);
326
+ case 'git_status':
327
+ return this.renderGitStatus(event);
328
+ case 'command_execute':
329
+ return this.renderCommand(event);
330
+ case 'error':
331
+ return this.renderError(event);
332
+ case 'text_block':
333
+ return this.renderText(event);
334
+ case 'code_block':
335
+ return this.renderCode(event);
336
+ case 'thinking_block':
337
+ return this.renderThinking(event);
338
+ case 'tool_use':
339
+ return this.renderToolUse(event);
340
+ default:
341
+ return this.renderGeneric(event);
342
+ }
343
+ } catch (error) {
344
+ console.error('Event render error:', error, event);
345
+ return this.renderError({ message: error.message, event });
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Render Claude message blocks with beautiful styling
351
+ */
352
+ renderBlock(block, context = {}, targetContainer = null) {
353
+ if (!block || !block.type) return null;
354
+
355
+ try {
356
+ switch (block.type) {
357
+ case 'text':
358
+ return this.renderBlockText(block, context, targetContainer);
359
+ case 'code':
360
+ return this.renderBlockCode(block, context);
361
+ case 'thinking':
362
+ return this.renderBlockThinking(block, context);
363
+ case 'tool_use':
364
+ return this.renderBlockToolUse(block, context);
365
+ case 'tool_result':
366
+ return this.renderBlockToolResult(block, context);
367
+ case 'image':
368
+ return this.renderBlockImage(block, context);
369
+ case 'bash':
370
+ return this.renderBlockBash(block, context);
371
+ case 'system':
372
+ return this.renderBlockSystem(block, context);
373
+ case 'result':
374
+ return this.renderBlockResult(block, context);
375
+ case 'tool_status':
376
+ return this.renderBlockToolStatus(block, context);
377
+ case 'usage':
378
+ return this.renderBlockUsage(block, context);
379
+ case 'plan':
380
+ return this.renderBlockPlan(block, context);
381
+ case 'premature':
382
+ return this.renderBlockPremature(block, context);
383
+ default:
384
+ return this.renderBlockGeneric(block, context);
385
+ }
386
+ } catch (error) {
387
+ console.error('Block render error:', error, block);
388
+ return this.renderBlockError(block, error);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Render text block with semantic HTML
394
+ */
395
+ renderBlockText(block, context, targetContainer = null) {
396
+ const text = block.text || '';
397
+ const isHtml = this.containsHtmlTags(text);
398
+ const cached = this.renderCache.get(text);
399
+ const html = cached || (isHtml ? this.sanitizeHtml(text) : this.parseAndRenderMarkdown(text));
400
+
401
+ if (!cached && this.renderCache.size < 2000) {
402
+ this.renderCache.set(text, html);
403
+ }
404
+
405
+ const container = targetContainer || this.outputContainer;
406
+ const lastChild = container && container.lastElementChild;
407
+ if (lastChild && lastChild.classList.contains('block-text') && !isHtml && !lastChild.classList.contains('html-content')) {
408
+ lastChild.innerHTML += html;
409
+ return null;
410
+ }
411
+
412
+ const div = document.createElement('div');
413
+ div.className = 'block-text';
414
+ if (isHtml) div.classList.add('html-content');
415
+ div.innerHTML = html;
416
+ div.classList.add(this._getBlockTypeClass('text'));
417
+ return div;
418
+ }
419
+
420
+ _getBlockTypeClass(blockType) {
421
+ const validTypes = ['text','tool_use','tool_result','code','thinking','bash','system','result','error','image','plan','usage','premature','tool_status','generic'];
422
+ return validTypes.includes(blockType) ? `block-type-${blockType}` : 'block-type-generic';
423
+ }
424
+
425
+ _getToolColorClass(toolName) {
426
+ const n = (toolName || '').replace(/^mcp__[^_]+__/, '').toLowerCase();
427
+ const map = { read: 'read', write: 'write', edit: 'edit', bash: 'bash', glob: 'glob', grep: 'grep', webfetch: 'web', websearch: 'web', todowrite: 'todo', task: 'task', notebookedit: 'edit' };
428
+ return `tool-color-${map[n] || 'default'}`;
429
+ }
430
+
431
+ containsHtmlTags(text) {
432
+ const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
433
+ return htmlPattern.test(text);
434
+ }
435
+
436
+ sanitizeHtml(html) {
437
+ const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
438
+ let cleaned = html.replace(dangerous, '');
439
+ cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
440
+ cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
441
+ cleaned = cleaned.replace(/javascript\s*:/gi, '');
442
+ return cleaned;
443
+ }
444
+
445
+ /**
446
+ * Parse markdown and render links, code, bold, italic
447
+ */
448
+ parseAndRenderMarkdown(text) {
449
+ let html = this.escapeHtml(text);
450
+
451
+ // Render markdown bold: **text** -> <strong>text</strong>
452
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong class="font-semibold text-gray-900 dark:text-gray-100">$1</strong>');
453
+
454
+ // Render markdown italic: *text* or _text_
455
+ html = html.replace(/\*([^*]+)\*/g, '<em class="italic text-gray-700 dark:text-gray-300">$1</em>');
456
+ html = html.replace(/_([^_]+)_/g, '<em class="italic text-gray-700 dark:text-gray-300">$1</em>');
457
+
458
+ // Render inline code: `code`
459
+ html = html.replace(/`([^`]+)`/g, '<code class="inline-code bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm font-mono text-red-600 dark:text-red-400">$1</code>');
460
+
461
+ // Render markdown links: [text](url)
462
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-blue-600 dark:text-blue-400 hover:underline" target="_blank">$1</a>');
463
+
464
+ // Convert line breaks
465
+ html = html.replace(/\n/g, '<br>');
466
+
467
+ return html;
468
+ }
469
+
470
+ /**
471
+ * Render code block with syntax highlighting
472
+ */
473
+ renderBlockCode(block, context) {
474
+ const div = document.createElement('div');
475
+ div.className = 'block-code';
476
+ div.classList.add(this._getBlockTypeClass('code'));
477
+
478
+ const code = block.code || '';
479
+ const language = (block.language || 'plaintext').toLowerCase();
480
+ const lineCount = code.split('\n').length;
481
+
482
+ const header = document.createElement('div');
483
+ header.className = 'code-block-header';
484
+ header.innerHTML = `
485
+ <span class="collapsible-code-label">${this.escapeHtml(language)} - ${lineCount} line${lineCount !== 1 ? 's' : ''}</span>
486
+ <button class="copy-code-btn" title="Copy code">
487
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
488
+ </button>
489
+ `;
490
+
491
+ const copyBtn = header.querySelector('.copy-code-btn');
492
+ copyBtn.addEventListener('click', (e) => {
493
+ e.preventDefault();
494
+ e.stopPropagation();
495
+ navigator.clipboard.writeText(code).then(() => {
496
+ const orig = copyBtn.innerHTML;
497
+ copyBtn.innerHTML = '<svg viewBox="0 0 20 20" fill="#34d399"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
498
+ setTimeout(() => { copyBtn.innerHTML = orig; }, 2000);
499
+ });
500
+ });
501
+
502
+ const preStyle = "background:#1e293b;padding:1rem;border-radius:0 0 0.375rem 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;border-top:none;margin:0";
503
+ const codeContainer = document.createElement('div');
504
+ codeContainer.innerHTML = `<pre style="${preStyle}"><code class="lazy-hl">${this.escapeHtml(code)}</code></pre>`;
505
+
506
+ div.appendChild(header);
507
+ div.appendChild(codeContainer);
508
+
509
+ return div;
510
+ }
511
+
512
+ /**
513
+ * Render thinking block (expandable)
514
+ */
515
+ renderBlockThinking(block, context) {
516
+ const div = document.createElement('div');
517
+ div.className = 'block-thinking';
518
+ div.classList.add(this._getBlockTypeClass('thinking'));
519
+
520
+ const thinking = block.thinking || '';
521
+ div.innerHTML = `
522
+ <details open>
523
+ <summary>
524
+ <svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
525
+ <span>Thinking Process</span>
526
+ </summary>
527
+ <div class="thinking-content">${this.escapeHtml(thinking)}</div>
528
+ </details>
529
+ `;
530
+
531
+ return div;
532
+ }
533
+
534
+ /**
535
+ * Get a tool-specific icon SVG string
536
+ */
537
+ getToolIcon(toolName) {
538
+ const icons = {
539
+ 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>',
540
+ Write: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>',
541
+ Edit: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"/></svg>',
542
+ Bash: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3.293 1.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L7.586 10 5.293 7.707a1 1 0 010-1.414zM11 12a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg>',
543
+ Glob: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/></svg>',
544
+ Grep: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>',
545
+ WebFetch: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 012 2v1a2 2 0 01-2 2 2 2 0 01-2 2v.5a6.003 6.003 0 01-6.668-7.473z" clip-rule="evenodd"/></svg>',
546
+ WebSearch: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>',
547
+ TodoWrite: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 011 1v3.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 111.414-1.414L6 11.586V8a1 1 0 011-1z" clip-rule="evenodd"/></svg>',
548
+ Task: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg>',
549
+ NotebookEdit: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z"/></svg>'
550
+ };
551
+ 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>';
552
+ }
553
+
554
+ /**
555
+ * Render a file path with icon, directory breadcrumb, and filename
556
+ */
557
+ renderFilePath(filePath) {
558
+ if (!filePath) return '';
559
+ const parts = pathSplit(filePath);
560
+ const fileName = parts.pop();
561
+ const dir = parts.join('/');
562
+ 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>`;
563
+ }
564
+
565
+ /**
566
+ * Render smart tool parameters based on tool type
567
+ */
568
+ renderSmartParams(toolName, input) {
569
+ if (!input || Object.keys(input).length === 0) return '';
570
+
571
+ const normalizedName = toolName.replace(/^mcp__[^_]+__/, '');
572
+
573
+ switch (normalizedName) {
574
+ case 'Read':
575
+ return `<div class="tool-params">${this.renderFilePath(input.file_path)}${input.offset ? `<div style="margin-top:0.375rem;font-size:0.75rem;color:var(--color-text-secondary)">Lines ${input.offset}${input.limit ? '–' + (input.offset + input.limit) : '+'}</div>` : ''}</div>`;
576
+
577
+ case 'Write':
578
+ return `<div class="tool-params">${this.renderFilePath(input.file_path)}${input.content ? this.renderContentPreview(input.content, 'Content') : ''}</div>`;
579
+
580
+ case 'Edit': {
581
+ let html = `<div class="tool-params">${this.renderFilePath(input.file_path)}`;
582
+ if (input.old_string || input.new_string) {
583
+ html += `<div class="tool-param-diff" style="margin-top:0.5rem">`;
584
+ if (input.old_string) {
585
+ html += `<div class="diff-header">Remove</div><div class="diff-old">${this.escapeHtml(this.truncateContent(input.old_string, 500))}</div>`;
586
+ }
587
+ if (input.new_string) {
588
+ html += `<div class="diff-header">Add</div><div class="diff-new">${this.escapeHtml(this.truncateContent(input.new_string, 500))}</div>`;
589
+ }
590
+ html += '</div>';
591
+ }
592
+ return html + '</div>';
593
+ }
594
+
595
+ case 'Bash': {
596
+ const cmd = input.command || input.commands || '';
597
+ const cmdText = typeof cmd === 'string' ? cmd : JSON.stringify(cmd);
598
+ let html = `<div class="tool-params"><div class="tool-param-command"><span class="prompt-char">$</span><span class="command-text">${this.escapeHtml(cmdText)}</span></div>`;
599
+ if (input.description) html += `<div style="margin-top:0.375rem;font-size:0.75rem;color:var(--color-text-secondary)">${this.escapeHtml(input.description)}</div>`;
600
+ return html + '</div>';
601
+ }
602
+
603
+ case 'Glob':
604
+ return `<div class="tool-params"><div class="tool-param-query"><span class="query-icon">&#128193;</span><code style="font-size:0.85rem">${this.escapeHtml(input.pattern || '')}</code></div>${input.path ? `<div style="margin-top:0.25rem;font-size:0.75rem;color:var(--color-text-secondary)">in ${this.escapeHtml(input.path)}</div>` : ''}</div>`;
605
+
606
+ case 'Grep':
607
+ return `<div class="tool-params"><div class="tool-param-query"><span class="query-icon">&#128269;</span><code style="font-size:0.85rem">${this.escapeHtml(input.pattern || '')}</code></div>${input.path ? `<div style="margin-top:0.25rem;font-size:0.75rem;color:var(--color-text-secondary)">in ${this.escapeHtml(input.path)}</div>` : ''}${input.glob ? `<div style="margin-top:0.125rem;font-size:0.7rem;color:var(--color-text-secondary)">files: ${this.escapeHtml(input.glob)}</div>` : ''}</div>`;
608
+
609
+ case 'WebFetch':
610
+ return `<div class="tool-params"><div class="tool-param-url"><span class="url-icon">&#127760;</span>${this.escapeHtml(input.url || '')}</div>${input.prompt ? `<div style="margin-top:0.375rem;font-size:0.8rem;color:var(--color-text-secondary)">${this.escapeHtml(this.truncateContent(input.prompt, 150))}</div>` : ''}</div>`;
611
+
612
+ case 'WebSearch':
613
+ return `<div class="tool-params"><div class="tool-param-query"><span class="query-icon">&#128269;</span><strong style="font-size:0.85rem">${this.escapeHtml(input.query || '')}</strong></div></div>`;
614
+
615
+ case 'TodoWrite':
616
+ if (input.todos && Array.isArray(input.todos)) {
617
+ const statusIcons = { completed: '&#9989;', in_progress: '&#9881;', pending: '&#9744;' };
618
+ const items = input.todos.map(t => `<div class="todo-item"><span class="todo-status">${statusIcons[t.status] || '&#9744;'}</span><span class="todo-text">${this.escapeHtml(t.content || '')}</span></div>`).join('');
619
+ return `<div class="tool-params"><div class="tool-param-todos">${items}</div></div>`;
620
+ }
621
+ return this.renderJsonParams(input);
622
+
623
+ case 'Task':
624
+ return `<div class="tool-params">${input.description ? `<div style="font-weight:600;font-size:0.85rem;margin-bottom:0.375rem">${this.escapeHtml(input.description)}</div>` : ''}${input.prompt ? `<div style="font-size:0.8rem;color:var(--color-text-secondary);max-height:100px;overflow-y:auto;white-space:pre-wrap;word-break:break-word">${this.escapeHtml(this.truncateContent(input.prompt, 300))}</div>` : ''}${input.subagent_type ? `<div style="margin-top:0.375rem;font-size:0.7rem"><code style="background:var(--color-bg-secondary);padding:0.125rem 0.375rem;border-radius:0.25rem">${this.escapeHtml(input.subagent_type)}</code></div>` : ''}</div>`;
625
+
626
+ case 'NotebookEdit':
627
+ return `<div class="tool-params">${this.renderFilePath(input.notebook_path)}${input.new_source ? this.renderContentPreview(input.new_source, 'Cell content') : ''}</div>`;
628
+
629
+ case 'dev__execute':
630
+ case 'dev_execute':
631
+ case 'execute': {
632
+ let html = '<div class="tool-params">';
633
+
634
+ if (input.workingDirectory) {
635
+ html += `<div style="margin-bottom:0.5rem;font-size:0.75rem;color:var(--color-text-secondary)"><span style="opacity:0.7">📁</span> ${this.escapeHtml(input.workingDirectory)}</div>`;
636
+ }
637
+
638
+ if (input.timeout) {
639
+ 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>`;
640
+ }
641
+
642
+ // Render code with syntax highlighting
643
+ if (input.code) {
644
+ const codeLines = input.code.split('\n');
645
+ const lineCount = codeLines.length;
646
+ const truncated = lineCount > 50;
647
+ const displayCode = truncated ? codeLines.slice(0, 50).join('\n') : input.code;
648
+ const lang = input.runtime || 'javascript';
649
+ 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>`;
650
+ }
651
+
652
+ // Render commands (bash commands)
653
+ if (input.commands) {
654
+ const cmds = Array.isArray(input.commands) ? input.commands : [input.commands];
655
+ cmds.forEach(cmd => {
656
+ html += `<div style="margin-top:0.375rem"><div class="tool-param-command"><span class="prompt-char">$</span><span class="command-text">${this.escapeHtml(typeof cmd === 'string' ? cmd : JSON.stringify(cmd))}</span></div></div>`;
657
+ });
658
+ }
659
+
660
+ html += '</div>';
661
+ return html;
662
+ }
663
+
664
+ default:
665
+ return this.renderJsonParams(input);
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Render content preview with truncation
671
+ */
672
+ renderContentPreview(content, label) {
673
+ const maxLen = 500;
674
+ const truncated = content.length > maxLen;
675
+ const displayContent = truncated ? content.substring(0, maxLen) : content;
676
+ const lineCount = content.split('\n').length;
677
+ const codeBody = StreamingRenderer.detectCodeContent(displayContent)
678
+ ? StreamingRenderer.renderCodeWithHighlight(displayContent, this.escapeHtml.bind(this), true)
679
+ : `<div class="preview-body">${this.escapeHtml(displayContent)}</div>`;
680
+ 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>`;
681
+ }
682
+
683
+ /**
684
+ * Render params as formatted JSON (default fallback for unknown tools)
685
+ */
686
+ renderJsonParams(input) {
687
+ return `<div class="tool-params">${this.renderParametersBeautiful(input)}</div>`;
688
+ }
689
+
690
+ /**
691
+ * Render tool use block with smart parameter display
692
+ */
693
+ getToolUseTitle(toolName, input) {
694
+ const normalizedName = toolName.replace(/^mcp__[^_]+__/, '');
695
+ if (normalizedName === 'Edit' && input.file_path) {
696
+ const parts = pathSplit(input.file_path);
697
+ const fileName = parts.pop();
698
+ const dir = parts.slice(-2).join('/');
699
+ return dir ? `${dir}/${fileName}` : fileName;
700
+ }
701
+ if (normalizedName === 'Read' && input.file_path) {
702
+ return pathBasename(input.file_path);
703
+ }
704
+ if (normalizedName === 'Write' && input.file_path) {
705
+ return pathBasename(input.file_path);
706
+ }
707
+ if (normalizedName === 'Bash' || normalizedName === 'bash') {
708
+ const cmd = input.command || input.commands || '';
709
+ const cmdText = typeof cmd === 'string' ? cmd : JSON.stringify(cmd);
710
+ return cmdText.length > 60 ? cmdText.substring(0, 57) + '...' : cmdText;
711
+ }
712
+ if (normalizedName === 'Glob' && input.pattern) return input.pattern;
713
+ if (normalizedName === 'Grep' && input.pattern) return input.pattern;
714
+ if (normalizedName === 'WebFetch' && input.url) {
715
+ try { return new URL(input.url).hostname; } catch (e) { return input.url.substring(0, 40); }
716
+ }
717
+ if (normalizedName === 'WebSearch' && input.query) return input.query.substring(0, 50);
718
+ if (input.file_path) return pathBasename(input.file_path);
719
+ if (input.command) {
720
+ const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command);
721
+ return c.length > 50 ? c.substring(0, 47) + '...' : c;
722
+ }
723
+ if (input.query) return input.query.substring(0, 50);
724
+ return '';
725
+ }
726
+
727
+ getToolUseDisplayName(toolName) {
728
+ const normalized = toolName.replace(/^mcp__[^_]+__/, '');
729
+ const knownTools = ['Read','Write','Edit','Bash','Glob','Grep','WebFetch','WebSearch','TodoWrite','Task','NotebookEdit'];
730
+ if (knownTools.includes(normalized)) return normalized;
731
+ if (toolName.startsWith('mcp__')) {
732
+ const parts = toolName.split('__');
733
+ return parts.length >= 3 ? parts[2] : parts[parts.length - 1];
734
+ }
735
+ return normalized || toolName;
736
+ }
737
+
738
+ renderBlockToolUse(block, context) {
739
+ const toolName = block.name || 'unknown';
740
+ const input = block.input || {};
741
+
742
+ const details = document.createElement('details');
743
+ details.className = 'block-tool-use folded-tool permanently-expanded';
744
+ details.setAttribute('open', '');
745
+ if (block.id) details.dataset.toolUseId = block.id;
746
+ details.classList.add(this._getBlockTypeClass('tool_use'));
747
+ details.classList.add(this._getToolColorClass(toolName));
748
+ const summary = document.createElement('summary');
749
+ summary.className = 'folded-tool-bar';
750
+ const displayName = this.getToolUseDisplayName(toolName);
751
+ const titleInfo = this.getToolUseTitle(toolName, input);
752
+ summary.innerHTML = `
753
+ <span class="folded-tool-icon">${this.getToolIcon(toolName)}</span>
754
+ <span class="folded-tool-name">${this.escapeHtml(displayName)}</span>
755
+ ${titleInfo ? `<span class="folded-tool-desc">${this.escapeHtml(titleInfo)}</span>` : ''}
756
+ `;
757
+ details.appendChild(summary);
758
+ if (Object.keys(input).length > 0) {
759
+ const paramsDiv = document.createElement('div');
760
+ paramsDiv.className = 'folded-tool-body';
761
+ paramsDiv.innerHTML = this.renderSmartParams(toolName, input);
762
+ details.appendChild(paramsDiv);
763
+ }
764
+ return details;
765
+ }
766
+
767
+ /**
768
+ * Render content smartly - detect JSON, images, file lists, markdown
769
+ */
770
+ renderSmartContent(contentStr) {
771
+ const trimmed = contentStr.trim();
772
+
773
+ if (trimmed.startsWith('data:image/')) {
774
+ return `<div style="padding:0.5rem"><img src="${this.escapeHtml(trimmed)}" style="max-width:100%;max-height:24rem;border-radius:0.375rem" loading="lazy"></div>`;
775
+ }
776
+
777
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
778
+ try {
779
+ const parsed = JSON.parse(trimmed);
780
+ return `<div style="padding:0.625rem 1rem">${this.renderParametersBeautiful(parsed)}</div>`;
781
+ } catch (e) {}
782
+ }
783
+
784
+ const lines = trimmed.split('\n');
785
+ const allFilePaths = lines.length > 1 && lines.every(l => {
786
+ const t = l.trim();
787
+ return t === '' || t.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(t);
788
+ });
789
+ if (allFilePaths && lines.filter(l => l.trim()).length > 0) {
790
+ const fileHtml = lines.filter(l => l.trim()).map(l => {
791
+ const p = l.trim();
792
+ const parts = pathSplit(p);
793
+ const name = parts.pop();
794
+ const dir = parts.join('/');
795
+ return `<div style="display:flex;align-items:center;gap:0.375rem;padding:0.1875rem 0;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${this.escapeHtml(dir)}/</span><span style="font-weight:600">${this.escapeHtml(name)}</span></div>`;
796
+ }).join('');
797
+ return `<div style="padding:0.625rem 1rem">${fileHtml}</div>`;
798
+ }
799
+
800
+ if (trimmed.length > 1500) {
801
+ return `<div class="result-body collapsed" 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><button class="expand-btn" onclick="this.previousElementSibling.classList.toggle('collapsed');this.textContent=this.textContent==='Show more'?'Show less':'Show more'">Show more</button>`;
802
+ }
803
+
804
+ 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>`;
805
+ }
806
+
807
+ /**
808
+ * Render parsed JSON/object as formatted key-value display
809
+ */
810
+ renderParametersBeautiful(data, depth = 0) {
811
+ if (data === null || data === undefined) return `<span style="color:var(--color-text-secondary);font-style:italic">null</span>`;
812
+ if (typeof data === 'boolean') return `<span style="color:#d97706;font-weight:600">${data}</span>`;
813
+ if (typeof data === 'number') return `<span style="color:#7c3aed;font-weight:600">${data}</span>`;
814
+
815
+ if (typeof data === 'string') {
816
+ if (data.length > 200 && StreamingRenderer.detectCodeContent(data)) {
817
+ const displayData = data.length > 1000 ? data.substring(0, 1000) : data;
818
+ const suffix = data.length > 1000 ? `<div style="font-size:0.7rem;color:var(--color-text-secondary);text-align:center;padding:0.25rem">... ${data.length - 1000} more characters</div>` : '';
819
+ return `<div style="max-height:200px;overflow-y:auto">${StreamingRenderer.renderCodeWithHighlight(displayData, this.escapeHtml.bind(this), true)}${suffix}</div>`;
820
+ }
821
+ if (data.length > 500) {
822
+ const lines = data.split('\n').length;
823
+ return `<div style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;background:var(--color-bg-code);color:#d1d5db;padding:0.5rem;border-radius:0.375rem;line-height:1.5">${this.escapeHtml(data.substring(0, 1000))}${data.length > 1000 ? '\n... (' + (data.length - 1000) + ' more chars, ' + lines + ' lines)' : ''}</div>`;
824
+ }
825
+ const looksLikePath = data.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(data);
826
+ if (looksLikePath && !data.includes(' ') && data.includes('.')) return this.renderFilePath(data);
827
+ return `<span style="color:var(--color-text-primary)">${this.escapeHtml(data)}</span>`;
828
+ }
829
+
830
+ if (Array.isArray(data)) {
831
+ if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
832
+ if (data.every(i => typeof i === 'string') && data.length <= 20) {
833
+ // Render as an itemized list instead of inline badges
834
+ 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>`;
835
+ }
836
+ 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>`;
837
+ }
838
+
839
+ if (typeof data === 'object') {
840
+ const entries = Object.entries(data);
841
+ if (entries.length === 0) return `<span style="color:var(--color-text-secondary)">{}</span>`;
842
+ return `<div style="display:flex;flex-direction:column;gap:0.375rem;${depth > 0 ? 'padding-left:1rem' : ''}">${entries.map(([k, v]) => `<div style="display:flex;gap:0.5rem;align-items:flex-start"><span style="font-weight:600;font-size:0.75rem;color:#0891b2;flex-shrink:0;min-width:fit-content;font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(k)}</span><div style="flex:1;min-width:0;font-size:0.8rem">${this.renderParametersBeautiful(v, depth + 1)}</div></div>`).join('')}</div>`;
843
+ }
844
+
845
+ return `<span>${this.escapeHtml(String(data))}</span>`;
846
+ }
847
+
848
+ /**
849
+ * Static HTML version of smart content rendering for use in string templates
850
+ */
851
+ static renderSmartContentHTML(contentStr, escapeHtml, flat = false) {
852
+ const trimmed = contentStr.trim();
853
+ const esc = escapeHtml || window._escHtml;
854
+
855
+ if (trimmed.startsWith('data:image/')) {
856
+ return `<div style="padding:0.5rem"><img src="${esc(trimmed)}" style="max-width:100%;max-height:24rem;border-radius:0.375rem" loading="lazy"></div>`;
857
+ }
858
+
859
+ // Parse JSON and render as structured content
860
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
861
+ try {
862
+ const parsed = JSON.parse(trimmed);
863
+
864
+ // Handle Claude content block arrays: [{type:"text", text:"..."}]
865
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed[0] && parsed[0].type === 'text') {
866
+ const textParts = parsed.filter(b => b.type === 'text' && b.text);
867
+ if (textParts.length > 0) {
868
+ const combined = textParts.map(b => b.text).join('\n');
869
+ return StreamingRenderer.renderSmartContentHTML(combined, esc, flat);
870
+ }
871
+ }
872
+
873
+ // For other JSON, render as itemized key-value structure
874
+ return `<div style="padding:0.5rem 0.75rem">${StreamingRenderer.renderParamsHTML(parsed, 0, esc)}</div>`;
875
+ } catch (e) {
876
+ // Not valid JSON, might be code with braces
877
+ }
878
+ }
879
+
880
+ // Check if this looks like `cat -n` output or grep with line numbers
881
+ const lines = trimmed.split('\n');
882
+ const isCatNOutput = lines.length > 1 && lines[0].match(/^\s*\d+→/);
883
+ const isGrepOutput = lines.length > 1 && lines[0].match(/^\s*\d+-/);
884
+
885
+ if (isCatNOutput || isGrepOutput) {
886
+ // Strip line numbers and arrows/hyphens from output
887
+ const cleanedLines = lines.map(line => {
888
+ // Skip grep context separator lines
889
+ if (line === '--') return null;
890
+
891
+ // Handle both cat -n () and grep (-n) formats
892
+ // Also handle grep with colon (:) for matching lines
893
+ const match = line.match(/^\s*\d+[→\-:](.*)/);
894
+ return match ? match[1] : line;
895
+ }).filter(line => line !== null);
896
+ const cleanedContent = cleanedLines.join('\n');
897
+
898
+ // Try to detect and highlight code based on content patterns
899
+ return StreamingRenderer.renderCodeWithHighlight(cleanedContent, esc, flat);
900
+ }
901
+
902
+ // Check for system reminder tags and format them specially
903
+ const systemReminderPattern = /<system-reminder>([\s\S]*?)<\/system-reminder>/g;
904
+ const systemReminders = [];
905
+ let contentWithoutReminders = trimmed;
906
+
907
+ let reminderMatch;
908
+ while ((reminderMatch = systemReminderPattern.exec(trimmed)) !== null) {
909
+ systemReminders.push(reminderMatch[1].trim());
910
+ contentWithoutReminders = contentWithoutReminders.replace(reminderMatch[0], '');
911
+ }
912
+
913
+ // Clean up the content after removing reminders
914
+ contentWithoutReminders = contentWithoutReminders.trim();
915
+
916
+ // Check if this looks like a tool success message with formatted output
917
+ const successPatterns = [
918
+ /^Success\s+toolu_[\w]+$/m,
919
+ /^The file .* has been (updated|created|modified)/,
920
+ /^Here's the result of running `cat -n`/,
921
+ /^Applied \d+ edits? to/,
922
+ /^\w+ tool completed successfully/
923
+ ];
924
+
925
+ const hasSuccessPattern = successPatterns.some(pattern => pattern.test(contentWithoutReminders));
926
+
927
+ if (hasSuccessPattern) {
928
+ const contentLines = contentWithoutReminders.split('\n');
929
+ let successEndIndex = -1;
930
+ let codeStartIndex = -1;
931
+
932
+ // Find the success message and where code starts
933
+ for (let i = 0; i < contentLines.length; i++) {
934
+ const line = contentLines[i];
935
+ if (line.match(/^Success\s+toolu_/)) {
936
+ successEndIndex = i;
937
+ // Look for the next non-empty line that contains code
938
+ for (let j = i + 1; j < contentLines.length; j++) {
939
+ if (contentLines[j].trim() && !contentLines[j].match(/^The file|^Here's the result/)) {
940
+ codeStartIndex = j;
941
+ break;
942
+ }
943
+ }
944
+ break;
945
+ } else if (line.match(/^The file .* has been|^Applied \d+ edits? to|^Replaced|^Created|^Deleted/)) {
946
+ // For edit/write operations, code typically starts after the success message
947
+ // Look for "Here's the result" line or line numbers
948
+ for (let j = i + 1; j < contentLines.length; j++) {
949
+ if (contentLines[j].match(/^Here's the result|^\s*\d+→/)) {
950
+ // If it's "Here's the result", code starts on next line
951
+ if (contentLines[j].match(/^Here's the result/)) {
952
+ codeStartIndex = j + 1;
953
+ } else {
954
+ codeStartIndex = j;
955
+ }
956
+ break;
957
+ } else if (contentLines[j].trim() && !contentLines[j].match(/^cat -n|^Running/)) {
958
+ // If we find non-empty content that's not a command, assume it's code
959
+ codeStartIndex = j;
960
+ break;
961
+ }
962
+ }
963
+ if (codeStartIndex === -1) {
964
+ // No line numbers found, treat next content as code
965
+ codeStartIndex = i + 2;
966
+ }
967
+ successEndIndex = codeStartIndex - 1;
968
+ break;
969
+ }
970
+ }
971
+
972
+ if (codeStartIndex > 0 && codeStartIndex < contentLines.length) {
973
+ const beforeCode = contentLines.slice(0, codeStartIndex).join('\n');
974
+ let codeContent = contentLines.slice(codeStartIndex).join('\n');
975
+
976
+ // Check if code has line numbers and strip them
977
+ if (codeContent.match(/^\s*\d+→/m)) {
978
+ const codeLines = codeContent.split('\n');
979
+ codeContent = codeLines.map(line => {
980
+ const match = line.match(/^\s*\d+→(.*)/);
981
+ return match ? match[1] : line;
982
+ }).join('\n');
983
+ }
984
+
985
+ // Build the formatted output
986
+ let html = '';
987
+
988
+ // Add success message
989
+ if (beforeCode.trim()) {
990
+ html += `<div style="color:var(--color-success);font-weight:600;margin-bottom:0.75rem;font-size:0.9rem">${esc(beforeCode.trim())}</div>`;
991
+ }
992
+
993
+ // Add highlighted code
994
+ if (codeContent.trim()) {
995
+ html += StreamingRenderer.renderCodeWithHighlight(codeContent, esc, flat);
996
+ }
997
+
998
+ // Add system reminders if any
999
+ if (systemReminders.length > 0) {
1000
+ html += StreamingRenderer.renderSystemReminders(systemReminders, esc);
1001
+ }
1002
+
1003
+ return html;
1004
+ }
1005
+ }
1006
+
1007
+ // If there are system reminders but no success pattern, render them separately
1008
+ if (systemReminders.length > 0) {
1009
+ let html = '';
1010
+
1011
+ // Render the main content
1012
+ if (contentWithoutReminders) {
1013
+ // Check if remaining content looks like code
1014
+ if (StreamingRenderer.detectCodeContent(contentWithoutReminders)) {
1015
+ html += StreamingRenderer.renderCodeWithHighlight(contentWithoutReminders, esc, flat);
1016
+ } else {
1017
+ html += `<pre class="tool-result-pre">${esc(contentWithoutReminders)}</pre>`;
1018
+ }
1019
+ }
1020
+
1021
+ // Add system reminders
1022
+ html += StreamingRenderer.renderSystemReminders(systemReminders, esc);
1023
+ return html;
1024
+ }
1025
+
1026
+ const allFilePaths = lines.length > 1 && lines.every(l => {
1027
+ const t = l.trim();
1028
+ return t === '' || t.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(t);
1029
+ });
1030
+ if (allFilePaths && lines.filter(l => l.trim()).length > 0) {
1031
+ const fileHtml = lines.filter(l => l.trim()).map(l => {
1032
+ const p = l.trim();
1033
+ const parts = pathSplit(p);
1034
+ const name = parts.pop();
1035
+ const dir = parts.join('/');
1036
+ return `<div style="display:flex;align-items:center;gap:0.375rem;padding:0.1875rem 0;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${esc(dir)}/</span><span style="font-weight:600">${esc(name)}</span></div>`;
1037
+ }).join('');
1038
+ return `<div style="padding:0.625rem 1rem">${fileHtml}</div>`;
1039
+ }
1040
+
1041
+ // Check if this looks like code
1042
+ const looksLikeCode = StreamingRenderer.detectCodeContent(trimmed);
1043
+ if (looksLikeCode) {
1044
+ return StreamingRenderer.renderCodeWithHighlight(trimmed, esc, flat);
1045
+ }
1046
+
1047
+ const displayContent = trimmed.length > 2000 ? trimmed.substring(0, 2000) + '\n... (truncated)' : trimmed;
1048
+ return `<pre class="tool-result-pre">${esc(displayContent)}</pre>`;
1049
+ }
1050
+
1051
+ /**
1052
+ * Render system reminders in a clean, formatted way
1053
+ */
1054
+ static renderSystemReminders(reminders, esc) {
1055
+ if (!reminders || reminders.length === 0) return '';
1056
+
1057
+ const reminderHtml = reminders.map(reminder => {
1058
+ // Parse reminder content for better formatting
1059
+ const lines = reminder.split('\n').filter(l => l.trim());
1060
+ const formattedLines = lines.map(line => {
1061
+ // Make key points stand out
1062
+ if (line.includes('IMPORTANT:') || line.includes('WARNING:')) {
1063
+ return `<div style="font-weight:600;color:var(--color-warning);margin:0.25rem 0">${esc(line)}</div>`;
1064
+ }
1065
+ return `<div style="margin:0.125rem 0">${esc(line)}</div>`;
1066
+ }).join('');
1067
+
1068
+ return formattedLines;
1069
+ }).join('');
1070
+
1071
+ return `
1072
+ <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)">
1073
+ <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem">
1074
+ <span style="color:var(--color-info)">ℹ</span>
1075
+ <span style="font-weight:600;font-size:0.85rem;color:var(--color-text-primary)">System Reminder</span>
1076
+ </div>
1077
+ ${reminderHtml}
1078
+ </div>
1079
+ `;
1080
+ }
1081
+
1082
+ /**
1083
+ * Detect if content looks like code
1084
+ */
1085
+ static detectCodeContent(content) {
1086
+ // Common code patterns
1087
+ const codePatterns = [
1088
+ /^\s*(function|const|let|var|class|import|export|async|await)/m, // JavaScript
1089
+ /^\s*(def|class|import|from|if __name__|lambda|async def)/m, // Python
1090
+ /^\s*(public|private|protected|class|interface|package|import)/m, // Java/TypeScript
1091
+ /^\s*(<\?php|namespace|use|trait)/m, // PHP
1092
+ /^\s*(#include|int main|void|struct|typedef)/m, // C/C++
1093
+ /[{}\[\];()]/, // Brackets and semicolons
1094
+ /=>|->|::/, // Arrow functions, pointers
1095
+ ];
1096
+
1097
+ return codePatterns.some(pattern => pattern.test(content));
1098
+ }
1099
+
1100
+ /**
1101
+ * Render code with basic syntax highlighting
1102
+ */
1103
+ static renderCodeWithHighlight(code, esc, flat = false) {
1104
+ const preStyle = "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;margin:0";
1105
+ const codeHtml = `<pre style="${preStyle}"><code class="lazy-hl">${esc(code)}</code></pre>`;
1106
+ if (flat) return codeHtml;
1107
+ const lineCount = code.split('\n').length;
1108
+ const summaryLabel = `code - ${lineCount} line${lineCount !== 1 ? 's' : ''}`;
1109
+ return `<details class="collapsible-code"><summary class="collapsible-code-summary">${summaryLabel}</summary>${codeHtml}</details>`;
1110
+ }
1111
+
1112
+ static _setupGlobalLazyHL() {
1113
+ if (StreamingRenderer._lazyHLSetup) return;
1114
+ StreamingRenderer._lazyHLSetup = true;
1115
+ const root = document.getElementById('output-scroll') || document.body;
1116
+ root.addEventListener('toggle', (e) => {
1117
+ const details = e.target;
1118
+ if (!details.open || details.tagName !== 'DETAILS') return;
1119
+ const codeEls = details.querySelectorAll('code.lazy-hl');
1120
+ if (codeEls.length === 0) return;
1121
+ if (typeof hljs === 'undefined') return;
1122
+ for (const el of codeEls) {
1123
+ try {
1124
+ const raw = el.textContent;
1125
+ const result = hljs.highlightAuto(raw);
1126
+ el.classList.remove('lazy-hl');
1127
+ el.classList.add('hljs');
1128
+ el.innerHTML = result.value;
1129
+ } catch (_) {}
1130
+ }
1131
+ }, true);
1132
+ }
1133
+
1134
+ static getToolDisplayName(toolName) {
1135
+ const normalized = toolName.replace(/^mcp__[^_]+__/, '');
1136
+ const knownTools = ['Read','Write','Edit','Bash','Glob','Grep','WebFetch','WebSearch','TodoWrite','Task','NotebookEdit'];
1137
+ if (knownTools.includes(normalized)) return normalized;
1138
+ if (toolName.startsWith('mcp__')) {
1139
+ const parts = toolName.split('__');
1140
+ return parts.length >= 3 ? parts[2] : parts[parts.length - 1];
1141
+ }
1142
+ return normalized || toolName;
1143
+ }
1144
+
1145
+ static getToolTitle(toolName, input) {
1146
+ const n = toolName.replace(/^mcp__[^_]+__/, '');
1147
+ if (n === 'Edit' && input.file_path) { const p = pathSplit(input.file_path); const f = p.pop(); const d = p.slice(-2).join('/'); return d ? d+'/'+f : f; }
1148
+ if (n === 'Read' && input.file_path) return pathBasename(input.file_path);
1149
+ if (n === 'Write' && input.file_path) return pathBasename(input.file_path);
1150
+ if ((n === 'Bash' || n === 'bash') && (input.command || input.commands)) { const c = typeof (input.command||input.commands) === 'string' ? (input.command||input.commands) : JSON.stringify(input.command||input.commands); return c.length > 60 ? c.substring(0,57)+'...' : c; }
1151
+ if (n === 'Glob' && input.pattern) return input.pattern;
1152
+ if (n === 'Grep' && input.pattern) return input.pattern;
1153
+ if (n === 'WebFetch' && input.url) { try { return new URL(input.url).hostname; } catch(e) { return input.url.substring(0,40); } }
1154
+ if (n === 'WebSearch' && input.query) return input.query.substring(0,50);
1155
+ if (input.file_path) return pathBasename(input.file_path);
1156
+ if (input.command) { const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command); return c.length > 50 ? c.substring(0,47)+'...' : c; }
1157
+ if (input.query) return input.query.substring(0,50);
1158
+ return '';
1159
+ }
1160
+
1161
+ /**
1162
+ * Static HTML version of parameter rendering
1163
+ */
1164
+ static renderParamsHTML(data, depth, esc) {
1165
+ if (data === null || data === undefined) return `<span style="color:var(--color-text-secondary);font-style:italic">null</span>`;
1166
+ if (typeof data === 'boolean') return `<span style="color:#d97706;font-weight:600">${data}</span>`;
1167
+ if (typeof data === 'number') return `<span style="color:#7c3aed;font-weight:600">${data}</span>`;
1168
+
1169
+ if (typeof data === 'string') {
1170
+ if (data.length > 200 && StreamingRenderer.detectCodeContent(data)) {
1171
+ const displayData = data.length > 1000 ? data.substring(0, 1000) : data;
1172
+ const suffix = data.length > 1000 ? `<div style="font-size:0.7rem;color:var(--color-text-secondary);text-align:center;padding:0.25rem">... ${data.length - 1000} more characters</div>` : '';
1173
+ return `<div style="max-height:200px;overflow-y:auto">${StreamingRenderer.renderCodeWithHighlight(displayData, esc, true)}${suffix}</div>`;
1174
+ }
1175
+ if (data.length > 500) {
1176
+ return `<div style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;background:var(--color-bg-code);color:#d1d5db;padding:0.5rem;border-radius:0.375rem;line-height:1.5">${esc(data.substring(0, 1000))}${data.length > 1000 ? '\n... (' + (data.length - 1000) + ' more chars)' : ''}</div>`;
1177
+ }
1178
+ const looksLikePath = /^[A-Za-z]:[\\\/]/.test(data) || data.startsWith('/');
1179
+ if (looksLikePath && !data.includes(' ') && data.includes('.')) {
1180
+ const parts = pathSplit(data);
1181
+ const name = parts.pop();
1182
+ const dir = parts.join('/');
1183
+ return `<div style="display:flex;align-items:center;gap:0.375rem;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.8rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${esc(dir)}/</span><span style="font-weight:600">${esc(name)}</span></div>`;
1184
+ }
1185
+ return `<span style="color:var(--color-text-primary)">${esc(data)}</span>`;
1186
+ }
1187
+
1188
+ if (Array.isArray(data)) {
1189
+ if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
1190
+ if (data.every(i => typeof i === 'string') && data.length <= 20) {
1191
+ // Render as an itemized list instead of inline badges
1192
+ 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>`;
1193
+ }
1194
+ 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>`;
1195
+ }
1196
+
1197
+ if (typeof data === 'object') {
1198
+ const entries = Object.entries(data);
1199
+ if (entries.length === 0) return `<span style="color:var(--color-text-secondary)">{}</span>`;
1200
+ return `<div style="display:flex;flex-direction:column;gap:0.375rem;${depth > 0 ? 'padding-left:1rem' : ''}">${entries.map(([k, v]) => `<div style="display:flex;gap:0.5rem;align-items:flex-start"><span style="font-weight:600;font-size:0.75rem;color:#0891b2;flex-shrink:0;min-width:fit-content;font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${esc(k)}</span><div style="flex:1;min-width:0;font-size:0.8rem">${StreamingRenderer.renderParamsHTML(v, depth + 1, esc)}</div></div>`).join('')}</div>`;
1201
+ }
1202
+
1203
+ return `<span>${esc(String(data))}</span>`;
1204
+ }
1205
+
1206
+ /**
1207
+ * Render tool result as inline content to be merged into preceding tool_use block
1208
+ */
1209
+ renderBlockToolResult(block, context) {
1210
+ const isError = block.is_error || false;
1211
+ const content = block.content || '';
1212
+ const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
1213
+ const parentIsOpen = context.parentIsOpen !== undefined ? context.parentIsOpen : true;
1214
+
1215
+ const wrapper = document.createElement('div');
1216
+ wrapper.className = 'tool-result-inline' + (isError ? ' tool-result-error' : ' tool-result-success');
1217
+ wrapper.dataset.eventType = 'tool_result';
1218
+ if (block.tool_use_id) wrapper.dataset.toolUseId = block.tool_use_id;
1219
+ wrapper.classList.add(this._getBlockTypeClass('tool_result'));
1220
+
1221
+ const header = document.createElement('div');
1222
+ header.className = 'tool-result-status';
1223
+ const iconSvg = isError
1224
+ ? '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'
1225
+ : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
1226
+ header.innerHTML = `
1227
+ <span class="folded-tool-icon">${iconSvg}</span>
1228
+ <span class="folded-tool-name">${isError ? 'Error' : 'Success'}</span>
1229
+ `;
1230
+ wrapper.appendChild(header);
1231
+
1232
+ const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this), true);
1233
+ const body = document.createElement('div');
1234
+ body.className = 'folded-tool-body';
1235
+ if (!parentIsOpen) {
1236
+ body.style.display = 'none';
1237
+ }
1238
+ body.innerHTML = renderedContent;
1239
+ wrapper.appendChild(body);
1240
+
1241
+ return wrapper;
1242
+ }
1243
+
1244
+ /**
1245
+ * Render image block
1246
+ */
1247
+ renderBlockImage(block, context) {
1248
+ const div = document.createElement('div');
1249
+ div.className = 'block-image';
1250
+ div.classList.add(this._getBlockTypeClass('image'));
1251
+
1252
+ let src = block.image || block.src || '';
1253
+ const alt = block.alt || 'Image';
1254
+
1255
+ // Handle base64 data
1256
+ if (block.data && block.media_type) {
1257
+ src = `data:${block.media_type};base64,${block.data}`;
1258
+ }
1259
+
1260
+ div.innerHTML = `
1261
+ <img src="${this.escapeHtml(src)}" alt="${this.escapeHtml(alt)}" loading="lazy">
1262
+ ${block.alt ? `<div class="image-caption">${this.escapeHtml(alt)}</div>` : ''}
1263
+ `;
1264
+
1265
+ return div;
1266
+ }
1267
+
1268
+ /**
1269
+ * Render bash command block
1270
+ */
1271
+ renderBlockBash(block, context) {
1272
+ const div = document.createElement('div');
1273
+ div.className = 'block-bash';
1274
+ div.classList.add(this._getBlockTypeClass('bash'));
1275
+
1276
+ const command = block.command || block.code || '';
1277
+ const output = block.output || '';
1278
+
1279
+ // For the command, use simple escaping
1280
+ let html = `<div class="bash-command"><span class="prompt">$</span><code>${this.escapeHtml(command)}</code></div>`;
1281
+
1282
+ // For output, check if it looks like code and use syntax highlighting
1283
+ if (output) {
1284
+ if (StreamingRenderer.detectCodeContent(output)) {
1285
+ html += StreamingRenderer.renderCodeWithHighlight(output, this.escapeHtml.bind(this), true);
1286
+ } else {
1287
+ html += `<pre class="bash-output"><code>${this.escapeHtml(output)}</code></pre>`;
1288
+ }
1289
+ }
1290
+
1291
+ div.innerHTML = html;
1292
+ return div;
1293
+ }
1294
+
1295
+ /**
1296
+ * Render system event
1297
+ */
1298
+ renderBlockSystem(block, context) {
1299
+ const details = document.createElement('details');
1300
+ details.className = 'folded-tool folded-tool-info permanently-expanded';
1301
+ details.setAttribute('open', '');
1302
+ details.dataset.eventType = 'system';
1303
+ details.classList.add(this._getBlockTypeClass('system'));
1304
+ const desc = block.model ? this.escapeHtml(block.model) : 'Session';
1305
+ const summary = document.createElement('summary');
1306
+ summary.className = 'folded-tool-bar';
1307
+ summary.innerHTML = `
1308
+ <span class="folded-tool-icon"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg></span>
1309
+ <span class="folded-tool-name">Session</span>
1310
+ <span class="folded-tool-desc">${desc}</span>
1311
+ `;
1312
+ details.appendChild(summary);
1313
+ const body = document.createElement('div');
1314
+ body.className = 'folded-tool-body block-system';
1315
+ body.innerHTML = `
1316
+ <div class="system-body">
1317
+ ${block.model ? `<div class="sys-field"><span class="sys-label">Model</span><span class="sys-value"><code>${this.escapeHtml(block.model)}</code></span></div>` : ''}
1318
+ ${block.cwd ? `<div class="sys-field"><span class="sys-label">Directory</span><span class="sys-value"><code>${this.escapeHtml(block.cwd)}</code></span></div>` : ''}
1319
+ ${block.session_id ? `<div class="sys-field"><span class="sys-label">Session</span><span class="sys-value"><code>${this.escapeHtml(block.session_id)}</code></span></div>` : ''}
1320
+ ${block.tools && Array.isArray(block.tools) ? `<div class="sys-field" style="flex-direction:column;gap:0.375rem"><span class="sys-label">Tools (${block.tools.length})</span><div class="tools-list">${block.tools.map(t => `<span class="tool-badge">${this.escapeHtml(t)}</span>`).join('')}</div></div>` : ''}
1321
+ </div>
1322
+ `;
1323
+ details.appendChild(body);
1324
+ return details;
1325
+ }
1326
+
1327
+ /**
1328
+ * Render result block (execution summary)
1329
+ */
1330
+ renderBlockResult(block, context) {
1331
+ const isError = block.is_error || false;
1332
+ const duration = block.duration_ms ? (block.duration_ms / 1000).toFixed(1) + 's' : '';
1333
+ const cost = block.total_cost_usd ? '$' + block.total_cost_usd.toFixed(4) : '';
1334
+ const turns = block.num_turns || '';
1335
+ const statsDesc = [duration, cost, turns ? turns + ' turns' : ''].filter(Boolean).join(' / ');
1336
+
1337
+ const details = document.createElement('details');
1338
+ details.className = isError ? 'folded-tool folded-tool-error permanently-expanded' : 'folded-tool permanently-expanded';
1339
+ details.setAttribute('open', '');
1340
+ details.dataset.eventType = 'result';
1341
+ details.classList.add(this._getBlockTypeClass(isError ? 'error' : 'result'));
1342
+
1343
+ const iconSvg = isError
1344
+ ? '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>'
1345
+ : '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>';
1346
+
1347
+ const summary = document.createElement('summary');
1348
+ summary.className = 'folded-tool-bar';
1349
+ summary.innerHTML = `
1350
+ <span class="folded-tool-icon">${iconSvg}</span>
1351
+ <span class="folded-tool-name">${isError ? 'Failed' : 'Complete'}</span>
1352
+ <span class="folded-tool-desc">${this.escapeHtml(statsDesc)}</span>
1353
+ `;
1354
+ details.appendChild(summary);
1355
+
1356
+ if (block.result || duration || cost || turns) {
1357
+ const body = document.createElement('div');
1358
+ body.className = 'folded-tool-body';
1359
+ let bodyHtml = '';
1360
+ if (duration || cost || turns) {
1361
+ bodyHtml += `<div class="block-result"><div class="result-stats">
1362
+ ${duration ? `<div class="result-stat"><span class="stat-icon">&#9202;</span><span class="stat-value">${this.escapeHtml(duration)}</span><span class="stat-label">duration</span></div>` : ''}
1363
+ ${cost ? `<div class="result-stat"><span class="stat-icon">&#128176;</span><span class="stat-value">${this.escapeHtml(cost)}</span><span class="stat-label">cost</span></div>` : ''}
1364
+ ${turns ? `<div class="result-stat"><span class="stat-icon">&#128260;</span><span class="stat-value">${this.escapeHtml(String(turns))}</span><span class="stat-label">turns</span></div>` : ''}
1365
+ </div></div>`;
1366
+ }
1367
+ if (block.result) {
1368
+ const r = typeof block.result === 'string' ? block.result : JSON.stringify(block.result, null, 2);
1369
+ const rendered = this.containsHtmlTags(r) ? '<div class="html-content">' + this.sanitizeHtml(r) + '</div>' : `<div style="font-size:0.8rem;white-space:pre-wrap;word-break:break-word;line-height:1.5">${this.escapeHtml(r)}</div>`;
1370
+ bodyHtml += rendered;
1371
+ }
1372
+ body.innerHTML = bodyHtml;
1373
+ details.appendChild(body);
1374
+ }
1375
+
1376
+ return details;
1377
+ }
1378
+
1379
+ /**
1380
+ * Render tool status block (ACP in_progress/pending updates)
1381
+ */
1382
+ renderBlockToolStatus(block, context) {
1383
+ const status = block.status || 'pending';
1384
+ const statusIcons = {
1385
+ pending: '<svg viewBox="0 0 20 20" fill="currentColor" style="color:var(--color-text-secondary)"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>',
1386
+ in_progress: '<svg viewBox="0 0 20 20" fill="currentColor" class="animate-spin" style="color:var(--color-info)"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>'
1387
+ };
1388
+ const statusLabels = {
1389
+ pending: 'Pending',
1390
+ in_progress: 'Running...'
1391
+ };
1392
+
1393
+ const div = document.createElement('div');
1394
+ div.className = 'block-tool-status';
1395
+ div.dataset.toolUseId = block.tool_use_id || '';
1396
+ div.classList.add(this._getBlockTypeClass('tool_status'));
1397
+ div.innerHTML = `
1398
+ <div style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0.5rem;font-size:0.75rem;color:var(--color-text-secondary)">
1399
+ ${statusIcons[status] || statusIcons.pending}
1400
+ <span>${statusLabels[status] || status}</span>
1401
+ </div>
1402
+ `;
1403
+ return div;
1404
+ }
1405
+
1406
+ /**
1407
+ * Render usage block (ACP usage updates)
1408
+ */
1409
+ renderBlockUsage(block, context) {
1410
+ const usage = block.usage || {};
1411
+ const used = usage.used || 0;
1412
+ const size = usage.size || 0;
1413
+ const cost = usage.cost ? '$' + usage.cost.toFixed(4) : '';
1414
+
1415
+ const div = document.createElement('div');
1416
+ div.className = 'block-usage';
1417
+ div.classList.add(this._getBlockTypeClass('usage'));
1418
+ div.innerHTML = `
1419
+ <div style="display:flex;gap:1rem;padding:0.25rem 0.5rem;font-size:0.7rem;color:var(--color-text-secondary);background:var(--color-bg-secondary);border-radius:0.25rem">
1420
+ ${used ? `<span><strong>Used:</strong> ${used.toLocaleString()}</span>` : ''}
1421
+ ${size ? `<span><strong>Context:</strong> ${size.toLocaleString()}</span>` : ''}
1422
+ ${cost ? `<span><strong>Cost:</strong> ${cost}</span>` : ''}
1423
+ </div>
1424
+ `;
1425
+ return div;
1426
+ }
1427
+
1428
+ /**
1429
+ * Render plan block (ACP plan updates)
1430
+ */
1431
+ renderBlockPlan(block, context) {
1432
+ const entries = block.entries || [];
1433
+ if (entries.length === 0) return null;
1434
+
1435
+ const priorityColors = {
1436
+ high: '#ef4444',
1437
+ medium: '#f59e0b',
1438
+ low: '#6b7280'
1439
+ };
1440
+ const statusIcons = {
1441
+ pending: '○',
1442
+ in_progress: '◐',
1443
+ completed: ''
1444
+ };
1445
+
1446
+ const div = document.createElement('div');
1447
+ div.className = 'block-plan';
1448
+ div.classList.add(this._getBlockTypeClass('plan'));
1449
+ div.innerHTML = `
1450
+ <details class="folded-tool folded-tool-info">
1451
+ <summary class="folded-tool-bar">
1452
+ <span class="folded-tool-icon"><svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg></span>
1453
+ <span class="folded-tool-name">Plan</span>
1454
+ <span class="folded-tool-desc">${entries.length} tasks</span>
1455
+ </summary>
1456
+ <div class="folded-tool-body">
1457
+ <div style="display:flex;flex-direction:column;gap:0.375rem">
1458
+ ${entries.map(e => `
1459
+ <div style="display:flex;align-items:center;gap:0.5rem;font-size:0.8rem">
1460
+ <span style="color:${priorityColors[e.priority] || priorityColors.low}">${statusIcons[e.status] || statusIcons.pending}</span>
1461
+ <span style="${e.status === 'completed' ? 'text-decoration:line-through;opacity:0.6' : ''}">${this.escapeHtml(e.content || '')}</span>
1462
+ </div>
1463
+ `).join('')}
1464
+ </div>
1465
+ </div>
1466
+ </details>
1467
+ `;
1468
+ return div;
1469
+ }
1470
+
1471
+ renderBlockPremature(block, context) {
1472
+ const div = document.createElement('div');
1473
+ div.className = 'folded-tool folded-tool-error block-premature';
1474
+ div.classList.add(this._getBlockTypeClass('premature'));
1475
+ const code = block.exitCode != null ? ` (exit ${block.exitCode})` : '';
1476
+ const stderrDisplay = block.stderrText ? `<div class="folded-tool-content" style="margin-top:8px;padding:8px;background:rgba(0,0,0,0.05);border-radius:4px;font-family:monospace;font-size:0.9em;white-space:pre-wrap;">${this.escapeHtml(block.stderrText)}</div>` : '';
1477
+ div.innerHTML = `
1478
+ <div class="folded-tool-bar" style="background:rgba(245,158,11,0.1)">
1479
+ <span class="folded-tool-icon" style="color:#f59e0b"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg></span>
1480
+ <span class="folded-tool-name" style="color:#f59e0b">ACP Ended Prematurely${this.escapeHtml(code)}</span>
1481
+ <span class="folded-tool-desc">${this.escapeHtml(block.error || 'Process exited without output')}</span>
1482
+ </div>
1483
+ ${stderrDisplay}
1484
+ `;
1485
+ return div;
1486
+ }
1487
+
1488
+ /**
1489
+ * Render generic block with formatted key-value pairs
1490
+ */
1491
+ renderBlockGeneric(block, context) {
1492
+ const div = document.createElement('div');
1493
+ div.className = 'block-generic';
1494
+ div.classList.add(this._getBlockTypeClass('generic'));
1495
+
1496
+ // Show key-value pairs instead of raw JSON
1497
+ const fieldsHtml = Object.entries(block)
1498
+ .filter(([key]) => key !== 'type')
1499
+ .map(([key, value]) => {
1500
+ let displayValue;
1501
+ if (typeof value === 'string') {
1502
+ displayValue = value.length > 200 ? value.substring(0, 200) + '...' : value;
1503
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
1504
+ displayValue = String(value);
1505
+ } else {
1506
+ displayValue = JSON.stringify(value, null, 2);
1507
+ if (displayValue.length > 200) displayValue = displayValue.substring(0, 200) + '...';
1508
+ }
1509
+ return `<div class="generic-field"><span class="field-key">${this.escapeHtml(key)}:</span><span class="field-value">${this.escapeHtml(displayValue)}</span></div>`;
1510
+ }).join('');
1511
+
1512
+ div.innerHTML = `
1513
+ <div class="generic-type">${this.escapeHtml(block.type)}</div>
1514
+ <div class="generic-fields">${fieldsHtml}</div>
1515
+ `;
1516
+
1517
+ return div;
1518
+ }
1519
+
1520
+ /**
1521
+ * Render block error
1522
+ */
1523
+ renderBlockError(block, error) {
1524
+ const div = document.createElement('div');
1525
+ div.className = 'block-error';
1526
+ div.classList.add(this._getBlockTypeClass('error'));
1527
+
1528
+ div.innerHTML = `
1529
+ <div style="display:flex;align-items:flex-start;gap:0.625rem">
1530
+ <svg viewBox="0 0 20 20" fill="currentColor" style="color:#ef4444;flex-shrink:0;margin-top:0.125rem">
1531
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
1532
+ </svg>
1533
+ <div>
1534
+ <div style="font-weight:600;color:#991b1b">Render Error</div>
1535
+ <div style="font-size:0.85rem;color:#7f1d1d;margin-top:0.25rem">${this.escapeHtml(error.message)}</div>
1536
+ </div>
1537
+ </div>
1538
+ `;
1539
+
1540
+ return div;
1541
+ }
1542
+
1543
+ /**
1544
+ * Render streaming start event
1545
+ */
1546
+ renderStreamingStart(event) {
1547
+ const div = document.createElement('div');
1548
+ div.className = 'event-streaming-start card mb-3 p-4 bg-blue-50 dark:bg-blue-900';
1549
+ div.dataset.eventId = event.id || event.sessionId || '';
1550
+ div.dataset.eventType = 'streaming_start';
1551
+
1552
+ const time = new Date(event.timestamp).toLocaleTimeString();
1553
+ div.innerHTML = `
1554
+ <div class="flex items-center gap-2">
1555
+ <svg class="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1556
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" opacity="0.25"></circle>
1557
+ <path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
1558
+ </svg>
1559
+ <div class="flex-1">
1560
+ <h4 class="font-semibold text-blue-900 dark:text-blue-200">Streaming Started</h4>
1561
+ <p class="text-sm text-blue-700 dark:text-blue-300">Agent: ${this.escapeHtml(event.agentId || 'unknown')} • ${time}</p>
1562
+ </div>
1563
+ </div>
1564
+ `;
1565
+ return div;
1566
+ }
1567
+
1568
+ /**
1569
+ * Render streaming progress event
1570
+ */
1571
+ renderStreamingProgress(event) {
1572
+ // If there's a block in the progress event, render it beautifully
1573
+ if (event.block) {
1574
+ return this.renderBlock(event.block, event);
1575
+ }
1576
+
1577
+ // Fallback: simple progress indicator
1578
+ const div = document.createElement('div');
1579
+ div.className = 'event-streaming-progress mb-2 p-2';
1580
+ div.dataset.eventId = event.id || '';
1581
+ div.dataset.eventType = 'streaming_progress';
1582
+
1583
+ const percentage = event.progress || 0;
1584
+ div.innerHTML = `
1585
+ <div class="flex items-center gap-2 text-sm">
1586
+ <span class="text-secondary">${percentage}%</span>
1587
+ <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
1588
+ <div class="bg-blue-500 h-full transition-all" style="width: ${percentage}%"></div>
1589
+ </div>
1590
+ </div>
1591
+ `;
1592
+ return div;
1593
+ }
1594
+
1595
+ /**
1596
+ * Render streaming complete event with metadata
1597
+ */
1598
+ renderStreamingComplete(event) {
1599
+ const div = document.createElement('div');
1600
+ div.className = 'event-streaming-complete card mb-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950 dark:to-emerald-950 border border-green-200 dark:border-green-800 rounded-lg';
1601
+ div.dataset.eventId = event.id || event.sessionId || '';
1602
+ div.dataset.eventType = 'streaming_complete';
1603
+
1604
+ const time = new Date(event.timestamp).toLocaleTimeString();
1605
+ const eventCount = event.eventCount || 0;
1606
+
1607
+ div.innerHTML = `
1608
+ <div class="flex items-start gap-3">
1609
+ <div class="flex-shrink-0 mt-0.5">
1610
+ <svg class="w-6 h-6 text-green-600 dark:text-green-400 animate-bounce" fill="currentColor" viewBox="0 0 20 20">
1611
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
1612
+ </svg>
1613
+ </div>
1614
+ <div class="flex-1">
1615
+ <h4 class="font-bold text-lg text-green-900 dark:text-green-200">✨ Execution Complete</h4>
1616
+ <div class="mt-2 grid grid-cols-2 gap-3 text-sm">
1617
+ <div>
1618
+ <span class="text-green-700 dark:text-green-400 font-semibold">${eventCount}</span>
1619
+ <span class="text-green-600 dark:text-green-500">events processed</span>
1620
+ </div>
1621
+ <div class="text-right">
1622
+ <span class="text-green-600 dark:text-green-500">${time}</span>
1623
+ </div>
1624
+ </div>
1625
+ </div>
1626
+ </div>
1627
+ `;
1628
+ return div;
1629
+ }
1630
+
1631
+ /**
1632
+ * Render file read event
1633
+ */
1634
+ renderFileRead(event) {
1635
+ const fileName = event.path ? event.path.split('/').pop() : 'unknown';
1636
+ const details = document.createElement('details');
1637
+ details.className = 'block-tool-use folded-tool';
1638
+ details.classList.add(this._getBlockTypeClass('tool_use'));
1639
+ details.classList.add(this._getToolColorClass('Read'));
1640
+ details.dataset.eventId = event.id || '';
1641
+ details.dataset.eventType = 'file_read';
1642
+ const summary = document.createElement('summary');
1643
+ summary.className = 'folded-tool-bar';
1644
+ summary.innerHTML = `
1645
+ <span class="folded-tool-icon">${this.getToolIcon('Read')}</span>
1646
+ <span class="folded-tool-name">Read</span>
1647
+ <span class="folded-tool-desc">${this.escapeHtml(fileName)}</span>
1648
+ `;
1649
+ details.appendChild(summary);
1650
+ if (event.path || event.content) {
1651
+ const body = document.createElement('div');
1652
+ body.className = 'folded-tool-body';
1653
+ let html = '';
1654
+ if (event.path) html += this.renderFilePath(event.path);
1655
+ if (event.content) {
1656
+ html += `<pre style="background:#1e293b;padding:0.75rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;line-height:1.5;color:#e2e8f0;margin:0.5rem 0 0 0"><code class="lazy-hl">${this.escapeHtml(this.truncateContent(event.content, 2000))}</code></pre>`;
1657
+ }
1658
+ body.innerHTML = html;
1659
+ details.appendChild(body);
1660
+ }
1661
+ return details;
1662
+ }
1663
+
1664
+ /**
1665
+ * Render file write event
1666
+ */
1667
+ renderFileWrite(event) {
1668
+ const fileName = event.path ? event.path.split('/').pop() : 'unknown';
1669
+ const details = document.createElement('details');
1670
+ details.className = 'block-tool-use folded-tool';
1671
+ details.classList.add(this._getBlockTypeClass('tool_use'));
1672
+ details.classList.add(this._getToolColorClass('Write'));
1673
+ details.dataset.eventId = event.id || '';
1674
+ details.dataset.eventType = 'file_write';
1675
+ const summary = document.createElement('summary');
1676
+ summary.className = 'folded-tool-bar';
1677
+ summary.innerHTML = `
1678
+ <span class="folded-tool-icon">${this.getToolIcon('Write')}</span>
1679
+ <span class="folded-tool-name">Write</span>
1680
+ <span class="folded-tool-desc">${this.escapeHtml(fileName)}</span>
1681
+ `;
1682
+ details.appendChild(summary);
1683
+ if (event.path) {
1684
+ const body = document.createElement('div');
1685
+ body.className = 'folded-tool-body';
1686
+ body.innerHTML = this.renderFilePath(event.path);
1687
+ details.appendChild(body);
1688
+ }
1689
+ return details;
1690
+ }
1691
+
1692
+ /**
1693
+ * Render git status event
1694
+ */
1695
+ renderGitStatus(event) {
1696
+ const div = document.createElement('div');
1697
+ div.className = 'event-git-status card mb-3 p-4';
1698
+ div.dataset.eventId = event.id || '';
1699
+ div.dataset.eventType = 'git_status';
1700
+
1701
+ const branch = event.branch || 'unknown';
1702
+ const changes = event.changes || {};
1703
+ const total = (changes.added || 0) + (changes.modified || 0) + (changes.deleted || 0);
1704
+
1705
+ div.innerHTML = `
1706
+ <div class="flex items-center gap-3 mb-2">
1707
+ <svg class="w-4 h-4 text-orange-600 dark:text-orange-400" fill="currentColor" viewBox="0 0 20 20">
1708
+ <path fill-rule="evenodd" d="M9.243 3.03a1 1 0 01.727 1.155L9.53 6h2.94l.56-2.243a1 1 0 111.94.486L14.53 6H17a1 1 0 110 2h-2.97l-.5 2H17a1 1 0 110 2h-3.03l-.56 2.243a1 1 0 11-1.94-.486L12.47 14H9.53l-.56 2.243a1 1 0 11-1.94-.486L7.47 14H4a1 1 0 110-2h3.03l.5-2H4a1 1 0 110-2h2.97l.56-2.243a1 1 0 011.155-.727zM9.03 8l.5 2h2.94l-.5-2H9.03z" clip-rule="evenodd"></path>
1709
+ </svg>
1710
+ <div class="flex-1">
1711
+ <h4 class="font-semibold text-sm">Git Status</h4>
1712
+ <p class="text-xs text-secondary">Branch: ${this.escapeHtml(branch)}</p>
1713
+ </div>
1714
+ </div>
1715
+ <div class="flex gap-4 text-xs">
1716
+ ${changes.added ? `<span class="text-green-600 dark:text-green-400">+${changes.added}</span>` : ''}
1717
+ ${changes.modified ? `<span class="text-blue-600 dark:text-blue-400">~${changes.modified}</span>` : ''}
1718
+ ${changes.deleted ? `<span class="text-red-600 dark:text-red-400">-${changes.deleted}</span>` : ''}
1719
+ ${total === 0 ? '<span class="text-secondary">no changes</span>' : ''}
1720
+ </div>
1721
+ `;
1722
+ return div;
1723
+ }
1724
+
1725
+ /**
1726
+ * Render command execution event
1727
+ */
1728
+ renderCommand(event) {
1729
+ const command = event.command || '';
1730
+ const output = event.output || '';
1731
+ const exitCode = event.exitCode !== undefined ? event.exitCode : null;
1732
+ const cmdPreview = command.length > 60 ? command.substring(0, 57) + '...' : command;
1733
+
1734
+ const details = document.createElement('details');
1735
+ details.className = 'block-tool-use folded-tool';
1736
+ details.classList.add(this._getBlockTypeClass('tool_use'));
1737
+ details.classList.add(this._getToolColorClass('Bash'));
1738
+ details.dataset.eventId = event.id || '';
1739
+ details.dataset.eventType = 'command_execute';
1740
+ const summary = document.createElement('summary');
1741
+ summary.className = 'folded-tool-bar';
1742
+ summary.innerHTML = `
1743
+ <span class="folded-tool-icon">${this.getToolIcon('Bash')}</span>
1744
+ <span class="folded-tool-name">Bash</span>
1745
+ <span class="folded-tool-desc">${this.escapeHtml(cmdPreview)}</span>
1746
+ `;
1747
+ details.appendChild(summary);
1748
+
1749
+ const body = document.createElement('div');
1750
+ body.className = 'folded-tool-body';
1751
+ let html = `<div class="tool-param-command"><span class="prompt-char">$</span><span class="command-text">${this.escapeHtml(command)}</span></div>`;
1752
+ if (output) {
1753
+ html += `<pre style="background:#1e293b;padding:0.75rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;line-height:1.5;color:#e2e8f0;margin:0.5rem 0 0 0"><code class="lazy-hl">${this.escapeHtml(this.truncateContent(output, 2000))}</code></pre>`;
1754
+ }
1755
+ if (exitCode !== null && exitCode !== 0) {
1756
+ html += `<div style="margin-top:0.375rem;font-size:0.75rem;color:#ef4444;font-weight:600">Exit code: ${exitCode}</div>`;
1757
+ }
1758
+ body.innerHTML = html;
1759
+ details.appendChild(body);
1760
+ return details;
1761
+ }
1762
+
1763
+ /**
1764
+ * Render error event
1765
+ */
1766
+ renderError(event) {
1767
+ const message = event.message || event.error || 'Unknown error';
1768
+ const severity = event.severity || 'error';
1769
+ const msgPreview = message.length > 80 ? message.substring(0, 77) + '...' : message;
1770
+
1771
+ const details = document.createElement('details');
1772
+ details.className = 'folded-tool folded-tool-error permanently-expanded';
1773
+ details.setAttribute('open', '');
1774
+ details.dataset.eventId = event.id || '';
1775
+ details.dataset.eventType = 'error';
1776
+ const summary = document.createElement('summary');
1777
+ summary.className = 'folded-tool-bar';
1778
+ summary.innerHTML = `
1779
+ <span class="folded-tool-icon"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg></span>
1780
+ <span class="folded-tool-name">Error</span>
1781
+ <span class="folded-tool-desc">${this.escapeHtml(msgPreview)}</span>
1782
+ `;
1783
+ details.appendChild(summary);
1784
+
1785
+ const body = document.createElement('div');
1786
+ body.className = 'folded-tool-body';
1787
+ body.innerHTML = `<div style="font-size:0.8rem;white-space:pre-wrap;word-break:break-word;line-height:1.5">${this.escapeHtml(message)}</div>`;
1788
+ details.appendChild(body);
1789
+ return details;
1790
+ }
1791
+
1792
+ isHtmlContent(text) {
1793
+ const openTag = /<(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])\b[^>]*>/i;
1794
+ const closeTag = /<\/(?:div|table|section|article|form|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6])>/i;
1795
+ return openTag.test(text) && closeTag.test(text);
1796
+ }
1797
+
1798
+ parseMarkdownCodeBlocks(text) {
1799
+ const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
1800
+ const parts = [];
1801
+ let lastIndex = 0;
1802
+ let match;
1803
+
1804
+ while ((match = codeBlockRegex.exec(text)) !== null) {
1805
+ if (match.index > lastIndex) {
1806
+ const segment = text.substring(lastIndex, match.index);
1807
+ parts.push({ type: this.isHtmlContent(segment) ? 'html' : 'text', content: segment });
1808
+ }
1809
+ parts.push({ type: 'code', language: match[1] || 'plain', code: match[2] });
1810
+ lastIndex = codeBlockRegex.lastIndex;
1811
+ }
1812
+
1813
+ if (lastIndex < text.length) {
1814
+ const segment = text.substring(lastIndex);
1815
+ parts.push({ type: this.isHtmlContent(segment) ? 'html' : 'text', content: segment });
1816
+ }
1817
+
1818
+ if (parts.length === 0) {
1819
+ return [{ type: this.isHtmlContent(text) ? 'html' : 'text', content: text }];
1820
+ }
1821
+
1822
+ return parts;
1823
+ }
1824
+
1825
+ /**
1826
+ * Render text block event - for backward compatibility
1827
+ */
1828
+ renderText(event) {
1829
+ const div = document.createElement('div');
1830
+ div.className = 'event-text mb-3';
1831
+ div.dataset.eventId = event.id || '';
1832
+ div.dataset.eventType = 'text_block';
1833
+
1834
+ const text = event.text || event.content || '';
1835
+ const parts = this.parseMarkdownCodeBlocks(text);
1836
+ let html = '';
1837
+ parts.forEach(part => {
1838
+ if (part.type === 'html') {
1839
+ html += `<div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto mb-3">${part.content}</div>`;
1840
+ } else if (part.type === 'text') {
1841
+ html += `<div class="p-4 bg-white dark:bg-gray-950 rounded-lg border border-gray-200 dark:border-gray-800 mb-3 leading-relaxed text-sm">${this.parseAndRenderMarkdown(part.content)}</div>`;
1842
+ } else if (part.type === 'code') {
1843
+ if (part.language.toLowerCase() === 'html') {
1844
+ html += `<div class="html-rendered-container mb-3 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-800">
1845
+ <div class="html-rendered-label px-4 py-2 bg-blue-100 dark:bg-blue-900 text-xs font-semibold text-blue-900 dark:text-blue-200">Rendered HTML</div>
1846
+ <div class="html-content bg-white dark:bg-gray-800 p-4 overflow-x-auto">${part.code}</div>
1847
+ </div>`;
1848
+ } else {
1849
+ const partLineCount = part.code.split('\n').length;
1850
+ html += `<div class="mb-3 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-800">
1851
+ <details class="collapsible-code">
1852
+ <summary class="collapsible-code-summary">
1853
+ <span>${this.escapeHtml(part.language)} - ${partLineCount} line${partLineCount !== 1 ? 's' : ''}</span>
1854
+ <button class="copy-code-btn text-gray-400 hover:text-gray-200 transition-colors p-1 rounded hover:bg-gray-800" title="Copy code" onclick="event.preventDefault();event.stopPropagation();navigator.clipboard.writeText(this.closest('.collapsible-code').querySelector('code').textContent)">
1855
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1856
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
1857
+ </svg>
1858
+ </button>
1859
+ </summary>
1860
+ <pre class="bg-gray-900 text-gray-100 p-4 overflow-x-auto" style="margin:0;border-radius:0 0 0.375rem 0.375rem"><code class="language-${this.escapeHtml(part.language)}">${this.escapeHtml(part.code)}</code></pre>
1861
+ </details>
1862
+ </div>`;
1863
+ }
1864
+ }
1865
+ });
1866
+ div.innerHTML = html;
1867
+
1868
+ // Add copy button functionality
1869
+ div.querySelectorAll('.copy-code-btn').forEach(btn => {
1870
+ btn.addEventListener('click', () => {
1871
+ const codeElement = btn.closest('.mb-3')?.querySelector('code');
1872
+ if (codeElement) {
1873
+ const code = codeElement.textContent;
1874
+ navigator.clipboard.writeText(code).then(() => {
1875
+ const originalText = btn.innerHTML;
1876
+ btn.innerHTML = '<svg class="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>';
1877
+ setTimeout(() => { btn.innerHTML = originalText; }, 2000);
1878
+ });
1879
+ }
1880
+ });
1881
+ });
1882
+
1883
+ return div;
1884
+ }
1885
+
1886
+ /**
1887
+ * Render code block event
1888
+ */
1889
+ renderCode(event) {
1890
+ const div = document.createElement('div');
1891
+ div.className = 'event-code mb-3';
1892
+ div.dataset.eventId = event.id || '';
1893
+ div.dataset.eventType = 'code_block';
1894
+
1895
+ const code = event.code || event.content || '';
1896
+ const language = event.language || 'plaintext';
1897
+
1898
+ // Render HTML code blocks as actual HTML elements
1899
+ if (language === 'html') {
1900
+ div.innerHTML = `
1901
+ <div class="html-rendered-container mb-2 p-2 bg-blue-50 dark:bg-blue-900 rounded border border-blue-200 dark:border-blue-700 text-xs text-blue-700 dark:text-blue-300">
1902
+ Rendered HTML
1903
+ </div>
1904
+ <div class="html-content bg-white dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
1905
+ ${code}
1906
+ </div>
1907
+ `;
1908
+ } else {
1909
+ const codeLineCount = code.split('\n').length;
1910
+ div.innerHTML = `
1911
+ <details class="collapsible-code">
1912
+ <summary class="collapsible-code-summary">${this.escapeHtml(language)} - ${codeLineCount} line${codeLineCount !== 1 ? 's' : ''}</summary>
1913
+ <pre class="bg-gray-900 text-gray-100 p-4 overflow-x-auto" style="margin:0;border-radius:0 0 0.375rem 0.375rem"><code class="language-${this.escapeHtml(language)}">${this.escapeHtml(code)}</code></pre>
1914
+ </details>
1915
+ `;
1916
+ }
1917
+ return div;
1918
+ }
1919
+
1920
+ /**
1921
+ * Render thinking block event
1922
+ */
1923
+ renderThinking(event) {
1924
+ const div = document.createElement('div');
1925
+ div.className = 'event-thinking mb-3 p-4 bg-purple-50 dark:bg-purple-900 rounded';
1926
+ div.dataset.eventId = event.id || '';
1927
+ div.dataset.eventType = 'thinking_block';
1928
+
1929
+ const text = event.thinking || event.content || '';
1930
+ div.innerHTML = `
1931
+ <details>
1932
+ <summary class="cursor-pointer font-semibold text-purple-900 dark:text-purple-200">Thinking</summary>
1933
+ <p class="mt-3 text-sm text-purple-800 dark:text-purple-300 whitespace-pre-wrap">${this.escapeHtml(text)}</p>
1934
+ </details>
1935
+ `;
1936
+ return div;
1937
+ }
1938
+
1939
+ /**
1940
+ * Render tool use event - for backward compatibility
1941
+ */
1942
+ renderToolUse(event) {
1943
+ // Use the new block-based renderer for consistency
1944
+ const block = {
1945
+ type: 'tool_use',
1946
+ name: event.toolName || event.tool || 'unknown',
1947
+ input: event.input || {}
1948
+ };
1949
+ const div = this.renderBlockToolUse(block, event);
1950
+ div.className = 'event-tool-use mb-3';
1951
+ div.dataset.eventId = event.id || '';
1952
+ div.dataset.eventType = 'tool_use';
1953
+ return div;
1954
+ }
1955
+
1956
+ /**
1957
+ * Render generic event with formatted key-value pairs
1958
+ */
1959
+ renderGeneric(event) {
1960
+ const div = document.createElement('div');
1961
+ div.className = 'event-generic mb-3 p-3 bg-gray-100 dark:bg-gray-800 rounded text-sm';
1962
+ div.dataset.eventId = event.id || '';
1963
+ div.dataset.eventType = event.type;
1964
+
1965
+ const time = new Date(event.timestamp).toLocaleTimeString();
1966
+
1967
+ // Format event data as key-value pairs
1968
+ const fieldsHtml = Object.entries(event)
1969
+ .filter(([key]) => !['type', 'timestamp'].includes(key))
1970
+ .map(([key, value]) => {
1971
+ let displayValue;
1972
+ if (typeof value === 'string') {
1973
+ displayValue = value.length > 100 ? value.substring(0, 100) + '...' : value;
1974
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
1975
+ displayValue = String(value);
1976
+ } else if (value === null) {
1977
+ displayValue = 'null';
1978
+ } else {
1979
+ displayValue = JSON.stringify(value);
1980
+ if (displayValue.length > 100) displayValue = displayValue.substring(0, 100) + '...';
1981
+ }
1982
+ return `<div style="font-size:0.75rem;margin-bottom:0.25rem"><span style="font-weight:600;color:var(--color-text-secondary)">${this.escapeHtml(key)}:</span> <span style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace">${this.escapeHtml(displayValue)}</span></div>`;
1983
+ }).join('');
1984
+
1985
+ div.innerHTML = `
1986
+ <div style="display:flex;justify-content:space-between;margin-bottom:0.5rem">
1987
+ <span style="font-weight:600;color:var(--color-text-primary)">${this.escapeHtml(event.type)}</span>
1988
+ <span style="font-size:0.75rem;color:var(--color-text-secondary)">${time}</span>
1989
+ </div>
1990
+ <div>${fieldsHtml || '<span style="color:var(--color-text-secondary);font-size:0.75rem">No additional data</span>'}</div>
1991
+ `;
1992
+ return div;
1993
+ }
1994
+
1995
+ /**
1996
+ * Auto-scroll to bottom of container
1997
+ */
1998
+ autoScroll() {
1999
+ if (this._scrollRafPending || this._userScrolledUp) return;
2000
+ this._scrollRafPending = true;
2001
+ requestAnimationFrame(() => {
2002
+ this._scrollRafPending = false;
2003
+ if (this.scrollContainer) {
2004
+ this._programmaticScroll = true;
2005
+ try { this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight; } catch (_) {}
2006
+ this._programmaticScroll = false;
2007
+ }
2008
+ });
2009
+ }
2010
+
2011
+ resetScrollState() {
2012
+ this._userScrolledUp = false;
2013
+ }
2014
+
2015
+ updateVirtualScroll() {
2016
+ }
2017
+
2018
+ /**
2019
+ * Update DOM node count for monitoring
2020
+ */
2021
+ updateDOMNodeCount() {
2022
+ this.domNodeCount = this.outputContainer?.querySelectorAll('[data-event-id]').length || 0;
2023
+ }
2024
+
2025
+ /**
2026
+ * HTML escape utility
2027
+ */
2028
+ escapeHtml(text) {
2029
+ return window._escHtml(text);
2030
+ }
2031
+
2032
+ /**
2033
+ * Format file size for display
2034
+ */
2035
+ formatFileSize(bytes) {
2036
+ if (bytes === 0) return '0 B';
2037
+ const k = 1024;
2038
+ const sizes = ['B', 'KB', 'MB', 'GB'];
2039
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2040
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
2041
+ }
2042
+
2043
+ /**
2044
+ * Truncate content for display
2045
+ */
2046
+ truncateContent(content, maxLength = 200) {
2047
+ if (content.length <= maxLength) return content;
2048
+ return content.substring(0, maxLength) + '...';
2049
+ }
2050
+
2051
+ /**
2052
+ * Clear all rendered events
2053
+ */
2054
+ clear() {
2055
+ if (this.outputContainer) {
2056
+ this.outputContainer.innerHTML = '';
2057
+ }
2058
+ this.eventQueue = [];
2059
+ this.eventHistory = [];
2060
+ this.domNodeCount = 0;
2061
+ this.dedupMap.clear();
2062
+ }
2063
+
2064
+ /**
2065
+ * Get performance metrics
2066
+ */
2067
+ getMetrics() {
2068
+ return {
2069
+ ...this.performanceMetrics,
2070
+ domNodeCount: this.domNodeCount,
2071
+ queueLength: this.eventQueue.length,
2072
+ historyLength: this.eventHistory.length,
2073
+ lastRenderTime: this.lastRenderTime
2074
+ };
2075
+ }
2076
+
2077
+ /**
2078
+ * Add event listener
2079
+ */
2080
+ on(event, callback) {
2081
+ if (!this.listeners[event]) {
2082
+ this.listeners[event] = [];
2083
+ }
2084
+ this.listeners[event].push(callback);
2085
+ }
2086
+
2087
+ /**
2088
+ * Emit event to listeners
2089
+ */
2090
+ emit(event, data) {
2091
+ if (this.listeners[event]) {
2092
+ this.listeners[event].forEach(callback => {
2093
+ try {
2094
+ callback(data);
2095
+ } catch (e) {
2096
+ console.error('Listener error:', e);
2097
+ }
2098
+ });
2099
+ }
2100
+ }
2101
+
2102
+ /**
2103
+ * Cleanup resources
2104
+ */
2105
+ destroy() {
2106
+ if (this.observer) {
2107
+ this.observer.disconnect();
2108
+ }
2109
+ if (this.resizeObserver) {
2110
+ this.resizeObserver.disconnect();
2111
+ }
2112
+ if (this.batchTimer) {
2113
+ clearTimeout(this.batchTimer);
2114
+ }
2115
+ this.listeners = {};
2116
+ this.clear();
2117
+ }
2118
+ }
2119
+
2120
+ // Export for use in browser
2121
+ if (typeof module !== 'undefined' && module.exports) {
2122
+ module.exports = StreamingRenderer;
2123
+ }