@symerian/symi 3.5.0 → 3.5.1
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/dist/build-info.json +3 -3
- package/dist/bundled/boot-md/handler.js +4 -4
- package/dist/bundled/session-memory/handler.js +4 -4
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/{chrome-C_I81hbq.js → chrome-B7-rO4i9.js} +4 -4
- package/dist/{chrome-BKUACyeO.js → chrome-DPjznJQ-.js} +4 -4
- package/dist/control-ui/css/revert-red-theme.md +141 -0
- package/dist/control-ui/css/style.css +5843 -0
- package/dist/control-ui/css/style.css.backup-2026-03-03-162525 +3546 -0
- package/dist/control-ui/css/style.css.backup-before-red-2026-03-03-162525 +3546 -0
- package/dist/control-ui/css/style.css.backup-before-red-theme-2026-03-03-162530 +3546 -0
- package/dist/control-ui/css/style.css.pre-2row +2165 -0
- package/dist/control-ui/css/style.css.pre-brand +1776 -0
- package/dist/control-ui/css/style.css.pre-history +1974 -0
- package/dist/control-ui/css/style.css.pre-nav +2264 -0
- package/dist/control-ui/css/style.css.pre-newsession +1898 -0
- package/dist/control-ui/css/style.css.pre-queue +2195 -0
- package/dist/control-ui/css/style.css.pre-red-prompt +2524 -0
- package/dist/control-ui/css/style.css.pre-stop +2239 -0
- package/dist/control-ui/css/style.css.pre-textarea +2184 -0
- package/dist/control-ui/css/style.css.pre-watchdog +1848 -0
- package/dist/control-ui/css/style.css.red-theme +2999 -0
- package/dist/control-ui/index.html +1049 -0
- package/dist/control-ui/js/app.js +1304 -0
- package/dist/control-ui/js/app.js.pre-2row +463 -0
- package/dist/control-ui/js/app.js.pre-heartbeat-filter +595 -0
- package/dist/control-ui/js/app.js.pre-newsession +408 -0
- package/dist/control-ui/js/app.js.pre-queue +476 -0
- package/dist/control-ui/js/app.js.pre-stop +564 -0
- package/dist/control-ui/js/app.js.pre-textarea +467 -0
- package/dist/control-ui/js/app.js.pre-watchdog +293 -0
- package/dist/control-ui/js/connections.js +438 -0
- package/dist/control-ui/js/gateway.js +233 -0
- package/dist/control-ui/js/gateway.js.pre-stop +110 -0
- package/dist/control-ui/js/history.js +732 -0
- package/dist/control-ui/js/logs.js +238 -0
- package/dist/control-ui/js/menu.js +232 -0
- package/dist/control-ui/js/menu.js.pre-nav +66 -0
- package/dist/control-ui/js/metrics.js +53 -0
- package/dist/control-ui/js/models.js +138 -0
- package/dist/control-ui/js/render.js +882 -0
- package/dist/control-ui/js/render.test.js +112 -0
- package/dist/control-ui/js/scheduling.js +461 -0
- package/dist/control-ui/js/settings.js +910 -0
- package/dist/control-ui/js/slash-autocomplete.js +168 -0
- package/dist/control-ui/js/subagents.js +560 -0
- package/dist/control-ui/js/utils.js +29 -0
- package/dist/control-ui/vendor/highlight.min.js +2518 -0
- package/dist/control-ui/vendor/marked.min.js +69 -0
- package/dist/{deliver-DyO3QD8O.js → deliver-DTRkeYm3.js} +4 -4
- package/dist/{deliver-Cjyb6h4g.js → deliver-oWGJwzFf.js} +4 -4
- package/dist/extensionAPI.js +4 -4
- package/dist/llm-slug-generator.js +4 -4
- package/dist/{manager-rvtFoeFT.js → manager-CFenq_aO.js} +1 -1
- package/dist/{manager-PTSjHNVq.js → manager-CsxTf96V.js} +1 -1
- package/dist/{pi-embedded-BPuUM-gD.js → pi-embedded-Cdub5Vs9.js} +10 -10
- package/dist/{pw-ai-BFS9ezWe.js → pw-ai-BOOB8qoi.js} +1 -1
- package/dist/{pw-ai-Cx-Ko_FL.js → pw-ai-D2pEVS5n.js} +1 -1
- package/dist/{synthesis-7UL3pCpj.js → synthesis-Be9nYyDd.js} +4 -4
- package/dist/{synthesis-fD8J2vag.js → synthesis-CBIT6Vnk.js} +4 -4
- package/dist/{unified-runner-BIiKFnNF.js → unified-runner-BVvvnjXW.js} +10 -10
- package/package.json +1 -1
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
// ── Symi UI — Message Renderer ────────────────────────────────────────
|
|
2
|
+
// Handles all content types: text (markdown), thinking, tool_use,
|
|
3
|
+
// tool_result, image. Mirrors the Standard UI feature set.
|
|
4
|
+
|
|
5
|
+
// ── Configure marked ──────────────────────────────────────────────────
|
|
6
|
+
if (typeof marked !== "undefined") {
|
|
7
|
+
marked.setOptions({
|
|
8
|
+
breaks: true,
|
|
9
|
+
gfm: true,
|
|
10
|
+
pedantic: false,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// GFM task list extension (- [ ] and - [x])
|
|
14
|
+
const _taskListExt = {
|
|
15
|
+
name: "taskList",
|
|
16
|
+
level: "inline",
|
|
17
|
+
start(src) {
|
|
18
|
+
return src.indexOf("[ ]") !== -1 || src.indexOf("[x]") !== -1 ? 0 : -1;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
marked.use({
|
|
22
|
+
hooks: {
|
|
23
|
+
postprocess(html) {
|
|
24
|
+
return html
|
|
25
|
+
.replace(
|
|
26
|
+
/<li>\s*\[ \]/g,
|
|
27
|
+
'<li class="task-item"><input type="checkbox" disabled class="task-cb"> ',
|
|
28
|
+
)
|
|
29
|
+
.replace(
|
|
30
|
+
/<li>\s*\[x\]/gi,
|
|
31
|
+
'<li class="task-item task-done"><input type="checkbox" disabled checked class="task-cb"> ',
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Custom renderer — open links in new tab, add copy buttons to code blocks
|
|
38
|
+
const renderer = new marked.Renderer();
|
|
39
|
+
|
|
40
|
+
// marked v15+: renderer methods receive a token object
|
|
41
|
+
renderer.link = (token) => {
|
|
42
|
+
const href = typeof token === "string" ? token : (token.href ?? "");
|
|
43
|
+
const title = typeof token === "object" ? (token.title ?? "") : "";
|
|
44
|
+
const text = typeof token === "object" ? (token.text ?? "") : "";
|
|
45
|
+
const t = title ? ` title="${escAttr(title)}"` : "";
|
|
46
|
+
return `<a href="${escAttr(href)}"${t} target="_blank" rel="noopener">${text}</a>`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
renderer.code = (token) => {
|
|
50
|
+
const raw = typeof token === "string" ? token : (token.text ?? "");
|
|
51
|
+
const meta = typeof token === "object" ? (token.lang ?? "") : "";
|
|
52
|
+
|
|
53
|
+
// Split "python filename.py" -> lang + optional filename
|
|
54
|
+
const parts = meta.trim().split(/\s+/);
|
|
55
|
+
const lang = parts[0] || "";
|
|
56
|
+
const filename = parts.slice(1).join(" ");
|
|
57
|
+
const language = lang && hljs && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
58
|
+
|
|
59
|
+
let highlighted = escHtml(raw);
|
|
60
|
+
try {
|
|
61
|
+
if (typeof hljs !== "undefined") {
|
|
62
|
+
highlighted = hljs.highlight(raw, { language }).value;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
/* ignore */
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build line-numbered HTML
|
|
69
|
+
const lines = highlighted.split("\n");
|
|
70
|
+
if (lines[lines.length - 1] === "") {
|
|
71
|
+
lines.pop();
|
|
72
|
+
}
|
|
73
|
+
const numbered = lines
|
|
74
|
+
.map((line, i) => {
|
|
75
|
+
return (
|
|
76
|
+
'<span class="code-line"><span class="line-num">' +
|
|
77
|
+
(i + 1) +
|
|
78
|
+
'</span><span class="line-content">' +
|
|
79
|
+
(line || "\u200b") +
|
|
80
|
+
"</span></span>"
|
|
81
|
+
);
|
|
82
|
+
})
|
|
83
|
+
.join("\n");
|
|
84
|
+
|
|
85
|
+
const langIcon = codeIcon(lang);
|
|
86
|
+
const langColor = codeLangColor(lang);
|
|
87
|
+
const id = "code-" + Math.random().toString(36).slice(2);
|
|
88
|
+
const collapsible = lines.length > 30;
|
|
89
|
+
const linesLabel = lines.length + " line" + (lines.length !== 1 ? "s" : "");
|
|
90
|
+
|
|
91
|
+
let html =
|
|
92
|
+
'<div class="code-block code-block-full' +
|
|
93
|
+
(collapsible ? " code-collapsible" : "") +
|
|
94
|
+
'" id="' +
|
|
95
|
+
id +
|
|
96
|
+
'">';
|
|
97
|
+
html += '<div class="code-header">';
|
|
98
|
+
html += '<div class="code-header-left">';
|
|
99
|
+
html +=
|
|
100
|
+
'<span class="code-lang-badge" style="--lang-color:' +
|
|
101
|
+
langColor +
|
|
102
|
+
'">' +
|
|
103
|
+
langIcon +
|
|
104
|
+
escHtml(lang || "code") +
|
|
105
|
+
"</span>";
|
|
106
|
+
if (filename) {
|
|
107
|
+
html += '<span class="code-filename">' + escHtml(filename) + "</span>";
|
|
108
|
+
}
|
|
109
|
+
html += '<span class="code-lines-count">' + linesLabel + "</span>";
|
|
110
|
+
html += "</div>";
|
|
111
|
+
html += '<div class="code-header-right">';
|
|
112
|
+
if (collapsible) {
|
|
113
|
+
html +=
|
|
114
|
+
'<button class="code-expand-btn" onclick="toggleCodeExpand(\'' +
|
|
115
|
+
id +
|
|
116
|
+
'\')" title="Expand">⤢</button>';
|
|
117
|
+
}
|
|
118
|
+
html += '<button class="code-copy" onclick="copyCode(this)" title="Copy code">';
|
|
119
|
+
html +=
|
|
120
|
+
'<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/></svg> Copy';
|
|
121
|
+
html += "</button></div></div>";
|
|
122
|
+
html += '<div class="code-body' + (collapsible ? " code-body-collapsed" : "") + '">';
|
|
123
|
+
html +=
|
|
124
|
+
'<pre class="code-pre"><code class="hljs language-' +
|
|
125
|
+
escHtml(language) +
|
|
126
|
+
' code-numbered">' +
|
|
127
|
+
numbered +
|
|
128
|
+
"</code></pre>";
|
|
129
|
+
html += "</div>";
|
|
130
|
+
if (collapsible) {
|
|
131
|
+
html +=
|
|
132
|
+
'<button class="code-show-more" onclick="toggleCodeExpand(\'' +
|
|
133
|
+
id +
|
|
134
|
+
"')\">Show all " +
|
|
135
|
+
lines.length +
|
|
136
|
+
" lines ▾</button>";
|
|
137
|
+
}
|
|
138
|
+
html += "</div>";
|
|
139
|
+
return html;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
renderer.codespan = (token) => {
|
|
143
|
+
const text = typeof token === "string" ? token : (token.text ?? "");
|
|
144
|
+
return `<code class="inline-code">${text}</code>`;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
marked.use({ renderer });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
151
|
+
function escHtml(t) {
|
|
152
|
+
return String(t ?? "")
|
|
153
|
+
.replace(/&/g, "&")
|
|
154
|
+
.replace(/</g, "<")
|
|
155
|
+
.replace(/>/g, ">")
|
|
156
|
+
.replace(/"/g, """)
|
|
157
|
+
.replace(/'/g, "'");
|
|
158
|
+
}
|
|
159
|
+
function escAttr(t) {
|
|
160
|
+
return escHtml(t);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Language icon + color ─────────────────────────────────────────────
|
|
164
|
+
function codeIcon(lang = "") {
|
|
165
|
+
const l = lang.toLowerCase();
|
|
166
|
+
if (["js", "javascript", "ts", "typescript", "jsx", "tsx"].includes(l)) {
|
|
167
|
+
return "⬡ ";
|
|
168
|
+
}
|
|
169
|
+
if (["py", "python"].includes(l)) {
|
|
170
|
+
return "🐍 ";
|
|
171
|
+
}
|
|
172
|
+
if (["bash", "sh", "zsh", "shell"].includes(l)) {
|
|
173
|
+
return "$ ";
|
|
174
|
+
}
|
|
175
|
+
if (["json"].includes(l)) {
|
|
176
|
+
return "{ ";
|
|
177
|
+
}
|
|
178
|
+
if (["css", "scss", "sass"].includes(l)) {
|
|
179
|
+
return "🎨 ";
|
|
180
|
+
}
|
|
181
|
+
if (["html", "xml", "svg"].includes(l)) {
|
|
182
|
+
return "⟨⟩ ";
|
|
183
|
+
}
|
|
184
|
+
if (["sql"].includes(l)) {
|
|
185
|
+
return "⛁ ";
|
|
186
|
+
}
|
|
187
|
+
if (["go"].includes(l)) {
|
|
188
|
+
return "◉ ";
|
|
189
|
+
}
|
|
190
|
+
if (["rust"].includes(l)) {
|
|
191
|
+
return "⚙ ";
|
|
192
|
+
}
|
|
193
|
+
if (["java", "kotlin"].includes(l)) {
|
|
194
|
+
return "☕ ";
|
|
195
|
+
}
|
|
196
|
+
if (["c", "cpp", "c++"].includes(l)) {
|
|
197
|
+
return "⚡ ";
|
|
198
|
+
}
|
|
199
|
+
if (["yaml", "yml", "toml"].includes(l)) {
|
|
200
|
+
return "⚙ ";
|
|
201
|
+
}
|
|
202
|
+
if (["dockerfile", "docker"].includes(l)) {
|
|
203
|
+
return "🐳 ";
|
|
204
|
+
}
|
|
205
|
+
return "◇ ";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function codeLangColor(lang = "") {
|
|
209
|
+
const l = lang.toLowerCase();
|
|
210
|
+
if (["js", "javascript", "jsx"].includes(l)) {
|
|
211
|
+
return "#f7df1e";
|
|
212
|
+
}
|
|
213
|
+
if (["ts", "typescript", "tsx"].includes(l)) {
|
|
214
|
+
return "#3178c6";
|
|
215
|
+
}
|
|
216
|
+
if (["py", "python"].includes(l)) {
|
|
217
|
+
return "#3572a5";
|
|
218
|
+
}
|
|
219
|
+
if (["bash", "sh", "zsh", "shell"].includes(l)) {
|
|
220
|
+
return "#89e051";
|
|
221
|
+
}
|
|
222
|
+
if (["json"].includes(l)) {
|
|
223
|
+
return "#cbcb41";
|
|
224
|
+
}
|
|
225
|
+
if (["css", "scss"].includes(l)) {
|
|
226
|
+
return "#563d7c";
|
|
227
|
+
}
|
|
228
|
+
if (["html", "xml"].includes(l)) {
|
|
229
|
+
return "#e34c26";
|
|
230
|
+
}
|
|
231
|
+
if (["sql"].includes(l)) {
|
|
232
|
+
return "#e38c00";
|
|
233
|
+
}
|
|
234
|
+
if (["go"].includes(l)) {
|
|
235
|
+
return "#00add8";
|
|
236
|
+
}
|
|
237
|
+
if (["rust"].includes(l)) {
|
|
238
|
+
return "#dea584";
|
|
239
|
+
}
|
|
240
|
+
if (["java"].includes(l)) {
|
|
241
|
+
return "#b07219";
|
|
242
|
+
}
|
|
243
|
+
if (["c", "cpp"].includes(l)) {
|
|
244
|
+
return "#555555";
|
|
245
|
+
}
|
|
246
|
+
if (["yaml", "yml"].includes(l)) {
|
|
247
|
+
return "#cb171e";
|
|
248
|
+
}
|
|
249
|
+
if (["dockerfile", "docker"].includes(l)) {
|
|
250
|
+
return "#0db7ed";
|
|
251
|
+
}
|
|
252
|
+
return "#aaaaaa";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Toggle code block expand ──────────────────────────────────────────
|
|
256
|
+
window.toggleCodeExpand = (id) => {
|
|
257
|
+
const block = document.getElementById(id);
|
|
258
|
+
if (!block) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const body = block.querySelector(".code-body");
|
|
262
|
+
const showBtn = block.querySelector(".code-show-more");
|
|
263
|
+
const expBtn = block.querySelector(".code-expand-btn");
|
|
264
|
+
const isOpen = block.classList.toggle("code-expanded");
|
|
265
|
+
if (body) {
|
|
266
|
+
body.classList.toggle("code-body-collapsed", !isOpen);
|
|
267
|
+
}
|
|
268
|
+
if (showBtn) {
|
|
269
|
+
showBtn.textContent = isOpen
|
|
270
|
+
? "Collapse ▴"
|
|
271
|
+
: `Show all ${body?.querySelectorAll(".code-line").length || "?"} lines ▾`;
|
|
272
|
+
}
|
|
273
|
+
if (expBtn) {
|
|
274
|
+
expBtn.textContent = isOpen ? "⤡" : "⤢";
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
window.copyCode = async (btn) => {
|
|
279
|
+
const code = btn.closest(".code-block")?.querySelector("code")?.innerText ?? "";
|
|
280
|
+
try {
|
|
281
|
+
await navigator.clipboard.writeText(code);
|
|
282
|
+
btn.classList.add("copied");
|
|
283
|
+
btn.textContent = "Copied!";
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
btn.classList.remove("copied");
|
|
286
|
+
btn.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/></svg> Copy`;
|
|
287
|
+
}, 2000);
|
|
288
|
+
} catch {}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// ── Model token stripping (profile-driven) ───────────────────────────
|
|
292
|
+
// Strips whatever patterns the active profile declares in
|
|
293
|
+
// profile.filters.stripPatterns. There are no hardcoded Gemma patterns
|
|
294
|
+
// here — relying on the profile means Qwen/Nemotron/Claude (which declare
|
|
295
|
+
// empty stripPatterns) pass through untouched, and the Ollama/Gemma
|
|
296
|
+
// profiles apply their own patterns. Server-side stripping in
|
|
297
|
+
// output-normalizer.ts remains the primary defense; this is best-effort
|
|
298
|
+
// for history messages that predate the active profile.
|
|
299
|
+
function stripModelTokensClient(text) {
|
|
300
|
+
const profile = window.activeModelProfile;
|
|
301
|
+
const patterns = profile?.filters?.stripPatterns;
|
|
302
|
+
if (!patterns?.length) {
|
|
303
|
+
return text;
|
|
304
|
+
}
|
|
305
|
+
let cleaned = text;
|
|
306
|
+
for (const pat of patterns) {
|
|
307
|
+
try {
|
|
308
|
+
cleaned = cleaned.replace(new RegExp(pat, "gi"), "");
|
|
309
|
+
} catch {
|
|
310
|
+
/* skip invalid patterns */
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return cleaned;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// NOTE: Monologue suppression has moved to the OutputNormalizer (server-side).
|
|
317
|
+
// The model stream is cleaned before events reach the Glass UI, so client-side
|
|
318
|
+
// monologue detection is no longer needed.
|
|
319
|
+
|
|
320
|
+
// ── Markdown renderer ─────────────────────────────────────────────────
|
|
321
|
+
function renderMarkdown(text) {
|
|
322
|
+
if (!text) {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return typeof marked !== "undefined"
|
|
327
|
+
? marked.parse(text)
|
|
328
|
+
: escHtml(text).replace(/\n/g, "<br>");
|
|
329
|
+
} catch {
|
|
330
|
+
return escHtml(text).replace(/\n/g, "<br>");
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Extract plain text from content blocks ────────────────────────────
|
|
335
|
+
window.extractText = function (content) {
|
|
336
|
+
if (!content) {
|
|
337
|
+
return "";
|
|
338
|
+
}
|
|
339
|
+
if (typeof content === "string") {
|
|
340
|
+
return content;
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(content)) {
|
|
343
|
+
return content
|
|
344
|
+
.filter((b) => b?.type === "text")
|
|
345
|
+
.map((b) => b.text ?? "")
|
|
346
|
+
.join("");
|
|
347
|
+
}
|
|
348
|
+
if (content?.content) {
|
|
349
|
+
return window.extractText(content.content);
|
|
350
|
+
}
|
|
351
|
+
return "";
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// ── Tool name formatter ───────────────────────────────────────────────
|
|
355
|
+
function formatToolName(name) {
|
|
356
|
+
if (!name) {
|
|
357
|
+
return "Tool";
|
|
358
|
+
}
|
|
359
|
+
return name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function toolIcon(name = "") {
|
|
363
|
+
if (name.includes("read") || name.includes("file")) {
|
|
364
|
+
return "📄";
|
|
365
|
+
}
|
|
366
|
+
if (name.includes("write") || name.includes("edit")) {
|
|
367
|
+
return "✏️";
|
|
368
|
+
}
|
|
369
|
+
if (name.includes("exec") || name.includes("run")) {
|
|
370
|
+
return "⚡";
|
|
371
|
+
}
|
|
372
|
+
if (name.includes("search") || name.includes("web")) {
|
|
373
|
+
return "🔍";
|
|
374
|
+
}
|
|
375
|
+
if (name.includes("browser")) {
|
|
376
|
+
return "🌐";
|
|
377
|
+
}
|
|
378
|
+
if (name.includes("memory")) {
|
|
379
|
+
return "🧠";
|
|
380
|
+
}
|
|
381
|
+
if (name.includes("spawn") || name.includes("agent")) {
|
|
382
|
+
return "🤖";
|
|
383
|
+
}
|
|
384
|
+
return "🔧";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Render a single content block ─────────────────────────────────────
|
|
388
|
+
function renderBlock(block) {
|
|
389
|
+
const el = document.createElement("div");
|
|
390
|
+
|
|
391
|
+
switch (block.type) {
|
|
392
|
+
// ── Plain text with markdown ──────────────────────────────────────
|
|
393
|
+
case "text": {
|
|
394
|
+
el.className = "msg-text";
|
|
395
|
+
const rawText = block.text ?? "";
|
|
396
|
+
const stripped = stripModelTokensClient(rawText);
|
|
397
|
+
// If stripping ate everything but we started with real text, render
|
|
398
|
+
// the raw text rather than silently dropping the message. Prevents
|
|
399
|
+
// the "response appeared then disappeared" regression.
|
|
400
|
+
const safeText = stripped.trim() ? stripped : rawText;
|
|
401
|
+
if (!safeText) {
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
el.innerHTML = renderMarkdown(safeText);
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Thinking block (collapsible) ──────────────────────────────────
|
|
409
|
+
case "thinking": {
|
|
410
|
+
el.className = "msg-thinking";
|
|
411
|
+
const id = "think-" + Math.random().toString(36).slice(2);
|
|
412
|
+
el.innerHTML = `
|
|
413
|
+
<button class="thinking-toggle" onclick="toggleThinking('${id}')">
|
|
414
|
+
<span class="thinking-icon">◈</span>
|
|
415
|
+
<span class="thinking-label">Reasoning</span>
|
|
416
|
+
<span class="thinking-chevron">▾</span>
|
|
417
|
+
</button>
|
|
418
|
+
<div class="thinking-body" id="${id}">
|
|
419
|
+
<div class="thinking-text">${escHtml(block.thinking ?? "")}</div>
|
|
420
|
+
</div>
|
|
421
|
+
`;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Tool use (what was called) ────────────────────────────────────
|
|
426
|
+
case "tool_use": {
|
|
427
|
+
el.className = "msg-tool-card";
|
|
428
|
+
const name = block.name ?? "tool";
|
|
429
|
+
const input = block.input ? JSON.stringify(block.input, null, 2) : "";
|
|
430
|
+
const id = "tool-" + Math.random().toString(36).slice(2);
|
|
431
|
+
el.innerHTML = `
|
|
432
|
+
<button class="tool-header" onclick="toggleTool('${id}')">
|
|
433
|
+
<span class="tool-icon">${toolIcon(name)}</span>
|
|
434
|
+
<span class="tool-name">${escHtml(formatToolName(name))}</span>
|
|
435
|
+
<span class="tool-chevron">▾</span>
|
|
436
|
+
</button>
|
|
437
|
+
<div class="tool-body" id="${id}">
|
|
438
|
+
<pre class="tool-input">${escHtml(input)}</pre>
|
|
439
|
+
</div>
|
|
440
|
+
`;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Tool result ───────────────────────────────────────────────────
|
|
445
|
+
case "tool_result": {
|
|
446
|
+
el.className = "msg-tool-result";
|
|
447
|
+
const content = Array.isArray(block.content)
|
|
448
|
+
? block.content.map((b) => b.text ?? "").join("\n")
|
|
449
|
+
: String(block.content ?? "");
|
|
450
|
+
const _preview = content.slice(0, 120);
|
|
451
|
+
const more = content.length > 120;
|
|
452
|
+
const id = "result-" + Math.random().toString(36).slice(2);
|
|
453
|
+
el.innerHTML = `
|
|
454
|
+
<div class="result-header">
|
|
455
|
+
<span class="result-icon">↩</span>
|
|
456
|
+
<span class="result-label">Result</span>
|
|
457
|
+
${more ? `<button class="result-expand" onclick="toggleTool('${id}')">Expand</button>` : ""}
|
|
458
|
+
</div>
|
|
459
|
+
<div class="result-preview" id="${id}">
|
|
460
|
+
<pre class="result-text">${escHtml(content)}</pre>
|
|
461
|
+
</div>
|
|
462
|
+
`;
|
|
463
|
+
if (!more) {
|
|
464
|
+
el.querySelector(".result-preview")?.classList.add("result-open");
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── Image ─────────────────────────────────────────────────────────
|
|
470
|
+
case "image": {
|
|
471
|
+
el.className = "msg-image";
|
|
472
|
+
const src =
|
|
473
|
+
block.source?.url ??
|
|
474
|
+
(block.source?.type === "base64"
|
|
475
|
+
? `data:${block.source.media_type};base64,${block.source.data}`
|
|
476
|
+
: "");
|
|
477
|
+
if (src) {
|
|
478
|
+
el.innerHTML = `<img src="${escAttr(src)}" alt="image" />`;
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
default:
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return el;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Toggle helpers ────────────────────────────────────────────────────
|
|
491
|
+
window.toggleThinking = (id) => {
|
|
492
|
+
const body = document.getElementById(id);
|
|
493
|
+
const btn = body?.previousElementSibling;
|
|
494
|
+
if (!body) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const open = body.classList.toggle("thinking-open");
|
|
498
|
+
if (btn) {
|
|
499
|
+
btn.querySelector(".thinking-chevron").textContent = open ? "▴" : "▾";
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
window.toggleTool = (id) => {
|
|
504
|
+
const body = document.getElementById(id);
|
|
505
|
+
if (!body) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
body.classList.toggle("tool-open");
|
|
509
|
+
const btn = body?.previousElementSibling?.querySelector?.(".tool-chevron");
|
|
510
|
+
if (btn) {
|
|
511
|
+
btn.textContent = body.classList.contains("tool-open") ? "▴" : "▾";
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// ── Copy message text ─────────────────────────────────────────────────
|
|
516
|
+
window.copyMessage = async (btn) => {
|
|
517
|
+
const bubble = btn.closest(".stream-bubble, .bubble");
|
|
518
|
+
const text = bubble?.querySelector(".msg-text")?.innerText ?? bubble?.innerText ?? "";
|
|
519
|
+
try {
|
|
520
|
+
await navigator.clipboard.writeText(text.trim());
|
|
521
|
+
btn.classList.add("copied");
|
|
522
|
+
setTimeout(() => btn.classList.remove("copied"), 2000);
|
|
523
|
+
} catch {}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// ── Timestamp formatter ───────────────────────────────────────────────
|
|
527
|
+
function formatTime(ts) {
|
|
528
|
+
if (!ts) {
|
|
529
|
+
return "";
|
|
530
|
+
}
|
|
531
|
+
const d = new Date(typeof ts === "number" && ts < 1e12 ? ts * 1000 : ts);
|
|
532
|
+
return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Expose renderBlock for use in app.js streaming finalization
|
|
536
|
+
window.renderBlock = renderBlock;
|
|
537
|
+
|
|
538
|
+
// ── Reasoning Panel — append a block ─────────────────────────────────
|
|
539
|
+
window.appendToReasoningPanel = function (block) {
|
|
540
|
+
const feed = document.getElementById("reasoning-feed");
|
|
541
|
+
if (!feed) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Remove empty-state placeholder on first real entry
|
|
546
|
+
const empty = document.getElementById("reasoning-empty");
|
|
547
|
+
if (empty) {
|
|
548
|
+
empty.remove();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const entry = document.createElement("div");
|
|
552
|
+
entry.className = "reasoning-entry";
|
|
553
|
+
|
|
554
|
+
switch (block.type) {
|
|
555
|
+
// ── Narration text (agent step descriptions routed here) ────────
|
|
556
|
+
case "text": {
|
|
557
|
+
const text = (block.text ?? "").trim();
|
|
558
|
+
if (!text) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
entry.innerHTML = `
|
|
562
|
+
<div class="reasoning-entry-header">
|
|
563
|
+
<span class="reasoning-entry-badge badge-narration">✦ NOTE</span>
|
|
564
|
+
<span class="reasoning-entry-name">${escHtml(String(text).split("\n")[0].slice(0, 60))}${text.length > 60 ? "…" : ""}</span>
|
|
565
|
+
</div>
|
|
566
|
+
`;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ── Thinking block ──────────────────────────────────────────────
|
|
571
|
+
case "thinking": {
|
|
572
|
+
const text = (block.thinking ?? "").trim();
|
|
573
|
+
if (!text) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
entry.innerHTML = `
|
|
577
|
+
<div class="reasoning-entry-header">
|
|
578
|
+
<span class="reasoning-entry-badge badge-think">◈ THINK</span>
|
|
579
|
+
<span class="reasoning-entry-name">${escHtml(String(text).split("\n")[0].slice(0, 60))}${text.length > 60 ? "…" : ""}</span>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="reasoning-think-body">${escHtml(text)}</div>
|
|
582
|
+
`;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ── Tool call (Anthropic: tool_use, Ollama/OpenAI: toolCall) ────
|
|
587
|
+
case "tool_use":
|
|
588
|
+
case "toolCall": {
|
|
589
|
+
const name = block.name ?? "tool";
|
|
590
|
+
const inputData = block.input ?? block.arguments ?? {};
|
|
591
|
+
const inputJson = inputData ? JSON.stringify(inputData, null, 2) : "{}";
|
|
592
|
+
// One-line preview: first key=value pair
|
|
593
|
+
let preview = "";
|
|
594
|
+
try {
|
|
595
|
+
const obj = typeof inputData === "string" ? JSON.parse(inputData) : inputData;
|
|
596
|
+
const keys = Object.keys(obj ?? {});
|
|
597
|
+
if (keys.length) {
|
|
598
|
+
const val = String(obj[keys[0]]).slice(0, 50);
|
|
599
|
+
preview = `${keys[0]}: ${val}${val.length >= 50 ? "…" : ""}`;
|
|
600
|
+
}
|
|
601
|
+
} catch {
|
|
602
|
+
/* ignore */
|
|
603
|
+
}
|
|
604
|
+
entry.innerHTML = `
|
|
605
|
+
<div class="reasoning-entry-header">
|
|
606
|
+
<span class="reasoning-entry-badge badge-tool">${escHtml(toolIcon(name))} TOOL</span>
|
|
607
|
+
<span class="reasoning-entry-name">${escHtml(formatToolName(name))}</span>
|
|
608
|
+
</div>
|
|
609
|
+
${preview ? `<div class="reasoning-tool-preview">${escHtml(preview)}</div>` : ""}
|
|
610
|
+
<div class="reasoning-tool-detail">${escHtml(inputJson)}</div>
|
|
611
|
+
`;
|
|
612
|
+
// Toggle expand on click
|
|
613
|
+
entry.style.cursor = "pointer";
|
|
614
|
+
entry.addEventListener("click", () => entry.classList.toggle("expanded"));
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ── Tool result ─────────────────────────────────────────────────
|
|
619
|
+
case "tool_result": {
|
|
620
|
+
const content = Array.isArray(block.content)
|
|
621
|
+
? block.content.map((b) => b.text ?? "").join("\n")
|
|
622
|
+
: String(block.content ?? "");
|
|
623
|
+
const chars = content.length;
|
|
624
|
+
const isError = block.is_error === true;
|
|
625
|
+
entry.innerHTML = `
|
|
626
|
+
<div class="reasoning-entry-header">
|
|
627
|
+
<span class="reasoning-entry-badge badge-result">${isError ? "⚠ ERROR" : "↩ RESULT"}</span>
|
|
628
|
+
<span class="reasoning-entry-name">${chars} chars</span>
|
|
629
|
+
</div>
|
|
630
|
+
<div class="reasoning-result-meta">${escHtml(content.slice(0, 80))}${chars > 80 ? "…" : ""}</div>
|
|
631
|
+
`;
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
default:
|
|
636
|
+
return; // unknown block — skip
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
feed.appendChild(entry);
|
|
640
|
+
feed.scrollTop = feed.scrollHeight;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// ── Reasoning Panel — run separator ──────────────────────────────────
|
|
644
|
+
window.injectReasoningRunSeparator = function () {
|
|
645
|
+
const feed = document.getElementById("reasoning-feed");
|
|
646
|
+
if (!feed) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const empty = document.getElementById("reasoning-empty");
|
|
650
|
+
if (empty) {
|
|
651
|
+
empty.remove();
|
|
652
|
+
}
|
|
653
|
+
const now = new Date();
|
|
654
|
+
const hhmm = now.toLocaleTimeString("en-US", {
|
|
655
|
+
hour: "2-digit",
|
|
656
|
+
minute: "2-digit",
|
|
657
|
+
hour12: false,
|
|
658
|
+
});
|
|
659
|
+
const sep = document.createElement("div");
|
|
660
|
+
sep.className = "reasoning-run-sep";
|
|
661
|
+
sep.innerHTML = `<span class="reasoning-run-sep-label">RUN · ${hhmm}</span>`;
|
|
662
|
+
feed.appendChild(sep);
|
|
663
|
+
feed.scrollTop = feed.scrollHeight;
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// ── Reasoning Panel — clear ───────────────────────────────────────────
|
|
667
|
+
window.clearReasoningPanel = function () {
|
|
668
|
+
const feed = document.getElementById("reasoning-feed");
|
|
669
|
+
if (!feed) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
feed.innerHTML = `
|
|
673
|
+
<div class="reasoning-empty" id="reasoning-empty">
|
|
674
|
+
<div class="reasoning-empty-icon">
|
|
675
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
676
|
+
<circle cx="12" cy="12" r="3"/>
|
|
677
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
|
678
|
+
</svg>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="reasoning-empty-text">No reasoning yet</div>
|
|
681
|
+
</div>`;
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// ── Reasoning Panel — live thinking update (streaming) ───────────────
|
|
685
|
+
// Called repeatedly during a run to accumulate thinking text in real-time.
|
|
686
|
+
// Creates a single "live" entry and updates it in-place until finalized.
|
|
687
|
+
let _liveThinkEntry = null;
|
|
688
|
+
let _liveThinkBody = null;
|
|
689
|
+
let _liveThinkBuffer = "";
|
|
690
|
+
|
|
691
|
+
window.openLiveThinking = function () {
|
|
692
|
+
const feed = document.getElementById("reasoning-feed");
|
|
693
|
+
if (!feed) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const empty = document.getElementById("reasoning-empty");
|
|
697
|
+
if (empty) {
|
|
698
|
+
empty.remove();
|
|
699
|
+
}
|
|
700
|
+
_liveThinkBuffer = "";
|
|
701
|
+
const entry = document.createElement("div");
|
|
702
|
+
entry.className = "reasoning-entry";
|
|
703
|
+
entry.id = "reasoning-live-think";
|
|
704
|
+
entry.innerHTML = `
|
|
705
|
+
<div class="reasoning-entry-header">
|
|
706
|
+
<span class="reasoning-entry-badge badge-think">◈ THINK</span>
|
|
707
|
+
<span class="reasoning-entry-name" id="reasoning-live-name">…</span>
|
|
708
|
+
</div>
|
|
709
|
+
<div class="reasoning-think-body" id="reasoning-live-body"></div>
|
|
710
|
+
`;
|
|
711
|
+
feed.appendChild(entry);
|
|
712
|
+
_liveThinkEntry = entry;
|
|
713
|
+
_liveThinkBody = entry.querySelector("#reasoning-live-body");
|
|
714
|
+
feed.scrollTop = feed.scrollHeight;
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
window.updateLiveThinking = function (text) {
|
|
718
|
+
if (!_liveThinkBody) {
|
|
719
|
+
window.openLiveThinking();
|
|
720
|
+
}
|
|
721
|
+
_liveThinkBuffer += text;
|
|
722
|
+
if (_liveThinkBody) {
|
|
723
|
+
_liveThinkBody.textContent = _liveThinkBuffer;
|
|
724
|
+
const feed = document.getElementById("reasoning-feed");
|
|
725
|
+
if (feed) {
|
|
726
|
+
feed.scrollTop = feed.scrollHeight;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Update name preview
|
|
730
|
+
const nameEl = _liveThinkEntry?.querySelector("#reasoning-live-name");
|
|
731
|
+
if (nameEl) {
|
|
732
|
+
const firstLine = _liveThinkBuffer.split("\n")[0].slice(0, 60);
|
|
733
|
+
nameEl.textContent = firstLine + (_liveThinkBuffer.length > 60 ? "…" : "");
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
window.closeLiveThinking = function () {
|
|
738
|
+
if (_liveThinkEntry) {
|
|
739
|
+
_liveThinkEntry.removeAttribute("id");
|
|
740
|
+
_liveThinkEntry = null;
|
|
741
|
+
_liveThinkBody = null;
|
|
742
|
+
_liveThinkBuffer = "";
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// ── Extract thinking text from a content array (for streaming deltas) ─
|
|
747
|
+
window.extractThinkingText = function (content) {
|
|
748
|
+
if (!content) {
|
|
749
|
+
return "";
|
|
750
|
+
}
|
|
751
|
+
if (typeof content === "string") {
|
|
752
|
+
return "";
|
|
753
|
+
}
|
|
754
|
+
if (Array.isArray(content)) {
|
|
755
|
+
return content
|
|
756
|
+
.filter((b) => b?.type === "thinking")
|
|
757
|
+
.map((b) => b.thinking ?? "")
|
|
758
|
+
.join("");
|
|
759
|
+
}
|
|
760
|
+
if (content?.content) {
|
|
761
|
+
return window.extractThinkingText(content.content);
|
|
762
|
+
}
|
|
763
|
+
return "";
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// ── Main render function — builds a full message element ──────────────
|
|
767
|
+
window.renderMessage = function (message) {
|
|
768
|
+
const { role, content, timestamp } = message;
|
|
769
|
+
const isUser = role === "user";
|
|
770
|
+
|
|
771
|
+
// ── Filter noisy/internal messages ────────────────────────────────
|
|
772
|
+
const _msgText = Array.isArray(content)
|
|
773
|
+
? content
|
|
774
|
+
.filter((b) => b.type === "text")
|
|
775
|
+
.map((b) => b.text ?? "")
|
|
776
|
+
.join("")
|
|
777
|
+
.trim()
|
|
778
|
+
: extractText(content).trim();
|
|
779
|
+
|
|
780
|
+
// NOTE: Monologue suppression moved server-side to OutputNormalizer.
|
|
781
|
+
|
|
782
|
+
// Plugin context messages (e.g. "[Outlook 365] Not connected...")
|
|
783
|
+
if (role === "user" && /^\[.+\]\s+(Not connected|Connected as)\b/i.test(_msgText)) {
|
|
784
|
+
if (typeof window.appendToReasoningPanel === "function") {
|
|
785
|
+
window.appendToReasoningPanel({ type: "text", text: "[Plugin context]" });
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Post-generation verification user messages
|
|
791
|
+
if (
|
|
792
|
+
role === "user" &&
|
|
793
|
+
/^Review your response against the original user request/i.test(_msgText)
|
|
794
|
+
) {
|
|
795
|
+
if (typeof window.appendToReasoningPanel === "function") {
|
|
796
|
+
window.appendToReasoningPanel({ type: "text", text: "[Post-generation verification]" });
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const wrapper = document.createElement("div");
|
|
802
|
+
wrapper.className = `message ${isUser ? "user-msg" : "symi-msg"}`;
|
|
803
|
+
|
|
804
|
+
const bubble = document.createElement("div");
|
|
805
|
+
bubble.className = isUser ? "bubble" : "bubble stream-bubble done";
|
|
806
|
+
|
|
807
|
+
// Add copy button for assistant messages
|
|
808
|
+
if (!isUser) {
|
|
809
|
+
const copyBtn = document.createElement("button");
|
|
810
|
+
copyBtn.className = "msg-copy-btn";
|
|
811
|
+
copyBtn.title = "Copy";
|
|
812
|
+
copyBtn.addEventListener("click", function () {
|
|
813
|
+
window.copyMessage(this);
|
|
814
|
+
});
|
|
815
|
+
copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none">
|
|
816
|
+
<rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" stroke-width="2"/>
|
|
817
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
|
|
818
|
+
</svg>`;
|
|
819
|
+
bubble.appendChild(copyBtn);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Render each content block — route reasoning/tool blocks to the side panel
|
|
823
|
+
const REASONING_TYPES = new Set(["thinking", "tool_use", "tool_result", "toolCall"]);
|
|
824
|
+
const isNarration = !isUser && message.stopReason === "toolUse";
|
|
825
|
+
const blocks = Array.isArray(content) ? content : [{ type: "text", text: extractText(content) }];
|
|
826
|
+
for (const block of blocks) {
|
|
827
|
+
if (!isUser && REASONING_TYPES.has(block.type)) {
|
|
828
|
+
// Route to Reasoning Panel — keep response bubble clean
|
|
829
|
+
window.appendToReasoningPanel(block);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
// Text blocks in tool-use messages are narration — route to Reasoning Panel
|
|
833
|
+
if (isNarration && block.type === "text") {
|
|
834
|
+
window.appendToReasoningPanel(block);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const el = renderBlock(block);
|
|
838
|
+
if (el.childNodes.length || el.innerHTML) {
|
|
839
|
+
bubble.appendChild(el);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// If the bubble has no visible content (only copy button), skip it entirely
|
|
844
|
+
const visibleChildren = bubble.querySelectorAll(
|
|
845
|
+
".msg-text, .msg-image, .msg-thinking, .msg-tool-card, .msg-tool-result",
|
|
846
|
+
);
|
|
847
|
+
if (!isUser && visibleChildren.length === 0) {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Timestamp
|
|
852
|
+
if (timestamp) {
|
|
853
|
+
const ts = document.createElement("div");
|
|
854
|
+
ts.className = "msg-timestamp";
|
|
855
|
+
ts.textContent = formatTime(timestamp);
|
|
856
|
+
bubble.appendChild(ts);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
wrapper.appendChild(bubble);
|
|
860
|
+
wrapper.appendChild(
|
|
861
|
+
Object.assign(document.createElement("div"), { className: "message-clearfix" }),
|
|
862
|
+
);
|
|
863
|
+
return wrapper;
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
// ── Re-render for streaming (plain text, no markdown until final) ──────
|
|
867
|
+
window.renderStreamText = function (el, text) {
|
|
868
|
+
if (!el) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
el.textContent = text;
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
// ── Apply highlight.js to any unhighlighted code blocks ──────────────
|
|
875
|
+
window.applyHighlighting = function (container) {
|
|
876
|
+
if (typeof hljs === "undefined") {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
container.querySelectorAll("pre code:not(.hljs)").forEach((block) => {
|
|
880
|
+
hljs.highlightElement(block);
|
|
881
|
+
});
|
|
882
|
+
};
|