# Assistant Context API
URL: /docs/guides/context-api

Read and update assistant state to build custom React components in your chat UI — composable context API for thread, message, and runtime data via assistant-ui.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

The Context API gives custom components access to the state, actions, and events of the assistant runtime.

## Introduction

The Context API 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 resolve their context (message, thread, etc.) from where they render

All assistant-ui primitives are built on it. When the built-in components don't meet your needs, use the Context API to build custom components with the same capabilities.

The Context API is backed by the runtime you provide to `<AssistantRuntimeProvider>`. The runtime is the store that holds assistant state, handles actions, and dispatches events.

## Core Concepts

### Scopes and Hierarchy

assistant-ui organizes state into **scopes**. Each scope corresponds to a part of the chat interface and exposes the state and actions for that part.

```
🗂️  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.)
        │   ├── 🧠 ChainOfThought (chainOfThought) - Reasoning steps within a message
        │   │   └── 📝 Part (part) - Individual reasoning/tool-call step
        │   ├── 📎 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

💡 Suggestions (suggestions) - Follow-up message suggestions
    └── 💬 Suggestion (suggestion) - Individual suggestion item
🔧 Tools (tools) - Custom UI components for tool calls
🧩 ModelContext (modelContext) - Model context and tool registration
```

**How scopes work:**

- Scopes are determined by where your component is rendered
- A button inside a `<ThreadPrimitive.Messages>` gets `message` scope
- A button inside a `<ComposerPrimitive.Attachments>` gets `attachment` scope
- Child scopes can access parent scope data (e.g., a `message` component can access `thread` data)

```
// Inside a message component
function MessageButton() {
  // ✅ Available: message scope (current message)
  const role = useAuiState((s) => s.message.role);

  // ✅ Available: thread scope (parent)
  const isRunning = useAuiState((s) => s.thread.isRunning);
}
```

### State Management Model

The Context API separates four concerns:

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 and re-render when the selected value changes. This hook works like Zustand's selector pattern: you provide a function that extracts the data you need, and your component re-renders only when that data changes.

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

// Basic usage - extract a single property
const role = useAuiState((s) => s.message.role); // "user" | "assistant"
const isRunning = useAuiState((s) => s.thread.isRunning); // boolean

// Access nested data
const attachmentCount = useAuiState(
  (s) => s.composer.attachments.length,
);
const lastMessage = useAuiState((s) => s.thread.messages.at(-1));
```

The selector function receives all scopes available at your component's location.

**Common patterns:**

```
// Access multiple scopes
const canSend = useAuiState(
  (s) => !s.thread.isRunning && s.composer.text.length > 0,
);

// Compute derived state
const messageCount = useAuiState((s) => s.thread.messages.length);
```

**Important:** Never create new objects in selectors. Return primitive values or stable references to avoid infinite re-renders.

```
// ❌ Bad - creates new object every time
const data = useAuiState((s) => ({
  role: s.message.role,
  content: s.message.content,
}));

// ✅ Good - returns stable values
const role = useAuiState((s) => s.message.role);
const content = useAuiState((s) => s.message.content);
```

### useAui

Access the API instance for imperative operations and actions. Unlike `useAuiState`, this hook does not subscribe to state updates. The client's identity changes only on structural changes (a scope resolving to a different instance), so it suits event handlers and imperative operations.

```
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 (
    <div>
      <button onClick={handleSend}>Send</button>
      <button onClick={handleReload}>Reload</button>
      <button onClick={handleConditionalAction}>Smart Send</button>
    </div>
  );
}
```

Use the API object 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:**

```
// Thread actions
aui.thread.append(message);
aui.thread.startRun(config);
aui.thread.resumeRun(config);
aui.thread.cancelRun();
aui.thread.loadEarlier();
aui.thread.getState();
aui.thread.message({ index: idx });
aui.thread.message({ id: messageId });
aui.thread.composer();

// Message actions
aui.message.reload();
aui.message.speak();
aui.message.stopSpeaking();
aui.message.submitFeedback({ type: "positive" });
aui.message.submitFeedback({ type: "negative", comment: "Quoted the wrong date" });
aui.message.switchToBranch({ position, branchId });
aui.message.getState();
aui.message.part({ index: idx });
aui.message.part({ toolCallId });
aui.message.composer();

