agentgui 1.0.67 → 1.0.69

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 (55) hide show
  1. package/.prd +92 -0
  2. package/.prd-browser +607 -0
  3. package/CLAUDE.md +1559 -125
  4. package/browser-test-harness.js +371 -0
  5. package/browser-test.js +409 -0
  6. package/execute-tests.js +164 -0
  7. package/lib/claude-runner.js +41 -12
  8. package/lib/database-service.ts +252 -0
  9. package/lib/sync-service.ts +275 -0
  10. package/lib/types.ts +168 -0
  11. package/package.json +1 -1
  12. package/readme.md +586 -0
  13. package/run-e2e-test.sh +88 -0
  14. package/server.js +274 -8
  15. package/static/index.html +487 -180
  16. package/static/js/client.js +558 -0
  17. package/static/js/event-filter.js +311 -0
  18. package/static/js/event-processor.js +454 -0
  19. package/static/js/streaming-renderer.js +813 -0
  20. package/static/js/syntax-highlighter.js +271 -0
  21. package/static/js/ui-components.js +433 -0
  22. package/static/js/websocket-manager.js +482 -0
  23. package/static/templates/INDEX.html +465 -0
  24. package/static/templates/README.md +190 -0
  25. package/static/templates/agent-capabilities.html +56 -0
  26. package/static/templates/agent-metadata-panel.html +44 -0
  27. package/static/templates/agent-status-badge.html +30 -0
  28. package/static/templates/code-annotation-panel.html +155 -0
  29. package/static/templates/code-suggestion-panel.html +184 -0
  30. package/static/templates/command-header.html +77 -0
  31. package/static/templates/command-output-scrollable.html +118 -0
  32. package/static/templates/elapsed-time.html +54 -0
  33. package/static/templates/error-alert.html +106 -0
  34. package/static/templates/error-history-timeline.html +160 -0
  35. package/static/templates/error-recovery-options.html +109 -0
  36. package/static/templates/error-stack-trace.html +95 -0
  37. package/static/templates/error-summary.html +80 -0
  38. package/static/templates/event-counter.html +48 -0
  39. package/static/templates/execution-actions.html +97 -0
  40. package/static/templates/execution-progress-bar.html +80 -0
  41. package/static/templates/execution-stepper.html +120 -0
  42. package/static/templates/file-breadcrumb.html +118 -0
  43. package/static/templates/file-diff-viewer.html +121 -0
  44. package/static/templates/file-metadata.html +133 -0
  45. package/static/templates/file-read-panel.html +66 -0
  46. package/static/templates/file-write-panel.html +120 -0
  47. package/static/templates/git-branch-remote.html +107 -0
  48. package/static/templates/git-diff-list.html +101 -0
  49. package/static/templates/git-log-visualization.html +153 -0
  50. package/static/templates/git-status-panel.html +115 -0
  51. package/static/templates/quality-metrics-display.html +170 -0
  52. package/static/templates/terminal-output-panel.html +87 -0
  53. package/static/templates/test-results-display.html +144 -0
  54. package/test-browser.js +457 -0
  55. package/test-runner.js +182 -0
