Skip to content
Component installs need the registry setup
VegaStack Design

Message Parts

What an agent's answer is made of beyond its text — steps, a plan, status, changes, sources, the disclaimer, actions, versions and a fold.

Status
stable
Since
0.41.0
Accessibility pattern
states in words; actions stay in the Tab order

Last updated

What did we decide about the 4000 K downlight?
Assistant

In the 28 Oct review you agreed to keep 4000 K for the IP65 range and test 3000 K samples by Friday.

Created task SR-41 Test 3000 K samples

Generated by AI · can make mistakes

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Message Parts before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify message-parts

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/message-parts

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/button, @vegastack/collapsible, @vegastack/disclosure-trigger, @vegastack/separator, @vegastack/spinner, @vegastack/status-line, @vegastack/tool-call-chip, @vegastack/use-overflow.

Usage

import { Citation } from "@/components/ui/cited-list";
import { Message, MessageContent } from "@/components/ui/message";
import {
  MessageActions,
  MessageDisclaimer,
  MessageSources,
  MessageStep,
  MessageSteps,
} from "@/components/ui/message-parts";

<Message>
  <MessageContent>
    <MessageSteps summary="2 steps · Searched meetings">
      <MessageStep label="Searched meetings" meta="2 results" />
      <MessageStep label="Read Range review 28 Oct" />
    </MessageSteps>
    <p>You agreed to keep 4000 K for the IP65 range.</p>
    <MessageSources>
      <Citation href="/meetings/28">Range review 28 Oct</Citation>
    </MessageSources>
    <MessageDisclaimer>Generated by AI · can make mistakes</MessageDisclaimer>
    <MessageActions>{/* Copy, Retry */}</MessageActions>
  </MessageContent>
</Message>;

Compose these inside MessageContent, next to the text. A person's message needs none of them: it is Bubble with align="end".

MessageSources takes Citation chips, which it does not bring with it: install cited-list too. An item the reader can't open is a Citation with render={<span />}.

The whole conversation

MessageScroller            role="log" aria-label="Messages"
├── Message                role="article", named "{author}, {time}"
│   └── MessageContent
│       ├── MessageStatus
│       ├── the text       aria-busy while it streams
│       └── the other parts
└── one useAnnouncer region, mounted for the conversation's life

Keep aria-busy on the text's own wrapper, never on Message: anything inside a busy subtree can go unannounced, and the status and any failure sit next to the text. Announce the status and "replied" through the one useAnnouncer region.

What did we decide about the 4000 K downlight?
Assistant

In the 28 Oct review you agreed to keep 4000 K for the IP65 range and test 3000 K samples by Friday.

Created task SR-41 Test 3000 K samples

Generated by AI · can make mistakes

Anatomy

MessageSteps — data-slot="message-steps" | "message-steps-content" | "message-steps-trigger"
MessageStep — data-slot="message-step" | "message-step-error"
MessagePlan — data-slot="message-plan" | "message-plan-item"
MessageStatus — data-slot="message-status"
MessageNotice — data-slot="message-notice"
MessageChange — data-slot="message-change" | "message-change-action"
MessageHandoff — data-slot="message-handoff"
MessageSources — data-slot="message-sources"
MessageDisclaimer — data-slot="message-disclaimer"
MessageActions — data-slot="message-actions"
MessageVersionSwitch — data-slot="message-version-switch" | "message-version-switch-position"
MessageFold — data-slot="message-fold" | "message-fold-content"

Examples

Status while it works

MessageStatus is the one line saying what the agent is doing now. Replace its text as the agent moves on and remove it when the answer's text starts. The text below it is aria-busy until the turn ends, so a screen reader doesn't read it word by word.

Assistant
Reading 2 meeting notes…

In the 28 Oct review you agreed to keep

Steps

Closed by default, so the answer stays the first thing to read. A failed step keeps its reason in muted text under the chip, and a step that was running when the turn was stopped says so.

  • Done:Searched the web4 results
  • Failed:Opened acme.comThe site didn't answer.
  • Running:Reading datasheet.pdf…
  • Stopped:Compared the rates

Plan

A checklist the agent edits in place. Pass the whole list on every update.

  1. Done:Read the BOQ sheet
  2. Done:Match lines 1–9
  3. In progress:Match lines 10–20
  4. Failed:Fetch the project rates
  5. Stopped:Check the totals
  6. To do:Update the rows

Changes and hand-offs

One MessageChange per thing the agent changed, with Undo at the end of the row. Past a few, fold them into one row that opens the full list. MessageHandoff marks where another agent takes over.

Created task SR-41 Test 3000 K samples
12 changes
Assistant asked BOQ processor

Notices and failures

MessageNotice is a quiet line with one action. A failed turn is MessageStatus with status="error" and a Retry. Neither is a live region, so reopening a chat stays silent; pass live to the one that just appeared because of what the person did.

1 more source is private to you.
The answer stopped partway.

Versions

Shows once an answer has been retried, so the earlier one stays within reach. It renders nothing for a single version. formatPosition can say more: Previous answer (1 of 2).

2 of 2

Long messages

MessageFold holds a long message to 20rem with a fade and Show more. It measures, so content that fits is left alone. Open a turn streamed in this session with defaultOpen: only history folds, and an answer never shuts while someone is reading it.

Line 1 of the BOQ matched a 4000 K IP65 downlight.

Line 2 of the BOQ matched a 4000 K IP65 downlight.

Line 3 of the BOQ matched a 4000 K IP65 downlight.

