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

# WallyAssistant

The closed Wally panel: pass a `ChatSnapshot` (messages, status, suggestions, permissions, context) and callbacks, and it renders the whole Figma chat — header, stick-to-bottom log, greeting, chain of thought, docked approval and input. It holds no agent state: the app folds its stream into a new snapshot on every delta. Send is blocked while the last message has a pending approval (typing still works), and a resolved card stays docked until the next message. Every rule lives in exported pieces — `ChatViewProvider`, `deriveChatState`, `ChatMessageView`, `ChatPartView` — so a custom layout reuses them (Custom Layout). Seams like `renderPart`, `renderStepContent` and `renderMessageActions` hand back the default rendering. Stories are driven by `useMockWally()` from `WallyAssistant/mocks` and keep feedback and permissions in app state. Messages render with the default `AssistantMessageMarkdown`, and fenced code highlights with the lazy Prism adapter WallyAssistant mounts unless the app provides a `CodeSnippetAdapterProvider`.

[Source](https://github.com/wallarm/design-system/tree/main/packages/design-system/src/components/WallyAssistant) · [Storybook](https://wallarm.github.io/design-system/storybook/?path=/docs/ai-wallyassistant--overview)

```tsx
import { useState } from 'react';
import { type ChatAssistantMessage, type ChatFeedback, type ChatToolPermission, WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
import { EMPTY, useMockWally } from './mocks';

export const Empty = () => {
  const wally = useMockWally({ initial: EMPTY });
  const [feedback, setFeedback] = useState<Record<string, ChatFeedback | null>>({});
  const [permissions, setPermissions] = useState(EMPTY.permissions);
  const messages = wally.snapshot.messages.map(m =>
    m.role === 'assistant' && m.id in feedback ? withFeedback(m, feedback[m.id] ?? null) : m,
  );

  const handleFeedback = (message: ChatAssistantMessage, value: ChatFeedback | null) => {
    setFeedback(f => ({ ...f, [message.id]: value }));
  };

  const handlePermissionChange = (permission: ChatToolPermission, allowed: boolean) => {
    setPermissions(list => list?.map(x => (x.id === permission.id ? { ...x, allowed } : x)));
  };

  return (
    <div className='h-640 w-480'>
      <WallyAssistant
        data-testid='wally'
        {...wally.snapshot}
        messages={messages}
        permissions={permissions}
        formatTimestamp={fixedTime}
        onSubmit={wally.send}
        onStop={wally.stop}
        onApprovalRespond={wally.respond}
        onFeedback={handleFeedback}
        onPermissionChange={handlePermissionChange}
      />
    </div>
  );
};
```

## Usage

```tsx
import { type ChatAssistantMessage, type ChatFeedback, type ChatToolPermission, WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
```

## Examples

### Conversation

Two finished turns: folded chain of thought, answers with Copy, thumbs and timestamps, follow-up pills and the permissions button.

```tsx
import { useState } from 'react';
import { type ChatAssistantMessage, type ChatFeedback, type ChatToolPermission, WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
import { CONVERSATION, useMockWally } from './mocks';

export const Conversation = () => {
  const wally = useMockWally({ initial: CONVERSATION });
  const [feedback, setFeedback] = useState<Record<string, ChatFeedback | null>>({});
  const [permissions, setPermissions] = useState(CONVERSATION.permissions);
  const messages = wally.snapshot.messages.map(m =>
    m.role === 'assistant' && m.id in feedback ? withFeedback(m, feedback[m.id] ?? null) : m,
  );

  const handleFeedback = (message: ChatAssistantMessage, value: ChatFeedback | null) => {
    setFeedback(f => ({ ...f, [message.id]: value }));
  };

  const handlePermissionChange = (permission: ChatToolPermission, allowed: boolean) => {
    setPermissions(list => list?.map(x => (x.id === permission.id ? { ...x, allowed } : x)));
  };

  return (
    <div className='h-640 w-480'>
      <WallyAssistant
        data-testid='wally'
        {...wally.snapshot}
        messages={messages}
        permissions={permissions}
        formatTimestamp={fixedTime}
        onSubmit={wally.send}
        onStop={wally.stop}
        onApprovalRespond={wally.respond}
        onFeedback={handleFeedback}
        onPermissionChange={handlePermissionChange}
      />
    </div>
  );
};
```

### Streaming simulation

The whole Wally script from `useMockWally()`: send a message or click a pill to watch the loader,
a live reasoning step, three tool steps with the Working row between them, the streamed answer,
then the fold under "Worked for 12s". Stop cancels the run. `speed` scales every delay (0 = instant).

```tsx
import { useState } from 'react';
import { type ChatAssistantMessage, type ChatFeedback, type ChatToolPermission, WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
import { CONVERSATION, useMockWally } from './mocks';

export const StreamingSimulation = ({ speed }) => {
  const wally = useMockWally({ initial: CONVERSATION, speed });
  const [feedback, setFeedback] = useState<Record<string, ChatFeedback | null>>({});
  const [permissions, setPermissions] = useState(CONVERSATION.permissions);
  const messages = wally.snapshot.messages.map(m =>
    m.role === 'assistant' && m.id in feedback ? withFeedback(m, feedback[m.id] ?? null) : m,
  );

  const handleFeedback = (message: ChatAssistantMessage, value: ChatFeedback | null) => {
    setFeedback(f => ({ ...f, [message.id]: value }));
  };

  const handlePermissionChange = (permission: ChatToolPermission, allowed: boolean) => {
    setPermissions(list => list?.map(x => (x.id === permission.id ? { ...x, allowed } : x)));
  };

  return (
    <div className='h-640 w-480'>
      <WallyAssistant
        data-testid='wally'
        {...wally.snapshot}
        messages={messages}
        permissions={permissions}
        formatTimestamp={fixedTime}
        onSubmit={wally.send}
        onStop={wally.stop}
        onApprovalRespond={wally.respond}
        onFeedback={handleFeedback}
        onPermissionChange={handlePermissionChange}
      />
    </div>
  );
};
```

### Approval

A pending approval docked above the input: Send is blocked (typing still works). Allow or Reject goes through `onApprovalRespond`, and the next snapshot resolves the card in place.

```tsx
import { useState } from 'react';
import { type ChatAssistantMessage, type ChatFeedback, type ChatToolPermission, WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
import { APPROVAL_PENDING, useMockWally } from './mocks';

export const Approval = () => {
  const wally = useMockWally({ initial: APPROVAL_PENDING });
  const [feedback, setFeedback] = useState<Record<string, ChatFeedback | null>>({});
  const [permissions, setPermissions] = useState(APPROVAL_PENDING.permissions);
  const messages = wally.snapshot.messages.map(m =>
    m.role === 'assistant' && m.id in feedback ? withFeedback(m, feedback[m.id] ?? null) : m,
  );

  const handleFeedback = (message: ChatAssistantMessage, value: ChatFeedback | null) => {
    setFeedback(f => ({ ...f, [message.id]: value }));
  };

  const handlePermissionChange = (permission: ChatToolPermission, allowed: boolean) => {
    setPermissions(list => list?.map(x => (x.id === permission.id ? { ...x, allowed } : x)));
  };

  return (
    <div className='h-640 w-480'>
      <WallyAssistant
        data-testid='wally'
        {...wally.snapshot}
        messages={messages}
        permissions={permissions}
        formatTimestamp={fixedTime}
        onSubmit={wally.send}
        onStop={wally.stop}
        onApprovalRespond={wally.respond}
        onFeedback={handleFeedback}
        onPermissionChange={handlePermissionChange}
      />
    </div>
  );
};
```

### Error state

A run that failed before any output: `AssistantMessageError` with Retry, wired to `onRetry`.

```tsx
import { useState } from 'react';
import { type ChatAssistantMessage, type ChatFeedback, type ChatToolPermission, WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
import { ERROR_TURN, useMockWally } from './mocks';

export const ErrorState = () => {
  const wally = useMockWally({ initial: ERROR_TURN });
  const [feedback, setFeedback] = useState<Record<string, ChatFeedback | null>>({});
  const [permissions, setPermissions] = useState(ERROR_TURN.permissions);
  const [retries, setRetries] = useState(0);
  const messages = wally.snapshot.messages.map(m =>
    m.role === 'assistant' && m.id in feedback ? withFeedback(m, feedback[m.id] ?? null) : m,
  );

  const handleFeedback = (message: ChatAssistantMessage, value: ChatFeedback | null) => {
    setFeedback(f => ({ ...f, [message.id]: value }));
  };

  const handlePermissionChange = (permission: ChatToolPermission, allowed: boolean) => {
    setPermissions(list => list?.map(x => (x.id === permission.id ? { ...x, allowed } : x)));
  };

  const handleRetry = () => {
    setRetries(n => n + 1);
  };

  return (
    <div className='h-640 w-480'>
      <WallyAssistant
        data-testid='wally'
        {...wally.snapshot}
        messages={messages}
        permissions={permissions}
        formatTimestamp={fixedTime}
        onSubmit={wally.send}
        onStop={wally.stop}
        onApprovalRespond={wally.respond}
        onFeedback={handleFeedback}
        onPermissionChange={handlePermissionChange}
        onRetry={handleRetry}
        footerStart={
          retries ? <p className='text-xs text-text-secondary'>Retried {retries}×</p> : null
        }
      />
    </div>
  );
};
```

### Overrides

Every seam at once: `renderPart` draws a `custom` attack summary, `renderStepContent` puts a table
under `search_attacks`, `renderMessageActions` adds a tagged Copy and an Ellipsis menu, a thread
`Select` in `headerTitle`, an attach button in `inputTools`, and German `labels`.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Paperclip } from '@wallarm-org/design-system/icons';
import { Select, SelectButton, SelectContent, SelectOption, SelectOptionIndicator, SelectOptionText, SelectPositioner } from '@wallarm-org/design-system/Select';
import { type ChatAssistantMessage, type ChatFeedback, type ChatToolPermission, WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
import { useMockWally } from './mocks';

export const Overrides = () => {
  const wally = useMockWally({ initial: OVERRIDES });
  const [feedback, setFeedback] = useState<Record<string, ChatFeedback | null>>({});
  const [permissions, setPermissions] = useState(OVERRIDES.permissions);
  const messages = wally.snapshot.messages.map(m =>
    m.role === 'assistant' && m.id in feedback ? withFeedback(m, feedback[m.id] ?? null) : m,
  );

  const handleFeedback = (message: ChatAssistantMessage, value: ChatFeedback | null) => {
    setFeedback(f => ({ ...f, [message.id]: value }));
  };

  const handlePermissionChange = (permission: ChatToolPermission, allowed: boolean) => {
    setPermissions(list => list?.map(x => (x.id === permission.id ? { ...x, allowed } : x)));
  };

  return (
    <div className='h-640 w-480'>
      <WallyAssistant
        data-testid='wally'
        {...wally.snapshot}
        messages={messages}
        permissions={permissions}
        formatTimestamp={fixedTime}
        onSubmit={wally.send}
        onStop={wally.stop}
        onApprovalRespond={wally.respond}
        onFeedback={handleFeedback}
        onPermissionChange={handlePermissionChange}
        renderPart={renderPart}
        renderStepContent={renderStepContent}
        renderMessageActions={renderMessageActions}
        headerTitle={
          <Select collection={threads} defaultValue={['attacks']}>
            <SelectButton variant='ghost' size='small' placeholder='New chat' />
            <SelectPositioner>
              <SelectContent>
                {threads.items.map(item => (
                  <SelectOption key={item.value} item={item}>
                    <SelectOptionText>{item.label}</SelectOptionText>
                    <SelectOptionIndicator />
                  </SelectOption>
                ))}
              </SelectContent>
            </SelectPositioner>
          </Select>
        }
        inputTools={
          <Button variant='ghost' color='neutral' size='small' aria-label='Datei anhängen'>
            <Paperclip />
          </Button>
        }
        labels={{
          placeholder: 'Frag Wally...',
          input: 'Nachricht an Wally',
          send: 'Senden',
          stop: 'Antwort stoppen',
          disclaimer: 'Wally kann sich irren; bitte prüfen.',
          suggestions: 'Vorschläge',
          feedbackPositive: 'Gute Antwort',
          feedbackNegative: 'Schlechte Antwort',
        }}
      />
    </div>
  );
};
```

### Custom layout

A full-page chat with a centered 768px column, built from `ChatViewProvider`, `deriveChatState`, `Chat`, `Conversation*`, `ChatMessageView`, docked `ChatPartView` and its own `ChatInput` — proof the open parts reuse every rule.

```tsx
import { useState } from 'react';
import { Chat, ChatFooter, ChatHeader, ChatHeaderTitle } from '@wallarm-org/design-system/Chat';
import { ChatInput, ChatInputSubmit, ChatInputTextarea } from '@wallarm-org/design-system/ChatInput';
import { ConversationContent, Conversation as ConversationRoot, ConversationScrollButton } from '@wallarm-org/design-system/Conversation';
import { ChatMessageView, ChatPartView, ChatViewProvider, deriveChatState } from '@wallarm-org/design-system/WallyAssistant';
import { CONVERSATION, useMockWally } from './mocks';

export const CustomLayout = () => {
  const wally = useMockWally({ initial: CONVERSATION });
  const [draft, setDraft] = useState('');
  const { messages, status } = wally.snapshot;
  const state = deriveChatState(messages, status, 'docked');
  const last = state.rows[state.rows.length - 1];

  const handleSubmit = (text: string) => {
    if (state.awaitingApproval) return;
    wally.send(text);
    setDraft('');
  };

  return (
    <ChatViewProvider
      status={status}
      approvalPlacement='docked'
      formatTimestamp={fixedTime}
      onApprovalRespond={wally.respond}
    >
      <div className='h-screen'>
        <Chat data-testid='wally' background={state.empty ? 'strong' : 'subtle'}>
          <ChatHeader>
            <ChatHeaderTitle label='Wally workspace' />
          </ChatHeader>
          <ConversationRoot autoScrollKey={state.lastUserMessageId}>
            <ConversationContent aria-busy={state.busy} contentClassName='mx-auto w-full max-w-768'>
              {state.rows.map((m, i) => (
                <ChatMessageView
                  key={m.id}
                  message={m}
                  index={i}
                  isLast={i === state.rows.length - 1}
                />
              ))}
            </ConversationContent>
            <ConversationScrollButton />
          </ConversationRoot>
          <ChatFooter className='mx-auto w-full max-w-768'>
            {last &&
              state.dockedApprovals.map(a => (
                <ChatPartView key={a.id} part={a} message={last} isLast placement='docked' />
              ))}
            <ChatInput
              value={draft}
              onValueChange={setDraft}
              onSubmit={handleSubmit}
              onStop={wally.stop}
              isRunning={state.running}
            >
              <ChatInputTextarea />
              <ChatInputSubmit disabled={state.awaitingApproval && !state.running} />
            </ChatInput>
          </ChatFooter>
        </Chat>
      </div>
    </ChatViewProvider>
  );
};
```

### In drawer

Hosted in a 480px Drawer opened by `ChatTrigger`. Esc in the textarea while a reply streams stops it without closing the drawer — ChatInput handles that itself, no host recipe needed.

```tsx
import { Button } from '@wallarm-org/design-system/Button';
import { Chat, ChatTrigger } from '@wallarm-org/design-system/Chat';
import { Drawer, DrawerClose, DrawerContent, DrawerTitle, DrawerTrigger } from '@wallarm-org/design-system/Drawer';
import { History, Plus, X } from '@wallarm-org/design-system/icons';
import { Separator } from '@wallarm-org/design-system/Separator';
import { WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
import { CONVERSATION, useMockWally } from './mocks';

export const InDrawer = () => {
  const wally = useMockWally({ initial: CONVERSATION });

  return (
    <Drawer width={480}>
      <DrawerTrigger asChild>
        <ChatTrigger data-testid='chat-trigger' />
      </DrawerTrigger>
      <DrawerContent>
        {/* Names the dialog "Wally" (Ark points aria-labelledby at the title); hidden because
            the panel header already shows it. */}
        <div className='sr-only'>
          <DrawerTitle>Wally</DrawerTitle>
        </div>
        <WallyAssistant
          data-testid='wally'
          {...wally.snapshot}
          formatTimestamp={fixedTime}
          onSubmit={wally.send}
          onStop={wally.stop}
          onApprovalRespond={wally.respond}
          headerActions={
            <>
              <Button variant='ghost' color='neutral' size='small' aria-label='New chat'>
                <Plus />
              </Button>
              <Button variant='ghost' color='neutral' size='small' aria-label='Chat history'>
                <History />
              </Button>
              <Separator orientation='vertical' />
              <DrawerClose asChild>
                <Button variant='ghost' color='neutral' size='small' aria-label='Close chat'>
                  <X />
                </Button>
              </DrawerClose>
            </>
          }
        />
      </DrawerContent>
    </Drawer>
  );
};
```

### Analytics

Analytics attributes land on real nodes: the root `<section>`, header-action Buttons and a tagged Copy from `renderMessageActions` (like, dislike and timestamp stay default). Closed targets (pills, Send, approval options) report through their callbacks — see ANALYTICS_GAPS.md.

```tsx
import { useState } from 'react';
import { Button } from '@wallarm-org/design-system/Button';
import { Plus } from '@wallarm-org/design-system/icons';
import { type ChatAssistantMessage, type ChatFeedback, type ChatToolPermission, WallyAssistant } from '@wallarm-org/design-system/WallyAssistant';
import { CONVERSATION, useMockWally } from './mocks';

export const Analytics = () => {
  const wally = useMockWally({ initial: CONVERSATION });
  const [feedback, setFeedback] = useState<Record<string, ChatFeedback | null>>({});
  const [permissions, setPermissions] = useState(CONVERSATION.permissions);
  const messages = wally.snapshot.messages.map(m =>
    m.role === 'assistant' && m.id in feedback ? withFeedback(m, feedback[m.id] ?? null) : m,
  );

  const handleFeedback = (message: ChatAssistantMessage, value: ChatFeedback | null) => {
    setFeedback(f => ({ ...f, [message.id]: value }));
  };

  const handlePermissionChange = (permission: ChatToolPermission, allowed: boolean) => {
    setPermissions(list => list?.map(x => (x.id === permission.id ? { ...x, allowed } : x)));
  };

  return (
    <div className='h-640 w-480'>
      <WallyAssistant
        data-testid='wally'
        data-analytics-id='wally.panel'
        data-analytics-props='{"surface":"story"}'
        {...wally.snapshot}
        messages={messages}
        permissions={permissions}
        formatTimestamp={fixedTime}
        onSubmit={wally.send}
        onStop={wally.stop}
        onApprovalRespond={wally.respond}
        onFeedback={handleFeedback}
        onPermissionChange={handlePermissionChange}
        headerActions={
          <Button
            variant='ghost'
            color='neutral'
            size='small'
            aria-label='New chat'
            data-analytics-id='wally.new-chat'
          >
            <Plus />
          </Button>
        }
        renderMessageActions={renderAnalyticsActions}
      />
    </div>
  );
};
```

## Props

| Prop                 | Type                                                                        | Default  | Description                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------- | --------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| defaultChecked       | `boolean \| undefined`                                                      |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| autoFocus            | `boolean \| undefined`                                                      |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| id                   | `string \| undefined`                                                       |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| tabIndex             | `number \| undefined`                                                       |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| rel                  | `string \| undefined`                                                       |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| onChange             | `ChangeEventHandler<T, Element> \| undefined`                               |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| data-testid          | `string \| undefined`                                                       |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| background           | `ChatBackground \| undefined`                                               |          | Decorative mesh behind the panel (Figma chat-bg 67:1254). `'strong'` is Main (12%, for an empty chat), `'subtle'` is Chat (5%, for a running conversation), `'none'` renders nothing.                                                                                                                                                                                                    |
| messages \*          | `readonly ChatMessage[]`                                                    |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| status               | `ChatStatus \| undefined`                                                   | `ready`  | Default 'ready'.                                                                                                                                                                                                                                                                                                                                                                         |
| suggestions          | `readonly ChatSuggestionItem[] \| undefined`                                |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| permissions          | `readonly ChatToolPermission[] \| undefined`                                |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| context              | `readonly ChatContextItem[] \| undefined`                                   |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| value                | `string \| undefined`                                                       |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| onValueChange        | `((value: string) => void) \| undefined`                                    |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| disabled             | `boolean \| undefined`                                                      |          | Disables the textarea, Send and the suggestion pills (e.g. `!agent.isReady`).                                                                                                                                                                                                                                                                                                            |
| textareaRef          | `Ref<HTMLTextAreaElement> \| undefined`                                     |          | Focus management, e.g. focus the input after a thread switch.                                                                                                                                                                                                                                                                                                                            |
| onSubmit \*          | `(text: string) => void`                                                    |          | Trimmed text. Not called when empty, while running, while an approval is pending, or when disabled.                                                                                                                                                                                                                                                                                      |
| onStop               | `(() => void) \| undefined`                                                 |          | Stop button, and Esc in the textarea while running (also inside a Drawer/Dialog: ChatInput claims that Esc from the layer, so no host recipe is needed).                                                                                                                                                                                                                                 |
| onSuggestionSelect   | `((suggestion: ChatSuggestionItem) => void) \| undefined`                   |          | Default: onSubmit(suggestion.prompt ?? suggestion.label).                                                                                                                                                                                                                                                                                                                                |
| onPermissionChange   | `((permission: ChatToolPermission, allowed: boolean) => void) \| undefined` |          | Absent, or no permissions → no permissions button in the input.                                                                                                                                                                                                                                                                                                                          |
| onContextRemove      | `((item: ChatContextItem) => void) \| undefined`                            |          | Absent → context chips aren't removable.                                                                                                                                                                                                                                                                                                                                                 |
| header               | `ReactNode`                                                                 |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| headerTitle          | `ReactNode`                                                                 |          | ChatHeaderTitle children, e.g. a thread Select (Figma Variant2).                                                                                                                                                                                                                                                                                                                         |
| headerActions        | `ReactNode`                                                                 |          | New chat, history, close: your Buttons, so analytics land on them.                                                                                                                                                                                                                                                                                                                       |
| emptyState           | `ReactNode`                                                                 |          | Replaces the greeting and its pills (e.g. a history-loading state).                                                                                                                                                                                                                                                                                                                      |
| footerStart          | `ReactNode`                                                                 |          | Top of ChatFooter, above the suggestions (banners).                                                                                                                                                                                                                                                                                                                                      |
| inputTools           | `ReactNode`                                                                 |          | Extra toolbar items in the input, between permissions and Send.                                                                                                                                                                                                                                                                                                                          |
| input                | `ReactNode`                                                                 |          | Replaces the whole ChatInput composition (you wire value and submit yourself).                                                                                                                                                                                                                                                                                                           |
| onCopy               | `((message: ChatMessage, text: string) => void) \| undefined`               |          | Fired after Copy copied; Copy itself shows whenever the message has text.                                                                                                                                                                                                                                                                                                                |
| labels               | `Partial<ChatLabels> \| undefined`                                          |          |                                                                                                                                                                                                                                                                                                                                                                                          |
| icons                | `Readonly<Record<string, ReactNode>> \| undefined`                          |          | Icon key → element from src/icons; wins over the built-in Figma 483:1899 table.                                                                                                                                                                                                                                                                                                          |
| markdown             | `ComponentType<ChatMarkdownProps> \| undefined`                             |          | The markdown renderer, at module scope (never inline). Default: the DS `AssistantMessageMarkdown` (GFM, streaming-safe, no raw HTML, http/https/mailto/relative links only, no images). A replacement owns its own sanitizing. To change one element, wrap it at module scope: `const WallyMarkdown: FC<ChatMarkdownProps> = p => <AssistantMessageMarkdown {...p} components={MAP} />`. |
| formatTimestamp      | `((createdAt: string) => string) \| undefined`                              |          | ISO → label. Default: DS formatRelativeTime (honours DateFormatProvider). Give it a new identity on your clock tick to refresh "2 min ago" — the DS runs no clock.                                                                                                                                                                                                                       |
| approvalPlacement    | `"inline" \| "docked" \| undefined`                                         | `docked` | 'docked': the last message's approvals render in the footer. Default 'inline' here; WallyAssistant defaults to 'docked' (Figma). A custom layout passes the same value to `deriveChatState`, or the approval renders twice (provider 'inline') or never (provider 'docked').                                                                                                             |
| renderPart           | `complex`                                                                   |          | Called for every part, custom and unknown types included. undefined = the default; null = nothing. A render function: no hooks inside — put them in the components you return. While it is set, every part counts as rendered for the loader and the actions row (its output is unknown up front), so a running reply made only of parts it maps to nothing shows no loader.             |
| renderStepContent    | `complex`                                                                   |          | A step's collapsible body: rich tool output, or a card for one tool via `step.name`. undefined = the default; null = no body (the row isn't collapsible). No hooks inside.                                                                                                                                                                                                               |
| renderMessageActions | `complex`                                                                   |          | The actions row's children (Figma Wally order: Copy, ThumbsUp, ThumbsDown, Ellipsis, Timestamp). undefined = the default order; null = no row. No hooks inside.                                                                                                                                                                                                                          |
| onApprovalRespond    | `complex`                                                                   |          | Absent → approval buttons are disabled.                                                                                                                                                                                                                                                                                                                                                  |
| onFeedback           | `complex`                                                                   |          | Absent → no thumbs. Clicking the pressed thumb sends null.                                                                                                                                                                                                                                                                                                                               |
| onRetry              | `complex`                                                                   |          | Absent → no Retry on error parts.                                                                                                                                                                                                                                                                                                                                                        |

### ApprovalCardView

| Prop           | Type                                                      | Default | Description                                                                          |
| -------------- | --------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------ |
| approval \*    | `ChatApprovalPart`                                        |         |                                                                                      |
| message        | `ChatMessage \| undefined`                                |         |                                                                                      |
| onRespond      | `((response: ChatApprovalResponse) => void) \| undefined` |         | Default: the provider's onApprovalRespond. Absent → buttons disabled.                |
| disabled       | `boolean \| undefined`                                    | `false` | ORed with `approval.disabled`; ChatPartView sets it for approvals in older messages. |
| 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`             |         |                                                                                      |
| data-testid    | `string \| undefined`                                     |         |                                                                                      |

### ChainOfThoughtView

| Prop           | Type                                                 | Default | Description                                                                    |
| -------------- | ---------------------------------------------------- | ------- | ------------------------------------------------------------------------------ |
| part \*        | `ChatChainOfThoughtPart`                             |         |                                                                                |
| message \*     | `ChatMessage`                                        |         |                                                                                |
| active         | `boolean \| undefined`                               | `false` | The run is still producing this message: the block stays flat and never folds. |
| working        | `boolean \| undefined`                               | `false` | Append the synthetic "Working..." row: the run is busy but no step is live.    |
| 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           | `"a" \| "i" \| "1" \| "A" \| "I" \| undefined`       |         |                                                                                |
| data-testid    | `string \| undefined`                                |         |                                                                                |

### ChatMessageView

| Prop       | Type          | Default | Description                                                                                               |
| ---------- | ------------- | ------- | --------------------------------------------------------------------------------------------------------- |
| message \* | `ChatMessage` |         |                                                                                                           |
| index \*   | `number`      |         | Position in the rendered rows: drives the test id (`user-message-{index}` / `assistant-message-{index}`). |
| isLast \*  | `boolean`     |         | The last row: loader, streaming, docked approvals, always-visible actions.                                |

### ChatPartView

| Prop       | Type                                | Default  | Description                                                                      |
| ---------- | ----------------------------------- | -------- | -------------------------------------------------------------------------------- |
| part \*    | `ChatPart`                          |          |                                                                                  |
| message \* | `ChatMessage`                       |          |                                                                                  |
| isLast \*  | `boolean`                           |          |                                                                                  |
| placement  | `"inline" \| "docked" \| undefined` | `inline` | 'docked' when you render a docked approval in your own footer. Default 'inline'. |

### ChatViewProvider

| Prop                 | Type                                                          | Default                    | Description                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------- | ------------------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| status               | `ChatStatus \| undefined`                                     | `ready`                    | Default 'ready'.                                                                                                                                                                                                                                                                                                                                                                         |
| labels               | `Partial<ChatLabels> \| undefined`                            |                            |                                                                                                                                                                                                                                                                                                                                                                                          |
| icons                | `Readonly<Record<string, ReactNode>> \| undefined`            |                            | Icon key → element from src/icons; wins over the built-in Figma 483:1899 table.                                                                                                                                                                                                                                                                                                          |
| markdown             | `ComponentType<ChatMarkdownProps> \| undefined`               | `AssistantMessageMarkdown` | The markdown renderer, at module scope (never inline). Default: the DS `AssistantMessageMarkdown` (GFM, streaming-safe, no raw HTML, http/https/mailto/relative links only, no images). A replacement owns its own sanitizing. To change one element, wrap it at module scope: `const WallyMarkdown: FC<ChatMarkdownProps> = p => <AssistantMessageMarkdown {...p} components={MAP} />`. |
| formatTimestamp      | `((createdAt: string) => string) \| undefined`                |                            | ISO → label. Default: DS formatRelativeTime (honours DateFormatProvider). Give it a new identity on your clock tick to refresh "2 min ago" — the DS runs no clock.                                                                                                                                                                                                                       |
| approvalPlacement    | `"inline" \| "docked" \| undefined`                           | `inline`                   | 'docked': the last message's approvals render in the footer. Default 'inline' here; WallyAssistant defaults to 'docked' (Figma). A custom layout passes the same value to `deriveChatState`, or the approval renders twice (provider 'inline') or never (provider 'docked').                                                                                                             |
| renderPart           | `complex`                                                     |                            | Called for every part, custom and unknown types included. undefined = the default; null = nothing. A render function: no hooks inside — put them in the components you return. While it is set, every part counts as rendered for the loader and the actions row (its output is unknown up front), so a running reply made only of parts it maps to nothing shows no loader.             |
| renderStepContent    | `complex`                                                     |                            | A step's collapsible body: rich tool output, or a card for one tool via `step.name`. undefined = the default; null = no body (the row isn't collapsible). No hooks inside.                                                                                                                                                                                                               |
| renderMessageActions | `complex`                                                     |                            | The actions row's children (Figma Wally order: Copy, ThumbsUp, ThumbsDown, Ellipsis, Timestamp). undefined = the default order; null = no row. No hooks inside.                                                                                                                                                                                                                          |
| onApprovalRespond    | `complex`                                                     |                            | Absent → approval buttons are disabled.                                                                                                                                                                                                                                                                                                                                                  |
| onFeedback           | `complex`                                                     |                            | Absent → no thumbs. Clicking the pressed thumb sends null.                                                                                                                                                                                                                                                                                                                               |
| onCopy               | `((message: ChatMessage, text: string) => void) \| undefined` |                            | Fired after Copy copied; Copy itself shows whenever the message has text.                                                                                                                                                                                                                                                                                                                |
| onRetry              | `complex`                                                     |                            | Absent → no Retry on error parts.                                                                                                                                                                                                                                                                                                                                                        |
