perf: tool rendering in conversation page (#7937)

* 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 <wanger@example.com>
This commit is contained in:
wanger
2026-05-03 14:41:12 +08:00
committed by GitHub
parent 8098a92f33
commit af6632769e

View File

@@ -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;