// Part actions
aui.part.addToolResult(result);
aui.part.resumeToolCall(result);
aui.part.getState();

// Composer actions
aui.composer.send();
aui.composer.setText(text);
aui.composer.setRole(role);
aui.composer.addAttachment(file); // File object
aui.composer.addAttachment({ name, content }); // external source
await aui.composer.clearAttachments();
await 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.reload();
await aui.threads.getLoadThreadsPromise();
aui.threads.getState();

// ThreadListItem actions
aui.threadListItem.switchTo();
aui.threadListItem.rename(title);
aui.threadListItem.updateCustom(custom);
aui.threadListItem.archive();
aui.threadListItem.unarchive();
aui.threadListItem.delete();
aui.threadListItem.getState();

// Suggestions actions
aui.suggestions.getState();
aui.suggestions.suggestion({ index: 0 });

// Suggestion actions
aui.suggestion.getState();

// ChainOfThought actions
aui.chainOfThought.getState();
aui.chainOfThought.setCollapsed(collapsed);
aui.chainOfThought.part({ index: 0 });

// ModelContext actions — see /docs/copilots/model-context for full usage
aui.modelContext.register(provider);
aui.modelContext.getState();

// Tools actions
aui.tools.setToolUI(toolName, render);
aui.tools.getState();
```

### useAuiEvent

Subscribe to events. The subscription is removed on unmount. Use this hook to run side effects on user interactions and system events, such as analytics tracking.

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

// Listen to current scope events (most common)
useAuiEvent("composer.send", (event) => {
  console.log("Composer sent message in thread:", event.threadId);
});

// Listen to thread events
useAuiEvent("thread.modelContextUpdate", (event) => {
  console.log("Model context updated in thread:", event.threadId);
});

// Listen to all events of a type across all scopes
useAuiEvent({ event: "composer.send", scope: "*" }, (event) => {
  console.log("Any composer sent a message:", event.threadId);
});

// Listen to ALL events (useful for debugging or analytics)
useAuiEvent("*", (event) => {
  console.log("Event occurred:", event.event, event.payload);
});

// Practical example: Track user interactions
function AnalyticsTracker() {
  useAuiEvent("composer.send", (event) => {
    analytics.track("message_sent", {
      threadId: event.threadId,
    });
  });

  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
- **Task** (`task`): Tool call in the thread that carries a nested conversation
- **Message** (`message`): Individual message (user or assistant)
- **Part** (`part`): Content part within a message (text, tool calls, etc.)
- **ChainOfThought** (`chainOfThought`): Reasoning steps grouped within a message
- **Composer** (`composer`): Text input for sending or editing messages
- **QueueItem** (`queueItem`): Message queued in a composer
- **Attachment** (`attachment`): File or media attached to a message or composer
- **Suggestions** (`suggestions`): Collection of follow-up message suggestions
- **Suggestion** (`suggestion`): Individual follow-up suggestion
- **Tools** (`tools`): Tool UI components
- **ModelContext** (`modelContext`): Model context and tool registration

### Scope Resolution

The Context API resolves the current scope from the component's location:

```
function MessageButton() {
  const aui = useAui();

  // Automatically uses the current message scope
  const handleReload = () => {
    aui.message.reload();
  };

  return <button onClick={handleReload}>Reload</button>;
}
```

### Checking Scope Availability

Read scopes that may be unavailable through the optional view, where they resolve to `undefined` instead of throwing:

```
const role = useAuiState((s) => s.optional.message?.role);
```

### Accessing Nested Scopes

Navigate through the scope hierarchy programmatically:

```
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 list item by ID, index, or the "main" selector
const threadItem = aui.threads.item({ id: "thread_123" });
const threadByIndex = aui.threads.item({ index: 0 });
const archivedThread = aui.threads.item({ index: 0, archived: true });

