Nachrichten-Render-Komponenten
Drei Komponenten — MessageList, StreamingMessageContainer und Messages.ts — sind gemeinsam dafür zuständig, Agent.state.messages als sichtbaren Chat zu rendern. MessageList ist die stabile Liste (abgeschlossene Nachrichten), StreamingMessageContainer der Streaming-Container (die aktuell erzeugte Nachricht). Die Trennung verhindert, dass während des Streamings die gesamte Liste neu rendert. Messages.ts definiert die Sub-Elemente user-message, assistant-message, tool-message, aborted-message und übernimmt das Rendering der konkreten Inhaltsblöcke.
Zuständigkeiten
- Stabile Liste:
MessageList.buildRenderItemsiteriert übermessages, überspringt die Rolleartifact, versucht zuerstrenderMessageals Custom-Rendering und fällt aufuser-message/assistant-messagezurück; dierepeat-Directive recycled DOM nach key. Siehepackages/web-ui/src/components/MessageList.ts:27-81. - Streaming-Container:
StreamingMessageContainer.setMessagefasst Updates mitrequestAnimationFramezusammen, damit während des Streamings nicht pro Token neu gerendert wird. Siehepackages/web-ui/src/components/StreamingMessageContainer.ts:28-61. - Tiefe Kopie gegen Referenzvergleich:
JSON.parse(JSON.stringify(this._pendingMessage))kopiert die zu rendernde Nachricht tief, damit Lit Änderungen an verschachtelten Properties (etwatoolCall.arguments) erkennt. Siehepackages/web-ui/src/components/StreamingMessageContainer.ts:50-54. - Assistant-Block-Rendering:
AssistantMessage.rendergeht das Arraymessage.contentin Reihenfolge durch und renderttext/thinking/toolCall-Blöcke. Siehepackages/web-ui/src/components/Messages.ts:104-167. - Tool-Aufrufe paaren:
MessageListbaut zuerst eine Map dertoolResult-Rolle nachtoolCallId;AssistantMessageholt über das ProptoolResultsByIddas Resultat und rendert<tool-message>inline. Siehepackages/web-ui/src/components/Messages.ts:115-136. - Anhang-Nachrichten wandeln:
defaultConvertToLlmwandeltuser-with-attachmentsin eine Standard-user-Nachricht um; Bilder werden zuImageContent, Dokumente zuTextContent; artifact-Nachrichten werden herausgefiltert und nicht an den LLM geschickt. Siehepackages/web-ui/src/components/Messages.ts:348-383.
Designmotivation
Warum trennt man in stabile Liste und Streaming-Container? Weil während des Streamings laufend neue Token an message.content angehängt werden. Würde die gesamte MessageList mit requestUpdate mitlaufen, müsste bei jedem Update buildRenderItems die gesamte Historie neu durchlaufen — je länger der Chat, desto ruckeliger. Nach der Trennung kommen fertiggestellte Nachrichten in MessageList und ändern sich nicht mehr; nur die gerade erzeugte liegt in StreamingMessageContainer, der Updates mit requestAnimationFrame bündelt und pro Frame maximal einmal rendert.
Warum die langsame Operation JSON.parse(JSON.stringify(...))? Weil Lit Änderungen an Properties nur per Referenzvergleich erkennt und Agent während des Streamings dasselbe AssistantMessage-Objekt direkt mutiert (die content-Array und toolCall.arguments werden in-place geändert). Ohne Kopie sieht Lit keine Referenzänderung und die UI bleibt stehen. Die tiefe Kopie ist zwar langsam, pro Frame fällt aber nur eine Nachricht an — der Preis ist akzeptabel.
Warum wird ein tool result nicht als eigene Nachricht gerendert? Weil die assistant-Nachricht des LLM einen toolCall enthält, auf den direkt der passende toolResult folgt. Würde man sie getrennt rendern, risse der Kontext auseinander; AssistantMessage nutzt die toolResultsById-Map und legt das Resultat direkt neben den toolCall, visuell sind sie eine Gruppe. Genau deswegen überspringt MessageList standalone toolResult-Rollen. Siehe packages/web-ui/src/components/MessageList.ts:75-79.
Wichtige Dateien
packages/web-ui/src/components/MessageList.ts:11-26—class MessageList-Deklaration und props.packages/web-ui/src/components/MessageList.ts:27-81—buildRenderItems: überspringt artifact, ruftrenderMessageund bautuser-message/assistant-messagezusammen.packages/web-ui/src/components/MessageList.ts:83-92—render: nutztrepeat-Directive zum DOM-Recycling nach key.packages/web-ui/src/components/StreamingMessageContainer.ts:6-26—class StreamingMessageContainer-Deklaration.packages/web-ui/src/components/StreamingMessageContainer.ts:28-61—setMessage: immediate geht direkt, sonst überrequestAnimationFramegebündelt.packages/web-ui/src/components/Messages.ts:42-82—UserMessage-Komponente, rendert Text und Anhang-Tiles.packages/web-ui/src/components/Messages.ts:84-168—AssistantMessage-Komponente, rendert das content-Array blockweise.packages/web-ui/src/components/Messages.ts:226-277—ToolMessage-Komponente, ruftrenderToolund wählt Custom oder Default-Karte.packages/web-ui/src/components/Messages.ts:348-383—defaultConvertToLlm: wandeltuser-with-attachmentsundartifactum und filtert letztere.
Das Bündeln in setMessage ist der Performance-Schlüssel beim Streaming:
// 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 geht das content-Array in Reihenfolge durch; toolCall holt über toolResultsById das passende Resultat:
// 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>`,
);
}
}Datenfluss
Nachdem AgentInterface Events abonniert hat, verteilt es die Nachrichten auf die beiden Komponenten:
Randbedingungen und Fehler
- toolCall-Paarung während Streaming unvollständig:
toolResultsByIdhat für eine id kein Resultat;resultistundefined,ToolMessagewird als pending markiert. Siehepackages/web-ui/src/components/Messages.ts:118-122. - aborted-Nachrichten: Wenn
stopReason === "aborted"und kein Resultat vorliegt, fülltToolMessagemit einem synthetischen isError-Resultat auf, damit kein toolCall in der Luft hängt. Siehepackages/web-ui/src/components/Messages.ts:248-258. - pending verbergen gegen Doppelung: Wenn
hidePendingToolCallstrue ist, überspringt die stabile Liste toolCalls ohne Resultat und überlässt sie demStreamingMessageContainer. Siehepackages/web-ui/src/components/Messages.ts:122-124. - artifact-Nachrichten werden nicht angezeigt:
MessageListüberspringt die Rolleartifactexplizit; solche Nachrichten dienen nur dem Wiederherstellen der Konversation. Siehepackages/web-ui/src/components/MessageList.ts:39-42. - Leere Nachricht im Streaming-Container: Während des Streamings zeigt der Container bei leerer Nachricht einen Pulse-Ladebalken, damit kein weißes Flackern entsteht. Siehe
packages/web-ui/src/components/StreamingMessageContainer.ts:64-70.
Zusammenfassung
MessageList und StreamingMessageContainer teilen sich die Arbeit: Die stabile Liste bleibt unverändert, der Streaming-Container bündelt Updates mit requestAnimationFrame; AssistantMessage rendert das content-Array blockweise in Reihenfolge und paart toolCall über toolResultsById inline. Die Render-Details für Tools stehen in Tool-Renderer-Registry, die Herkunft der Events in AgentInterface Session-Host.