Message
One conversation turn: a user bubble with inline editing, or an assistant answer that assembles reasoning, tool calls, markdown, code, and artifacts.
Installation
npx shadcn@latest add miskibin/chat-components/messageAlso installs message-parts, change-summary.
Usage
import { Message } from "@/components/ui/message"
export function Turn() {
return (
<>
<Message sender="user" content="Why does the build fail?" />
<Message
sender="assistant"
content="Because `generateStaticParams` returns the wrong shape."
reasoning="The stack trace points at the docs route."
reasoningDuration={4}
/>
</>
)
}Examples
Right-click any file, click any line
One fileActions array reaches the change card, the tool rows, the path chips and the images — the host decides what “open” means and gets the path back. A chip such as app/page.tsx:42 reports its line through onFileReferenceClick, ready for FilePreview's focusLine.
The quote pill lives in , and the list wires it through . Turn latency over the last five runs:
Right-click either chip — or the chart — for the same menu the change card
shows. The image is a real file behind a URL, so it carries the menu too; an
inline data: image would not.
Quote a selection
Select text inside an assistant answer and a “Quote” pill appears over it; onQuote receives the selection, for a composer to turn into a blockquote.
The retry budget is per turn, not per tool call: a turn that spends it on
a flaky fetch has nothing left for the write that follows.
Two things follow from that. The budget resets when the turn does, and a tool that fails twice is dropped rather than retried a third time — which is why a slow provider shows up as a shorter turn instead of a longer one.
Recolor the bubble
contentClassName merges last, so a single prop re-tints the user bubble. Anything the turn renders further down — tool rows, code, artifacts — is reachable by data-slot from an ancestor.
Target one sender
The wrapper stamps data-sender, so a single selector on an ancestor restyles every user turn and leaves assistant answers alone.
Attach images to a user turn
url can be a data: URL — handy when the composer reads a file with FileReader and never needs to upload it anywhere just to show a thumbnail.
<Message
sender="user"
content="What's wrong with this chart?"
attachments={[
{ id: "a1", name: "chart.png", mimeType: "image/png", url: dataUrl },
]}
/>Render citations inline
patternHandlers run over paragraph and list text after markdown parsing, so an agent can emit a plain token and the UI decides what it becomes — a footnote, a link, a button that opens the source. Use a global regex so scanning advances.
The registry build reads every file listed in registry.json and writes one JSON payload per item . Nothing else is copied.
Add your own hover actions
actionButtons fill the row that fades in under a turn on hover or focus — retry, feedback, share. Icons inherit the built-in size and focus ring, so a custom action is indistinguishable from the shipped ones.
Hover this answer — the action row sits under it.
Notes
API reference
Message
content*string- Markdown for assistant turns, plain text for user turns. A
<think>block is parsed out into the reasoning disclosure. sender*"user" | "assistant"- Picks the layout: right-aligned bubble, or full-width answer.
reasoningstring | null- Explicit reasoning shown above the answer. When set it wins over the
<think>parse. reasoningDefaultOpenboolean = false- Start the reasoning disclosure expanded. The latest thought still opens while it streams, then collapses when the turn finishes.
reasoningDurationnumber- Renders as "Thought for N seconds".
toolsMessageToolCallData[] = []- Tool rows above the answer. Three or more collapse behind a summary.
partsMessagePart[]- Chronological thinking, text, and tool segments. When set, it replaces the tools-then-content layout. Consecutive thinking chunks merge; a tool or text in between starts a new thinking span.
codeBlocksMessageCodeBlockData[] = []- Standalone snippets appended after the answer, each with its own copy button.
artifactsMessageArtifactData[] = []- Artifact cards appended after the code blocks.
attachmentsMessageAttachmentData[] = []- Files attached to a user turn —
{ id, name, mimeType, url }. Images render as thumbnails above the bubble, opening full-size in a new tab; other mime types fall back to a named chip. Ignored on assistant turns. editableboolean = false- Shows the edit affordance. Needs
onEditto do anything. onEdit(content: string) => void- Called on save (⌘↵); Escape cancels.
actionButtonsActionButton[] = []- Extra hover actions —
{ id, icon, onClick, title?, position? }. patternHandlersPatternHandler[] = []- Inline replacements applied to paragraph and list text — citations, mentions, ticket links.
onAskAnswer(toolId: string, result: AskQuestionResult) => void- Fired when an Ask Question tool on this turn is submitted or skipped.
workedFornumber- Elapsed seconds. Labels the “Worked for 12s” row the thinking and tool parts collapse into once the turn settles.
changesChangeSummaryFile[]- File-change card. When omitted, Edit / Write / ApplyPatch tools are summarised automatically.
onReviewChanges() => void- Makes the Review label on the change card a button.
onOpenFile(tool: MessageToolCallData) => void- Forwarded to every tool row: Edit / Write / Read headlines that name a file become buttons that open it — pair it with
FilePreview. onChangeFileClick(file: ChangeSummaryFile) => void- Makes each row of the turn's change-summary card clickable.
onFileReferenceClick(path: string, line?: number) => void- Makes the file references inside the answer's markdown clickable — every
MessageMarkdownin the turn gets it. The path arrives without its:linesuffix, and the line it named arrives beside it. fileActionsFileActionItem[]- Right-click menu for every file this turn names — the change card, the tool rows, the path chips and the images in the answer. Keep the array stable; see
ChangeSummary. onQuote(text: string) => void- Offers a “Quote” pill over a selection inside an assistant answer, for dropping the selected text into a composer. Assistant turns only.
resolveFileUrl(path: string) => string | undefined- Turns a path a tool names into a URL this page can load. Only the host knows how the machine's files reach the browser, so this is how a tool row shows an image the agent wrote or read instead of its bytes.
resolveFileUrl(path: string) => string | undefined- Turns a path a tool names into a URL this page can load. Only the host knows how a file on the machine reaches the browser, so without it an image an agent wrote or read renders as the stand-in line its harness returned. Return
undefinedfor anything you cannot serve. isAnimatingboolean = false- True while this turn is still streaming; keeps incomplete markdown from flickering.
contentClassNamestring- Merged onto the bubble (user) or the answer body (assistant).
Data slots
Every part below is addressable from a parent, so you can restyle internals without forking the component.
messagemessage-contentmessage-quotemessage-attachmentsmessage-attachmentmessage-actionsmessage-processmessage-turn-summary