- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Vue Chat UI
Install @tanstack/ai-vue. Import the UI factory from @tanstack/ai-vue/ui. Call createChatHook({ options, ...components }) once. Your app calls useAppChat() to create the instance. Pass the descriptor as ui into UIChat, UIProvider, and the other static primitives.
Deprecated. Do not install @tanstack/ai-vue-ui. That package re-exports this subpath until 1.0.0. See Chat UI packages.
The factory needs a toolsComponents entry for every tool name in chatOptions. It also needs an interruptsComponents.generic entry for every interrupt id. generic.fallback is optional. Widgets go in components, partsComponents, toolsComponents, and interruptsComponents, the same way Form and Table register components.
The server route matches the React page. Use gpt-5.6 on the OpenAI text adapter.
The chat UI recipes show the same option groups one at a time. The code there is React, and the shape carries over.
Client#
import { defineComponent, h } from 'vue'
import { fetchServerSentEvents } from '@tanstack/ai-vue'
import { createChatHook, UIChat } 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],
}
const { useAppChat, ui } = createChatHook({
options: chatOptions,
components: {
layout: defineComponent((_, { slots }) => () =>
h('div', [slots.messages?.(), slots.interrupts?.(), slots.queue?.(), slots.input?.()]),
),
message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())),
},
partsComponents: {
fallback: defineComponent({
props: ['part'],
setup(props) {
return () => h('span', props.part.type)
},
}),
},
toolsComponents: {
getWeather: defineComponent({
props: ['part'],
setup(props) {
return () => h('strong', props.part.input?.city)
},
}),
},
})
export default defineComponent({
setup() {
const chat = useAppChat({ threadId: 'support-1' })
return () => h(UIChat, { ui, chat })
},
})Layout uses slots messages, interrupts, queue, 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 createChatHook.
import { defineComponent, h } from 'vue'
import { fetchServerSentEvents } from '@tanstack/ai-vue'
import { createChatHook, 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<typeof chatOptions, 'getWeather'>) => {
return () => h('strong', props.part.input?.city)
},
)
export const { useAppChat, ui } = createChatHook({
options: chatOptions,
components: {
layout: defineComponent((_, { slots }) => () =>
h('div', [slots.messages?.(), slots.interrupts?.(), slots.input?.()]),
),
message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())),
},
partsComponents: { fallback: defineComponent(() => () => null) },
toolsComponents: { getWeather: WeatherTool },
})Part components use PartProps<typeof chatOptions, 'text'>. Then part is already a text part.
Interrupt components use InterruptProps<typeof chatOptions, 'choosePlan'>. Then interrupt.payload matches the definition.
Mapped components do not receive chat as a prop. Call ui.useChatContext() when a component needs live chat. That call opts the component into chat updates. Nested children can call it too.
Read chat from ui.useChatContext()#
Call ui.useChatContext() inside a child of UIChat or UIProvider.
import { defineComponent, h } from 'vue'
import type { Component } from 'vue'
import { fetchServerSentEvents } from '@tanstack/ai-vue'
import { createChatHook, UIChat } from '@tanstack/ai-vue/ui'
const chatOptions = {
connection: fetchServerSentEvents('/api/chat'),
}
const { useAppChat, ui } = createChatHook({
options: chatOptions,
components: {
layout: defineComponent((_, { slots }) => () =>
h('main', [h(StatusLine), slots.messages?.()]),
),
message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())),
},
partsComponents: { fallback: defineComponent(() => () => null) },
})
// Declared after `ui`, and annotated, so that reading `ui.useChatContext()`
// here does not form an inference cycle with the components map that renders
// it. `h(StatusLine)` above only runs at render time, so the order is fine.
const StatusLine: Component = defineComponent({
setup() {
const chat = ui.useChatContext()
return () => {
const messages = Array.isArray(chat.messages) ? chat.messages : []
return h('p', String(messages.length) + ' messages')
}
},
})
export default defineComponent({
setup() {
const chat = useAppChat()
return () => h(UIChat, { ui, chat })
},
})useChat(chatOptions) from @tanstack/ai-vue owns the state. ui.useChatContext() reads the instance you passed into UIChat.
Interrupts#
Tool approvals sit in the tool when you read the interrupt prop. Put a component on interruptsComponents.tools to send that approval to the list instead. Generic interrupts always sit in the list under interruptsComponents.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.
Render markdown#
Model replies arrive as markdown. TextPart from @tanstack/ai-vue/ui renders it with TanStack Markdown. By default, raw HTML is escaped and executable URLs are removed.
import { defineComponent, h } from 'vue'
import { TextPart } from '@tanstack/ai-vue/ui'
export default defineComponent({
props: { content: { type: String, required: true } },
setup(props) {
return () => h(TextPart, { content: props.content })
},
})Fenced code stays plain until you pass a highlighter. Build one with TanStack Highlight: Highlight markdown code. A custom highlighter returns trusted HTML. Use createTanStackMarkdownHighlighter, or another highlighter that escapes source text.
- highlighter: a synchronous CodeHighlighter.
- extensions: extra TanStack Markdown extensions. The streaming extension is always on.
If you still pass remarkPlugins, rehypePlugins, or disableDefaultPlugins, see TextPart markdown.