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.
- package/.prd +92 -0
- package/.prd-browser +607 -0
- package/CLAUDE.md +1559 -125
- package/browser-test-harness.js +371 -0
- package/browser-test.js +409 -0
- package/execute-tests.js +164 -0
- package/lib/claude-runner.js +41 -12
- package/lib/database-service.ts +252 -0
- package/lib/sync-service.ts +275 -0
- package/lib/types.ts +168 -0
- package/package.json +1 -1
- package/readme.md +586 -0
- package/run-e2e-test.sh +88 -0
- package/server.js +274 -8
- package/static/index.html +487 -180
- package/static/js/client.js +558 -0
- package/static/js/event-filter.js +311 -0
- package/static/js/event-processor.js +454 -0
- package/static/js/streaming-renderer.js +813 -0
- package/static/js/syntax-highlighter.js +271 -0
- package/static/js/ui-components.js +433 -0
- package/static/js/websocket-manager.js +482 -0
- package/static/templates/INDEX.html +465 -0
- package/static/templates/README.md +190 -0
- package/static/templates/agent-capabilities.html +56 -0
- package/static/templates/agent-metadata-panel.html +44 -0
- package/static/templates/agent-status-badge.html +30 -0
- package/static/templates/code-annotation-panel.html +155 -0
- package/static/templates/code-suggestion-panel.html +184 -0
- package/static/templates/command-header.html +77 -0
- package/static/templates/command-output-scrollable.html +118 -0
- package/static/templates/elapsed-time.html +54 -0
- package/static/templates/error-alert.html +106 -0
- package/static/templates/error-history-timeline.html +160 -0
- package/static/templates/error-recovery-options.html +109 -0
- package/static/templates/error-stack-trace.html +95 -0
- package/static/templates/error-summary.html +80 -0
- package/static/templates/event-counter.html +48 -0
- package/static/templates/execution-actions.html +97 -0
- package/static/templates/execution-progress-bar.html +80 -0
- package/static/templates/execution-stepper.html +120 -0
- package/static/templates/file-breadcrumb.html +118 -0
- package/static/templates/file-diff-viewer.html +121 -0
- package/static/templates/file-metadata.html +133 -0
- package/static/templates/file-read-panel.html +66 -0
- package/static/templates/file-write-panel.html +120 -0
- package/static/templates/git-branch-remote.html +107 -0
- package/static/templates/git-diff-list.html +101 -0
- package/static/templates/git-log-visualization.html +153 -0
- package/static/templates/git-status-panel.html +115 -0
- package/static/templates/quality-metrics-display.html +170 -0
- package/static/templates/terminal-output-panel.html +87 -0
- package/static/templates/test-results-display.html +144 -0
- package/test-browser.js +457 -0
- 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
|
+
}
|