agentgui 1.0.827 → 1.0.828
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/CHANGELOG.md +3 -0
- package/package.json +1 -1
- package/static/index.html +14 -0
- package/static/js/streaming-renderer-dispatch.js +144 -0
- package/static/js/streaming-renderer-events.js +163 -0
- package/static/js/streaming-renderer-events2.js +125 -0
- package/static/js/streaming-renderer-params.js +38 -0
- package/static/js/streaming-renderer-render-misc.js +107 -0
- package/static/js/streaming-renderer-render.js +181 -0
- package/static/js/streaming-renderer-render2.js +149 -0
- package/static/js/streaming-renderer-render3.js +142 -0
- package/static/js/streaming-renderer-static.js +181 -0
- package/static/js/streaming-renderer-static2.js +140 -0
- package/static/js/streaming-renderer-stream.js +170 -0
- package/static/js/streaming-renderer-text.js +185 -0
- package/static/js/streaming-renderer-tools.js +189 -0
- package/static/js/streaming-renderer-tools2.js +92 -0
- package/static/js/streaming-renderer.js +4 -1996
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Object.assign(StreamingRenderer, {
|
|
2
|
+
renderSystemReminders(reminders, esc) {
|
|
3
|
+
if (!reminders || reminders.length === 0) return '';
|
|
4
|
+
|
|
5
|
+
const reminderHtml = reminders.map(reminder => {
|
|
6
|
+
const lines = reminder.split('\n').filter(l => l.trim());
|
|
7
|
+
const formattedLines = lines.map(line => {
|
|
8
|
+
if (line.includes('IMPORTANT:') || line.includes('WARNING:')) {
|
|
9
|
+
return `<div style="font-weight:600;color:var(--color-warning);margin:0.25rem 0">${esc(line)}</div>`;
|
|
10
|
+
}
|
|
11
|
+
return `<div style="margin:0.125rem 0">${esc(line)}</div>`;
|
|
12
|
+
}).join('');
|
|
13
|
+
|
|
14
|
+
return formattedLines;
|
|
15
|
+
}).join('');
|
|
16
|
+
|
|
17
|
+
return `
|
|
18
|
+
<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)">
|
|
19
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem">
|
|
20
|
+
<span style="color:var(--color-info)">ℹ</span>
|
|
21
|
+
<span style="font-weight:600;font-size:0.85rem;color:var(--color-text-primary)">System Reminder</span>
|
|
22
|
+
</div>
|
|
23
|
+
${reminderHtml}
|
|
24
|
+
</div>
|
|
25
|
+
`;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
detectCodeContent(content) {
|
|
29
|
+
const codePatterns = [
|
|
30
|
+
/^\s*(function|const|let|var|class|import|export|async|await)/m, // JavaScript
|
|
31
|
+
/^\s*(def|class|import|from|if __name__|lambda|async def)/m, // Python
|
|
32
|
+
/^\s*(public|private|protected|class|interface|package|import)/m, // Java/TypeScript
|
|
33
|
+
/^\s*(<\?php|namespace|use|trait)/m, // PHP
|
|
34
|
+
/^\s*(#include|int main|void|struct|typedef)/m, // C/C++
|
|
35
|
+
/[{}\[\];()]/, // Brackets and semicolons
|
|
36
|
+
/=>|->|::/, // Arrow functions, pointers
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
return codePatterns.some(pattern => pattern.test(content));
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
renderCodeWithHighlight(code, esc, flat = false) {
|
|
43
|
+
const preStyle = "background:var(--color-bg-code);padding:1rem;border-radius:0.375rem;overflow-x:auto;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.875rem;line-height:1.6;color:var(--color-code-text);border:1px solid var(--color-code-border);margin:0";
|
|
44
|
+
const codeHtml = `<pre style="${preStyle}"><code class="lazy-hl">${esc(code)}</code></pre>`;
|
|
45
|
+
if (flat) return codeHtml;
|
|
46
|
+
const lineCount = code.split('\n').length;
|
|
47
|
+
const summaryLabel = `code - ${lineCount} line${lineCount !== 1 ? 's' : ''}`;
|
|
48
|
+
return `<details class="collapsible-code"><summary class="collapsible-code-summary">${summaryLabel}</summary>${codeHtml}</details>`;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
_setupGlobalLazyHL() {
|
|
52
|
+
if (StreamingRenderer._lazyHLSetup) return;
|
|
53
|
+
StreamingRenderer._lazyHLSetup = true;
|
|
54
|
+
const root = document.getElementById('output-scroll') || document.body;
|
|
55
|
+
root.addEventListener('toggle', (e) => {
|
|
56
|
+
const details = e.target;
|
|
57
|
+
if (!details.open || details.tagName !== 'DETAILS') return;
|
|
58
|
+
const codeEls = details.querySelectorAll('code.lazy-hl');
|
|
59
|
+
if (codeEls.length === 0) return;
|
|
60
|
+
if (typeof hljs === 'undefined') return;
|
|
61
|
+
for (const el of codeEls) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = el.textContent;
|
|
64
|
+
const result = hljs.highlightAuto(raw);
|
|
65
|
+
el.classList.remove('lazy-hl');
|
|
66
|
+
el.classList.add('hljs');
|
|
67
|
+
el.innerHTML = result.value;
|
|
68
|
+
} catch (_) {}
|
|
69
|
+
}
|
|
70
|
+
}, true);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
getToolDisplayName(toolName) {
|
|
74
|
+
const normalized = toolName.replace(/^mcp__.*?__/, '');
|
|
75
|
+
const knownTools = ['Read','Write','Edit','Bash','Glob','Grep','WebFetch','WebSearch','TodoWrite','Task','NotebookEdit'];
|
|
76
|
+
if (knownTools.includes(normalized)) return normalized;
|
|
77
|
+
if (toolName.startsWith('mcp__')) {
|
|
78
|
+
const parts = toolName.split('__');
|
|
79
|
+
return parts.length >= 3 ? parts[2] : parts[parts.length - 1];
|
|
80
|
+
}
|
|
81
|
+
return normalized || toolName;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
getToolTitle(toolName, input) {
|
|
85
|
+
const n = toolName.replace(/^mcp__.*?__/, '');
|
|
86
|
+
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; }
|
|
87
|
+
if (n === 'Read' && input.file_path) return pathBasename(input.file_path);
|
|
88
|
+
if (n === 'Write' && input.file_path) return pathBasename(input.file_path);
|
|
89
|
+
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; }
|
|
90
|
+
if (n === 'Glob' && input.pattern) return input.pattern;
|
|
91
|
+
if (n === 'Grep' && input.pattern) return input.pattern;
|
|
92
|
+
if (n === 'WebFetch' && input.url) { try { return new URL(input.url).hostname; } catch(e) { return input.url.substring(0,40); } }
|
|
93
|
+
if (n === 'WebSearch' && input.query) return input.query.substring(0,50);
|
|
94
|
+
if (input.file_path) return pathBasename(input.file_path);
|
|
95
|
+
if (input.command) { const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command); return c.length > 50 ? c.substring(0,47)+'...' : c; }
|
|
96
|
+
if (input.query) return input.query.substring(0,50);
|
|
97
|
+
return '';
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
renderParamsHTML(data, depth, esc) {
|
|
101
|
+
if (data === null || data === undefined) return `<span style="color:var(--color-text-secondary);font-style:italic">null</span>`;
|
|
102
|
+
if (typeof data === 'boolean') return `<span style="color:#d97706;font-weight:600">${data}</span>`;
|
|
103
|
+
if (typeof data === 'number') return `<span style="color:#7c3aed;font-weight:600">${data}</span>`;
|
|
104
|
+
|
|
105
|
+
if (typeof data === 'string') {
|
|
106
|
+
if (data.length > 200 && StreamingRenderer.detectCodeContent(data)) {
|
|
107
|
+
const displayData = data.length > 1000 ? data.substring(0, 1000) : data;
|
|
108
|
+
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>` : '';
|
|
109
|
+
return `<div style="max-height:200px;overflow-y:auto">${StreamingRenderer.renderCodeWithHighlight(displayData, esc, true)}${suffix}</div>`;
|
|
110
|
+
}
|
|
111
|
+
if (data.length > 500) {
|
|
112
|
+
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:var(--color-code-text);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>`;
|
|
113
|
+
}
|
|
114
|
+
const looksLikePath = /^[A-Za-z]:[\\\/]/.test(data) || data.startsWith('/');
|
|
115
|
+
if (looksLikePath && !data.includes(' ') && data.includes('.')) {
|
|
116
|
+
const parts = pathSplit(data);
|
|
117
|
+
const name = parts.pop();
|
|
118
|
+
const dir = parts.join('/');
|
|
119
|
+
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>`;
|
|
120
|
+
}
|
|
121
|
+
return `<span style="color:var(--color-text-primary)">${esc(data)}</span>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(data)) {
|
|
125
|
+
if (data.length === 0) return `<span style="color:var(--color-text-secondary)">[]</span>`;
|
|
126
|
+
if (data.every(i => typeof i === 'string') && data.length <= 20) {
|
|
127
|
+
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>`;
|
|
128
|
+
}
|
|
129
|
+
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>`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof data === 'object') {
|
|
133
|
+
const entries = Object.entries(data);
|
|
134
|
+
if (entries.length === 0) return `<span style="color:var(--color-text-secondary)">{}</span>`;
|
|
135
|
+
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>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return `<span>${esc(String(data))}</span>`;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Object.assign(StreamingRenderer.prototype, {
|
|
2
|
+
renderStreamingStart(event) {
|
|
3
|
+
const div = document.createElement('div');
|
|
4
|
+
div.className = 'event-streaming-start card mb-3 p-4 alert alert-info';
|
|
5
|
+
div.dataset.eventId = event.id || event.sessionId || '';
|
|
6
|
+
div.dataset.eventType = 'streaming_start';
|
|
7
|
+
|
|
8
|
+
const time = new Date(event.timestamp).toLocaleTimeString();
|
|
9
|
+
div.innerHTML = `
|
|
10
|
+
<div class="flex items-center gap-2">
|
|
11
|
+
<svg class="w-5 h-5 text-info animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
12
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" opacity="0.25"></circle>
|
|
13
|
+
<path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
|
|
14
|
+
</svg>
|
|
15
|
+
<div class="flex-1">
|
|
16
|
+
<h4 class="font-semibold text-info-content">Streaming Started</h4>
|
|
17
|
+
<p class="text-sm text-info-content/70">${time}</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
`;
|
|
21
|
+
return div;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
renderStreamingProgress(event) {
|
|
25
|
+
if (event.block) {
|
|
26
|
+
return this.renderBlock(event.block, event);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const div = document.createElement('div');
|
|
30
|
+
div.className = 'event-streaming-progress mb-2 p-2';
|
|
31
|
+
div.dataset.eventId = event.id || '';
|
|
32
|
+
div.dataset.eventType = 'streaming_progress';
|
|
33
|
+
|
|
34
|
+
const percentage = event.progress || 0;
|
|
35
|
+
div.innerHTML = `
|
|
36
|
+
<div class="flex items-center gap-2 text-sm">
|
|
37
|
+
<span class="text-secondary">${percentage}%</span>
|
|
38
|
+
<div class="progress progress-primary flex-1">
|
|
39
|
+
<div class="progress-bar" style="width: ${percentage}%"></div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
`;
|
|
43
|
+
return div;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
renderStreamingComplete(event) {
|
|
47
|
+
const div = document.createElement('div');
|
|
48
|
+
div.className = 'event-streaming-complete card mb-3 p-4 alert alert-success rounded-lg';
|
|
49
|
+
div.dataset.eventId = event.id || event.sessionId || '';
|
|
50
|
+
div.dataset.eventType = 'streaming_complete';
|
|
51
|
+
|
|
52
|
+
const time = new Date(event.timestamp).toLocaleTimeString();
|
|
53
|
+
const eventCount = event.eventCount || 0;
|
|
54
|
+
|
|
55
|
+
div.innerHTML = `
|
|
56
|
+
<div class="flex items-start gap-3">
|
|
57
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
58
|
+
<svg class="w-6 h-6 text-success animate-bounce" fill="currentColor" viewBox="0 0 20 20">
|
|
59
|
+
<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>
|
|
60
|
+
</svg>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="flex-1">
|
|
63
|
+
<h4 class="font-bold text-lg text-success-content">✨ Execution Complete</h4>
|
|
64
|
+
<div class="mt-2 grid grid-cols-2 gap-3 text-sm">
|
|
65
|
+
<div>
|
|
66
|
+
<span class="text-success font-semibold">${eventCount}</span>
|
|
67
|
+
<span class="text-success/70">events processed</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="text-right">
|
|
70
|
+
<span class="text-success/70">${time}</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
return div;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
detectBase64Image(content) {
|
|
80
|
+
if (!content || typeof content !== 'string') return null;
|
|
81
|
+
const trimmed = content.trim();
|
|
82
|
+
const signatures = {
|
|
83
|
+
'png': /^iVBORw0KGgo/,
|
|
84
|
+
'jpeg': /^\/9j\/4AAQ/,
|
|
85
|
+
'webp': /^UklGRi/,
|
|
86
|
+
'gif': /^R0lGODlh/
|
|
87
|
+
};
|
|
88
|
+
for (const [type, pattern] of Object.entries(signatures)) {
|
|
89
|
+
if (pattern.test(trimmed)) {
|
|
90
|
+
return { type, isBase64: true, data: trimmed };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
renderFileRead(event) {
|
|
97
|
+
const fileName = event.path ? event.path.split('/').pop() : 'unknown';
|
|
98
|
+
const details = document.createElement('details');
|
|
99
|
+
details.className = 'block-tool-use folded-tool';
|
|
100
|
+
details.classList.add(this._getBlockTypeClass('tool_use'));
|
|
101
|
+
details.classList.add(this._getToolColorClass('Read'));
|
|
102
|
+
details.dataset.eventId = event.id || '';
|
|
103
|
+
details.dataset.eventType = 'file_read';
|
|
104
|
+
const summary = document.createElement('summary');
|
|
105
|
+
summary.className = 'folded-tool-bar';
|
|
106
|
+
summary.innerHTML = `
|
|
107
|
+
<span class="folded-tool-icon">${this.getToolIcon('Read')}</span>
|
|
108
|
+
<span class="folded-tool-name">Read</span>
|
|
109
|
+
<span class="folded-tool-desc">${this.escapeHtml(fileName)}</span>
|
|
110
|
+
`;
|
|
111
|
+
details.appendChild(summary);
|
|
112
|
+
if (event.path || event.content) {
|
|
113
|
+
const body = document.createElement('div');
|
|
114
|
+
body.className = 'folded-tool-body';
|
|
115
|
+
let html = '';
|
|
116
|
+
if (event.path) html += this.renderFilePath(event.path);
|
|
117
|
+
if (event.content) {
|
|
118
|
+
let base64Data = null;
|
|
119
|
+
let mimeType = event.media_type || 'application/octet-stream';
|
|
120
|
+
if (typeof event.content === 'string') {
|
|
121
|
+
const imageInfo = this.detectBase64Image(event.content);
|
|
122
|
+
if (imageInfo) {
|
|
123
|
+
base64Data = imageInfo.data;
|
|
124
|
+
mimeType = imageInfo.type === 'jpeg' ? 'image/jpeg' : `image/${imageInfo.type}`;
|
|
125
|
+
}
|
|
126
|
+
} else if (typeof event.content === 'object' && event.content !== null) {
|
|
127
|
+
if (event.content.source?.type === 'base64' && event.content.source?.data) {
|
|
128
|
+
base64Data = event.content.source.data;
|
|
129
|
+
} else if (event.content.type === 'base64' && event.content.data) {
|
|
130
|
+
base64Data = event.content.data;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (base64Data) {
|
|
134
|
+
html += `<div style="padding:0.5rem;display:flex;flex-direction:column;gap:0.5rem"><img src="data:${mimeType};base64,${this.escapeHtml(base64Data)}" style="max-width:100%;max-height:600px;border-radius:0.375rem;border:1px solid var(--color-code-border)" loading="lazy"><div style="font-size:0.7rem;color:var(--color-text-secondary);font-family:'Monaco','Menlo','Ubuntu Mono',monospace;word-break:break-all">${this.escapeHtml(event.path)}</div></div>`;
|
|
135
|
+
} else {
|
|
136
|
+
const contentStr = typeof event.content === 'string' ? event.content : JSON.stringify(event.content, null, 2);
|
|
137
|
+
html += `<pre style="background:var(--color-bg-code);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:var(--color-code-text);margin:0.5rem 0 0 0"><code class="lazy-hl">${this.escapeHtml(this.truncateContent(contentStr, 2000))}</code></pre>`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
body.innerHTML = html;
|
|
141
|
+
details.appendChild(body);
|
|
142
|
+
}
|
|
143
|
+
return details;
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
renderFileWrite(event) {
|
|
147
|
+
const fileName = event.path ? event.path.split('/').pop() : 'unknown';
|
|
148
|
+
const details = document.createElement('details');
|
|
149
|
+
details.className = 'block-tool-use folded-tool';
|
|
150
|
+
details.classList.add(this._getBlockTypeClass('tool_use'));
|
|
151
|
+
details.classList.add(this._getToolColorClass('Write'));
|
|
152
|
+
details.dataset.eventId = event.id || '';
|
|
153
|
+
details.dataset.eventType = 'file_write';
|
|
154
|
+
const summary = document.createElement('summary');
|
|
155
|
+
summary.className = 'folded-tool-bar';
|
|
156
|
+
summary.innerHTML = `
|
|
157
|
+
<span class="folded-tool-icon">${this.getToolIcon('Write')}</span>
|
|
158
|
+
<span class="folded-tool-name">Write</span>
|
|
159
|
+
<span class="folded-tool-desc">${this.escapeHtml(fileName)}</span>
|
|
160
|
+
`;
|
|
161
|
+
details.appendChild(summary);
|
|
162
|
+
if (event.path) {
|
|
163
|
+
const body = document.createElement('div');
|
|
164
|
+
body.className = 'folded-tool-body';
|
|
165
|
+
body.innerHTML = this.renderFilePath(event.path);
|
|
166
|
+
details.appendChild(body);
|
|
167
|
+
}
|
|
168
|
+
return details;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Object.assign(StreamingRenderer.prototype, {
|
|
2
|
+
renderBlockText(block, context, targetContainer = null) {
|
|
3
|
+
const text = block.text || '';
|
|
4
|
+
const isHtml = this.containsHtmlTags(text);
|
|
5
|
+
const cached = this.renderCache.get(text);
|
|
6
|
+
const html = cached || (isHtml ? this.sanitizeHtml(text) : this.parseAndRenderMarkdown(text));
|
|
7
|
+
|
|
8
|
+
if (!cached && this.renderCache.size < 2000) {
|
|
9
|
+
this.renderCache.set(text, html);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const container = targetContainer || this.outputContainer;
|
|
13
|
+
const lastChild = container && container.lastElementChild;
|
|
14
|
+
if (lastChild && lastChild.classList.contains('block-text') && !isHtml && !lastChild.classList.contains('html-content')) {
|
|
15
|
+
lastChild.innerHTML += html;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const div = document.createElement('div');
|
|
20
|
+
div.className = 'block-text';
|
|
21
|
+
if (isHtml) div.classList.add('html-content');
|
|
22
|
+
div.innerHTML = html;
|
|
23
|
+
div.classList.add(this._getBlockTypeClass('text'));
|
|
24
|
+
return div;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
_getBlockTypeClass(blockType) {
|
|
28
|
+
const validTypes = ['text','tool_use','tool_result','code','thinking','bash','system','result','error','image','plan','usage','premature','tool_status','generic'];
|
|
29
|
+
return validTypes.includes(blockType) ? `block-type-${blockType}` : 'block-type-generic';
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
_getToolColorClass(toolName) {
|
|
33
|
+
const n = (toolName || '').replace(/^mcp__.*?__/, '').toLowerCase();
|
|
34
|
+
const map = {
|
|
35
|
+
read: 'read', write: 'write', edit: 'edit', bash: 'bash', glob: 'glob', grep: 'grep',
|
|
36
|
+
webfetch: 'web', websearch: 'web', todowrite: 'todo', task: 'task', notebookedit: 'edit',
|
|
37
|
+
execute: 'execute', sleep: 'sleep', search: 'search', skill: 'skill'
|
|
38
|
+
};
|
|
39
|
+
return `tool-color-${map[n] || 'default'}`;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
containsHtmlTags(text) {
|
|
43
|
+
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;
|
|
44
|
+
return htmlPattern.test(text);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
sanitizeHtml(html) {
|
|
48
|
+
const dangerous = /<\s*\/?\s*(script|iframe|object|embed|applet|form|input|button|select|textarea)\b[^>]*>/gi;
|
|
49
|
+
let cleaned = html.replace(dangerous, '');
|
|
50
|
+
cleaned = cleaned.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '');
|
|
51
|
+
cleaned = cleaned.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '');
|
|
52
|
+
cleaned = cleaned.replace(/javascript\s*:/gi, '');
|
|
53
|
+
return cleaned;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
parseAndRenderMarkdown(text) {
|
|
57
|
+
const esc = this.escapeHtml.bind(this);
|
|
58
|
+
const lines = text.split('\n');
|
|
59
|
+
const out = [];
|
|
60
|
+
let i = 0;
|
|
61
|
+
while (i < lines.length) {
|
|
62
|
+
const line = lines[i];
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (trimmed.startsWith('### ')) {
|
|
65
|
+
out.push(`<h3 style="margin:0.4rem 0 0.2rem;font-size:0.95rem;font-weight:700">${this._mdInline(esc(trimmed.slice(4)))}</h3>`);
|
|
66
|
+
i++; continue;
|
|
67
|
+
}
|
|
68
|
+
if (trimmed.startsWith('## ')) {
|
|
69
|
+
out.push(`<h2 style="margin:0.5rem 0 0.25rem;font-size:1rem;font-weight:700">${this._mdInline(esc(trimmed.slice(3)))}</h2>`);
|
|
70
|
+
i++; continue;
|
|
71
|
+
}
|
|
72
|
+
if (trimmed.startsWith('# ')) {
|
|
73
|
+
out.push(`<h2 style="margin:0.5rem 0 0.25rem;font-size:1.05rem;font-weight:700">${this._mdInline(esc(trimmed.slice(2)))}</h2>`);
|
|
74
|
+
i++; continue;
|
|
75
|
+
}
|
|
76
|
+
if (trimmed.startsWith('> ')) {
|
|
77
|
+
out.push(`<blockquote style="margin:0.25rem 0;padding:0.2rem 0.75rem;border-left:3px solid currentColor;opacity:0.75">${this._mdInline(esc(trimmed.slice(2)))}</blockquote>`);
|
|
78
|
+
i++; continue;
|
|
79
|
+
}
|
|
80
|
+
if (trimmed === '---' || trimmed === '***' || trimmed === '___') {
|
|
81
|
+
out.push('<hr style="border:none;border-top:1px solid currentColor;opacity:0.2;margin:0.5rem 0">');
|
|
82
|
+
i++; continue;
|
|
83
|
+
}
|
|
84
|
+
if (/^[-*+] /.test(trimmed)) {
|
|
85
|
+
const items = [];
|
|
86
|
+
while (i < lines.length && /^[-*+] /.test(lines[i].trim())) {
|
|
87
|
+
items.push(`<li style="margin:0.1rem 0">${this._mdInline(esc(lines[i].trim().slice(2)))}</li>`);
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
out.push(`<ul style="margin:0.25rem 0;padding-left:1.25rem">${items.join('')}</ul>`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (/^\d+\. /.test(trimmed)) {
|
|
94
|
+
const items = [];
|
|
95
|
+
while (i < lines.length && /^\d+\. /.test(lines[i].trim())) {
|
|
96
|
+
items.push(`<li style="margin:0.1rem 0">${this._mdInline(esc(lines[i].trim().replace(/^\d+\. /, '')))}</li>`);
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
out.push(`<ol style="margin:0.25rem 0;padding-left:1.25rem">${items.join('')}</ol>`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (trimmed === '') {
|
|
103
|
+
out.push('<br>');
|
|
104
|
+
i++; continue;
|
|
105
|
+
}
|
|
106
|
+
out.push(this._mdInline(esc(line)));
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
return out.join('\n');
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
_mdInline(html) {
|
|
113
|
+
return html
|
|
114
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
115
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
116
|
+
.replace(/_([^_]+)_/g, '<em>$1</em>')
|
|
117
|
+
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
|
118
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="text-decoration:underline;opacity:0.85" target="_blank">$1</a>');
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
renderBlockCode(block, context) {
|
|
122
|
+
const div = document.createElement('div');
|
|
123
|
+
div.className = 'block-code';
|
|
124
|
+
div.classList.add(this._getBlockTypeClass('code'));
|
|
125
|
+
|
|
126
|
+
const code = block.code || '';
|
|
127
|
+
const language = (block.language || 'plaintext').toLowerCase();
|
|
128
|
+
const lineCount = code.split('\n').length;
|
|
129
|
+
|
|
130
|
+
const header = document.createElement('div');
|
|
131
|
+
header.className = 'code-block-header';
|
|
132
|
+
header.innerHTML = `
|
|
133
|
+
<span class="collapsible-code-label">${this.escapeHtml(language)} - ${lineCount} line${lineCount !== 1 ? 's' : ''}</span>
|
|
134
|
+
<button class="copy-code-btn" title="Copy code">
|
|
135
|
+
<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>
|
|
136
|
+
</button>
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
const copyBtn = header.querySelector('.copy-code-btn');
|
|
140
|
+
copyBtn.addEventListener('click', (e) => {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
144
|
+
const orig = copyBtn.innerHTML;
|
|
145
|
+
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>';
|
|
146
|
+
setTimeout(() => { copyBtn.innerHTML = orig; }, 2000);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const preStyle = "background:var(--color-bg-code);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:var(--color-code-text);border:1px solid var(--color-code-border);border-top:none;margin:0";
|
|
151
|
+
const codeContainer = document.createElement('div');
|
|
152
|
+
codeContainer.innerHTML = `<pre style="${preStyle}"><code class="lazy-hl">${this.escapeHtml(code)}</code></pre>`;
|
|
153
|
+
|
|
154
|
+
div.appendChild(header);
|
|
155
|
+
div.appendChild(codeContainer);
|
|
156
|
+
|
|
157
|
+
return div;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
renderBlockThinking(block, context) {
|
|
161
|
+
const thinking = block.thinking || '';
|
|
162
|
+
const hasSignature = !!block.signature;
|
|
163
|
+
const div = document.createElement('div');
|
|
164
|
+
div.className = 'block-thinking';
|
|
165
|
+
div.classList.add(this._getBlockTypeClass('thinking'));
|
|
166
|
+
|
|
167
|
+
if (!thinking) {
|
|
168
|
+
div.style.cssText = 'display:flex;align-items:center;gap:0.375rem;padding:0.25rem 0;font-size:0.7rem;color:var(--color-text-secondary);opacity:0.6';
|
|
169
|
+
div.innerHTML = '<svg viewBox="0 0 20 20" fill="currentColor" style="width:0.875rem;height:0.875rem"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/></svg>'
|
|
170
|
+
+ '<span>Thinking</span>'
|
|
171
|
+
+ (hasSignature ? '<svg viewBox="0 0 20 20" fill="currentColor" style="width:0.75rem;height:0.75rem;color:#3b82f6"><path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 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>' : '');
|
|
172
|
+
return div;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
div.innerHTML = '<details>'
|
|
176
|
+
+ '<summary>'
|
|
177
|
+
+ '<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>'
|
|
178
|
+
+ '<span>Thinking Process</span>'
|
|
179
|
+
+ (hasSignature ? '<span style="margin-left:0.5rem;font-size:0.65rem;opacity:0.5;font-weight:400">verified</span>' : '')
|
|
180
|
+
+ '</summary>'
|
|
181
|
+
+ '<div class="thinking-content">' + this.parseAndRenderMarkdown(thinking) + '</div>'
|
|
182
|
+
+ '</details>';
|
|
183
|
+
return div;
|
|
184
|
+
}
|
|
185
|
+
});
|