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

# 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](https://github.com/wallarm/design-system/tree/main/packages/design-system/src/components/AssistantMessage) · [Storybook](https://wallarm.github.io/design-system/storybook/?path=/docs/ai-assistantmessage--overview)

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

export const AnswerProse = () => (
  <AssistantMessage data-testid='prose'>
    <AssistantMessageContent>
      <h3>Blocked attacks, last 24 hours</h3>
      <p>
        Wallarm blocked 1,204 SQL injection attempts, almost all against{' '}
        <InlineCodeSnippet code='/api/login' size='sm' />.
      </p>
      <ul>
        <li>
          Most came from <InlineCodeSnippet code='203.0.113.0/24' size='sm' />.
        </li>
        <li>The peak was at 03:00 UTC.</li>
        <li>No request reached the application.</li>
      </ul>
      <p>Want me to add a rule that denies the whole subnet?</p>
    </AssistantMessageContent>
  </AssistantMessage>
);
```

## Usage

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

```tsx
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { ChainOfThought, ChainOfThoughtStep } from '@wallarm-org/design-system/ChainOfThought';
import { InlineCodeSnippet } from '@wallarm-org/design-system/CodeSnippet';
import { Brain, Database, Search } from '@wallarm-org/design-system/icons';

export const WithReasoning = () => (
  <div className='flex flex-col gap-24'>
    <AssistantMessage data-testid='collapsed'>
      <ChainOfThought>
        <ChainOfThoughtStep icon={<Brain />} label='Thought for 5s'>
          <p>
            The user wants blocked attacks for a day. Query the events, then group them by path.
          </p>
        </ChainOfThoughtStep>
        <ChainOfThoughtStep icon={<Search />} label='Searched for blocked requests' />
        <ChainOfThoughtStep icon={<Database />} label='Queried attack events' />
      </ChainOfThought>
      <AssistantMessageContent>
        <h3>Blocked attacks, last 24 hours</h3>
        <p>
          Wallarm blocked 1,204 SQL injection attempts, almost all against{' '}
          <InlineCodeSnippet code='/api/login' size='sm' />.
        </p>
        <ul>
          <li>
            Most came from <InlineCodeSnippet code='203.0.113.0/24' size='sm' />.
          </li>
          <li>The peak was at 03:00 UTC.</li>
          <li>No request reached the application.</li>
        </ul>
        <p>Want me to add a rule that denies the whole subnet?</p>
      </AssistantMessageContent>
    </AssistantMessage>
    <AssistantMessage data-testid='expanded'>
      <ChainOfThought>
        <ChainOfThoughtStep icon={<Brain />} label='Thought for 5s' defaultOpen>
          <p>
            The user wants blocked attacks for a day. Query the events, then group them by path.
          </p>
        </ChainOfThoughtStep>
        <ChainOfThoughtStep icon={<Search />} label='Searched for blocked requests' />
        <ChainOfThoughtStep icon={<Database />} label='Queried attack events' />
      </ChainOfThought>
      <AssistantMessageContent>
        <h3>Blocked attacks, last 24 hours</h3>
        <p>
          Wallarm blocked 1,204 SQL injection attempts, almost all against{' '}
          <InlineCodeSnippet code='/api/login' size='sm' />.
        </p>
        <ul>
          <li>
            Most came from <InlineCodeSnippet code='203.0.113.0/24' size='sm' />.
          </li>
          <li>The peak was at 03:00 UTC.</li>
          <li>No request reached the application.</li>
        </ul>
        <p>Want me to add a rule that denies the whole subnet?</p>
      </AssistantMessageContent>
    </AssistantMessage>
  </div>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { ChainOfThought, ChainOfThoughtStep } from '@wallarm-org/design-system/ChainOfThought';
import { InlineCodeSnippet } from '@wallarm-org/design-system/CodeSnippet';
import { Copyable, CopyableIcon } from '@wallarm-org/design-system/Copyable';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@wallarm-org/design-system/DropdownMenu';
import { Brain, Database, Ellipsis, Search, ThumbsDown, ThumbsUp } from '@wallarm-org/design-system/icons';
import { MessageAction, MessageActions, MessageTimestamp } from '@wallarm-org/design-system/MessageActions';

export const WithActionsVisible = () => (
  <AssistantMessage data-testid='with-actions'>
    <ChainOfThought>
      <ChainOfThoughtStep icon={<Brain />} label='Thought for 5s'>
        <p>The user wants blocked attacks for a day. Query the events, then group them by path.</p>
      </ChainOfThoughtStep>
      <ChainOfThoughtStep icon={<Search />} label='Searched for blocked requests' />
      <ChainOfThoughtStep icon={<Database />} label='Queried attack events' />
    </ChainOfThought>
    <AssistantMessageContent>
      <h3>Blocked attacks, last 24 hours</h3>
      <p>
        Wallarm blocked 1,204 SQL injection attempts, almost all against{' '}
        <InlineCodeSnippet code='/api/login' size='sm' />.
      </p>
      <ul>
        <li>
          Most came from <InlineCodeSnippet code='203.0.113.0/24' size='sm' />.
        </li>
        <li>The peak was at 03:00 UTC.</li>
        <li>No request reached the application.</li>
      </ul>
      <p>Want me to add a rule that denies the whole subnet?</p>
    </AssistantMessageContent>
    <MessageActions visible>
      <Copyable text={ANSWER_TEXT}>
        <MessageAction aria-label='Copy'>
          <CopyableIcon />
        </MessageAction>
      </Copyable>
      <MessageAction aria-label='Good response'>
        <ThumbsUp />
      </MessageAction>
      <MessageAction aria-label='Bad response'>
        <ThumbsDown />
      </MessageAction>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <MessageAction aria-label='More actions'>
            <Ellipsis />
          </MessageAction>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Regenerate</DropdownMenuItem>
          <DropdownMenuItem>Report a problem</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <MessageTimestamp dateTime='2026-10-21T09:42:00Z'>1m ago</MessageTimestamp>
    </MessageActions>
  </AssistantMessage>
);
```

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

```tsx
import { ApprovalCard, ApprovalCardActions, ApprovalCardContent } from '@wallarm-org/design-system/ApprovalCard';
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { Button } from '@wallarm-org/design-system/Button';
import { ChainOfThought, ChainOfThoughtStep } from '@wallarm-org/design-system/ChainOfThought';
import { InlineCodeSnippet } from '@wallarm-org/design-system/CodeSnippet';
import { Copyable, CopyableIcon } from '@wallarm-org/design-system/Copyable';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@wallarm-org/design-system/DropdownMenu';
import { Brain, Database, Ellipsis, Search, ThumbsDown, ThumbsUp } from '@wallarm-org/design-system/icons';
import { MessageAction, MessageActions, MessageTimestamp } from '@wallarm-org/design-system/MessageActions';

export const WithApproval = () => (
  <AssistantMessage data-testid='with-approval'>
    <ChainOfThought>
      <ChainOfThoughtStep icon={<Brain />} label='Thought for 5s'>
        <p>The user wants blocked attacks for a day. Query the events, then group them by path.</p>
      </ChainOfThoughtStep>
      <ChainOfThoughtStep icon={<Search />} label='Searched for blocked requests' />
      <ChainOfThoughtStep icon={<Database />} label='Queried attack events' />
    </ChainOfThought>
    <ApprovalCard>
      <ApprovalCardContent>
        Add 203.0.113.0/24 to the denylist on all applications
      </ApprovalCardContent>
      <ApprovalCardActions>
        <Button>Allow</Button>
        <Button variant='secondary' color='neutral'>
          Reject
        </Button>
      </ApprovalCardActions>
    </ApprovalCard>
    <AssistantMessageContent>
      <h3>Blocked attacks, last 24 hours</h3>
      <p>
        Wallarm blocked 1,204 SQL injection attempts, almost all against{' '}
        <InlineCodeSnippet code='/api/login' size='sm' />.
      </p>
      <ul>
        <li>
          Most came from <InlineCodeSnippet code='203.0.113.0/24' size='sm' />.
        </li>
        <li>The peak was at 03:00 UTC.</li>
        <li>No request reached the application.</li>
      </ul>
      <p>Want me to add a rule that denies the whole subnet?</p>
    </AssistantMessageContent>
    <MessageActions visible>
      <Copyable text={ANSWER_TEXT}>
        <MessageAction aria-label='Copy'>
          <CopyableIcon />
        </MessageAction>
      </Copyable>
      <MessageAction aria-label='Good response'>
        <ThumbsUp />
      </MessageAction>
      <MessageAction aria-label='Bad response'>
        <ThumbsDown />
      </MessageAction>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <MessageAction aria-label='More actions'>
            <Ellipsis />
          </MessageAction>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Regenerate</DropdownMenuItem>
          <DropdownMenuItem>Report a problem</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <MessageTimestamp dateTime='2026-10-21T09:42:00Z'>1m ago</MessageTimestamp>
    </MessageActions>
  </AssistantMessage>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { InlineCodeSnippet } from '@wallarm-org/design-system/CodeSnippet';
import { Copyable, CopyableIcon } from '@wallarm-org/design-system/Copyable';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@wallarm-org/design-system/DropdownMenu';
import { Ellipsis, ThumbsDown, ThumbsUp } from '@wallarm-org/design-system/icons';
import { MessageAction, MessageActions, MessageTimestamp } from '@wallarm-org/design-system/MessageActions';

export const WithActionsOnHover = () => (
  <AssistantMessage data-testid='hover-actions'>
    <AssistantMessageContent>
      <h3>Blocked attacks, last 24 hours</h3>
      <p>
        Wallarm blocked 1,204 SQL injection attempts, almost all against{' '}
        <InlineCodeSnippet code='/api/login' size='sm' />.
      </p>
      <ul>
        <li>
          Most came from <InlineCodeSnippet code='203.0.113.0/24' size='sm' />.
        </li>
        <li>The peak was at 03:00 UTC.</li>
        <li>No request reached the application.</li>
      </ul>
      <p>Want me to add a rule that denies the whole subnet?</p>
    </AssistantMessageContent>
    <MessageActions>
      <Copyable text={ANSWER_TEXT}>
        <MessageAction aria-label='Copy'>
          <CopyableIcon />
        </MessageAction>
      </Copyable>
      <MessageAction aria-label='Good response'>
        <ThumbsUp />
      </MessageAction>
      <MessageAction aria-label='Bad response'>
        <ThumbsDown />
      </MessageAction>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <MessageAction aria-label='More actions'>
            <Ellipsis />
          </MessageAction>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem>Regenerate</DropdownMenuItem>
          <DropdownMenuItem>Report a problem</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <MessageTimestamp dateTime='2026-10-21T09:42:00Z'>1m ago</MessageTimestamp>
    </MessageActions>
  </AssistantMessage>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { ChainOfThought, ChainOfThoughtStep } from '@wallarm-org/design-system/ChainOfThought';
import { Database, Search } from '@wallarm-org/design-system/icons';

export const Streaming = () => (
  <AssistantMessage data-testid='streaming' aria-busy>
    <ChainOfThought>
      <ChainOfThoughtStep icon={<Search />} label='Searched for blocked requests' />
      <ChainOfThoughtStep icon={<Database />} label='Querying attack events...' status='running' />
    </ChainOfThought>
    <AssistantMessageContent>
      <p>Wallarm blocked 1,204 SQL injection attempts, almost all against</p>
    </AssistantMessageContent>
  </AssistantMessage>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageLoader } from '@wallarm-org/design-system/AssistantMessage';

export const Loading = () => (
  <AssistantMessage data-testid='loading' aria-busy>
    <AssistantMessageLoader />
  </AssistantMessage>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageContent, AssistantMessageError } from '@wallarm-org/design-system/AssistantMessage';
import { Button } from '@wallarm-org/design-system/Button';
import { RefreshCcw } from '@wallarm-org/design-system/icons';

export const ErrorState = () => (
  <AssistantMessage data-testid='error'>
    <AssistantMessageContent>
      <p>Wallarm blocked 1,204 SQL injection attempts, almost all against</p>
    </AssistantMessageContent>
    <AssistantMessageError
      action={
        <Button
          variant='ghost'
          color='neutral'
          size='small'
          data-testid='error--error--retry'
          data-analytics-id='WALLY_ANSWER_RETRY'
        >
          <RefreshCcw />
          Retry
        </Button>
      }
    >
      The connection was lost before the answer finished.
    </AssistantMessageError>
  </AssistantMessage>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { Copyable, CopyableIcon } from '@wallarm-org/design-system/Copyable';
import { MessageAction, MessageActions } from '@wallarm-org/design-system/MessageActions';
import { WALLY_ANSWER } from './mocks';

export const Analytics = () => (
  <AssistantMessage data-testid='analytics' data-analytics-id='WALLY_ANSWER'>
    <AssistantMessageContent data-analytics-id='WALLY_ANSWER_CONTENT'>
      <p>{ANSWER_TEXT}</p>
    </AssistantMessageContent>
    <MessageActions visible>
      <Copyable text={ANSWER_TEXT}>
        <MessageAction aria-label='Copy' data-analytics-id='WALLY_ANSWER_COPY'>
          <CopyableIcon />
        </MessageAction>
      </Copyable>
    </MessageActions>
  </AssistantMessage>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageContent, AssistantMessageMarkdown } from '@wallarm-org/design-system/AssistantMessage';
import { CodeSnippetAdapterProvider, loadPrismAdapter } from '@wallarm-org/design-system/CodeSnippet';
import { WALLY_ANSWER } from './mocks';

export const Markdown = () => (
  <CodeSnippetAdapterProvider adapter={loadPrismAdapter}>
    <AssistantMessage data-testid='markdown'>
      <AssistantMessageContent>
        <AssistantMessageMarkdown text={WALLY_ANSWER} />
      </AssistantMessageContent>
    </AssistantMessage>
  </CodeSnippetAdapterProvider>
);
```

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

```tsx
import { useEffect, useState } from 'react';
import { AssistantMessage, AssistantMessageContent, AssistantMessageMarkdown } from '@wallarm-org/design-system/AssistantMessage';
import { Button } from '@wallarm-org/design-system/Button';
import { CodeSnippetAdapterProvider, loadPrismAdapter } from '@wallarm-org/design-system/CodeSnippet';
import { RefreshCcw } from '@wallarm-org/design-system/icons';
import { WALLY_ANSWER } from './mocks';

export const MarkdownStreaming = () => {
  const [length, setLength] = useState(0);
  const streaming = length < WALLY_ANSWER.length;

  useEffect(() => {
    if (length >= WALLY_ANSWER.length) return;
    const id = setTimeout(() => setLength(Math.min(length + 6, WALLY_ANSWER.length)), 40);
    return () => clearTimeout(id);
  }, [length]);

  const handleClick = () => {
    setLength(0);
  };

  return (
    <div className='flex flex-col items-start gap-16'>
      <CodeSnippetAdapterProvider adapter={loadPrismAdapter}>
        <AssistantMessage data-testid='markdown-streaming' aria-busy={streaming}>
          <AssistantMessageContent>
            <AssistantMessageMarkdown text={WALLY_ANSWER.slice(0, length)} streaming={streaming} />
          </AssistantMessageContent>
        </AssistantMessage>
      </CodeSnippetAdapterProvider>
      <Button
        variant='outline'
        color='neutral'
        size='small'
        disabled={streaming}
        data-testid='markdown-streaming--replay'
        onClick={handleClick}
      >
        <RefreshCcw />
        Replay
      </Button>
    </div>
  );
};
```

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

```tsx
import { AssistantMessage, AssistantMessageMarkdown } from '@wallarm-org/design-system/AssistantMessage';
import { ChainOfThought, ChainOfThoughtStep } from '@wallarm-org/design-system/ChainOfThought';
import { Brain } from '@wallarm-org/design-system/icons';
import { COT_TEXT } from './mocks';

export const MarkdownInChainOfThought = () => (
  <AssistantMessage data-testid='cot-markdown'>
    <ChainOfThought>
      <ChainOfThoughtStep label='Thought for 5s' icon={<Brain />} defaultOpen>
        <AssistantMessageMarkdown variant='chain-of-thought' text={COT_TEXT} />
      </ChainOfThoughtStep>
    </ChainOfThought>
  </AssistantMessage>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageContent, AssistantMessageMarkdown } from '@wallarm-org/design-system/AssistantMessage';
import { HOSTILE } from './mocks';

export const MarkdownUnsafeContent = () => (
  <AssistantMessage data-testid='unsafe-markdown'>
    <AssistantMessageContent>
      <AssistantMessageMarkdown text={HOSTILE} />
    </AssistantMessageContent>
  </AssistantMessage>
);
```

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

```tsx
import { AssistantMessage, AssistantMessageContent, AssistantMessageMarkdown } from '@wallarm-org/design-system/AssistantMessage';

export const MarkdownCustomElements = () => (
  <AssistantMessage data-testid='custom-markdown'>
    <AssistantMessageContent>
      <AssistantMessageMarkdown
        components={MARKDOWN_COMPONENTS}
        text={
          'Read the [rule guide](https://docs.wallarm.com/user-guides/rules/) first.\n\n![Wallarm logo](./favicon.svg)\n\n![tracker](https://evil.example/p.gif)'
        }
      />
    </AssistantMessageContent>
  </AssistantMessage>
);
```

## Props

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| title          | `string \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| data-testid    | `string \| undefined`                                |         |             |

### AssistantMessageContent

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| title          | `string \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| data-testid    | `string \| undefined`                                |         |             |

### AssistantMessageError

| Prop           | Type                                                 | Default | Description                                                                                                                                                                                                |
| -------------- | ---------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| action         | `ReactNode`                                          |         | Trailing 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. |
| defaultChecked | `boolean \| undefined`                               |         |                                                                                                                                                                                                            |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |                                                                                                                                                                                                            |
| autoFocus      | `boolean \| undefined`                               |         |                                                                                                                                                                                                            |
| id             | `string \| undefined`                                |         |                                                                                                                                                                                                            |
| tabIndex       | `number \| undefined`                                |         |                                                                                                                                                                                                            |
| title          | `string \| undefined`                                |         |                                                                                                                                                                                                            |
| rel            | `string \| undefined`                                |         |                                                                                                                                                                                                            |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                                                                                                                                                            |
| data-testid    | `string \| undefined`                                |         |                                                                                                                                                                                                            |

### AssistantMessageLoader

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| title          | `string \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| data-testid    | `string \| undefined`                                |         |             |

### AssistantMessageMarkdown

| Prop       | Type                                                       | Default  | Description                                                                                                                                                   |
| ---------- | ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| text \*    | `string`                                                   |          | Untrusted GFM: the whole text so far (a snapshot, never a delta).                                                                                             |
| streaming  | `boolean \| undefined`                                     | `false`  | The 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. |
| variant    | `AssistantMessageMarkdownVariant \| undefined`             | `answer` | Where it renders. 'chain-of-thought': muted, non-copyable inline code and 12px links. Default 'answer'.                                                       |
| components | `Partial<AssistantMessageMarkdownComponents> \| undefined` |          | Element overrides merged over the defaults. Module scope: a new object re-renders every block.                                                                |

### AssistantMessageMarkdownCodeBlock

| Prop           | Type                                                 | Default | Description                                                                                                                         |
| -------------- | ---------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| code \*        | `string`                                             |         |                                                                                                                                     |
| language       | `string \| undefined`                                |         | The fence info string (`yaml`, `json`, `bash`, `http`…). The app's adapter resolves aliases; unknown → plain.                       |
| complete       | `boolean \| undefined`                               | `true`  | false while the fence is still open in a streaming answer: plain text (no re-highlight per token) and no copy button. Default true. |
| defaultChecked | `boolean \| undefined`                               |         |                                                                                                                                     |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |                                                                                                                                     |
| autoFocus      | `boolean \| undefined`                               |         |                                                                                                                                     |
| id             | `string \| undefined`                                |         |                                                                                                                                     |
| tabIndex       | `number \| undefined`                                |         |                                                                                                                                     |
| title          | `string \| undefined`                                |         |                                                                                                                                     |
| rel            | `string \| undefined`                                |         |                                                                                                                                     |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                                                                                     |
| data-testid    | `string \| undefined`                                |         |                                                                                                                                     |

### AssistantMessageMarkdownInlineCode

| Prop           | Type                                                 | Default  | Description                                                                                |
| -------------- | ---------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------ |
| variant        | `AssistantMessageMarkdownVariant \| undefined`       | `answer` | 'chain-of-thought' mutes the colour and turns copy off (Figma 494:4292). Default 'answer'. |
| code \*        | `string`                                             |          | The code content to display                                                                |
| title          | `string \| undefined`                                |          |                                                                                            |
| defaultChecked | `boolean \| undefined`                               |          |                                                                                            |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |          |                                                                                            |
| autoFocus      | `boolean \| undefined`                               |          |                                                                                            |
| id             | `string \| undefined`                                |          |                                                                                            |
| tabIndex       | `number \| undefined`                                |          |                                                                                            |
| rel            | `string \| undefined`                                |          |                                                                                            |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |          |                                                                                            |
| copyable       | `boolean \| undefined`                               |          | Enable click to copy functionality (default: true)                                         |
| data-testid    | `string \| undefined`                                |          |                                                                                            |

### AssistantMessageMarkdownLink

| Prop           | Type                                                 | Default  | Description                                                        |
| -------------- | ---------------------------------------------------- | -------- | ------------------------------------------------------------------ |
| href \*        | `string`                                             |          | Already allow-listed by the renderer: http(s), mailto or relative. |
| variant        | `AssistantMessageMarkdownVariant \| undefined`       | `answer` | 'chain-of-thought' → 12px, otherwise 14px. Default 'answer'.       |
| title          | `string \| undefined`                                |          |                                                                    |
| defaultChecked | `boolean \| undefined`                               |          |                                                                    |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |          |                                                                    |
| autoFocus      | `boolean \| undefined`                               |          |                                                                    |
| id             | `string \| undefined`                                |          |                                                                    |
| tabIndex       | `number \| undefined`                                |          |                                                                    |
| rel            | `string \| undefined`                                |          |                                                                    |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |          |                                                                    |
| type           | `string \| undefined`                                |          |                                                                    |
| disabled       | `{ true: string; }`                                  |          |                                                                    |
| weight         | `{ regular: string; medium: string; }`               |          |                                                                    |
| download       | `any`                                                |          |                                                                    |
| hrefLang       | `string \| undefined`                                |          |                                                                    |
| target         | `HTMLAttributeAnchorTarget \| undefined`             |          |                                                                    |
| data-testid    | `string \| undefined`                                |          |                                                                    |

### AssistantMessageMarkdownTable

| Prop           | Type                                                 | Default | Description |
| -------------- | ---------------------------------------------------- | ------- | ----------- |
| defaultChecked | `boolean \| undefined`                               |         |             |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |             |
| autoFocus      | `boolean \| undefined`                               |         |             |
| id             | `string \| undefined`                                |         |             |
| tabIndex       | `number \| undefined`                                |         |             |
| title          | `string \| undefined`                                |         |             |
| rel            | `string \| undefined`                                |         |             |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |             |
| data-testid    | `string \| undefined`                                |         |             |

## Explorer

