Componentes de render de mensajes
MessageList, StreamingMessageContainer y Messages.ts son los tres componentes que entre todos renderizan Agent.state.messages como una conversación visible. MessageList es la lista estable (mensajes finalizados); StreamingMessageContainer es el contenedor de streaming (el mensaje generándose ahora mismo); la división evita que la lista entera se rerenderice durante el streaming. Messages.ts define los sub-elementos user-message, assistant-message, tool-message, aborted-message y procesa el render concreto de los bloques de contenido.
Responsabilidades
- Lista estable:
MessageList.buildRenderItemsiteramessages, salta el rolartifact, primero intentarenderMessagecustom, y si no hace fallback auser-message/assistant-message, usando la directiverepeatpara reutilizar DOM por key. Verpackages/web-ui/src/components/MessageList.ts:27-81. - Contenedor de streaming:
StreamingMessageContainer.setMessageusarequestAnimationFramepara fusionar updates en lote, evitando un rerender por cada token durante el streaming. Verpackages/web-ui/src/components/StreamingMessageContainer.ts:28-61. - Clonado profundo para evitar comparación por referencia:
JSON.parse(JSON.stringify(this._pendingMessage))clona el mensaje pendiente, para que Lit detecte cambios en propiedades anidadas (comotoolCall.arguments). Verpackages/web-ui/src/components/StreamingMessageContainer.ts:50-54. - Render por bloques del assistant:
AssistantMessage.renderrecorremessage.contenten orden y renderiza los tres tipos de bloquetext/thinking/toolCall. Verpackages/web-ui/src/components/Messages.ts:104-167. - Pareo de tool calls:
MessageListprimero construye un Map portoolCallIdde los rolestoolResult, yAssistantMessagelo consulta vía proptoolResultsByIdpara renderizar inline el<tool-message>. Verpackages/web-ui/src/components/Messages.ts:115-136. - Conversión de mensajes con attachments:
defaultConvertToLlmconvierteuser-with-attachmentsa un user message estándar, imágenes aImageContenty documentos aTextContent; los mensajes artifact se filtran y no se envían al LLM. Verpackages/web-ui/src/components/Messages.ts:348-383.
Motivación de diseño
¿Por qué separar la lista estable y el contenedor de streaming? Porque durante el streaming message.content va acumulando tokens; si toda la MessageList hace requestUpdate con cada cambio, en cada update se ejecuta buildRenderItems sobre toda la historia, y cuánto más larga la charla más se cuelga. Tras la separación, los mensajes finalizados entran en MessageList y no cambian; sólo el mensaje generándose entra en StreamingMessageContainer, que fusiona updates con requestAnimationFrame y renderiza como mucho una vez por frame.
¿Por qué usar JSON.parse(JSON.stringify(...)) si es lento? Porque la detección de cambios de propiedades en Lit es shallow; y el Agent durante el streaming muta in place el array content del mismo objeto AssistantMessage (y toolCall.arguments también in place). Si no se clona, Lit no ve cambio de referencia y la UI no se actualiza. El clonado profundo es lento pero sólo procesa un mensaje por frame, coste asumible.
¿Por qué el tool result no se renderiza como mensaje independiente? Porque el mensaje assistant devuelto por el LLM tiene toolCall, y el toolResult que sigue es su pareja. Renderizarlos por separado rompería el contexto; AssistantMessage con el Map toolResultsById hace el result inline junto al toolCall, visualmente como un grupo. Que MessageList skip explícitamente el rol toolResult standalone es por esto mismo. Ver packages/web-ui/src/components/MessageList.ts:75-79.
Archivos clave
packages/web-ui/src/components/MessageList.ts:11-26— Declaración y props declass MessageList.packages/web-ui/src/components/MessageList.ts:27-81—buildRenderItems: salta artifact, invocarenderMessage, ensamblauser-message/assistant-message.packages/web-ui/src/components/MessageList.ts:83-92—render: usa la directiverepeatpara reutilizar por key.packages/web-ui/src/components/StreamingMessageContainer.ts:6-26— Declaración declass StreamingMessageContainer.packages/web-ui/src/components/StreamingMessageContainer.ts:28-61—setMessage: immediate directo, si norequestAnimationFramefusiona.packages/web-ui/src/components/Messages.ts:42-82— ComponenteUserMessage, renderiza texto y tiles de attachment.packages/web-ui/src/components/Messages.ts:84-168— ComponenteAssistantMessage, render por bloques según content.packages/web-ui/src/components/Messages.ts:226-277— ComponenteToolMessage, llama arenderToolpara decidir custom o tarjeta por defecto.packages/web-ui/src/components/Messages.ts:348-383—defaultConvertToLlm: filtra y convierteuser-with-attachmentsyartifact.
La fusión por lote de setMessage es clave de rendimiento del render 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 recorre el array content en orden; los toolCall buscan su result emparejado vía 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>`,
);
}
}Flujo de datos
AgentInterface se suscribe a eventos y reparte a los dos componentes:
Límites y fallos
- Pareo incompleto de toolCall durante streaming:
toolResultsByIdpuede no tener result para un id;resultesundefinedyToolMessagelo marca como pending. Verpackages/web-ui/src/components/Messages.ts:118-122. - Mensaje aborted: si
stopReason === "aborted"y no hay result,ToolMessagesintetiza un result isError placeholder, evitando toolCall colgando. Verpackages/web-ui/src/components/Messages.ts:248-258. - Ocultar pending para evitar duplicado: si
hidePendingToolCallses true, la lista estable salta los toolCall sin result y se los deja aStreamingMessageContainer. Verpackages/web-ui/src/components/Messages.ts:122-124. - Mensaje artifact no se muestra:
MessageListsalta explícitamente el rolartifact; estos mensajes sólo se usan para reconstruir la sesión. Verpackages/web-ui/src/components/MessageList.ts:39-42. StreamingMessageContainercon mensaje vacío en streaming: muestra un pulse de loading para evitar parpadeo en blanco. Verpackages/web-ui/src/components/StreamingMessageContainer.ts:64-70.
Resumen
MessageList y StreamingMessageContainer se reparten: la lista estable no cambia; el contenedor de streaming fusiona con requestAnimationFrame; AssistantMessage renderiza por bloques según content y parea toolCall con toolResultsById inline. Los detalles de render de herramientas en registro de renderers de herramientas; la fuente de eventos en host de sesión AgentInterface.