# Context API URL: /docs/guides/context-api Read and update assistant state to build custom components. *** title: Context API description: Read and update assistant state to build custom components. ------------------------------------------------------------------------ The Context API provides direct access to assistant-ui's state management system, enabling you to build custom components that integrate seamlessly with the assistant runtime. ## Introduction The Context API is assistant-ui's powerful state management system that enables you to build custom components with full access to the assistant's state and capabilities. It provides: * **Reactive state access** - Subscribe to state changes with automatic re-renders * **Action execution** - Trigger operations like sending messages or reloading responses * **Event listening** - React to user interactions and system events * **Scope-aware design** - Components automatically know their context (message, thread, etc.) It's the foundation that powers all assistant-ui primitives. When the built-in components don't meet your needs, you can use the Context API to create custom components with the same capabilities. The Context API is backed by the runtime you provide to ``. This runtime acts as a unified store that manages all assistant state, handles actions, and dispatches events across your entire application. ## Core Concepts ### Scopes and Hierarchy assistant-ui organizes state into **scopes** - logical boundaries that provide access to relevant data and actions. Each scope corresponds to a specific part of the chat interface and automatically provides context-aware functionality. ``` 🗂️ ThreadList (threads) - Manages the list of conversations ├── 📄 ThreadListItem (threadListItem) - Individual thread in the list └── 💬 Thread (thread) - Active conversation with messages ├── 🔵 Message (message) - User or assistant message │ ├── 📝 Part (part) - Content within a message (text, tool calls, etc.) │ ├── 📎 Attachment (attachment) - Files attached to messages │ └── ✏️ Composer (composer) - Edit mode for existing messages │ └── 📎 Attachment (attachment) - Files in edit mode └── ✏️ Composer (composer) - New message input └── 📎 Attachment (attachment) - Files being added 🔧 Tools (tools) - Custom UI components for tool calls ``` **How scopes work:** * Scopes are **automatically determined** by where your component is rendered * A button inside a `` automatically gets `message` scope * A button inside a `` automatically gets `attachment` scope * Child scopes can access parent scope data (e.g., a `message` component can access `thread` data) ```tsx // Inside a message component function MessageButton() { // ✅ Available: message scope (current message) const role = useAuiState(({ message }) => message.role); // ✅ Available: thread scope (parent) const isRunning = useAuiState(({ thread }) => thread.isRunning); } ``` ### State Management Model The Context API follows a predictable state management pattern: 1. **State** is immutable and flows down through scopes 2. **Actions** are methods that trigger state changes 3. **Events** notify components of state changes and user interactions 4. **Subscriptions** let components react to changes ## Essential Hooks ### useAuiState Read state reactively with automatic re-renders when values change. This hook works like Zustand's selector pattern - you provide a function that extracts the specific data you need, and your component only re-renders when that data changes. ```tsx import { useAuiState } from "@assistant-ui/react"; // Basic usage - extract a single property const role = useAuiState(({ message }) => message.role); // "user" | "assistant" const isRunning = useAuiState(({ thread }) => thread.isRunning); // boolean // Access nested data const attachmentCount = useAuiState( ({ composer }) => composer.attachments.length, ); const lastMessage = useAuiState(({ thread }) => thread.messages.at(-1)); ``` The selector function receives all available scopes for your component's location and should return a specific value. The component re-renders only when that returned value changes. **Common patterns:** ```tsx // Access multiple scopes const canSend = useAuiState( ({ thread, composer }) => !thread.isRunning && composer.text.length > 0, ); // Compute derived state const messageCount = useAuiState(({ thread }) => thread.messages.length); ``` **Important:** Never create new objects in selectors. Return primitive values or stable references to avoid infinite re-renders. ```tsx // ❌ Bad - creates new object every time const data = useAuiState(({ message }) => ({ role: message.role, content: message.content, })); // ✅ Good - returns stable values const role = useAuiState(({ message }) => message.role); const content = useAuiState(({ message }) => message.content); ``` ### useAui Access the API instance for imperative operations and actions. Unlike `useAuiState`, this hook returns a stable object that never changes, making it perfect for event handlers and imperative operations. ```tsx import { useAui } from "@assistant-ui/react"; function CustomMessageActions() { const aui = useAui(); // Perform actions in event handlers const handleSend = () => { aui.composer().send(); }; const handleReload = () => { aui.message().reload(); }; // Read state imperatively when needed const handleConditionalAction = () => { const { isRunning } = aui.thread().getState(); const { text } = aui.composer().getState(); if (!isRunning && text.length > 0) { aui.composer().send(); } }; return (
); } ``` The API object is stable and doesn't cause re-renders. Use it for: * **Triggering actions** in event handlers and callbacks * **Reading current state** imperatively when you don't need subscriptions * **Accessing nested scopes** programmatically * **Checking scope availability** before performing actions **Available actions by scope:** ```tsx // Thread actions aui.thread().append(message); aui.thread().startRun(config); aui.thread().cancelRun(); aui.thread().switchToNewThread(); aui.thread().switchToThread(threadId); aui.thread().getState(); aui.thread().message(idOrIndex); aui.thread().composer; // Message actions aui.message().reload(); aui.message().speak(); aui.message().stopSpeaking(); aui.message().submitFeedback({ type: "positive" | "negative" }); aui.message().switchToBranch({ position, branchId }); aui.message().getState(); aui.message().part(indexOrToolCallId); aui.message().composer; // Part actions api.part().addResult(result); api.part().getState(); // Composer actions aui.composer().send(); aui.composer().setText(text); aui.composer().setRole(role); aui.composer().addAttachment(file); aui.composer().clearAttachments(); aui.composer().reset(); aui.composer().getState(); // Attachment actions aui.attachment().remove(); aui.attachment().getState(); // ThreadList actions aui.threads().switchToNewThread(); aui.threads().switchToThread(threadId); aui.threads().getState(); // ThreadListItem actions aui.threadListItem().switchTo(); aui.threadListItem().rename(title); aui.threadListItem().archive(); aui.threadListItem().unarchive(); aui.threadListItem().delete(); aui.threads().getState(); // Tools actions aui.tools().setToolUI(toolName, render); aui.tools().getState(); ``` ### useAuiEvent Subscribe to events with automatic cleanup on unmount. This hook is perfect for reacting to user interactions, system events, or integrating with external analytics. ```tsx import { useAuiEvent } from "@assistant-ui/react"; // Listen to current scope events (most common) useAuiEvent("composer.send", (event) => { console.log("Current composer sent message:", event.message); }); // Listen to all events of a type across all scopes useAuiEvent({ event: "composer.send", scope: "*" }, (event) => { console.log("Any composer sent a message:", event); }); // Listen to ALL events (useful for debugging or analytics) useAuiEvent("*", (event) => { console.log("Event occurred:", event.type, "from:", event.source); // Send to analytics, logging, etc. }); // Practical example: Track user interactions function AnalyticsTracker() { useAuiEvent("composer.send", (event) => { analytics.track("message_sent", { messageLength: event.message.content.length, hasAttachments: event.message.attachments.length > 0, }); }); return null; // This component only tracks events } ``` **Event name patterns:** * Event names follow `source.action` format in camelCase (e.g., `composer.send`, `thread.runStart`) * Use `"*"` as the event name to listen to all events * The `scope` parameter controls which instances trigger the event ## Working with Scopes ### Available Scopes Each scope provides access to specific state and actions: * **ThreadList** (`threads`): Collection and management of threads * **ThreadListItem** (`threadListItem`): Individual thread in the list * **Thread** (`thread`): Conversation with messages * **Message** (`message`): Individual message (user or assistant) * **Part** (`part`): Content part within a message (text, tool calls, etc.) * **Composer** (`composer`): Text input for sending or editing messages * **Attachment** (`attachment`): File or media attached to a message or composer * **Tools** (`tools`): Tool UI components ### Scope Resolution The Context API automatically resolves the current scope based on component location: ```tsx function MessageButton() { const aui = useAui(); // Automatically uses the current message scope const handleReload = () => { aui.message().reload(); }; return ; } ``` ### Checking Scope Availability Before accessing a scope, check if it's available: ```tsx const aui = useAui(); // Check if message scope exists if (api.message.source) { // Safe to use message scope const { role } = aui.message().getState(); } ``` ### Accessing Nested Scopes Navigate through the scope hierarchy programmatically: ```tsx const aui = useAui(); // Access specific message by ID or index const messageById = aui.thread().message({ id: "msg_123" }); const messageByIndex = aui.thread().message({ index: 0 }); // Access part by index or tool call ID const partByIndex = aui.message().part({ index: 0 }); const partByToolCall = aui.message().part({ toolCallId: "call_123" }); // Access attachment by index const attachment = aui.composer().attachment({ index: 0 }).getState(); // Access thread from thread list const thread = aui.threads().thread("main"); const threadItem = aui.threads().item({ id: "thread_123" }); ``` ## Common Patterns ### Conditional Rendering ```tsx function RunIndicator() { const isRunning = useAuiState(({ thread }) => thread.isRunning); if (!isRunning) return null; return
Assistant is thinking...
; } ``` ### Custom Action Buttons ```tsx function CopyButton() { const aui = useAui(); const handleCopy = () => { navigator.clipboard.writeText(aui.message().getCopyText()); }; return ; } ``` ### State-Aware Components ```tsx function SmartComposer() { const aui = useAui(); const isRunning = useAuiState(({ thread }) => thread.isRunning); const text = useAuiState(({ composer }) => composer.text); const canSend = !isRunning && text.length > 0; return (