npx skills add ...
npx skills add assistant-ui/skills --skill primitives
Builds and customizes assistant-ui chat UI from composable, unstyled @assistant-ui/react primitives: ThreadPrimitive, ComposerPrimitive (with Unstable_TriggerPopover for mentions and slash commands), MessagePrimitive and MessagePartPrimitive, ActionBarPrimitive, BranchPickerPrimitive, AttachmentPrimitive, ErrorPrimitive, AssistantModalPrimitive, ChainOfThoughtPrimitive, SelectionToolbarPrimitive, SuggestionPrimitive, QueueItemPrimitive, and the ThreadList primitives. Use when assembling or styling a custom Thread, Composer, message list, action bar, branch picker, or suggestion grid; wiring composer input, dictation, quoting, or attachments; or handling editing, branching, virtualization, scrolling, or chain of thought UI. Covers MessagePrimitive.Parts render functions per part type, groupPartByType, AuiIf in selector form, and the unstyled gotchas (AssistantRuntimeProvider, className). For prebuilt UI use setup, for the styled catalog use elements, for multi-thread sidebars use thread-list.
npx skills add assistant-ui/skills --skill primitives
Always consult assistant-ui.com/llms.txt for the latest API.
Primitives are composable, unstyled components that follow Radix style part composition: a .Root provides context, .PartName children read it, and every part accepts asChild to merge its behavior onto your own element instead of rendering a wrapper. They ship with no styles and no opinions about layout, only behavior: keyboard shortcuts, auto scroll, streaming state, focus management, and disabled logic. Wrap your tree in AssistantRuntimeProvider (or a nested AuiProvider) before using any of them.
@ mentions, / slash commands, custom trigger matchersSuggestions([...]), the suggestion adapter, ThreadPrimitive.Suggestions| Primitive | Parts |
|---|---|
ThreadPrimitive | .Root, .Viewport, .ViewportProvider, .ViewportFooter, .Messages, .MessageByIndex, .Unstable_MessageById, .ScrollToBottom, .Suggestions, .SuggestionByIndex, .Suggestion, .Empty (deprecated), .If (deprecated) |
ComposerPrimitive | .Root, .Input, .Send, .Cancel, .AddAttachment, .Attachments, .AttachmentByIndex, .AttachmentDropzone, .Dictate, .StopDictation, .DictationTranscript, .Quote, .QuoteText, .QuoteDismiss, .Queue, .Unstable_TriggerPopoverRoot, .Unstable_TriggerPopover (with .Directive / .Action), .Unstable_TriggerPopoverCategories, .Unstable_TriggerPopoverCategoryItem, .Unstable_TriggerPopoverItems, .Unstable_TriggerPopoverItem, .Unstable_TriggerPopoverBack, .If (deprecated) |
MessagePrimitive | .Root, .Parts (.Content is a deprecated alias), .PartByIndex, .GroupedParts, .Unstable_PartsGrouped, .Unstable_PartsGroupedByParentId, .Attachments, .AttachmentByIndex, .Quote, .Error, .GenerativeUI, .If (deprecated) |
MessagePartPrimitive | .Text, .Image, .InProgress, .Messages |
ActionBarPrimitive | .Root, .Copy, .Reload, .Edit, .Speak, .StopSpeaking, .FeedbackPositive, .FeedbackNegative, .ExportMarkdown |
ActionBarMorePrimitive | .Root, .Trigger, .Content, .Item, .Separator |
BranchPickerPrimitive | .Root, .Previous, .Next, .Number, .Count |
AttachmentPrimitive | .Root, .Name, .Remove, .unstable_Thumb |
ErrorPrimitive | .Root, .Message |
AssistantModalPrimitive | .Root, .Trigger, .Content, .Anchor |
ChainOfThoughtPrimitive | .Root, .AccordionTrigger, .Parts (legacy, prefer MessagePrimitive.GroupedParts) |
SelectionToolbarPrimitive | .Root, .Quote |
SuggestionPrimitive | .Title, .Description, .Trigger |
QueueItemPrimitive | .Text, .Steer, .Remove |
ThreadListPrimitive | .Root, .New, .Items, .ItemByIndex, .LoadMore |
ThreadListItemPrimitive | .Root, .Trigger, .Title, .Archive, .Unarchive, .Delete |
ThreadListItemMorePrimitive | .Root, .Trigger, .Content, .Item, .Separator |
ThreadListPrimitive, ThreadListItemPrimitive, and ThreadListItemMorePrimitive drive multi-thread sidebars. This skill covers them only in the table above; see thread-list for the full custom UI, CRUD operations, and remote adapter walkthrough.
Reach for AuiIf for every new condition. It takes a selector over the full assistant state (thread, message, composer, part, attachment) instead of a fixed set of boolean props, and it replaces the deprecated .If that still ships on ThreadPrimitive, MessagePrimitive, and ComposerPrimitive.
AuiIf.Condition is exported for typing a condition function outside JSX. s.message and s.part are only populated inside a message or part context; s.thread and s.composer are always available.
A complete thread built from primitives: welcome suggestions, per role messages with an inline edit composer, an action bar, a branch picker, and a composer footer that swaps Send for Cancel mid run.
The assistant message's .Parts callback above handles all eight part types the pipeline can hand you: the three modality parts (text, image, file), the four provider channel parts (reasoning, source, tool-call, generative-ui), and the open ended data part. Returning null from the default case still lets a tool UI registered by name, or a data renderer registered by name, take over automatically; return <></> instead when you want to suppress that part entirely.
BranchPickerPrimitive reads branch state from the nearest MessagePrimitive.Root, so it must render inside one. Previous and Next auto disable at the boundaries (and while a run is in flight, unless the runtime supports switchBranchDuringRun); hideWhenSingleBranch removes the whole picker until a message actually has alternatives, which keeps the action row from jumping around on the common case of one branch.
A new branch appears when a user message is edited and resent, or when ActionBarPrimitive.Reload (or aui.message.reload()) regenerates an assistant message. See messages.md for programmatic branch navigation with aui.message.switchToBranch.
Primitive renders nothing, or a hook throws outside a provider
AssistantRuntimeProvider runtime={...} (from a runtime hook like useChatRuntime or useLocalRuntime), or nest an AuiProvider extends={aui} config={AuiConfig({...})} for an isolated scope.ActionBarPrimitive, BranchPickerPrimitive, and ErrorPrimitive additionally need a MessagePrimitive.Root ancestor; SelectionToolbarPrimitive needs a ThreadPrimitive.Root ancestor but must sit outside ThreadPrimitive.Viewport.Nothing is styled
<div>, <button>, <textarea>, ...) with no classes. Add className yourself, or pass asChild to merge behavior onto a styled component you already have.ThreadPrimitive.ViewportSlack no longer exists
MessagePrimitive.Root when Viewport has turnAnchor="top"; replace any fillClampThreshold / fillClampOffset customization with topAnchorMessageClamp on Viewport instead.MessagePrimitive.Content, .Empty, and every .If still work but are legacy
.Content is a deprecated alias for .Parts. ThreadPrimitive.Empty and every primitive's .If are superseded by AuiIf; keep them only in code you have not migrated yet.groupPartByType({ "mcp-app": [...] }) silently stops matching
"mcp-app" key was removed in 0.15. Use "standalone-tool-call", a superset that also matches any tool call whose registered UI opts into display: "standalone". See part-grouping.md.Unstable_ prefixed props, hooks, and primitives
unstable_useComposerInput, unstable_useComposerInputHistory, and unstable_useThreadMessageIds are explicitly unstable and can change without a major version bump. They are safe to build on, but pin the exact behavior you rely on before upgrading.create, init, add, and the rest of the CLI scaffolduseAui, useAuiState, AuiConfig, and the state every primitive readsrender entries that resolve into part.toolUI inside MessagePrimitive.PartsThreadListPrimitive and ThreadListItemPrimitive in depth<AuiIf condition={(s) => s.thread.isEmpty}>
<WelcomeScreen />
</AuiIf>
<AuiIf condition={(s) => s.thread.isRunning}>
<ComposerPrimitive.Cancel>Stop</ComposerPrimitive.Cancel>
</AuiIf>
<AuiIf
condition={(s) => s.message.role === "assistant" && s.message.status?.type === "complete"}
>
<FollowUpCard />
</AuiIf>
<AuiIf condition={(s) => s.composer.dictation != null}>
<ComposerPrimitive.StopDictation>Stop</ComposerPrimitive.StopDictation>
</AuiIf>function CustomThread() {
return (
<ThreadPrimitive.Root className="flex h-full flex-col">
<ThreadPrimitive.Viewport className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
<AuiIf condition={(s) => s.thread.isEmpty}>
<div className="flex flex-1 flex-col items-center justify-center gap-3">
<p>Ask me anything.</p>
<ThreadPrimitive.Suggestions>
{() => (
<SuggestionPrimitive.Trigger send className="rounded-lg border px-3 py-2">
<SuggestionPrimitive.Title />
</SuggestionPrimitive.Trigger>
)}
</ThreadPrimitive.Suggestions>
</div>
</AuiIf>
<ThreadPrimitive.Messages>
{({ message }) => {
if (message.role === "user") {
return message.composer.isEditing ? <EditComposer /> : <UserMessage />;
}
return <AssistantMessage />;
}}
</ThreadPrimitive.Messages>
<ThreadPrimitive.ViewportFooter className="sticky bottom-0 pt-2">
<ComposerPrimitive.Root className="flex items-end gap-2 rounded-2xl border bg-background p-2">
<ComposerPrimitive.Input
placeholder="Send a message..."
rows={1}
className="flex-1 resize-none bg-transparent px-2 py-1.5 focus:outline-none"
/>
<AuiIf condition={(s) => !s.thread.isRunning}>
<ComposerPrimitive.Send className="rounded-full bg-primary px-3 py-1.5 text-primary-foreground" />
</AuiIf>
<AuiIf condition={(s) => s.thread.isRunning}>
<ComposerPrimitive.Cancel className="rounded-full border px-3 py-1.5" />
</AuiIf>
</ComposerPrimitive.Root>
</ThreadPrimitive.ViewportFooter>
</ThreadPrimitive.Viewport>
</ThreadPrimitive.Root>
);
}
function UserMessage() {
return (
<MessagePrimitive.Root className="flex flex-col items-end gap-1">
<MessagePrimitive.Quote>
{({ text }) => <blockquote className="border-l pl-2 text-sm italic">{text}</blockquote>}
</MessagePrimitive.Quote>
<div className="max-w-[80%] rounded-2xl bg-primary px-4 py-2 text-primary-foreground">
<MessagePrimitive.Parts>
{({ part }) => {
if (part.type === "text") return <MessagePartPrimitive.Text />;
if (part.type === "image") return <MessagePartPrimitive.Image className="max-w-full rounded-lg" />;
return null;
}}
</MessagePrimitive.Parts>
</div>
<ActionBarPrimitive.Root hideWhenRunning autohide="not-last">
<ActionBarPrimitive.Edit>Edit</ActionBarPrimitive.Edit>
</ActionBarPrimitive.Root>
</MessagePrimitive.Root>
);
}
function EditComposer() {
return (
<MessagePrimitive.Root className="flex justify-end">
<ComposerPrimitive.Root className="w-[80%] rounded-2xl border p-2">
<ComposerPrimitive.Input className="w-full resize-none bg-transparent focus:outline-none" />
<div className="flex justify-end gap-2 pt-1">
<ComposerPrimitive.Cancel className="rounded-md px-2 py-1 text-sm">Cancel</ComposerPrimitive.Cancel>
<ComposerPrimitive.Send className="rounded-md bg-primary px-2 py-1 text-sm text-primary-foreground">
Save
</ComposerPrimitive.Send>
</div>
</ComposerPrimitive.Root>
</MessagePrimitive.Root>
);
}
function AssistantMessage() {
return (
<MessagePrimitive.Root className="flex flex-col items-start gap-1">
<div className="max-w-[80%] rounded-2xl bg-muted px-4 py-2">
<MessagePrimitive.Parts>
{({ part }) => {
switch (part.type) {
case "text":
return <p className="whitespace-pre-wrap"><MessagePartPrimitive.Text /></p>;
case "image":
return <MessagePartPrimitive.Image className="max-w-full rounded-lg" />;
case "file":
return (
<a href={part.data ?? part.url} download={part.filename} className="text-sm underline">
{part.filename ?? part.mimeType}
</a>
);
case "reasoning":
return (
<details className="text-sm text-muted-foreground">
<summary>Thinking</summary>
{part.text}
</details>
);
case "source":
return part.sourceType === "url" ? (
<a href={part.url} className="text-sm underline">{part.title ?? part.url}</a>
) : (
<span className="text-sm">{part.title}</span>
);
case "tool-call":
return part.toolUI ?? <div className="rounded-md border p-2 text-sm">{part.toolName}</div>;
case "data":
return part.dataRendererUI ?? null;
case "generative-ui":
return <MessagePrimitive.GenerativeUI />;
default:
return null; // registered tool and data UIs still render when you return null
}
}}
</MessagePrimitive.Parts>
</div>
<MessagePrimitive.Error>
<ErrorPrimitive.Root className="text-sm text-destructive">
<ErrorPrimitive.Message />
</ErrorPrimitive.Root>
</MessagePrimitive.Error>
<div className="flex items-center gap-2">
<BranchPickerPrimitive.Root hideWhenSingleBranch className="flex items-center gap-1 text-xs">
<BranchPickerPrimitive.Previous>←</BranchPickerPrimitive.Previous>
<span><BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /></span>
<BranchPickerPrimitive.Next>→</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>
<ActionBarPrimitive.Root hideWhenRunning autohide="not-last">
<ActionBarPrimitive.Copy>Copy</ActionBarPrimitive.Copy>
<ActionBarPrimitive.Reload>Regenerate</ActionBarPrimitive.Reload>
</ActionBarPrimitive.Root>
</div>
</MessagePrimitive.Root>
);
}<BranchPickerPrimitive.Root hideWhenSingleBranch className="inline-flex items-center gap-1">
<BranchPickerPrimitive.Previous>←</BranchPickerPrimitive.Previous>
<span><BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /></span>
<BranchPickerPrimitive.Next>→</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>