# Migration to v0.15
URL: /docs/migrations/v0-15

Drops the v0.12-era legacy runtime hooks, the deprecated tools map, and the "mcp-app" group key. Scope accessors become properties.

> 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.

Version 0.15 finishes the deprecation cycle started in v0.12: the legacy context hooks are removed in favor of `useAui` / `useAuiState`, and `aui` scope accessors become properties.

## Migrate with an AI Agent

Paste this into an AI coding agent to run the migration for you:

```
Migrate this codebase from assistant-ui v0.14 to v0.15.

1. Read the migration guide: https://assistant-ui.com/docs/migrations/v0-15
2. Run `npx assistant-ui@latest upgrade` to apply the codemods.
3. Apply the remaining changes from the guide by hand:
   - Replace removed legacy hooks with the useAui / useAuiState
     equivalents from the guide's mapping table.
   - Replace availability checks with `aui.<scope>.source != null`.
   - Replace `s.tools.tools` reads with `s.tools.toolUIs`.
   - Replace the "mcp-app" groupPartByType key with "standalone-tool-call".
4. Typecheck, build, and run tests; fix any remaining fallout.
```

## Automatic Migration

```
npx assistant-ui@latest upgrade
```

This runs the `v0-15/aui-accessor-calls-to-properties` codemod, which rewrites nullary accessor calls (`aui.thread()`) to property access (`aui.thread`).

## Scope Accessors Are Properties

Nullary scope accessors are now properties. Calling them still works but is deprecated:

```
// Before
aui.thread().getState();
aui.threads().switchToNewThread();

// After
aui.thread.getState();
aui.threads.switchToNewThread();
```

Selecting an unavailable scope no longer throws: `aui.thread` always succeeds and is always truthy. Its `source` is `null` when the scope is unavailable, and any other property read (or a call) throws. Check availability via:

```
if (aui.thread.source != null) {
  // scope is available
}
```

Accessors expose `source`, `query`, and `name` selection metadata as properties — previously found on the accessor function, now on the same names on the proxy. These three names are reserved and never resolve to scope methods.

## Legacy Context Hooks Removed

The v0.12-era runtime hooks are removed. Replace state reads with `useAuiState` and actions with `useAui`:

| Removed                           | Replacement                                 |
| --------------------------------- | ------------------------------------------- |
| `useAssistantRuntime()`           | `useAui()`                                  |
| `useThreadList(selector)`         | `useAuiState((s) => s.threads)`             |
| `useThreadRuntime()`              | `useAui().thread`                           |
| `useThread(selector)`             | `useAuiState((s) => s.thread)`              |
| `useThreadComposer(selector)`     | `useAuiState((s) => s.thread.composer)`     |
| `useThreadModelContext(selector)` | `useAuiState((s) => s.thread.modelContext)` |
| `useMessageRuntime()`             | `useAui().message`                          |
| `useMessage(selector)`            | `useAuiState((s) => s.message)`             |
| `useEditComposer(selector)`       | `useAuiState((s) => s.message.composer)`    |
| `useComposerRuntime()`            | `useAui().composer`                         |
| `useComposer(selector)`           | `useAuiState((s) => s.composer)`            |
| `useMessagePartRuntime()`         | `useAui().part`                             |
| `useMessagePart(selector)`        | `useAuiState((s) => s.part)`                |
| `useAttachmentRuntime()`          | `useAui().attachment`                       |
| `useAttachment(selector)`         | `useAuiState((s) => s.attachment)`          |
| `useThreadListItemRuntime()`      | `useAui().threadListItem`                   |
| `useThreadListItem(selector)`     | `useAuiState((s) => s.threadListItem)`      |

The attachment variants (`useThreadComposerAttachment(Runtime)`, `useEditComposerAttachment(Runtime)`, `useMessageAttachment(Runtime)`) are removed with them; use `useAui().attachment` / `useAuiState((s) => s.attachment)`.

```
// Before
const runtime = useAssistantRuntime();
const isRunning = useThread((s) => s.isRunning);
runtime.threads.switchToNewThread();

// After
const aui = useAui();
const isRunning = useAuiState((s) => s.thread.isRunning);
aui.threads.switchToNewThread();
```

## `ToolsState.tools` Removed

The component-only tool-UI map is replaced by `toolUIs`, whose entries carry the renderer alongside its presentation options:

```
// Before
const Render = useAuiState((s) => s.tools.tools[toolName]?.[0]);

// After
const Render = useAuiState((s) => s.tools.toolUIs[toolName]?.[0]?.render);
```

## `"mcp-app"` Group Key Removed

`groupPartByType` no longer accepts the `"mcp-app"` key. Use `"standalone-tool-call"`, a superset that matches MCP-app tool calls plus any tool call whose registered UI opts into standalone display:

```
// Before
groupPartByType({
  "tool-call": ["group-tool"],
  "mcp-app": [],
});

// After
groupPartByType({
  "tool-call": ["group-tool"],
  "standalone-tool-call": [],
});
```

## `useAui` `{ parent }` Config Removed

The second argument (`useAui(clients, { parent })`) is removed. Provide the parent via context instead: wrap with `AuiProvider` and call the context form beneath it.

