Radix UI Primitives

MessagePartPrimitive

Primitives for text, images, tool calls, and other message content.

Each message can have any number of message parts. Message parts are usually one of text, reasoning, audio, tool-call, or data.

Message part Types

Text

Standard text content, used for both user and assistant messages.

Reasoning

Exposes the assistant's reasoning process, showing how it arrived at its responses. This is typically used only in assistant messages.

Audio

Audio content that can be played back.

Tool Call

Interactive elements that represent tool operations.

Data

Custom data events that can be rendered as UI at their position in the message stream. Each data part has a name and a data payload.

You can use either the explicit format { type: "data", name: "workflow", data: {...} } or the shorthand data-* prefixed format { type: "data-workflow", data: {...} }. The prefixed format is automatically converted to a DataMessagePart (stripping the data- prefix as the name). Unknown message part types that don't match any built-in type are silently skipped with a console warning.

Streaming Data Parts

Data parts can be sent from the server using appendData() on the stream controller:

controller.appendData({
  type: "data",
  name: "chart",
  data: { labels: ["Q1", "Q2"], values: [10, 20] },
});

Register a renderer with makeAssistantDataUI to display data parts:

import { makeAssistantDataUI } from "@assistant-ui/react";

const ChartUI = makeAssistantDataUI({
  name: "chart",
  render: ({ data }) => <MyChart data={data} />,
});

Anatomy

import { MessagePartPrimitive } from "@assistant-ui/react";

const TextMessagePart = () => {
  return <MessagePartPrimitive.Text />;
};

Primitives

Plain Text

import { MessagePartPrimitive } from "@assistant-ui/react";

<MessagePartPrimitive.Text />;

Markdown Text

Renders the message's text as Markdown.

import { MarkdownTextPrimitive } from "@assistant-ui/react-markdown";

<MarkdownTextPrimitive />;

Audio

Coming soon.

InProgress

Renders children only if the message part is in progress.

import { MessagePartPrimitive } from "@assistant-ui/react";

<MessagePartPrimitive.InProgress>
  <LoadingIndicator />
</MessagePartPrimitive.InProgress>;

Tool UI

You can map tool calls to UI components. We provide a few utility functions to make this easier, such as makeAssistantToolUI.

const MyWeatherToolUI = makeAssistantToolUI({
  toolName: "get_weather",
  render: function MyWeatherToolUI({ args, result }) {
    return (
      <div className="mb-4 flex flex-col items-center">
        <pre className="whitespace-pre-wrap break-all text-center">
          get_weather({JSON.stringify(args)})
        </pre>
        {result !== undefined && (
          <pre className="whitespace-pre-wrap break-all text-center">
            {JSON.stringify(result)}
          </pre>
        )}
      </div>
    );
  },
});

Data UI

You can map data events to UI components, similar to tool UIs. There are two approaches:

Inline configuration

Pass a data config to MessagePrimitive.Parts:

<MessagePrimitive.Parts>
  {({ part }) => {
    if (part.type === "data" && part.name === "my_chart") return <ChartComponent data={part.data} />;
    if (part.type === "data") return <pre>{JSON.stringify(part.data, null, 2)}</pre>;
    return null;
  }}
</MessagePrimitive.Parts>

Global registration

Use makeAssistantDataUI or useAssistantDataUI to register data UIs globally. Global registrations take priority over inline configuration.

import { makeAssistantDataUI } from "@assistant-ui/react";

const MyChartUI = makeAssistantDataUI({
  name: "my_chart",
  render: ({ name, data }) => <ChartComponent data={data} />,
});

// Place inside AssistantRuntimeProvider
function App() {
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <Thread />
      <MyChartUI />
    </AssistantRuntimeProvider>
  );
}

The hook variant allows access to component state:

import { useAssistantDataUI } from "@assistant-ui/react";

function MyComponent() {
  useAssistantDataUI({
    name: "my_chart",
    render: ({ name, data }) => <ChartComponent data={data} />,
  });
  return null;
}

Each data component receives the full data part as props: { type: "data", name: string, data: T, status: MessagePartStatus }.

Messages (Sub-Agent)

Renders nested messages from a tool call part's messages field. This is used in multi-agent setups where a sub-agent's conversation is embedded inside a tool call.

