Message

Free · MIT

One message in a thread.

  • A person's message sits right, on the user surface, with their name and time under it (meta hides 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 timeFormat asks 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.

Markdown

Preview

Install

With the Candor CLI

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

TSX · The example above
"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>

PropTypeDefaultDescription
messagerequiredMessageData—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.
ratingRating | 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.
footerReact.ReactNode | undefined—More lines under an agent reply, after who checked it and above the actions row: e.g. MemoryUsed.
actionsfalse | 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.
metafalse | 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".
bareCodeboolean | undefined—Code and other payload blocks without their frame (Payload's `bare`).
classNamestring | undefined—Extra classes for the outer element.

<MessageHeader>

The row above a reply: who sent it, a detail (their specialty or team), and when.

PropTypeDefaultDescription
namerequiredstring—The sender's displayed name.
detailstring | undefined—A quieter detail after the name: an agent's specialty, a person's team.
createdAtstring | 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).
childrenReact.ReactNode | undefined—Anything else for the row, after the time (a badge, a menu).
classNamestring | 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.

PropTypeDefaultDescription
messagerequiredMessageData—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.
bareCodeboolean | undefined—Code and other payload blocks without their frame.
classNamestring | 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.

PropTypeDefaultDescription
messagerequiredMessageData—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.
ratingRating | 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.
footerReact.ReactNode | undefined—More lines after who checked it and above the actions row: e.g. MemoryUsed.
actionsfalse | 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".
classNamestring | 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.

PropTypeDefaultDescription
attributionMessageData["attribution"] | undefined—The service's measured values; nothing shows when both are absent.
classNamestring | 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.

KeyEnglish
youYou
personLabel{name}, {team}
agentLabel{name}, {noun} agent
interruptedStopped before it finished. What arrived is kept.
continueContinue
failedThe agent couldn’t finish this reply.
failedMissingThe rest of the answer
checkedByChecked by {name} before sending
editedByEdited by {name} before sending