Skip to content
ConversationMessage partsMenu

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.

Reasoning, tools, text, sources and an unknown part.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/message-parts.json

Usage

tsx
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

PartDefaultOverride
textPlain text with paragraphs and the streaming caretrenderText
reasoningOne collapsed line: Thinking, then Thought for 4 scomponents.reasoning
tool-*, dynamic-toolToolCall, consecutive calls groupedcomponents.tool, toolProps
source-url, source-documentQuiet source chips, groupedcomponents.sources
fileImage preview or a file chipcomponents.file
data-*Nothingcomponents["data-weather"]
step-startNothing
Anything elseUnsupported part: typecomponents.fallback

Markdown

tsx
<MessageParts
  message={message}
  renderText={(text, { streaming }) => <Markdown streaming={streaming}>{text}</Markdown>}
/>

Props

PropTypeNotes
messageChatcnMessage
streamingbooleanShows the thinking indicator in gaps between parts
stoppedbooleanUnfinished tool calls render as cancelled
renderText(text, { part, streaming }) => ReactNodePlug in markdown
componentsPartial<MessagePartComponents>Replace any renderer
toolProps(part) => Partial<ToolCallProps>Approval and retry handlers
toolVariant"card" | "inline"Defaults to inline
thinkingLabelstringDefaults 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.