@tanstack/ai-react 0.16.4 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ export { useChat } from './use-chat.js';
2
2
  export { useRealtimeChat } from './use-realtime-chat.js';
3
3
  export { useMcpAppBridge } from './use-mcp-app-bridge.js';
4
4
  export type { UseMcpAppBridgeOptions } from './use-mcp-app-bridge.js';
5
- export type { DeepPartial, UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, } from './types.js';
5
+ export type { DeepPartial, UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, QueuedMessage, SendMessageOptions, WhenBusy, QueueConfig, QueueStrategy, QueueOption, } from './types.js';
6
6
  export type { UseRealtimeChatOptions, UseRealtimeChatReturn, } from './realtime-types.js';
7
7
  export { useGeneration } from './use-generation.js';
8
8
  export type { UseGenerationOptions, UseGenerationReturn, } from './use-generation.js';
@@ -1,4 +1,4 @@
1
- import { AnyClientTool, RealtimeMessage, RealtimeMode, RealtimeStatus, RealtimeToken } from '@tanstack/ai';
1
+ import { AnyClientTool, RealtimeMessage, RealtimeMode, RealtimeSessionConfig, RealtimeStatus, RealtimeToken, UsageInfo } from '@tanstack/ai';
2
2
  import { RealtimeAdapter } from '@tanstack/ai-client';
3
3
  /**
4
4
  * Options for the useRealtimeChat hook.
@@ -59,6 +59,9 @@ export interface UseRealtimeChatOptions {
59
59
  onMessage?: (message: RealtimeMessage) => void;
60
60
  onModeChange?: (mode: RealtimeMode) => void;
61
61
  onInterrupted?: () => void;
62
+ onUsage?: (usage: UsageInfo) => void;
63
+ onGoAway?: (timeLeft?: string) => void;
64
+ onStatusChange?: (status: RealtimeStatus) => void;
62
65
  }
63
66
  /**
64
67
  * Return type for the useRealtimeChat hook.
@@ -102,8 +105,5 @@ export interface UseRealtimeChatReturn {
102
105
  getInputTimeDomainData: () => Uint8Array;
103
106
  /** Get time domain data for output waveform */
104
107
  getOutputTimeDomainData: () => Uint8Array;
105
- /** Current VAD mode */
106
- vadMode: 'server' | 'semantic' | 'manual';
107
- /** Change VAD mode at runtime */
108
- setVADMode: (mode: 'server' | 'semantic' | 'manual') => void;
108
+ updateSession: (config: RealtimeSessionConfig) => void;
109
109
  }
@@ -1,6 +1,6 @@
1
1
  import { AnyClientTool, InferSchemaType, ModelMessage, SchemaInput } from '@tanstack/ai/client';
