Message
Free · MITOne message in a thread.
- A person's message sits right, on the user surface, with their name and time under it (
metahides or replaces that line). - An agent's sits left, under its avatar, name and role, with reasoning above the answer, sources and actions below. While streaming, a caret follows the last block; an interrupted reply keeps what arrived and says it stopped.
- A person on the team (support, say) sits left like an agent, under a neutral avatar with their name and team, so they never read as the agent.
- A system line is centred and quiet.
- A reply a person checked (or edited) before it went out can say so in a caption under it, so a reader knows a person stood behind it.
- Times are relative ("5 minutes ago") unless
timeFormatasks for an absolute "time", "date" or "datetime" in the locale.
Build blocks with plainBlocks(text) for a person's words and markdownBlocks(text) for an agent's, from safe-markdown.
Preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/message
Brings in: agent-avatar, chat, citation, degraded-panel, locale, message-actions, message-content, reasoning, relative-time, thread-event, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import { Message, MessageBody, MessageFooter, MessageHeader } from "@/components/message"
import { AgentAvatar } from "@/components/agent-avatar"
import type { Message as MessageData } from "@/lib/chat"
import { Button } from "@/components/ui/button"
const agent = { id: "a1", name: "Support agent", role: "assistant" }
const reply: MessageData = {
id: "a",
author: { role: "agent", agent },
status: "complete",
attribution: { model: "claude-sonnet-5-5", cost: "$0.004" },
blocks: [
{ kind: "tools", calls: [{ id: "t", tool: "orders.lookup", title: "Looked up order #4821", status: "succeeded", duration: "0.4s" }] },
{ kind: "paragraph", nodes: [{ kind: "text", text: "It shipped this morning and should arrive on " }, { kind: "strong", text: "Thursday" }, { kind: "text", text: "." }] },
],
}
export default function Example() {
return (
<div className="flex max-w-2xl flex-col gap-5">
<Message message={{ id: "u", author: { role: "user" }, status: "complete", blocks: [{ kind: "paragraph", nodes: [{ kind: "text", text: "Where is my order #4821?" }] }] }} />
<Message onRate={() => {}} message={reply} />
{/* Your own actions: the model and cost stay, on their own line. */}
<Message
message={{ ...reply, id: "b" }}
actions={
<div className="flex gap-2">
<Button size="xs" variant="outline">
Track the parcel
</Button>
</div>
}
/>
{/* Rebuilt from the parts, with a badge in the header. */}
<article aria-label="Support agent, pinned reply" className="flex gap-3 rounded-lg border p-3">
<AgentAvatar agent={agent} className="mt-px" />
<div className="min-w-0 flex-1">
<MessageHeader name={agent.name} detail="Orders">
<span className="rounded-xs bg-accent-weak px-1 text-2xs text-accent-text">Pinned</span>
</MessageHeader>
<MessageBody message={reply} className="mt-1" />
<MessageFooter message={reply} actions={false} className="mt-3" />
</div>
</article>
</div>
)
}<Message>
| Prop | Type | Default | Description |
|---|---|---|---|
| messagerequired | MessageData | — | The message: author, status, blocks, and optional reasoning, sources and attribution. |
| onOpenEntity | ((entity: EntityRef) => void) | undefined | — | Makes entity chips open their thing. |
| onNavigate | ((to: string) => void) | undefined | — | Follows in-app links. |
| onOpenAttachment | ((attachment: Attachment) => void) | undefined | — | Makes attachment tiles buttons that open their file. |
| onRetry | (() => void) | undefined | — | Shows Retry on a failed or finished reply. |
| onContinue | (() => void) | undefined | — | Shows Continue on an interrupted reply. |
| rating | Rating | undefined | — | The current rating of an agent reply. |
| onRate | ((rating: Rating) => void) | undefined | — | Shows thumbs up and down under agent replies. |
| reviewedBy | { name: string; at?: string | undefined; edited?: boolean | undefined } | undefined | — | A person who checked this agent reply before it was sent: "Checked by Sam", or "Edited by Sam" when they changed it. |
| footer | React.ReactNode | undefined | — | More lines under an agent reply, after who checked it and above the actions row: e.g. MemoryUsed. |
| actions | false | React.ReactNode | undefined | — | The row under an agent reply (copy, retry, rating). `false` hides it; a node replaces it, e.g. your own buttons. Model and cost still show, on their own line. To rebuild a message, use MessageHeader, MessageBody and MessageFooter. |
| meta | false | React.ReactNode | undefined | — | The line under a person's own message: their name and when they sent it. `false` hides it; a node replaces it. |
| timeFormat | "relative" | DateStyle | undefined | — | How times show: "relative" (the default, "5 minutes ago") or an absolute "time", "date" or "datetime" in the locale. |
| density | "default" | "compact" | undefined | "default" | "compact" draws the header row smaller: a 12px name in a 24px row, for dense layouts. Defaults to "default". |
| bareCode | boolean | undefined | — | Code and other payload blocks without their frame (Payload's `bare`). |
| className | string | undefined | — | Extra classes for the outer element. |
<MessageHeader>
The row above a reply: who sent it, a detail (their specialty or team), and when.
| Prop | Type | Default | Description |
|---|---|---|---|
| namerequired | string | — | The sender's displayed name. |
| detail | string | undefined | — | A quieter detail after the name: an agent's specialty, a person's team. |
| createdAt | string | undefined | — | When it was sent; shown as a relative time unless `timeFormat` says otherwise. |
| timeFormat | "relative" | DateStyle | undefined | — | "relative" (the default) or an absolute "time", "date" or "datetime" in the locale. |
| density | "default" | "compact" | undefined | "default" | "compact": a 12px name in a 24px row. Defaults to "default" (14px in 28px). |
| children | React.ReactNode | undefined | — | Anything else for the row, after the time (a badge, a menu). |
| className | string | undefined | — | Extra classes for the outer element. |
<MessageBody>
A reply's content: its reasoning, then its blocks, with a caret after the last one while it streams.
| Prop | Type | Default | Description |
|---|---|---|---|
| messagerequired | MessageData | — | The message whose reasoning and blocks to show. |
| onOpenEntity | ((entity: EntityRef) => void) | undefined | — | Makes entity chips open their thing. |
| onNavigate | ((to: string) => void) | undefined | — | Follows in-app links. |
| onOpenAttachment | ((attachment: Attachment) => void) | undefined | — | Makes attachment tiles buttons that open their file. |
| bareCode | boolean | undefined | — | Code and other payload blocks without their frame. |
| className | string | undefined | — | Extra classes for the outer element. |
<MessageFooter>
What sits under a reply: that it stopped or failed, its sources, who checked it, then the actions row with model and cost. actions={false} hides the row and a node replaces it; either way the model and cost stay, on their own line, because a reader should always see what produced the answer.
| Prop | Type | Default | Description |
|---|---|---|---|
| messagerequired | MessageData | — | The message the footer belongs to. |
| onRetry | (() => void) | undefined | — | Shows Retry on a failed or finished reply. |
| onContinue | (() => void) | undefined | — | Shows Continue on an interrupted reply. |
| rating | Rating | undefined | — | The current rating. |
| onRate | ((rating: Rating) => void) | undefined | — | Shows thumbs up and down. |
| reviewedBy | { name: string; at?: string | undefined; edited?: boolean | undefined } | undefined | — | A person who checked the reply before it was sent. |
| footer | React.ReactNode | undefined | — | More lines after who checked it and above the actions row: e.g. MemoryUsed. |
| actions | false | React.ReactNode | undefined | — | `false` hides the actions row, a node replaces it; model and cost still show. |
| timeFormat | "relative" | DateStyle | undefined | — | How the reviewer's time shows: "relative" (the default) or an absolute "time", "date" or "datetime". |
| className | string | undefined | — | Extra classes for the outer element. |
<MessageAttribution>
Model and cost, on a line of their own: what the actions row shows, for when it's hidden or replaced.
| Prop | Type | Default | Description |
|---|---|---|---|
| attribution | MessageData["attribution"] | undefined | — | The service's measured values; nothing shows when both are absent. |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under message in <LocaleProvider strings={{ … }}>; the table is exported as messageStrings.
| Key | English |
|---|---|
| you | You |
| personLabel | {name}, {team} |
| agentLabel | {name}, {noun} agent |
| interrupted | Stopped before it finished. What arrived is kept. |
| continue | Continue |
| failed | The agent couldn’t finish this reply. |
| failedMissing | The rest of the answer |
| checkedBy | Checked by {name} before sending |
| editedBy | Edited by {name} before sending |