diff --git a/.changeset/create-chat-hook.md b/.changeset/create-chat-hook.md
new file mode 100644
index 0000000000..aea3db151e
--- /dev/null
+++ b/.changeset/create-chat-hook.md
@@ -0,0 +1,12 @@
+---
+'@tanstack/ai-react': minor
+'@tanstack/ai-solid': minor
+'@tanstack/ai-vue': minor
+'@tanstack/ai-svelte': minor
+---
+
+Chat UI now lives on the framework packages at `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, and `@tanstack/ai-svelte/ui`.
+
+Call `useChat(options)` from the framework package. Pass that instance and a typed `components` map to ` `. `useChatContext()` reads the same instance from inside `Chat`.
+
+`createChatHook(options)` on the main package still returns a bound `useChat`. It does not take `chatComponents`.
diff --git a/.changeset/deprecate-ai-ui-packages.md b/.changeset/deprecate-ai-ui-packages.md
new file mode 100644
index 0000000000..164cc1c77d
--- /dev/null
+++ b/.changeset/deprecate-ai-ui-packages.md
@@ -0,0 +1,11 @@
+---
+'@tanstack/ai-react-ui': minor
+'@tanstack/ai-solid-ui': minor
+'@tanstack/ai-vue-ui': minor
+---
+
+Deprecated. Import chat UI from `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, or `@tanstack/ai-vue/ui` instead.
+
+These packages re-export the new `/ui` subpath until 1.0.0. `npm install` prints a deprecation warning.
+
+`@tanstack/ai-svelte-ui` was never published. Svelte chat UI lives only on `@tanstack/ai-svelte/ui`.
diff --git a/.changeset/typed-headless-chat-ui.md b/.changeset/typed-headless-chat-ui.md
index db5dfc9eec..14c84dab83 100644
--- a/.changeset/typed-headless-chat-ui.md
+++ b/.changeset/typed-headless-chat-ui.md
@@ -1,9 +1,9 @@
---
'@tanstack/ai-client': minor
-'@tanstack/ai-react-ui': minor
-'@tanstack/ai-solid-ui': minor
-'@tanstack/ai-vue-ui': minor
-'@tanstack/ai-svelte-ui': minor
+'@tanstack/ai-react': minor
+'@tanstack/ai-solid': minor
+'@tanstack/ai-vue': minor
+'@tanstack/ai-svelte': minor
---
Add typed headless `createChatUI()` adapters. Chat options control the types of message parts, tools, structured output, and interrupts. `defineComponents` requires a component for every tool name and every registered interrupt id. `InterruptProps` pins a tool approval or a registered generic interrupt. Old Chat orchestration stays importable and deprecated until 1.0.
diff --git a/README.md b/README.md
index c9bbb8c10d..d524f36907 100644
--- a/README.md
+++ b/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/codemods/ag-ui-compliance/transform.ts b/codemods/ag-ui-compliance/transform.ts
index 37eb154620..d4924f7a90 100644
--- a/codemods/ag-ui-compliance/transform.ts
+++ b/codemods/ag-ui-compliance/transform.ts
@@ -36,11 +36,11 @@ import type {
const FRAMEWORK_USE_CHAT_PACKAGES = new Set([
'@tanstack/ai-react',
- '@tanstack/ai-react-ui',
+ '@tanstack/ai-react/ui',
'@tanstack/ai-vue',
- '@tanstack/ai-vue-ui',
+ '@tanstack/ai-vue/ui',
'@tanstack/ai-solid',
- '@tanstack/ai-solid-ui',
+ '@tanstack/ai-solid/ui',
'@tanstack/ai-preact',
])
diff --git a/docs/api/ai-react.md b/docs/api/ai-react.md
index ff4fafe768..5ee34a4001 100644
--- a/docs/api/ai-react.md
+++ b/docs/api/ai-react.md
@@ -14,7 +14,7 @@ keywords:
React hooks for TanStack AI, providing convenient React bindings for the headless client.
-For a typed headless chat UI, see [React Chat UI](../ui/react) and [Migrate to createChatUI](../migration/create-ui).
+For a typed headless chat UI, see [React Chat UI](../ui/react) and [Chat UI packages](../migration/create-ui).
For React Native, the documented support surface is narrow: `useChat` with chat
connection adapters. React DOM-specific UI packages and TanStack AI devtools UI
are not part of the React Native support surface.
@@ -28,6 +28,27 @@ For a complete native journey, see
npm install @tanstack/ai-react
```
+## `createChatHook(options)`
+
+Bind `chatOptions` once at module scope. Call `useChat()` in the screen to create the instance. Per-call overrides may set `threadId`, `initialMessages`, `live`, and `forwardedProps`. They must not change `tools`, `interrupts`, or `outputSchema`.
+
+```tsx
+import { createChatHook, fetchServerSentEvents } from "@tanstack/ai-react";
+
+const chatOptions = {
+ connection: fetchServerSentEvents("/api/chat"),
+};
+
+const { useChat } = createChatHook(chatOptions);
+
+function ChatScreen() {
+ const chat = useChat({ threadId: "support-1" });
+ return null;
+}
+```
+
+`useChat(chatOptions)` from this package still works when you want to pass the full object at the call site. Rename the bound hook if both are in one file: `const { useChat: useSupportChat } = createChatHook(chatOptions)`.
+
## `useChat(options?)`
Main hook for managing chat state in React with full type safety.
diff --git a/docs/api/ai-solid.md b/docs/api/ai-solid.md
index 3fd64dfb6a..3bada592f7 100644
--- a/docs/api/ai-solid.md
+++ b/docs/api/ai-solid.md
@@ -15,7 +15,7 @@ keywords:
SolidJS primitives for TanStack AI, providing convenient SolidJS bindings for the headless client.
-For a typed headless chat UI, see [Solid Chat UI](../ui/solid) and [Migrate to createChatUI](../migration/create-ui).
+For a typed headless chat UI, see [Solid Chat UI](../ui/solid) and [Chat UI packages](../migration/create-ui).
## Installation
@@ -23,6 +23,27 @@ For a typed headless chat UI, see [Solid Chat UI](../ui/solid) and [Migrate to c
npm install @tanstack/ai-solid
```
+## `createChatHook(options)`
+
+Bind `chatOptions` once at module scope. Call `useChat()` in the screen to create the instance. Per-call overrides may set `threadId`, `initialMessages`, `live`, and `forwardedProps`. They must not change `tools`, `interrupts`, or `outputSchema`.
+
+```tsx
+import { createChatHook, fetchServerSentEvents } from "@tanstack/ai-solid";
+
+const chatOptions = {
+ connection: fetchServerSentEvents("/api/chat"),
+};
+
+const { useChat } = createChatHook(chatOptions);
+
+function ChatScreen() {
+ const chat = useChat({ threadId: "support-1" });
+ return null;
+}
+```
+
+`useChat(chatOptions)` from this package still works when you want to pass the full object at the call site. Rename the bound primitive if both are in one file: `const { useChat: useSupportChat } = createChatHook(chatOptions)`.
+
## `useChat(options?)`
Main primitive for managing chat state in SolidJS with full type safety.
diff --git a/docs/api/ai-svelte.md b/docs/api/ai-svelte.md
index a59e25da52..ba99f6de4e 100644
--- a/docs/api/ai-svelte.md
+++ b/docs/api/ai-svelte.md
@@ -15,7 +15,7 @@ keywords:
Svelte 5 bindings for TanStack AI, providing reactive factory functions for the headless client using Svelte runes.
-For a typed headless chat UI, see [Svelte Chat UI](../ui/svelte) and [Migrate to createChatUI](../migration/create-ui).
+For a typed headless chat UI, see [Svelte Chat UI](../ui/svelte) and [Chat UI packages](../migration/create-ui).
## Installation
@@ -23,6 +23,23 @@ For a typed headless chat UI, see [Svelte Chat UI](../ui/svelte) and [Migrate to
npm install @tanstack/ai-svelte
```
+## `createChatHook(options)`
+
+Bind `chatOptions` once at module scope. Call `createChat()` to create the instance. Per-call overrides may set `threadId`, `initialMessages`, `live`, and `forwardedProps`. They must not change `tools`, `interrupts`, or `outputSchema`.
+
+```ts
+import { createChatHook, fetchServerSentEvents } from "@tanstack/ai-svelte";
+
+const chatOptions = {
+ connection: fetchServerSentEvents("/api/chat"),
+};
+
+const { createChat } = createChatHook(chatOptions);
+const chat = createChat({ threadId: "support-1" });
+```
+
+`createChat(chatOptions)` from this package still works when you want to pass the full object at the call site. Rename the bound factory if both are in one file: `const { createChat: createSupportChat } = createChatHook(chatOptions)`.
+
## `createChat(options)`
Factory function for managing chat state in Svelte 5 with full type safety.
diff --git a/docs/api/ai-vue.md b/docs/api/ai-vue.md
index cd46cf22e6..ffc8336a80 100644
--- a/docs/api/ai-vue.md
+++ b/docs/api/ai-vue.md
@@ -15,7 +15,7 @@ keywords:
Vue composables for TanStack AI, providing convenient Vue 3 bindings for the headless client.
-For a typed headless chat UI, see [Vue Chat UI](../ui/vue) and [Migrate to createChatUI](../migration/create-ui).
+For a typed headless chat UI, see [Vue Chat UI](../ui/vue) and [Chat UI packages](../migration/create-ui).
## Installation
@@ -23,6 +23,24 @@ For a typed headless chat UI, see [Vue Chat UI](../ui/vue) and [Migrate to creat
npm install @tanstack/ai-vue
```
+## `createChatHook(options)`
+
+Bind `chatOptions` once at module scope. Call `useChat()` in the screen to create the instance. Per-call overrides may set `threadId`, `initialMessages`, `live`, and `forwardedProps`. They must not change `tools`, `interrupts`, or `outputSchema`.
+
+```ts
+import { createChatHook, fetchServerSentEvents } from "@tanstack/ai-vue";
+
+const chatOptions = {
+ connection: fetchServerSentEvents("/api/chat"),
+};
+
+const { useChat } = createChatHook(chatOptions);
+
+const chat = useChat({ threadId: "support-1" });
+```
+
+`useChat(chatOptions)` from this package still works when you want to pass the full object at the call site. Rename the bound composable if both are in one file: `const { useChat: useSupportChat } = createChatHook(chatOptions)`.
+
## `useChat(options?)`
Main composable for managing chat state in Vue with full type safety.
diff --git a/docs/config.json b/docs/config.json
index 0b79db5749..a838e2d832 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -219,31 +219,31 @@
"label": "React",
"to": "ui/react",
"addedAt": "2026-08-26",
- "updatedAt": "2026-08-27"
+ "updatedAt": "2026-08-30"
},
{
"label": "Solid",
"to": "ui/solid",
"addedAt": "2026-08-26",
- "updatedAt": "2026-08-27"
+ "updatedAt": "2026-08-30"
},
{
"label": "Vue",
"to": "ui/vue",
"addedAt": "2026-08-26",
- "updatedAt": "2026-08-27"
+ "updatedAt": "2026-08-30"
},
{
"label": "Svelte",
"to": "ui/svelte",
"addedAt": "2026-08-26",
- "updatedAt": "2026-08-27"
+ "updatedAt": "2026-08-30"
},
{
"label": "Custom Adapters",
"to": "ui/custom-adapters",
"addedAt": "2026-08-26",
- "updatedAt": "2026-08-26"
+ "updatedAt": "2026-08-30"
}
]
},
@@ -886,10 +886,10 @@
"updatedAt": "2026-07-22"
},
{
- "label": "createChatUI",
+ "label": "Chat UI packages",
"to": "migration/create-ui",
"addedAt": "2026-08-26",
- "updatedAt": "2026-08-27"
+ "updatedAt": "2026-08-30"
},
{
"label": "From Vercel AI SDK",
diff --git a/docs/migration/create-ui.md b/docs/migration/create-ui.md
index 6a6836231f..8d4baa4fc3 100644
--- a/docs/migration/create-ui.md
+++ b/docs/migration/create-ui.md
@@ -1,44 +1,35 @@
---
-title: Migrate to createChatUI
+title: Chat UI packages
id: migrate-create-ui
order: 5
-description: "Move chat-state ownership out of the old Chat component and onto createChatUI with a typed component map."
+description: "Move from @tanstack/ai-*-ui to the framework /ui subpath. Call useChat and render Chat with a component map."
keywords:
- tanstack ai
- - createChatUI
+ - Chat
- migration
- deprecation
---
-The old `Chat` component owned chat state and lost configured types. `createChatUI` keeps types from your `chatOptions` and leaves `useChat` in your app.
+Change your import from `@tanstack/ai-react-ui` to `@tanstack/ai-react/ui`. Call `useChat(chatOptions)` and render ` `.
-This is a semantic migration. There is no codemod.
+The same move applies to Solid, Vue, and Svelte.
+
+> **Deprecated.** `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` re-export the new `/ui` subpath until each package's `1.0.0`. `npm install` prints a warning. Import from `/ui` in new code. Svelte never published a `*-ui` package. Use `@tanstack/ai-svelte/ui`.
## What changes
-1. You call `useChat` or `createChat` yourself.
-2. You supply every visible component.
-3. Tool inputs stay optional while they stream.
-4. Tool approvals come from `chat.interrupts`.
-5. Unknown runtime keys can use a fallback or render nothing.
-6. `createChatUI()` must run at module scope so identity stays stable.
+1. Chat UI lives on the framework package: `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui`.
+2. You call `useChat` or `createChat` yourself.
+3. You pass a `components` map into `Chat`.
+4. You supply every visible component. There is no default markup.
## Why
-The old APIs drop configured types, keep unused properties, use a deprecated approval path, cover only part of the message protocol, and own chat state. Two orchestration models duplicate fixes.
-
-## Minimum versions
-
-- `@tanstack/ai-react-ui` 0.9.0
-- `@tanstack/ai-solid-ui` 0.8.0
-- `@tanstack/ai-vue-ui` 0.3.0
-- `@tanstack/ai-svelte-ui` 0.2.0
-
-Old orchestration exports stay importable until each package's `1.0.0`. `TextPart` and `ThinkingPart` stay supported.
+A separate `*-ui` package split chat UI from the framework package. The old `Chat` component also owned chat state and dropped configured types. `Chat` from `/ui` keeps types from your `chatOptions` and leaves state on `useChat`.
## Before
-```tsx
+```tsx ignore
import { fetchServerSentEvents } from '@tanstack/ai-react'
import { Chat, ChatMessages, ChatInput } from '@tanstack/ai-react-ui'
@@ -58,15 +49,13 @@ export function OldChat() {
```tsx
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
-import { createChatUI } from '@tanstack/ai-react-ui'
+import { Chat, type ChatUIComponents } from '@tanstack/ai-react/ui'
const chatOptions = {
connection: fetchServerSentEvents('/api/chat'),
}
-const UI = createChatUI(chatOptions)
-
-const components = UI.defineComponents({
+const components = {
layout: ({ renderMessages, renderInput }) => (
{renderMessages()}
@@ -74,9 +63,7 @@ const components = UI.defineComponents({
),
message: ({ renderParts }) => {renderParts()} ,
- input: () => {
- const chat = UI.useChat()
- return (
+ input: ({ chat }) => (
- )
- },
+ ),
parts: { fallback: () => null },
-})
+} satisfies ChatUIComponents
export function NewChat() {
const chat = useChat(chatOptions)
- return
+ return
}
```
## Steps
1. Move `connection`, `tools`, and `interrupts` into a module-level `chatOptions` object.
-2. Call `createChatUI(chatOptions)` next to that object.
+2. Build a `components` map typed with `ChatUIComponents`.
3. Call `useChat(chatOptions)` in the screen component.
-4. Define `layout`, `message`, `parts`, `tools`, and `interrupts` in `defineComponents`.
-5. Replace `` with ` `.
-
-## Gotchas
-
-- A shared `chatOptions` variable does not need `as const`.
-- A mapped tool can read `interrupt` and render the approval itself. That approval stays off the list. A component on `interrupts.tools` uses the list.
-- Generic interrupts live under `interrupts.generic`: a registered id such as `choosePlan`, plus `fallback`. Unbound interrupts use `fallback`.
-- TypeScript requires a `tools` component for every tool name and an `interrupts.generic` component for every interrupt id. `generic.fallback` is optional.
-- Matched `tool-result` parts are hidden in automatic traversal. Unmatched results stay visible.
-- Nested providers use the nearest chat instance.
+4. Render ` `.
-See the [React UI guide](../ui/react) for a full map.
+Old orchestration exports stay importable from the deprecated `*-ui` packages until `1.0.0`.
diff --git a/docs/ui/custom-adapters.md b/docs/ui/custom-adapters.md
index d51a4e8f60..10c2c61470 100644
--- a/docs/ui/custom-adapters.md
+++ b/docs/ui/custom-adapters.md
@@ -5,7 +5,7 @@ order: 5
description: "Build a framework adapter on @tanstack/ai-client/ui. The core is types and selectors only."
keywords:
- tanstack ai
- - createChatUI
+ - Chat
- custom adapter
- headless ui
---
@@ -20,7 +20,7 @@ The subpath gives you:
Your adapter owns:
-1. Native components and context
+1. Native components
2. Native reactivity
3. Render callbacks, slots, or snippets
4. Development warnings for missing mapped keys
@@ -29,6 +29,6 @@ Do not add default markup. Do not add a new store. The app owns `useChat` or `cr
Call `selectChatUI({ messages, interrupts, inlineToolNames })`. Automatic traversal skips a `tool-result` only when `matched` is true. Keep unmatched results.
-Warn once per missing runtime key in development. Each build tool detects development mode differently, so the adapter prints the warning.
+Warn once per missing runtime key in development.
-See the [React](./react), [Solid](./solid), [Vue](./vue), and [Svelte](./svelte) adapters for the public names to match: `Chat`, `Provider`, `Messages`, `Message`, `Part`, `Interrupts`, `Interrupt`, and `defineComponents`.
+See the [React](./react), [Solid](./solid), [Vue](./vue), and [Svelte](./svelte) adapters. The public names to match are `Chat` and `ChatUIComponents`.
diff --git a/docs/ui/react.md b/docs/ui/react.md
index 2a05727143..0bad7e7096 100644
--- a/docs/ui/react.md
+++ b/docs/ui/react.md
@@ -2,21 +2,21 @@
title: React Chat UI
id: typed-headless-ui-react
order: 1
-description: "Build a typed, headless React chat UI with createChatUI. Your chat options control the types of tools, parts, and interrupts."
+description: "Render a typed chat tree with Chat and a component map. Your chat options control the types of tools, parts, and interrupts."
keywords:
- tanstack ai
- - createChatUI
+ - Chat
- react
- headless ui
- useChat
- ToolProps
---
-Install `@tanstack/ai-react-ui`, then call `createChatUI(chatOptions)` once at module scope. Your app owns `useChat`. The UI only renders. Call `UI.useChat()` inside a mapped component when it needs live chat. That call is the same value you passed into `UI.Chat`.
+Install `@tanstack/ai-react`. Import `Chat` from `@tanstack/ai-react/ui`. Call `useChat(chatOptions)` in the screen. Pass a module-level `components` object into `Chat`.
You supply every visible component. There is no default markup, style, or copy.
-`defineComponents` needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional.
+The map needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional.
## Server
@@ -42,7 +42,7 @@ export async function POST(request: Request) {
```tsx
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
-import { createChatUI } from '@tanstack/ai-react-ui'
+import { Chat, type ChatUIComponents } from '@tanstack/ai-react/ui'
import { defineInterrupt, toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
@@ -74,18 +74,9 @@ const chatOptions = {
outputSchema: z.object({ answer: z.string() }),
}
-const UI = createChatUI(chatOptions)
-
-const components = UI.defineComponents({
- layout: function Layout({
- renderMessages,
- renderInterrupts,
- renderInput,
- }) {
- const chat = UI.useChat()
+const components = {
+ layout: ({ chat, renderMessages, renderInterrupts, renderInput }) => {
if (chat.error) return {chat.error.message}
- if (chat.isLoading && chat.messages.length === 0) return Loading
- if (chat.messages.length === 0) return Empty
return (
{renderMessages()}
@@ -94,47 +85,30 @@ const components = UI.defineComponents({
)
},
- message: function Message({ message, renderParts }) {
- return {renderParts()}
- },
- input: function Input() {
- const chat = UI.useChat()
- return (
-
- )
- },
+ message: ({ renderParts }) => {renderParts()} ,
+ input: ({ chat }) => (
+
+ ),
parts: {
- text: ({ part }) => {part.content}
,
- structuredOutput: ({ part }) => {part.raw} ,
- toolResult: ({ part }) => {String(part.content)} ,
- fallback: ({ part }) => {part.type} ,
+ text: ({ part }) => (part.type === 'text' ? {part.content}
: null),
+ structuredOutput: ({ part }) =>
+ part.type === 'structured-output' ? {part.raw} : null,
+ fallback: () => null,
},
tools: {
- getWeather: ({ part, result }) => {
- if (part.state === 'awaiting-input') return Waiting
- if (part.state === 'input-streaming') return Streaming input
- if (part.state === 'input-complete') return {part.input?.city}
- if (part.state === 'approval-requested') return Need approval
- if (part.state === 'approval-responded') return Responded
- if (part.state === 'error') return Error
- return (
-
- {part.input?.city}: {String(part.output?.temperature ?? result?.content)}
-
- )
- },
+ getWeather: ({ part }) => {part.input?.city}
,
purchaseItem: ({ part, interrupt }) => (
{part.input?.item}
@@ -156,390 +130,37 @@ const components = UI.defineComponents({
fallback: ({ interrupt }) =>
{interrupt.reason}
,
},
},
-})
+} satisfies ChatUIComponents
export function ChatScreen() {
const chat = useChat(chatOptions)
- return
+ return
}
```
## Type a component in its own file
-A tool map grows fast. Move a tool into its own file and type the props with `ToolProps`.
-
-`ToolProps` takes your `chatOptions` type and the tool name. Then `part.input` and `part.output` stay exact.
-
-Part components work the same way. `PartProps` already has a text part. You do not check `part.type`. Use `'structuredOutput'`, `'thinking'`, `'toolResult'`, and the other keys from the `parts` map. `fallback` still sees every part type.
+Use `ToolProps`, `PartProps`, `InterruptProps`, `LayoutProps`, or `InputProps` with `typeof chatOptions`.
```tsx
-import { fetchServerSentEvents } from '@tanstack/ai-react'
-import { createChatUI, type PartProps, type ToolProps } from '@tanstack/ai-react-ui'
-import { toolDefinition } from '@tanstack/ai'
-import { z } from 'zod'
-
-const getWeather = toolDefinition({
- name: 'getWeather',
- description: 'Look up weather',
- inputSchema: z.object({ city: z.string() }),
- outputSchema: z.object({ temperature: z.number() }),
-}).client()
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
- tools: [getWeather],
-}
+import type { ToolProps } from '@tanstack/ai-react/ui'
+import type { chatOptions } from './options'
export function WeatherTool({
part,
- result,
}: ToolProps) {
- if (part.state === 'awaiting-input') return Waiting
- if (part.state === 'input-streaming') return Streaming input
- if (part.state === 'error') return Error
- return (
-
- {part.input?.city}: {String(part.output?.temperature ?? result?.content)}
-
- )
-}
-
-export function TextPart({ part }: PartProps) {
- return {part.content}
-}
-
-const UI = createChatUI(chatOptions)
-
-export const components = UI.defineComponents({
- layout: ({ renderMessages }) => renderMessages(),
- message: ({ renderParts }) => {renderParts()} ,
- parts: { text: TextPart, fallback: () => null },
- tools: { getWeather: WeatherTool },
-})
-```
-
-1. Put `chatOptions` in a shared module.
-2. Import `ToolProps` or `PartProps` from `@tanstack/ai-react-ui`.
-3. Type the component with `ToolProps` or `PartProps`.
-4. Pass that component into `tools.getWeather` or `parts.text`.
-
-For an interrupt, use `InterruptProps`. Pass a tool name or a registered interrupt id as the second type argument. Then you do not check `interrupt.kind`.
-
-- A tool approval: `InterruptProps`. Then `interrupt.toolName` is `'purchaseItem'`.
-- A registered generic interrupt: `InterruptProps`. Then `interrupt.payload` and `interrupt.resolveInterrupt` match the definition.
-
-```tsx
-import { fetchServerSentEvents } from '@tanstack/ai-react'
-import { createChatUI, type InterruptProps } from '@tanstack/ai-react-ui'
-import { defineInterrupt } from '@tanstack/ai'
-import { z } from 'zod'
-
-const choosePlan = defineInterrupt({
- id: 'choosePlan',
- payloadSchema: z.object({ title: z.string() }),
- responseSchema: z.string(),
-})
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
- interrupts: [choosePlan],
-}
-
-export function ChoosePlan({
- interrupt,
-}: InterruptProps) {
- return (
- interrupt.resolveInterrupt('approved')}>
- {interrupt.payload?.title ?? 'Choose plan'}
-
- )
-}
-
-const UI = createChatUI(chatOptions)
-
-export const components = UI.defineComponents({
- layout: ({ renderInterrupts }) => renderInterrupts(),
- message: ({ renderParts }) => {renderParts()} ,
- parts: { fallback: () => null },
- interrupts: {
- generic: {
- choosePlan: ChoosePlan,
- },
- },
-})
-```
-
-Other prop types from the same package:
-
-- `LayoutProps`
-- `MessageProps`
-- `InputProps`
-- `PartProps` with a part key such as `'text'`
-- `InterruptProps` for tool approvals, registered generic interrupts, and `generic.fallback`. Pass a tool name or interrupt id as the second type argument.
-
-## Read chat from `UI.useChat()`
-
-Mapped components do not receive `chat` as a prop. Call `UI.useChat()` inside a component when it needs live chat. That call opts the component into chat re-renders. Nested children can call it too.
-
-```tsx
-import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
-import { createChatUI } from '@tanstack/ai-react-ui'
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
-}
-
-const UI = createChatUI(chatOptions)
-
-function StatusLine() {
- const chat = UI.useChat()
- if (chat.error) return {chat.error.message}
- if (chat.isLoading) return Loading
- return {chat.messages.length} messages
-}
-
-const components = UI.defineComponents({
- layout: ({ renderMessages, renderInput }) => (
-
-
- {renderMessages()}
- {renderInput()}
-
- ),
- message: ({ renderParts }) => {renderParts()} ,
- parts: { fallback: () => null },
-})
-
-export function ChatScreen() {
- const chat = useChat(chatOptions)
- return
-}
-```
-
-Call `UI.useChat()` only inside `UI.Chat` or `UI.Provider`. A call outside that tree throws.
-
-`useChat(chatOptions)` from `@tanstack/ai-react` still owns the state. `UI.useChat()` only reads the instance that you passed into the provider.
-
-## Tool approvals: inline or list
-
-A tool with `needsApproval: true` can render its approval in two places.
-
-### Inline, next to the tool
-
-Read `interrupt` on the tool. Render the approval in that same component. Do not register `interrupts.tools` for that name. A mapped tool keeps its approval off the list.
-
-`interrupt` is already the approval for that tool name. You do not check `interrupt.kind`.
-
-```tsx
-import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
-import { createChatUI, type ToolProps } from '@tanstack/ai-react-ui'
-import { toolDefinition } from '@tanstack/ai'
-import { z } from 'zod'
-
-const purchaseItem = toolDefinition({
- name: 'purchaseItem',
- description: 'Buy an item',
- needsApproval: true,
- inputSchema: z.object({ item: z.string() }),
- outputSchema: z.object({ ok: z.boolean() }),
-}).client()
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
- tools: [purchaseItem],
-}
-
-const UI = createChatUI(chatOptions)
-
-function PurchaseItem({
- part,
- interrupt,
-}: ToolProps) {
- return (
-
- {part.input?.item}
- {interrupt?.status === 'pending' ? (
- interrupt.resolveInterrupt(true)}>
- Approve
-
- ) : null}
-
- )
-}
-
-const components = UI.defineComponents({
- layout: ({ renderMessages, renderInterrupts }) => (
-
- {renderMessages()}
- {renderInterrupts()}
-
- ),
- message: ({ renderParts }) => {renderParts()} ,
- parts: { fallback: () => null },
- tools: {
- purchaseItem: PurchaseItem,
- },
-})
-
-export function InlineApprovalChat() {
- const chat = useChat(chatOptions)
- return
+ return {part.input?.city}
}
```
-To split the approval into its own file, type it with `InterruptProps`. Render that component from the tool.
-
-### List, in `renderInterrupts()`
-
-Register the approval under `interrupts.tools`. That component appears in the interrupt list. Do not also render `interrupt` on the tool unless you want it in both places.
-
-```tsx
-import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
-import { createChatUI } from '@tanstack/ai-react-ui'
-import { toolDefinition } from '@tanstack/ai'
-import { z } from 'zod'
-
-const purchaseItem = toolDefinition({
- name: 'purchaseItem',
- description: 'Buy an item',
- needsApproval: true,
- inputSchema: z.object({ item: z.string() }),
- outputSchema: z.object({ ok: z.boolean() }),
-}).client()
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
- tools: [purchaseItem],
-}
-
-const UI = createChatUI(chatOptions)
-
-const components = UI.defineComponents({
- layout: ({ renderMessages, renderInterrupts }) => (
-
- {renderMessages()}
- {renderInterrupts()}
-
- ),
- message: ({ renderParts }) => {renderParts()} ,
- parts: { fallback: () => null },
- tools: {
- purchaseItem: ({ part }) => {part.input?.item}
,
- },
- interrupts: {
- tools: {
- purchaseItem: ({ interrupt }) => (
- interrupt.resolveInterrupt(true)}>
- Approve
-
- ),
- },
- },
-})
-
-export function ListApprovalChat() {
- const chat = useChat(chatOptions)
- return
-}
-```
+`layout` and `input` receive `chat` as a prop. Nested children can call `useChatContext()` inside `Chat`.
-## Generic interrupts
+## Tool approvals
-Generic interrupts always render in the list (`renderInterrupts()` / ``). They never render inside a tool.
+A mapped tool that is not listed under `interrupts.tools` keeps its approval off the list. Read `interrupt` on the tool.
-Map them under `interrupts.generic`:
-
-- A registered id such as `choosePlan`: the component for that definition
-- `fallback`: every other list interrupt, including an unknown generic id and an unbound interrupt this chat does not own
-
-```tsx
-import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
-import { createChatUI } from '@tanstack/ai-react-ui'
-import { defineInterrupt } from '@tanstack/ai'
-import { z } from 'zod'
-
-const choosePlan = defineInterrupt({
- id: 'choosePlan',
- payloadSchema: z.object({ title: z.string() }),
- responseSchema: z.string(),
-})
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
- interrupts: [choosePlan],
-}
-
-const UI = createChatUI(chatOptions)
-
-const components = UI.defineComponents({
- layout: ({ renderInterrupts }) => renderInterrupts(),
- message: ({ renderParts }) => {renderParts()} ,
- parts: { fallback: () => null },
- interrupts: {
- generic: {
- choosePlan: ({ interrupt }) => (
- interrupt.resolveInterrupt('approved')}>
- {interrupt.payload?.title ?? 'Choose plan'}
-
- ),
- fallback: ({ interrupt }) =>
- interrupt.kind === 'unbound' ? (
- Paused elsewhere: {interrupt.reason}
- ) : (
- {interrupt.reason}
- ),
- },
- },
-})
-
-export function GenericInterruptChat() {
- const chat = useChat(chatOptions)
- return
-}
-```
-
-You can mix this map with `interrupts.tools` in the same `defineComponents` call.
-
-## Manual traversal
-
-```tsx
-import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
-import { createChatUI } from '@tanstack/ai-react-ui'
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
-}
-
-const UI = createChatUI(chatOptions)
-
-const components = UI.defineComponents({
- layout: ({ renderMessages }) => renderMessages(),
- message: ({ renderParts }) => {renderParts()} ,
- parts: { fallback: () => null },
-})
-
-export function ManualChat() {
- const chat = useChat(chatOptions)
- return (
-
-
- {(messages) =>
- messages.map((message) => (
-
- {(parts) =>
- parts.map((part, index) => (
- {part.key}
- ))
- }
-
- ))
- }
-
-
- )
-}
-```
+If you register `interrupts.tools.purchaseItem`, that component renders in `renderInterrupts()` instead.
-Unknown runtime tool names warn once in development and render nothing. Add a `parts.fallback` for unknown part types.
+Generic interrupts always render in the list.
-See also [Solid](./solid), [Vue](./vue), [Svelte](./svelte), and [custom adapters](./custom-adapters).
+Unknown runtime tool names warn once in development and render nothing. Add `parts.fallback` for unknown part types.
diff --git a/docs/ui/solid.md b/docs/ui/solid.md
index b5fcaeb36e..1c52e96a92 100644
--- a/docs/ui/solid.md
+++ b/docs/ui/solid.md
@@ -2,18 +2,18 @@
title: Solid Chat UI
id: typed-headless-ui-solid
order: 2
-description: "Build a typed, headless Solid chat UI with createChatUI. Accessors stay tracked. Your app owns useChat."
+description: "Render a typed chat tree with Chat and a component map. Keep accessors tracked."
keywords:
- tanstack ai
- - createChatUI
+ - Chat
- solid
- headless ui
- ToolProps
---
-Install `@tanstack/ai-solid-ui`, then call `createChatUI(chatOptions)` once at module scope. Do not destructure reactive props.
+Install `@tanstack/ai-solid`. Import `Chat` from `@tanstack/ai-solid/ui`. Call `useChat(chatOptions)` in the screen. Pass a module-level `components` object into `Chat`. Do not destructure reactive props.
-`defineComponents` needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional.
+> **Deprecated.** Do not install `@tanstack/ai-solid-ui`. That package re-exports this subpath until 1.0.0. See [Chat UI packages](../migration/create-ui).
The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI text adapter.
@@ -21,7 +21,7 @@ The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI
```tsx
import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid'
-import { createChatUI } from '@tanstack/ai-solid-ui'
+import { Chat, type ChatUIComponents } from '@tanstack/ai-solid/ui'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
@@ -37,9 +37,7 @@ const chatOptions = {
tools: [getWeather],
}
-const UI = createChatUI(chatOptions)
-
-const components = UI.defineComponents({
+const components = {
layout: (props) => (
<>
{props.renderMessages()}
@@ -54,100 +52,12 @@ const components = UI.defineComponents({
tools: {
getWeather: (props) => {props.part.input?.city} ,
},
-})
+} satisfies ChatUIComponents
-export function ChatScreen() {
+export function Support() {
const chat = useChat(chatOptions)
- return
-}
-```
-
-## Type a component in its own file
-
-Use `ToolProps` the same way as React. Keep the `props` object so Solid can track it.
-
-```tsx
-import { fetchServerSentEvents } from '@tanstack/ai-solid'
-import { createChatUI, type ToolProps } from '@tanstack/ai-solid-ui'
-import { toolDefinition } from '@tanstack/ai'
-import { z } from 'zod'
-
-const getWeather = toolDefinition({
- name: 'getWeather',
- description: 'Look up weather',
- inputSchema: z.object({ city: z.string() }),
- outputSchema: z.object({ temperature: z.number() }),
-}).client()
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
- tools: [getWeather],
-}
-
-export function WeatherTool(
- props: ToolProps,
-) {
- return {props.part.input?.city}
+ return
}
-
-const UI = createChatUI(chatOptions)
-
-export const components = UI.defineComponents({
- layout: (props) => props.renderMessages(),
- message: (props) => {props.renderParts()} ,
- parts: { fallback: () => null },
- tools: { getWeather: WeatherTool },
-})
```
-Part components use `PartProps`. Then `part` is already a text part.
-
-Interrupt components use `InterruptProps`. Then `interrupt.payload` matches the definition.
-
-Mapped components do not receive `chat` as a prop. Call `UI.useChat()` when a component needs live chat. That call opts the component into chat updates. Nested children can call it too.
-
-## Read chat from `UI.useChat()`
-
-```tsx
-import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid'
-import { createChatUI } from '@tanstack/ai-solid-ui'
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
-}
-
-const UI = createChatUI(chatOptions)
-
-function StatusLine() {
- const chat = UI.useChat()
- return {chat.messages.length} messages
-}
-
-const components = UI.defineComponents({
- layout: (props) => (
- <>
-
- {props.renderMessages()}
- >
- ),
- message: (props) => {props.renderParts()} ,
- parts: { fallback: () => null },
-})
-
-export function ChatScreen() {
- const chat = useChat(chatOptions)
- return
-}
-```
-
-Call `UI.useChat()` only inside `UI.Chat` or `UI.Provider`.
-
-## Interrupts
-
-Tool approvals sit in the tool when you read `props.interrupt`. Put a component on `interrupts.tools` to send that approval to the list instead. Generic interrupts always sit in the list under `interrupts.generic`: `{ choosePlan, fallback }`. An unbound interrupt uses `fallback`. Branch on `interrupt.kind === 'unbound'` if the copy must differ.
-
-The full map is on the [React page](./react).
-
-Manual list: `{(messages) => {messages().length} } `.
-
-Pass `props.chat`, `props.part`, and `props.renderParts()` without destructure.
+Use `ToolProps` the same way as React. Keep the `props` object so Solid can track it. `layout` and `input` receive `chat` as a prop. Nested children can call `useChatContext()` inside `Chat`.
diff --git a/docs/ui/svelte.md b/docs/ui/svelte.md
index 1fea54dca2..c1f13a4c9d 100644
--- a/docs/ui/svelte.md
+++ b/docs/ui/svelte.md
@@ -2,18 +2,16 @@
title: Svelte Chat UI
id: typed-headless-ui-svelte
order: 4
-description: "Build a typed, headless Svelte 5 chat UI with createChatUI, snippets, and static components."
+description: "Render a typed chat tree with Chat, snippets, and a component map."
keywords:
- tanstack ai
- - createChatUI
+ - Chat
- svelte
- headless ui
- ToolProps
---
-Install `@tanstack/ai-svelte-ui`. Call `createChatUI(chatOptions)` once. Pass `{ui}`, `{chat}`, and `{components}` into `UIChat`.
-
-`defineComponents` needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional.
+Install `@tanstack/ai-svelte`. Import `Chat` from `@tanstack/ai-svelte/ui`. Call `createChat(chatOptions)` in the page. Pass `{chat}` and `components` into `Chat`.
The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI text adapter.
@@ -21,81 +19,31 @@ The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI
```svelte
-
-
-```
-
-`Layout.svelte` receives snippets `messages`, `interrupts`, and `input`. `Message.svelte` receives snippet `parts`. A tool with an approval receives prop `interrupt`.
-
-## Type a component in its own file
-
-Type the `$props()` of a tool file with `ToolProps`. Share the same `chatOptions` module that you pass to `createChatUI`.
-
-```svelte
-
-
-{part.input?.city}
-```
-
-Part components use `PartProps`. Then `part` is already a text part.
-
-Interrupt components use `InterruptProps`. Then `interrupt.payload` matches the definition.
-
-Mapped components do not receive `chat` as a prop. Call `ui.useChat()` when a component needs live chat. That call opts the component into chat updates. Nested children can call it too.
-
-## Read chat from `ui.useChat()`
-
-Import the same `ui` descriptor in a child file. Call `ui.useChat()` only under `UIChat` or `UIProvider`.
-
-```svelte
-
-{chat.messages.length} messages
+
```
-`createChat(chatOptions)` owns the state. `ui.useChat()` reads the instance you passed into `UIChat`. A call outside that tree throws.
-
-## Interrupts
-
-Tool approvals sit in the tool when you read the `interrupt` prop. Put a component on `interrupts.tools` to send that approval to the list instead. Generic interrupts always sit in the list under `interrupts.generic`: `{ choosePlan, fallback }`. An unbound interrupt uses `fallback`. Branch on `interrupt.kind === 'unbound'` if the copy must differ.
+Layout receives a `chat` prop and `messages`, `interrupts`, and `input` snippets. Message receives a `parts` snippet.
-The full map is on the [React page](./react).
+Type a tool file with `ToolProps`. Share the same `chatOptions` module that you pass to `createChat`.
diff --git a/docs/ui/vue.md b/docs/ui/vue.md
index 0624e31bb6..5e80a2c8e1 100644
--- a/docs/ui/vue.md
+++ b/docs/ui/vue.md
@@ -2,18 +2,18 @@
title: Vue Chat UI
id: typed-headless-ui-vue
order: 3
-description: "Build a typed, headless Vue chat UI with createChatUI and static primitives. Slots replace render callbacks."
+description: "Render a typed chat tree with Chat and a component map. Slots replace render callbacks."
keywords:
- tanstack ai
- - createChatUI
+ - Chat
- vue
- headless ui
- ToolProps
---
-Install `@tanstack/ai-vue-ui`. Call `createChatUI(chatOptions)` once. Pass the descriptor as `ui` into `UIChat`, `UIProvider`, and the other static primitives.
+Install `@tanstack/ai-vue`. Import `Chat` from `@tanstack/ai-vue/ui`. Call `useChat(chatOptions)` in the screen. Pass a `components` object into `Chat`. Layout uses `messages`, `interrupts`, and `input` slots. Message uses a `parts` slot.
-`defineComponents` needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional.
+> **Deprecated.** Do not install `@tanstack/ai-vue-ui`. That package re-exports this subpath until 1.0.0. See [Chat UI packages](../migration/create-ui).
The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI text adapter.
@@ -22,7 +22,7 @@ The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI
```ts
import { defineComponent, h } from 'vue'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-vue'
-import { createChatUI, UIChat } from '@tanstack/ai-vue-ui'
+import { Chat } from '@tanstack/ai-vue/ui'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
@@ -38,13 +38,13 @@ const chatOptions = {
tools: [getWeather],
}
-const ui = createChatUI(chatOptions)
-
-const components = ui.defineComponents({
+const components = {
layout: defineComponent((_, { slots }) => () =>
- h('div', [slots.messages?.(), slots.interrupts?.(), slots.input?.()]),
+ h('main', [slots.messages?.(), slots.input?.()]),
+ ),
+ message: defineComponent((_, { slots }) => () =>
+ h('article', slots.parts?.()),
),
- message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())),
parts: {
fallback: defineComponent({
props: ['part'],
@@ -61,110 +61,14 @@ const components = ui.defineComponents({
},
}),
},
-})
-
-export default defineComponent({
- setup() {
- const chat = useChat(chatOptions)
- return () => h(UIChat, { ui, chat, components })
- },
-})
-```
-
-Layout uses slots `messages`, `interrupts`, and `input`. Message uses slot `parts`. Manual lists use the default slot on `UIMessages` with `{ messages }`.
-
-## Type a component in its own file
-
-Use `ToolProps` on the component props. Share the same `chatOptions` object that you pass to `createChatUI`.
-
-```ts
-import { defineComponent, h } from 'vue'
-import { fetchServerSentEvents } from '@tanstack/ai-vue'
-import { createChatUI, type ToolProps } from '@tanstack/ai-vue-ui'
-import { toolDefinition } from '@tanstack/ai'
-import { z } from 'zod'
-
-const getWeather = toolDefinition({
- name: 'getWeather',
- description: 'Look up weather',
- inputSchema: z.object({ city: z.string() }),
- outputSchema: z.object({ temperature: z.number() }),
-}).client()
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
- tools: [getWeather],
-}
-
-export const WeatherTool = defineComponent(
- (props: ToolProps) => {
- return () => h('strong', props.part.input?.city)
- },
-)
-
-const ui = createChatUI(chatOptions)
-
-export const components = ui.defineComponents({
- layout: defineComponent((_, { slots }) => () =>
- h('div', [slots.messages?.(), slots.interrupts?.(), slots.input?.()]),
- ),
- message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())),
- parts: { fallback: defineComponent(() => () => null) },
- tools: { getWeather: WeatherTool },
-})
-```
-
-Part components use `PartProps`. Then `part` is already a text part.
-
-Interrupt components use `InterruptProps`. Then `interrupt.payload` matches the definition.
-
-Mapped components do not receive `chat` as a prop. Call `ui.useChat()` when a component needs live chat. That call opts the component into chat updates. Nested children can call it too.
-
-## Read chat from `ui.useChat()`
-
-Call `ui.useChat()` inside a child of `UIChat` or `UIProvider`.
-
-```ts
-import { defineComponent, h } from 'vue'
-import { fetchServerSentEvents, useChat } from '@tanstack/ai-vue'
-import { createChatUI, UIChat } from '@tanstack/ai-vue-ui'
-
-const chatOptions = {
- connection: fetchServerSentEvents('/api/chat'),
}
-const ui = createChatUI(chatOptions)
-
-const StatusLine = defineComponent({
- setup() {
- const chat = ui.useChat()
- return () => {
- const messages = Array.isArray(chat.messages) ? chat.messages : []
- return h('p', String(messages.length) + ' messages')
- }
- },
-})
-
-const components = ui.defineComponents({
- layout: defineComponent((_, { slots }) => () =>
- h('main', [h(StatusLine), slots.messages?.()]),
- ),
- message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())),
- parts: { fallback: defineComponent(() => () => null) },
-})
-
export default defineComponent({
setup() {
const chat = useChat(chatOptions)
- return () => h(UIChat, { ui, chat, components })
+ return () => h(Chat, { chat, components })
},
})
```
-`useChat(chatOptions)` from `@tanstack/ai-vue` owns the state. `ui.useChat()` reads the instance you passed into `UIChat`.
-
-## Interrupts
-
-Tool approvals sit in the tool when you read the `interrupt` prop. Put a component on `interrupts.tools` to send that approval to the list instead. Generic interrupts always sit in the list under `interrupts.generic`: `{ choosePlan, fallback }`. An unbound interrupt uses `fallback`. Branch on `interrupt.kind === 'unbound'` if the copy must differ.
-
-The full map is on the [React page](./react).
+Use `ToolProps` on a tool component's props. `input` receives `chat` as a prop.
diff --git a/examples/README.md b/examples/README.md
index d23370be39..2e03103fdd 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -164,7 +164,7 @@ A React SPA that connects to **Go, Rust, PHP, Zig, Bash, and Python chat servers
**Tech Stack:**
- React + Vite (SPA)
-- `@tanstack/ai-react` + `@tanstack/ai-react-ui`
+- `@tanstack/ai-react` + `@tanstack/ai-react/ui`
- Go chat server (`net/http`, `:8001`)
- Rust chat server (Axum, `:8002`)
- PHP chat server (built-in server + curl, `:8003`)
diff --git a/examples/ag-ui/README.md b/examples/ag-ui/README.md
index 770997aaf1..38f4bd949e 100644
--- a/examples/ag-ui/README.md
+++ b/examples/ag-ui/README.md
@@ -8,7 +8,7 @@ This example shows that any backend can serve `@tanstack/ai-react` clients as lo
| Layer | Stack |
| ------------- | ------------------------------------------------------------------------ |
-| Client | React, Vite, `@tanstack/ai-react`, `@tanstack/ai-react-ui` |
+| Client | React, Vite, `@tanstack/ai-react`, `@tanstack/ai-react/ui` |
| Go server | `net/http`, hand-rolled AG-UI SSE, OpenAI/Anthropic streaming on `:8001` |
| Rust server | Axum, hand-rolled AG-UI SSE, OpenAI/Anthropic streaming on `:8002` |
| PHP server | Built-in PHP server + curl, hand-rolled AG-UI SSE on `:8003` |
diff --git a/examples/ag-ui/package.json b/examples/ag-ui/package.json
index 5be67b3556..3e8c09fcd0 100644
--- a/examples/ag-ui/package.json
+++ b/examples/ag-ui/package.json
@@ -21,7 +21,6 @@
"dependencies": {
"@tanstack/ai-client": "workspace:*",
"@tanstack/ai-react": "workspace:*",
- "@tanstack/ai-react-ui": "workspace:*",
"react": "^19.2.3",
"react-dom": "^19.2.3"
},
diff --git a/examples/ag-ui/src/App.tsx b/examples/ag-ui/src/App.tsx
index 84b0f5180e..95bfee6534 100644
--- a/examples/ag-ui/src/App.tsx
+++ b/examples/ag-ui/src/App.tsx
@@ -1,43 +1,38 @@
-import { useEffect, useMemo, useState } from 'react'
+import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
import type { ConnectionAdapter } from '@tanstack/ai-react'
-import { ChatMessage, createChatUI } from '@tanstack/ai-react-ui'
+import { Chat } from '@tanstack/ai-react/ui'
+import type { ChatUIComponents } from '@tanstack/ai-react/ui'
-const UI = createChatUI({})
+const LabelsContext = createContext({ placeholder: '', emptyLabel: '' })
-function AgUiChat({
- connection,
- placeholder,
- emptyLabel,
-}: {
- connection: ConnectionAdapter
- placeholder: string
- emptyLabel: string
-}) {
- const chat = useChat({ connection })
- const [draft, setDraft] = useState('')
- const components = UI.defineComponents({
- layout: ({ renderMessages, renderInput }) => {
- const current = UI.useChat()
- return (
-
- {current.error ? (
-
{current.error.message}
- ) : current.messages.length === 0 ? (
-
- {emptyLabel}
-
- ) : (
-
- {renderMessages()}
-
- )}
-
{renderInput()}
-
- )
- },
- message: ({ message }) => ,
- input: () => (
+const chatComponents = {
+ layout: ({ chat, renderMessages, renderInput }) => {
+ const { emptyLabel } = useContext(LabelsContext)
+ return (
+
+ {chat.error ? (
+
{chat.error.message}
+ ) : chat.messages.length === 0 ? (
+
+ {emptyLabel}
+
+ ) : (
+
+ {renderMessages()}
+
+ )}
+
{renderInput()}
+
+ )
+ },
+ message: ({ message, renderParts }) => (
+ {renderParts()}
+ ),
+ input: function Input({ chat }) {
+ const { placeholder } = useContext(LabelsContext)
+ const [draft, setDraft] = useState('')
+ return (
- ),
- parts: { fallback: () => null },
- })
- return
+ )
+ },
+ parts: {
+ text: ({ part }) => (part.type === 'text' ? {part.content}
: null),
+ fallback: () => null,
+ },
+} satisfies ChatUIComponents<{}>
+
+function AgUiChat({
+ connection,
+ placeholder,
+ emptyLabel,
+}: {
+ connection: ConnectionAdapter
+ placeholder: string
+ emptyLabel: string
+}) {
+ const chat = useChat({ connection })
+ return (
+
+
+
+ )
}
type Backend = 'go' | 'rust' | 'php' | 'zig' | 'bash' | 'python'
diff --git a/examples/ts-react-chat/package.json b/examples/ts-react-chat/package.json
index f685519340..18faaadfcf 100644
--- a/examples/ts-react-chat/package.json
+++ b/examples/ts-react-chat/package.json
@@ -42,7 +42,6 @@
"@tanstack/ai-openrouter": "workspace:*",
"@tanstack/ai-persistence": "workspace:*",
"@tanstack/ai-react": "workspace:*",
- "@tanstack/ai-react-ui": "workspace:*",
"@tanstack/ai-sandbox": "workspace:*",
"@tanstack/ai-sandbox-daytona": "workspace:*",
"@tanstack/ai-sandbox-docker": "workspace:*",
diff --git a/examples/ts-react-chat/src/routes/index.tsx b/examples/ts-react-chat/src/routes/index.tsx
index c40d1a01f9..9fdca10439 100644
--- a/examples/ts-react-chat/src/routes/index.tsx
+++ b/examples/ts-react-chat/src/routes/index.tsx
@@ -36,7 +36,7 @@ import {
startOpenRouterPkceLogin,
} from '@tanstack/ai-openrouter/pkce'
import { clientTools } from '@tanstack/ai-client'
-import { ThinkingPart } from '@tanstack/ai-react-ui'
+import { ThinkingPart } from '@tanstack/ai-react/ui'
import type { BoundInterrupts } from '@tanstack/ai-client'
import type { UIMessage } from '@tanstack/ai-react'
import type { ContentPart } from '@tanstack/ai'
diff --git a/examples/ts-react-chat/src/routes/mcp-demo.tsx b/examples/ts-react-chat/src/routes/mcp-demo.tsx
index 2bc99b5c6e..d3d196b558 100644
--- a/examples/ts-react-chat/src/routes/mcp-demo.tsx
+++ b/examples/ts-react-chat/src/routes/mcp-demo.tsx
@@ -7,7 +7,7 @@ import rehypeSanitize from 'rehype-sanitize'
import rehypeHighlight from 'rehype-highlight'
import remarkGfm from 'remark-gfm'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
-import { ThinkingPart } from '@tanstack/ai-react-ui'
+import { ThinkingPart } from '@tanstack/ai-react/ui'
import type { UIMessage } from '@tanstack/ai-react'
import { MCP_PROVIDERS, type McpProvider } from '@/lib/mcp-providers'
diff --git a/examples/ts-react-chat/src/routes/threads.tsx b/examples/ts-react-chat/src/routes/threads.tsx
index 8889cd67fe..766d83b71c 100644
--- a/examples/ts-react-chat/src/routes/threads.tsx
+++ b/examples/ts-react-chat/src/routes/threads.tsx
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
import { clientTools } from '@tanstack/ai-client'
-import { ThinkingPart } from '@tanstack/ai-react-ui'
+import { ThinkingPart } from '@tanstack/ai-react/ui'
import ReactMarkdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize'
diff --git a/examples/ts-react-ui-chatbot/README.md b/examples/ts-react-ui-chatbot/README.md
index b7d7e829c7..da7794b899 100644
--- a/examples/ts-react-ui-chatbot/README.md
+++ b/examples/ts-react-ui-chatbot/README.md
@@ -1,6 +1,6 @@
# Typed headless chat + shadcn AI components
-One-route TanStack Start app. It uses `createChatUI()` from `@tanstack/ai-react-ui` and the chatbot pieces from [shadcn.io/ai/chatbot](https://www.shadcn.io/ai/chatbot): Conversation, Message, Prompt Input, Model Selector, Reasoning, Sources, plus Tool for approvals.
+One-route TanStack Start app. It uses `useChat` from `@tanstack/ai-react` and `Chat` from `@tanstack/ai-react/ui` with the chatbot pieces from [shadcn.io/ai/chatbot](https://www.shadcn.io/ai/chatbot): Conversation, Message, Prompt Input, Model Selector, Reasoning, Sources, plus Tool for approvals.
```bash
pnpm install
diff --git a/examples/ts-react-ui-chatbot/package.json b/examples/ts-react-ui-chatbot/package.json
index 9508019edc..a33bc233fd 100644
--- a/examples/ts-react-ui-chatbot/package.json
+++ b/examples/ts-react-ui-chatbot/package.json
@@ -15,7 +15,6 @@
"@tanstack/ai-client": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-react": "workspace:*",
- "@tanstack/ai-react-ui": "workspace:*",
"@tanstack/react-devtools": "^0.9.10",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-router-devtools": "^1.158.4",
diff --git a/examples/ts-react-ui-chatbot/src/chat/options.ts b/examples/ts-react-ui-chatbot/src/chat/options.ts
index 73870c0f50..7a5f8ffcde 100644
--- a/examples/ts-react-ui-chatbot/src/chat/options.ts
+++ b/examples/ts-react-ui-chatbot/src/chat/options.ts
@@ -1,5 +1,4 @@
import { fetchServerSentEvents } from '@tanstack/ai-react'
-import { createChatUI } from '@tanstack/ai-react-ui'
import { byok } from './byok'
import { chatInterrupts } from './interrupts'
import { itinerarySchema } from './schema'
@@ -13,5 +12,3 @@ export const chatOptions = {
byok,
byokProvider: () => 'openai' as const,
}
-
-export const UI = createChatUI(chatOptions)
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/book-stay-tool.tsx b/examples/ts-react-ui-chatbot/src/components/chat/book-stay-tool.tsx
index ace01c9535..096e413244 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/book-stay-tool.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/book-stay-tool.tsx
@@ -1,4 +1,4 @@
-import type { InterruptProps, ToolProps } from '@tanstack/ai-react-ui'
+import type { InterruptProps, ToolProps } from '@tanstack/ai-react/ui'
import { Button } from '@/components/ui/button'
import {
Tool,
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/choose-budget.tsx b/examples/ts-react-ui-chatbot/src/components/chat/choose-budget.tsx
index 4cdeabbff9..0cc24b8cd2 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/choose-budget.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/choose-budget.tsx
@@ -1,4 +1,4 @@
-import type { InterruptProps } from '@tanstack/ai-react-ui'
+import type { InterruptProps } from '@tanstack/ai-react/ui'
import { Button } from '@/components/ui/button'
import type { chatOptions } from '@/chat/options'
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/confirm-payment-tool.tsx b/examples/ts-react-ui-chatbot/src/components/chat/confirm-payment-tool.tsx
index 240c035860..4d1a6176de 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/confirm-payment-tool.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/confirm-payment-tool.tsx
@@ -1,4 +1,4 @@
-import type { InterruptProps, ToolProps } from '@tanstack/ai-react-ui'
+import type { InterruptProps, ToolProps } from '@tanstack/ai-react/ui'
import { Button } from '@/components/ui/button'
import {
Tool,
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/fallback-interrupt.tsx b/examples/ts-react-ui-chatbot/src/components/chat/fallback-interrupt.tsx
index 7548b2f81a..ce8b986256 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/fallback-interrupt.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/fallback-interrupt.tsx
@@ -1,4 +1,4 @@
-import type { InterruptProps } from '@tanstack/ai-react-ui'
+import type { InterruptProps } from '@tanstack/ai-react/ui'
import type { chatOptions } from '@/chat/options'
export function FallbackInterrupt({
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/fallback-part.tsx b/examples/ts-react-ui-chatbot/src/components/chat/fallback-part.tsx
index 1932e75ef9..2f46e96062 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/fallback-part.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/fallback-part.tsx
@@ -1,4 +1,4 @@
-import type { PartProps } from '@tanstack/ai-react-ui'
+import type { PartProps } from '@tanstack/ai-react/ui'
import type { chatOptions } from '@/chat/options'
export function FallbackPart({ part }: PartProps) {
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/input.tsx b/examples/ts-react-ui-chatbot/src/components/chat/input.tsx
index 19201c9b61..e8745fb0b5 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/input.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/input.tsx
@@ -13,7 +13,8 @@ import {
XIcon,
} from 'lucide-react'
import { byok } from '@/chat/byok'
-import { UI } from '@/chat/options'
+import type { InputProps } from '@tanstack/ai-react/ui'
+import type { chatOptions } from '@/chat/options'
import {
PromptInput,
PromptInputButton,
@@ -68,8 +69,7 @@ async function fileToDraft(file: File): Promise {
}
}
-export function ChatPromptInput() {
- const chat = UI.useChat()
+export function ChatPromptInput({ chat }: InputProps) {
const [model, setModel] = useState(selectedModel)
const [text, setText] = useState('')
const [drafts, setDrafts] = useState>([])
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/layout.tsx b/examples/ts-react-ui-chatbot/src/components/chat/layout.tsx
index 1857bd5bc0..fd15f798b8 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/layout.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/layout.tsx
@@ -1,5 +1,5 @@
import { CompassIcon } from 'lucide-react'
-import type { LayoutProps } from '@tanstack/ai-react-ui'
+import type { LayoutProps } from '@tanstack/ai-react/ui'
import type { ContentPart } from '@tanstack/ai/client'
import {
Conversation,
@@ -8,7 +8,6 @@ import {
ConversationScrollButton,
} from '@/components/ai/conversation'
import { Suggestion, Suggestions } from '@/components/ai/suggestions'
-import { UI } from '@/chat/options'
import type { chatOptions } from '@/chat/options'
const LISBON_PHOTO =
@@ -35,11 +34,11 @@ const PROMPTS: Array<{ label: string; content?: Array }> = [
]
export function ChatLayout({
+ chat,
renderMessages,
renderInterrupts,
renderInput,
}: LayoutProps) {
- const chat = UI.useChat()
if (chat.error) {
return {chat.error.message}
}
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/lookup-place-tool.tsx b/examples/ts-react-ui-chatbot/src/components/chat/lookup-place-tool.tsx
index f5c18a0412..1c2ef8d4ad 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/lookup-place-tool.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/lookup-place-tool.tsx
@@ -1,4 +1,4 @@
-import type { ToolProps } from '@tanstack/ai-react-ui'
+import type { ToolProps } from '@tanstack/ai-react/ui'
import {
Source,
Sources,
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/media-parts.tsx b/examples/ts-react-ui-chatbot/src/components/chat/media-parts.tsx
index 98f433dcd5..d10a57f218 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/media-parts.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/media-parts.tsx
@@ -1,4 +1,4 @@
-import type { PartProps } from '@tanstack/ai-react-ui'
+import type { PartProps } from '@tanstack/ai-react/ui'
import type { chatOptions } from '@/chat/options'
function sourceHref(source: {
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/message.tsx b/examples/ts-react-ui-chatbot/src/components/chat/message.tsx
index c65bf3c3c1..d32ed35068 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/message.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/message.tsx
@@ -1,4 +1,4 @@
-import type { MessageProps } from '@tanstack/ai-react-ui'
+import type { MessageProps } from '@tanstack/ai-react/ui'
import { Message, MessageAvatar, MessageContent } from '@/components/ai/message'
import type { chatOptions } from '@/chat/options'
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/structured-output.tsx b/examples/ts-react-ui-chatbot/src/components/chat/structured-output.tsx
index 4962289cce..fff90c4e68 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/structured-output.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/structured-output.tsx
@@ -1,4 +1,4 @@
-import type { PartProps } from '@tanstack/ai-react-ui'
+import type { PartProps } from '@tanstack/ai-react/ui'
import type { chatOptions } from '@/chat/options'
export function StructuredOutputPart({
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/text-part.tsx b/examples/ts-react-ui-chatbot/src/components/chat/text-part.tsx
index ed8b1c5f1c..277c212168 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/text-part.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/text-part.tsx
@@ -1,4 +1,4 @@
-import type { PartProps } from '@tanstack/ai-react-ui'
+import type { PartProps } from '@tanstack/ai-react/ui'
import { MessageResponse } from '@/components/ai/message'
import type { chatOptions } from '@/chat/options'
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/thinking-part.tsx b/examples/ts-react-ui-chatbot/src/components/chat/thinking-part.tsx
index 6fe0d4b86a..39ca921106 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/thinking-part.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/thinking-part.tsx
@@ -1,4 +1,4 @@
-import type { PartProps } from '@tanstack/ai-react-ui'
+import type { PartProps } from '@tanstack/ai-react/ui'
import {
Reasoning,
ReasoningContent,
diff --git a/examples/ts-react-ui-chatbot/src/components/chat/ui-components.tsx b/examples/ts-react-ui-chatbot/src/components/chat/ui-components.tsx
index f09b9e1fec..bf12ab861d 100644
--- a/examples/ts-react-ui-chatbot/src/components/chat/ui-components.tsx
+++ b/examples/ts-react-ui-chatbot/src/components/chat/ui-components.tsx
@@ -1,4 +1,5 @@
-import { UI } from '@/chat/options'
+import type { ChatUIComponents } from '@tanstack/ai-react/ui'
+import { chatOptions } from '@/chat/options'
import { BookStayApproval, BookStayTool } from './book-stay-tool'
import { ConfirmPaymentTool } from './confirm-payment-tool'
import { ChatPromptInput } from './input'
@@ -20,7 +21,7 @@ import { StructuredOutputPart } from './structured-output'
import { TextPart } from './text-part'
import { ThinkingPart } from './thinking-part'
-export const components = UI.defineComponents({
+export const chatComponents = {
layout: ChatLayout,
message: ChatMessage,
input: ChatPromptInput,
@@ -50,4 +51,4 @@ export const components = UI.defineComponents({
fallback: FallbackInterrupt,
},
},
-})
+} satisfies ChatUIComponents
diff --git a/examples/ts-react-ui-chatbot/src/routes/index.tsx b/examples/ts-react-ui-chatbot/src/routes/index.tsx
index 4006447d35..03457c1aa3 100644
--- a/examples/ts-react-ui-chatbot/src/routes/index.tsx
+++ b/examples/ts-react-ui-chatbot/src/routes/index.tsx
@@ -1,9 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import { useChat } from '@tanstack/ai-react'
-import { UI } from '@/chat/options'
+import { Chat } from '@tanstack/ai-react/ui'
import { chatOptions } from '@/chat/options'
import { selectedModel } from '@/chat/model'
-import { components } from '@/components/chat/ui-components'
+import { chatComponents } from '@/components/chat/ui-components'
export const Route = createFileRoute('/')({
component: TripDesk,
@@ -12,8 +12,8 @@ export const Route = createFileRoute('/')({
function TripDesk() {
const chat = useChat({
...chatOptions,
- get forwardedProps() {
- return { model: selectedModel }
+ forwardedProps: {
+ model: selectedModel,
},
})
@@ -32,7 +32,7 @@ function TripDesk() {
src="/brand/logos/tanstack-landscape-white.svg"
/>
-
+
)
}
diff --git a/examples/ts-solid-chat/package.json b/examples/ts-solid-chat/package.json
index c00979bd29..bccf27dce9 100644
--- a/examples/ts-solid-chat/package.json
+++ b/examples/ts-solid-chat/package.json
@@ -19,7 +19,6 @@
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-solid": "workspace:*",
- "@tanstack/ai-solid-ui": "workspace:*",
"@tanstack/nitro-v2-vite-plugin": "^1.155.0",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/solid-ai-devtools": "workspace:*",
diff --git a/examples/ts-solid-chat/src/routes/index.tsx b/examples/ts-solid-chat/src/routes/index.tsx
index fba21a969d..9023b4d27d 100644
--- a/examples/ts-solid-chat/src/routes/index.tsx
+++ b/examples/ts-solid-chat/src/routes/index.tsx
@@ -3,7 +3,7 @@ import Send from 'lucide-solid/icons/send'
import Square from 'lucide-solid/icons/square'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid'
import { createChatClientOptions } from '@tanstack/ai-client'
-import { TextPart, ThinkingPart } from '@tanstack/ai-solid-ui'
+import { TextPart, ThinkingPart } from '@tanstack/ai-solid/ui'
import { For, createSignal } from 'solid-js'
import type { InferChatMessages, UIMessage } from '@tanstack/ai-client'
diff --git a/examples/ts-solid-chat/src/routes/solid-ui.tsx b/examples/ts-solid-chat/src/routes/solid-ui.tsx
index 017a61e989..c95a0c50ed 100644
--- a/examples/ts-solid-chat/src/routes/solid-ui.tsx
+++ b/examples/ts-solid-chat/src/routes/solid-ui.tsx
@@ -1,6 +1,7 @@
import { createFileRoute } from '@tanstack/solid-router'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid'
-import { createChatUI } from '@tanstack/ai-solid-ui'
+import { Chat } from '@tanstack/ai-solid/ui'
+import type { ChatUIComponents } from '@tanstack/ai-solid/ui'
import { createSignal } from 'solid-js'
import { clientTools } from '@/lib/guitar-tools'
@@ -9,26 +10,23 @@ const chatOptions = {
tools: clientTools,
}
-const UI = createChatUI(chatOptions)
-
-function SolidUIPage() {
- const chat = useChat(chatOptions)
- const [draft, setDraft] = createSignal('')
- const components = UI.defineComponents({
- layout: (props) => (
-
-
- {props.renderMessages()}
-
- {props.renderInput()}
+const chatComponents = {
+ layout: (props) => (
+
+
+ {props.renderMessages()}
- ),
- message: (props) => (
-
- {props.renderParts()}
-
- ),
- input: () => (
+ {props.renderInput()}
+
+ ),
+ message: (props) => (
+
+ {props.renderParts()}
+
+ ),
+ input: function Input(props) {
+ const [draft, setDraft] = createSignal('')
+ return (
+ )
+ },
+ parts: {
+ fallback: (props) =>
+ props.part.type === 'text' ?
{props.part.content}
: null,
+ },
+ tools: {
+ recommendGuitar: (props) =>
{props.part.input?.id}
,
+ getPersonalGuitarPreference: (props) => (
+
{props.part.output?.preference}
),
- parts: {
- fallback: (props) =>
- props.part.type === 'text' ?
{props.part.content}
: null,
- },
- tools: {
- recommendGuitar: (props) =>
{props.part.input?.id}
,
- getPersonalGuitarPreference: (props) => (
-
{props.part.output?.preference}
- ),
- addToWishList: (props) => (
-
- {props.part.input?.guitarId}
- {props.interrupt?.status === 'pending' ? (
- props.interrupt?.resolveInterrupt(true)}
- >
- Approve
-
- ) : null}
-
- ),
- addToCart: (props) => (
-
- {props.part.input?.guitarId}
- {props.interrupt?.status === 'pending' ? (
- props.interrupt?.resolveInterrupt(true)}
- >
- Approve
-
- ) : null}
-
- ),
- },
- })
+ addToWishList: (props) => (
+
+ {props.part.input?.guitarId}
+ {props.interrupt?.status === 'pending' ? (
+ props.interrupt?.resolveInterrupt(true)}
+ >
+ Approve
+
+ ) : null}
+
+ ),
+ addToCart: (props) => (
+
+ {props.part.input?.guitarId}
+ {props.interrupt?.status === 'pending' ? (
+ props.interrupt?.resolveInterrupt(true)}
+ >
+ Approve
+
+ ) : null}
+
+ ),
+ },
+} satisfies ChatUIComponents
- return
+function SolidUIPage() {
+ const chat = useChat(chatOptions)
+ return
}
export const Route = createFileRoute('/solid-ui')({
diff --git a/examples/ts-solid-chat/vite.config.ts b/examples/ts-solid-chat/vite.config.ts
index b7c77c0506..c248359822 100644
--- a/examples/ts-solid-chat/vite.config.ts
+++ b/examples/ts-solid-chat/vite.config.ts
@@ -10,7 +10,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
const config = defineConfig({
ssr: {
- noExternal: ['@tanstack/ai-solid-ui'],
+ noExternal: ['@tanstack/ai-solid/ui'],
},
resolve: {
tsconfigPaths: true,
diff --git a/examples/ts-svelte-chat/package.json b/examples/ts-svelte-chat/package.json
index 5ce76cb7c7..80a3e08237 100644
--- a/examples/ts-svelte-chat/package.json
+++ b/examples/ts-svelte-chat/package.json
@@ -20,7 +20,6 @@
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-svelte": "workspace:*",
- "@tanstack/ai-svelte-ui": "workspace:*",
"highlight.js": "^11.11.1",
"lucide-svelte": "^0.468.0",
"marked": "^15.0.6",
diff --git a/examples/ts-svelte-chat/src/routes/svelte-ui/+page.svelte b/examples/ts-svelte-chat/src/routes/svelte-ui/+page.svelte
index 137b8d005c..bfdb10f3c5 100644
--- a/examples/ts-svelte-chat/src/routes/svelte-ui/+page.svelte
+++ b/examples/ts-svelte-chat/src/routes/svelte-ui/+page.svelte
@@ -1,10 +1,10 @@
- TanStack AI - Svelte createChatUI
+ TanStack AI - Svelte Chat UI
-
+
diff --git a/examples/ts-vue-chat/package.json b/examples/ts-vue-chat/package.json
index 26964075ea..fdc51a7b56 100644
--- a/examples/ts-vue-chat/package.json
+++ b/examples/ts-vue-chat/package.json
@@ -18,7 +18,6 @@
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-vue": "workspace:*",
- "@tanstack/ai-vue-ui": "workspace:*",
"marked": "^15.0.6",
"vue": "^3.5.25",
"vue-router": "^4.5.0",
diff --git a/examples/ts-vue-chat/src/views/VueUIView.vue b/examples/ts-vue-chat/src/views/VueUIView.vue
index face539c0d..e09c06aaf2 100644
--- a/examples/ts-vue-chat/src/views/VueUIView.vue
+++ b/examples/ts-vue-chat/src/views/VueUIView.vue
@@ -2,7 +2,7 @@
import { computed, defineComponent, h, ref } from 'vue'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-vue'
import { clientTools } from '@tanstack/ai-client'
-import { createChatUI, UIChat } from '@tanstack/ai-vue-ui'
+import { Chat } from '@tanstack/ai-vue/ui'
import type { ModelOption } from '@/lib/model-selection'
@@ -83,11 +83,9 @@ const chatOptions = {
},
}
-const ui = createChatUI(chatOptions)
-const chat = useChat(chatOptions)
const draft = ref('')
-const components = ui.defineComponents({
+const chatComponents = {
layout: defineComponent(
(_, { slots }) =>
() =>
@@ -98,18 +96,18 @@ const components = ui.defineComponents({
),
message: defineComponent({
props: ['message'],
- setup(props) {
+ setup(props, { slots }) {
return () =>
- h('article', { 'data-role': props.message.role }, [
- ...(props.message.parts ?? []).map(
- (part: { type: string; content?: string }) =>
- part.type === 'text' ? h('p', part.content) : null,
- ),
- ])
+ h(
+ 'article',
+ { 'data-role': props.message.role },
+ slots.parts?.() ?? slots.default?.(),
+ )
},
}),
input: defineComponent({
- setup() {
+ props: ['chat'],
+ setup(props) {
return () =>
h('div', { class: 'border-t border-orange-500/20 bg-gray-800 p-4' }, [
h(
@@ -120,7 +118,7 @@ const components = ui.defineComponents({
const text = draft.value.trim()
if (!text) return
draft.value = ''
- void chat.sendMessage(text)
+ void props.chat.sendMessage(text)
},
},
[
@@ -193,7 +191,9 @@ const components = ui.defineComponents({
},
}),
},
-})
+}
+
+const chat = useChat(chatOptions)
@@ -225,13 +225,13 @@ const components = ui.defineComponents({
- @tanstack/ai-vue-ui
+ @tanstack/ai-vue/ui
-
+
diff --git a/knip.json b/knip.json
index e6abef78b5..66b0d7c6a4 100644
--- a/knip.json
+++ b/knip.json
@@ -58,17 +58,11 @@
"ignoreDependencies": ["cloudflare"]
},
"packages/ai-react": {
- "ignoreDependencies": ["@mcp-ui/client"]
+ "ignoreDependencies": ["@mcp-ui/client", "react-dom"]
},
"packages/ai-preact": {
"ignoreDependencies": ["@mcp-ui/client"]
},
- "packages/ai-react-ui": {
- "ignoreDependencies": ["react-dom"]
- },
- "packages/ai-vue-ui": {
- "ignore": ["src/use-chat-context.ts"]
- },
"packages/ai-bedrock": {},
"packages/ai-grok-build": {
"ignoreDependencies": ["@tanstack/ai-acp"]
diff --git a/packages/ai-client/README.md b/packages/ai-client/README.md
index c9bbb8c10d..d524f36907 100644
--- a/packages/ai-client/README.md
+++ b/packages/ai-client/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/packages/ai-client/src/ui.ts b/packages/ai-client/src/ui.ts
index d745111870..b7f59e1148 100644
--- a/packages/ai-client/src/ui.ts
+++ b/packages/ai-client/src/ui.ts
@@ -24,6 +24,7 @@ export type {
ChatUISelectedGenericPart,
ChatUISelectedMessage,
ChatUISelectedPart,
+ ChatUISelectedPartOf,
ChatUISelectedResultPart,
ChatUISelectedToolPart,
ChatUISelection,
diff --git a/packages/ai-client/src/ui/types.ts b/packages/ai-client/src/ui/types.ts
index 32aad63230..85f3b58e69 100644
--- a/packages/ai-client/src/ui/types.ts
+++ b/packages/ai-client/src/ui/types.ts
@@ -206,6 +206,18 @@ export type ChatUISelectedPart =
| ChatUISelectedResultPart
| ChatUISelectedGenericPart
+export type ChatUISelectedPartOf<
+ TOptions,
+ TKey extends ChatUIPartKey = ChatUIPartKey,
+> = TKey extends 'toolCall'
+ ? ChatUIToolPart
+ : TKey extends 'toolResult'
+ ? ChatUISelectedResultPart
+ : {
+ key: TKey
+ part: ChatUIPartOf
+ }
+
export type ChatUISelectedMessage = {
message: UIMessage
parts: Array
diff --git a/packages/ai-client/tests/ui-types.test-d.ts b/packages/ai-client/tests/ui-types.test-d.ts
index 87aec41609..0d570e6ee4 100644
--- a/packages/ai-client/tests/ui-types.test-d.ts
+++ b/packages/ai-client/tests/ui-types.test-d.ts
@@ -3,6 +3,7 @@ import type {
ChatUIData,
ChatUIInterrupt,
ChatUIInterruptOf,
+ ChatUISelectedPartOf,
ChatUIToolPart,
RegisteredUIInterrupt,
} from '../src/ui'
@@ -35,3 +36,7 @@ expectTypeOf().toEqualTypeOf<'purchaseItem'>()
type AnyInterrupt = ChatUIInterruptOf
expectTypeOf().toEqualTypeOf()
+
+type TextSelected = ChatUISelectedPartOf
+expectTypeOf().toEqualTypeOf<'text'>()
+expectTypeOf().toEqualTypeOf()
diff --git a/packages/ai-devtools/README.md b/packages/ai-devtools/README.md
index c9bbb8c10d..d524f36907 100644
--- a/packages/ai-devtools/README.md
+++ b/packages/ai-devtools/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/packages/ai-gemini/README.md b/packages/ai-gemini/README.md
index c9bbb8c10d..d524f36907 100644
--- a/packages/ai-gemini/README.md
+++ b/packages/ai-gemini/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/packages/ai-ollama/README.md b/packages/ai-ollama/README.md
index c9bbb8c10d..d524f36907 100644
--- a/packages/ai-ollama/README.md
+++ b/packages/ai-ollama/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/packages/ai-openai/README.md b/packages/ai-openai/README.md
index c9bbb8c10d..d524f36907 100644
--- a/packages/ai-openai/README.md
+++ b/packages/ai-openai/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/packages/ai-openrouter/README.md b/packages/ai-openrouter/README.md
index c9bbb8c10d..d524f36907 100644
--- a/packages/ai-openrouter/README.md
+++ b/packages/ai-openrouter/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/packages/ai-preact/README.md b/packages/ai-preact/README.md
index c9bbb8c10d..d524f36907 100644
--- a/packages/ai-preact/README.md
+++ b/packages/ai-preact/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/packages/ai-react-ui/CHANGELOG.md b/packages/ai-react-ui/CHANGELOG.md
deleted file mode 100644
index 591480e967..0000000000
--- a/packages/ai-react-ui/CHANGELOG.md
+++ /dev/null
@@ -1,442 +0,0 @@
-# @tanstack/ai-react-ui
-
-## 0.8.22
-
-### Patch Changes
-
-- Updated dependencies [[`62c19ed`](https://github.com/TanStack/ai/commit/62c19edce7a814d868491ca920003899ec4c486b), [`4e9c5d2`](https://github.com/TanStack/ai/commit/4e9c5d2887dc9a3d64fce6ed424aa68f451e20f6), [`62c19ed`](https://github.com/TanStack/ai/commit/62c19edce7a814d868491ca920003899ec4c486b)]:
- - @tanstack/ai-client@0.29.0
- - @tanstack/ai-react@0.22.2
-
-## 0.8.21
-
-### Patch Changes
-
-- Updated dependencies [[`ad3840c`](https://github.com/TanStack/ai/commit/ad3840c44bd09860b29cd22f24a0cdce3adaf9dc), [`365efb8`](https://github.com/TanStack/ai/commit/365efb870e5a3fe084d0969f71781ffc772e39a0), [`a955710`](https://github.com/TanStack/ai/commit/a9557101048286c225145bad058ca81ff275b330), [`a74817a`](https://github.com/TanStack/ai/commit/a74817a38e8deb4c7ff86d81067db12befbadf5d)]:
- - @tanstack/ai-client@0.28.0
- - @tanstack/ai-react@0.22.1
-
-## 0.8.20
-
-### Patch Changes
-
-- Updated dependencies [[`b7ebcb0`](https://github.com/TanStack/ai/commit/b7ebcb0bbe63e425facb5e38f138bd0cd36637dd)]:
- - @tanstack/ai-client@0.27.0
- - @tanstack/ai-react@0.22.0
-
-## 0.8.19
-
-### Patch Changes
-
-- Updated dependencies [[`1c0415b`](https://github.com/TanStack/ai/commit/1c0415bec4bbefcd3abf784d0209af05aca5db46)]:
- - @tanstack/ai-client@0.26.0
- - @tanstack/ai-react@0.21.3
-
-## 0.8.18
-
-### Patch Changes
-
-- Updated dependencies [[`5f68cbc`](https://github.com/TanStack/ai/commit/5f68cbccf3621b48dae73cedcb1e59cb4cbe72b4), [`32e62ab`](https://github.com/TanStack/ai/commit/32e62ab8b7dc6a8a13ca3851c8925ab806e08f29)]:
- - @tanstack/ai-client@0.25.0
- - @tanstack/ai-react@0.21.0
-
-## 0.8.17
-
-### Patch Changes
-
-- Updated dependencies [[`f6b9d89`](https://github.com/TanStack/ai/commit/f6b9d89e7365f0cb30a41cf3a46b7dd237fbc73c), [`ecd12a4`](https://github.com/TanStack/ai/commit/ecd12a408987bc75649c21aada6948282a2a66dd)]:
- - @tanstack/ai-client@0.24.0
- - @tanstack/ai-react@0.20.0
-
-## 0.8.16
-
-### Patch Changes
-
-- Updated dependencies [[`7499171`](https://github.com/TanStack/ai/commit/74991716aea4d90a5d0363676a1e3349689a48e8)]:
- - @tanstack/ai-client@0.23.0
- - @tanstack/ai-react@0.19.0
-
-## 0.8.15
-
-### Patch Changes
-
-- Updated dependencies [[`35946e3`](https://github.com/TanStack/ai/commit/35946e3c39fb123c133ebe662f8e2cf0139f2b8c)]:
- - @tanstack/ai-client@0.22.0
- - @tanstack/ai-react@0.18.0
-
-## 0.8.14
-
-### Patch Changes
-
-- Updated dependencies [[`2665085`](https://github.com/TanStack/ai/commit/2665085970ab4d792778bb2b635ef27fbdcb6be1), [`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589), [`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589)]:
- - @tanstack/ai-client@0.21.0
- - @tanstack/ai-react@0.17.0
-
-## 0.8.13
-
-### Patch Changes
-
-- Updated dependencies [[`5deda27`](https://github.com/TanStack/ai/commit/5deda27085c8785894a28feb5bb3655dbd8f7e0a)]:
- - @tanstack/ai-client@0.20.0
- - @tanstack/ai-react@0.16.4
-
-## 0.8.12
-
-### Patch Changes
-
-- Updated dependencies [[`33acdd4`](https://github.com/TanStack/ai/commit/33acdd4df4aef13d594700d9b52087252091bd40), [`c22c663`](https://github.com/TanStack/ai/commit/c22c6632fdca761033cb9c4273bf61fc8ce86662), [`c1a8732`](https://github.com/TanStack/ai/commit/c1a87327b4a3463d37158f32ca90184b5fd092bb)]:
- - @tanstack/ai-client@0.19.0
- - @tanstack/ai-react@0.16.0
-
-## 0.8.11
-
-### Patch Changes
-
-- [#844](https://github.com/TanStack/ai/pull/844) [`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff) - Republish all packages with their compiled `dist/` output.
-
- Releases `0.33.0`–`0.36.0` were published without a `dist/` directory: the
- release workflow relied on an Nx-cached `build` whose outputs were not
- materialized to disk before `changeset publish` packed the tarballs, and
- `files: ["dist"]` silently includes nothing when `dist/` is absent. The
- published packages therefore contained only `src/`, so every export
- (`./dist/esm/*.js`) resolved to a missing file and the packages were
- uninstallable.
-
- The publish step now runs a fresh, cache-bypassing build of all packages
- immediately before publishing, guaranteeing compiled artifacts are present in
- every tarball.
-
-- Updated dependencies [[`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff)]:
- - @tanstack/ai-client@0.18.6
- - @tanstack/ai-react@0.15.15
-
-## 0.8.10
-
-### Patch Changes
-
-- [#783](https://github.com/TanStack/ai/pull/783) [`0278a90`](https://github.com/TanStack/ai/commit/0278a900e80ab7203124a74c841222b20b1c3bd3) - Fix `ChatMessage` rendering of multimodal tool results. Tool-result content is `string | Array`, but the renderer previously typed the message part as `any` and passed the raw content straight to React — an array of content-part objects would throw React's "Objects are not valid as a React child". The part is now typed as `UIMessage['parts'][number]`, and array content is flattened to the concatenation of its text parts (non-text parts are skipped) before rendering, both for the built-in renderer and the `toolResultRenderer` prop.
-
-- Updated dependencies [[`540cbf1`](https://github.com/TanStack/ai/commit/540cbf18a2f7d6c07b44f7f4da0ac3873c0d2581)]:
- - @tanstack/ai-client@0.18.3
- - @tanstack/ai-react@0.15.12
-
-## 0.8.9
-
-### Patch Changes
-
-- Updated dependencies [[`8fa6cc5`](https://github.com/TanStack/ai/commit/8fa6cc56c5f36e22885c98a511dcceb2bfc0da1f)]:
- - @tanstack/ai-client@0.18.0
- - @tanstack/ai-react@0.15.9
-
-## 0.8.8
-
-### Patch Changes
-
-- [#769](https://github.com/TanStack/ai/pull/769) [`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b) - Add repository metadata (`homepage`, `bugs`, `funding`), fix `repository.directory` to point at each package, and include an MIT `LICENSE` file in every published package.
-
-- Updated dependencies [[`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b)]:
- - @tanstack/ai-client@0.17.1
- - @tanstack/ai-react@0.15.6
-
-## 0.8.7
-
-### Patch Changes
-
-- Updated dependencies [[`7d44569`](https://github.com/TanStack/ai/commit/7d445693ea079d7a85498a4465179ddd5f548cb0)]:
- - @tanstack/ai-client@0.17.0
- - @tanstack/ai-react@0.15.5
-
-## 0.8.6
-
-### Patch Changes
-
-- Updated dependencies [[`755e995`](https://github.com/TanStack/ai/commit/755e9953a31e879c4b88df0e7672ce1224886c97)]:
- - @tanstack/ai-client@0.16.0
- - @tanstack/ai-react@0.15.0
-
-## 0.8.5
-
-### Patch Changes
-
-- Updated dependencies [[`c1ae8b9`](https://github.com/TanStack/ai/commit/c1ae8b94c83d70508975568eb4fc9b45f1af540b), [`8036b50`](https://github.com/TanStack/ai/commit/8036b5054330a180023c6e3225b8d2735a43a919)]:
- - @tanstack/ai-client@0.15.0
- - @tanstack/ai-react@0.14.0
-
-## 0.8.4
-
-### Patch Changes
-
-- Updated dependencies [[`d5645cf`](https://github.com/TanStack/ai/commit/d5645cfd4d1b9cfc877f7d4d714517e166a99ce3)]:
- - @tanstack/ai-client@0.14.0
- - @tanstack/ai-react@0.13.0
-
-## 0.8.3
-
-### Patch Changes
-
-- Updated dependencies [[`5634f18`](https://github.com/TanStack/ai/commit/5634f186a4946ca3e1942fbfcbf1291ec9bd9855)]:
- - @tanstack/ai-client@0.13.0
- - @tanstack/ai-react@0.12.1
-
-## 0.8.2
-
-### Patch Changes
-
-- Updated dependencies [[`ad23da9`](https://github.com/TanStack/ai/commit/ad23da92c279759b3778672dcee3d1616a02994b)]:
- - @tanstack/ai-client@0.12.0
- - @tanstack/ai-react@0.12.0
-
-## 0.8.1
-
-### Patch Changes
-
-- Refresh package README content and npm metadata for better discoverability. ([#626](https://github.com/TanStack/ai/pull/626))
-
-- Updated dependencies [[`ebeb22e`](https://github.com/TanStack/ai/commit/ebeb22ec68f456b09e0181ac6f5d1ac25a0affd2)]:
- - @tanstack/ai-client@0.11.6
- - @tanstack/ai-react@0.11.6
-
-## 0.8.0
-
-### Minor Changes
-
-- `TextPart` now accepts `remarkPlugins`, `rehypePlugins`, and (React/Solid) ([#599](https://github.com/TanStack/ai/pull/599))
- `components` props, plus a `disableDefaultPlugins` escape hatch. User plugins
- merge with the secure defaults — `rehype-sanitize` continues to run last
- unless defaults are disabled.
-
- This fixes [#164](https://github.com/TanStack/ai/issues/164): bold and
- emphasis in Japanese, Chinese, and Korean text rendered incorrectly because
- of a CommonMark spec defect. Consumers can now drop in
- [`remark-cjk-friendly`](https://www.npmjs.com/package/remark-cjk-friendly)
- with a single prop:
-
- ```tsx
- import remarkCjkFriendly from 'remark-cjk-friendly'
- ;
- ```
-
- Also fixes a latent bug in `@tanstack/ai-react-ui` where `remark-gfm` was
- passed inside the rehype plugin array, silently disabling GFM features
- (tables, strikethrough, task lists) in the React `TextPart`.
-
- `@tanstack/ai-vue-ui` omits the `components` prop because its underlying
- renderer (`@crazydos/vue-markdown`) does not expose component overrides;
- use that library's slot API for custom rendering.
-
-### Patch Changes
-
-- Adopt `@tanstack/eslint-config@0.4.0` and clean up the local override layer. ([#607](https://github.com/TanStack/ai/pull/607))
- - Bump `@tanstack/eslint-config` from `0.3.3` to `0.4.0`.
- - Drop dead `pnpm/enforce-catalog` and `pnpm/json-enforce-catalog` disables (upstream removed `eslint-plugin-pnpm` in `0.3.1`).
- - Drop the `no-case-declarations: off` override — no current source actually violates it.
- - Drop the `no-shadow: off` override — upstream sets it to `warn`, so it surfaces in editors without blocking CI.
- - Remove ~25 unnecessary type assertions across the publishable packages that the upgraded `typescript-eslint` now catches via `no-unnecessary-type-assertion`. One deliberately defensive cast in `ag-ui-wire.ts` is preserved with an inline opt-out and a reason comment.
-
- No public-API or runtime-behavior changes.
-
-- Updated dependencies [[`a03d12b`](https://github.com/TanStack/ai/commit/a03d12b13ade93f3e262c6ffa996696ce27472ef)]:
- - @tanstack/ai-client@0.11.4
- - @tanstack/ai-react@0.11.4
-
-## 0.7.2
-
-### Patch Changes
-
-- Tighten TypeScript safety: enable `noImplicitOverride`, ([#579](https://github.com/TanStack/ai/pull/579))
- `noFallthroughCasesInSwitch`, and `useDefineForClassFields` in the
- root `tsconfig.json`; add a typed-ESLint block scoped to
- `packages/*/src/**` that turns on `no-floating-promises`,
- `no-misused-promises`, `await-thenable`,
- `switch-exhaustiveness-check`, `consistent-type-exports`,
- `prefer-readonly`, and `no-non-null-assertion` (errors), plus
- `no-explicit-any` (warning). `@ts-ignore` and `@ts-nocheck` are
- disallowed in library source via `@typescript-eslint/ban-ts-comment`,
- and `as unknown as ` double-casts are blocked by a
- `no-restricted-syntax` rule (escape hatches available with an inline
- reason). Two flags from the original five-flag set —
- `noPropertyAccessFromIndexSignature` and `exactOptionalPropertyTypes`
- — were tried and rolled back: they produced ~500 lines of bracket-
- access and conditional-spread churn without catching any real bugs,
- and `exactOptionalPropertyTypes` would have forced consumers using
- it themselves to deal with our internals' style preferences.
-
- User-visible API surface is unchanged; this is a hardening pass to
- keep streaming/agent-loop correctness and discriminated-union
- exhaustiveness honest going forward. See issue #564.
-
-- Updated dependencies []:
- - @tanstack/ai-client@0.11.3
- - @tanstack/ai-react@0.11.3
-
-## 0.7.1
-
-### Patch Changes
-
-- Updated dependencies [[`2e0e2eb`](https://github.com/TanStack/ai/commit/2e0e2eb72684aac82e570d57767656e218289b49), [`b05adb1`](https://github.com/TanStack/ai/commit/b05adb1b9ca756edc926c6b86ee6fae37dd966c0)]:
- - @tanstack/ai-client@0.11.0
- - @tanstack/ai-react@0.11.0
-
-## 0.7.0
-
-### Minor Changes
-
-- **Breaking:** AG-UI client-to-server compliance. ([#511](https://github.com/TanStack/ai/pull/511))
-
- `@tanstack/ai-client` now POSTs an AG-UI `RunAgentInput` request body and `@tanstack/ai` server endpoints must use the new `chatParamsFromRequestBody` + `mergeAgentTools` helpers. Upgrade both packages together.
-
- Highlights:
- - **Wire format**: `{threadId, runId, state, messages, tools, context, forwardedProps}` (per AG-UI 0.0.52 `RunAgentInputSchema`) instead of `{messages, data}`.
- - **New server helpers** exported from `@tanstack/ai`: `chatParamsFromRequestBody`, `mergeAgentTools`.
- - **`chat()` accepts `threadId`, `runId`, `parentRunId`** as optional fields for AG-UI run correlation.
- - **`ChatClient` accepts `threadId`** option; auto-generates and persists per session if omitted; fresh `runId` per send.
- - **Client tools auto-advertised** to the server via `RunAgentInput.tools`.
- - **Foreign AG-UI clients** can hit a TanStack server: `developer` collapses to `system`, `reasoning`/`activity` drop.
-
- See `docs/migration/ag-ui-compliance.md` for full migration steps.
-
-### Patch Changes
-
-- Updated dependencies [[`a9d1916`](https://github.com/TanStack/ai/commit/a9d19165a5028515cf1d091d611c8ac4b5b86099)]:
- - @tanstack/ai-client@0.10.0
- - @tanstack/ai-react@0.10.0
-
-## 0.6.4
-
-### Patch Changes
-
-- Updated dependencies [[`98979f7`](https://github.com/TanStack/ai/commit/98979f7e72f4b5bfb816fb14b60a12871f8c4bec)]:
- - @tanstack/ai-react@0.9.0
- - @tanstack/ai-client@0.9.2
-
-## 0.6.3
-
-### Patch Changes
-
-- Updated dependencies [[`b2d3cc1`](https://github.com/TanStack/ai/commit/b2d3cc131a31c54bd1e5841f958fbe333514e508), [`13cceae`](https://github.com/TanStack/ai/commit/13cceaedf64e398ca15b8dbbbfe215329ea26794)]:
- - @tanstack/ai-client@0.9.0
- - @tanstack/ai-react@0.8.1
-
-## 0.6.2
-
-### Patch Changes
-
-- Updated dependencies [[`54523f5`](https://github.com/TanStack/ai/commit/54523f5e9a9b4d4ea6c49e4551936bc2cc25593a), [`008f015`](https://github.com/TanStack/ai/commit/008f0154f852e7e6734d3e3d35cad47780b52b7a), [`af9eb7b`](https://github.com/TanStack/ai/commit/af9eb7bbb875b23b7e99b2e6b743636daad402d1)]:
- - @tanstack/ai-client@0.8.0
- - @tanstack/ai-react@0.8.0
-
-## 0.6.1
-
-### Patch Changes
-
-- Updated dependencies [[`86be1c8`](https://github.com/TanStack/ai/commit/86be1c8262bb3176ea786aa0af115b38c3e3f51a)]:
- - @tanstack/ai-client@0.7.0
- - @tanstack/ai-react@0.7.0
-
-## 0.6.0
-
-### Patch Changes
-
-- Updated dependencies [[`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e), [`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e)]:
- - @tanstack/ai-client@0.6.0
- - @tanstack/ai-react@0.6.4
-
-## 0.5.0
-
-### Patch Changes
-
-- Updated dependencies [[`5aa6acc`](https://github.com/TanStack/ai/commit/5aa6acc1a4faea5346f750322e80984abf2d7059), [`1f800aa`](https://github.com/TanStack/ai/commit/1f800aacf57081f37a075bc8d08ff397cb33cbe9)]:
- - @tanstack/ai-client@0.5.0
- - @tanstack/ai-react@0.6.0
-
-## 0.5.1
-
-### Patch Changes
-
-- Updated dependencies [[`5d98472`](https://github.com/TanStack/ai/commit/5d984722e1f84725e3cfda834fbda3d0341ecedd)]:
- - @tanstack/ai-client@0.4.4
- - @tanstack/ai-react@0.5.1
-
-## 0.5.0
-
-### Patch Changes
-
-- Updated dependencies [[`0158d14`](https://github.com/TanStack/ai/commit/0158d14df00639ff5325680ae91b7791c189e60f)]:
- - @tanstack/ai-react@0.5.0
- - @tanstack/ai-client@0.4.1
-
-## 0.4.0
-
-### Patch Changes
-
-- Updated dependencies [[`99ccee5`](https://github.com/TanStack/ai/commit/99ccee5c72df12adc13bede98142c6da84d13cc4), [`230bab6`](https://github.com/TanStack/ai/commit/230bab6417c8ff2c25586a12126c85e27dd7bc15)]:
- - @tanstack/ai-client@0.4.0
- - @tanstack/ai-react@0.4.0
-
-## 0.3.0
-
-### Patch Changes
-
-- Updated dependencies [[`e52135f`](https://github.com/TanStack/ai/commit/e52135f6ec3285227679411636e208ae84a408d7)]:
- - @tanstack/ai-client@0.3.0
- - @tanstack/ai-react@0.3.0
-
-## 0.2.1
-
-### Patch Changes
-
-- fix up readmes ([#188](https://github.com/TanStack/ai/pull/188))
-
-- Updated dependencies [[`181e0ac`](https://github.com/TanStack/ai/commit/181e0acdfb44b27db6cf871b36593c0f867cadf9)]:
- - @tanstack/ai-react@0.2.1
-
-## 0.2.0
-
-### Patch Changes
-
-- Updated dependencies [[`c5df33c`](https://github.com/TanStack/ai/commit/c5df33c2d3e72c3332048ffe7c64a553e5ea86fb)]:
- - @tanstack/ai-react@0.2.0
-
-## 0.1.0
-
-### Minor Changes
-
-- Split up adapters for better tree shaking into separate functionalities ([#137](https://github.com/TanStack/ai/pull/137))
-
-### Patch Changes
-
-- Updated dependencies [[`8d77614`](https://github.com/TanStack/ai/commit/8d776146f94ffd1579e1ab01b26dcb94d1bb3092)]:
- - @tanstack/ai-client@0.1.0
- - @tanstack/ai-react@0.1.0
-
-## 0.0.3
-
-### Patch Changes
-
-- Updated dependencies []:
- - @tanstack/ai-client@0.0.3
- - @tanstack/ai-react@0.0.3
-
-## 0.0.2
-
-### Patch Changes
-
-- Updated dependencies [[`a7bd563`](https://github.com/TanStack/ai/commit/a7bd5639eb2fbf1b4169eb307f77149f4a85a915)]:
- - @tanstack/ai-client@0.0.2
- - @tanstack/ai-react@0.0.2
-
-## 0.0.1
-
-### Patch Changes
-
-- initial release of AI ([#76](https://github.com/TanStack/ai/pull/76))
-
-## 0.0.1
-
-### Patch Changes
-
-- Initial release of TanStack AI ([#72](https://github.com/TanStack/ai/pull/72))
-
-- Updated dependencies [[`a9b54c2`](https://github.com/TanStack/ai/commit/a9b54c21282d16036a427761e0784b159a6f2d99)]:
- - @tanstack/ai-client@0.0.1
- - @tanstack/ai-react@0.0.1
diff --git a/packages/ai-react-ui/README.md b/packages/ai-react-ui/README.md
index c9bbb8c10d..9d1e775ab0 100644
--- a/packages/ai-react-ui/README.md
+++ b/packages/ai-react-ui/README.md
@@ -1,328 +1,9 @@
-
-
-
-
-
-
-
+# @tanstack/ai-react-ui (deprecated)
-
+This package is deprecated since 0.9.0. It will be removed in 1.0.0.
-
+Import chat UI from [`@tanstack/ai-react/ui`](https://tanstack.com/ai/latest/docs/ui/react) instead.
-
+This package re-exports that subpath so existing installs keep working during the move.
-
-
-# TanStack AI
-
-Type-safe, provider-agnostic TypeScript SDK for building streaming chat,
-tool-calling agents, structured outputs, realtime voice, media generation, and
-framework-native AI apps.
-
-TanStack AI is built from composable activities and provider adapters. Use one
-provider or switch between many. Import only chat, or add image, audio, video,
-speech, transcription, summarization, realtime, Code Mode, devtools, and
-framework bindings as your app needs them.
-
-## Read the docs ->
-
-## Start Here
-
-- [Overview](https://tanstack.com/ai/latest/docs/getting-started/overview) -
- what TanStack AI is and how the packages fit together.
-- [Quick Start: React](https://tanstack.com/ai/latest/docs/getting-started/quick-start) -
- add streaming chat to a React app.
-- [Quick Start: Vue](https://tanstack.com/ai/latest/docs/getting-started/quick-start-vue) -
- build with Vue composables.
-- [Quick Start: Svelte](https://tanstack.com/ai/latest/docs/getting-started/quick-start-svelte) -
- build with Svelte 5 runes.
-- [Quick Start: Server Only](https://tanstack.com/ai/latest/docs/getting-started/quick-start-server) -
- use TanStack AI from a server endpoint, script, or backend service.
-- [TanStack AI vs Vercel AI SDK](https://tanstack.com/ai/latest/docs/comparison/vercel-ai-sdk) -
- compare architecture, feature coverage, and tradeoffs.
-
-## What You Can Build
-
-- Streaming chat experiences with typed messages, tool calls, reasoning parts,
- and configurable connection adapters.
-- Type-safe tools that can run on the server or client from one shared
- `toolDefinition()` contract.
-- Structured output flows backed by JSON Schema, Zod, ArkType, Valibot, or
- plain JSON Schema.
-- Multimodal prompts and responses that include text, images, audio, video, and
- documents.
-- Image, audio, video, speech, transcription, and summarization workflows using
- a shared generation client pattern.
-- Realtime voice chat with provider adapters for realtime sessions and token
- minting.
-- Code Mode agents that let an LLM write and execute TypeScript in an isolated
- sandbox to orchestrate tools with loops, branches, and parallel calls.
-- Devtools and observability pipelines for inspecting messages, tool calls,
- stream chunks, errors, usage, and OpenTelemetry traces.
-- Framework-native clients for React, Solid, Vue, Svelte, and Preact, plus a
- headless client for custom runtimes.
-
-## Install
-
-Install the core package and the provider/framework packages your app uses:
-
-```bash
-pnpm add @tanstack/ai @tanstack/ai-openai
-```
-
-For a React chat UI:
-
-```bash
-pnpm add @tanstack/ai @tanstack/ai-client @tanstack/ai-react @tanstack/ai-openai
-```
-
-OpenRouter is also a good starting point if you want access to many providers
-through one API key:
-
-```bash
-pnpm add @tanstack/ai @tanstack/ai-openrouter
-```
-
-## Streaming Chat
-
-```typescript
-import { chat, toServerSentEventsResponse } from '@tanstack/ai'
-import { openaiText } from '@tanstack/ai-openai'
-
-export async function POST(request: Request) {
- const body = await request.json()
-
- const stream = chat({
- adapter: openaiText('gpt-5.2'),
- messages: body.messages,
- })
-
- return toServerSentEventsResponse(stream)
-}
-```
-
-Learn more in the
-[Chat & Streaming docs](https://tanstack.com/ai/latest/docs/chat/streaming) and
-[Connection Adapters docs](https://tanstack.com/ai/latest/docs/chat/connection-adapters).
-
-## Type-Safe Tools
-
-Define a tool once, then attach a server or client implementation with the same
-input and output types:
-
-```typescript
-import { toolDefinition } from '@tanstack/ai'
-import { z } from 'zod'
-
-const getProducts = toolDefinition({
- name: 'getProducts',
- description: 'Search the product catalog',
- inputSchema: z.object({ query: z.string() }),
- outputSchema: z.array(
- z.object({
- id: z.string(),
- name: z.string(),
- }),
- ),
-}).server(async ({ query }) => {
- return db.products.search(query)
-})
-```
-
-Learn more in the
-[Tools docs](https://tanstack.com/ai/latest/docs/tools/tools),
-[Tool Approval Flow docs](https://tanstack.com/ai/latest/docs/tools/tool-approval),
-and
-[Lazy Tool Discovery docs](https://tanstack.com/ai/latest/docs/tools/lazy-tool-discovery).
-
-## Structured Outputs
-
-Use `outputSchema` when you need typed objects instead of freeform text:
-
-```typescript
-import { chat } from '@tanstack/ai'
-import { openaiText } from '@tanstack/ai-openai'
-import { z } from 'zod'
-
-const Person = z.object({
- name: z.string(),
- age: z.number(),
-})
-
-const person = await chat({
- adapter: openaiText('gpt-5.2'),
- messages: [{ role: 'user', content: 'Ada Lovelace, 36' }],
- outputSchema: Person,
-})
-```
-
-Learn more in the
-[Structured Outputs docs](https://tanstack.com/ai/latest/docs/structured-outputs/overview).
-
-## Media, Realtime, and Code Mode
-
-- [Generations](https://tanstack.com/ai/latest/docs/media/generations) - one
- pattern for image generation, text-to-speech, transcription, summarization,
- audio generation, and video generation.
-- [Realtime Voice Chat](https://tanstack.com/ai/latest/docs/media/realtime-chat) -
- build low-latency realtime voice experiences.
-- [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let
- models write and execute TypeScript inside a secure isolate.
-- [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) -
- give Code Mode reusable runtime capabilities.
-
-## Providers
-
-Official adapters include:
-
-| Package | Use it for |
-| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
-| [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
-| [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
-| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude |
-| [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
-| [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials |
-| [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
-| [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
-| [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
-| [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
-| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
-| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
-
-The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
-`openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so
-on.
-
-## Framework Packages
-
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
-
-## Advanced Docs
-
-- [Middleware](https://tanstack.com/ai/latest/docs/advanced/middleware) - hook
- into chat configuration, chunks, tool calls, usage, errors, and structured
- outputs.
-- [OpenTelemetry](https://tanstack.com/ai/latest/docs/advanced/otel) - emit
- vendor-neutral GenAI traces and metrics.
-- [Observability](https://tanstack.com/ai/latest/docs/advanced/observability) -
- subscribe to typed TanStack AI events.
-- [Per-Model Type Safety](https://tanstack.com/ai/latest/docs/advanced/per-model-type-safety) -
- narrow model options and content modalities to the selected model.
-- [Runtime Adapter Switching](https://tanstack.com/ai/latest/docs/advanced/runtime-adapter-switching) -
- switch providers at runtime.
-- [Tree-Shaking](https://tanstack.com/ai/latest/docs/advanced/tree-shaking) -
- ship only the activities and adapters you use.
-- [Agent Skills](https://tanstack.com/ai/latest/docs/getting-started/agent-skills) -
- install TanStack AI skills into Claude Code, Cursor, GitHub Copilot, Codex,
- and other coding agents with TanStack Intent.
-
-## Get Involved
-
-- Read the [docs](https://tanstack.com/ai).
-- Participate in [GitHub discussions](https://github.com/TanStack/ai/discussions).
-- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ).
-- See [CONTRIBUTING.md](https://github.com/TanStack/ai/blob/main/CONTRIBUTING.md)
- for setup instructions.
-- [Become a sponsor](https://github.com/sponsors/tannerlinsley/).
-
-## Partners
-
-
-
-
-
-
- We're looking for TanStack AI partners to join our mission. Partner with us
- to push the boundaries of TanStack AI and build amazing things together.
-
-
LET'S CHAT
-
-
-## Explore the TanStack Ecosystem
-
-- TanStack Config -
- tooling for JS/TS packages
-- TanStack DB - reactive
- sync client store
-- TanStack Devtools -
- unified devtools panel
-- TanStack Form -
- type-safe form state
-- TanStack Pacer -
- debouncing, throttling, batching
-- TanStack Query -
- async state and caching
-- TanStack Ranger -
- range and slider primitives
-- TanStack Router -
- type-safe routing, caching, and URL state
-- TanStack Start -
- full-stack SSR and streaming
-- TanStack Store -
- reactive data store
-- TanStack Table -
- headless datagrids
-- TanStack Virtual -
- virtualized rendering
-
-...and more at TanStack.com .
+See the [migration guide](https://tanstack.com/ai/latest/docs/migration/create-ui).
diff --git a/packages/ai-react-ui/package.json b/packages/ai-react-ui/package.json
index a49b3d1525..a7207617a4 100644
--- a/packages/ai-react-ui/package.json
+++ b/packages/ai-react-ui/package.json
@@ -1,7 +1,7 @@
{
"name": "@tanstack/ai-react-ui",
"version": "0.8.22",
- "description": "Headless React components for building TanStack AI chat interfaces with streamed message parts.",
+ "description": "Deprecated. Import chat UI from @tanstack/ai-react/ui instead.",
"author": "Tanner Linsley",
"license": "MIT",
"homepage": "https://tanstack.com/ai",
@@ -17,6 +17,7 @@
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
+ "deprecated": "Use @tanstack/ai-react/ui instead. See https://tanstack.com/ai/latest/docs/migration/create-ui",
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"type": "module",
@@ -27,7 +28,8 @@
}
},
"files": [
- "dist"
+ "dist",
+ "src"
],
"scripts": {
"build": "vite build",
@@ -40,38 +42,18 @@
},
"keywords": [
"ai",
- "ai-sdk",
- "typescript",
"tanstack",
"react",
"chat",
"ui",
- "headless",
- "components",
- "streaming",
- "generative-ai"
+ "deprecated"
],
"dependencies": {
- "react-markdown": "^10.1.0",
- "rehype-highlight": "^7.0.2",
- "rehype-raw": "^7.0.0",
- "rehype-sanitize": "^6.0.0",
- "remark-gfm": "^4.0.1"
- },
- "peerDependencies": {
- "@tanstack/ai-client": "workspace:^",
- "@tanstack/ai-react": "workspace:^",
- "@types/react": ">=18.0.0",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
+ "@tanstack/ai-react": "workspace:^"
},
"devDependencies": {
- "@tanstack/ai-client": "workspace:*",
- "@tanstack/ai-react": "workspace:*",
"@types/react": "^19.2.7",
- "@vitest/coverage-v8": "4.1.10",
"react": "^19.2.3",
- "react-dom": "^19.2.3",
"vite": "^8.2.1"
}
}
diff --git a/packages/ai-react-ui/src/create-ui.tsx b/packages/ai-react-ui/src/create-ui.tsx
deleted file mode 100644
index eacbbb76eb..0000000000
--- a/packages/ai-react-ui/src/create-ui.tsx
+++ /dev/null
@@ -1,468 +0,0 @@
-import { createContext, memo, useCallback, useContext, useMemo } from 'react'
-import type { ComponentType, ReactNode } from 'react'
-import {
- automaticPartsForMessage,
- collectInlineToolNames,
- resolveInterruptComponent,
- selectChatUI,
- selectMessageUI,
-} from '@tanstack/ai-client/ui'
-import type {
- ChatUIData,
- ChatUIHasNamedInterrupts,
- ChatUIHasNamedTools,
- ChatUIInterrupt,
- ChatUIInterruptName,
- ChatUIInterruptOf,
- ChatUIInterruptsOf,
- ChatUIMessages,
- ChatUINamedInterruptId,
- ChatUIPartKey,
- ChatUIPartOf,
- ChatUISchemaOf,
- ChatUISelectedPart,
- ChatUIToolApproval,
- ChatUIToolName,
- ChatUIToolsOf,
-} from '@tanstack/ai-client/ui'
-import type {
- MessagePart,
- ToolCallPart,
- ToolResultPart,
- UIMessage,
-} from '@tanstack/ai-client'
-import type { UseChatReturn } from '@tanstack/ai-react'
-
-export type ChatUIHost = UseChatReturn<
- ChatUIToolsOf,
- ChatUISchemaOf,
- ChatUIInterruptsOf
->
-
-export type LayoutProps = {
- renderMessages: () => ReactNode
- renderInterrupts: () => ReactNode
- renderInput: () => ReactNode
- readonly __ui?: TOptions
-}
-
-export type MessageProps = {
- message: UIMessage, ChatUIData>
- renderParts: () => ReactNode
-}
-
-export type InputProps = {
- readonly __ui?: TOptions
-}
-
-export type PartProps = {
- part: ChatUIPartOf
-}
-
-export type ToolProps<
- TOptions,
- TName extends ChatUIToolName = ChatUIToolName,
-> = {
- part: Extract>, { name: TName }>
- result?: ToolResultPart
- interrupt?: ChatUIToolApproval
-}
-
-export type InterruptProps<
- TOptions,
- TName extends ChatUIInterruptName = never,
-> = {
- interrupt: ChatUIInterruptOf
- readonly __ui?: TOptions
-}
-
-type GenericInterruptComponents =
- ChatUIHasNamedInterrupts extends true
- ? {
- [K in ChatUINamedInterruptId]: ComponentType<
- InterruptProps>
- >
- } & {
- fallback?: ComponentType>
- }
- : {
- fallback?: ComponentType>
- }
-
-type ToolApprovalMap = {
- [K in ChatUIToolName]?: ComponentType<
- InterruptProps>
- >
-}
-
-export type ChatUIComponents = {
- layout: ComponentType>
- message: ComponentType>
- input?: ComponentType>
- parts: {
- [K in ChatUIPartKey]?: ComponentType>
- } & {
- fallback?: ComponentType>
- }
-} & (ChatUIHasNamedTools extends true
- ? {
- tools: {
- [K in ChatUIToolName]: ComponentType>
- }
- }
- : {
- tools?: {
- [K in ChatUIToolName]?: ComponentType>
- }
- }) &
- (ChatUIHasNamedInterrupts extends true
- ? {
- interrupts: {
- tools?: ToolApprovalMap
- generic: GenericInterruptComponents
- }
- }
- : {
- interrupts?: {
- tools?: ToolApprovalMap
- generic?: GenericInterruptComponents
- }
- })
-
-type ComponentsValue = {
- components: ChatUIComponents
- warn: (key: string, message: string) => void
- inlineToolNames: ReadonlyArray
-}
-
-function createWarnOnce() {
- const seen = new Set()
- return (key: string, message: string) => {
- if (process.env.NODE_ENV === 'production') return
- if (seen.has(key)) return
- seen.add(key)
- console.warn(message)
- }
-}
-
-function readMessages(chat: ChatUIHost) {
- return chat.messages as ChatUIMessages
-}
-
-function readInterrupts(chat: ChatUIHost) {
- return chat.interrupts ?? []
-}
-
-function selectedPartPropsEqual(
- prev: { selected: ChatUISelectedPart },
- next: { selected: ChatUISelectedPart },
-) {
- if (prev.selected.key !== next.selected.key) return false
- if (prev.selected.part !== next.selected.part) return false
- if (prev.selected.key === 'toolCall' && next.selected.key === 'toolCall') {
- return (
- prev.selected.result === next.selected.result &&
- prev.selected.interrupt === next.selected.interrupt
- )
- }
- if (
- prev.selected.key === 'toolResult' &&
- next.selected.key === 'toolResult'
- ) {
- return prev.selected.matched === next.selected.matched
- }
- return true
-}
-
-export function createChatUI(options: TOptions) {
- void options
- const warn = createWarnOnce()
- const ChatContext = createContext | null>(null)
- const ComponentsContext = createContext | null>(
- null,
- )
-
- function useChat() {
- const chat = useContext(ChatContext)
- if (!chat) {
- throw new Error(
- 'Chat UI components must be wrapped in UI.Provider or UI.Chat.',
- )
- }
- return chat
- }
-
- function useComponents() {
- const value = useContext(ComponentsContext)
- if (!value) {
- throw new Error(
- 'Chat UI components must be wrapped in UI.Provider or UI.Chat.',
- )
- }
- return value
- }
-
- function defineComponents(
- components: ChatUIComponents,
- ): ChatUIComponents {
- return components
- }
-
- function inlineNames(components: ChatUIComponents) {
- return collectInlineToolNames(
- components.interrupts?.tools as Record | undefined,
- Object.keys(components.tools ?? {}),
- )
- }
-
- function Provider({
- chat,
- components,
- children,
- }: {
- chat: ChatUIHost
- components: ChatUIComponents
- children?: ReactNode
- }) {
- const componentsValue = useMemo(
- () => ({
- components,
- warn,
- inlineToolNames: inlineNames(components),
- }),
- [components],
- )
- return (
-
- {children}
-
- )
- }
-
- function Chat({
- chat,
- components,
- }: {
- chat: ChatUIHost
- components: ChatUIComponents
- }) {
- const Layout = components.layout
- const renderMessages = useCallback(() => , [])
- const renderInterrupts = useCallback(() => , [])
- const renderInput = useCallback(() => {
- const Input = components.input
- return Input ? : null
- }, [components.input])
- return (
-
-
-
- )
- }
-
- function Messages({
- children,
- }: {
- children?: (messages: ChatUIMessages) => ReactNode
- } = {}) {
- const chat = useChat()
- const { inlineToolNames } = useComponents()
- const messages = readMessages(chat)
- const interrupts = readInterrupts(chat)
- if (children) return <>{children(messages)}>
- return (
- <>
- {messages.map((message) => (
-
- ))}
- >
- )
- }
-
- const MessageView = memo(function MessageView({
- message,
- interrupts,
- inlineToolNames,
- children,
- }: {
- message: ChatUIMessages[number]
- interrupts: ReadonlyArray
- inlineToolNames: ReadonlyArray
- children?: (parts: Array) => ReactNode
- }) {
- const { components } = useComponents()
- const selected = selectMessageUI(message, {
- interrupts,
- inlineToolNames,
- })
- if (children) return <>{children(selected.parts)}>
- const MessageComponent = components.message
- return (
- (
-
- )}
- />
- )
- })
-
- function Message({
- message,
- children,
- }: {
- message: ChatUIMessages[number]
- children?: (parts: Array) => ReactNode
- }) {
- const chat = useChat()
- const { inlineToolNames } = useComponents()
- return (
-
- )
- }
-
- const AutomaticParts = memo(function AutomaticParts({
- message,
- interrupts,
- inlineToolNames,
- }: {
- message: ChatUIMessages[number]
- interrupts: ReadonlyArray
- inlineToolNames: ReadonlyArray
- }) {
- const selected = selectMessageUI(message, {
- interrupts,
- inlineToolNames,
- })
- return (
- <>
- {automaticPartsForMessage(selected).map((part, index) => (
-
- ))}
- >
- )
- })
-
- const SelectedPartView = memo(function SelectedPartView({
- selected,
- }: {
- selected: ChatUISelectedPart
- }) {
- const { components, warn: warnMissing } = useComponents()
- if (selected.key === 'toolCall') {
- const name = selected.part.name
- const Tool = components.tools?.[name as ChatUIToolName] as
- | ComponentType>
- | undefined
- if (!Tool) {
- warnMissing(
- `tool:${name}`,
- `[tanstack-ai-ui] Missing tools.${name} component`,
- )
- return null
- }
- return (
- ['part']}
- result={selected.result}
- interrupt={selected.interrupt as ToolProps['interrupt']}
- />
- )
- }
-
- const PartComponent = (components.parts[selected.key] ??
- components.parts.fallback) as
- | ComponentType>
- | undefined
- if (!PartComponent) {
- warnMissing(
- `part:${selected.key}`,
- `[tanstack-ai-ui] Missing parts.${selected.key} component`,
- )
- return null
- }
- return ['part']} />
- }, selectedPartPropsEqual)
-
- function Part({ part }: { part: MessagePart }) {
- const chat = useChat()
- const selected = selectMessageUI(
- { id: 'part', role: 'assistant', parts: [part] },
- { interrupts: readInterrupts(chat), inlineToolNames: [] },
- ).parts[0]
- if (!selected) return null
- return
- }
-
- function Interrupts({
- children,
- }: {
- children?: (interrupts: ReadonlyArray) => ReactNode
- } = {}) {
- const chat = useChat()
- const { inlineToolNames } = useComponents()
- const selected = selectChatUI({
- messages: readMessages(chat),
- interrupts: readInterrupts(chat),
- inlineToolNames,
- })
- if (children) return <>{children(selected.interrupts)}>
- return (
- <>
- {selected.interrupts.map((interrupt) => (
-
- ))}
- >
- )
- }
-
- const Interrupt = memo(function Interrupt({
- interrupt,
- }: {
- interrupt: ChatUIInterrupt
- }) {
- const { components, warn: warnMissing } = useComponents()
- const Component = resolveInterruptComponent(
- interrupt,
- components.interrupts,
- ) as ComponentType> | undefined
- if (!Component) {
- warnMissing(
- `interrupt:${interrupt.id}`,
- `[tanstack-ai-ui] Missing interrupt component for ${interrupt.kind}`,
- )
- return null
- }
- return
- })
-
- return {
- Chat,
- Provider,
- Messages,
- Message,
- Part,
- Interrupts,
- Interrupt,
- defineComponents,
- useChat,
- }
-}
diff --git a/packages/ai-react-ui/src/index.ts b/packages/ai-react-ui/src/index.ts
index 7d5633f8bf..033d0f45d8 100644
--- a/packages/ai-react-ui/src/index.ts
+++ b/packages/ai-react-ui/src/index.ts
@@ -1,73 +1,6 @@
/**
- * @tanstack/ai-react-ui
- *
- * Headless React components for building AI chat interfaces.
- *
- * Features:
- * - Parts-based message rendering (text, tool calls, tool results)
- * - Native tool approval workflows
- * - Client-side tool execution support
- * - Streaming support
- * - Fully customizable with render props
- * - Compound component pattern
- *
- * @example
- * ```tsx
- * import { Chat, ChatMessages, ChatInput, ChatMessage } from '@tanstack/ai-react-ui'
- *
- *
- *
- * {(message) => }
- *
- *
- *
- * ```
+ * @deprecated Since 0.9.0. Import from `@tanstack/ai-react/ui` instead.
+ * See https://tanstack.com/ai/latest/docs/migration/create-ui
+ * Removed in 1.0.0.
*/
-
-export {
- createChatUI,
- type ChatUIComponents,
- type ChatUIHost,
- type InputProps,
- type InterruptProps,
- type LayoutProps,
- type MessageProps,
- type PartProps,
- type ToolProps,
-} from './create-ui'
-
-// Main components
-export { Chat, useChatContext, type ChatProps } from './chat'
-export { ChatMessages, type ChatMessagesProps } from './chat-messages'
-export {
- ChatMessage,
- type ChatMessageProps,
- type ToolCallRenderProps,
-} from './chat-message'
-export {
- ChatInput,
- type ChatInputProps,
- type ChatInputRenderProps,
-} from './chat-input'
-export {
- ToolApproval,
- type ToolApprovalProps,
- type ToolApprovalRenderProps,
-} from './tool-approval'
-export { TextPart, type TextPartProps } from './text-part'
-export { ThinkingPart, type ThinkingPartProps } from './thinking-part'
-
-// Re-export hooks from @tanstack/ai-react for convenience
-export { useChat } from '@tanstack/ai-react'
-
-// Re-export types from @tanstack/ai-react
-export type {
- UIMessage,
- MessagePart,
- ToolCallPart,
- ToolResultPart,
- TextPart as TextPartType,
- ConnectionAdapter,
-} from '@tanstack/ai-client'
-
-export type { UseChatOptions, UseChatReturn } from '@tanstack/ai-react'
+export * from '@tanstack/ai-react/ui'
diff --git a/packages/ai-react-ui/tests/coexistence.test.ts b/packages/ai-react-ui/tests/coexistence.test.ts
deleted file mode 100644
index 4c9c6cb332..0000000000
--- a/packages/ai-react-ui/tests/coexistence.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import {
- Chat,
- ChatInput,
- ChatMessage,
- ChatMessages,
- TextPart,
- ThinkingPart,
- createChatUI,
-} from '../src'
-
-describe('public coexistence', () => {
- it('exports old and new APIs before 1.0', () => {
- expect(Chat).toBeDefined()
- expect(ChatInput).toBeDefined()
- expect(ChatMessage).toBeDefined()
- expect(ChatMessages).toBeDefined()
- expect(TextPart).toBeDefined()
- expect(ThinkingPart).toBeDefined()
- expect(createChatUI).toBeDefined()
- })
-})
diff --git a/packages/ai-react-ui/tests/create-ui.test.tsx b/packages/ai-react-ui/tests/create-ui.test.tsx
deleted file mode 100644
index 845cad4e40..0000000000
--- a/packages/ai-react-ui/tests/create-ui.test.tsx
+++ /dev/null
@@ -1,337 +0,0 @@
-import { renderToStaticMarkup } from 'react-dom/server'
-import { describe, expect, it, vi } from 'vitest'
-import { createChatUI } from '../src/create-ui'
-import type { ChatUIHost } from '../src/create-ui'
-import {
- chatOptions,
- createChatResult,
- genericInterrupt,
- messageWithToolResults,
- orphanResultMessage,
- purchaseApprovalInterrupt,
- purchaseApprovalMessage,
- unboundInterrupt,
- unknownToolMessage,
-} from '../../ai-client/tests/ui-fixtures'
-import type { ToolCallState } from '@tanstack/ai-client'
-
-function host(
- init?: Parameters[0],
-): ChatUIHost {
- return createChatResult(init ?? {}) as unknown as ChatUIHost<
- typeof chatOptions
- >
-}
-
-describe('createChatUI', () => {
- it('renders automatic and manual trees', () => {
- const UI = createChatUI(chatOptions)
- const components = UI.defineComponents({
- layout: ({ renderMessages, renderInterrupts }) => (
- <>
- {renderMessages()}
- {renderInterrupts()}
- >
- ),
- message: ({ renderParts }) => {renderParts()} ,
- parts: { fallback: ({ part }) => {part.type} },
- tools: {
- getWeather: ({ part }) => {part.input?.city} ,
- purchaseItem: () => null,
- },
- interrupts: {
- generic: {
- choosePlan: () => null,
- fallback: ({ interrupt }) => {interrupt.reason} ,
- },
- },
- })
-
- const chat = host({ messages: [messageWithToolResults] })
- const automatic = renderToStaticMarkup(
- ,
- )
- expect(automatic).toContain('Paris ')
-
- const manual = renderToStaticMarkup(
-
-
- {(messages) => {messages.length} }
-
- ,
- )
- expect(manual).toContain('1 ')
- })
-
- it('warns once for a missing runtime key', () => {
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
- const UI = createChatUI(chatOptions)
- const chat = host({ messages: [unknownToolMessage] })
- const components = UI.defineComponents({
- layout: ({ renderMessages }) => renderMessages(),
- message: ({ renderParts }) => renderParts(),
- parts: { fallback: () => null },
- tools: {
- getWeather: () => null,
- purchaseItem: () => null,
- },
- interrupts: {
- generic: { choosePlan: () => null, fallback: () => null },
- },
- })
- renderToStaticMarkup( )
- renderToStaticMarkup( )
- expect(warn).toHaveBeenCalledTimes(1)
- warn.mockRestore()
- })
-
- it('keeps unmatched tool results and suppresses matched ones', () => {
- const UI = createChatUI(chatOptions)
- const components = UI.defineComponents({
- layout: ({ renderMessages }) => renderMessages(),
- message: ({ renderParts }) => renderParts(),
- parts: {
- toolResult: ({ part }) =>
- part.type === 'tool-result' ? {String(part.content)} : null,
- fallback: () => null,
- },
- tools: {
- getWeather: () => weather ,
- purchaseItem: () => null,
- },
- interrupts: {
- generic: { choosePlan: () => null, fallback: () => null },
- },
- })
-
- const matched = renderToStaticMarkup(
- ,
- )
- expect(matched).toContain('weather ')
- expect(matched).not.toContain('')
-
- const unmatched = renderToStaticMarkup(
- ,
- )
- expect(unmatched).toContain('standalone ')
- })
-
- it('puts list approvals in Interrupts when interrupts.tools has the tool', () => {
- const UI = createChatUI(chatOptions)
- const list = UI.defineComponents({
- layout: ({ renderMessages, renderInterrupts }) => (
- <>
- {renderMessages()}
- {renderInterrupts()}
- >
- ),
- message: ({ renderParts }) => renderParts(),
- parts: { fallback: () => null },
- tools: {
- getWeather: () => null,
- purchaseItem: () => tool
,
- },
- interrupts: {
- tools: {
- purchaseItem: () => list-approval ,
- },
- generic: { choosePlan: () => null, fallback: () => null },
- },
- })
-
- const listMarkup = renderToStaticMarkup(
- ,
- )
- expect(listMarkup).toContain('list-approval')
- expect(listMarkup).toContain('tool')
- })
-
- it('lets a tool render its approval from interrupt without a registered interrupt component', () => {
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
- const UI = createChatUI(chatOptions)
- const components = UI.defineComponents({
- layout: ({ renderMessages, renderInterrupts }) => (
- <>
- {renderMessages()}
- {renderInterrupts()}
- >
- ),
- message: ({ renderParts }) => renderParts(),
- parts: { fallback: () => null },
- tools: {
- getWeather: () => null,
- purchaseItem: ({ interrupt }) =>
- interrupt ? {interrupt.toolName} : tool ,
- },
- interrupts: {
- generic: { choosePlan: () => null, fallback: () => list },
- },
- })
- const markup = renderToStaticMarkup(
- ,
- )
- expect(markup).toContain('purchaseItem ')
- expect(markup).not.toContain('list ')
- expect(
- warn.mock.calls.filter((call) =>
- String(call[0]).includes('[tanstack-ai-ui]'),
- ),
- ).toHaveLength(0)
- warn.mockRestore()
- })
-
- it('renders registered generic interrupts and sends the rest to fallback', () => {
- const UI = createChatUI(chatOptions)
- const components = UI.defineComponents({
- layout: ({ renderInterrupts }) => renderInterrupts(),
- message: ({ renderParts }) => renderParts(),
- parts: { fallback: () => null },
- tools: {
- getWeather: () => null,
- purchaseItem: () => null,
- },
- interrupts: {
- generic: {
- choosePlan: () => plan ,
- fallback: () => fallback ,
- },
- },
- })
-
- const markup = renderToStaticMarkup(
- ,
- )
- expect(markup).toContain('plan')
- expect(markup).toContain('fallback')
- })
-
- it('omits input when no input component exists', () => {
- const UI = createChatUI(chatOptions)
- const components = UI.defineComponents({
- layout: ({ renderInput }) => {renderInput()} ,
- message: ({ renderParts }) => renderParts(),
- parts: { fallback: () => null },
- tools: {
- getWeather: () => null,
- purchaseItem: () => null,
- },
- interrupts: {
- generic: { choosePlan: () => null },
- },
- })
- const markup = renderToStaticMarkup(
- ,
- )
- expect(markup).toBe(' ')
- })
-
- it('reads chat from nested provider context and throws outside a provider', () => {
- const UI = createChatUI(chatOptions)
- const components = UI.defineComponents({
- layout: () => {
- const chat = UI.useChat()
- return {chat.messages.length}
- },
- message: ({ renderParts }) => renderParts(),
- parts: { fallback: () => null },
- tools: {
- getWeather: () => null,
- purchaseItem: () => null,
- },
- interrupts: {
- generic: { choosePlan: () => null },
- },
- })
-
- const inner = host({ messages: [messageWithToolResults] })
- const outer = host({ messages: [] })
- const markup = renderToStaticMarkup(
-
-
- ,
- )
- expect(markup).toContain('1
')
-
- function Broken() {
- UI.useChat()
- return null
- }
- expect(() => renderToStaticMarkup( )).toThrow(
- /UI.Provider or UI.Chat/,
- )
- })
-
- it('renders a tool component for every ToolCallState', () => {
- const states: Array = [
- 'awaiting-input',
- 'input-streaming',
- 'input-complete',
- 'approval-requested',
- 'approval-responded',
- 'complete',
- 'error',
- ]
- const UI = createChatUI(chatOptions)
- const components = UI.defineComponents({
- layout: ({ renderMessages }) => renderMessages(),
- message: ({ renderParts }) => renderParts(),
- parts: { fallback: () => null },
- tools: {
- getWeather: ({ part }) => {part.state} ,
- purchaseItem: () => null,
- },
- interrupts: {
- generic: { choosePlan: () => null, fallback: () => null },
- },
- })
-
- for (const state of states) {
- const markup = renderToStaticMarkup(
- ,
- )
- expect(markup).toContain(`${state} `)
- }
- })
-})
diff --git a/packages/ai-react-ui/tsconfig.json b/packages/ai-react-ui/tsconfig.json
index d3e610bff5..aae0726181 100644
--- a/packages/ai-react-ui/tsconfig.json
+++ b/packages/ai-react-ui/tsconfig.json
@@ -6,6 +6,6 @@
"lib": ["ES2022", "DOM"],
"rootDir": "../.."
},
- "include": ["src", "tests"],
+ "include": ["src"],
"exclude": ["node_modules", "dist"]
}
diff --git a/packages/ai-react-ui/vite.config.ts b/packages/ai-react-ui/vite.config.ts
index 782c82ec49..190315c4bf 100644
--- a/packages/ai-react-ui/vite.config.ts
+++ b/packages/ai-react-ui/vite.config.ts
@@ -9,20 +9,8 @@ const config = defineConfig({
watch: false,
globals: true,
environment: 'node',
- include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
- coverage: {
- provider: 'v8',
- reporter: ['text', 'json', 'html', 'lcov'],
- exclude: [
- 'node_modules/',
- 'dist/',
- 'tests/',
- '**/*.test.ts',
- '**/*.config.ts',
- '**/types.ts',
- ],
- include: ['src/**/*.ts'],
- },
+ include: ['tests/**/*.test.ts'],
+ passWithNoTests: true,
},
})
diff --git a/packages/ai-react/README.md b/packages/ai-react/README.md
index c9bbb8c10d..d524f36907 100644
--- a/packages/ai-react/README.md
+++ b/packages/ai-react/README.md
@@ -226,15 +226,15 @@ on.
## Framework Packages
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
+| Package | What it provides |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
+| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
+| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
+| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
+| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
+| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
+| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. |
## Advanced Docs
diff --git a/packages/ai-react/package.json b/packages/ai-react/package.json
index abe8955b9b..2f7d2282c3 100644
--- a/packages/ai-react/package.json
+++ b/packages/ai-react/package.json
@@ -28,6 +28,10 @@
"./mcp-apps": {
"types": "./dist/esm/mcp-apps.d.ts",
"import": "./dist/esm/mcp-apps.js"
+ },
+ "./ui": {
+ "types": "./dist/esm/ui.d.ts",
+ "import": "./dist/esm/ui.js"
}
},
"files": [
@@ -59,17 +63,26 @@
"media-generation"
],
"dependencies": {
- "@tanstack/ai-client": "workspace:^"
+ "@tanstack/ai-client": "workspace:^",
+ "react-markdown": "^10.1.0",
+ "rehype-highlight": "^7.0.2",
+ "rehype-raw": "^7.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1"
},
"peerDependencies": {
"@mcp-ui/client": "^7",
"@tanstack/ai": "workspace:^",
"@types/react": ">=18.0.0",
- "react": ">=18.0.0"
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
},
"peerDependenciesMeta": {
"@mcp-ui/client": {
"optional": true
+ },
+ "react-dom": {
+ "optional": true
}
},
"devDependencies": {
@@ -81,6 +94,7 @@
"@vitest/coverage-v8": "4.1.10",
"jsdom": "^27.4.0",
"react": "^19.2.3",
+ "react-dom": "^19.2.3",
"vite": "^8.2.1"
}
}
diff --git a/packages/ai-react-ui/src/chat-input.tsx b/packages/ai-react/src/chat-ui/chat-input.tsx
similarity index 100%
rename from packages/ai-react-ui/src/chat-input.tsx
rename to packages/ai-react/src/chat-ui/chat-input.tsx
diff --git a/packages/ai-react-ui/src/chat-message.tsx b/packages/ai-react/src/chat-ui/chat-message.tsx
similarity index 99%
rename from packages/ai-react-ui/src/chat-message.tsx
rename to packages/ai-react/src/chat-ui/chat-message.tsx
index 8df82253cf..41dd183161 100644
--- a/packages/ai-react-ui/src/chat-message.tsx
+++ b/packages/ai-react/src/chat-ui/chat-message.tsx
@@ -1,7 +1,7 @@
import { ThinkingPart } from './thinking-part'
import { toolResultContentToString } from './tool-result-content'
import type { ReactNode } from 'react'
-import type { UIMessage } from '@tanstack/ai-react'
+import type { UIMessage } from '../types'
export interface ToolCallRenderProps {
id: string
diff --git a/packages/ai-react-ui/src/chat-messages.tsx b/packages/ai-react/src/chat-ui/chat-messages.tsx
similarity index 97%
rename from packages/ai-react-ui/src/chat-messages.tsx
rename to packages/ai-react/src/chat-ui/chat-messages.tsx
index e05526442e..f87ea034da 100644
--- a/packages/ai-react-ui/src/chat-messages.tsx
+++ b/packages/ai-react/src/chat-ui/chat-messages.tsx
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'
import { useChatContext } from './chat'
import { ChatMessage } from './chat-message'
import type { ReactNode } from 'react'
-import type { UIMessage } from '@tanstack/ai-react'
+import type { UIMessage } from '../types'
/** @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.9.0. Removed in 1.0.0. */
export interface ChatMessagesProps {
diff --git a/packages/ai-react-ui/src/chat.tsx b/packages/ai-react/src/chat-ui/chat.tsx
similarity index 87%
rename from packages/ai-react-ui/src/chat.tsx
rename to packages/ai-react/src/chat-ui/chat.tsx
index 7b78b872af..463fa2d20f 100644
--- a/packages/ai-react-ui/src/chat.tsx
+++ b/packages/ai-react/src/chat-ui/chat.tsx
@@ -1,11 +1,8 @@
import { createContext, useContext } from 'react'
-import { useChat } from '@tanstack/ai-react'
+import type { ConnectionAdapter } from '@tanstack/ai-client'
+import { useChat } from '../use-chat'
import type { ReactNode } from 'react'
-import type {
- ConnectionAdapter,
- UIMessage,
- UseChatReturn,
-} from '@tanstack/ai-react'
+import type { UIMessage, UseChatReturn } from '../types'
/**
* Chat context - provides chat state to all child components
@@ -16,7 +13,7 @@ const ChatContext = createContext(null)
* Hook to access chat context
* @throws Error if used outside of Chat component
*/
-/** @deprecated Use `createChatUI().useChat()` instead. Deprecated in 0.9.0. Removed in 1.0.0. */
+/** @deprecated Use `createChatUI().useChatContext()` instead. Deprecated in 0.9.0. Removed in 1.0.0. */
export function useChatContext() {
const context = useContext(ChatContext)
if (!context) {
@@ -54,9 +51,9 @@ export interface ChatProps {
}
/**
- * @deprecated Use `createChatUI()` from `@tanstack/ai-react-ui` instead.
+ * @deprecated Since 0.9.0. Use `Chat` from `@tanstack/ai-react/ui` with `useChat` from `@tanstack/ai-react`.
* See https://tanstack.com/ai/latest/docs/migration/create-ui
- * Deprecated in 0.9.0. Removed in 1.0.0.
+ * Removed in 1.0.0.
*
* Root Chat component - provides context for all chat subcomponents
*
diff --git a/packages/ai-react/src/chat-ui/create-ui.tsx b/packages/ai-react/src/chat-ui/create-ui.tsx
new file mode 100644
index 0000000000..07379a8990
--- /dev/null
+++ b/packages/ai-react/src/chat-ui/create-ui.tsx
@@ -0,0 +1,304 @@
+import { createContext, memo, useContext } from 'react'
+import type { ComponentType, ReactNode } from 'react'
+import {
+ automaticPartsForMessage,
+ collectInlineToolNames,
+ resolveInterruptComponent,
+ selectChatUI,
+ selectMessageUI,
+} from '@tanstack/ai-client/ui'
+import type {
+ ChatUIData,
+ ChatUIHasNamedInterrupts,
+ ChatUIHasNamedTools,
+ ChatUIInterruptName,
+ ChatUIInterruptOf,
+ ChatUIInterruptsOf,
+ ChatUIMessages,
+ ChatUINamedInterruptId,
+ ChatUIPartKey,
+ ChatUIPartOf,
+ ChatUISchemaOf,
+ ChatUISelectedPart,
+ ChatUIToolApproval,
+ ChatUIToolName,
+ ChatUIToolsOf,
+} from '@tanstack/ai-client/ui'
+import type {
+ ToolCallPart,
+ ToolResultPart,
+ UIMessage,
+} from '@tanstack/ai-client'
+import type { UseChatReturn } from '../types'
+
+export type ChatUIHost = UseChatReturn<
+ ChatUIToolsOf,
+ ChatUISchemaOf,
+ ChatUIInterruptsOf
+>
+
+export type LayoutProps = {
+ chat: ChatUIHost
+ renderMessages: () => ReactNode
+ renderInterrupts: () => ReactNode
+ renderInput: () => ReactNode
+}
+
+export type MessageProps = {
+ chat: ChatUIHost
+ message: UIMessage, ChatUIData>
+ renderParts: () => ReactNode
+}
+
+export type InputProps = {
+ chat: ChatUIHost
+}
+
+export type PartProps = {
+ part: ChatUIPartOf
+}
+
+export type ToolProps<
+ TOptions,
+ TName extends ChatUIToolName = ChatUIToolName,
+> = {
+ part: Extract>, { name: TName }>
+ result?: ToolResultPart
+ interrupt?: ChatUIToolApproval
+}
+
+export type InterruptProps<
+ TOptions,
+ TName extends ChatUIInterruptName = never,
+> = {
+ interrupt: ChatUIInterruptOf
+}
+
+type GenericInterruptComponents =
+ ChatUIHasNamedInterrupts extends true
+ ? {
+ [K in ChatUINamedInterruptId]: ComponentType<
+ InterruptProps>
+ >
+ } & {
+ fallback?: ComponentType>
+ }
+ : {
+ fallback?: ComponentType>
+ }
+
+type ToolApprovalMap = {
+ [K in ChatUIToolName]?: ComponentType<
+ InterruptProps>
+ >
+}
+
+export type ChatUIComponents = {
+ layout: ComponentType>
+ message: ComponentType>
+ input?: ComponentType>
+ parts: {
+ [K in ChatUIPartKey]?: ComponentType>
+ } & {
+ fallback?: ComponentType>
+ }
+} & (ChatUIHasNamedTools extends true
+ ? {
+ tools: {
+ [K in ChatUIToolName]: ComponentType>
+ }
+ }
+ : {
+ tools?: {
+ [K in ChatUIToolName]?: ComponentType>
+ }
+ }) &
+ (ChatUIHasNamedInterrupts extends true
+ ? {
+ interrupts: {
+ tools?: ToolApprovalMap
+ generic: GenericInterruptComponents
+ }
+ }
+ : {
+ interrupts?: {
+ tools?: ToolApprovalMap
+ generic?: GenericInterruptComponents
+ }
+ })
+
+type ChatTree = {
+ chat: ChatUIHost
+ components: ChatUIComponents
+ warn: (key: string, message: string) => void
+ inlineToolNames: Array
+}
+
+const ChatTreeContext = createContext(null)
+
+const warnedKeys = new Set()
+function warnMissing(key: string, message: string) {
+ if (warnedKeys.has(key)) return
+ warnedKeys.add(key)
+ console.warn(message)
+}
+
+function useChatTree() {
+ const tree = useContext(ChatTreeContext)
+ if (!tree) {
+ throw new Error('`useChatContext` must be used within `Chat`.')
+ }
+ return tree
+}
+
+export function useChatContext(): ChatUIHost {
+ return useChatTree().chat
+}
+
+function selectedPartPropsEqual(
+ prev: { selected: ChatUISelectedPart },
+ next: { selected: ChatUISelectedPart },
+) {
+ if (prev.selected.key !== next.selected.key) return false
+ if (prev.selected.part !== next.selected.part) return false
+ if (prev.selected.key === 'toolCall' && next.selected.key === 'toolCall') {
+ return (
+ prev.selected.result === next.selected.result &&
+ prev.selected.interrupt === next.selected.interrupt
+ )
+ }
+ if (
+ prev.selected.key === 'toolResult' &&
+ next.selected.key === 'toolResult'
+ ) {
+ return prev.selected.matched === next.selected.matched
+ }
+ return true
+}
+
+const SelectedPartInner = memo(function SelectedPartInner({
+ selected,
+}: {
+ selected: ChatUISelectedPart
+}) {
+ const { components, warn } = useChatTree()
+ if (selected.key === 'toolCall') {
+ const name = selected.part.name
+ const Tool = components.tools?.[name as never] as
+ | ComponentType>
+ | undefined
+ if (!Tool) {
+ warn(`tool:${name}`, `[tanstack-ai-ui] Missing tools.${name} component`)
+ return null
+ }
+ return (
+ ['interrupt']}
+ />
+ )
+ }
+
+ const PartComponent = (components.parts[selected.key] ??
+ components.parts.fallback) as ComponentType> | undefined
+ if (!PartComponent) {
+ warn(
+ `part:${selected.key}`,
+ `[tanstack-ai-ui] Missing parts.${selected.key} component`,
+ )
+ return null
+ }
+ return ['part']} />
+}, selectedPartPropsEqual)
+
+function AutomaticParts({ message }: { message: ChatUIMessages[number] }) {
+ const { chat, inlineToolNames } = useChatTree()
+ const selected = selectMessageUI(message, {
+ interrupts: chat.interrupts ?? [],
+ inlineToolNames,
+ })
+ return (
+ <>
+ {automaticPartsForMessage(selected).map((part, index) => (
+
+ ))}
+ >
+ )
+}
+
+function Messages() {
+ const { chat, components } = useChatTree()
+ const MessageComponent = components.message
+ return (
+ <>
+ {(chat.messages as ChatUIMessages).map((message) => (
+ }
+ />
+ ))}
+ >
+ )
+}
+
+function Interrupts() {
+ const { chat, components, warn, inlineToolNames } = useChatTree()
+ const selected = selectChatUI({
+ messages: chat.messages,
+ interrupts: chat.interrupts ?? [],
+ inlineToolNames,
+ })
+ return (
+ <>
+ {selected.interrupts.map((interrupt) => {
+ const Component = resolveInterruptComponent(
+ interrupt,
+ components.interrupts,
+ ) as ComponentType> | undefined
+ if (!Component) {
+ warn(
+ `interrupt:${interrupt.id}`,
+ `[tanstack-ai-ui] Missing interrupt component for ${interrupt.kind}`,
+ )
+ return null
+ }
+ return
+ })}
+ >
+ )
+}
+
+export function Chat({
+ chat,
+ components,
+}: {
+ chat: ChatUIHost
+ components: ChatUIComponents
+}) {
+ const tools = (components as ChatUIComponents).tools
+ const inlineToolNames = collectInlineToolNames(
+ components.interrupts?.tools as Record | undefined,
+ Object.keys(tools ?? {}),
+ )
+ const Layout = components.layout
+ const Input = components.input
+ const tree: ChatTree = {
+ chat: chat as ChatUIHost,
+ components: components as ChatUIComponents,
+ warn: warnMissing,
+ inlineToolNames,
+ }
+ return (
+
+ }
+ renderInterrupts={() => }
+ renderInput={() => (Input ? : null)}
+ />
+
+ )
+}
diff --git a/packages/ai-react-ui/src/markdown-plugins.ts b/packages/ai-react/src/chat-ui/markdown-plugins.ts
similarity index 100%
rename from packages/ai-react-ui/src/markdown-plugins.ts
rename to packages/ai-react/src/chat-ui/markdown-plugins.ts
diff --git a/packages/ai-react-ui/src/text-part.tsx b/packages/ai-react/src/chat-ui/text-part.tsx
similarity index 100%
rename from packages/ai-react-ui/src/text-part.tsx
rename to packages/ai-react/src/chat-ui/text-part.tsx
diff --git a/packages/ai-react-ui/src/thinking-part.tsx b/packages/ai-react/src/chat-ui/thinking-part.tsx
similarity index 100%
rename from packages/ai-react-ui/src/thinking-part.tsx
rename to packages/ai-react/src/chat-ui/thinking-part.tsx
diff --git a/packages/ai-react-ui/src/tool-approval.tsx b/packages/ai-react/src/chat-ui/tool-approval.tsx
similarity index 100%
rename from packages/ai-react-ui/src/tool-approval.tsx
rename to packages/ai-react/src/chat-ui/tool-approval.tsx
diff --git a/packages/ai-react-ui/src/tool-result-content.ts b/packages/ai-react/src/chat-ui/tool-result-content.ts
similarity index 95%
rename from packages/ai-react-ui/src/tool-result-content.ts
rename to packages/ai-react/src/chat-ui/tool-result-content.ts
index a6d5659f3a..8825b51ad9 100644
--- a/packages/ai-react-ui/src/tool-result-content.ts
+++ b/packages/ai-react/src/chat-ui/tool-result-content.ts
@@ -1,4 +1,4 @@
-import type { UIMessage } from '@tanstack/ai-react'
+import type { UIMessage } from '../types'
type ToolResultPart = Extract<
UIMessage['parts'][number],
diff --git a/packages/ai-react/src/create-chat-hook.ts b/packages/ai-react/src/create-chat-hook.ts
new file mode 100644
index 0000000000..f143af1ef1
--- /dev/null
+++ b/packages/ai-react/src/create-chat-hook.ts
@@ -0,0 +1,67 @@
+import { useChat as useUnboundChat } from './use-chat'
+import type {
+ AnyClientTool,
+ InterruptDefinition,
+ SchemaInput,
+} from '@tanstack/ai/client'
+import type { InferredClientContext } from '@tanstack/ai-client'
+import type { UseChatOptions } from './types'
+
+type ChatHookOverrides<
+ TTools extends ReadonlyArray,
+ TSchema extends SchemaInput | undefined,
+ TContext,
+ TInterrupts extends ReadonlyArray>,
+> = {
+ threadId?: string
+ live?: boolean
+ forwardedProps?: Record
+ body?: Record
+ initialMessages?: UseChatOptions<
+ TTools,
+ TSchema,
+ TContext,
+ TInterrupts
+ >['initialMessages']
+}
+
+/**
+ * Bind chat options once at module scope. The returned `useChat` hook
+ * creates a chat instance from those options.
+ *
+ * Pass per-call overrides for instance keys such as `threadId`,
+ * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
+ * `interrupts`, or `outputSchema` here. Those stay on the factory options.
+ *
+ * Rename the hook at the call site if you already import `useChat` from
+ * `@tanstack/ai-react`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
+ *
+ * @example
+ * ```tsx
+ * const { useChat } = createChatHook(chatOptions)
+ *
+ * function Support() {
+ * const chat = useChat({ threadId: 'support-1' })
+ * return
+ * }
+ * ```
+ */
+export function createChatHook<
+ const TTools extends ReadonlyArray = any,
+ TSchema extends SchemaInput | undefined = undefined,
+ TContext = InferredClientContext,
+ const TInterrupts extends ReadonlyArray<
+ InterruptDefinition
+ > = readonly [],
+>(options: UseChatOptions) {
+ function useChat(
+ overrides?: ChatHookOverrides,
+ ) {
+ if (!overrides) {
+ return useUnboundChat(options)
+ }
+ return useUnboundChat({ ...options, ...overrides })
+ }
+
+ return { useChat }
+}
diff --git a/packages/ai-react/src/index.ts b/packages/ai-react/src/index.ts
index 0ea29a70f3..1ac2e347ab 100644
--- a/packages/ai-react/src/index.ts
+++ b/packages/ai-react/src/index.ts
@@ -1,4 +1,5 @@
export { useChat } from './use-chat'
+export { createChatHook } from './create-chat-hook'
export { useByok } from './use-byok'
export { useRealtimeChat } from './use-realtime-chat'
export { useMcpAppBridge } from './use-mcp-app-bridge'
diff --git a/packages/ai-react/src/ui.ts b/packages/ai-react/src/ui.ts
new file mode 100644
index 0000000000..454f39bdaa
--- /dev/null
+++ b/packages/ai-react/src/ui.ts
@@ -0,0 +1,40 @@
+// Barrel entry for the `@tanstack/ai-react/ui` subpath. The JSX
+// implementation lives under `./chat-ui`; this `.ts` re-export exists so
+// kiira's dist->src resolution (which maps `dist/esm/ui.d.ts` to `src/ui.ts`,
+// never a directory index) can type-check docs snippets that import this
+// subpath.
+export {
+ Chat,
+ useChatContext,
+ type ChatUIComponents,
+ type ChatUIHost,
+ type InputProps,
+ type InterruptProps,
+ type LayoutProps,
+ type MessageProps,
+ type PartProps,
+ type ToolProps,
+} from './chat-ui/create-ui'
+export {
+ Chat as DeprecatedChat,
+ useChatContext as useDeprecatedChatContext,
+ type ChatProps,
+} from './chat-ui/chat'
+export { ChatMessages, type ChatMessagesProps } from './chat-ui/chat-messages'
+export {
+ ChatMessage,
+ type ChatMessageProps,
+ type ToolCallRenderProps,
+} from './chat-ui/chat-message'
+export {
+ ChatInput,
+ type ChatInputProps,
+ type ChatInputRenderProps,
+} from './chat-ui/chat-input'
+export {
+ ToolApproval,
+ type ToolApprovalProps,
+ type ToolApprovalRenderProps,
+} from './chat-ui/tool-approval'
+export { TextPart, type TextPartProps } from './chat-ui/text-part'
+export { ThinkingPart, type ThinkingPartProps } from './chat-ui/thinking-part'
diff --git a/packages/ai-react/tests/chat-ui/coexistence.test.ts b/packages/ai-react/tests/chat-ui/coexistence.test.ts
new file mode 100644
index 0000000000..2db3c28849
--- /dev/null
+++ b/packages/ai-react/tests/chat-ui/coexistence.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, it } from 'vitest'
+import { Chat, useChatContext } from '../../src/ui'
+import { createChatHook } from '../../src/create-chat-hook'
+
+describe('public coexistence', () => {
+ it('exports Chat UI and the options-only createChatHook', () => {
+ expect(Chat).toBeDefined()
+ expect(useChatContext).toBeDefined()
+ expect(createChatHook).toBeDefined()
+ })
+})
diff --git a/packages/ai-react-ui/tests/create-ui-types.test.tsx b/packages/ai-react/tests/chat-ui/create-ui-types.test.tsx
similarity index 81%
rename from packages/ai-react-ui/tests/create-ui-types.test.tsx
rename to packages/ai-react/tests/chat-ui/create-ui-types.test.tsx
index 9dfe8e6644..9c951999fb 100644
--- a/packages/ai-react-ui/tests/create-ui-types.test.tsx
+++ b/packages/ai-react/tests/chat-ui/create-ui-types.test.tsx
@@ -1,12 +1,12 @@
import { expectTypeOf, it } from 'vitest'
-import { createChatUI } from '../src/create-ui'
import type {
+ ChatUIComponents,
ChatUIHost,
InterruptProps,
PartProps,
ToolProps,
-} from '../src/create-ui'
-import { chatOptions } from '../../ai-client/tests/ui-fixtures'
+} from '../../src/chat-ui/create-ui'
+import { chatOptions } from '../../../ai-client/tests/ui-fixtures'
it('types tool and interrupt component props from chatOptions', () => {
type TextPartProps = PartProps
@@ -47,13 +47,9 @@ it('types tool and interrupt component props from chatOptions', () => {
{ title: string } | undefined
>()
- const UI = createChatUI(chatOptions)
-
- UI.defineComponents({
- layout: ({ renderMessages }) => {
- expectTypeOf(UI.useChat()).toEqualTypeOf>()
- expectTypeOf(UI.useChat().sendMessage).toBeFunction()
- expectTypeOf(UI.useChat().queue).toBeArray()
+ const components = {
+ layout: ({ chat, renderMessages }) => {
+ expectTypeOf(chat).toEqualTypeOf>()
return renderMessages()
},
message: ({ renderParts }) => renderParts(),
@@ -99,13 +95,13 @@ it('types tool and interrupt component props from chatOptions', () => {
fallback: () => null,
},
},
- })
+ } satisfies ChatUIComponents
+ void components
- UI.defineComponents({
+ const missingTool = {
layout: () => null,
message: () => null,
parts: { fallback: () => null },
- // @ts-expect-error Every configured tool needs a component.
tools: {
getWeather: () => null,
},
@@ -114,9 +110,11 @@ it('types tool and interrupt component props from chatOptions', () => {
choosePlan: () => null,
},
},
- })
+ }
+ // @ts-expect-error Every configured tool needs a component.
+ missingTool satisfies ChatUIComponents
- UI.defineComponents({
+ const missingInterrupt = {
layout: () => null,
message: () => null,
parts: { fallback: () => null },
@@ -125,17 +123,11 @@ it('types tool and interrupt component props from chatOptions', () => {
purchaseItem: () => null,
},
interrupts: {
- // @ts-expect-error Every registered interrupt id needs a component.
generic: {
fallback: () => null,
},
},
- })
-
- const Untyped = createChatUI({})
- Untyped.defineComponents({
- layout: () => null,
- message: () => null,
- parts: { fallback: () => null },
- })
+ }
+ // @ts-expect-error Every registered interrupt id needs a component.
+ missingInterrupt satisfies ChatUIComponents
})
diff --git a/packages/ai-react/tests/chat-ui/create-ui.test.tsx b/packages/ai-react/tests/chat-ui/create-ui.test.tsx
new file mode 100644
index 0000000000..a66ccddb09
--- /dev/null
+++ b/packages/ai-react/tests/chat-ui/create-ui.test.tsx
@@ -0,0 +1,248 @@
+import { renderToStaticMarkup } from 'react-dom/server'
+import { describe, expect, it, vi } from 'vitest'
+import { Chat, useChatContext } from '../../src/chat-ui/create-ui'
+import type { ChatUIComponents, ChatUIHost } from '../../src/chat-ui/create-ui'
+import {
+ chatOptions,
+ createChatResult,
+ genericInterrupt,
+ messageWithToolResults,
+ orphanResultMessage,
+ purchaseApprovalInterrupt,
+ purchaseApprovalMessage,
+ unboundInterrupt,
+ unknownToolMessage,
+} from '../../../ai-client/tests/ui-fixtures'
+import type { ToolCallState } from '@tanstack/ai-client'
+
+function host(
+ init?: Parameters[0],
+): ChatUIHost {
+ return createChatResult(init ?? {}) as unknown as ChatUIHost<
+ typeof chatOptions
+ >
+}
+
+const baseComponents: ChatUIComponents = {
+ layout: ({ renderMessages, renderInterrupts, renderInput }) => (
+ <>
+ {renderMessages()}
+ {renderInterrupts()}
+ {renderInput()}
+ >
+ ),
+ message: ({ renderParts }) => {renderParts()} ,
+ parts: { fallback: ({ part }) => {part.type} },
+ tools: {
+ getWeather: ({ part }) => {part.input?.city} ,
+ purchaseItem: () => null,
+ },
+ interrupts: {
+ generic: {
+ choosePlan: () => null,
+ fallback: ({ interrupt }) => {interrupt.reason} ,
+ },
+ },
+}
+
+function render(
+ chat: ChatUIHost,
+ patch?: Partial> & {
+ parts?: ChatUIComponents['parts']
+ tools?: ChatUIComponents['tools']
+ interrupts?: ChatUIComponents['interrupts']
+ },
+) {
+ const components: ChatUIComponents = {
+ ...baseComponents,
+ ...patch,
+ parts: { ...baseComponents.parts, ...patch?.parts },
+ tools: { ...baseComponents.tools, ...patch?.tools },
+ interrupts: {
+ tools: {
+ ...baseComponents.interrupts.tools,
+ ...patch?.interrupts?.tools,
+ },
+ generic: {
+ ...baseComponents.interrupts.generic,
+ ...patch?.interrupts?.generic,
+ },
+ },
+ }
+ return renderToStaticMarkup( )
+}
+
+describe('Chat', () => {
+ it('renders mapped tools from chat.messages', () => {
+ expect(render(host({ messages: [messageWithToolResults] }))).toContain(
+ 'Paris ',
+ )
+ })
+
+ it('warns once for a missing runtime key', () => {
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
+ const chat = host({ messages: [unknownToolMessage] })
+ render(chat)
+ render(chat)
+ expect(warn).toHaveBeenCalledTimes(1)
+ warn.mockRestore()
+ })
+
+ it('keeps unmatched tool results and suppresses matched ones', () => {
+ const parts = {
+ toolResult: ({ part }: { part: { type: string; content?: unknown } }) =>
+ part.type === 'tool-result' ? {String(part.content)} : null,
+ fallback: () => null,
+ }
+ const matched = render(host({ messages: [messageWithToolResults] }), {
+ parts,
+ tools: {
+ getWeather: () => weather ,
+ purchaseItem: () => null,
+ },
+ })
+ expect(matched).toContain('weather ')
+ expect(matched).not.toContain('')
+
+ const unmatched = render(host({ messages: [orphanResultMessage] }), {
+ parts,
+ tools: {
+ getWeather: () => weather ,
+ purchaseItem: () => null,
+ },
+ })
+ expect(unmatched).toContain('standalone ')
+ })
+
+ it('puts list approvals in the interrupt list when interrupts.tools has the tool', () => {
+ const markup = render(
+ host({
+ messages: [purchaseApprovalMessage],
+ interrupts: [purchaseApprovalInterrupt],
+ }),
+ {
+ tools: {
+ getWeather: () => null,
+ purchaseItem: () => tool
,
+ },
+ interrupts: {
+ tools: {
+ purchaseItem: () => list-approval ,
+ },
+ generic: { choosePlan: () => null, fallback: () => null },
+ },
+ },
+ )
+ expect(markup).toContain('list-approval')
+ expect(markup).toContain('tool')
+ })
+
+ it('lets a tool render its approval from interrupt without a registered interrupt component', () => {
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
+ const markup = render(
+ host({
+ messages: [purchaseApprovalMessage],
+ interrupts: [purchaseApprovalInterrupt],
+ }),
+ {
+ tools: {
+ getWeather: () => null,
+ purchaseItem: ({ interrupt }) =>
+ interrupt ? {interrupt.toolName} : tool ,
+ },
+ interrupts: {
+ generic: { choosePlan: () => null, fallback: () => list },
+ },
+ },
+ )
+ expect(markup).toContain('purchaseItem ')
+ expect(markup).not.toContain('list ')
+ expect(
+ warn.mock.calls.filter((call) =>
+ String(call[0]).includes('[tanstack-ai-ui]'),
+ ),
+ ).toHaveLength(0)
+ warn.mockRestore()
+ })
+
+ it('renders registered generic interrupts and sends the rest to fallback', () => {
+ const markup = render(
+ host({
+ interrupts: [genericInterrupt, unboundInterrupt],
+ }),
+ {
+ layout: ({ renderInterrupts }) => renderInterrupts(),
+ interrupts: {
+ generic: {
+ choosePlan: () => plan ,
+ fallback: () => fallback ,
+ },
+ },
+ },
+ )
+ expect(markup).toContain('plan')
+ expect(markup).toContain('fallback')
+ })
+
+ it('omits input when no input component exists', () => {
+ const markup = render(host({}), {
+ layout: ({ renderInput }) => {renderInput()} ,
+ })
+ expect(markup).toBe(' ')
+ })
+
+ it('passes chat into the input component', () => {
+ const markup = render(host({}), {
+ input: ({ chat }) => {chat.messages.length} ,
+ })
+ expect(markup).toContain('0 ')
+ })
+
+ it('throws useChatContext outside Chat', () => {
+ function Broken() {
+ useChatContext()
+ return null
+ }
+ expect(() => renderToStaticMarkup( )).toThrow(/useChatContext/)
+ })
+
+ it('renders a tool component for every ToolCallState', () => {
+ const states: Array = [
+ 'awaiting-input',
+ 'input-streaming',
+ 'input-complete',
+ 'approval-requested',
+ 'approval-responded',
+ 'complete',
+ 'error',
+ ]
+ for (const state of states) {
+ const markup = render(
+ host({
+ messages: [
+ {
+ id: state,
+ role: 'assistant',
+ parts: [
+ {
+ type: 'tool-call',
+ id: `call-${state}`,
+ name: 'getWeather',
+ arguments: '{}',
+ state,
+ },
+ ],
+ },
+ ],
+ }),
+ {
+ tools: {
+ getWeather: ({ part }) => {part.state} ,
+ purchaseItem: () => null,
+ },
+ },
+ )
+ expect(markup).toContain(`${state} `)
+ }
+ })
+})
diff --git a/packages/ai-react-ui/tests/markdown-plugins.test.ts b/packages/ai-react/tests/chat-ui/markdown-plugins.test.ts
similarity index 98%
rename from packages/ai-react-ui/tests/markdown-plugins.test.ts
rename to packages/ai-react/tests/chat-ui/markdown-plugins.test.ts
index 6e1011bddc..c783a39d68 100644
--- a/packages/ai-react-ui/tests/markdown-plugins.test.ts
+++ b/packages/ai-react/tests/chat-ui/markdown-plugins.test.ts
@@ -8,7 +8,7 @@ import {
DEFAULT_REHYPE_PLUGINS_BEFORE_USER,
DEFAULT_REMARK_PLUGINS,
resolveMarkdownPlugins,
-} from '../src/markdown-plugins'
+} from '../../src/chat-ui/markdown-plugins'
const userRemark = () => () => {}
const userRehype = () => () => {}
diff --git a/packages/ai-react-ui/tests/react-dom-server.d.ts b/packages/ai-react/tests/chat-ui/react-dom-server.d.ts
similarity index 100%
rename from packages/ai-react-ui/tests/react-dom-server.d.ts
rename to packages/ai-react/tests/chat-ui/react-dom-server.d.ts
diff --git a/packages/ai-react-ui/tests/tool-result-content.test.ts b/packages/ai-react/tests/chat-ui/tool-result-content.test.ts
similarity index 94%
rename from packages/ai-react-ui/tests/tool-result-content.test.ts
rename to packages/ai-react/tests/chat-ui/tool-result-content.test.ts
index 4f51c69e82..3a142833e0 100644
--- a/packages/ai-react-ui/tests/tool-result-content.test.ts
+++ b/packages/ai-react/tests/chat-ui/tool-result-content.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { toolResultContentToString } from '../src/tool-result-content'
+import { toolResultContentToString } from '../../src/chat-ui/tool-result-content'
describe('toolResultContentToString', () => {
it('returns string content unchanged', () => {
diff --git a/packages/ai-react/tests/create-chat-hook-types.test.ts b/packages/ai-react/tests/create-chat-hook-types.test.ts
new file mode 100644
index 0000000000..def6d30c2d
--- /dev/null
+++ b/packages/ai-react/tests/create-chat-hook-types.test.ts
@@ -0,0 +1,38 @@
+import { expectTypeOf, it } from 'vitest'
+import { toolDefinition } from '@tanstack/ai'
+import { createChatHook } from '../src/create-chat-hook'
+import { useChat as useUnboundChat } from '../src/use-chat'
+
+it('bound useChat infers tool names from factory options', () => {
+ const check = () => {
+ const getWeather = toolDefinition({
+ name: 'getWeather',
+ description: 'Look up weather',
+ }).client(() => ({ ok: true }))
+ const purchaseItem = toolDefinition({
+ name: 'purchaseItem',
+ description: 'Buy an item',
+ }).client(() => ({ ok: true }))
+
+ const chatOptions = {
+ connection: { connect: async function* () {} },
+ tools: [getWeather, purchaseItem],
+ }
+
+ const { useChat } = createChatHook(chatOptions)
+ const fromHook = useChat()
+ const fromUseChat = useUnboundChat(chatOptions)
+
+ expectTypeOf(fromHook).toEqualTypeOf(fromUseChat)
+
+ const message = fromHook.messages[0]
+ if (message?.role === 'assistant') {
+ for (const part of message.parts) {
+ if (part.type === 'tool-call') {
+ expectTypeOf(part.name).toEqualTypeOf<'getWeather' | 'purchaseItem'>()
+ }
+ }
+ }
+ }
+ void check
+})
diff --git a/packages/ai-react/tests/create-chat-hook.test.ts b/packages/ai-react/tests/create-chat-hook.test.ts
new file mode 100644
index 0000000000..6a8462d268
--- /dev/null
+++ b/packages/ai-react/tests/create-chat-hook.test.ts
@@ -0,0 +1,54 @@
+import { renderHook } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import { createChatHook } from '../src/create-chat-hook'
+import { createMockConnectionAdapter } from './test-utils'
+import type { UIMessage } from '../src/types'
+
+describe('createChatHook', () => {
+ it('creates a chat from factory options', () => {
+ const initialMessages: Array = [
+ {
+ id: 'msg-1',
+ role: 'user',
+ parts: [{ type: 'text', content: 'Hello' }],
+ createdAt: new Date(),
+ },
+ ]
+ const { useChat } = createChatHook({
+ connection: createMockConnectionAdapter(),
+ initialMessages,
+ })
+
+ const { result } = renderHook(() => useChat())
+
+ expect(result.current.messages).toEqual(initialMessages)
+ })
+
+ it('merges per-call instance overrides', () => {
+ const { useChat } = createChatHook({
+ connection: createMockConnectionAdapter(),
+ initialMessages: [
+ {
+ id: 'factory',
+ role: 'user',
+ parts: [{ type: 'text', content: 'factory' }],
+ createdAt: new Date(),
+ },
+ ],
+ })
+
+ const overrideMessages: Array = [
+ {
+ id: 'override',
+ role: 'user',
+ parts: [{ type: 'text', content: 'override' }],
+ createdAt: new Date(),
+ },
+ ]
+ const { result } = renderHook(() =>
+ useChat({ initialMessages: overrideMessages, threadId: 'thread-1' }),
+ )
+
+ expect(result.current.messages).toEqual(overrideMessages)
+ })
+})
diff --git a/packages/ai-react/vite.config.ts b/packages/ai-react/vite.config.ts
index a697ee62f8..0ccd51800e 100644
--- a/packages/ai-react/vite.config.ts
+++ b/packages/ai-react/vite.config.ts
@@ -29,7 +29,7 @@ const config = defineConfig({
export default mergeConfig(
config,
tanstackViteConfig({
- entry: ['./src/index.ts', './src/mcp-apps.ts'],
+ entry: ['./src/index.ts', './src/mcp-apps.ts', './src/ui.ts'],
srcDir: './src',
cjs: false,
}),
diff --git a/packages/ai-solid-ui/CHANGELOG.md b/packages/ai-solid-ui/CHANGELOG.md
deleted file mode 100644
index 93dc65a432..0000000000
--- a/packages/ai-solid-ui/CHANGELOG.md
+++ /dev/null
@@ -1,403 +0,0 @@
-# @tanstack/ai-solid-ui
-
-## 0.7.21
-
-### Patch Changes
-
-- Updated dependencies [[`62c19ed`](https://github.com/TanStack/ai/commit/62c19edce7a814d868491ca920003899ec4c486b), [`4e9c5d2`](https://github.com/TanStack/ai/commit/4e9c5d2887dc9a3d64fce6ed424aa68f451e20f6), [`62c19ed`](https://github.com/TanStack/ai/commit/62c19edce7a814d868491ca920003899ec4c486b)]:
- - @tanstack/ai-client@0.29.0
- - @tanstack/ai-solid@0.19.2
-
-## 0.7.20
-
-### Patch Changes
-
-- Updated dependencies [[`ad3840c`](https://github.com/TanStack/ai/commit/ad3840c44bd09860b29cd22f24a0cdce3adaf9dc), [`365efb8`](https://github.com/TanStack/ai/commit/365efb870e5a3fe084d0969f71781ffc772e39a0), [`a955710`](https://github.com/TanStack/ai/commit/a9557101048286c225145bad058ca81ff275b330), [`a74817a`](https://github.com/TanStack/ai/commit/a74817a38e8deb4c7ff86d81067db12befbadf5d)]:
- - @tanstack/ai-client@0.28.0
- - @tanstack/ai-solid@0.19.1
-
-## 0.7.19
-
-### Patch Changes
-
-- Updated dependencies [[`b7ebcb0`](https://github.com/TanStack/ai/commit/b7ebcb0bbe63e425facb5e38f138bd0cd36637dd)]:
- - @tanstack/ai-client@0.27.0
- - @tanstack/ai-solid@0.19.0
-
-## 0.7.18
-
-### Patch Changes
-
-- Updated dependencies [[`1c0415b`](https://github.com/TanStack/ai/commit/1c0415bec4bbefcd3abf784d0209af05aca5db46)]:
- - @tanstack/ai-client@0.26.0
- - @tanstack/ai-solid@0.18.3
-
-## 0.7.17
-
-### Patch Changes
-
-- Updated dependencies [[`5f68cbc`](https://github.com/TanStack/ai/commit/5f68cbccf3621b48dae73cedcb1e59cb4cbe72b4), [`32e62ab`](https://github.com/TanStack/ai/commit/32e62ab8b7dc6a8a13ca3851c8925ab806e08f29)]:
- - @tanstack/ai-client@0.25.0
- - @tanstack/ai-solid@0.18.0
-
-## 0.7.16
-
-### Patch Changes
-
-- Updated dependencies [[`f6b9d89`](https://github.com/TanStack/ai/commit/f6b9d89e7365f0cb30a41cf3a46b7dd237fbc73c), [`ecd12a4`](https://github.com/TanStack/ai/commit/ecd12a408987bc75649c21aada6948282a2a66dd)]:
- - @tanstack/ai-client@0.24.0
- - @tanstack/ai-solid@0.17.0
-
-## 0.7.15
-
-### Patch Changes
-
-- Updated dependencies [[`7499171`](https://github.com/TanStack/ai/commit/74991716aea4d90a5d0363676a1e3349689a48e8)]:
- - @tanstack/ai-client@0.23.0
- - @tanstack/ai-solid@0.16.0
-
-## 0.7.14
-
-### Patch Changes
-
-- Updated dependencies [[`35946e3`](https://github.com/TanStack/ai/commit/35946e3c39fb123c133ebe662f8e2cf0139f2b8c)]:
- - @tanstack/ai-client@0.22.0
- - @tanstack/ai-solid@0.15.0
-
-## 0.7.13
-
-### Patch Changes
-
-- Updated dependencies [[`2665085`](https://github.com/TanStack/ai/commit/2665085970ab4d792778bb2b635ef27fbdcb6be1), [`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589), [`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589)]:
- - @tanstack/ai-client@0.21.0
- - @tanstack/ai-solid@0.14.4
-
-## 0.7.12
-
-### Patch Changes
-
-- Updated dependencies [[`5deda27`](https://github.com/TanStack/ai/commit/5deda27085c8785894a28feb5bb3655dbd8f7e0a)]:
- - @tanstack/ai-client@0.20.0
- - @tanstack/ai-solid@0.14.3
-
-## 0.7.11
-
-### Patch Changes
-
-- Updated dependencies [[`33acdd4`](https://github.com/TanStack/ai/commit/33acdd4df4aef13d594700d9b52087252091bd40), [`c22c663`](https://github.com/TanStack/ai/commit/c22c6632fdca761033cb9c4273bf61fc8ce86662), [`c1a8732`](https://github.com/TanStack/ai/commit/c1a87327b4a3463d37158f32ca90184b5fd092bb)]:
- - @tanstack/ai-client@0.19.0
- - @tanstack/ai-solid@0.14.0
-
-## 0.7.10
-
-### Patch Changes
-
-- [#844](https://github.com/TanStack/ai/pull/844) [`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff) - Republish all packages with their compiled `dist/` output.
-
- Releases `0.33.0`–`0.36.0` were published without a `dist/` directory: the
- release workflow relied on an Nx-cached `build` whose outputs were not
- materialized to disk before `changeset publish` packed the tarballs, and
- `files: ["dist"]` silently includes nothing when `dist/` is absent. The
- published packages therefore contained only `src/`, so every export
- (`./dist/esm/*.js`) resolved to a missing file and the packages were
- uninstallable.
-
- The publish step now runs a fresh, cache-bypassing build of all packages
- immediately before publishing, guaranteeing compiled artifacts are present in
- every tarball.
-
-- Updated dependencies [[`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff)]:
- - @tanstack/ai-client@0.18.6
- - @tanstack/ai-solid@0.13.15
-
-## 0.7.9
-
-### Patch Changes
-
-- Updated dependencies [[`8fa6cc5`](https://github.com/TanStack/ai/commit/8fa6cc56c5f36e22885c98a511dcceb2bfc0da1f)]:
- - @tanstack/ai-client@0.18.0
- - @tanstack/ai-solid@0.13.9
-
-## 0.7.8
-
-### Patch Changes
-
-- [#769](https://github.com/TanStack/ai/pull/769) [`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b) - Add repository metadata (`homepage`, `bugs`, `funding`), fix `repository.directory` to point at each package, and include an MIT `LICENSE` file in every published package.
-
-- Updated dependencies [[`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b)]:
- - @tanstack/ai-client@0.17.1
- - @tanstack/ai-solid@0.13.6
-
-## 0.7.7
-
-### Patch Changes
-
-- Updated dependencies [[`7d44569`](https://github.com/TanStack/ai/commit/7d445693ea079d7a85498a4465179ddd5f548cb0)]:
- - @tanstack/ai-client@0.17.0
- - @tanstack/ai-solid@0.13.5
-
-## 0.7.6
-
-### Patch Changes
-
-- Updated dependencies [[`755e995`](https://github.com/TanStack/ai/commit/755e9953a31e879c4b88df0e7672ce1224886c97)]:
- - @tanstack/ai-client@0.16.0
- - @tanstack/ai-solid@0.13.0
-
-## 0.7.5
-
-### Patch Changes
-
-- Updated dependencies [[`c1ae8b9`](https://github.com/TanStack/ai/commit/c1ae8b94c83d70508975568eb4fc9b45f1af540b), [`8036b50`](https://github.com/TanStack/ai/commit/8036b5054330a180023c6e3225b8d2735a43a919)]:
- - @tanstack/ai-client@0.15.0
- - @tanstack/ai-solid@0.12.0
-
-## 0.7.4
-
-### Patch Changes
-
-- Updated dependencies [[`d5645cf`](https://github.com/TanStack/ai/commit/d5645cfd4d1b9cfc877f7d4d714517e166a99ce3)]:
- - @tanstack/ai-client@0.14.0
- - @tanstack/ai-solid@0.11.0
-
-## 0.7.3
-
-### Patch Changes
-
-- Updated dependencies [[`5634f18`](https://github.com/TanStack/ai/commit/5634f186a4946ca3e1942fbfcbf1291ec9bd9855)]:
- - @tanstack/ai-client@0.13.0
- - @tanstack/ai-solid@0.10.10
-
-## 0.7.2
-
-### Patch Changes
-
-- Updated dependencies [[`ad23da9`](https://github.com/TanStack/ai/commit/ad23da92c279759b3778672dcee3d1616a02994b)]:
- - @tanstack/ai-client@0.12.0
- - @tanstack/ai-solid@0.10.9
-
-## 0.7.1
-
-### Patch Changes
-
-- Refresh package README content and npm metadata for better discoverability. ([#626](https://github.com/TanStack/ai/pull/626))
-
-- Updated dependencies [[`ebeb22e`](https://github.com/TanStack/ai/commit/ebeb22ec68f456b09e0181ac6f5d1ac25a0affd2)]:
- - @tanstack/ai-client@0.11.6
- - @tanstack/ai-solid@0.10.6
-
-## 0.7.0
-
-### Minor Changes
-
-- `TextPart` now accepts `remarkPlugins`, `rehypePlugins`, and (React/Solid) ([#599](https://github.com/TanStack/ai/pull/599))
- `components` props, plus a `disableDefaultPlugins` escape hatch. User plugins
- merge with the secure defaults — `rehype-sanitize` continues to run last
- unless defaults are disabled.
-
- This fixes [#164](https://github.com/TanStack/ai/issues/164): bold and
- emphasis in Japanese, Chinese, and Korean text rendered incorrectly because
- of a CommonMark spec defect. Consumers can now drop in
- [`remark-cjk-friendly`](https://www.npmjs.com/package/remark-cjk-friendly)
- with a single prop:
-
- ```tsx
- import remarkCjkFriendly from 'remark-cjk-friendly'
- ;
- ```
-
- Also fixes a latent bug in `@tanstack/ai-react-ui` where `remark-gfm` was
- passed inside the rehype plugin array, silently disabling GFM features
- (tables, strikethrough, task lists) in the React `TextPart`.
-
- `@tanstack/ai-vue-ui` omits the `components` prop because its underlying
- renderer (`@crazydos/vue-markdown`) does not expose component overrides;
- use that library's slot API for custom rendering.
-
-### Patch Changes
-
-- Updated dependencies [[`a03d12b`](https://github.com/TanStack/ai/commit/a03d12b13ade93f3e262c6ffa996696ce27472ef)]:
- - @tanstack/ai-client@0.11.4
- - @tanstack/ai-solid@0.10.4
-
-## 0.6.7
-
-### Patch Changes
-
-- Tighten TypeScript safety: enable `noImplicitOverride`, ([#579](https://github.com/TanStack/ai/pull/579))
- `noFallthroughCasesInSwitch`, and `useDefineForClassFields` in the
- root `tsconfig.json`; add a typed-ESLint block scoped to
- `packages/*/src/**` that turns on `no-floating-promises`,
- `no-misused-promises`, `await-thenable`,
- `switch-exhaustiveness-check`, `consistent-type-exports`,
- `prefer-readonly`, and `no-non-null-assertion` (errors), plus
- `no-explicit-any` (warning). `@ts-ignore` and `@ts-nocheck` are
- disallowed in library source via `@typescript-eslint/ban-ts-comment`,
- and `as unknown as ` double-casts are blocked by a
- `no-restricted-syntax` rule (escape hatches available with an inline
- reason). Two flags from the original five-flag set —
- `noPropertyAccessFromIndexSignature` and `exactOptionalPropertyTypes`
- — were tried and rolled back: they produced ~500 lines of bracket-
- access and conditional-spread churn without catching any real bugs,
- and `exactOptionalPropertyTypes` would have forced consumers using
- it themselves to deal with our internals' style preferences.
-
- User-visible API surface is unchanged; this is a hardening pass to
- keep streaming/agent-loop correctness and discriminated-union
- exhaustiveness honest going forward. See issue #564.
-
-- Updated dependencies []:
- - @tanstack/ai-client@0.11.3
- - @tanstack/ai-solid@0.10.3
-
-## 0.6.6
-
-### Patch Changes
-
-- Updated dependencies [[`2e0e2eb`](https://github.com/TanStack/ai/commit/2e0e2eb72684aac82e570d57767656e218289b49)]:
- - @tanstack/ai-client@0.11.0
- - @tanstack/ai-solid@0.10.0
-
-## 0.6.5
-
-### Patch Changes
-
-- Updated dependencies [[`a9d1916`](https://github.com/TanStack/ai/commit/a9d19165a5028515cf1d091d611c8ac4b5b86099)]:
- - @tanstack/ai-client@0.10.0
- - @tanstack/ai-solid@0.9.0
-
-## 0.6.4
-
-### Patch Changes
-
-- Updated dependencies [[`98979f7`](https://github.com/TanStack/ai/commit/98979f7e72f4b5bfb816fb14b60a12871f8c4bec)]:
- - @tanstack/ai-solid@0.8.0
- - @tanstack/ai-client@0.9.2
-
-## 0.6.3
-
-### Patch Changes
-
-- Updated dependencies [[`b2d3cc1`](https://github.com/TanStack/ai/commit/b2d3cc131a31c54bd1e5841f958fbe333514e508), [`13cceae`](https://github.com/TanStack/ai/commit/13cceaedf64e398ca15b8dbbbfe215329ea26794)]:
- - @tanstack/ai-client@0.9.0
- - @tanstack/ai-solid@0.7.1
-
-## 0.6.2
-
-### Patch Changes
-
-- Updated dependencies [[`54523f5`](https://github.com/TanStack/ai/commit/54523f5e9a9b4d4ea6c49e4551936bc2cc25593a), [`008f015`](https://github.com/TanStack/ai/commit/008f0154f852e7e6734d3e3d35cad47780b52b7a), [`af9eb7b`](https://github.com/TanStack/ai/commit/af9eb7bbb875b23b7e99b2e6b743636daad402d1)]:
- - @tanstack/ai-client@0.8.0
- - @tanstack/ai-solid@0.7.0
-
-## 0.6.1
-
-### Patch Changes
-
-- Updated dependencies [[`86be1c8`](https://github.com/TanStack/ai/commit/86be1c8262bb3176ea786aa0af115b38c3e3f51a)]:
- - @tanstack/ai-client@0.7.0
- - @tanstack/ai-solid@0.6.5
-
-## 0.6.0
-
-### Patch Changes
-
-- Updated dependencies [[`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e), [`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e)]:
- - @tanstack/ai-client@0.6.0
- - @tanstack/ai-solid@0.6.4
-
-## 0.5.0
-
-### Patch Changes
-
-- Updated dependencies [[`5aa6acc`](https://github.com/TanStack/ai/commit/5aa6acc1a4faea5346f750322e80984abf2d7059), [`1f800aa`](https://github.com/TanStack/ai/commit/1f800aacf57081f37a075bc8d08ff397cb33cbe9)]:
- - @tanstack/ai-client@0.5.0
- - @tanstack/ai-solid@0.6.0
-
-## 0.5.1
-
-### Patch Changes
-
-- Updated dependencies [[`5d98472`](https://github.com/TanStack/ai/commit/5d984722e1f84725e3cfda834fbda3d0341ecedd)]:
- - @tanstack/ai-client@0.4.4
- - @tanstack/ai-solid@0.5.1
-
-## 0.5.0
-
-### Patch Changes
-
-- Updated dependencies [[`0158d14`](https://github.com/TanStack/ai/commit/0158d14df00639ff5325680ae91b7791c189e60f)]:
- - @tanstack/ai-solid@0.5.0
- - @tanstack/ai-client@0.4.1
-
-## 0.4.0
-
-### Patch Changes
-
-- Updated dependencies [[`99ccee5`](https://github.com/TanStack/ai/commit/99ccee5c72df12adc13bede98142c6da84d13cc4), [`230bab6`](https://github.com/TanStack/ai/commit/230bab6417c8ff2c25586a12126c85e27dd7bc15)]:
- - @tanstack/ai-client@0.4.0
- - @tanstack/ai-solid@0.4.0
-
-## 0.3.0
-
-### Patch Changes
-
-- Updated dependencies [[`e52135f`](https://github.com/TanStack/ai/commit/e52135f6ec3285227679411636e208ae84a408d7)]:
- - @tanstack/ai-client@0.3.0
- - @tanstack/ai-solid@0.3.0
-
-## 0.2.1
-
-### Patch Changes
-
-- Updated dependencies [[`181e0ac`](https://github.com/TanStack/ai/commit/181e0acdfb44b27db6cf871b36593c0f867cadf9), [`181e0ac`](https://github.com/TanStack/ai/commit/181e0acdfb44b27db6cf871b36593c0f867cadf9)]:
- - @tanstack/ai-solid@0.2.1
- - @tanstack/ai-client@0.2.1
-
-## 0.2.0
-
-### Patch Changes
-
-- Updated dependencies [[`c5df33c`](https://github.com/TanStack/ai/commit/c5df33c2d3e72c3332048ffe7c64a553e5ea86fb)]:
- - @tanstack/ai-client@0.2.0
- - @tanstack/ai-solid@0.2.0
-
-## 0.1.0
-
-### Patch Changes
-
-- Updated dependencies [[`8d77614`](https://github.com/TanStack/ai/commit/8d776146f94ffd1579e1ab01b26dcb94d1bb3092)]:
- - @tanstack/ai-client@0.1.0
- - @tanstack/ai-solid@0.1.0
-
-## 0.0.3
-
-### Patch Changes
-
-- Updated dependencies []:
- - @tanstack/ai-client@0.0.3
- - @tanstack/ai-solid@0.0.3
-
-## 0.0.2
-
-### Patch Changes
-
-- Updated dependencies [[`a7bd563`](https://github.com/TanStack/ai/commit/a7bd5639eb2fbf1b4169eb307f77149f4a85a915)]:
- - @tanstack/ai-client@0.0.2
- - @tanstack/ai-solid@0.0.2
-
-## 0.0.1
-
-### Patch Changes
-
-- initial release of AI ([#76](https://github.com/TanStack/ai/pull/76))
-
-## 0.0.1
-
-### Patch Changes
-
-- Initial release of TanStack AI ([#72](https://github.com/TanStack/ai/pull/72))
-
-- Updated dependencies [[`a9b54c2`](https://github.com/TanStack/ai/commit/a9b54c21282d16036a427761e0784b159a6f2d99)]:
- - @tanstack/ai-client@0.0.1
- - @tanstack/ai-solid@0.0.1
diff --git a/packages/ai-solid-ui/README.md b/packages/ai-solid-ui/README.md
index c9bbb8c10d..ae45778b89 100644
--- a/packages/ai-solid-ui/README.md
+++ b/packages/ai-solid-ui/README.md
@@ -1,328 +1,9 @@
-
-
-
-
-
-
-
+# @tanstack/ai-solid-ui (deprecated)
-
+This package is deprecated since 0.8.0. It will be removed in 1.0.0.
-
+Import chat UI from [`@tanstack/ai-solid/ui`](https://tanstack.com/ai/latest/docs/ui/solid) instead.
-
+This package re-exports that subpath so existing installs keep working during the move.
-
-
-# TanStack AI
-
-Type-safe, provider-agnostic TypeScript SDK for building streaming chat,
-tool-calling agents, structured outputs, realtime voice, media generation, and
-framework-native AI apps.
-
-TanStack AI is built from composable activities and provider adapters. Use one
-provider or switch between many. Import only chat, or add image, audio, video,
-speech, transcription, summarization, realtime, Code Mode, devtools, and
-framework bindings as your app needs them.
-
-## Read the docs ->
-
-## Start Here
-
-- [Overview](https://tanstack.com/ai/latest/docs/getting-started/overview) -
- what TanStack AI is and how the packages fit together.
-- [Quick Start: React](https://tanstack.com/ai/latest/docs/getting-started/quick-start) -
- add streaming chat to a React app.
-- [Quick Start: Vue](https://tanstack.com/ai/latest/docs/getting-started/quick-start-vue) -
- build with Vue composables.
-- [Quick Start: Svelte](https://tanstack.com/ai/latest/docs/getting-started/quick-start-svelte) -
- build with Svelte 5 runes.
-- [Quick Start: Server Only](https://tanstack.com/ai/latest/docs/getting-started/quick-start-server) -
- use TanStack AI from a server endpoint, script, or backend service.
-- [TanStack AI vs Vercel AI SDK](https://tanstack.com/ai/latest/docs/comparison/vercel-ai-sdk) -
- compare architecture, feature coverage, and tradeoffs.
-
-## What You Can Build
-
-- Streaming chat experiences with typed messages, tool calls, reasoning parts,
- and configurable connection adapters.
-- Type-safe tools that can run on the server or client from one shared
- `toolDefinition()` contract.
-- Structured output flows backed by JSON Schema, Zod, ArkType, Valibot, or
- plain JSON Schema.
-- Multimodal prompts and responses that include text, images, audio, video, and
- documents.
-- Image, audio, video, speech, transcription, and summarization workflows using
- a shared generation client pattern.
-- Realtime voice chat with provider adapters for realtime sessions and token
- minting.
-- Code Mode agents that let an LLM write and execute TypeScript in an isolated
- sandbox to orchestrate tools with loops, branches, and parallel calls.
-- Devtools and observability pipelines for inspecting messages, tool calls,
- stream chunks, errors, usage, and OpenTelemetry traces.
-- Framework-native clients for React, Solid, Vue, Svelte, and Preact, plus a
- headless client for custom runtimes.
-
-## Install
-
-Install the core package and the provider/framework packages your app uses:
-
-```bash
-pnpm add @tanstack/ai @tanstack/ai-openai
-```
-
-For a React chat UI:
-
-```bash
-pnpm add @tanstack/ai @tanstack/ai-client @tanstack/ai-react @tanstack/ai-openai
-```
-
-OpenRouter is also a good starting point if you want access to many providers
-through one API key:
-
-```bash
-pnpm add @tanstack/ai @tanstack/ai-openrouter
-```
-
-## Streaming Chat
-
-```typescript
-import { chat, toServerSentEventsResponse } from '@tanstack/ai'
-import { openaiText } from '@tanstack/ai-openai'
-
-export async function POST(request: Request) {
- const body = await request.json()
-
- const stream = chat({
- adapter: openaiText('gpt-5.2'),
- messages: body.messages,
- })
-
- return toServerSentEventsResponse(stream)
-}
-```
-
-Learn more in the
-[Chat & Streaming docs](https://tanstack.com/ai/latest/docs/chat/streaming) and
-[Connection Adapters docs](https://tanstack.com/ai/latest/docs/chat/connection-adapters).
-
-## Type-Safe Tools
-
-Define a tool once, then attach a server or client implementation with the same
-input and output types:
-
-```typescript
-import { toolDefinition } from '@tanstack/ai'
-import { z } from 'zod'
-
-const getProducts = toolDefinition({
- name: 'getProducts',
- description: 'Search the product catalog',
- inputSchema: z.object({ query: z.string() }),
- outputSchema: z.array(
- z.object({
- id: z.string(),
- name: z.string(),
- }),
- ),
-}).server(async ({ query }) => {
- return db.products.search(query)
-})
-```
-
-Learn more in the
-[Tools docs](https://tanstack.com/ai/latest/docs/tools/tools),
-[Tool Approval Flow docs](https://tanstack.com/ai/latest/docs/tools/tool-approval),
-and
-[Lazy Tool Discovery docs](https://tanstack.com/ai/latest/docs/tools/lazy-tool-discovery).
-
-## Structured Outputs
-
-Use `outputSchema` when you need typed objects instead of freeform text:
-
-```typescript
-import { chat } from '@tanstack/ai'
-import { openaiText } from '@tanstack/ai-openai'
-import { z } from 'zod'
-
-const Person = z.object({
- name: z.string(),
- age: z.number(),
-})
-
-const person = await chat({
- adapter: openaiText('gpt-5.2'),
- messages: [{ role: 'user', content: 'Ada Lovelace, 36' }],
- outputSchema: Person,
-})
-```
-
-Learn more in the
-[Structured Outputs docs](https://tanstack.com/ai/latest/docs/structured-outputs/overview).
-
-## Media, Realtime, and Code Mode
-
-- [Generations](https://tanstack.com/ai/latest/docs/media/generations) - one
- pattern for image generation, text-to-speech, transcription, summarization,
- audio generation, and video generation.
-- [Realtime Voice Chat](https://tanstack.com/ai/latest/docs/media/realtime-chat) -
- build low-latency realtime voice experiences.
-- [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let
- models write and execute TypeScript inside a secure isolate.
-- [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) -
- give Code Mode reusable runtime capabilities.
-
-## Providers
-
-Official adapters include:
-
-| Package | Use it for |
-| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
-| [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
-| [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
-| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude |
-| [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
-| [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials |
-| [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
-| [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
-| [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
-| [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
-| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
-| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
-
-The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
-`openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so
-on.
-
-## Framework Packages
-
-| Package | What it provides |
-| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
-| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients |
-| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks |
-| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations |
-| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations |
-| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations |
-| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat |
-| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces |
-
-## Advanced Docs
-
-- [Middleware](https://tanstack.com/ai/latest/docs/advanced/middleware) - hook
- into chat configuration, chunks, tool calls, usage, errors, and structured
- outputs.
-- [OpenTelemetry](https://tanstack.com/ai/latest/docs/advanced/otel) - emit
- vendor-neutral GenAI traces and metrics.
-- [Observability](https://tanstack.com/ai/latest/docs/advanced/observability) -
- subscribe to typed TanStack AI events.
-- [Per-Model Type Safety](https://tanstack.com/ai/latest/docs/advanced/per-model-type-safety) -
- narrow model options and content modalities to the selected model.
-- [Runtime Adapter Switching](https://tanstack.com/ai/latest/docs/advanced/runtime-adapter-switching) -
- switch providers at runtime.
-- [Tree-Shaking](https://tanstack.com/ai/latest/docs/advanced/tree-shaking) -
- ship only the activities and adapters you use.
-- [Agent Skills](https://tanstack.com/ai/latest/docs/getting-started/agent-skills) -
- install TanStack AI skills into Claude Code, Cursor, GitHub Copilot, Codex,
- and other coding agents with TanStack Intent.
-
-## Get Involved
-
-- Read the [docs](https://tanstack.com/ai).
-- Participate in [GitHub discussions](https://github.com/TanStack/ai/discussions).
-- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ).
-- See [CONTRIBUTING.md](https://github.com/TanStack/ai/blob/main/CONTRIBUTING.md)
- for setup instructions.
-- [Become a sponsor](https://github.com/sponsors/tannerlinsley/).
-
-## Partners
-
-
-
-
-
-
- We're looking for TanStack AI partners to join our mission. Partner with us
- to push the boundaries of TanStack AI and build amazing things together.
-
-
LET'S CHAT
-
-
-## Explore the TanStack Ecosystem
-
-- TanStack Config -
- tooling for JS/TS packages
-- TanStack DB - reactive
- sync client store
-- TanStack Devtools -
- unified devtools panel
-- TanStack Form -
- type-safe form state
-- TanStack Pacer -
- debouncing, throttling, batching
-- TanStack Query -
- async state and caching
-- TanStack Ranger -
- range and slider primitives
-- TanStack Router -
- type-safe routing, caching, and URL state
-- TanStack Start -
- full-stack SSR and streaming
-- TanStack Store -
- reactive data store
-- TanStack Table -
- headless datagrids
-- TanStack Virtual -
- virtualized rendering
-
-...and more at TanStack.com .
+See the [migration guide](https://tanstack.com/ai/latest/docs/migration/create-ui).
diff --git a/packages/ai-solid-ui/package.json b/packages/ai-solid-ui/package.json
index 81d69c7582..b714c31a81 100644
--- a/packages/ai-solid-ui/package.json
+++ b/packages/ai-solid-ui/package.json
@@ -1,7 +1,7 @@
{
"name": "@tanstack/ai-solid-ui",
"version": "0.7.21",
- "description": "Headless Solid components for building TanStack AI chat interfaces with streamed message parts.",
+ "description": "Deprecated. Import chat UI from @tanstack/ai-solid/ui instead.",
"author": "Tanner Linsley",
"license": "MIT",
"homepage": "https://tanstack.com/ai",
@@ -17,6 +17,7 @@
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
+ "deprecated": "Use @tanstack/ai-solid/ui instead. See https://tanstack.com/ai/latest/docs/migration/create-ui",
"module": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
@@ -42,37 +43,17 @@
},
"keywords": [
"ai",
- "ai-sdk",
- "typescript",
"tanstack",
"solid",
- "solidjs",
"chat",
"ui",
- "headless",
- "components",
- "streaming",
- "generative-ai"
+ "deprecated"
],
"dependencies": {
- "rehype-highlight": "^7.0.2",
- "rehype-raw": "^7.0.0",
- "rehype-sanitize": "^6.0.0",
- "remark-gfm": "^4.0.1",
- "solid-markdown": "^2.1.0"
- },
- "peerDependencies": {
- "@tanstack/ai-client": "workspace:^",
- "@tanstack/ai-solid": "workspace:^",
- "solid-js": ">=1.9.7"
+ "@tanstack/ai-solid": "workspace:^"
},
"devDependencies": {
- "@tanstack/ai-client": "workspace:*",
- "@tanstack/ai-solid": "workspace:*",
- "@vitest/coverage-v8": "4.1.10",
- "happy-dom": "^20.11.2",
"solid-js": "^1.9.10",
- "vite": "^8.2.1",
- "vite-plugin-solid": "^2.11.14"
+ "vite": "^8.2.1"
}
}
diff --git a/packages/ai-solid-ui/src/create-ui.tsx b/packages/ai-solid-ui/src/create-ui.tsx
deleted file mode 100644
index 9bec0dec8a..0000000000
--- a/packages/ai-solid-ui/src/create-ui.tsx
+++ /dev/null
@@ -1,432 +0,0 @@
-import { For, createContext, useContext } from 'solid-js'
-import type { Accessor, Component, JSX } from 'solid-js'
-import {
- automaticPartsForMessage,
- collectInlineToolNames,
- resolveInterruptComponent,
- selectChatUI,
- selectMessageUI,
-} from '@tanstack/ai-client/ui'
-import type {
- ChatUIData,
- ChatUIHasNamedInterrupts,
- ChatUIHasNamedTools,
- ChatUIInterrupt,
- ChatUIInterruptName,
- ChatUIInterruptOf,
- ChatUIInterruptsOf,
- ChatUINamedInterruptId,
- ChatUIPartKey,
- ChatUIPartOf,
- ChatUISchemaOf,
- ChatUISelectedPart,
- ChatUIToolApproval,
- ChatUIToolName,
- ChatUIToolsOf,
-} from '@tanstack/ai-client/ui'
-import type {
- MessagePart,
- ToolCallPart,
- ToolResultPart,
- UIMessage,
-} from '@tanstack/ai-client'
-import type { UseChatReturn } from '@tanstack/ai-solid'
-
-export type ChatUIHost = UseChatReturn<
- ChatUIToolsOf,
- ChatUISchemaOf,
- ChatUIInterruptsOf
->
-
-export type LayoutProps = {
- renderMessages: () => JSX.Element
- renderInterrupts: () => JSX.Element
- renderInput: () => JSX.Element
- readonly __ui?: TOptions
-}
-
-export type MessageProps = {
- message: UIMessage, ChatUIData>
- renderParts: () => JSX.Element
-}
-
-export type InputProps = {
- readonly __ui?: TOptions
-}
-
-export type PartProps = {
- part: ChatUIPartOf
-}
-
-export type ToolProps<
- TOptions,
- TName extends ChatUIToolName = ChatUIToolName,
-> = {
- part: Extract>, { name: TName }>
- result?: ToolResultPart
- interrupt?: ChatUIToolApproval
-}
-
-export type InterruptProps<
- TOptions,
- TName extends ChatUIInterruptName = never,
-> = {
- interrupt: ChatUIInterruptOf
- readonly __ui?: TOptions
-}
-
-type GenericInterruptComponents =
- ChatUIHasNamedInterrupts extends true
- ? {
- [K in ChatUINamedInterruptId]: Component<
- InterruptProps>
- >
- } & {
- fallback?: Component>
- }
- : {
- fallback?: Component>
- }
-
-type ToolApprovalMap = {
- [K in ChatUIToolName]?: Component<
- InterruptProps>
- >
-}
-
-export type ChatUIComponents = {
- layout: Component>
- message: Component>
- input?: Component>
- parts: {
- [K in ChatUIPartKey]?: Component>
- } & {
- fallback?: Component>
- }
-} & (ChatUIHasNamedTools extends true
- ? {
- tools: {
- [K in ChatUIToolName]: Component>
- }
- }
- : {
- tools?: {
- [K in ChatUIToolName]?: Component>
- }
- }) &
- (ChatUIHasNamedInterrupts extends true
- ? {
- interrupts: {
- tools?: ToolApprovalMap
- generic: GenericInterruptComponents
- }
- }
- : {
- interrupts?: {
- tools?: ToolApprovalMap
- generic?: GenericInterruptComponents
- }
- })
-
-type ComponentsValue = {
- components: ChatUIComponents
- warn: (key: string, message: string) => void
- inlineToolNames: ReadonlyArray
-}
-
-function createWarnOnce() {
- const seen = new Set()
- return (key: string, message: string) => {
- if (process.env.NODE_ENV === 'production') return
- if (seen.has(key)) return
- seen.add(key)
- console.warn(message)
- }
-}
-
-function readMessages(
- chat: ChatUIHost,
-): ReadonlyArray {
- return chat.messages()
-}
-
-function readInterrupts(
- chat: ChatUIHost,
-): ReadonlyArray {
- return chat.interrupts()
-}
-
-function messagesAccessor(
- chat: ChatUIHost,
-): Accessor> {
- return chat.messages
-}
-
-export function createChatUI(options: TOptions) {
- void options
- const warn = createWarnOnce()
- const ChatContext = createContext | null>(null)
- const ComponentsContext = createContext | null>(
- null,
- )
-
- function useChat() {
- const chat = useContext(ChatContext)
- if (!chat) {
- throw new Error(
- 'Chat UI components must be wrapped in UI.Provider or UI.Chat.',
- )
- }
- return chat
- }
-
- function useComponents() {
- const value = useContext(ComponentsContext)
- if (!value) {
- throw new Error(
- 'Chat UI components must be wrapped in UI.Provider or UI.Chat.',
- )
- }
- return value
- }
-
- function defineComponents(components: ChatUIComponents) {
- return components
- }
-
- function inlineNames(components: ChatUIComponents