// Traverse to the main thread directly
const mainThread = aui.threads.thread("main");
const message = aui.threads.thread("main").message({ id: "msg_123" });
```

## Common Patterns

### Conditional Rendering

```
function RunIndicator() {
  const isRunning = useAuiState((s) => s.thread.isRunning);

  if (!isRunning) return null;
  return <div>Assistant is thinking...</div>;
}
```

### Custom Action Buttons

```
function CopyButton() {
  const aui = useAui();

  const handleCopy = () => {
    navigator.clipboard.writeText(aui.message.getCopyText());
  };

  return <button onClick={handleCopy}>Copy</button>;
}
```

### State-Aware Components

```
function SmartComposer() {
  const aui = useAui();
  const isRunning = useAuiState((s) => s.thread.isRunning);
  const text = useAuiState((s) => s.composer.text);

  const canSend = !isRunning && text.length > 0;

  return (
    <div>
      <textarea
        value={text}
        onChange={(e) => aui.composer.setText(e.target.value)}
        disabled={isRunning}
      />
      <button onClick={() => aui.composer.send()} disabled={!canSend}>
        Send
      </button>
    </div>
  );
}
```

### Event-Driven Updates

```
function MessageCounter() {
  const [sendCount, setSendCount] = useState(0);

  useAuiEvent("composer.send", () => {
    setSendCount((c) => c + 1);
  });

  return <div>Messages sent: {sendCount}</div>;
}
```

## Advanced Topics

### Resolution Dynamics

Scopes are resolved during render and bound into the client. State updates never change the client's identity; a structural change (for example, switching threads) produces a new client and re-renders consumers through React:

```
const aui = useAui();

// Bound to the thread current at this render
aui.thread.append({ role: "user", content: "Hello" });

// After a thread switch, the component re-renders with a new client.
// Accessors read from a previous render keep pointing at the old thread.
```

### Performance Optimization

**Selector optimization:**

```
// ❌ Expensive computation in selector (runs on every store update)
const result = useAuiState(
  (s) => s.thread.messages.filter((m) => m.role === "user").length,
);

// ✅ Memoize expensive computations
const messages = useAuiState((s) => s.thread.messages);
const userCount = useMemo(
  () => messages.filter((m) => m.role === "user").length,
  [messages],
);
```

**Minimize re-renders:**

```
// ❌ Subscribes to entire thread state
const thread = useAuiState((s) => s.thread);

