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.
- package/CLAUDE.md +280 -280
- package/IPFS_DOWNLOADER.md +277 -277
- package/TASK_2C_COMPLETION.md +334 -334
- package/bin/gmgui.cjs +54 -54
- package/build-portable.js +3 -42
- package/database.js +1422 -1406
- package/lib/claude-runner.js +1130 -1130
- package/lib/ipfs-downloader.js +459 -459
- package/lib/speech.js +152 -152
- package/package.json +1 -1
- package/readme.md +76 -76
- package/server.js +3787 -3794
- package/setup-npm-token.sh +68 -68
- package/static/app.js +773 -773
- package/static/event-rendering-showcase.html +708 -708
- package/static/index.html +3178 -3180
- package/static/js/agent-auth.js +298 -298
- package/static/js/audio-recorder-processor.js +18 -18
- package/static/js/client.js +2656 -2656
- package/static/js/conversations.js +583 -583
- package/static/js/dialogs.js +267 -267
- package/static/js/event-consolidator.js +101 -101
- package/static/js/event-filter.js +311 -311
- package/static/js/event-processor.js +452 -452
- package/static/js/features.js +413 -413
- package/static/js/kalman-filter.js +67 -67
- package/static/js/progress-dialog.js +130 -130
- package/static/js/script-runner.js +219 -219
- package/static/js/streaming-renderer.js +2123 -2120
- package/static/js/syntax-highlighter.js +269 -269
- package/static/js/tts-websocket-handler.js +152 -152
- package/static/js/ui-components.js +431 -431
- package/static/js/voice.js +849 -849
- package/static/js/websocket-manager.js +596 -596
- package/static/templates/INDEX.html +465 -465
- package/static/templates/README.md +190 -190
- package/static/templates/agent-capabilities.html +56 -56
- package/static/templates/agent-metadata-panel.html +44 -44
- package/static/templates/agent-status-badge.html +30 -30
- package/static/templates/code-annotation-panel.html +155 -155
- package/static/templates/code-suggestion-panel.html +184 -184
- package/static/templates/command-header.html +77 -77
- package/static/templates/command-output-scrollable.html +118 -118
- package/static/templates/elapsed-time.html +54 -54
- package/static/templates/error-alert.html +106 -106
- package/static/templates/error-history-timeline.html +160 -160
- package/static/templates/error-recovery-options.html +109 -109
- package/static/templates/error-stack-trace.html +95 -95
- package/static/templates/error-summary.html +80 -80
- package/static/templates/event-counter.html +48 -48
- package/static/templates/execution-actions.html +97 -97
- package/static/templates/execution-progress-bar.html +80 -80
- package/static/templates/execution-stepper.html +120 -120
- package/static/templates/file-breadcrumb.html +118 -118
- package/static/templates/file-diff-viewer.html +121 -121
- package/static/templates/file-metadata.html +133 -133
- package/static/templates/file-read-panel.html +66 -66
- package/static/templates/file-write-panel.html +120 -120
- package/static/templates/git-branch-remote.html +107 -107
- package/static/templates/git-diff-list.html +101 -101
- package/static/templates/git-log-visualization.html +153 -153
- package/static/templates/git-status-panel.html +115 -115
- package/static/templates/quality-metrics-display.html +170 -170
- package/static/templates/terminal-output-panel.html +87 -87
- package/static/templates/test-results-display.html +144 -144
- package/static/theme.js +72 -72
- package/test-download-progress.js +223 -223
- package/test-websocket-broadcast.js +147 -147
- 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">📄</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">📁</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">🔍</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">🌐</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">🔍</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: '✅', in_progress: '⚙', pending: '☐' };
|
|
618
|
-
const items = input.todos.map(t => `<div class="todo-item"><span class="todo-status">${statusIcons[t.status] || '☐'}</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
|
-
|
|
745
|
-
details.
|
|
746
|
-
details.classList.add(this.
|
|
747
|
-
|
|
748
|
-
summary
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
<span class="folded-tool-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
paramsDiv
|
|
760
|
-
paramsDiv.
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
const
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
if (
|
|
812
|
-
if (typeof data === '
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (data.
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
//
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
/^
|
|
919
|
-
/^
|
|
920
|
-
/^
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
let
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
if
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
const
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
<span style="
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
/^\s*(
|
|
1089
|
-
/^\s*(
|
|
1090
|
-
/^\s*(
|
|
1091
|
-
/^\s*(
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
StreamingRenderer._lazyHLSetup
|
|
1114
|
-
|
|
1115
|
-
root.
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
el.classList.
|
|
1127
|
-
el.
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if (n === '
|
|
1148
|
-
if (n === '
|
|
1149
|
-
if (
|
|
1150
|
-
if (n === '
|
|
1151
|
-
if (n === '
|
|
1152
|
-
if (n === '
|
|
1153
|
-
if (n === '
|
|
1154
|
-
if (input.
|
|
1155
|
-
if (input.
|
|
1156
|
-
if (input.
|
|
1157
|
-
return
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (
|
|
1166
|
-
if (typeof data === '
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
if (data.
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
wrapper
|
|
1216
|
-
wrapper.
|
|
1217
|
-
|
|
1218
|
-
wrapper.
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
header
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
<span class="folded-tool-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const
|
|
1233
|
-
body
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
div
|
|
1249
|
-
div.
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
${
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
div
|
|
1273
|
-
div.
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
details
|
|
1300
|
-
details.
|
|
1301
|
-
details.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
summary
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
<span class="folded-tool-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
body
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
${block.
|
|
1318
|
-
${block.
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
const
|
|
1332
|
-
const
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
details
|
|
1338
|
-
details.
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
div.
|
|
1394
|
-
div.
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
div
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
div
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
<
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
div.
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
div.
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
<
|
|
1531
|
-
<
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
div
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
div.
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
<
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
div
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
div.
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
div
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
<span class="text-green-600 dark:text-green-500"
|
|
1620
|
-
</div>
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
details.
|
|
1637
|
-
details.
|
|
1638
|
-
details.
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
details.
|
|
1670
|
-
details.
|
|
1671
|
-
details.
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
div
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
<
|
|
1708
|
-
<
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
${
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
const
|
|
1730
|
-
|
|
1731
|
-
const
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
details.
|
|
1735
|
-
details.
|
|
1736
|
-
details.
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
}
|
|
1752
|
-
if (
|
|
1753
|
-
html += `<
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
details
|
|
1772
|
-
details.
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
div
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
div.
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
div
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
<div class="html-
|
|
1902
|
-
|
|
1903
|
-
</div>
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
div
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
div.
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
div
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
div
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
this.
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
this.
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
this.listeners[event]
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
if (this.
|
|
2107
|
-
this.
|
|
2108
|
-
}
|
|
2109
|
-
if (this.
|
|
2110
|
-
|
|
2111
|
-
}
|
|
2112
|
-
this.
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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">📄</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">📁</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">🔍</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">🌐</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">🔍</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: '✅', in_progress: '⚙', pending: '☐' };
|
|
618
|
+
const items = input.todos.map(t => `<div class="todo-item"><span class="todo-status">${statusIcons[t.status] || '☐'}</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">📄</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">📄</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">📄</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">⏲</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">💰</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">🔄</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
|
+
}
|