import { MessagePartPrimitive } from "@assistant-ui/react";

<MessagePartPrimitive.Messages>
  {({ message }) => {
    if (message.role === "user") return <MyUserMessage />;
    return <MyAssistantMessage />;
  }}
</MessagePartPrimitive.Messages>;

This primitive must be used inside a tool call part context (e.g. inside a makeAssistantToolUI render function). It reads the messages field from the current ToolCallMessagePart and renders them in a readonly thread context.

Parent tool UI registrations are inherited — tools registered via makeAssistantToolUI at the parent level are available inside sub-agent messages.

See the Multi-Agent Guide for detailed usage.

Context Provider

Message part context is provided by MessagePrimitive.Parts or TextMessagePartProvider

MessagePrimitive.Parts

import { MessagePrimitive } from "@assistant-ui/react";

<MessagePrimitive.Parts>
  {({ part }) => {
    if (part.type === "text") return <MyText />;
    if (part.type === "reasoning") return <MyReasoning {...part} />;
    if (part.type === "audio") return <MyAudio {...part} />;
    if (part.type === "tool-call" && part.toolName === "get_weather") return <MyWeatherToolUI {...part} />;
    if (part.type === "tool-call") return <MyFallbackToolUI {...part} />;
    if (part.type === "data" && part.name === "my_chart") return <MyChartComponent {...part} />;
    if (part.type === "data") return <GenericDataComponent {...part} />;
    return null;
  }}
</MessagePrimitive.Parts>;

TextMessagePartProvider

This is a helper context provider to allow you to reuse the message part components outside a message part.

import { TextMessagePartProvider } from "@assistant-ui/react";

<TextMessagePartProvider text="Hello world" isRunning={false}>
  <MessagePartPrimitive.Text />
</TextMessagePartProvider>;

Runtime API

useAui (Message Part Actions)

import { useAui } from "@assistant-ui/react";

const aui = useAui();
aui.part().addToolResult(result);
const partState = aui.part().getState();
MessagePartRuntime
addToolResult: (result: any) => void

resumeToolCall: (payload: unknown) => void

path: MessagePartRuntimePath

getState: () => MessagePartState

subscribe: (callback: () => void) => Unsubscribe

useAuiState (Message Part State)

import { useAuiState } from "@assistant-ui/react";

const type = useAuiState((s) => s.part.type);
const status = useAuiState((s) => s.part.status);
TextMessagePartState
type: "text"

text: string

parentId?: string

status: { readonly type: "running"; } | { readonly type: "complete"; } | { readonly type: "incomplete"; readonly reason: "length" | "cancelled" | "content-filter" | "other" | "error"; readonly error?: unknown; } | { readonly type: "requires-action"; readonly reason: "interrupt"; }

AudioMessagePartState
type: "audio"

audio: { readonly data: string; readonly format: "mp3" | "wav"; }

status: { readonly type: "running"; } | { readonly type: "complete"; } | { readonly type: "incomplete"; readonly reason: "length" | "cancelled" | "content-filter" | "other" | "error"; readonly error?: unknown; } | { readonly type: "requires-action"; readonly reason: "interrupt"; }

ToolCallMessagePartState
type: "tool-call"

toolCallId: string

toolName: string

args: ReadonlyJSONObject

result?: unknown

isError?: boolean | undefined

argsText: string

artifact?: unknown

interrupt?: { type: "human"; payload: unknown; }

parentId?: string

messages?: readonly ThreadMessage[]

status: { readonly type: "running"; } | { readonly type: "complete"; } | { readonly type: "incomplete"; readonly reason: "length" | "cancelled" | "content-filter" | "other" | "error"; readonly error?: unknown; } | { readonly type: "requires-action"; readonly reason: "interrupt"; }

useMessagePartText

import { useMessagePartText } from "@assistant-ui/react";

const { text, status } = useMessagePartText();
TextMessagePartState
type: "text"

text: string

parentId?: string

status: { readonly type: "running"; } | { readonly type: "complete"; } | { readonly type: "incomplete"; readonly reason: "length" | "cancelled" | "content-filter" | "other" | "error"; readonly error?: unknown; } | { readonly type: "requires-action"; readonly reason: "interrupt"; }