diff --git a/interfaces/admin/static/chat.html b/interfaces/admin/static/chat.html
index 880176bd..a6c2d4be 100644
--- a/interfaces/admin/static/chat.html
+++ b/interfaces/admin/static/chat.html
@@ -574,7 +574,7 @@
diff --git a/interfaces/admin/static/chat.js b/interfaces/admin/static/chat.js
index 5bc24a47..aca8f1b9 100644
--- a/interfaces/admin/static/chat.js
+++ b/interfaces/admin/static/chat.js
@@ -57,6 +57,7 @@ const I18N = {
"chat.marker.reclaimed": "本轮回收 turn 标记 {n}",
"chat.historyTruncated": "仅显示最近 {shown} 条,共 {total} 条",
"reasoning.summary": "模型推理片段",
+ "reasoning.processNotes": "主通道过程说明",
"tool.streamTitle": "工具与进度(本轮)",
"auth.login": "登录",
"auth.logout": "退出登录",
@@ -235,6 +236,7 @@ const I18N = {
"chat.marker.reclaimed": "Turn markers reclaimed {n}",
"chat.historyTruncated": "Showing last {shown} of {total} messages",
"reasoning.summary": "Model reasoning",
+ "reasoning.processNotes": "Main-channel notes",
"tool.streamTitle": "Tools & progress (this turn)",
"auth.login": "Login",
"auth.logout": "Logout",
@@ -482,6 +484,25 @@ function _appendAssistantTextSegments(inner, rawText, collapsedItems) {
}
}
+function _normFoldDedupText(s) {
+ return String(s || "")
+ .replace(/\s+/g, " ")
+ .trim()
+ .toLowerCase();
+}
+
+/** True when ``processText`` is already covered by text already queued for the reasoning fold. */
+function _foldProcessTextRedundant(processText, collapsedItems) {
+ const p = _normFoldDedupText(processText);
+ if (!p) return true;
+ const hay = (Array.isArray(collapsedItems) ? collapsedItems : [])
+ .map((x) => _normFoldDedupText((x && x.text) || ""))
+ .join("\n\n");
+ if (!hay) return false;
+ if (hay.includes(p)) return true;
+ return false;
+}
+
async function _buildAggregatedAssistantBubble(tsIso, items) {
const inner = el("div", { class: "chat-msg chat-msg--assistant chat-msg--rich" });
const collapsedItems = [];
@@ -498,9 +519,19 @@ async function _buildAggregatedAssistantBubble(tsIso, items) {
continue;
}
if (kind === "assistant_text") {
- _appendAssistantTextSegments(inner, text, collapsedItems);
- const attsAssistant = await renderAttachmentsEl(it && it.attachments);
- if (attsAssistant) inner.appendChild(attsAssistant);
+ const aet = String((it && it.assistantEventType) || "assistant_text").toLowerCase();
+ if (aet === "tool_call" && adminChatShowToolOutput) {
+ const t0 = String(text || "").trim();
+ if (t0 && !_foldProcessTextRedundant(t0, collapsedItems)) {
+ collapsedItems.push({ title: t("reasoning.processNotes"), text: t0 });
+ }
+ const attsAssistant = await renderAttachmentsEl(it && it.attachments);
+ if (attsAssistant) inner.appendChild(attsAssistant);
+ } else {
+ _appendAssistantTextSegments(inner, text, collapsedItems);
+ const attsAssistant = await renderAttachmentsEl(it && it.attachments);
+ if (attsAssistant) inner.appendChild(attsAssistant);
+ }
} else if (kind === "reasoning") {
if (adminChatShowToolOutput) collapsedItems.push({ title: t("reasoning.summary"), text });
} else if (kind === "tool_call") {
@@ -605,11 +636,23 @@ function _buildRenderRows(msgs) {
if (String(content || "").trim()) {
agg._items.push({ kind: "reasoning", text: content });
}
+ } else if (eventType === "tool_call") {
+ const hasText = !!String(content || "").trim();
+ const attsParsed = parseAttachments(m && m.attachments);
+ if (hasText || attsParsed.length) {
+ const piece = { kind: "assistant_text", text: content, assistantEventType: "tool_call" };
+ if (attsParsed.length) piece.attachments = attsParsed;
+ agg._items.push(piece);
+ }
} else if (eventType === "assistant_text" || eventType === "assistant" || !eventType) {
const hasText = !!String(content || "").trim();
const attsParsed = parseAttachments(m && m.attachments);
if (hasText || attsParsed.length) {
- const piece = { kind: "assistant_text", text: content };
+ const piece = {
+ kind: "assistant_text",
+ text: content,
+ assistantEventType: eventType || "assistant_text",
+ };
if (attsParsed.length) piece.attachments = attsParsed;
agg._items.push(piece);
}