Message

One conversation turn: a user bubble with inline editing, or an assistant answer that assembles reasoning, tool calls, markdown, code, and artifacts.

Open in
error.png
Why does the docs build fail? Here's the error.

The build fails because generateStaticParams returns string[] instead of { slug: string }[].

ts
export function generateStaticParams() {  return componentSlugs.map((slug) => ({ slug }))}
1 File ChangedReview
page.tsx+3-1

Installation

npx shadcn@latest add miskibin/chat-components/message

Also 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:

Turn latency

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.

3 Files Changed
message.tsx+48-3
message-list.tsx+26-4
message-markdown.tsx+31-9

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.

Ship the release build.

The release script exited before publishing — the tool row below is tinted by a data-status selector.

Target one sender

The wrapper stamps data-sender, so a single selector on an ancestor restyles every user turn and leaves assistant answers alone.

Why does the registry build fail?

registry.json lists a file that no longer exists — the build resolves paths eagerly, so a stale entry is fatal.

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 onEdit to 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 MessageMarkdown in the turn gets it. The path arrives without its :line suffix, 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 undefined for 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