消息渲染组件
MessageList、StreamingMessageContainer、Messages.ts 三个组件合起来负责把 Agent.state.messages 渲染成可见的对话。MessageList 是稳定列表(已完成的消息),StreamingMessageContainer 是流式容器(当前正在生成的消息),两者分工是为了避免流式期间整个列表重渲染。Messages.ts 定义 user-message、assistant-message、tool-message、aborted-message 等子元素,处理具体的内容块渲染。
职责
- 稳定列表:
MessageList.buildRenderItems遍历messages,跳过artifact角色,先尝试renderMessage自定义渲染,再回退到user-message/assistant-message,用repeatdirective 按 key 复用 DOM,见packages/web-ui/src/components/MessageList.ts:27-81。 - 流式容器:
StreamingMessageContainer.setMessage用requestAnimationFrame批量合并更新,流式期间不会每 token 都重渲染,见packages/web-ui/src/components/StreamingMessageContainer.ts:28-61。 - 深度克隆避免引用比较:
JSON.parse(JSON.stringify(this._pendingMessage))把待渲染的消息深拷贝,让 Lit 检测到嵌套属性(如toolCall.arguments)的变更,见packages/web-ui/src/components/StreamingMessageContainer.ts:50-54。 - assistant 消息分块渲染:
AssistantMessage.render按message.content数组顺序渲染text/thinking/toolCall三类块,见packages/web-ui/src/components/Messages.ts:104-167。 - 工具调用配对:
MessageList先把toolResult角色按toolCallId建 Map,AssistantMessage通过toolResultsByIdprop 查到结果,内联渲染<tool-message>,见packages/web-ui/src/components/Messages.ts:115-136。 - 附件消息转换:
defaultConvertToLlm把user-with-attachments转成标准 user message,图片变ImageContent、文档变TextContent,artifact 消息被过滤掉不发给 LLM,见packages/web-ui/src/components/Messages.ts:348-383。
设计动机
为什么分稳定列表和流式容器两个组件?因为流式期间 message.content 数组不断追加新 token,如果整个 MessageList 都跟着 requestUpdate,每次更新都要重新 buildRenderItems 遍历全部历史消息,聊天越长越卡。拆开后,已完成消息进 MessageList 不再变,只有当前正在生成的消息进 StreamingMessageContainer,后者用 requestAnimationFrame 批量合并 update,每帧最多渲染一次。
为什么用 JSON.parse(JSON.stringify(...)) 这种慢操作?因为 Lit 的属性变更检测是浅比较引用,而 Agent 在流式时是直接 mutate 同一个 AssistantMessage 对象的 content 数组(toolCall.arguments 也是原地改)。如果不克隆,Lit 看不到引用变化,UI 不更新。深拷贝虽慢但每帧只处理一条消息,代价可接受。
为什么 tool result 不直接渲染成独立消息?因为 LLM 返回的 assistant 消息里有 toolCall,紧接着的 toolResult 是配对的。如果分开渲染会断开上下文,AssistantMessage 用 toolResultsById Map 把 result 内联到 toolCall 旁边,视觉上是一组。MessageList 显式 skip standalone toolResult 角色就是这个原因,见 packages/web-ui/src/components/MessageList.ts:75-79。
关键文件
packages/web-ui/src/components/MessageList.ts:11-26—class MessageList声明与 props。packages/web-ui/src/components/MessageList.ts:27-81—buildRenderItems:跳过 artifact、调renderMessage、组装user-message/assistant-message。packages/web-ui/src/components/MessageList.ts:83-92—render:用repeatdirective 按 key 复用。packages/web-ui/src/components/StreamingMessageContainer.ts:6-26—class StreamingMessageContainer声明。packages/web-ui/src/components/StreamingMessageContainer.ts:28-61—setMessage:immediate 直发,非 immediate 走requestAnimationFrame批量合并。packages/web-ui/src/components/Messages.ts:42-82—UserMessage组件,渲染文本与附件 tile。packages/web-ui/src/components/Messages.ts:84-168—AssistantMessage组件,按 content 数组分块渲染。packages/web-ui/src/components/Messages.ts:226-277—ToolMessage组件,调renderTool决定用自定义还是默认卡片。packages/web-ui/src/components/Messages.ts:348-383—defaultConvertToLlm:把user-with-attachments与artifact过滤转换。
setMessage 的批量合并是流式渲染的性能关键:
// packages/web-ui/src/components/StreamingMessageContainer.ts:44-60
if (!this._updateScheduled) {
this._updateScheduled = true;
requestAnimationFrame(async () => {
if (!this._immediateUpdate && this._pendingMessage !== null) {
this._message = JSON.parse(JSON.stringify(this._pendingMessage));
this.requestUpdate();
}
this._pendingMessage = null;
this._updateScheduled = false;
this._immediateUpdate = false;
});
}AssistantMessage.render 按 content 数组顺序渲染,toolCall 通过 toolResultsById 找配对结果:
// packages/web-ui/src/components/Messages.ts:115-136
} else if (chunk.type === "toolCall") {
if (!this.hideToolCalls) {
const tool = this.tools?.find((t) => t.name === chunk.name);
const pending = this.pendingToolCalls?.has(chunk.id) ?? false;
const result = this.toolResultsById?.get(chunk.id);
if (this.hidePendingToolCalls && pending && !result) {
continue;
}
const aborted = this.message.stopReason === "aborted" && !result;
orderedParts.push(
html`<tool-message
.tool=${tool}
.toolCall=${chunk}
.result=${result}
.pending=${pending}
.aborted=${aborted}
.isStreaming=${this.isStreaming}
></tool-message>`,
);
}
}数据流
AgentInterface 订阅事件后把消息分流到两个组件:
边界与失败
- 流式期间 toolCall 配对不全:
toolResultsById可能没有对应 id 的 result,此时result为undefined,ToolMessage标记为 pending,见packages/web-ui/src/components/Messages.ts:118-122。 - aborted 消息:
stopReason === "aborted"且没 result 时,ToolMessage用合成的 isError result 占位,避免悬空 toolCall,见packages/web-ui/src/components/Messages.ts:248-258。 - 隐藏 pending 防重复:
hidePendingToolCalls为 true 时,稳定列表跳过还没结果的 toolCall,留给StreamingMessageContainer渲染,见packages/web-ui/src/components/Messages.ts:122-124。 - artifact 消息不显示:
MessageList显式 skipartifact角色,这类消息只用于会话重建,见packages/web-ui/src/components/MessageList.ts:39-42。 - 流式时
StreamingMessageContainer空消息:仍显示一个加载指示 pulse 条,避免空白闪烁,见packages/web-ui/src/components/StreamingMessageContainer.ts:64-70。
小结
MessageList 与 StreamingMessageContainer 分工:稳定列表不变,流式容器用 requestAnimationFrame 批量合并;AssistantMessage 按 content 顺序分块渲染,toolCall 用 toolResultsById 配对内联。工具调用的具体渲染细节看 工具渲染器注册表,事件来源看 AgentInterface 会话宿主。