fix(chat): keep tool-call process notes out of visible body

Preserve tool_call event_type when hydrating WS final messages, never render main-channel process notes as assistant body text, and prefer structured message content over stream buffer on finalize.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-31 12:41:12 +08:00
parent 4610989d72
commit 39fd1905b6

View file

@ -522,10 +522,13 @@ async function _buildAggregatedAssistantBubble(tsIso, items) {
} }
if (kind === "assistant_text") { if (kind === "assistant_text") {
const aet = String((it && it.assistantEventType) || "assistant_text").toLowerCase(); const aet = String((it && it.assistantEventType) || "assistant_text").toLowerCase();
if (aet === "tool_call" && adminChatShowToolOutput) { if (aet === "tool_call") {
const t0 = String(text || "").trim(); // Main-channel process notes belong in the reasoning fold only — never in visible body text.
if (t0 && !_foldProcessTextRedundant(t0, collapsedItems)) { if (adminChatShowToolOutput) {
collapsedItems.push({ title: t("reasoning.processNotes"), text: t0 }); 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); const attsAssistant = await renderAttachmentsEl(it && it.attachments);
if (attsAssistant) inner.appendChild(attsAssistant); if (attsAssistant) inner.appendChild(attsAssistant);
@ -4263,6 +4266,8 @@ async function renderChatUi() {
if (!message || typeof message !== "object") return []; if (!message || typeof message !== "object") return [];
const mesAttParsed = parseAttachments(message.attachments); const mesAttParsed = parseAttachments(message.attachments);
const mesAtt = mesAttParsed.length ? mesAttParsed : null; const mesAtt = mesAttParsed.length ? mesAttParsed : null;
const srcEventType = _normalizeEventType(message.event_type || message.eventType);
const fallbackEventType = srcEventType === "tool_call" ? "tool_call" : "assistant_text";
const base = { const base = {
role: "assistant", role: "assistant",
id: message.id, id: message.id,
@ -4301,25 +4306,32 @@ async function renderChatUi() {
const textFallback = decodeEscapedNewlines( const textFallback = decodeEscapedNewlines(
typeof message.content === "string" ? message.content : typeof message.text === "string" ? message.text : "", typeof message.content === "string" ? message.content : typeof message.text === "string" ? message.text : "",
).trim(); ).trim();
const hasAssistantText = out.some((row) => String(row.event_type || "") === "assistant_text"); const hasBodyText = out.some((row) => {
if (textFallback && !hasAssistantText) { const et = _normalizeEventType(row.event_type);
out.push({ ...base, content: textFallback, event_type: "assistant_text" }); return et === "assistant_text" || et === "tool_call";
});
if (textFallback && !hasBodyText) {
out.push({ ...base, content: textFallback, event_type: fallbackEventType });
} }
if (!out.length && (textFallback || (mesAtt && mesAtt.length))) { if (!out.length && (textFallback || (mesAtt && mesAtt.length))) {
return [ return [
{ {
...base, ...base,
content: textFallback, content: textFallback,
event_type: "assistant_text", event_type: fallbackEventType,
tool_calls: message.tool_calls != null ? message.tool_calls : message.toolCalls, tool_calls: message.tool_calls != null ? message.tool_calls : message.toolCalls,
...(mesAtt && mesAtt.length ? { attachments: mesAtt } : {}), ...(mesAtt && mesAtt.length ? { attachments: mesAtt } : {}),
}, },
]; ];
} }
if (out.length) { if (out.length) {
const _isBodyRow = (row) => {
const et = _normalizeEventType(row && row.event_type);
return et === "assistant_text" || et === "tool_call";
};
let attachIdx = out.length - 1; let attachIdx = out.length - 1;
for (let i = out.length - 1; i >= 0; i -= 1) { for (let i = out.length - 1; i >= 0; i -= 1) {
if (String(out[i].event_type || "") === "assistant_text") { if (_isBodyRow(out[i])) {
attachIdx = i; attachIdx = i;
break; break;
} }
@ -4328,10 +4340,10 @@ async function renderChatUi() {
...out[attachIdx], ...out[attachIdx],
tool_calls: message.tool_calls != null ? message.tool_calls : message.toolCalls, tool_calls: message.tool_calls != null ? message.tool_calls : message.toolCalls,
}; };
// Avoid duplicating blobs: attach WS/API root attachments to the latest assistant_text row only. // Avoid duplicating blobs: attach WS/API root attachments to the latest body row only.
if (mesAtt && mesAtt.length) { if (mesAtt && mesAtt.length) {
for (let i = out.length - 1; i >= 0; i -= 1) { for (let i = out.length - 1; i >= 0; i -= 1) {
if (String(out[i].event_type || "") === "assistant_text") { if (_isBodyRow(out[i])) {
out[i] = { ...out[i], attachments: mesAtt }; out[i] = { ...out[i], attachments: mesAtt };
break; break;
} }
@ -4934,7 +4946,7 @@ ${autoLimit ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
hasRealStreamText || hasRealStreamText ||
(Array.isArray(chatStreamSegments) && chatStreamSegments.length > 0) || (Array.isArray(chatStreamSegments) && chatStreamSegments.length > 0) ||
!!String(chatStream || "").trim(); !!String(chatStream || "").trim();
const fbLine = chatStream || extractWsAssistantText(payload.message || {}); const fbLine = extractWsAssistantText(payload.message || {}) || chatStream;
const fbTrim = String(fbLine || "").trim(); const fbTrim = String(fbLine || "").trim();
const fbOk = !!fbTrim && !_isSilentReplyStream(fbTrim); const fbOk = !!fbTrim && !_isSilentReplyStream(fbTrim);
const n = await loadMessagesForActive({ keepDomIfNoHistoryRows: hadStream }); const n = await loadMessagesForActive({ keepDomIfNoHistoryRows: hadStream });
@ -4959,7 +4971,10 @@ ${autoLimit ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
} }
scrollMessagesToBottom(true); scrollMessagesToBottom(true);
} else { } else {
ok = await appendFinalAssistant(payload.message, chatStream || extractWsAssistantText(payload.message || {})); ok = await appendFinalAssistant(
payload.message,
extractWsAssistantText(payload.message || {}) || chatStream,
);
} }
if (!ok) _markStreamTerminal("end", t("chat.status.end")); if (!ok) _markStreamTerminal("end", t("chat.status.end"));
chatStream = ""; chatStream = "";