Install @tanstack/ai-octane, then call useChat the same way you would in React. The hook modules are .tsrx and compile in your Octane plugin.
npm install @tanstack/ai-octane octaneoctane is a required peer. This package publishes uncompiled source, like Svelte packages that ship .svelte.
Manages chat state in an Octane component.
import { useState } from 'octane'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'
import {
createChatClientOptions,
type InferChatMessages,
} from '@tanstack/ai-client'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
const updateUIDef = toolDefinition({
name: 'updateUI',
description: 'Show a notification in the UI',
inputSchema: z.object({ message: z.string() }),
})
export function ChatComponent() {
const [notification, setNotification] = useState<string | null>(null)
const updateUI = updateUIDef.client((input) => {
setNotification(input.message)
return { success: true }
})
const tools = [updateUI]
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents('/api/chat'),
tools,
})
type ChatMessages = InferChatMessages<typeof chatOptions>
const { messages, sendMessage, isLoading, error, addToolApprovalResponse } =
useChat(chatOptions)
return (
<div>
{notification}
{isLoading ? 'Loading' : null}
{error ? error.message : null}
<button onClick={() => void sendMessage('hi')} type="button">
Send
</button>
<button
onClick={() =>
void addToolApprovalResponse({ id: 'approval-1', approved: true })
}
type="button"
>
Approve
</button>
{messages.length}
</div>
)
}The matching server route still runs chat({ adapter, messages }) and returns SSE. See Quick Start: Octane.
Extends ChatClientOptions from @tanstack/ai-client. Pass connection or fetcher, not both.
connection or fetcher - how the hook talks to your server
tools? - client tool implementations from .client()
threadId? - the only identity for this chat. Required when persistence is on
initialMessages? - starting transcript
forwardedProps? - JSON sent to the server on the AG-UI forwardedProps field
live? - subscribe on mount, unsubscribe on unmount
queue? - what to do when sendMessage runs while a turn is in flight. Default queues
interrupts? - typed interrupt definitions
context? - client-only runtime context for client tools. Not sent to the server
onResponse? / onChunk? / onFinish? / onError? / onInterruptStateChange?
devtools? - display options. The hook always tags framework: 'octane'
body? - deprecated. Use forwardedProps
Client tools run automatically. There is no onToolCall callback.
Changing connection or fetcher updates the live ChatClient. Changing threadId creates a new client.
import type { UIMessage } from '@tanstack/ai-octane'
import type { ModelMessage } from '@tanstack/ai/client'
import type {
MultimodalContent,
ChatClientState,
ConnectionStatus,
QueuedMessage,
SendMessageOptions,
} from '@tanstack/ai-client'
interface UseChatReturn {
messages: Array<UIMessage>
sendMessage: (
content: string | MultimodalContent,
options?: SendMessageOptions,
) => Promise<void>
append: (message: ModelMessage | UIMessage) => Promise<void>
addToolResult: (result: {
toolCallId: string
tool: string
output: unknown
state?: 'output-available' | 'output-error'
errorText?: string
}) => Promise<void>
addToolApprovalResponse: (response: {
id: string
approved: boolean
}) => Promise<void>
reload: () => Promise<void>
stop: () => void
isLoading: boolean
error: Error | undefined
status: ChatClientState
isSubscribed: boolean
connectionStatus: ConnectionStatus
sessionGenerating: boolean
setMessages: (messages: Array<UIMessage>) => void
clear: () => void
queue: Array<QueuedMessage>
cancelQueued: (id: string) => void
runId: string | null
}queue holds sends that wait while a run is busy. runId is the in-flight turn, or null. Interrupt helpers (interrupts, resolveInterrupts, cancelInterrupts, retryInterrupts) are on the same object when you pass interrupts.
Re-exported from @tanstack/ai-client:
import {
fetchServerSentEvents,
fetchHttpStream,
stream,
type ConnectionAdapter,
} from '@tanstack/ai-octane'import { useState } from 'octane'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'
export function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
return (
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts
.filter((part) => part.type === 'text')
.map((part) => part.content)
.join('')}
</div>
))}
<input
value={input}
disabled={isLoading}
onInput={(event) => setInput(event.currentTarget.value)}
/>
<button
disabled={isLoading}
onClick={() => {
void sendMessage(input)
setInput('')
}}
type="button"
>
Send
</button>
</div>
)
}import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'
export function ChatWithApproval() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
return (
<div>
<button onClick={() => void sendMessage('run the tool')} type="button">
Send
</button>
{messages.map((message) =>
message.parts.map((part) => {
if (
part.type !== 'tool-call' ||
part.state !== 'approval-requested' ||
!part.approval
) {
return null
}
const approvalId = part.approval.id
return (
<div key={part.id}>
<p>Approve: {part.name}</p>
<button
onClick={() =>
void addToolApprovalResponse({
id: approvalId,
approved: true,
})
}
type="button"
>
Approve
</button>
<button
onClick={() =>
void addToolApprovalResponse({
id: approvalId,
approved: false,
})
}
type="button"
>
Deny
</button>
</div>
)
}),
)}
</div>
)
}The package also exports useRealtimeChat, useMcpAppBridge, useGeneration, useGenerateImage, useGenerateAudio, useGenerateSpeech, useGenerateVideo, useTranscription, useSummarize, and useAudioRecorder.
The ./mcp-apps React AppRenderer subpath is not in this package. useMcpAppBridge is.
Re-exported from @tanstack/ai-client:
UIMessage<TTools>
ChatClientOptions<TTools, TContext>
InferChatMessages<T>
QueuedMessage, SendMessageOptions, WhenBusy