From af6632769e32e85f099fb6a0ba766eb342ef3bfd Mon Sep 17 00:00:00 2001 From: wanger <122891289+10knamesmore@users.noreply.github.com> Date: Sun, 3 May 2026 14:41:12 +0800 Subject: [PATCH] perf: tool rendering in conversation page (#7937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(dashboard): route conversation history tool messages through ToolCallCard When viewing conversation history, large tool outputs (e.g. a single git log --stat producing tens of KB) caused the browser renderer to freeze. Root cause: formattedMessages mapped every role (including tool / system / _checkpoint) into user/bot bubbles, and bot plain strings went through markstream-vue's MarkdownRender. Single 88KB tool messages plus 88-of-them adding up to ~349KB of synchronous markdown parsing was enough to block the main thread for 5+ seconds. This patch: - Indexes tool-role messages by tool_call_id - Filters formattedMessages to user/assistant only — tool, system and _checkpoint roles no longer render as standalone bubbles - Converts assistant.tool_calls (OpenAI shape, with tc.name/tc.arguments fallbacks) into the existing tool_call MessagePart, attaching the paired result so MessageList's ToolCallCard renders it (default collapsed, no longer feeds large strings into the markdown renderer) - Drops empty placeholder plain parts when an assistant message only carries tool_calls - Sets ts/finished_ts to 0 as a sentinel: ToolCallCard.toolCallDuration returns "" when startTime <= 0, suppressing a misleading "0ms" duration that would otherwise appear because conversation history has no real timing data Behavior change: tool results are now embedded in their assistant's ToolCallCard.result instead of appearing as separate bot bubbles. This matches the main chat UI's behavior. Fixes #7929 Refs #7372 #7456 * style(dashboard): use single scrollbar in conversation history preview ToolCallCard's result/args panes have their own max-height + overflow, which produced a nested scrollbar when nested inside the history preview's already-scrollable .conversation-messages-container. Override those constraints inside the preview only — the outer 500px-bounded container already provides scroll bounds, so a single scrollbar feels cleaner. The main chat UI is unaffected. --------- Co-authored-by: wanger --- dashboard/src/views/ConversationPage.vue | 80 +++++++++++++++++------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/dashboard/src/views/ConversationPage.vue b/dashboard/src/views/ConversationPage.vue index f7dceca64..27e3f8491 100644 --- a/dashboard/src/views/ConversationPage.vue +++ b/dashboard/src/views/ConversationPage.vue @@ -539,28 +539,54 @@ export default { // 将对话历史转换为 MessageList 组件期望的格式 formattedMessages() { - return this.conversationHistory.map(msg => { - console.log('处理消息:', msg.role, msg.content); - - // 将消息内容转换为 MessagePart[] 格式 - const messageParts = this.convertContentToMessageParts(msg.content); - - if (msg.role === 'user') { - return { - content: { - type: 'user', - message: messageParts - } - }; - } else { - return { - content: { - type: 'bot', - message: messageParts - } - }; + // 按 tool_call_id 索引 tool 角色消息的执行结果 + const toolResultsById = {}; + for (const msg of this.conversationHistory) { + if (msg.role === 'tool' && msg.tool_call_id) { + toolResultsById[msg.tool_call_id] = msg.content; } - }); + } + + return this.conversationHistory + // tool / system 等非聊天角色不直接渲染为气泡,避免大文本走 markdown 路径卡死页面 + .filter(msg => msg.role === 'user' || msg.role === 'assistant') + .map(msg => { + console.log('处理消息:', msg.role, msg.content); + + const messageParts = this.convertContentToMessageParts(msg.content) + // 丢弃 convertContentToMessageParts 兜底插入的空 plain,避免 assistant 仅有工具调用时渲染空气泡 + .filter(part => part.type !== 'plain' || (part.text && part.text.trim())); + + // 把 OpenAI 风格的 assistant.tool_calls 转成 MessageList 已支持的 tool_call part + if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length) { + const toolCalls = msg.tool_calls.map(tc => { + const fn = tc.function || {}; + return { + id: tc.id, + name: fn.name || tc.name, + args: fn.arguments ?? tc.arguments, + result: toolResultsById[tc.id], + // 历史回放无真实耗时数据: + // ts: 0 → ToolCallCard.toolCallDuration 在 startTime<=0 时早退,跳过时长显示 + // finished_ts: 1 → MessageList.toolCallStatusText 视为已完成(避免误显示"运行中") + ts: 0, + finished_ts: 1, + }; + }); + messageParts.push({ type: 'tool_call', tool_calls: toolCalls }); + } + + const finalParts = messageParts.length + ? messageParts + : [{ type: 'plain', text: '' }]; + + return { + content: { + type: msg.role === 'user' ? 'user' : 'bot', + message: finalParts, + } + }; + }); } }, @@ -1173,6 +1199,18 @@ export default { background-color: #f9f9f9; } +/* 让 ToolCallCard 内部的 args/result 自然展开,由外层容器统一滚动,避免双滚动条 */ +.conversation-messages-container .detail-json, +.conversation-messages-container .detail-result { + max-height: none; + overflow: visible; +} + +/* 历史回放无真实状态数据,隐藏 IPython 工具的"已完成"标签,与其它工具卡片保持一致 */ +.conversation-messages-container .tool-call-inline-status { + display: none; +} + /* 暗色模式下的聊天消息容器 */ .v-theme--dark .conversation-messages-container { background-color: #1e1e1e;