2
- import { AIDevtoolsDisplayOptions, ChatClientOptions, ChatClientState, ChatRequestBody, ClientContextOptionFromTools, ConnectionStatus, DistributedOmit, InferredClientContext, MultimodalContent, UIMessage } from '@tanstack/ai-client';
3
- export type { ChatRequestBody, MultimodalContent, UIMessage };
2
+ import { AIDevtoolsDisplayOptions, ChatClientOptions, ChatClientState, ChatRequestBody, ClientContextOptionFromTools, ConnectionStatus, DistributedOmit, InferredClientContext, MultimodalContent, QueueConfig, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from '@tanstack/ai-client';
3
+ export type { ChatRequestBody, MultimodalContent, QueueConfig, QueuedMessage, QueueOption, QueueStrategy, SendMessageOptions, UIMessage, WhenBusy, };
4
4
  /**
5
5
  * Recursive partial — every property and every nested array element is optional.
6
6
  * Used to type the in-flight `partial` value the hook exposes while a structured
@@ -35,7 +35,7 @@ export type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartia
35
35
  * Note: Connection and body changes will recreate the ChatClient instance.
36
36
  * To update these options, remount the component or use a key prop.
37
37
  */
38
- export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>> = DistributedOmit<ChatClientOptions<TTools, TContext>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'context' | 'devtools'> & {
38
+ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>> = DistributedOmit<ChatClientOptions<TTools, TContext>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'onQueueChange' | 'context' | 'devtools'> & {
39
39
  /** Display options for TanStack AI Devtools. */
40
40
  devtools?: AIDevtoolsDisplayOptions;
41
41
  /**
@@ -81,8 +81,20 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, T
81
81
  /**
82
82
  * Send a message and get a response.
83
83
  * Can be a simple string or multimodal content with images, audio, etc.
84
+ * By default, sends while busy are queued until the run settles successfully
85
+ * (`queue: 'drop'` restores the old drop-while-busy behavior).
86
+ * Pass `{ whenBusy }` to override the policy for a single send.
84
87
  */
85
- sendMessage: (content: string | MultimodalContent) => Promise<void>;
88
+ sendMessage: (content: string | MultimodalContent, options?: SendMessageOptions) => Promise<void>;
89
+ /**
90
+ * Pending messages queued while the client is busy (streaming, claiming a
91
+ * send, or draining). Separate from `messages` until they drain.
92
+ */
93
+ queue: Array<QueuedMessage>;
94
+ /**
95
+ * Cancel a queued message before it drains. No-op if already sent.
96
+ */
97
+ cancelQueued: (id: string) => void;
86
98
  /**
87
99
  * Append a message to the conversation
88
100
  */
@@ -1,4 +1,4 @@
1
1
  import { AnyClientTool, SchemaInput } from '@tanstack/ai/client';
2
2
  import { InferredClientContext } from '@tanstack/ai-client';
3
3
  import { UseChatOptions, UseChatReturn } from './types.js';
4
- export declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>>(options: UseChatOptions<TTools, TSchema, TContext>): UseChatReturn<TTools, TSchema>;
4
+ export declare function useChat<const TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>>(options: UseChatOptions<TTools, TSchema, TContext>): UseChatReturn<TTools, TSchema>;
@@ -13,6 +13,7 @@ function useChat(options) {
13
13
  const [isSubscribed, setIsSubscribed] = useState(false);
14
14
  const [connectionStatus, setConnectionStatus] = useState("disconnected");
15
15
  const [sessionGenerating, setSessionGenerating] = useState(false);
16
+ const [queue, setQueue] = useState([]);
16
17
  const messagesRef = useRef(
17
18
  options.initialMessages || []
18
19
  );
@@ -106,6 +107,13 @@ function useChat(options) {
106
107
  onSessionGeneratingChange: (isGenerating) => {
107
108
  if (activeClientRef.current !== instance) return;
108
109
  setSessionGenerating(isGenerating);
110
+ },
111
+ ...optionsRef.current.queue !== void 0 && {
112
+ queue: optionsRef.current.queue
113
+ },
114
+ onQueueChange: (nextQueue) => {
115
+ if (activeClientRef.current !== instance) return;
116
+ setQueue(nextQueue);
109
117
  }
110
118
  });
111
119
  activeClientRef.current = instance;
@@ -133,6 +141,11 @@ function useChat(options) {
133
141
  useEffect(() => {
134
142
  client.updateOptions({ context: options.context });
135
143
  }, [client, options.context]);
144
+ useEffect(() => {
145
+ if (options.queue !== void 0) {
146
+ client.updateOptions({ queue: options.queue });
147
+ }
148
+ }, [client, options.queue]);
136
149
  useEffect(() => {
137
150
  if (options.live) {
138
151
  client.subscribe();
@@ -163,8 +176,14 @@ function useChat(options) {
163
176
  };
164
177
  }, [client]);
165
178
  const sendMessage = useCallback(
166
- async (content) => {
167
- await client.sendMessage(content);
179
+ async (content, sendOptions) => {
180
+ await client.sendMessage(content, void 0, sendOptions);
181
+ },
182
+ [client]
183
+ );
184
+ const cancelQueued = useCallback(
185
+ (id) => {
186
+ client.cancelQueued(id);
168
187
  },
169
188
  [client]
170
189
  );
@@ -248,6 +267,8 @@ function useChat(options) {
248
267
  clear,
249
268
  addToolResult,
250
269
  addToolApprovalResponse,
270
+ queue,
271
+ cancelQueued,
251
272
  partial,
252
273
  final
253
274
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\n\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n>(\n options: UseChatOptions<TTools, TSchema, TContext>,\n): UseChatReturn<TTools, TSchema> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\n\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n const activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n // Update ref synchronously during render so it's always current when useMemo runs.\n messagesRef.current = messages\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools, TSchema, TContext>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n const client = useMemo(() => {\n const messagesToUse = options.initialMessages || []\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n const instance = new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.threadId !== undefined && {\n threadId: initialOptions.threadId,\n }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.persistence !== undefined && {\n persistence: initialOptions.persistence,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'react',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => {\n if (activeClientRef.current !== instance) return\n void optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message: UIMessage<TTools>) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onError?.(error)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n onCustomEvent: (eventType, data, context) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n if (activeClientRef.current !== instance) return\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (activeClientRef.current !== instance) return\n setError(newError)\n },\n onStatusChange: (status: ChatClientState) => {\n if (activeClientRef.current !== instance) return\n setStatus(status)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n if (activeClientRef.current !== instance) return\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n if (activeClientRef.current !== instance) return\n setSessionGenerating(isGenerating)\n },\n })\n activeClientRef.current = instance\n return instance\n }, [clientId])\n\n useEffect(() => {\n const clientMessages = client.getMessages()\n if (clientMessages !== messagesRef.current) {\n setMessages(clientMessages)\n }\n }, [client])\n\n // Sync each wire-payload slot in its own effect so an unrelated option\n // changing doesn't re-run the others. `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT, so guard the optional\n // slots before passing them.\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\n\n useEffect(() => {\n if (options.forwardedProps !== undefined) {\n client.updateOptions({ forwardedProps: options.forwardedProps })\n }\n }, [client, options.forwardedProps])\n\n useEffect(() => {\n if (options.tools !== undefined) {\n client.updateOptions({ tools: options.tools })\n }\n }, [client, options.tools])\n\n useEffect(() => {\n client.updateOptions({ context: options.context })\n }, [client, options.context])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n useEffect(() => {\n if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n activeClientRef.current = client\n client.mountDevtools()\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n const sendMessage = useCallback(\n async (content: string | MultimodalContent) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client.addToolResult(result)\n },\n [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\n )\n\n // The \"active\" structured-output part is the one on the assistant message\n // that follows the latest user message. No such message exists between\n // sendMessage() and the first chunk, so partial/final naturally read as\n // cleared. Historical parts on earlier assistant messages remain available\n // via `messages` directly.\n //\n // When there is NO user message yet (e.g. `initialMessages` contains only\n // a stale assistant turn or a system prompt) we deliberately return null\n // rather than scanning historical assistants — otherwise a `final` from a\n // previous session would leak into the hook value on first render.\n const renderedMessages = client.getMessages()\n\n const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {\n let lastUserIndex = -1\n for (let i = renderedMessages.length - 1; i >= 0; i--) {\n if (renderedMessages[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {\n const m = renderedMessages[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n }, [renderedMessages])\n\n const partial = useMemo<Partial>(() => {\n if (!activeStructuredPart) return {} as Partial\n const v = activeStructuredPart.partial ?? activeStructuredPart.data\n return (v ?? {}) as Partial\n }, [activeStructuredPart])\n\n const final = useMemo<Final | null>(() => {\n if (!activeStructuredPart || activeStructuredPart.status !== 'complete') {\n return null\n }\n return activeStructuredPart.data as Final\n }, [activeStructuredPart])\n\n // The runtime shape unconditionally exposes partial/final; the public\n // return type hides them when no outputSchema was supplied. TS can't\n // structurally narrow across that conditional, so the `as` is the seam.\n // eslint-disable-next-line no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow\n return {\n messages: renderedMessages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"names":["error","status"],"mappings":"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"}
1
+ {"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\n\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n const TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n>(\n options: UseChatOptions<TTools, TSchema, TContext>,\n): UseChatReturn<TTools, TSchema> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\n const [queue, setQueue] = useState<Array<QueuedMessage>>([])\n\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n const activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n // Update ref synchronously during render so it's always current when useMemo runs.\n messagesRef.current = messages\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools, TSchema, TContext>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n const client = useMemo(() => {\n const messagesToUse = options.initialMessages || []\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n const instance = new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.threadId !== undefined && {\n threadId: initialOptions.threadId,\n }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.persistence !== undefined && {\n persistence: initialOptions.persistence,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'react',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => {\n if (activeClientRef.current !== instance) return\n void optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message: UIMessage<TTools>) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onError?.(error)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n onCustomEvent: (eventType, data, context) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n if (activeClientRef.current !== instance) return\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (activeClientRef.current !== instance) return\n setError(newError)\n },\n onStatusChange: (status: ChatClientState) => {\n if (activeClientRef.current !== instance) return\n setStatus(status)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n if (activeClientRef.current !== instance) return\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n if (activeClientRef.current !== instance) return\n setSessionGenerating(isGenerating)\n },\n ...(optionsRef.current.queue !== undefined && {\n queue: optionsRef.current.queue,\n }),\n onQueueChange: (nextQueue: Array<QueuedMessage>) => {\n if (activeClientRef.current !== instance) return\n setQueue(nextQueue)\n },\n })\n activeClientRef.current = instance\n return instance\n }, [clientId])\n\n useEffect(() => {\n const clientMessages = client.getMessages()\n if (clientMessages !== messagesRef.current) {\n setMessages(clientMessages)\n }\n }, [client])\n\n // Sync each wire-payload slot in its own effect so an unrelated option\n // changing doesn't re-run the others. `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT, so guard the optional\n // slots before passing them.\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\n\n useEffect(() => {\n if (options.forwardedProps !== undefined) {\n client.updateOptions({ forwardedProps: options.forwardedProps })\n }\n }, [client, options.forwardedProps])\n\n useEffect(() => {\n if (options.tools !== undefined) {\n client.updateOptions({ tools: options.tools })\n }\n }, [client, options.tools])\n\n useEffect(() => {\n client.updateOptions({ context: options.context })\n }, [client, options.context])\n\n useEffect(() => {\n if (options.queue !== undefined) {\n client.updateOptions({ queue: options.queue })\n }\n }, [client, options.queue])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n useEffect(() => {\n if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n activeClientRef.current = client\n client.mountDevtools()\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n const sendMessage = useCallback(\n async (\n content: string | MultimodalContent,\n sendOptions?: SendMessageOptions,\n ) => {\n await client.sendMessage(content, undefined, sendOptions)\n },\n [client],\n )\n\n const cancelQueued = useCallback(\n (id: string) => {\n client.cancelQueued(id)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client.addToolResult(result)\n },\n [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\n )\n\n // The \"active\" structured-output part is the one on the assistant message\n // that follows the latest user message. No such message exists between\n // sendMessage() and the first chunk, so partial/final naturally read as\n // cleared. Historical parts on earlier assistant messages remain available\n // via `messages` directly.\n //\n // When there is NO user message yet (e.g. `initialMessages` contains only\n // a stale assistant turn or a system prompt) we deliberately return null\n // rather than scanning historical assistants — otherwise a `final` from a\n // previous session would leak into the hook value on first render.\n const renderedMessages = client.getMessages()\n\n const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {\n let lastUserIndex = -1\n for (let i = renderedMessages.length - 1; i >= 0; i--) {\n if (renderedMessages[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {\n const m = renderedMessages[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n }, [renderedMessages])\n\n const partial = useMemo<Partial>(() => {\n if (!activeStructuredPart) return {} as Partial\n const v = activeStructuredPart.partial ?? activeStructuredPart.data\n return (v ?? {}) as Partial\n }, [activeStructuredPart])\n\n const final = useMemo<Final | null>(() => {\n if (!activeStructuredPart || activeStructuredPart.status !== 'complete') {\n return null\n }\n return activeStructuredPart.data as Final\n }, [activeStructuredPart])\n\n // The runtime shape unconditionally exposes partial/final; the public\n // return type hides them when no outputSchema was supplied. TS can't\n // structurally narrow across that conditional, so the `as` is the seam.\n // eslint-disable-next-line no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow\n return {\n messages: renderedMessages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n queue,\n cancelQueued,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"names":["error","status"],"mappings":"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"}
@@ -11,9 +11,6 @@ function useRealtimeChat(options) {
11
11
  const [error, setError] = useState(null);
12
12
  const [inputLevel, setInputLevel] = useState(0);
13
13
  const [outputLevel, setOutputLevel] = useState(0);
14
- const [vadMode, setVADModeState] = useState(
15
- options.vadMode ?? "server"
16
- );
17
14
  const clientRef = useRef(null);
18
15
  const optionsRef = useRef(options);
19
16
  optionsRef.current = options;
@@ -58,6 +55,12 @@ function useRealtimeChat(options) {
58
55
  setMessages((prev) => [...prev, message]);
59
56
  optionsRef.current.onMessage?.(message);
60
57
  },
58
+ onUsage(usage) {
59
+ optionsRef.current.onUsage?.(usage);
60
+ },
61
+ onGoAway(timeLeft) {
62
+ optionsRef.current.onGoAway?.(timeLeft);
63
+ },
61
64
  onError: (err) => {
62
65
  setError(err);
63
66
  optionsRef.current.onError?.(err);
@@ -146,12 +149,9 @@ function useRealtimeChat(options) {
146
149
  const getOutputTimeDomainData = useCallback(() => {
147
150
  return clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData;
148
151
  }, []);
149
- const setVADMode = useCallback(
150
- (newMode) => {
151
- setVADModeState(newMode);
152
- },
153
- []
154
- );
152
+ const updateSession = useCallback((config) => {
153
+ clientRef.current?.updateSession(config);
154
+ }, []);
155
155
  return {
156
156
  // Connection state
157
157
  status,
@@ -178,9 +178,8 @@ function useRealtimeChat(options) {
178
178
  getOutputFrequencyData,
179
179
  getInputTimeDomainData,
180
180
  getOutputTimeDomainData,
181
- // VAD control
182
- vadMode,
183
- setVADMode
181
+ // Session control
182
+ updateSession
184
183
  };
185
184
  }
186
185
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"use-realtime-chat.js","sources":["../../src/use-realtime-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { RealtimeClient } from '@tanstack/ai-client'\nimport type {\n RealtimeMessage,\n RealtimeMode,\n RealtimeStatus,\n} from '@tanstack/ai'\nimport type {\n UseRealtimeChatOptions,\n UseRealtimeChatReturn,\n} from './realtime-types'\n\n// Empty frequency data for when client is not connected\nconst emptyFrequencyData = new Uint8Array(128)\nconst emptyTimeDomainData = new Uint8Array(128).fill(128)\n\n/**\n * React hook for realtime voice conversations.\n *\n * Provides a simple interface for voice-to-voice AI interactions\n * with support for multiple providers (OpenAI, ElevenLabs, etc.).\n *\n * @param options - Configuration options including adapter and callbacks\n * @returns Hook return value with state and control methods\n *\n * @example\n * ```typescript\n * import { useRealtimeChat } from '@tanstack/ai-react'\n * import { openaiRealtime } from '@tanstack/ai-openai'\n *\n * function VoiceChat() {\n * const {\n * status,\n * mode,\n * messages,\n * connect,\n * disconnect,\n * inputLevel,\n * outputLevel,\n * } = useRealtimeChat({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: openaiRealtime(),\n * })\n *\n * return (\n * <div>\n * <p>Status: {status}</p>\n * <p>Mode: {mode}</p>\n * <button onClick={status === 'idle' ? connect : disconnect}>\n * {status === 'idle' ? 'Start' : 'Stop'}\n * </button>\n * </div>\n * )\n * }\n * ```\n */\nexport function useRealtimeChat(\n options: UseRealtimeChatOptions,\n): UseRealtimeChatReturn {\n // State\n const [status, setStatus] = useState<RealtimeStatus>('idle')\n const [mode, setMode] = useState<RealtimeMode>('idle')\n const [messages, setMessages] = useState<Array<RealtimeMessage>>([])\n const [pendingUserTranscript, setPendingUserTranscript] = useState<\n string | null\n >(null)\n const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState<\n string | null\n >(null)\n const [error, setError] = useState<Error | null>(null)\n const [inputLevel, setInputLevel] = useState(0)\n const [outputLevel, setOutputLevel] = useState(0)\n const [vadMode, setVADModeState] = useState<'server' | 'semantic' | 'manual'>(\n options.vadMode ?? 'server',\n )\n\n // Refs\n const clientRef = useRef<RealtimeClient | null>(null)\n const optionsRef = useRef(options)\n optionsRef.current = options\n const animationFrameRef = useRef<number | null>(null)\n\n // Create client instance - use ref to ensure we reuse the same instance\n // This handles React StrictMode double-rendering\n if (!clientRef.current) {\n // Each optional source field is spread conditionally because the\n // `RealtimeClientOptions` target declares strict optionals\n // (`field?: T`) and `exactOptionalPropertyTypes` rejects passing\n // `undefined` for absent values.\n const initial = optionsRef.current\n clientRef.current = new RealtimeClient({\n getToken: initial.getToken,\n adapter: initial.adapter,\n ...(initial.tools !== undefined && { tools: initial.tools }),\n ...(initial.instructions !== undefined && {\n instructions: initial.instructions,\n }),\n ...(initial.voice !== undefined && { voice: initial.voice }),\n ...(initial.autoPlayback !== undefined && {\n autoPlayback: initial.autoPlayback,\n }),\n ...(initial.autoCapture !== undefined && {\n autoCapture: initial.autoCapture,\n }),\n ...(initial.vadMode !== undefined && { vadMode: initial.vadMode }),\n ...(initial.outputModalities !== undefined && {\n outputModalities: initial.outputModalities,\n }),\n ...(initial.temperature !== undefined && {\n temperature: initial.temperature,\n }),\n ...(initial.maxOutputTokens !== undefined && {\n maxOutputTokens: initial.maxOutputTokens,\n }),\n ...(initial.semanticEagerness !== undefined && {\n semanticEagerness: initial.semanticEagerness,\n }),\n onStatusChange: (newStatus) => {\n setStatus(newStatus)\n },\n onModeChange: (newMode) => {\n setMode(newMode)\n optionsRef.current.onModeChange?.(newMode)\n },\n onMessage: (message) => {\n setMessages((prev) => [...prev, message])\n optionsRef.current.onMessage?.(message)\n },\n onError: (err) => {\n setError(err)\n optionsRef.current.onError?.(err)\n },\n onConnect: () => {\n setError(null)\n optionsRef.current.onConnect?.()\n },\n onDisconnect: () => {\n optionsRef.current.onDisconnect?.()\n },\n onInterrupted: () => {\n setPendingAssistantTranscript(null)\n optionsRef.current.onInterrupted?.()\n },\n })\n\n // Subscribe to state changes for transcripts\n clientRef.current.onStateChange((state) => {\n setPendingUserTranscript(state.pendingUserTranscript)\n setPendingAssistantTranscript(state.pendingAssistantTranscript)\n })\n }\n\n const client = clientRef.current\n\n // Audio level animation loop\n useEffect(() => {\n function updateLevels() {\n if (clientRef.current?.audio) {\n setInputLevel(clientRef.current.audio.inputLevel)\n setOutputLevel(clientRef.current.audio.outputLevel)\n }\n animationFrameRef.current = requestAnimationFrame(updateLevels)\n }\n\n if (status === 'connected') {\n updateLevels()\n }\n\n return () => {\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current)\n animationFrameRef.current = null\n }\n }\n }, [status])\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n clientRef.current?.destroy()\n }\n }, [])\n\n // Connection methods\n const connect = useCallback(async () => {\n setError(null)\n setMessages([])\n setPendingUserTranscript(null)\n setPendingAssistantTranscript(null)\n await client.connect()\n }, [client])\n\n const disconnect = useCallback(async () => {\n await client.disconnect()\n }, [client])\n\n // Voice control methods\n const startListening = useCallback(() => {\n client.startListening()\n }, [client])\n\n const stopListening = useCallback(() => {\n client.stopListening()\n }, [client])\n\n const interrupt = useCallback(() => {\n client.interrupt()\n }, [client])\n\n // Text input\n const sendText = useCallback(\n (text: string) => {\n client.sendText(text)\n },\n [client],\n )\n\n // Image input\n const sendImage = useCallback(\n (imageData: string, mimeType: string) => {\n client.sendImage(imageData, mimeType)\n },\n [client],\n )\n\n // Audio visualization\n const getInputFrequencyData = useCallback(() => {\n return (\n clientRef.current?.audio?.getInputFrequencyData() ?? emptyFrequencyData\n )\n }, [])\n\n const getOutputFrequencyData = useCallback(() => {\n return (\n clientRef.current?.audio?.getOutputFrequencyData() ?? emptyFrequencyData\n )\n }, [])\n\n const getInputTimeDomainData = useCallback(() => {\n return (\n clientRef.current?.audio?.getInputTimeDomainData() ?? emptyTimeDomainData\n )\n }, [])\n\n const getOutputTimeDomainData = useCallback(() => {\n return (\n clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData\n )\n }, [])\n\n // VAD mode control\n const setVADMode = useCallback(\n (newMode: 'server' | 'semantic' | 'manual') => {\n setVADModeState(newMode)\n // TODO: Update session config if connected\n },\n [],\n )\n\n return {\n // Connection state\n status,\n error,\n connect,\n disconnect,\n\n // Conversation state\n mode,\n messages,\n pendingUserTranscript,\n pendingAssistantTranscript,\n\n // Voice control\n startListening,\n stopListening,\n interrupt,\n\n // Text input\n sendText,\n\n // Image input\n sendImage,\n\n // Audio visualization\n inputLevel,\n outputLevel,\n getInputFrequencyData,\n getOutputFrequencyData,\n getInputTimeDomainData,\n getOutputTimeDomainData,\n\n // VAD control\n vadMode,\n setVADMode,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-realtime-chat.js","sources":["../../src/use-realtime-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { RealtimeClient } from '@tanstack/ai-client'\nimport type {\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeStatus,\n} from '@tanstack/ai'\nimport type {\n UseRealtimeChatOptions,\n UseRealtimeChatReturn,\n} from './realtime-types'\n\n// Empty frequency data for when client is not connected\nconst emptyFrequencyData = new Uint8Array(128)\nconst emptyTimeDomainData = new Uint8Array(128).fill(128)\n\n/**\n * React hook for realtime voice conversations.\n *\n * Provides a simple interface for voice-to-voice AI interactions\n * with support for multiple providers (OpenAI, ElevenLabs, etc.).\n *\n * @param options - Configuration options including adapter and callbacks\n * @returns Hook return value with state and control methods\n *\n * @example\n * ```typescript\n * import { useRealtimeChat } from '@tanstack/ai-react'\n * import { openaiRealtime } from '@tanstack/ai-openai'\n *\n * function VoiceChat() {\n * const {\n * status,\n * mode,\n * messages,\n * connect,\n * disconnect,\n * inputLevel,\n * outputLevel,\n * } = useRealtimeChat({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: openaiRealtime(),\n * })\n *\n * return (\n * <div>\n * <p>Status: {status}</p>\n * <p>Mode: {mode}</p>\n * <button onClick={status === 'idle' ? connect : disconnect}>\n * {status === 'idle' ? 'Start' : 'Stop'}\n * </button>\n * </div>\n * )\n * }\n * ```\n */\nexport function useRealtimeChat(\n options: UseRealtimeChatOptions,\n): UseRealtimeChatReturn {\n // State\n const [status, setStatus] = useState<RealtimeStatus>('idle')\n const [mode, setMode] = useState<RealtimeMode>('idle')\n const [messages, setMessages] = useState<Array<RealtimeMessage>>([])\n const [pendingUserTranscript, setPendingUserTranscript] = useState<\n string | null\n >(null)\n const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState<\n string | null\n >(null)\n const [error, setError] = useState<Error | null>(null)\n const [inputLevel, setInputLevel] = useState(0)\n const [outputLevel, setOutputLevel] = useState(0)\n\n // Refs\n const clientRef = useRef<RealtimeClient | null>(null)\n const optionsRef = useRef(options)\n optionsRef.current = options\n const animationFrameRef = useRef<number | null>(null)\n\n // Create client instance - use ref to ensure we reuse the same instance\n // This handles React StrictMode double-rendering\n if (!clientRef.current) {\n // Each optional source field is spread conditionally because the\n // `RealtimeClientOptions` target declares strict optionals\n // (`field?: T`) and `exactOptionalPropertyTypes` rejects passing\n // `undefined` for absent values.\n const initial = optionsRef.current\n clientRef.current = new RealtimeClient({\n getToken: initial.getToken,\n adapter: initial.adapter,\n ...(initial.tools !== undefined && { tools: initial.tools }),\n ...(initial.instructions !== undefined && {\n instructions: initial.instructions,\n }),\n ...(initial.voice !== undefined && { voice: initial.voice }),\n ...(initial.autoPlayback !== undefined && {\n autoPlayback: initial.autoPlayback,\n }),\n ...(initial.autoCapture !== undefined && {\n autoCapture: initial.autoCapture,\n }),\n ...(initial.vadMode !== undefined && { vadMode: initial.vadMode }),\n ...(initial.outputModalities !== undefined && {\n outputModalities: initial.outputModalities,\n }),\n ...(initial.temperature !== undefined && {\n temperature: initial.temperature,\n }),\n ...(initial.maxOutputTokens !== undefined && {\n maxOutputTokens: initial.maxOutputTokens,\n }),\n ...(initial.semanticEagerness !== undefined && {\n semanticEagerness: initial.semanticEagerness,\n }),\n onStatusChange: (newStatus) => {\n setStatus(newStatus)\n },\n onModeChange: (newMode) => {\n setMode(newMode)\n optionsRef.current.onModeChange?.(newMode)\n },\n onMessage: (message) => {\n setMessages((prev) => [...prev, message])\n optionsRef.current.onMessage?.(message)\n },\n onUsage(usage) {\n optionsRef.current.onUsage?.(usage)\n },\n onGoAway(timeLeft) {\n optionsRef.current.onGoAway?.(timeLeft)\n },\n onError: (err) => {\n setError(err)\n optionsRef.current.onError?.(err)\n },\n onConnect: () => {\n setError(null)\n optionsRef.current.onConnect?.()\n },\n onDisconnect: () => {\n optionsRef.current.onDisconnect?.()\n },\n onInterrupted: () => {\n setPendingAssistantTranscript(null)\n optionsRef.current.onInterrupted?.()\n },\n })\n\n // Subscribe to state changes for transcripts\n clientRef.current.onStateChange((state) => {\n setPendingUserTranscript(state.pendingUserTranscript)\n setPendingAssistantTranscript(state.pendingAssistantTranscript)\n })\n }\n\n const client = clientRef.current\n\n // Audio level animation loop\n useEffect(() => {\n function updateLevels() {\n if (clientRef.current?.audio) {\n setInputLevel(clientRef.current.audio.inputLevel)\n setOutputLevel(clientRef.current.audio.outputLevel)\n }\n animationFrameRef.current = requestAnimationFrame(updateLevels)\n }\n\n if (status === 'connected') {\n updateLevels()\n }\n\n return () => {\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current)\n animationFrameRef.current = null\n }\n }\n }, [status])\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n clientRef.current?.destroy()\n }\n }, [])\n\n // Connection methods\n const connect = useCallback(async () => {\n setError(null)\n setMessages([])\n setPendingUserTranscript(null)\n setPendingAssistantTranscript(null)\n await client.connect()\n }, [client])\n\n const disconnect = useCallback(async () => {\n await client.disconnect()\n }, [client])\n\n // Voice control methods\n const startListening = useCallback(() => {\n client.startListening()\n }, [client])\n\n const stopListening = useCallback(() => {\n client.stopListening()\n }, [client])\n\n const interrupt = useCallback(() => {\n client.interrupt()\n }, [client])\n\n // Text input\n const sendText = useCallback(\n (text: string) => {\n client.sendText(text)\n },\n [client],\n )\n\n // Image input\n const sendImage = useCallback(\n (imageData: string, mimeType: string) => {\n client.sendImage(imageData, mimeType)\n },\n [client],\n )\n\n // Audio visualization\n const getInputFrequencyData = useCallback(() => {\n return (\n clientRef.current?.audio?.getInputFrequencyData() ?? emptyFrequencyData\n )\n }, [])\n\n const getOutputFrequencyData = useCallback(() => {\n return (\n clientRef.current?.audio?.getOutputFrequencyData() ?? emptyFrequencyData\n )\n }, [])\n\n const getInputTimeDomainData = useCallback(() => {\n return (\n clientRef.current?.audio?.getInputTimeDomainData() ?? emptyTimeDomainData\n )\n }, [])\n\n const getOutputTimeDomainData = useCallback(() => {\n return (\n clientRef.current?.audio?.getOutputTimeDomainData() ?? emptyTimeDomainData\n )\n }, [])\n\n const updateSession = useCallback((config: RealtimeSessionConfig) => {\n clientRef.current?.updateSession(config)\n }, [])\n\n return {\n // Connection state\n status,\n error,\n connect,\n disconnect,\n\n // Conversation state\n mode,\n messages,\n pendingUserTranscript,\n pendingAssistantTranscript,\n\n // Voice control\n startListening,\n stopListening,\n interrupt,\n\n // Text input\n sendText,\n\n // Image input\n sendImage,\n\n // Audio visualization\n inputLevel,\n outputLevel,\n getInputFrequencyData,\n getOutputFrequencyData,\n getInputTimeDomainData,\n getOutputTimeDomainData,\n\n // Session control\n updateSession,\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-react",
3
- "version": "0.16.4",
3
+ "version": "0.18.0",
4
4
  "description": "React hooks for TanStack AI streaming chat, realtime voice, structured outputs, and media generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -49,13 +49,13 @@
49
49
  "media-generation"
50
50
  ],
51
51
  "dependencies": {
52
- "@tanstack/ai-client": "0.20.0"
52
+ "@tanstack/ai-client": "0.22.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@mcp-ui/client": "^7",
56
56
  "@types/react": ">=18.0.0",
57
57
  "react": ">=18.0.0",
58
- "@tanstack/ai": "^0.40.0"
58
+ "@tanstack/ai": "^0.41.0"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "@mcp-ui/client": {
@@ -71,7 +71,7 @@
71
71
  "jsdom": "^27.2.0",
72
72
  "react": "^19.2.3",
73
73
  "vite": "^7.3.3",
74
- "@tanstack/ai": "0.40.0"
74
+ "@tanstack/ai": "0.41.0"
75
75
  },
76
76
  "scripts": {
77
77
  "clean": "premove ./build ./dist",
package/src/index.ts CHANGED
@@ -8,6 +8,12 @@ export type {
8
8
  UseChatReturn,
9
9
  UIMessage,
10
10
  ChatRequestBody,
11
+ QueuedMessage,
12
+ SendMessageOptions,
13
+ WhenBusy,
14
+ QueueConfig,
15
+ QueueStrategy,
16
+ QueueOption,
11
17
  } from './types'
12
18
  export type {
13
19
  UseRealtimeChatOptions,
@@ -2,8 +2,10 @@ import type {
2
2
  AnyClientTool,
3
3
  RealtimeMessage,
4
4
  RealtimeMode,
5
+ RealtimeSessionConfig,
5
6
  RealtimeStatus,
6
7
  RealtimeToken,
8
+ UsageInfo,
7
9
  } from '@tanstack/ai'
8
10
  import type { RealtimeAdapter } from '@tanstack/ai-client'
9
11
 
@@ -79,6 +81,9 @@ export interface UseRealtimeChatOptions {
79
81
  onMessage?: (message: RealtimeMessage) => void
80
82
  onModeChange?: (mode: RealtimeMode) => void
81
83
  onInterrupted?: () => void
84
+ onUsage?: (usage: UsageInfo) => void
85
+ onGoAway?: (timeLeft?: string) => void
86
+ onStatusChange?: (status: RealtimeStatus) => void
82
87
  }
83
88
 
84
89
  /**
@@ -135,9 +140,6 @@ export interface UseRealtimeChatReturn {
135
140
  /** Get time domain data for output waveform */
136
141
  getOutputTimeDomainData: () => Uint8Array
137
142
 
138
- // VAD control
139
- /** Current VAD mode */
140
- vadMode: 'server' | 'semantic' | 'manual'
141
- /** Change VAD mode at runtime */
142
- setVADMode: (mode: 'server' | 'semantic' | 'manual') => void
143
+ // Session control
144
+ updateSession: (config: RealtimeSessionConfig) => void
143
145
  }
package/src/types.ts CHANGED
@@ -14,11 +14,27 @@ import type {
14
14
  DistributedOmit,
15
15
  InferredClientContext,
16
16
  MultimodalContent,
17
+ QueueConfig,
18
+ QueueOption,
19
+ QueueStrategy,
20
+ QueuedMessage,
21
+ SendMessageOptions,
17
22
  UIMessage,
23
+ WhenBusy,
18
24
  } from '@tanstack/ai-client'
19
25
 
20
26
  // Re-export types from ai-client
21
- export type { ChatRequestBody, MultimodalContent, UIMessage }
27
+ export type {
28
+ ChatRequestBody,
29
+ MultimodalContent,
30
+ QueueConfig,
31
+ QueuedMessage,
32
+ QueueOption,
33
+ QueueStrategy,
34
+ SendMessageOptions,
35
+ UIMessage,
36
+ WhenBusy,
37
+ }
22
38
 
23
39
  /**
24
40
  * Recursive partial — every property and every nested array element is optional.
@@ -71,6 +87,7 @@ export type UseChatOptions<
71
87
  | 'onSubscriptionChange'
72
88
  | 'onConnectionStatusChange'
73
89
  | 'onSessionGeneratingChange'
90
+ | 'onQueueChange'
74
91
  | 'context'
75
92
  | 'devtools'
76
93
  > & {
@@ -134,8 +151,25 @@ interface BaseUseChatReturn<
134
151
  /**
135
152
  * Send a message and get a response.
136
153
  * Can be a simple string or multimodal content with images, audio, etc.
154
+ * By default, sends while busy are queued until the run settles successfully
155
+ * (`queue: 'drop'` restores the old drop-while-busy behavior).
156
+ * Pass `{ whenBusy }` to override the policy for a single send.
157
+ */
158
+ sendMessage: (
159
+ content: string | MultimodalContent,
160
+ options?: SendMessageOptions,
161
+ ) => Promise<void>
162
+
163
+ /**
164
+ * Pending messages queued while the client is busy (streaming, claiming a
165
+ * send, or draining). Separate from `messages` until they drain.
166
+ */
167
+ queue: Array<QueuedMessage>
168
+
169
+ /**
170
+ * Cancel a queued message before it drains. No-op if already sent.
137
171
  */
138
- sendMessage: (content: string | MultimodalContent) => Promise<void>
172
+ cancelQueued: (id: string) => void
139
173
 
140
174
  /**
141
175
  * Append a message to the conversation
package/src/use-chat.ts CHANGED
@@ -12,6 +12,8 @@ import type {
12
12
  ChatClientState,
13
13
  ConnectionStatus,
14
14
  InferredClientContext,
15
+ QueuedMessage,
16
+ SendMessageOptions,
15
17
  StructuredOutputPart,
16
18
  } from '@tanstack/ai-client'
17
19
 
@@ -24,7 +26,7 @@ import type {
24
26
  } from './types'
25
27
 
26
28
  export function useChat<
27
- TTools extends ReadonlyArray<AnyClientTool> = any,
29
+ const TTools extends ReadonlyArray<AnyClientTool> = any,
28
30
  TSchema extends SchemaInput | undefined = undefined,
29
31
  TContext = InferredClientContext<TTools>,
30
32
  >(
@@ -43,6 +45,7 @@ export function useChat<
43
45
  const [connectionStatus, setConnectionStatus] =
44
46
  useState<ConnectionStatus>('disconnected')
45
47
  const [sessionGenerating, setSessionGenerating] = useState(false)
48
+ const [queue, setQueue] = useState<Array<QueuedMessage>>([])
46
49
 
47
50
  type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
48
51
  type Final = InferSchemaType<NonNullable<TSchema>>
@@ -156,6 +159,13 @@ export function useChat<
156
159
  if (activeClientRef.current !== instance) return
157
160
  setSessionGenerating(isGenerating)
158
161
  },
162
+ ...(optionsRef.current.queue !== undefined && {
163
+ queue: optionsRef.current.queue,
164
+ }),
165
+ onQueueChange: (nextQueue: Array<QueuedMessage>) => {
166
+ if (activeClientRef.current !== instance) return
167
+ setQueue(nextQueue)
168
+ },
159
169
  })
160
170
  activeClientRef.current = instance
161
171
  return instance
@@ -192,6 +202,12 @@ export function useChat<
192
202
  client.updateOptions({ context: options.context })
193
203
  }, [client, options.context])
194
204
 
205
+ useEffect(() => {
206
+ if (options.queue !== undefined) {
207
+ client.updateOptions({ queue: options.queue })
208
+ }
209
+ }, [client, options.queue])
210
+
195
211
  useEffect(() => {
196
212
  if (options.live) {
197
213
  client.subscribe()
@@ -229,8 +245,18 @@ export function useChat<
229
245
  }, [client])
230
246
 
231
247
  const sendMessage = useCallback(
232
- async (content: string | MultimodalContent) => {
233
- await client.sendMessage(content)
248
+ async (
249
+ content: string | MultimodalContent,
250
+ sendOptions?: SendMessageOptions,
251
+ ) => {
252
+ await client.sendMessage(content, undefined, sendOptions)
253
+ },
254
+ [client],
255
+ )
256
+
257
+ const cancelQueued = useCallback(
258
+ (id: string) => {
259
+ client.cancelQueued(id)
234
260
  },
235
261
  [client],
236
262
  )
@@ -346,6 +372,8 @@ export function useChat<
346
372
  clear,
347
373
  addToolResult,
348
374
  addToolApprovalResponse,
375
+ queue,
376
+ cancelQueued,
349
377
  partial,
350
378
  final,
351
379
  } as unknown as UseChatReturn<TTools, TSchema>
@@ -3,6 +3,7 @@ import { RealtimeClient } from '@tanstack/ai-client'
3
3
  import type {
4
4
  RealtimeMessage,
5
5
  RealtimeMode,
6
+ RealtimeSessionConfig,
6
7
  RealtimeStatus,
7
8
  } from '@tanstack/ai'
8
9
  import type {
@@ -70,9 +71,6 @@ export function useRealtimeChat(
70
71
  const [error, setError] = useState<Error | null>(null)
71
72
  const [inputLevel, setInputLevel] = useState(0)
72
73
  const [outputLevel, setOutputLevel] = useState(0)
73
- const [vadMode, setVADModeState] = useState<'server' | 'semantic' | 'manual'>(
74
- options.vadMode ?? 'server',
75
- )
76
74
 
77
75
  // Refs
78
76
  const clientRef = useRef<RealtimeClient | null>(null)
@@ -126,6 +124,12 @@ export function useRealtimeChat(
126
124
  setMessages((prev) => [...prev, message])
127
125
  optionsRef.current.onMessage?.(message)
128
126
  },
127
+ onUsage(usage) {
128
+ optionsRef.current.onUsage?.(usage)
129
+ },
130
+ onGoAway(timeLeft) {
131
+ optionsRef.current.onGoAway?.(timeLeft)
132
+ },
129
133
  onError: (err) => {
130
134
  setError(err)
131
135
  optionsRef.current.onError?.(err)
@@ -248,14 +252,9 @@ export function useRealtimeChat(
248
252
  )
249
253
  }, [])
250
254
 
251
- // VAD mode control
252
- const setVADMode = useCallback(
253
- (newMode: 'server' | 'semantic' | 'manual') => {
254
- setVADModeState(newMode)
255
- // TODO: Update session config if connected
256
- },
257
- [],
258
- )
255
+ const updateSession = useCallback((config: RealtimeSessionConfig) => {
256
+ clientRef.current?.updateSession(config)
257
+ }, [])
259
258
 
260
259
  return {
261
260
  // Connection state
@@ -289,8 +288,7 @@ export function useRealtimeChat(
289
288
  getInputTimeDomainData,
290
289
  getOutputTimeDomainData,
291
290
 
292
- // VAD control
293
- vadMode,
294
- setVADMode,
291
+ // Session control
292
+ updateSession,
295
293
  }
296
294
  }