Line 4 of the BOQ matched a 4000 K IP65 downlight.

Line 5 of the BOQ matched a 4000 K IP65 downlight.

Line 6 of the BOQ matched a 4000 K IP65 downlight.

Line 7 of the BOQ matched a 4000 K IP65 downlight.

Line 8 of the BOQ matched a 4000 K IP65 downlight.

Line 9 of the BOQ matched a 4000 K IP65 downlight.

Line 10 of the BOQ matched a 4000 K IP65 downlight.

Line 11 of the BOQ matched a 4000 K IP65 downlight.

Line 12 of the BOQ matched a 4000 K IP65 downlight.

Line 13 of the BOQ matched a 4000 K IP65 downlight.

Line 14 of the BOQ matched a 4000 K IP65 downlight.

Line 15 of the BOQ matched a 4000 K IP65 downlight.

Line 16 of the BOQ matched a 4000 K IP65 downlight.

API Reference

PropTypeDefaultDescription
summary*React.ReactNode—The collapsed line: "3 steps · Searched meetings, read 2 notes".
classNamestring—Classes for the root.

Data attributes and CSS variables on MessageSteps

AttributeValues
data-slot"message-steps" | "message-steps-content" | "message-steps-trigger"
PropTypeDefaultDescription
label*React.ReactNode—What the step does or did: "Searching tasks…", "Searched tasks".
errorReact.ReactNode—Why a failed step failed, under the chip in muted text.
metaReact.ReactNode—A short result after the label: "2 results".
statusMessageStepStatus"done"
statusLabelsPartial<Record<MessageStepStatus, string>>{ running: "Running", done: "Done", failed: "Failed", stopped: "Stopped" }The words a screen reader hears for each status, since the icon is what shows it.

Data attributes and CSS variables on MessageStep

AttributeValues
data-slot"message-step" | "message-step-error"
data-statusmirrors a prop or state value
PropTypeDefaultDescription
items*readonly MessagePlanItem[]—The checklist, in order; the agent updates it in place.
stateLabelsPartial<Record<MessagePlanItemState, string>>{ todo: "To do", doing: "In progress", done: "Done", failed: "Failed", stopped: "Stopped" }The words a screen reader hears for each state, since the icon is what shows it.

Data attributes and CSS variables on MessagePlan

AttributeValues
data-slot"message-plan" | "message-plan-item"
data-statemirrors a prop or state value
PropTypeDefaultDescription
actionReact.ReactNode—The row's action, at its end: a small Button such as Undo.
iconReact.ReactNode—Replaces the check.

Data attributes and CSS variables on MessageChange

AttributeValues
data-slot"message-change" | "message-change-action"
PropTypeDefaultDescription
labelstring"Sources"The word before the chips, which also names the group.

Data attributes and CSS variables on MessageSources

AttributeValues
data-slot"message-sources"
PropTypeDefaultDescription
labelstring"Message actions"Names the group of buttons.
reveal"always" | "hover""hover"hover — shown while the pointer is over the message or focus is inside it, and always on a touch screen. always — for the last message, where the next step is likely one of these.

Data attributes and CSS variables on MessageActions

AttributeValues
data-revealmirrors a prop or state value
data-slot"message-actions"
PropTypeDefaultDescription
current*number—The version on screen, counting from 1.
total*number—How many versions there are.
formatPosition((current: number, total: number) => string)(current, total) => `${current} of ${total}`The position between the buttons.
labelstring"Versions"Names the control.
nextLabelstring"Next version"The › button's name.
onNext(() => void)—Go to the later version.
onPrevious(() => void)—Go to the earlier version.
previousLabelstring"Previous version"The ‹ button's name.

Data attributes and CSS variables on MessageVersionSwitch

AttributeValues
data-slot"message-version-switch" | "message-version-switch-position"
PropTypeDefaultDescription
defaultOpenbooleanfalseStart with the whole message showing. A turn streamed in this session starts open, so an answer never folds up while someone is reading it; only history folds.
onOpenChange((open: boolean) => void)—Called when Show more or Show less is pressed, or focus reaches folded content.
openboolean—Controlled: the whole message shows. Pair with onOpenChange.
showLessLabelstring"Show less"
showMoreLabelstring"Show more"

Data attributes and CSS variables on MessageFold

AttributeValues
data-folded""
data-slot"message-fold" | "message-fold-content"

MessageStatus and MessageNotice take StatusLine's props.

Accessibility

  • No part is a live region unless you pass live. A conversation is role="log", so a part that also announced itself would be read twice.
  • A step's status and a plan item's state are read out from statusLabels and stateLabels, since the icon alone shows them.
  • MessageActions keeps its buttons in the Tab order while hidden: focus reveals them.
  • Tabbing to a link inside a folded message opens the fold.
KeyAction
TabMove through the steps line, links, actions and versions
Enter / SpaceOpen the steps, press an action, Show more or Show less
ContractStates tested
Behavioursteps-closed, steps-open, step-running, step-done, step-failed, step-stopped, plan-todo, plan-doing, plan-done, plan-failed, plan-stopped, status-progress, status-error, notice, change, handoff, actions-hover, actions-always, version-ends, single-version, folded, unfolded, fits, focus-opens-fold
Accessibilitynamed-groups, state-in-words, silent-by-default, live-opt-in, browser-accessibility-test
Visualturn, streaming, steps, plan, changes, notices, versions, fold

Do / Don't

Do
Name the agent and the action in the words you pass: 'Created task SR-41'.
Don't
Show a prompt, a token count or a price in a step's meta.

On this page