For AI agents: the complete documentation index is available at https://wallarm.github.io/design-system/llms.txt, the full documentation bundle is available at https://wallarm.github.io/design-system/llms-full.txt, and this page is available as Markdown at https://wallarm.github.io/design-system/components/assistant-message.md.

AssistantMessage

A message from Wally: an <article> named "Wally" that left-aligns the answer and whatever led up to it. Stack its parts in this order — an optional ChainOfThought with the reasoning and tool calls, an optional ApprovalCard, the AssistantMessageContent, then a MessageActions row; the message spaces them 12px and 4px apart and lines the action icons up with the text. AssistantMessageContent spaces and styles markdown blocks; put AssistantMessageMarkdown inside it (WallyAssistant's default) or your own renderer's output. AssistantMessageMarkdown renders untrusted GFM as DS elements — no raw HTML, http/https/mailto/relative links only, images as alt text — and is streaming-safe: pass the whole text so far with streaming and it repairs only the unfinished tail. Set aria-busy on the message while the answer streams; before the first output, show only an AssistantMessageLoader "Working..." row, and if the answer fails, an AssistantMessageError after the content with your own Retry button in action.

Source · Storybook

Usage

import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { InlineCodeSnippet } from '@wallarm-org/design-system/CodeSnippet';

Examples

With reasoning

The reasoning sits above the answer as a ChainOfThought, 12px away. The first message keeps it collapsed; the second opens the "Thought for 5s" step.

With actions visible

The latest answer forces its actions on with visible: Copy, thumbs up and down, a more menu and the timestamp, with the first icon lined up with the text.

With approval

The full composition: reasoning, an ApprovalCard stretched to the message width, the answer and its actions. The card sits 12px from the parts around it, like the reasoning.

With actions on hover

Older answers keep their actions row transparent until the message is hovered or holds focus; the 30px row stays reserved so nothing jumps.

Streaming

While the answer streams, set aria-busy on the message and show the running step with a shimmering label; the actions come once it finishes.

Loading

Before the first output, the message holds only an AssistantMessageLoader: the Figma "Working..." row with a shimmering label, announced as role='status'. Pass children to change the text.

Error state

A failed answer: AssistantMessageError comes after whatever content streamed, announced as role='alert'. Put your own Retry Button in action, so its click and analytics are yours. The action renders inside the error's test-id cascade, but Button doesn't read it: either give it the full id yourself (here error--error--retry, i.e. the error's id error--error plus --retry), or read useTestId('retry') (exported from the package) inside your own Retry component to derive it. Not in Figma yet; pending design review.

Analytics

The message and its content are not interactive, but data-analytics-* still lands on their real nodes; put click analytics on each MessageAction instead.

Markdown

AssistantMessageMarkdown inside AssistantMessageContent: every GFM element in the DS look — heading, bold/italic/strike, inline code (click to copy), an aligned table, nested lists, a task list, a fenced block with a copy button, a quote, a rule and links. Highlighting follows the app's CodeSnippetAdapterProvider (WallyAssistant mounts the lazy Prism one itself).

Markdown streaming

Pass the whole text so far (a snapshot, never a delta) with streaming: only the unfinished tail is repaired, a dangling **, a half-typed table header and an empty fence wait a frame, and an open fence stays plain without a copy button until it closes. Finished blocks never re-render. Here a timer grows the string; press Replay to stream it again.

Markdown in chain of thought

variant='chain-of-thought' for reasoning inside a ChainOfThoughtStep (Figma 494:4292): the step body sets the 12/16 muted text, inline code is muted and not copyable, links are 12px.

Markdown unsafe content

Assistant text is untrusted (prompt injection). Raw HTML shows as typed (only <br> breaks a line), javascript:/data:/vbscript:/file: links render as plain text, and images show only their alt text, so no tracking pixel loads.

Markdown custom elements

components overrides any element at module scope; wrap the exported parts (AssistantMessageMarkdownLink, …InlineCode, …CodeBlock, …Table) to add analytics attributes to the real nodes. Here every link gets a data-analytics-id, and images render only from the app's own origin — the app owns that decision.

Props

PropTypeDefaultDescription
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
titlestring | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
data-testidstring | undefined

AssistantMessageContent

PropTypeDefaultDescription
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
titlestring | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
data-testidstring | undefined

AssistantMessageError

PropTypeDefaultDescription
actionReactNodeTrailing control, e.g. a Retry Button (yours, so analytics land on it). Rendered inside the error's test-id cascade: read useTestId('<slot>') in the action to get <error>--<slot> — Button doesn't.
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
titlestring | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
data-testidstring | undefined

AssistantMessageLoader

PropTypeDefaultDescription
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
titlestring | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
data-testidstring | undefined

AssistantMessageMarkdown

PropTypeDefaultDescription
text *stringUntrusted GFM: the whole text so far (a snapshot, never a delta).
streamingboolean | undefinedfalseThe text is still growing: repair the unfinished last block, hold back a half-typed table or an empty fence, keep an open fence unhighlighted. Default false.
variantAssistantMessageMarkdownVariant | undefinedanswerWhere it renders. 'chain-of-thought': muted, non-copyable inline code and 12px links. Default 'answer'.
componentsPartial<AssistantMessageMarkdownComponents> | undefinedElement overrides merged over the defaults. Module scope: a new object re-renders every block.

AssistantMessageMarkdownCodeBlock

PropTypeDefaultDescription
code *string
languagestring | undefinedThe fence info string (yaml, json, bash, http…). The app's adapter resolves aliases; unknown → plain.
completeboolean | undefinedtruefalse while the fence is still open in a streaming answer: plain text (no re-highlight per token) and no copy button. Default true.
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
titlestring | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
data-testidstring | undefined

AssistantMessageMarkdownInlineCode

PropTypeDefaultDescription
variantAssistantMessageMarkdownVariant | undefinedanswer'chain-of-thought' mutes the colour and turns copy off (Figma 494:4292). Default 'answer'.
code *stringThe code content to display
titlestring | undefined
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
copyableboolean | undefinedEnable click to copy functionality (default: true)
data-testidstring | undefined
PropTypeDefaultDescription
href *stringAlready allow-listed by the renderer: http(s), mailto or relative.
variantAssistantMessageMarkdownVariant | undefinedanswer'chain-of-thought' → 12px, otherwise 14px. Default 'answer'.
titlestring | undefined
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
typestring | undefined
disabled{ true: string; }
weight{ regular: string; medium: string; }
downloadany
hrefLangstring | undefined
targetHTMLAttributeAnchorTarget | undefined
data-testidstring | undefined

AssistantMessageMarkdownTable

PropTypeDefaultDescription
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
titlestring | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
data-testidstring | undefined

Explorer