@@ -0,0 +1,813 @@
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
+ class StreamingRenderer {
8
+ constructor(config = {}) {
9
+ // Configuration
10
+ this.config = {
11
+ batchSize: config.batchSize || 50,
12
+ batchInterval: config.batchInterval || 16, // ~60fps
13
+ maxQueueSize: config.maxQueueSize || 10000,
14
+ maxEventHistory: config.maxEventHistory || 1000,
15
+ virtualScrollThreshold: config.virtualScrollThreshold || 500,
16
+ debounceDelay: config.debounceDelay || 100,
17
+ ...config
18
+ };
19
+
20
+ // State
21
+ this.eventQueue = [];
22
+ this.eventHistory = [];
23
+ this.isProcessing = false;
24
+ this.batchTimer = null;
25
+ this.dedupMap = new Map();
26
+ this.renderCache = new Map();
27
+ this.domNodeCount = 0;
28
+ this.lastRenderTime = 0;
29
+ this.performanceMetrics = {
30
+ totalEvents: 0,
31
+ totalBatches: 0,
32
+ avgBatchSize: 0,
33
+ avgRenderTime: 0,
34
+ avgProcessTime: 0
35
+ };
36
+
37
+ // DOM references
38
+ this.outputContainer = null;
39
+ this.scrollContainer = null;
40
+ this.virtualScroller = null;
41
+
42
+ // Event listeners
43
+ this.listeners = {
44
+ 'event:queued': [],
45
+ 'event:dequeued': [],
46
+ 'batch:start': [],
47
+ 'batch:complete': [],
48
+ 'render:start': [],
49
+ 'render:complete': [],
50
+ 'error:render': []
51
+ };
52
+
53
+ // Performance monitoring
54
+ this.observer = null;
55
+ this.resizeObserver = null;
56
+ }
57
+
58
+ /**
59
+ * Initialize the renderer with DOM elements
60
+ */
61
+ init(outputContainerId, scrollContainerId = null) {
62
+ this.outputContainer = document.getElementById(outputContainerId);
63
+ this.scrollContainer = scrollContainerId ? document.getElementById(scrollContainerId) : this.outputContainer;
64
+
65
+ if (!this.outputContainer) {
66
+ throw new Error(`Output container not found: ${outputContainerId}`);
67
+ }
68
+
69
+ this.setupDOMObserver();
70
+ this.setupResizeObserver();
71
+ this.setupScrollOptimization();
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Setup DOM mutation observer for external changes
77
+ */
78
+ setupDOMObserver() {
79
+ try {
80
+ this.observer = new MutationObserver(() => {
81
+ this.updateDOMNodeCount();
82
+ });
83
+
84
+ this.observer.observe(this.outputContainer, {
85
+ childList: true,
86
+ subtree: true,
87
+ characterData: false,
88
+ attributes: false
89
+ });
90
+ } catch (e) {
91
+ console.warn('DOM observer setup failed:', e.message);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Setup resize observer for viewport changes
97
+ */
98
+ setupResizeObserver() {
99
+ try {
100
+ this.resizeObserver = new ResizeObserver(() => {
101
+ this.updateVirtualScroll();
102
+ });
103
+
104
+ if (this.scrollContainer) {
105
+ this.resizeObserver.observe(this.scrollContainer);
106
+ }
107
+ } catch (e) {
108
+ console.warn('Resize observer setup failed:', e.message);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Setup scroll optimization and auto-scroll
114
+ */
115
+ setupScrollOptimization() {
116
+ if (this.scrollContainer) {
117
+ this.scrollContainer.addEventListener('scroll', () => {
118
+ this.updateVirtualScroll();
119
+ }, { passive: true });
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Queue an event for batch processing
125
+ */
126
+ queueEvent(event) {
127
+ if (!event || typeof event !== 'object') return false;
128
+
129
+ // Add timestamp if not present
130
+ if (!event.timestamp) {
131
+ event.timestamp = Date.now();
132
+ }
133
+
134
+ // Deduplication
135
+ if (this.isDuplicate(event)) {
136
+ return false;
137
+ }
138
+
139
+ // Queue size check
140
+ if (this.eventQueue.length >= this.config.maxQueueSize) {
141
+ console.warn('Event queue overflow, dropping oldest events');
142
+ this.eventQueue.shift();
143
+ }
144
+
145
+ this.eventQueue.push(event);
146
+ this.eventHistory.push(event);
147
+
148
+ // Trim history
149
+ if (this.eventHistory.length > this.config.maxEventHistory) {
150
+ this.eventHistory.shift();
151
+ }
152
+
153
+ this.emit('event:queued', { event, queueLength: this.eventQueue.length });
154
+ this.scheduleBatchProcess();
155
+ return true;
156
+ }
157
+
158
+ /**
159
+ * Check if event is a duplicate
160
+ */
161
+ isDuplicate(event) {
162
+ const key = this.getEventKey(event);
163
+ if (!key) return false;
164
+
165
+ const lastTime = this.dedupMap.get(key);
166
+ const now = Date.now();
167
+
168
+ // Deduplicate within 100ms window
169
+ if (lastTime && (now - lastTime) < 100) {
170
+ return true;
171
+ }
172
+
173
+ this.dedupMap.set(key, now);
174
+ return false;
175
+ }
176
+
177
+ /**
178
+ * Generate deduplication key for event
179
+ */
180
+ getEventKey(event) {
181
+ if (!event.type) return null;
182
+ return `${event.type}:${event.id || event.sessionId || ''}`;
183
+ }
184
+
185
+ /**
186
+ * Schedule batch processing
187
+ */
188
+ scheduleBatchProcess() {
189
+ if (this.isProcessing || this.batchTimer) return;
190
+
191
+ if (this.eventQueue.length >= this.config.batchSize) {
192
+ // Process immediately if batch is full
193
+ this.processBatch();
194
+ } else {
195
+ // Schedule for later
196
+ this.batchTimer = setTimeout(() => {
197
+ this.batchTimer = null;
198
+ if (this.eventQueue.length > 0) {
199
+ this.processBatch();
200
+ }
201
+ }, this.config.batchInterval);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Process queued events as a batch
207
+ */
208
+ processBatch() {
209
+ if (this.isProcessing) return;
210
+ if (this.eventQueue.length === 0) return;
211
+
212
+ this.isProcessing = true;
213
+ const processStart = performance.now();
214
+ const batchSize = Math.min(this.eventQueue.length, this.config.batchSize);
215
+ const batch = this.eventQueue.splice(0, batchSize);
216
+
217
+ this.emit('batch:start', { batchSize, queueLength: this.eventQueue.length });
218
+
219
+ try {
220
+ // Process and render batch
221
+ const renderStart = performance.now();
222
+ this.renderBatch(batch);
223
+ const renderTime = performance.now() - renderStart;
224
+
225
+ // Update metrics
226
+ this.performanceMetrics.totalBatches++;
227
+ this.performanceMetrics.totalEvents += batchSize;
228
+ this.performanceMetrics.avgBatchSize = this.performanceMetrics.totalEvents / this.performanceMetrics.totalBatches;
229
+ this.performanceMetrics.avgRenderTime = (this.performanceMetrics.avgRenderTime * (this.performanceMetrics.totalBatches - 1) + renderTime) / this.performanceMetrics.totalBatches;
230
+
231
+ this.emit('batch:complete', {
232
+ batchSize,
233
+ renderTime,
234
+ metrics: this.performanceMetrics
235
+ });
236
+
237
+ // Process more if queue is still full
238
+ if (this.eventQueue.length >= this.config.batchSize) {
239
+ this.isProcessing = false;
240
+ setImmediate(() => this.processBatch());
241
+ } else {
242
+ this.isProcessing = false;
243
+ if (this.eventQueue.length > 0) {
244
+ this.scheduleBatchProcess();
245
+ }
246
+ }
247
+ } catch (error) {
248
+ console.error('Batch processing error:', error);
249
+ this.isProcessing = false;
250
+ this.emit('error:render', { error, batch });
251
+ }
252
+
253
+ const processTime = performance.now() - processStart;
254
+ this.performanceMetrics.avgProcessTime = this.performanceMetrics.avgProcessTime || processTime;
255
+ }
256
+
257
+ /**
258
+ * Render a batch of events
259
+ */
260
+ renderBatch(batch) {
261
+ if (!this.outputContainer) return;
262
+
263
+ this.emit('render:start', { eventCount: batch.length });
264
+ const renderStart = performance.now();
265
+
266
+ try {
267
+ // Create document fragment for batch
268
+ const fragment = document.createDocumentFragment();
269
+ let nodeCount = 0;
270
+
271
+ for (const event of batch) {
272
+ try {
273
+ const element = this.renderEvent(event);
274
+ if (element) {
275
+ fragment.appendChild(element);
276
+ nodeCount++;
277
+ }
278
+ } catch (error) {
279
+ console.error('Event render error:', error, event);
280
+ }
281
+ }
282
+
283
+ // Append all at once (minimizes reflows)
284
+ if (nodeCount > 0) {
285
+ this.outputContainer.appendChild(fragment);
286
+ this.domNodeCount += nodeCount;
287
+ }
288
+
289
+ // Auto-scroll to bottom
290
+ this.autoScroll();
291
+
292
+ const renderTime = performance.now() - renderStart;
293
+ this.lastRenderTime = renderTime;
294
+
295
+ this.emit('render:complete', {
296
+ eventCount: batch.length,
297
+ nodeCount,
298
+ renderTime
299
+ });
300
+ } catch (error) {
301
+ console.error('Batch render error:', error);
302
+ this.emit('error:render', { error, batch });
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Render a single event to DOM element
308
+ */
309
+ renderEvent(event) {
310
+ if (!event.type) return null;
311
+
312
+ try {
313
+ switch (event.type) {
314
+ case 'streaming_start':
315
+ return this.renderStreamingStart(event);
316
+ case 'streaming_progress':
317
+ return this.renderStreamingProgress(event);
318
+ case 'streaming_complete':
319
+ return this.renderStreamingComplete(event);
320
+ case 'file_read':
321
+ return this.renderFileRead(event);
322
+ case 'file_write':
323
+ return this.renderFileWrite(event);
324
+ case 'git_status':
325
+ return this.renderGitStatus(event);
326
+ case 'command_execute':
327
+ return this.renderCommand(event);
328
+ case 'error':
329
+ return this.renderError(event);
330
+ case 'text_block':
331
+ return this.renderText(event);
332
+ case 'code_block':
333
+ return this.renderCode(event);
334
+ case 'thinking_block':
335
+ return this.renderThinking(event);
336
+ case 'tool_use':
337
+ return this.renderToolUse(event);
338
+ default:
339
+ return this.renderGeneric(event);
340
+ }
341
+ } catch (error) {
342
+ console.error('Event render error:', error, event);
343
+ return this.renderError({ message: error.message, event });
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Render streaming start event
349
+ */
350
+ renderStreamingStart(event) {
351
+ const div = document.createElement('div');
352
+ div.className = 'event-streaming-start card mb-3 p-4 bg-blue-50 dark:bg-blue-900';
353
+ div.dataset.eventId = event.id || event.sessionId || '';
354
+ div.dataset.eventType = 'streaming_start';
355
+
356
+ const time = new Date(event.timestamp).toLocaleTimeString();
357
+ div.innerHTML = `
358
+ <div class="flex items-center gap-2">
359
+ <svg class="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
360
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" opacity="0.25"></circle>
361
+ <path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
362
+ </svg>
363
+ <div class="flex-1">
364
+ <h4 class="font-semibold text-blue-900 dark:text-blue-200">Streaming Started</h4>
365
+ <p class="text-sm text-blue-700 dark:text-blue-300">Agent: ${this.escapeHtml(event.agentId || 'unknown')} • ${time}</p>
366
+ </div>
367
+ </div>
368
+ `;
369
+ return div;
370
+ }
371
+
372
+ /**
373
+ * Render streaming progress event
374
+ */
375
+ renderStreamingProgress(event) {
376
+ const div = document.createElement('div');
377
+ div.className = 'event-streaming-progress mb-2 p-2 border-l-4 border-blue-500';
378
+ div.dataset.eventId = event.id || '';
379
+ div.dataset.eventType = 'streaming_progress';
380
+
381
+ const percentage = event.progress || 0;
382
+ div.innerHTML = `
383
+ <div class="flex items-center gap-2 text-sm">
384
+ <span class="text-secondary">${percentage}%</span>
385
+ <div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
386
+ <div class="bg-blue-500 h-full transition-all" style="width: ${percentage}%"></div>
387
+ </div>
388
+ </div>
389
+ `;
390
+ return div;
391
+ }
392
+
393
+ /**
394
+ * Render streaming complete event
395
+ */
396
+ renderStreamingComplete(event) {
397
+ const div = document.createElement('div');
398
+ div.className = 'event-streaming-complete card mb-3 p-4 bg-green-50 dark:bg-green-900';
399
+ div.dataset.eventId = event.id || event.sessionId || '';
400
+ div.dataset.eventType = 'streaming_complete';
401
+
402
+ const time = new Date(event.timestamp).toLocaleTimeString();
403
+ const duration = event.duration ? `${(event.duration / 1000).toFixed(2)}s` : 'unknown';
404
+ div.innerHTML = `
405
+ <div class="flex items-center gap-2">
406
+ <svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
407
+ <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>
408
+ </svg>
409
+ <div class="flex-1">
410
+ <h4 class="font-semibold text-green-900 dark:text-green-200">Streaming Complete</h4>
411
+ <p class="text-sm text-green-700 dark:text-green-300">Duration: ${this.escapeHtml(duration)} • ${time}</p>
412
+ </div>
413
+ </div>
414
+ `;
415
+ return div;
416
+ }
417
+
418
+ /**
419
+ * Render file read event
420
+ */
421
+ renderFileRead(event) {
422
+ const div = document.createElement('div');
423
+ div.className = 'event-file-read card mb-3 p-4';
424
+ div.dataset.eventId = event.id || '';
425
+ div.dataset.eventType = 'file_read';
426
+
427
+ const fileName = event.path ? event.path.split('/').pop() : 'unknown';
428
+ const size = event.size || 0;
429
+ const sizeStr = this.formatFileSize(size);
430
+
431
+ div.innerHTML = `
432
+ <div class="flex items-start justify-between gap-3 mb-3">
433
+ <div class="flex items-center gap-2 flex-1">
434
+ <svg class="w-4 h-4 text-primary flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
435
+ <path d="M5.5 13a3 3 0 01.369-1.618l1.83-1.83a3 3 0 015.604 0l.83 1.83A3 3 0 0113.5 13H11V9.413l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13H5.5z"></path>
436
+ </svg>
437
+ <div class="flex-1 min-w-0">
438
+ <h4 class="font-semibold text-sm truncate">${this.escapeHtml(fileName)}</h4>
439
+ <p class="text-xs text-secondary truncate" title="${this.escapeHtml(event.path || '')}">${this.escapeHtml(event.path || '')}</p>
440
+ </div>
441
+ </div>
442
+ <span class="badge badge-sm flex-shrink-0">${this.escapeHtml(sizeStr)}</span>
443
+ </div>
444
+ ${event.content ? `
445
+ <pre class="bg-gray-50 dark:bg-gray-900 p-3 rounded border text-xs overflow-x-auto"><code>${this.escapeHtml(this.truncateContent(event.content, 500))}</code></pre>
446
+ ` : ''}
447
+ `;
448
+ return div;
449
+ }
450
+
451
+ /**
452
+ * Render file write event
453
+ */
454
+ renderFileWrite(event) {
455
+ const div = document.createElement('div');
456
+ div.className = 'event-file-write card mb-3 p-4 border-l-4 border-yellow-500';
457
+ div.dataset.eventId = event.id || '';
458
+ div.dataset.eventType = 'file_write';
459
+
460
+ const fileName = event.path ? event.path.split('/').pop() : 'unknown';
461
+ const size = event.size || 0;
462
+ const sizeStr = this.formatFileSize(size);
463
+
464
+ div.innerHTML = `
465
+ <div class="flex items-start justify-between gap-3 mb-3">
466
+ <div class="flex items-center gap-2 flex-1">
467
+ <svg class="w-4 h-4 text-yellow-600 dark:text-yellow-400 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
468
+ <path d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 110-2V4z"></path>
469
+ </svg>
470
+ <div class="flex-1 min-w-0">
471
+ <h4 class="font-semibold text-sm truncate">${this.escapeHtml(fileName)}</h4>
472
+ <p class="text-xs text-secondary truncate" title="${this.escapeHtml(event.path || '')}">${this.escapeHtml(event.path || '')}</p>
473
+ </div>
474
+ </div>
475
+ <span class="badge badge-sm bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 flex-shrink-0">Written</span>
476
+ </div>
477
+ <span class="text-xs text-secondary">${this.escapeHtml(sizeStr)}</span>
478
+ `;
479
+ return div;
480
+ }
481
+
482
+ /**
483
+ * Render git status event
484
+ */
485
+ renderGitStatus(event) {
486
+ const div = document.createElement('div');
487
+ div.className = 'event-git-status card mb-3 p-4 border-l-4 border-orange-500';
488
+ div.dataset.eventId = event.id || '';
489
+ div.dataset.eventType = 'git_status';
490
+
491
+ const branch = event.branch || 'unknown';
492
+ const changes = event.changes || {};
493
+ const total = (changes.added || 0) + (changes.modified || 0) + (changes.deleted || 0);
494
+
495
+ div.innerHTML = `
496
+ <div class="flex items-center gap-3 mb-2">
497
+ <svg class="w-4 h-4 text-orange-600 dark:text-orange-400" fill="currentColor" viewBox="0 0 20 20">
498
+ <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>
499
+ </svg>
500
+ <div class="flex-1">
501
+ <h4 class="font-semibold text-sm">Git Status</h4>
502
+ <p class="text-xs text-secondary">Branch: ${this.escapeHtml(branch)}</p>
503
+ </div>
504
+ </div>
505
+ <div class="flex gap-4 text-xs">
506
+ ${changes.added ? `<span class="text-green-600 dark:text-green-400">+${changes.added}</span>` : ''}
507
+ ${changes.modified ? `<span class="text-blue-600 dark:text-blue-400">~${changes.modified}</span>` : ''}
508
+ ${changes.deleted ? `<span class="text-red-600 dark:text-red-400">-${changes.deleted}</span>` : ''}
509
+ ${total === 0 ? '<span class="text-secondary">no changes</span>' : ''}
510
+ </div>
511
+ `;
512
+ return div;
513
+ }
514
+
515
+ /**
516
+ * Render command execution event
517
+ */
518
+ renderCommand(event) {
519
+ const div = document.createElement('div');
520
+ div.className = 'event-command card mb-3 p-4 font-mono text-sm';
521
+ div.dataset.eventId = event.id || '';
522
+ div.dataset.eventType = 'command_execute';
523
+
524
+ const command = event.command || '';
525
+ const output = event.output || '';
526
+ const exitCode = event.exitCode !== undefined ? event.exitCode : null;
527
+
528
+ div.innerHTML = `
529
+ <div class="bg-gray-900 text-gray-100 p-3 rounded mb-2 overflow-x-auto">
530
+ <div class="text-green-400">$ ${this.escapeHtml(command)}</div>
531
+ </div>
532
+ ${output ? `
533
+ <div class="bg-gray-50 dark:bg-gray-900 p-3 rounded border text-xs overflow-x-auto">
534
+ <pre><code>${this.escapeHtml(this.truncateContent(output, 500))}</code></pre>
535
+ </div>
536
+ ` : ''}
537
+ ${exitCode !== null ? `
538
+ <div class="text-xs mt-2 ${exitCode === 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}">
539
+ Exit code: ${exitCode}
540
+ </div>
541
+ ` : ''}
542
+ `;
543
+ return div;
544
+ }
545
+
546
+ /**
547
+ * Render error event
548
+ */
549
+ renderError(event) {
550
+ const div = document.createElement('div');
551
+ div.className = 'event-error card mb-3 p-4 bg-red-50 dark:bg-red-900 border-l-4 border-red-500';
552
+ div.dataset.eventId = event.id || '';
553
+ div.dataset.eventType = 'error';
554
+
555
+ const message = event.message || event.error || 'Unknown error';
556
+ const severity = event.severity || 'error';
557
+
558
+ div.innerHTML = `
559
+ <div class="flex items-start gap-3">
560
+ <svg class="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
561
+ <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"></path>
562
+ </svg>
563
+ <div class="flex-1">
564
+ <h4 class="font-semibold text-red-900 dark:text-red-200">Error: ${this.escapeHtml(severity)}</h4>
565
+ <p class="text-sm text-red-800 dark:text-red-300 mt-1">${this.escapeHtml(message)}</p>
566
+ </div>
567
+ </div>
568
+ `;
569
+ return div;
570
+ }
571
+
572
+ /**
573
+ * Render text block event
574
+ */
575
+ renderText(event) {
576
+ const div = document.createElement('div');
577
+ div.className = 'event-text mb-3 p-3 bg-gray-50 dark:bg-gray-900 rounded border';
578
+ div.dataset.eventId = event.id || '';
579
+ div.dataset.eventType = 'text_block';
580
+
581
+ const text = event.text || event.content || '';
582
+ div.innerHTML = `<p class="text-sm leading-relaxed">${this.escapeHtml(text)}</p>`;
583
+ return div;
584
+ }
585
+
586
+ /**
587
+ * Render code block event
588
+ */
589
+ renderCode(event) {
590
+ const div = document.createElement('div');
591
+ div.className = 'event-code mb-3';
592
+ div.dataset.eventId = event.id || '';
593
+ div.dataset.eventType = 'code_block';
594
+
595
+ const code = event.code || event.content || '';
596
+ const language = event.language || 'plaintext';
597
+
598
+ div.innerHTML = `
599
+ <pre class="bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto"><code class="language-${this.escapeHtml(language)}">${this.escapeHtml(code)}</code></pre>
600
+ `;
601
+ return div;
602
+ }
603
+
604
+ /**
605
+ * Render thinking block event
606
+ */
607
+ renderThinking(event) {
608
+ const div = document.createElement('div');
609
+ div.className = 'event-thinking mb-3 p-4 bg-purple-50 dark:bg-purple-900 rounded border-l-4 border-purple-500';
610
+ div.dataset.eventId = event.id || '';
611
+ div.dataset.eventType = 'thinking_block';
612
+
613
+ const text = event.thinking || event.content || '';
614
+ div.innerHTML = `
615
+ <details>
616
+ <summary class="cursor-pointer font-semibold text-purple-900 dark:text-purple-200">Thinking</summary>
617
+ <p class="mt-3 text-sm text-purple-800 dark:text-purple-300 whitespace-pre-wrap">${this.escapeHtml(text)}</p>
618
+ </details>
619
+ `;
620
+ return div;
621
+ }
622
+
623
+ /**
624
+ * Render tool use event
625
+ */
626
+ renderToolUse(event) {
627
+ const div = document.createElement('div');
628
+ div.className = 'event-tool-use card mb-3 p-4 border-l-4 border-cyan-500';
629
+ div.dataset.eventId = event.id || '';
630
+ div.dataset.eventType = 'tool_use';
631
+
632
+ const toolName = event.toolName || event.tool || 'unknown';
633
+ const input = event.input || {};
634
+
635
+ div.innerHTML = `
636
+ <div class="flex items-center gap-2 mb-2">
637
+ <svg class="w-4 h-4 text-cyan-600 dark:text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
638
+ <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-.373zM14.6 15.477l-5.223-7.912h-3.5l5.223 7.912h3.5z" clip-rule="evenodd"></path>
639
+ </svg>
640
+ <h4 class="font-semibold text-sm">Tool: ${this.escapeHtml(toolName)}</h4>
641
+ </div>
642
+ ${Object.keys(input).length > 0 ? `
643
+ <pre class="bg-gray-50 dark:bg-gray-900 p-3 rounded text-xs overflow-x-auto"><code>${this.escapeHtml(JSON.stringify(input, null, 2))}</code></pre>
644
+ ` : ''}
645
+ `;
646
+ return div;
647
+ }
648
+
649
+ /**
650
+ * Render generic event
651
+ */
652
+ renderGeneric(event) {
653
+ const div = document.createElement('div');
654
+ div.className = 'event-generic mb-3 p-3 bg-gray-100 dark:bg-gray-800 rounded text-sm';
655
+ div.dataset.eventId = event.id || '';
656
+ div.dataset.eventType = event.type;
657
+
658
+ const time = new Date(event.timestamp).toLocaleTimeString();
659
+ div.innerHTML = `
660
+ <div class="flex items-center justify-between mb-2">
661
+ <span class="font-semibold text-gray-900 dark:text-gray-100">${this.escapeHtml(event.type)}</span>
662
+ <span class="text-xs text-gray-600 dark:text-gray-400">${time}</span>
663
+ </div>
664
+ <pre class="text-xs overflow-x-auto"><code>${this.escapeHtml(JSON.stringify(event, null, 2))}</code></pre>
665
+ `;
666
+ return div;
667
+ }
668
+
669
+ /**
670
+ * Auto-scroll to bottom of container
671
+ */
672
+ autoScroll() {
673
+ if (this.scrollContainer) {
674
+ try {
675
+ this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
676
+ } catch (e) {
677
+ // Ignore scroll errors
678
+ }
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Update virtual scroll based on viewport
684
+ */
685
+ updateVirtualScroll() {
686
+ if (!this.scrollContainer) return;
687
+
688
+ // Calculate visible items
689
+ const scrollTop = this.scrollContainer.scrollTop;
690
+ const viewportHeight = this.scrollContainer.clientHeight;
691
+ const itemHeight = 80; // Approximate item height
692
+
693
+ const firstVisible = Math.floor(scrollTop / itemHeight);
694
+ const lastVisible = Math.ceil((scrollTop + viewportHeight) / itemHeight);
695
+
696
+ // Update visibility of DOM nodes
697
+ const items = this.outputContainer?.querySelectorAll('[data-event-id]');
698
+ if (!items) return;
699
+
700
+ items.forEach((item, index) => {
701
+ const isVisible = index >= firstVisible && index <= lastVisible;
702
+ item.style.display = isVisible ? '' : 'none';
703
+ });
704
+ }
705
+
706
+ /**
707
+ * Update DOM node count for monitoring
708
+ */
709
+ updateDOMNodeCount() {
710
+ this.domNodeCount = this.outputContainer?.querySelectorAll('[data-event-id]').length || 0;
711
+ }
712
+
713
+ /**
714
+ * HTML escape utility
715
+ */
716
+ escapeHtml(text) {
717
+ const div = document.createElement('div');
718
+ div.textContent = text;
719
+ return div.innerHTML;
720
+ }
721
+
722
+ /**
723
+ * Format file size for display
724
+ */
725
+ formatFileSize(bytes) {
726
+ if (bytes === 0) return '0 B';
727
+ const k = 1024;
728
+ const sizes = ['B', 'KB', 'MB', 'GB'];
729
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
730
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
731
+ }
732
+
733
+ /**
734
+ * Truncate content for display
735
+ */
736
+ truncateContent(content, maxLength = 200) {
737
+ if (content.length <= maxLength) return content;
738
+ return content.substring(0, maxLength) + '...';
739
+ }
740
+
741
+ /**
742
+ * Clear all rendered events
743
+ */
744
+ clear() {
745
+ if (this.outputContainer) {
746
+ this.outputContainer.innerHTML = '';
747
+ }
748
+ this.eventQueue = [];
749
+ this.eventHistory = [];
750
+ this.domNodeCount = 0;
751
+ this.dedupMap.clear();
752
+ }
753
+
754
+ /**
755
+ * Get performance metrics
756
+ */
757
+ getMetrics() {
758
+ return {
759
+ ...this.performanceMetrics,
760
+ domNodeCount: this.domNodeCount,
761
+ queueLength: this.eventQueue.length,
762
+ historyLength: this.eventHistory.length,
763
+ lastRenderTime: this.lastRenderTime
764
+ };
765
+ }
766
+
767
+ /**
768
+ * Add event listener
769
+ */
770
+ on(event, callback) {
771
+ if (!this.listeners[event]) {
772
+ this.listeners[event] = [];
773
+ }
774
+ this.listeners[event].push(callback);
775
+ }
776
+
777
+ /**
778
+ * Emit event to listeners
779
+ */
780
+ emit(event, data) {
781
+ if (this.listeners[event]) {
782
+ this.listeners[event].forEach(callback => {
783
+ try {
784
+ callback(data);
785
+ } catch (e) {
786
+ console.error('Listener error:', e);
787
+ }
788
+ });
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Cleanup resources
794
+ */
795
+ destroy() {
796
+ if (this.observer) {
797
+ this.observer.disconnect();
798
+ }
799
+ if (this.resizeObserver) {
800
+ this.resizeObserver.disconnect();
801
+ }
802
+ if (this.batchTimer) {
803
+ clearTimeout(this.batchTimer);
804
+ }
805
+ this.listeners = {};
806
+ this.clear();
807
+ }
808
+ }
809
+
810
+ // Export for use in browser
811
+ if (typeof module !== 'undefined' && module.exports) {
812
+ module.exports = StreamingRenderer;
813
+ }