# Message

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`.

- Registry name: `message` (`@candor/message`)
- Tier: Free (MIT). No licence key needed.
- Docs: https://candor.themethodic.com/docs/message

## Install

```bash
npx candor-ui add message
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/message`.
It brings in: agent-avatar, chat, citation, degraded-panel, locale, message-actions, message-content, reasoning, relative-time, thread-event, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"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>

Props:

- `message: 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 `"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`

## <MessageHeader>

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

Props:

- `name: 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 `"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`

## <MessageBody>

A reply's content: its reasoning, then its blocks, with a caret after the last one while it streams.

Props:

- `message: 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`

## <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.

Props:

- `message: 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`

## <MessageAttribution>

Model and cost, on a line of their own: what the actions row shows, for when it's hidden or replaced.

Props:

- `attribution?: MessageData["attribution"] | undefined`: The service's measured values; nothing shows when both are absent.
- `className?: string | undefined`

## Strings (`message`)

The fixed words, English by default. Override any of them under `message` in `<LocaleProvider strings={{ message: { … } }}>`; the table is exported as `messageStrings`.

- `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

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
