/* Copyright 2026 Marimo. All rights reserved. */
import type { DataUIPart, ToolUIPart, UIMessage } from "ai";
import React from "react";
import { renderHTML } from "@/plugins/core/RenderHTML";
import { logNever } from "@/utils/assertNever";
import { Logger } from "@/utils/Logger";
import { MarkdownRenderer } from "../markdown/markdown-renderer";
import { AttachmentRenderer } from "./chat-components";
import { ReasoningAccordion } from "./reasoning-accordion";
import { ToolCallAccordion } from "./tool-call-accordion";
export const renderUIMessage = ({
message,
isStreamingReasoning,
isLast,
}: {
message: UIMessage;
isStreamingReasoning: boolean;
isLast: boolean;
}) => {
return (
<>{message.parts.map((part, index) => renderUIMessagePart(part, index))}>
);
function renderUIMessagePart(
part: UIMessage["parts"][number],
index: number,
) {
if (isToolPart(part)) {
return (
);
}
if (isDataPart(part)) {
Logger.debug("Found data part", part);
return null;
}
switch (part.type) {
case "text":
// Streamdown sanitizes the HTML which strips out marimo elements
// So instead, we render the HTML with our custom renderer.
if (part.text.includes("
{renderHTML({
html: part.text,
})}
);
}
return ;
case "reasoning":
return (
);
case "file":
return ;
case "dynamic-tool":
return (
);
case "source-document":
case "source-url":
case "step-start":
Logger.debug("Found non-renderable part", part);
return null;
default:
logNever(part);
return null;
}
}
};
function isToolPart(part: UIMessage["parts"][number]): part is ToolUIPart {
return part.type.startsWith("tool-");
}
function isDataPart(
part: UIMessage["parts"][number],
): part is DataUIPart> {
return part.type.startsWith("data-");
}