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.
Usage
Examples
With reasoning
The reasoning sits above the answer as a ChainOfThought, 12px away. The first message
keeps it collapsed; the second opens the "Thought for 5s" step.
With actions visible
The latest answer forces its actions on with visible: Copy, thumbs up and down, a more
menu and the timestamp, with the first icon lined up with the text.
With approval
The full composition: reasoning, an ApprovalCard stretched to the message width, the
answer and its actions. The card sits 12px from the parts around it, like the reasoning.
With actions on hover
Older answers keep their actions row transparent until the message is hovered or holds focus; the 30px row stays reserved so nothing jumps.
Streaming
While the answer streams, set aria-busy on the message and show the running step with a
shimmering label; the actions come once it finishes.
Loading
Before the first output, the message holds only an AssistantMessageLoader: the Figma
"Working..." row with a shimmering label, announced as role='status'. Pass children to
change the text.
Error state
A failed answer: AssistantMessageError comes after whatever content streamed, announced
as role='alert'. Put your own Retry Button in action, so its click and analytics are
yours. The action renders inside the error's test-id cascade, but Button doesn't read it:
either give it the full id yourself (here error--error--retry, i.e. the error's id error--error
plus --retry), or read useTestId('retry') (exported from the package) inside your own
Retry component to derive it. Not in Figma yet; pending design review.
Analytics
The message and its content are not interactive, but data-analytics-* still lands on
their real nodes; put click analytics on each MessageAction instead.
Markdown
AssistantMessageMarkdown inside AssistantMessageContent: every GFM element in the DS look —
heading, bold/italic/strike, inline code (click to copy), an aligned table, nested lists, a task
list, a fenced block with a copy button, a quote, a rule and links. Highlighting follows the
app's CodeSnippetAdapterProvider (WallyAssistant mounts the lazy Prism one itself).
Markdown streaming
Pass the whole text so far (a snapshot, never a delta) with streaming: only the unfinished
tail is repaired, a dangling **, a half-typed table header and an empty fence wait a frame,
and an open fence stays plain without a copy button until it closes. Finished blocks never
re-render. Here a timer grows the string; press Replay to stream it again.
Markdown in chain of thought
variant='chain-of-thought' for reasoning inside a ChainOfThoughtStep (Figma 494:4292): the
step body sets the 12/16 muted text, inline code is muted and not copyable, links are 12px.
Markdown unsafe content
Assistant text is untrusted (prompt injection). Raw HTML shows as typed (only <br> breaks a
line), javascript:/data:/vbscript:/file: links render as plain text, and images show
only their alt text, so no tracking pixel loads.
Markdown custom elements
components overrides any element at module scope; wrap the exported parts
(AssistantMessageMarkdownLink, …InlineCode, …CodeBlock, …Table) to add analytics
attributes to the real nodes. Here every link gets a data-analytics-id, and images render
only from the app's own origin — the app owns that decision.