Message parts
Renders a message's parts in order: text, reasoning, tool calls, sources, files and app data, with a quiet fallback for anything unknown.
On this page
Reasoning, tools, text, sources and an unknown part.
Installation
Usage
import { MessageParts } from "@/components/ui/chatcn/message-parts"
<MessageContent>
<MessageParts
message={message}
streaming={status === "streaming" && message.id === lastId}
toolProps={(part) => ({
onApprove: (id, scope) => approve(id, { scope }),
onDeny: (id, reason) => deny(id, reason),
})}
/>
</MessageContent>Messages follow the chatcn contract, a structural mirror of AI SDK UIMessage, so an AI SDK message can be passed straight in.
What renders what
| Part | Default | Override |
|---|---|---|
text | Plain text with paragraphs and the streaming caret | renderText |
reasoning | One collapsed line: Thinking, then Thought for 4 s | components.reasoning |
tool-*, dynamic-tool | ToolCall, consecutive calls grouped | components.tool, toolProps |
source-url, source-document | Quiet source chips, grouped | components.sources |
file | Image preview or a file chip | components.file |
data-* | Nothing | components["data-weather"] |
step-start | Nothing | |
| Anything else | Unsupported part: type | components.fallback |
Markdown
<MessageParts
message={message}
renderText={(text, { streaming }) => <Markdown streaming={streaming}>{text}</Markdown>}
/>Props
| Prop | Type | Notes |
|---|---|---|
message | ChatcnMessage | |
streaming | boolean | Shows the thinking indicator in gaps between parts |
stopped | boolean | Unfinished tool calls render as cancelled |
renderText | (text, { part, streaming }) => ReactNode | Plug in markdown |
components | Partial<MessagePartComponents> | Replace any renderer |
toolProps | (part) => Partial<ToolCallProps> | Approval and retry handlers |
toolVariant | "card" | "inline" | Defaults to inline |
thinkingLabel | string | Defaults to Thinking |
Accessibility
- Each part keeps its own semantics: tool calls are disclosures, reasoning is a disclosure, sources are links.
- A renderer that throws is caught per part and replaced by the fallback, so one bad part never blanks the transcript.
Motion
- Text streams in with no per-token effect. The caret marks where the model is writing.
- When a streaming reply pauses after a tool result, Thinking appears after 300 ms; a quick gap shows nothing.