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

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 · Storybook

Usage

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.

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

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.

Error state

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

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.

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.

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.

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.

Props

PropTypeDefaultDescription
defaultCheckedboolean | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
data-testidstring | undefined
backgroundChatBackground | undefinedDecorative 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[]
statusChatStatus | undefinedreadyDefault 'ready'.
suggestionsreadonly ChatSuggestionItem[] | undefined
permissionsreadonly ChatToolPermission[] | undefined
contextreadonly ChatContextItem[] | undefined
valuestring | undefined
onValueChange((value: string) => void) | undefined
disabledboolean | undefinedDisables the textarea, Send and the suggestion pills (e.g. !agent.isReady).
textareaRefRef<HTMLTextAreaElement> | undefinedFocus management, e.g. focus the input after a thread switch.
onSubmit *(text: string) => voidTrimmed text. Not called when empty, while running, while an approval is pending, or when disabled.
onStop(() => void) | undefinedStop 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) | undefinedDefault: onSubmit(suggestion.prompt ?? suggestion.label).
onPermissionChange((permission: ChatToolPermission, allowed: boolean) => void) | undefinedAbsent, or no permissions → no permissions button in the input.
onContextRemove((item: ChatContextItem) => void) | undefinedAbsent → context chips aren't removable.
headerReactNode
headerTitleReactNodeChatHeaderTitle children, e.g. a thread Select (Figma Variant2).
headerActionsReactNodeNew chat, history, close: your Buttons, so analytics land on them.
emptyStateReactNodeReplaces the greeting and its pills (e.g. a history-loading state).
footerStartReactNodeTop of ChatFooter, above the suggestions (banners).
inputToolsReactNodeExtra toolbar items in the input, between permissions and Send.
inputReactNodeReplaces the whole ChatInput composition (you wire value and submit yourself).
onCopy((message: ChatMessage, text: string) => void) | undefinedFired after Copy copied; Copy itself shows whenever the message has text.
labelsPartial<ChatLabels> | undefined
iconsReadonly<Record<string, ReactNode>> | undefinedIcon key → element from src/icons; wins over the built-in Figma 483:1899 table.
markdownComponentType<ChatMarkdownProps> | undefinedThe 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) | undefinedISO → 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" | undefineddocked'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').
renderPartcomplexCalled 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.
renderStepContentcomplexA 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.
renderMessageActionscomplexThe actions row's children (Figma Wally order: Copy, ThumbsUp, ThumbsDown, Ellipsis, Timestamp). undefined = the default order; null = no row. No hooks inside.
onApprovalRespondcomplexAbsent → approval buttons are disabled.
onFeedbackcomplexAbsent → no thumbs. Clicking the pressed thumb sends null.
onRetrycomplexAbsent → no Retry on error parts.

ApprovalCardView

PropTypeDefaultDescription
approval *ChatApprovalPart
messageChatMessage | undefined
onRespond((response: ChatApprovalResponse) => void) | undefinedDefault: the provider's onApprovalRespond. Absent → buttons disabled.
disabledboolean | undefinedfalseORed with approval.disabled; ChatPartView sets it for approvals in older messages.
titlestring | undefined
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
data-testidstring | undefined

ChainOfThoughtView

PropTypeDefaultDescription
part *ChatChainOfThoughtPart
message *ChatMessage
activeboolean | undefinedfalseThe run is still producing this message: the block stays flat and never folds.
workingboolean | undefinedfalseAppend the synthetic "Working..." row: the run is busy but no step is live.
titlestring | undefined
defaultCheckedboolean | undefined
defaultValuestring | number | readonly string[] | undefined
autoFocusboolean | undefined
idstring | undefined
tabIndexnumber | undefined
relstring | undefined
onChangeChangeEventHandler<T, Element> | undefined
type"a" | "i" | "1" | "A" | "I" | undefined
data-testidstring | undefined

ChatMessageView

PropTypeDefaultDescription
message *ChatMessage
index *numberPosition in the rendered rows: drives the test id (user-message-{index} / assistant-message-{index}).
isLast *booleanThe last row: loader, streaming, docked approvals, always-visible actions.

ChatPartView

PropTypeDefaultDescription
part *ChatPart
message *ChatMessage
isLast *boolean
placement"inline" | "docked" | undefinedinline'docked' when you render a docked approval in your own footer. Default 'inline'.

ChatViewProvider

PropTypeDefaultDescription
statusChatStatus | undefinedreadyDefault 'ready'.
labelsPartial<ChatLabels> | undefined
iconsReadonly<Record<string, ReactNode>> | undefinedIcon key → element from src/icons; wins over the built-in Figma 483:1899 table.
markdownComponentType<ChatMarkdownProps> | undefinedAssistantMessageMarkdownThe 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) | undefinedISO → 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" | undefinedinline'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').
renderPartcomplexCalled 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.
renderStepContentcomplexA 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.
renderMessageActionscomplexThe actions row's children (Figma Wally order: Copy, ThumbsUp, ThumbsDown, Ellipsis, Timestamp). undefined = the default order; null = no row. No hooks inside.
onApprovalRespondcomplexAbsent → approval buttons are disabled.
onFeedbackcomplexAbsent → no thumbs. Clicking the pressed thumb sends null.
onCopy((message: ChatMessage, text: string) => void) | undefinedFired after Copy copied; Copy itself shows whenever the message has text.
onRetrycomplexAbsent → no Retry on error parts.