TanStack
API

@tanstack/ai-octane

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.

Installation

shell
npm install @tanstack/ai-octane octane

octane is a required peer. This package publishes uncompiled source, like Svelte packages that ship .svelte.

useChat(options)

Manages chat state in an Octane component.

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

Options you pass first

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

Options you add later

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

Returns

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

Connection adapters

Re-exported from @tanstack/ai-client:

ts
import {
  fetchServerSentEvents,
  fetchHttpStream,
  stream,
  type ConnectionAdapter,
} from '@tanstack/ai-octane'

Example: basic chat

tsx
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>
  )
}

Example: tool approval

tsx
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>
  )
}

Other hooks

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.

Types

Re-exported from @tanstack/ai-client:

  • UIMessage<TTools>
  • ChatClientOptions<TTools, TContext>
  • InferChatMessages<T>
  • QueuedMessage, SendMessageOptions, WhenBusy

Next