// ✅ Subscribe only to needed values
const isRunning = useAuiState((s) => s.thread.isRunning);
```

## API Reference

### Hooks

| Hook                          | Purpose                    | Returns        |
| ----------------------------- | -------------------------- | -------------- |
| `useAuiState(selector)`       | Subscribe to state changes | Selected value |
| `useAui()`                    | Get API instance           | API object     |
| `useAuiEvent(event, handler)` | Subscribe to events        | void           |

### Scope States

| Scope          | Key State Properties                                                                                                                                                                                                        | Description                                                                                                  |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| ThreadList     | `mainThreadId`, `newThreadId`, `threadIds`, `archivedThreadIds`, `isLoading`, `threadItems` (`readonly ThreadListItemState[]`)                                                                                              | Manages all available conversation threads                                                                   |
| ThreadListItem | `id`, `title`, `status`, `remoteId`, `externalId`, `custom?: Record<string, unknown>`                                                                                                                                       | Individual thread metadata and status; `custom` carries arbitrary per-thread metadata set by remote runtimes |
| Thread         | `isRunning` (may be explicitly set by the runtime rather than derived from last-message status), `isLoading`, `isDisabled`, `isEmpty`, `hasEarlier`, `isLoadingEarlier`, `messages`, `tasks`, `capabilities`, `suggestions` | Active conversation state and message history                                                                |
| Message        | `role`, `content`, `status`, `attachments`, `parts`, `parentId`, `branchNumber`, `branchCount`, `isLast`, `index`                                                                                                           | Individual message content and metadata                                                                      |
| Part           | `type`, `status`, `text`, `toolCallId`, `toolName`                                                                                                                                                                          | Content parts within messages (text, tool calls)                                                             |
| ChainOfThought | `parts`, `collapsed`, `status`                                                                                                                                                                                              | Reasoning steps grouped within a message                                                                     |
| Composer       | `text`, `role`, `attachments`, `isEmpty`, `canCancel`, `type`, `isEditing`                                                                                                                                                  | Text input state for new/edited messages                                                                     |
| Attachment     | `id`, `type`, `name`, `contentType`, `status`                                                                                                                                                                               | File attachments metadata and content                                                                        |
| Suggestions    | `suggestions`                                                                                                                                                                                                               | Collection of follow-up message suggestions                                                                  |
| Suggestion     | `title`, `label`, `prompt`                                                                                                                                                                                                  | Individual suggestion with title, label, and prompt                                                          |
| ModelContext   | `modelName`, `toolNames`                                                                                                                                                                                                    | Model name and tool names from the merged model context; see [Model Context](/docs/copilots/model-context)   |

### Available Actions by Scope

The table below covers the most commonly used actions. For the full catalog, see the [API Reference](/docs/api-reference/overview).

| Scope          | Actions                                                                                                                                                                                                                             | Use Cases                                                                         |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| ThreadList     | `switchToNewThread()`, `switchToThread(id)`, `reload()`, `getLoadThreadsPromise()`, `item(selector)`, `thread("main")`, `getState()`                                                                                                | Thread navigation, creation, and sync                                             |
| ThreadListItem | `switchTo()`, `rename(title)`, `updateCustom(custom)`, `archive()`, `unarchive()`, `delete()`, `getState()`                                                                                                                         | Thread management operations                                                      |
| Thread         | `append(message)`, `startRun(config)`, `resumeRun(config)`, `cancelRun()`, `loadEarlier()`, `reset()`, `export()`, `import(repository)`, `message(selector)`, `task(selector)`, `composer()`, `getState()`                          | Message handling and conversation control                                         |
| Message        | `reload()`, `speak()`, `stopSpeaking()`, `submitFeedback(feedback)`, `switchToBranch(options)`, `getCopyText()`, `part(selector)`, `attachment(selector)`, `composer()`, `setIsCopied(value)`, `setIsHovering(value)`, `getState()` | Message interactions and regeneration                                             |
| Part           | `addToolResult(result)`, `resumeToolCall(result)`, `getState()`                                                                                                                                                                     | Tool call result handling                                                         |
| ChainOfThought | `setCollapsed(collapsed)`, `part({ index })`, `getState()`                                                                                                                                                                          | Expand/collapse reasoning steps                                                   |
| Composer       | `send()`, `setText(text)`, `setRole(role)`, `addAttachment(file \| attachment)`, `clearAttachments()` (async), `reset()` (async), `getState()`                                                                                      | Text input and message composition                                                |
| Attachment     | `remove()`, `getState()`                                                                                                                                                                                                            | File management                                                                   |
| Suggestions    | `suggestion({ index })`, `getState()`                                                                                                                                                                                               | Access follow-up suggestions                                                      |
| Suggestion     | `getState()`                                                                                                                                                                                                                        | Read individual suggestion data                                                   |
| ModelContext   | `register(provider)`, `getModelContext()`, `getState()`                                                                                                                                                                             | Register providers; full details in [Model Context](/docs/copilots/model-context) |

### Events vs State Observation

Use `useAuiState` to render the current value of something; use `useAuiEvent` to react to the moment something happens. Both are supported: many events mark a transition you could also observe in state, and subscribing to the event is the intended way to run a side effect at that transition.

The rule of thumb:

1. Rendering a value? → use `useAuiState`.
2. Are you the caller and want immediate feedback? → catch the rejection / read the return value.
3. Running a side effect when something happens? → use `useAuiEvent`.

#### Available Events

| Event                         | When It Fires                                                                                                                                                                                                                                                                                                                           |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `threads.selectionChanged`    | The main thread selection changed. Payload carries `threadId` (newly selected) and `previousThreadId`. Does not fire for the initially selected thread on mount.                                                                                                                                                                        |
| `thread.runStart` / `runEnd`  | A run started / ended on the thread (also visible as `isRunning` flipping)                                                                                                                                                                                                                                                              |
| `thread.cancelRun`            | The user stopped the run in progress on the thread                                                                                                                                                                                                                                                                                      |
| `thread.initialize`           | The thread transitioned from new to initialized, before the first message is added                                                                                                                                                                                                                                                      |
| `thread.modelContextUpdate`   | The model context provider notified a change. The model context lives in a provider, not in thread state, so this event has no state-derivable equivalent.                                                                                                                                                                              |
| `composer.send`               | The user sent the composer contents; `messageId` is set for edit-composer sends                                                                                                                                                                                                                                                         |
| `composer.attachmentAdd`      | An attachment was added to the composer                                                                                                                                                                                                                                                                                                 |
| `composer.attachmentAddError` | An `addAttachment()` call failed. Payload `reason` is `no-adapter`, `not-accepted`, or `adapter-error`, and `message` is a human-readable description. The underlying `Error` instance is available only through the low-level `runtime.unstable_on("attachmentAddError")` API, because raw `Error` objects are not store-serializable. |
| `composer.cancel`             | The composer cancelled the run in progress                                                                                                                                                                                                                                                                                              |
| `message.reload`              | The user asked for the assistant message to be generated again                                                                                                                                                                                                                                                                          |
| `message.branchSwitched`      | The user moved to another branch at the message                                                                                                                                                                                                                                                                                         |
| `message.copied`              | The user copied the message                                                                                                                                                                                                                                                                                                             |
| `message.speak`               | Speech synthesis started for the message                                                                                                                                                                                                                                                                                                |
| `message.error`               | The message entered an error status                                                                                                                                                                                                                                                                                                     |
| `thread.toolApprovalAnswered` | A tool approval request was answered. Payload carries `messageId`, `toolCallId`, `toolName`, and `approved`.                                                                                                                                                                                                                            |
| `thread.historyWriteError`    | A local runtime history write failed. Payload carries `threadId`, `operation` (`append`, `update`, or `delete`), affected `messageIds`, and a readable `message`. The underlying error is available only through the low-level `runtime.unstable_on("historyWriteError")` API. The stored history may differ from the thread.           |
| `thread.voiceStarted`         | The user started a voice session on the thread                                                                                                                                                                                                                                                                                          |

#### Deprecated Events

| Deprecated Event              | Use Instead                                                                         |
| ----------------------------- | ----------------------------------------------------------------------------------- |
| `threadListItem.switchedTo`   | `threads.selectionChanged`; its `threadId` is the newly selected thread             |
| `threadListItem.switchedAway` | `threads.selectionChanged`; its `previousThreadId` is the thread switched away from |

## Troubleshooting

### Common Errors

**"The current scope does not have a "message" property."**

```
// ❌ This will throw if not inside a message component
const role = useAuiState((s) => s.message.role);

