訊息渲染元件
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 會話宿主。