```
// Before
const aui = useAui(scopes, { parent });

// After
const Scoped = ({ children }) => {
  const aui = useAui();
  const config = AuiConfig(scopes);
  return (
    <AuiProvider extends={aui} config={config}>
      {children}
    </AuiProvider>
  );
};

const rootConfig = AuiConfig({});

<AuiProvider extends={parent} config={rootConfig}>
  <Scoped />
</AuiProvider>;
```

Where `{ parent: null }` was used to detach from context, `<AuiProvider extends={null} config={config}>` where `const config = AuiConfig({})` now provides an isolated empty root.

## `AuiProvider` Grammar

`AuiProvider` takes a `config` built with `AuiConfig(...)` — raw object literals are a type error. At the top level, `config` alone creates the subtree's client. Nested under a parent provider, `extends` is mandatory: `extends={aui}` extends the parent, `extends={null}` isolates (dev-enforced). `ref` receives the resulting client after mount.

```
const aui = useAui();
const config = AuiConfig({ tools: Tools({ toolkit }) });

// Top-level root
<AuiProvider config={config}>

// Nested: extend the parent
<AuiProvider extends={aui} config={config}>

// Nested: isolate from the parent
<AuiProvider extends={null} config={config}>
```

`AuiConfig` is exported from `@assistant-ui/store` and re-exported from `@assistant-ui/react`, `@assistant-ui/react-native`, and `@assistant-ui/react-ink`.

### `value` Prop Deprecated

```
// Before
<AuiProvider value={client}>
<AuiProvider value={null}>

// After
const config = AuiConfig({});

<AuiProvider extends={client} config={config}>
<AuiProvider extends={null} config={config}>
```

The replacement exposes a client extending the given one, not the same instance — `useAui()` beneath it returns the new client, with scope access delegating to `client`. The deprecated `value={client}` form behaves the same way: it also exposes a derived client rather than the exact instance, and the given client must implement `subscribe`.

### `useAui({ ... })` Extension Overload Deprecated

```
// Before
const aui = useAui({ tools: Tools({ toolkit }) });
return <AuiProvider value={aui}>{children}</AuiProvider>;

// After
const aui = useAui();
const config = AuiConfig({ tools: Tools({ toolkit }) });
return (
  <AuiProvider extends={aui} config={config}>
    {children}
  </AuiProvider>
);
```

Where the extended client was passed to `<AssistantRuntimeProvider aui={aui}>`, use the new `config` prop instead — the scopes are provided alongside the runtime's threads scope:

```
// Before
const aui = useAui({ tools: Tools({ toolkit }) });
return (
  <AssistantRuntimeProvider aui={aui} runtime={runtime}>
    {children}
  </AssistantRuntimeProvider>
);

// After
const config = AuiConfig({ tools: Tools({ toolkit }) });
return (
  <AssistantRuntimeProvider runtime={runtime} config={config}>
    {children}
  </AssistantRuntimeProvider>
);
```

## Thread Switch Events: `threads.selectionChanged`

The per-item thread switch events are deprecated in favor of a single event on the thread list. `threads.selectionChanged` fires once per switch and carries both sides of the transition:

```
// Before
useAuiEvent("threadListItem.switchedTo", ({ threadId }) => {
  // threadId: the newly selected thread
});
useAuiEvent("threadListItem.switchedAway", ({ threadId }) => {
  // threadId: the thread that was switched away from
});

// After
useAuiEvent("threads.selectionChanged", ({ threadId, previousThreadId }) => {
  // threadId: the newly selected thread
  // previousThreadId: the thread that was switched away from
});
```

Like its predecessors, it does not fire for the initially selected thread on mount. The deprecated events still fire and will keep working until the next major.

The deprecated pair was scope-filtered: inside a per-item `threadListItem` scope (such as `ThreadListPrimitive.Items`), `threadListItem.switchedTo` only fired for that item. `threads.selectionChanged` resolves against the shared `threads` scope, so every listener fires on every switch. Filter by id to reproduce the per-item behavior:

```
const id = useAuiState((s) => s.threadListItem.id);
useAuiEvent("threads.selectionChanged", ({ threadId }) => {
  if (threadId !== id) return;
  // this item became selected
});
```

The new event also fires in situations where the deprecated pair did not: `InMemoryThreadList` emits on selection changes (it previously emitted no switch events), and `switchToNewThread()` emits for the newly created thread. Selection-driven defaults such as `scrollToBottomOnThreadSwitch` and `unstable_focusOnThreadSwitched` now engage in both situations. Runtimes that resolve a deep-linked `threadId`/`initialThreadId` after mount (such as `useRemoteThreadListRuntime`) start on a placeholder new thread, so the deep link's resolution also fires the event — `previousThreadId` is the placeholder in that case.

## Still Deprecated (not removed)

- Primitive `If` components (`ThreadPrimitive.If`, `MessagePrimitive.If`, `ThreadPrimitive.Empty`) — replaced by `AuiIf`. The codemod migrates these.
- `useMessagePartText` / `useMessagePartReasoning` / `useMessagePartSource` / `useMessagePartImage` / `useMessagePartFile` / `useMessagePartData` — use `useAuiState` to select and narrow `s.part`.
- The `components` prop on primitives — replaced by the children render function pattern (see the [v0.14 guide](/docs/migrations/v0-14)).

## Getting Help

- File issues at <https://github.com/assistant-ui/assistant-ui/issues>
- See the [deprecation policy](/docs/migrations/deprecation-policy) for the support window