// ✅ Read through the optional view — unavailable scopes resolve to undefined
const role = useAuiState((s) => s.optional.message?.role ?? "none");
```

A component that renders outside `AssistantRuntimeProvider` has no scopes at all, and the error asks for an `AuiProvider` instead. Move it under the provider or publish the client to it; see [Reading State Outside the Thread](/docs/guides/state-outside-the-thread).

**"Maximum update depth exceeded" / Infinite re-renders**

```
// ❌ Creating new objects in selectors causes infinite re-renders
const data = useAuiState((s) => ({
  role: s.message.role,
  content: s.message.content, // New object every time!
}));

// ✅ Return primitive values or use separate selectors
const role = useAuiState((s) => s.message.role);
const content = useAuiState((s) => s.message.content);
```

**Stale scope references**

```
// ❌ Storing scope references can lead to stale data
const aui = useAui();
const thread = aui.thread; // This reference might become stale

useEffect(() => {
  // This might reference the wrong thread if user switched
  thread.cancelRun();
}, [thread]);

// ✅ Resolve scopes fresh each time
const aui = useAui();

useEffect(() => {
  // Always gets the current thread
  aui.thread.cancelRun();
}, [aui]);
```

## Quick Reference

```
// Read state
const value = useAuiState((s) => s.scope.property);

// Perform action
const aui = useAui();
aui.scope.action();

// Listen to events
useAuiEvent("source.event", (e) => {});

// Read a scope that may be unavailable
const maybe = useAuiState((s) => s.optional.scope?.property);

// Get state imperatively
const state = aui.scope.getState();

// Navigate scopes
aui.thread.message({ id: "..." }).getState();
```