import { Match, Show, Suspense, Switch, lazy } from "solid-js" import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state" import { Markdown } from "./markdown" import { useTheme } from "../lib/theme" import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/message" type ToolCallPart = Extract const LazyToolCall = lazy(() => import("./tool-call")) interface MessagePartProps { part: ClientPart messageType?: "user" | "assistant" instanceId: string sessionId: string // For user messages, keep the primary prompt text visible even when synthetic (optimistic). // Other synthetic text parts (tool traces, read outputs, etc.) should be hidden. primaryUserTextPartId?: string | null onRendered?: () => void } export default function MessagePart(props: MessagePartProps) { const { isDark } = useTheme() const partType = () => props.part?.type || "" const reasoningId = () => `reasoning-${props.part?.id || ""}` const isReasoningExpanded = () => isItemExpanded(reasoningId()) const isAssistantMessage = () => props.messageType === "assistant" const textContainerClass = () => (isAssistantMessage() ? "message-text message-text-assistant" : "message-text") const markdownContainerClass = () => "message-text message-text-assistant" const textContainerRole = () => props.messageType || "assistant" const shouldHideTextPart = () => { const part = props.part if (!part || part.type !== "text") return false return Boolean((part as any).synthetic) } const plainTextContent = () => { const part = props.part if ((part.type === "text" || part.type === "reasoning") && typeof part.text === "string") { return part.text } return "" } const canRenderMarkdown = () => { const id = (props.part as unknown as { id?: unknown })?.id return typeof id === "string" && id.length > 0 } function reasoningSegmentHasText(segment: unknown): boolean { if (typeof segment === "string") { return segment.trim().length > 0 } if (segment && typeof segment === "object") { const candidate = segment as { text?: unknown; value?: unknown; content?: unknown[] } if (typeof candidate.text === "string" && candidate.text.trim().length > 0) { return true } if (typeof candidate.value === "string" && candidate.value.trim().length > 0) { return true } if (Array.isArray(candidate.content)) { return candidate.content.some((entry) => reasoningSegmentHasText(entry)) } } return false } const hasReasoningContent = () => { if (props.part?.type !== "reasoning") { return false } if (reasoningSegmentHasText((props.part as any).text)) { return true } if (Array.isArray((props.part as any).content)) { return (props.part as any).content.some((entry: unknown) => reasoningSegmentHasText(entry)) } return false } const createTextPartForMarkdown = (): TextPart => { const part = props.part if (part.type === "text" && typeof part.text === "string") { // Pass through the original part so `renderCache` updates persist. return part as unknown as TextPart } if (part.type === "reasoning" && typeof (part as any).text === "string") { // Reasoning parts render as markdown in some views; normalize to TextPart. return { id: part.id, type: "text", text: (part as any).text, synthetic: false, version: (part as { version?: number }).version, renderCache: (part as any).renderCache, } } return { id: part.id, type: "text", text: "", synthetic: false, } } function handleReasoningClick(e: Event) { e.preventDefault() toggleItemExpanded(reasoningId()) } return (
{plainTextContent()}}>
}>
) }