@tanstack/ai-react 0.19.3 → 0.21.1

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.
package/README.md CHANGED
@@ -2,15 +2,15 @@
2
2
  <picture>
3
3
  <source
4
4
  media="(prefers-color-scheme: dark)"
5
- srcset="https://tanstack.com/api/readme/ai.png?framework=react&theme=dark"
5
+ srcset="https://tanstack.com/api/readme/ai.png?theme=dark"
6
6
  />
7
7
  <source
8
8
  media="(prefers-color-scheme: light)"
9
- srcset="https://tanstack.com/api/readme/ai.png?framework=react"
9
+ srcset="https://tanstack.com/api/readme/ai.png"
10
10
  />
11
11
  <img
12
- src="https://tanstack.com/api/readme/ai.png?framework=react"
13
- alt="TanStack React AI"
12
+ src="https://tanstack.com/api/readme/ai.png"
13
+ alt="TanStack AI"
14
14
  width="900"
15
15
  />
16
16
  </picture>
@@ -36,6 +36,14 @@
36
36
  </a>
37
37
  </div>
38
38
 
39
+ <br />
40
+
41
+ <div align="center">
42
+ <a href="https://tanstack.com/blog/tanstack-open-source-awards-2026">
43
+ <img src="https://raw.githubusercontent.com/TanStack/ai/16826d81cade868956df240d6239a671e689193c/media/js-open-source-award-2026-ai-project-of-the-year.svg" alt="Winner of the 2026 JavaScript Open Source Award for AI Project of the Year" width="250" />
44
+ </a>
45
+ </div>
46
+
39
47
  # TanStack AI
40
48
 
41
49
  Type-safe, provider-agnostic TypeScript SDK for building streaming chat,
@@ -191,7 +199,7 @@ Learn more in the
191
199
  build low-latency realtime voice experiences.
192
200
  - [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let
193
201
  models write and execute TypeScript inside a secure isolate.
194
- - [Code Mode with Skills](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-skills) -
202
+ - [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) -
195
203
  give Code Mode reusable runtime capabilities.
196
204
 
197
205
  ## Providers
@@ -208,6 +216,7 @@ Official adapters include:
208
216
  | [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
209
217
  | [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
210
218
  | [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
219
+ | [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
211
220
  | [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
212
221
 
213
222
  The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
@@ -20,4 +20,4 @@ export { useGenerateVideo } from './use-generate-video.js';
20
20
  export type { UseGenerateVideoOptions, UseGenerateVideoReturn, } from './use-generate-video.js';
21
21
  export { useAudioRecorder } from './use-audio-recorder.js';
22
22
  export type { UseAudioRecorderOptions, UseAudioRecorderReturn, } from './use-audio-recorder.js';
23
- export { fetchServerSentEvents, localStoragePersistence, sessionStoragePersistence, indexedDBPersistence, StorageUnavailableError, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatStorageAdapter, type WebStoragePersistenceOptions, type IndexedDBPersistenceOptions, fetchHttpStream, xhrServerSentEvents, xhrHttpStream, stream, rpcStream, createChatClientOptions, createMcpAppBridge, type McpAppBridge, type CreateMcpAppBridgeOptions, type ChatFetcher, type ChatFetcherInput, type ChatFetcherOptions, type ConnectionAdapter, type ConnectConnectionAdapter, type SubscribeConnectionAdapter, type RunAgentInputContext, type FetchConnectionOptions, type XhrConnectionOptions, type InferChatMessages, type GenerationClientState, type ImageGenerateInput, type AudioGenerateInput, type SpeechGenerateInput, type TranscriptionGenerateInput, type SummarizeGenerateInput, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, } from '@tanstack/ai-client';
23
+ export { fetchServerSentEvents, localStoragePersistence, sessionStoragePersistence, indexedDBPersistence, StorageUnavailableError, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatStorageAdapter, type WebStoragePersistenceOptions, type IndexedDBPersistenceOptions, fetchHttpStream, xhrServerSentEvents, xhrHttpStream, stream, rpcStream, webSocket, createChatClientOptions, createMcpAppBridge, type McpAppBridge, type CreateMcpAppBridgeOptions, type ChatFetcher, type ChatFetcherInput, type ChatFetcherOptions, type ConnectionAdapter, type ConnectConnectionAdapter, type SubscribeConnectionAdapter, type RunAgentInputContext, type FetchConnectionOptions, type XhrConnectionOptions, type WebSocketConnectionOptions, type InferChatMessages, type GenericInterrupt, type RegisteredGenericInterrupt, type GenerationClientState, type ImageGenerateInput, type AudioGenerateInput, type SpeechGenerateInput, type TranscriptionGenerateInput, type SummarizeGenerateInput, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, } from '@tanstack/ai-client';
package/dist/esm/index.js CHANGED
@@ -9,5 +9,5 @@ import { useTranscription } from "./use-transcription.js";
9
9
  import { useSummarize } from "./use-summarize.js";
10
10
  import { useGenerateVideo } from "./use-generate-video.js";
11
11
  import { useAudioRecorder } from "./use-audio-recorder.js";
12
- import { StorageUnavailableError, createChatClientOptions, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
13
- export { StorageUnavailableError, createChatClientOptions, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useMcpAppBridge, useRealtimeChat, useSummarize, useTranscription, xhrHttpStream, xhrServerSentEvents };
12
+ import { StorageUnavailableError, createChatClientOptions, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
13
+ export { StorageUnavailableError, createChatClientOptions, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useMcpAppBridge, useRealtimeChat, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
@@ -1,5 +1,5 @@
1
- import { AnyClientTool, InferSchemaType, ModelMessage, RunAgentResumeItem, SchemaInput } from '@tanstack/ai/client';
2
- import { AIDevtoolsDisplayOptions, BoundInterrupts, ChatClientOptions, ChatClientState, ChatInterrupt, ChatInterruptState, ChatRequestBody, ChatResumeState, ClientContextOptionFromTools, ConnectionStatus, DistributedOmit, InferredClientContext, MultimodalContent, QueueConfig, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from '@tanstack/ai-client';
1
+ import { AnyClientTool, InterruptDefinition, InferSchemaType, ModelMessage, RunAgentResumeItem, SchemaInput } from '@tanstack/ai/client';
2
+ import { AIDevtoolsDisplayOptions, BoundInterrupts, ChatClientOptions, ChatClientState, ResolvableChatInterrupt, ChatInterruptState, ChatRequestBody, ChatResumeState, ClientContextOptionFromTools, ConnectionStatus, DistributedOmit, InferredClientContext, MultimodalContent, QueueConfig, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from '@tanstack/ai-client';
3
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.
@@ -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' | 'onQueueChange' | 'onResumeStateChange' | 'onRunIdChange' | 'context' | 'devtools' | 'id'> & {
38
+ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = DistributedOmit<ChatClientOptions<TTools, TContext, TInterrupts>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'onQueueChange' | 'onResumeStateChange' | 'onRunIdChange' | 'context' | 'devtools'> & {
39
39
  /** Display options for TanStack AI Devtools. */
40
40
  devtools?: AIDevtoolsDisplayOptions;
41
41
  /**
@@ -56,7 +56,7 @@ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TS
56
56
  * typed `partial` / `final` fields; when it is omitted (default), the return
57
57
  * is unchanged.
58
58
  */
59
- export type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools, TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown> & (TSchema extends SchemaInput ? {
59
+ export type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = BaseUseChatReturn<TTools, TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown, TInterrupts> & (TSchema extends SchemaInput ? {
60
60
  /**
61
61
  * Live, progressively-parsed structured output. Updated from
62
62
  * `TEXT_MESSAGE_CONTENT` deltas via `parsePartialJSON` while the stream
@@ -71,7 +71,7 @@ export type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSc
71
71
  */
72
72
  final: InferSchemaType<TSchema> | null;
73
73
  } : Record<never, never>);
74
- interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown> {
74
+ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> {
75
75
  /**
76
76
  * Current messages in the conversation. When `outputSchema` is supplied,
77
77
  * `messages[i].parts.find(p => p.type === 'structured-output')` is typed
@@ -128,14 +128,14 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, T
128
128
  * it, correlate a log line).
129
129
  */
130
130
  runId: string | null;
131
- interrupts: BoundInterrupts<TTools>;
131
+ interrupts: BoundInterrupts<TTools, TInterrupts>;
132
132
  /** @deprecated Use `interrupts`. */
133
- pendingInterrupts: BoundInterrupts<TTools>;
134
- interruptErrors: ChatInterruptState<TTools>['interruptErrors'];
133
+ pendingInterrupts: BoundInterrupts<TTools, TInterrupts>;
134
+ interruptErrors: ChatInterruptState<TTools, TInterrupts>['interruptErrors'];
135
135
  resuming: boolean;
136
136
  resolveInterrupts: {
137
137
  (approved: boolean): void;
138
- (resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void;
138
+ (resolver: (interrupt: ResolvableChatInterrupt<TTools, TInterrupts>) => undefined): void;
139
139
  };
140
140
  cancelInterrupts: () => void;
141
141
  retryInterrupts: () => void;
@@ -1,4 +1,4 @@
1
- import { AnyClientTool, SchemaInput } from '@tanstack/ai/client';
1
+ import { AnyClientTool, InterruptDefinition, 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<const 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>, const TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>): UseChatReturn<TTools, TSchema, TInterrupts>;
@@ -36,7 +36,7 @@ function useChat(options) {
36
36
  setRunId(target.getCurrentRunId());
37
37
  setInterruptState(target.getInterruptState());
38
38
  }, []);
39
- const client = useMemo(() => {
39
+ const { client, initialization } = useMemo(() => {
40
40
  const messagesToUse = options.initialMessages || [];
41
41
  isFirstMountRef.current = false;
42
42
  const initialOptions = optionsRef.current;
@@ -47,15 +47,29 @@ function useChat(options) {
47
47
  if (!currentInstance || activeClientRef.current !== currentInstance) return;
48
48
  return currentInstance;
49
49
  };
50
- const pendingInitializationErrors = [];
50
+ const initializationState = {
51
+ ready: false,
52
+ callbacks: []
53
+ };
54
+ const runOrQueueForActiveInstance = (callback) => {
55
+ if (!initializationState.ready) {
56
+ initializationState.callbacks.push(callback);
57
+ return;
58
+ }
59
+ const currentInstance = instanceHolder.current;
60
+ if (!currentInstance || activeClientRef.current !== currentInstance) return;
61
+ callback();
62
+ };
51
63
  const instance = new ChatClient({
52
64
  devtoolsBridgeFactory: createChatDevtoolsBridge,
53
65
  ...transport,
54
66
  initialMessages: messagesToUse,
67
+ ...typeof initialOptions.threadId === "string" && initialOptions.persistence ? {
68
+ persistence: initialOptions.persistence,
69
+ threadId: initialOptions.threadId
70
+ } : { ...initialOptions.threadId !== void 0 && { threadId: initialOptions.threadId } },
55
71
  ...initialOptions.body !== void 0 && { body: initialOptions.body },
56
- ...initialOptions.threadId !== void 0 && { threadId: initialOptions.threadId },
57
72
  ...initialOptions.forwardedProps !== void 0 && { forwardedProps: initialOptions.forwardedProps },
58
- ...initialOptions.persistence !== void 0 && { persistence: initialOptions.persistence },
59
73
  ...initialOptions.initialResumeSnapshot !== void 0 && { initialResumeSnapshot: initialOptions.initialResumeSnapshot },
60
74
  ...initialOptions.context !== void 0 && { context: initialOptions.context },
61
75
  devtools: {
@@ -66,92 +80,116 @@ function useChat(options) {
66
80
  },
67
81
  onResponse: (response) => {
68
82
  if (!getActiveInstance()) return;
69
- optionsRef.current.onResponse?.(response);
83
+ return optionsRef.current.onResponse?.(response);
70
84
  },
71
85
  onChunk: (chunk) => {
72
- if (!getActiveInstance()) return;
73
- optionsRef.current.onChunk?.(chunk);
86
+ runOrQueueForActiveInstance(() => {
87
+ optionsRef.current.onChunk?.(chunk);
88
+ });
74
89
  },
75
90
  onFinish: (message) => {
76
- if (!getActiveInstance()) return;
77
- optionsRef.current.onFinish?.(message);
91
+ runOrQueueForActiveInstance(() => {
92
+ optionsRef.current.onFinish?.(message);
93
+ });
78
94
  },
79
95
  onError: (error) => {
80
- const currentInstance = instanceHolder.current;
81
- if (!currentInstance) {
82
- pendingInitializationErrors.push(error);
83
- return;
84
- }
85
- if (activeClientRef.current !== currentInstance) return;
86
- optionsRef.current.onError?.(error);
96
+ runOrQueueForActiveInstance(() => {
97
+ optionsRef.current.onError?.(error);
98
+ });
87
99
  },
88
100
  ...initialOptions.tools !== void 0 && { tools: initialOptions.tools },
101
+ ...initialOptions.interrupts !== void 0 && { interrupts: initialOptions.interrupts },
89
102
  onCustomEvent: (eventType, data, context) => {
90
- if (!getActiveInstance()) return;
91
- optionsRef.current.onCustomEvent?.(eventType, data, context);
103
+ runOrQueueForActiveInstance(() => {
104
+ optionsRef.current.onCustomEvent?.(eventType, data, context);
105
+ });
92
106
  },
93
107
  ...options.streamProcessor !== void 0 && { streamProcessor: options.streamProcessor },
94
108
  onMessagesChange: (newMessages) => {
95
- if (!getActiveInstance()) return;
96
- setMessages(newMessages);
109
+ runOrQueueForActiveInstance(() => {
110
+ setMessages(newMessages);
111
+ });
97
112
  },
98
113
  onLoadingChange: (newIsLoading) => {
99
- const currentInstance = getActiveInstance();
100
- if (!currentInstance) return;
101
- setIsLoading(newIsLoading);
102
- syncResumeState(currentInstance);
114
+ runOrQueueForActiveInstance(() => {
115
+ const currentInstance = getActiveInstance();
116
+ if (!currentInstance) return;
117
+ setIsLoading(newIsLoading);
118
+ syncResumeState(currentInstance);
119
+ });
103
120
  },
104
121
  onErrorChange: (newError) => {
105
- if (!getActiveInstance()) return;
106
- setError(newError);
122
+ runOrQueueForActiveInstance(() => {
123
+ setError(newError);
124
+ });
107
125
  },
108
126
  onStatusChange: (status) => {
109
- if (!getActiveInstance()) return;
110
- setStatus(status);
127
+ runOrQueueForActiveInstance(() => {
128
+ setStatus(status);
129
+ });
111
130
  },
112
131
  onSubscriptionChange: (nextIsSubscribed) => {
113
- if (!getActiveInstance()) return;
114
- setIsSubscribed(nextIsSubscribed);
132
+ runOrQueueForActiveInstance(() => {
133
+ setIsSubscribed(nextIsSubscribed);
134
+ });
115
135
  },
116
136
  onConnectionStatusChange: (nextStatus) => {
117
- if (!getActiveInstance()) return;
118
- setConnectionStatus(nextStatus);
137
+ runOrQueueForActiveInstance(() => {
138
+ setConnectionStatus(nextStatus);
139
+ });
119
140
  },
120
141
  onSessionGeneratingChange: (isGenerating) => {
121
- if (!getActiveInstance()) return;
122
- setSessionGenerating(isGenerating);
142
+ runOrQueueForActiveInstance(() => {
143
+ setSessionGenerating(isGenerating);
144
+ });
123
145
  },
124
146
  ...optionsRef.current.queue !== void 0 && { queue: optionsRef.current.queue },
125
147
  onQueueChange: (nextQueue) => {
126
- if (activeClientRef.current !== instance) return;
127
- setQueue(nextQueue);
148
+ runOrQueueForActiveInstance(() => {
149
+ setQueue(nextQueue);
150
+ });
128
151
  },
129
152
  onRunIdChange: (nextRunId) => {
130
- if (!getActiveInstance()) return;
131
- setRunId(nextRunId);
153
+ runOrQueueForActiveInstance(() => {
154
+ setRunId(nextRunId);
155
+ });
132
156
  },
133
157
  onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
134
- if (!getActiveInstance()) return;
135
- setInterruptState((current) => ({
136
- ...current,
137
- interrupts: nextPendingInterrupts,
138
- pendingInterrupts: nextPendingInterrupts
139
- }));
158
+ runOrQueueForActiveInstance(() => {
159
+ setInterruptState((current) => ({
160
+ ...current,
161
+ interrupts: nextPendingInterrupts,
162
+ pendingInterrupts: nextPendingInterrupts
163
+ }));
164
+ });
140
165
  },
141
- onInterruptStateChange: (nextInterruptState) => {
142
- if (!getActiveInstance()) return;
143
- setInterruptState(nextInterruptState);
144
- optionsRef.current.onInterruptStateChange?.(nextInterruptState);
166
+ onInterruptStateChange: (nextInterruptState, context) => {
167
+ runOrQueueForActiveInstance(() => {
168
+ setInterruptState(nextInterruptState);
169
+ optionsRef.current.onInterruptStateChange?.(nextInterruptState, context);
170
+ });
145
171
  }
146
172
  });
147
173
  instanceHolder.current = instance;
148
- activeClientRef.current = instance;
149
- for (const error of pendingInitializationErrors) {
150
- if (activeClientRef.current !== instance) break;
151
- optionsRef.current.onError?.(error);
152
- }
153
- return instance;
174
+ return {
175
+ client: instance,
176
+ initialization: initializationState
177
+ };
154
178
  }, [clientId, syncResumeState]);
179
+ useEffect(() => {
180
+ activeClientRef.current = client;
181
+ try {
182
+ while (initialization.callbacks.length > 0) {
183
+ if (activeClientRef.current !== client) {
184
+ initialization.callbacks.length = 0;
185
+ break;
186
+ }
187
+ initialization.callbacks.shift()?.();
188
+ }
189
+ } finally {
190
+ initialization.ready = true;
191
+ }
192
+ }, [client, initialization]);
155
193
  useEffect(() => {
156
194
  const clientMessages = client.getMessages();
157
195
  if (clientMessages !== messagesRef.current) setMessages(clientMessages);
@@ -195,7 +233,6 @@ function useChat(options) {
195
233
  clearTimeout(cleanupInvalidationRef.current);
196
234
  cleanupInvalidationRef.current = null;
197
235
  }
198
- activeClientRef.current = client;
199
236
  client.mountDevtools();
200
237
  syncResumeState(client);
201
238
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","names":[],"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 RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\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\nconst EMPTY_INTERRUPTS = Object.freeze([])\nconst EMPTY_INTERRUPT_ERRORS = Object.freeze([])\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 // The hook's identity is its `threadId` — also the persistence key, so a\n // reload with the same `threadId` restores the same conversation. `hookId` is\n // only a stable fallback for React's client-recreation keying when no\n // `threadId` is given (an ephemeral chat), never a persistence key.\n const hookId = useId()\n const clientId = options.threadId ?? 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 const [runId, setRunId] = useState<string | null>(null)\n const [interruptState, setInterruptState] = useState<\n ChatInterruptState<TTools>\n >(() => ({\n interrupts: EMPTY_INTERRUPTS,\n pendingInterrupts: EMPTY_INTERRUPTS,\n interruptErrors: EMPTY_INTERRUPT_ERRORS,\n resuming: false,\n }))\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 subscribedRef = useRef(false)\n const activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n const cleanupDisposalRef = useRef<{\n client: ChatClient\n timeout: ReturnType<typeof setTimeout>\n } | null>(null)\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 const syncResumeState = useCallback((target: ChatClient | null) => {\n if (!target) return\n setRunId(target.getCurrentRunId())\n setInterruptState(target.getInterruptState())\n }, [])\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 instanceHolder: {\n current: ChatClient<TTools, TContext> | undefined\n } = { current: undefined }\n const getActiveInstance = () => {\n const currentInstance = instanceHolder.current\n if (!currentInstance || activeClientRef.current !== currentInstance) {\n return undefined\n }\n return currentInstance\n }\n const pendingInitializationErrors: Array<Error> = []\n const instance = new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\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.initialResumeSnapshot !== undefined && {\n initialResumeSnapshot: initialOptions.initialResumeSnapshot,\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 (!getActiveInstance()) return\n void optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n if (!getActiveInstance()) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message: UIMessage<TTools>) => {\n if (!getActiveInstance()) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n const currentInstance = instanceHolder.current\n if (!currentInstance) {\n pendingInitializationErrors.push(error)\n return\n }\n if (activeClientRef.current !== currentInstance) return\n optionsRef.current.onError?.(error)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n onCustomEvent: (eventType, data, context) => {\n if (!getActiveInstance()) 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 (!getActiveInstance()) return\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n const currentInstance = getActiveInstance()\n if (!currentInstance) return\n setIsLoading(newIsLoading)\n syncResumeState(currentInstance)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (!getActiveInstance()) return\n setError(newError)\n },\n onStatusChange: (status: ChatClientState) => {\n if (!getActiveInstance()) return\n setStatus(status)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n if (!getActiveInstance()) return\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n if (!getActiveInstance()) return\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n if (!getActiveInstance()) 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 onRunIdChange: (nextRunId) => {\n if (!getActiveInstance()) return\n setRunId(nextRunId)\n },\n onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {\n if (!getActiveInstance()) return\n setInterruptState((current) => ({\n ...current,\n interrupts: nextPendingInterrupts,\n pendingInterrupts: nextPendingInterrupts,\n }))\n },\n onInterruptStateChange: (nextInterruptState) => {\n if (!getActiveInstance()) return\n setInterruptState(nextInterruptState)\n optionsRef.current.onInterruptStateChange?.(nextInterruptState)\n },\n })\n instanceHolder.current = instance\n activeClientRef.current = instance\n for (const error of pendingInitializationErrors) {\n if (activeClientRef.current !== instance) break\n optionsRef.current.onError?.(error)\n }\n return instance\n }, [clientId, syncResumeState])\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 subscribedRef.current = true\n } else if (subscribedRef.current) {\n // Only tear down a subscription we actually started. Calling\n // `unsubscribe()` on initial mount (when `live` was never enabled) would\n // abort an in-flight delivery resume — `resumeInFlightRun` is kicked off\n // in the client constructor, and `unsubscribe()` cancels the shared\n // in-flight stream — so a mid-stream reload would drop its rejoin before\n // it delivers a single chunk. This is exactly why a reload froze instead\n // of continuing.\n client.unsubscribe()\n subscribedRef.current = false\n }\n }, [client, options.live])\n\n // ONLY THE VIEW ON SCREEN HOLDS A STREAM.\n //\n // A page can own many chats — 40 sandboxes, 40 conversations — and a browser\n // allows only ~6 connections per origin. One long-lived stream per chat reaches\n // that ceiling after a handful of views, and every request after it QUEUES:\n // measured, an in-page fetch took over two minutes while the same request from\n // outside the browser took 17ms. So the connection follows the view.\n //\n // Immediate, not deferred: the deferred teardown below can be skipped when the\n // same client remounts, which is right for disposal but useless for a\n // connection. `attach` is idempotent and `detach` keeps the transcript and the\n // resume pointer, so a Strict Mode remount is just detach-then-attach and the\n // run is picked straight back up from the durable log.\n useEffect(() => {\n client.attach()\n return () => {\n client.detach()\n }\n }, [client])\n\n useEffect(() => {\n if (cleanupDisposalRef.current?.client === client) {\n clearTimeout(cleanupDisposalRef.current.timeout)\n cleanupDisposalRef.current = null\n }\n if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n activeClientRef.current = client\n client.mountDevtools()\n // Delivery-durability resume is transparent: the resumable SSE connection\n // adapter re-attaches via the browser's native Last-Event-ID on reconnect.\n // We only seed interrupt (state) resume from the client here.\n syncResumeState(client)\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Soft cleanup only: do NOT stop/unsubscribe here. React Strict Mode\n // remounts fire this cleanup then re-attach the same client one tick\n // later; calling `stop()` would abort a constructor rejoin\n // (`resumeInFlightRun`) and can wipe the durable resume pointer before\n // the first chunk. Real teardown lives in the deferred dispose path\n // below, which only runs when the client is not remounted.\n // Subscribe/unsubscribe on `options.live` is still owned by the\n // dedicated effect above for live toggles.\n const disposal = {\n client,\n timeout: setTimeout(() => {\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n if (cleanupDisposalRef.current === disposal) {\n cleanupDisposalRef.current = null\n }\n }, 0),\n }\n cleanupDisposalRef.current = disposal\n }\n }, [client, syncResumeState])\n\n const sendMessage = useCallback(\n async (\n content: string | MultimodalContent,\n sendOptions?: SendMessageOptions,\n ) => {\n try {\n await client.sendMessage(content, undefined, sendOptions)\n } finally {\n syncResumeState(client)\n }\n },\n [client, syncResumeState],\n )\n\n const cancelQueued = useCallback(\n (id: string) => {\n client.cancelQueued(id)\n },\n [client, syncResumeState],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n try {\n await client.append(message)\n } finally {\n syncResumeState(client)\n }\n },\n [client, syncResumeState],\n )\n\n const reload = useCallback(async () => {\n try {\n await client.reload()\n } finally {\n syncResumeState(client)\n }\n }, [client, syncResumeState])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n syncResumeState(client)\n }, [client, syncResumeState])\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 syncResumeState(client)\n },\n [client, syncResumeState],\n )\n\n const resumeInterrupts = useCallback(\n async (resumeItems: Array<RunAgentResumeItem>, state?: ChatResumeState) => {\n const result = await client.resumeInterrupts(\n resumeItems,\n state ?? undefined,\n )\n syncResumeState(client)\n return result\n },\n [client, syncResumeState],\n )\n\n const resolveInterrupts = useCallback(\n (\n resolution: boolean | ((interrupt: ChatInterrupt<TTools>) => undefined),\n ) => {\n if (typeof resolution === 'boolean') {\n client.resolveInterrupts(resolution)\n } else {\n client.resolveInterrupts(resolution)\n }\n },\n [client],\n )\n\n const cancelInterrupts = useCallback(() => {\n client.cancelInterrupts()\n }, [client])\n\n const retryInterrupts = useCallback(() => {\n client.retryInterrupts()\n }, [client])\n\n const resumeInterruptsUnsafe = useCallback(\n (resumeItems: Array<RunAgentResumeItem>, state?: ChatResumeState) =>\n client.resumeInterruptsUnsafe(resumeItems, state),\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 // oxlint-disable-next-line eslint-js/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 runId,\n interrupts: interruptState.interrupts,\n pendingInterrupts: interruptState.pendingInterrupts,\n interruptErrors: interruptState.interruptErrors,\n resuming: interruptState.resuming,\n resolveInterrupts,\n cancelInterrupts,\n retryInterrupts,\n resumeInterruptsUnsafe,\n resumeInterrupts,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-chat.js","names":[],"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 InterruptDefinition,\n ModelMessage,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ResolvableChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\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\nconst EMPTY_INTERRUPTS = Object.freeze([])\nconst EMPTY_INTERRUPT_ERRORS = Object.freeze([])\n\nexport function useChat<\n const TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(\n options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>,\n): UseChatReturn<TTools, TSchema, TInterrupts> {\n // The hook's identity is its `threadId`. Reload with the same `threadId`\n // restores the same conversation. `hookId` is only a React recreation key\n // when no `threadId` is given. It is never sent on the wire.\n const hookId = useId()\n const clientId = options.threadId ?? 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 const [runId, setRunId] = useState<string | null>(null)\n const [interruptState, setInterruptState] = useState<\n ChatInterruptState<TTools, TInterrupts>\n >(() => ({\n interrupts: EMPTY_INTERRUPTS,\n pendingInterrupts: EMPTY_INTERRUPTS,\n interruptErrors: EMPTY_INTERRUPT_ERRORS,\n resuming: false,\n }))\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 subscribedRef = useRef(false)\n const activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n const cleanupDisposalRef = useRef<{\n client: ChatClient\n timeout: ReturnType<typeof setTimeout>\n } | null>(null)\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 =\n useRef<UseChatOptions<TTools, TSchema, TContext, TInterrupts>>(options)\n optionsRef.current = options\n\n const syncResumeState = useCallback((target: ChatClient | null) => {\n if (!target) return\n setRunId(target.getCurrentRunId())\n setInterruptState(target.getInterruptState())\n }, [])\n\n // Create ChatClient instance with callbacks to sync state\n const { client, initialization } = 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 instanceHolder: {\n current: ChatClient<TTools, TContext, TInterrupts> | undefined\n } = { current: undefined }\n const getActiveInstance = () => {\n const currentInstance = instanceHolder.current\n if (!currentInstance || activeClientRef.current !== currentInstance) {\n return undefined\n }\n return currentInstance\n }\n // ChatClient may publish while its constructor is running or while async\n // persistence resolves before commit. Preserve those exact notifications\n // until this render commits; invoking them here would run state setters and\n // user callbacks for a client React may abandon.\n const initializationState = {\n ready: false,\n callbacks: [] as Array<() => void>,\n }\n const runOrQueueForActiveInstance = (callback: () => void) => {\n if (!initializationState.ready) {\n initializationState.callbacks.push(callback)\n return\n }\n const currentInstance = instanceHolder.current\n if (!currentInstance || activeClientRef.current !== currentInstance)\n return\n callback()\n }\n const instance = new ChatClient<TTools, TContext, TInterrupts>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n initialMessages: messagesToUse,\n ...(typeof initialOptions.threadId === 'string' &&\n initialOptions.persistence\n ? {\n persistence: initialOptions.persistence,\n threadId: initialOptions.threadId,\n }\n : {\n ...(initialOptions.threadId !== undefined && {\n threadId: initialOptions.threadId,\n }),\n }),\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.initialResumeSnapshot !== undefined && {\n initialResumeSnapshot: initialOptions.initialResumeSnapshot,\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 // ChatClient awaits this return value. Queuing would drop the promise.\n if (!getActiveInstance()) return\n return optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onChunk?.(chunk)\n })\n },\n onFinish: (message: UIMessage<TTools>) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onFinish?.(message)\n })\n },\n onError: (error: Error) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onError?.(error)\n })\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n ...(initialOptions.interrupts !== undefined && {\n interrupts: initialOptions.interrupts,\n }),\n onCustomEvent: (eventType, data, context) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n })\n },\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n runOrQueueForActiveInstance(() => {\n setMessages(newMessages)\n })\n },\n onLoadingChange: (newIsLoading: boolean) => {\n runOrQueueForActiveInstance(() => {\n const currentInstance = getActiveInstance()\n if (!currentInstance) return\n setIsLoading(newIsLoading)\n syncResumeState(currentInstance)\n })\n },\n onErrorChange: (newError: Error | undefined) => {\n runOrQueueForActiveInstance(() => {\n setError(newError)\n })\n },\n onStatusChange: (status: ChatClientState) => {\n runOrQueueForActiveInstance(() => {\n setStatus(status)\n })\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n runOrQueueForActiveInstance(() => {\n setIsSubscribed(nextIsSubscribed)\n })\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n runOrQueueForActiveInstance(() => {\n setConnectionStatus(nextStatus)\n })\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n runOrQueueForActiveInstance(() => {\n setSessionGenerating(isGenerating)\n })\n },\n ...(optionsRef.current.queue !== undefined && {\n queue: optionsRef.current.queue,\n }),\n onQueueChange: (nextQueue: Array<QueuedMessage>) => {\n runOrQueueForActiveInstance(() => {\n setQueue(nextQueue)\n })\n },\n onRunIdChange: (nextRunId) => {\n runOrQueueForActiveInstance(() => {\n setRunId(nextRunId)\n })\n },\n onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {\n runOrQueueForActiveInstance(() => {\n setInterruptState((current) => ({\n ...current,\n interrupts: nextPendingInterrupts,\n pendingInterrupts: nextPendingInterrupts,\n }))\n })\n },\n onInterruptStateChange: (nextInterruptState, context) => {\n runOrQueueForActiveInstance(() => {\n setInterruptState(nextInterruptState)\n optionsRef.current.onInterruptStateChange?.(\n nextInterruptState,\n context,\n )\n })\n },\n })\n instanceHolder.current = instance\n return { client: instance, initialization: initializationState }\n }, [clientId, syncResumeState])\n\n useEffect(() => {\n activeClientRef.current = client\n try {\n // Keep initialization closed while draining so callbacks published by a\n // queued callback are appended and delivered in the same commit.\n while (initialization.callbacks.length > 0) {\n if (activeClientRef.current !== client) {\n initialization.callbacks.length = 0\n break\n }\n initialization.callbacks.shift()?.()\n }\n } finally {\n // A throw from a queued user callback must not leave the queue closed.\n initialization.ready = true\n }\n }, [client, initialization])\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 subscribedRef.current = true\n } else if (subscribedRef.current) {\n // Only tear down a subscription we actually started. Calling\n // `unsubscribe()` on initial mount (when `live` was never enabled) would\n // abort an in-flight delivery resume — `resumeInFlightRun` is kicked off\n // in the client constructor, and `unsubscribe()` cancels the shared\n // in-flight stream — so a mid-stream reload would drop its rejoin before\n // it delivers a single chunk. This is exactly why a reload froze instead\n // of continuing.\n client.unsubscribe()\n subscribedRef.current = false\n }\n }, [client, options.live])\n\n // ONLY THE VIEW ON SCREEN HOLDS A STREAM.\n //\n // A page can own many chats — 40 sandboxes, 40 conversations — and a browser\n // allows only ~6 connections per origin. One long-lived stream per chat reaches\n // that ceiling after a handful of views, and every request after it QUEUES:\n // measured, an in-page fetch took over two minutes while the same request from\n // outside the browser took 17ms. So the connection follows the view.\n //\n // Immediate, not deferred: the deferred teardown below can be skipped when the\n // same client remounts, which is right for disposal but useless for a\n // connection. `attach` is idempotent and `detach` keeps the transcript and the\n // resume pointer, so a Strict Mode remount is just detach-then-attach and the\n // run is picked straight back up from the durable log.\n useEffect(() => {\n client.attach()\n return () => {\n client.detach()\n }\n }, [client])\n\n useEffect(() => {\n if (cleanupDisposalRef.current?.client === client) {\n clearTimeout(cleanupDisposalRef.current.timeout)\n cleanupDisposalRef.current = null\n }\n if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n client.mountDevtools()\n // Delivery-durability resume is transparent: the resumable SSE connection\n // adapter re-attaches via the browser's native Last-Event-ID on reconnect.\n // We only seed interrupt (state) resume from the client here.\n syncResumeState(client)\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Soft cleanup only: do NOT stop/unsubscribe here. React Strict Mode\n // remounts fire this cleanup then re-attach the same client one tick\n // later; calling `stop()` would abort a constructor rejoin\n // (`resumeInFlightRun`) and can wipe the durable resume pointer before\n // the first chunk. Real teardown lives in the deferred dispose path\n // below, which only runs when the client is not remounted.\n // Subscribe/unsubscribe on `options.live` is still owned by the\n // dedicated effect above for live toggles.\n const disposal = {\n client,\n timeout: setTimeout(() => {\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n if (cleanupDisposalRef.current === disposal) {\n cleanupDisposalRef.current = null\n }\n }, 0),\n }\n cleanupDisposalRef.current = disposal\n }\n }, [client, syncResumeState])\n\n const sendMessage = useCallback(\n async (\n content: string | MultimodalContent,\n sendOptions?: SendMessageOptions,\n ) => {\n try {\n await client.sendMessage(content, undefined, sendOptions)\n } finally {\n syncResumeState(client)\n }\n },\n [client, syncResumeState],\n )\n\n const cancelQueued = useCallback(\n (id: string) => {\n client.cancelQueued(id)\n },\n [client, syncResumeState],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n try {\n await client.append(message)\n } finally {\n syncResumeState(client)\n }\n },\n [client, syncResumeState],\n )\n\n const reload = useCallback(async () => {\n try {\n await client.reload()\n } finally {\n syncResumeState(client)\n }\n }, [client, syncResumeState])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n syncResumeState(client)\n }, [client, syncResumeState])\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 syncResumeState(client)\n },\n [client, syncResumeState],\n )\n\n const resumeInterrupts = useCallback(\n async (resumeItems: Array<RunAgentResumeItem>, state?: ChatResumeState) => {\n const result = await client.resumeInterrupts(\n resumeItems,\n state ?? undefined,\n )\n syncResumeState(client)\n return result\n },\n [client, syncResumeState],\n )\n\n const resolveInterrupts = useCallback(\n (\n resolution:\n | boolean\n | ((\n interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,\n ) => undefined),\n ) => {\n if (typeof resolution === 'boolean') {\n client.resolveInterrupts(resolution)\n } else {\n client.resolveInterrupts(resolution)\n }\n },\n [client],\n )\n\n const cancelInterrupts = useCallback(() => {\n client.cancelInterrupts()\n }, [client])\n\n const retryInterrupts = useCallback(() => {\n client.retryInterrupts()\n }, [client])\n\n const resumeInterruptsUnsafe = useCallback(\n (resumeItems: Array<RunAgentResumeItem>, state?: ChatResumeState) =>\n client.resumeInterruptsUnsafe(resumeItems, state),\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 // oxlint-disable-next-line eslint-js/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 runId,\n interrupts: interruptState.interrupts,\n pendingInterrupts: interruptState.pendingInterrupts,\n interruptErrors: interruptState.interruptErrors,\n resuming: interruptState.resuming,\n resolveInterrupts,\n cancelInterrupts,\n retryInterrupts,\n resumeInterruptsUnsafe,\n resumeInterrupts,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema, TInterrupts>\n}\n"],"mappings":";;;;AAgCA,IAAM,mBAAmB,OAAO,OAAO,CAAC,CAAC;AACzC,IAAM,yBAAyB,OAAO,OAAO,CAAC,CAAC;AAE/C,SAAgB,QAQd,SAC6C;CAI7C,MAAM,SAAS,MAAM;CACrB,MAAM,WAAW,QAAQ,YAAY;CAErC,MAAM,CAAC,UAAU,eAAe,SAC9B,QAAQ,mBAAmB,CAAC,CAC9B;CACA,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,OAAO,YAAY,SAA4B,KAAA,CAAS;CAC/D,MAAM,CAAC,QAAQ,aAAa,SAA0B,OAAO;CAC7D,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,kBAAkB,uBACvB,SAA2B,cAAc;CAC3C,MAAM,CAAC,mBAAmB,wBAAwB,SAAS,KAAK;CAChE,MAAM,CAAC,OAAO,YAAY,SAA+B,CAAC,CAAC;CAC3D,MAAM,CAAC,OAAO,YAAY,SAAwB,IAAI;CACtD,MAAM,CAAC,gBAAgB,qBAAqB,gBAEnC;EACP,YAAY;EACZ,mBAAmB;EACnB,iBAAiB;EACjB,UAAU;CACZ,EAAE;CAMF,MAAM,cAAc,OAClB,QAAQ,mBAAmB,CAAC,CAC9B;CACA,MAAM,kBAAkB,OAAO,IAAI;CACnC,MAAM,gBAAgB,OAAO,KAAK;CAClC,MAAM,kBAAkB,OAA0B,IAAI;CACtD,MAAM,yBAAyB,OAC7B,IACF;CACA,MAAM,qBAAqB,OAGjB,IAAI;CAGd,YAAY,UAAU;CAGtB,MAAM,aACJ,OAA+D,OAAO;CACxE,WAAW,UAAU;CAErB,MAAM,kBAAkB,aAAa,WAA8B;EACjE,IAAI,CAAC,QAAQ;EACb,SAAS,OAAO,gBAAgB,CAAC;EACjC,kBAAkB,OAAO,kBAAkB,CAAC;CAC9C,GAAG,CAAC,CAAC;CAGL,MAAM,EAAE,QAAQ,mBAAmB,cAAc;EAC/C,MAAM,gBAAgB,QAAQ,mBAAmB,CAAC;EAClD,gBAAgB,UAAU;EAM1B,MAAM,iBAAiB,WAAW;EAClC,MAAM,YAAY,eAAe,aAC7B,EAAE,YAAY,eAAe,WAAW,IACxC,EAAE,SAAS,eAAe,QAAQ;EAEtC,MAAM,iBAEF,EAAE,SAAS,KAAA,EAAU;EACzB,MAAM,0BAA0B;GAC9B,MAAM,kBAAkB,eAAe;GACvC,IAAI,CAAC,mBAAmB,gBAAgB,YAAY,iBAClD;GAEF,OAAO;EACT;EAKA,MAAM,sBAAsB;GAC1B,OAAO;GACP,WAAW,CAAC;EACd;EACA,MAAM,+BAA+B,aAAyB;GAC5D,IAAI,CAAC,oBAAoB,OAAO;IAC9B,oBAAoB,UAAU,KAAK,QAAQ;IAC3C;GACF;GACA,MAAM,kBAAkB,eAAe;GACvC,IAAI,CAAC,mBAAmB,gBAAgB,YAAY,iBAClD;GACF,SAAS;EACX;EACA,MAAM,WAAW,IAAI,WAA0C;GAC7D,uBAAuB;GACvB,GAAG;GACH,iBAAiB;GACjB,GAAI,OAAO,eAAe,aAAa,YACvC,eAAe,cACX;IACE,aAAa,eAAe;IAC5B,UAAU,eAAe;GAC3B,IACA,EACE,GAAI,eAAe,aAAa,KAAA,KAAa,EAC3C,UAAU,eAAe,SAC3B,EACF;GACJ,GAAI,eAAe,SAAS,KAAA,KAAa,EAAE,MAAM,eAAe,KAAK;GACrE,GAAI,eAAe,mBAAmB,KAAA,KAAa,EACjD,gBAAgB,eAAe,eACjC;GACA,GAAI,eAAe,0BAA0B,KAAA,KAAa,EACxD,uBAAuB,eAAe,sBACxC;GACA,GAAI,eAAe,YAAY,KAAA,KAAa,EAC1C,SAAS,eAAe,QAC1B;GACA,UAAU;IACR,GAAG,eAAe;IAClB,WAAW;IACX,UAAU;IACV,YAAY,eAAe,eAAe,eAAe;GAC3D;GACA,aAAa,aAAa;IAExB,IAAI,CAAC,kBAAkB,GAAG;IAC1B,OAAO,WAAW,QAAQ,aAAa,QAAQ;GACjD;GACA,UAAU,UAAuB;IAC/B,kCAAkC;KAChC,WAAW,QAAQ,UAAU,KAAK;IACpC,CAAC;GACH;GACA,WAAW,YAA+B;IACxC,kCAAkC;KAChC,WAAW,QAAQ,WAAW,OAAO;IACvC,CAAC;GACH;GACA,UAAU,UAAiB;IACzB,kCAAkC;KAChC,WAAW,QAAQ,UAAU,KAAK;IACpC,CAAC;GACH;GACA,GAAI,eAAe,UAAU,KAAA,KAAa,EACxC,OAAO,eAAe,MACxB;GACA,GAAI,eAAe,eAAe,KAAA,KAAa,EAC7C,YAAY,eAAe,WAC7B;GACA,gBAAgB,WAAW,MAAM,YAAY;IAC3C,kCAAkC;KAChC,WAAW,QAAQ,gBAAgB,WAAW,MAAM,OAAO;IAC7D,CAAC;GACH;GACA,GAAI,QAAQ,oBAAoB,KAAA,KAAa,EAC3C,iBAAiB,QAAQ,gBAC3B;GACA,mBAAmB,gBAA0C;IAC3D,kCAAkC;KAChC,YAAY,WAAW;IACzB,CAAC;GACH;GACA,kBAAkB,iBAA0B;IAC1C,kCAAkC;KAChC,MAAM,kBAAkB,kBAAkB;KAC1C,IAAI,CAAC,iBAAiB;KACtB,aAAa,YAAY;KACzB,gBAAgB,eAAe;IACjC,CAAC;GACH;GACA,gBAAgB,aAAgC;IAC9C,kCAAkC;KAChC,SAAS,QAAQ;IACnB,CAAC;GACH;GACA,iBAAiB,WAA4B;IAC3C,kCAAkC;KAChC,UAAU,MAAM;IAClB,CAAC;GACH;GACA,uBAAuB,qBAA8B;IACnD,kCAAkC;KAChC,gBAAgB,gBAAgB;IAClC,CAAC;GACH;GACA,2BAA2B,eAAiC;IAC1D,kCAAkC;KAChC,oBAAoB,UAAU;IAChC,CAAC;GACH;GACA,4BAA4B,iBAA0B;IACpD,kCAAkC;KAChC,qBAAqB,YAAY;IACnC,CAAC;GACH;GACA,GAAI,WAAW,QAAQ,UAAU,KAAA,KAAa,EAC5C,OAAO,WAAW,QAAQ,MAC5B;GACA,gBAAgB,cAAoC;IAClD,kCAAkC;KAChC,SAAS,SAAS;IACpB,CAAC;GACH;GACA,gBAAgB,cAAc;IAC5B,kCAAkC;KAChC,SAAS,SAAS;IACpB,CAAC;GACH;GACA,sBAAsB,kBAAkB,0BAA0B;IAChE,kCAAkC;KAChC,mBAAmB,aAAa;MAC9B,GAAG;MACH,YAAY;MACZ,mBAAmB;KACrB,EAAE;IACJ,CAAC;GACH;GACA,yBAAyB,oBAAoB,YAAY;IACvD,kCAAkC;KAChC,kBAAkB,kBAAkB;KACpC,WAAW,QAAQ,yBACjB,oBACA,OACF;IACF,CAAC;GACH;EACF,CAAC;EACD,eAAe,UAAU;EACzB,OAAO;GAAE,QAAQ;GAAU,gBAAgB;EAAoB;CACjE,GAAG,CAAC,UAAU,eAAe,CAAC;CAE9B,gBAAgB;EACd,gBAAgB,UAAU;EAC1B,IAAI;GAGF,OAAO,eAAe,UAAU,SAAS,GAAG;IAC1C,IAAI,gBAAgB,YAAY,QAAQ;KACtC,eAAe,UAAU,SAAS;KAClC;IACF;IACA,eAAe,UAAU,MAAM,CAAC,GAAG;GACrC;EACF,UAAU;GAER,eAAe,QAAQ;EACzB;CACF,GAAG,CAAC,QAAQ,cAAc,CAAC;CAE3B,gBAAgB;EACd,MAAM,iBAAiB,OAAO,YAAY;EAC1C,IAAI,mBAAmB,YAAY,SACjC,YAAY,cAAc;CAE9B,GAAG,CAAC,MAAM,CAAC;CAMX,gBAAgB;EACd,OAAO,cAAc,EAAE,MAAM,QAAQ,KAAK,CAAC;CAC7C,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;CAEzB,gBAAgB;EACd,IAAI,QAAQ,mBAAmB,KAAA,GAC7B,OAAO,cAAc,EAAE,gBAAgB,QAAQ,eAAe,CAAC;CAEnE,GAAG,CAAC,QAAQ,QAAQ,cAAc,CAAC;CAEnC,gBAAgB;EACd,IAAI,QAAQ,UAAU,KAAA,GACpB,OAAO,cAAc,EAAE,OAAO,QAAQ,MAAM,CAAC;CAEjD,GAAG,CAAC,QAAQ,QAAQ,KAAK,CAAC;CAE1B,gBAAgB;EACd,OAAO,cAAc,EAAE,SAAS,QAAQ,QAAQ,CAAC;CACnD,GAAG,CAAC,QAAQ,QAAQ,OAAO,CAAC;CAE5B,gBAAgB;EACd,IAAI,QAAQ,UAAU,KAAA,GACpB,OAAO,cAAc,EAAE,OAAO,QAAQ,MAAM,CAAC;CAEjD,GAAG,CAAC,QAAQ,QAAQ,KAAK,CAAC;CAE1B,gBAAgB;EACd,IAAI,QAAQ,MAAM;GAChB,OAAO,UAAU;GACjB,cAAc,UAAU;EAC1B,OAAO,IAAI,cAAc,SAAS;GAQhC,OAAO,YAAY;GACnB,cAAc,UAAU;EAC1B;CACF,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;CAezB,gBAAgB;EACd,OAAO,OAAO;EACd,aAAa;GACX,OAAO,OAAO;EAChB;CACF,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB;EACd,IAAI,mBAAmB,SAAS,WAAW,QAAQ;GACjD,aAAa,mBAAmB,QAAQ,OAAO;GAC/C,mBAAmB,UAAU;EAC/B;EACA,IAAI,uBAAuB,SAAS;GAClC,aAAa,uBAAuB,OAAO;GAC3C,uBAAuB,UAAU;EACnC;EACA,OAAO,cAAc;EAIrB,gBAAgB,MAAM;EAEtB,aAAa;GACX,uBAAuB,UAAU,iBAAiB;IAChD,IAAI,gBAAgB,YAAY,QAC9B,gBAAgB,UAAU;IAE5B,uBAAuB,UAAU;GACnC,GAAG,CAAC;GASJ,MAAM,WAAW;IACf;IACA,SAAS,iBAAiB;KACxB,IAAI,WAAW,QAAQ,MACrB,OAAO,YAAY;UAEnB,OAAO,KAAK;KAEd,OAAO,QAAQ;KACf,IAAI,mBAAmB,YAAY,UACjC,mBAAmB,UAAU;IAEjC,GAAG,CAAC;GACN;GACA,mBAAmB,UAAU;EAC/B;CACF,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,cAAc,YAClB,OACE,SACA,gBACG;EACH,IAAI;GACF,MAAM,OAAO,YAAY,SAAS,KAAA,GAAW,WAAW;EAC1D,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,eAAe,aAClB,OAAe;EACd,OAAO,aAAa,EAAE;CACxB,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,SAAS,YACb,OAAO,YAAsC;EAC3C,IAAI;GACF,MAAM,OAAO,OAAO,OAAO;EAC7B,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,SAAS,YAAY,YAAY;EACrC,IAAI;GACF,MAAM,OAAO,OAAO;EACtB,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,OAAO,kBAAkB;EAC7B,OAAO,KAAK;CACd,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,QAAQ,kBAAkB;EAC9B,OAAO,MAAM;EACb,gBAAgB,MAAM;CACxB,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,sBAAsB,aACzB,gBAA0C;EACzC,OAAO,oBAAoB,WAAW;CACxC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,gBAAgB,YACpB,OAAO,WAMD;EACJ,MAAM,OAAO,cAAc,MAAM;CACnC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,0BAA0B,YAC9B,OAAO,aAAgD;EACrD,MAAM,OAAO,wBAAwB,QAAQ;EAC7C,gBAAgB,MAAM;CACxB,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,mBAAmB,YACvB,OAAO,aAAwC,UAA4B;EACzE,MAAM,SAAS,MAAM,OAAO,iBAC1B,aACA,SAAS,KAAA,CACX;EACA,gBAAgB,MAAM;EACtB,OAAO;CACT,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,oBAAoB,aAEtB,eAKG;EACH,IAAI,OAAO,eAAe,WACxB,OAAO,kBAAkB,UAAU;OAEnC,OAAO,kBAAkB,UAAU;CAEvC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,mBAAmB,kBAAkB;EACzC,OAAO,iBAAiB;CAC1B,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,kBAAkB,kBAAkB;EACxC,OAAO,gBAAgB;CACzB,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,yBAAyB,aAC5B,aAAwC,UACvC,OAAO,uBAAuB,aAAa,KAAK,GAClD,CAAC,MAAM,CACT;CAYA,MAAM,mBAAmB,OAAO,YAAY;CAE5C,MAAM,uBAAuB,cAA2C;EACtE,IAAI,gBAAgB;EACpB,KAAK,IAAI,IAAI,iBAAiB,SAAS,GAAG,KAAK,GAAG,KAChD,IAAI,iBAAiB,EAAE,EAAE,SAAS,QAAQ;GACxC,gBAAgB;GAChB;EACF;EAEF,IAAI,kBAAkB,IAAI,OAAO;EACjC,KAAK,IAAI,IAAI,iBAAiB,SAAS,GAAG,IAAI,eAAe,KAAK;GAChE,MAAM,IAAI,iBAAiB;GAC3B,IAAI,GAAG,SAAS,aAAa;GAC7B,MAAM,OAAO,EAAE,MAAM,MAClB,MAAiC,EAAE,SAAS,mBAC/C;GACA,IAAI,MAAM,OAAO;EACnB;EACA,OAAO;CACT,GAAG,CAAC,gBAAgB,CAAC;CAErB,MAAM,UAAU,cAAuB;EACrC,IAAI,CAAC,sBAAsB,OAAO,CAAC;EAEnC,OADU,qBAAqB,WAAW,qBAAqB,QAClD,CAAC;CAChB,GAAG,CAAC,oBAAoB,CAAC;CAEzB,MAAM,QAAQ,cAA4B;EACxC,IAAI,CAAC,wBAAwB,qBAAqB,WAAW,YAC3D,OAAO;EAET,OAAO,qBAAqB;CAC9B,GAAG,CAAC,oBAAoB,CAAC;CAMzB,OAAO;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACA;EACA;EACA;EACA,YAAY,eAAe;EAC3B,mBAAmB,eAAe;EAClC,iBAAiB,eAAe;EAChC,UAAU,eAAe;EACzB;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -10,10 +10,6 @@ export interface UseGenerateAudioOptions<TOutput = AudioGenerationResult> {
10
10
  connection?: ConnectConnectionAdapter;
11
11
  /** Direct async function for audio generation */
12
12
  fetcher?: GenerationFetcher<AudioGenerateInput, AudioGenerationResult>;
13
- /**
14
- * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
15
- */
16
- id?: string;
17
13
  /** Additional body parameters to send with connect-based adapter requests */
18
14
  body?: Record<string, any>;
19
15
  /** Display options for TanStack AI Devtools. */
@@ -37,8 +33,8 @@ export interface UseGenerateAudioOptions<TOutput = AudioGenerationResult> {
37
33
  * id on the wire, which the protocol requires.
38
34
  *
39
35
  * **Required whenever `persistence` is set** — an app that cannot name the
40
- * scope has nothing to restore to. Optional for ephemeral generations, where
41
- * it falls back to `id` purely to satisfy the wire.
36
+ * scope has nothing to restore to. Optional for ephemeral generations. If
37
+ * omitted, the client mints a wire id after mount.
42
38
  */
43
39
  threadId?: string;
44
40
  /**
@@ -128,6 +124,6 @@ export interface UseGenerateAudioReturn<TOutput = AudioGenerationResult> {
128
124
  * }
129
125
  * ```
130
126
  */
131
- export declare function useGenerateAudio<TTransformed = void>(options: Omit<UseGenerateAudioOptions, 'onResult' | 'persistence' | 'threadId' | 'id'> & {
127
+ export declare function useGenerateAudio<TTransformed = void>(options: Omit<UseGenerateAudioOptions, 'onResult' | 'persistence' | 'threadId'> & {
132
128
  onResult?: (result: AudioGenerationResult) => TTransformed;
133
129
  } & GenerationPersistenceOptions): UseGenerateAudioReturn<InferGenerationOutputFromReturn<AudioGenerationResult, TTransformed>>;
@@ -1 +1 @@
1
- {"version":3,"file":"use-generate-audio.js","names":[],"sources":["../../src/use-generate-audio.ts"],"sourcesContent":["import { useGeneration } from './use-generation'\nimport { reconstructAudioResult } from '@tanstack/ai-client'\nimport type { AudioGenerationResult, StreamChunk } from '@tanstack/ai'\nimport type {\n AIDevtoolsDisplayOptions,\n AudioGenerateInput,\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n GenerationPersistenceOptions,\n InferGenerationOutputFromReturn,\n} from '@tanstack/ai-client'\n\n/**\n * Options for the useGenerateAudio hook.\n *\n * @template TOutput - The output type after optional transform (defaults to AudioGenerationResult)\n */\nexport interface UseGenerateAudioOptions<TOutput = AudioGenerationResult> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for audio generation */\n fetcher?: GenerationFetcher<AudioGenerateInput, AudioGenerationResult>\n /**\n * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).\n */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /** Display options for TanStack AI Devtools. */\n devtools?: AIDevtoolsDisplayOptions\n /**\n * How this generation persists across reloads.\n * - Omit / `false`: ephemeral, in-memory only.\n * - `true`: server-driven — on mount the client hydrates the last generation\n * for its `threadId` from the server (needs a connection with a\n * `hydrateGeneration` handler) and repaints it; it never auto-starts a run.\n */\n persistence?: boolean\n /**\n * The **scope** this generation belongs to: a stable, app-chosen name for the\n * slot successive runs fill — not a link to a chat conversation.\n *\n * The hook starts empty and produces many runs over its life; each gets its\n * own `runId`, but all belong to one scope. Persistence keys on this, so\n * derive it from your own domain and keep it identical across reloads (e.g.\n * `` `video-${videoId}-start-frame` ``). It is also sent as the AG-UI thread\n * id on the wire, which the protocol requires.\n *\n * **Required whenever `persistence` is set** — an app that cannot name the\n * scope has nothing to restore to. Optional for ephemeral generations, where\n * it falls back to `id` purely to satisfy the wire.\n */\n threadId?: string\n /**\n * Server-driven hydration handler for `persistence: true` when the\n * connection doesn't carry one (e.g. alongside `fetcher`, or a `stream()` /\n * `rpcStream()` adapter built without handlers) — typically a one-line\n * server-function call. The connection's own handler takes precedence.\n */\n hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']\n /**\n * Re-attach handler that replays a run still generating to completion on\n * mount, when the connection doesn't carry one. Without it, a restored\n * `running` snapshot surfaces as an (interrupted) error. The connection's\n * own handler takes precedence.\n */\n joinRun?: ConnectConnectionAdapter['joinRun']\n /**\n * Callback when audio is generated. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: AudioGenerationResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useGenerateAudio hook.\n *\n * @template TOutput - The output type (after optional transform)\n */\nexport interface UseGenerateAudioReturn<TOutput = AudioGenerationResult> {\n /** Trigger audio generation */\n generate: (input: AudioGenerateInput) => Promise<void>\n /** The generation result containing audio, or null */\n result: TOutput | null\n /** Whether generation is in progress */\n isLoading: boolean\n /** Current error, if any */\n error: Error | undefined\n /** Current state of the generation */\n status: GenerationClientState\n /** Abort the current generation */\n stop: () => void\n /** Clear result, error, and return to idle */\n reset: () => void\n /**\n * The id of the generation job currently running, or `null` when nothing is in\n * flight. Each call to `generate` is one job with its own id. Pass it to your\n * own endpoint to cancel or poll the provider job — `stop()` only aborts the\n * local stream, it does not stop work already running on the provider.\n */\n runId: string | null\n}\n\n/**\n * React hook for generating audio (music, sound effects) using AI models.\n *\n * Supports two transport modes:\n * - **ConnectConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom)\n * - **Fetcher** — Direct async function call\n *\n * @example\n * ```tsx\n * import { useGenerateAudio } from '@tanstack/ai-react'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function AudioGenerator() {\n * const { generate, result, isLoading, error, reset } = useGenerateAudio({\n * connection: fetchServerSentEvents('/api/generate/audio'),\n * })\n *\n * return (\n * <div>\n * <button onClick={() => generate({ prompt: 'An upbeat electronic track', duration: 10 })}>\n * Generate\n * </button>\n * {isLoading && <p>Generating...</p>}\n * {error && <p>Error: {error.message}</p>}\n * {result?.audio.url && <audio src={result.audio.url} controls />}\n * </div>\n * )\n * }\n * ```\n */\nexport function useGenerateAudio<TTransformed = void>(\n options: Omit<\n UseGenerateAudioOptions,\n 'onResult' | 'persistence' | 'threadId' | 'id'\n > & {\n onResult?: (result: AudioGenerationResult) => TTransformed\n } & GenerationPersistenceOptions,\n): UseGenerateAudioReturn<\n InferGenerationOutputFromReturn<AudioGenerationResult, TTransformed>\n> {\n const devtools = {\n ...options.devtools,\n framework: 'react',\n hookName: 'useGenerateAudio',\n outputKind: 'audio' as const,\n }\n const generation = useGeneration<\n AudioGenerateInput,\n AudioGenerationResult,\n TTransformed\n >({\n ...options,\n devtools,\n reconstructResult: reconstructAudioResult,\n })\n\n return generation\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+IA,SAAgB,iBACd,SAQA;CACA,MAAM,WAAW;EACf,GAAG,QAAQ;EACX,WAAW;EACX,UAAU;EACV,YAAY;CACd;CAWA,OAVmB,cAIjB;EACA,GAAG;EACH;EACA,mBAAmB;CACrB,CAEO;AACT"}
1
+ {"version":3,"file":"use-generate-audio.js","names":[],"sources":["../../src/use-generate-audio.ts"],"sourcesContent":["import { useGeneration } from './use-generation'\nimport { reconstructAudioResult } from '@tanstack/ai-client'\nimport type { AudioGenerationResult, StreamChunk } from '@tanstack/ai'\nimport type {\n AIDevtoolsDisplayOptions,\n AudioGenerateInput,\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n GenerationPersistenceOptions,\n InferGenerationOutputFromReturn,\n} from '@tanstack/ai-client'\n\n/**\n * Options for the useGenerateAudio hook.\n *\n * @template TOutput - The output type after optional transform (defaults to AudioGenerationResult)\n */\nexport interface UseGenerateAudioOptions<TOutput = AudioGenerationResult> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for audio generation */\n fetcher?: GenerationFetcher<AudioGenerateInput, AudioGenerationResult>\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /** Display options for TanStack AI Devtools. */\n devtools?: AIDevtoolsDisplayOptions\n /**\n * How this generation persists across reloads.\n * - Omit / `false`: ephemeral, in-memory only.\n * - `true`: server-driven — on mount the client hydrates the last generation\n * for its `threadId` from the server (needs a connection with a\n * `hydrateGeneration` handler) and repaints it; it never auto-starts a run.\n */\n persistence?: boolean\n /**\n * The **scope** this generation belongs to: a stable, app-chosen name for the\n * slot successive runs fill — not a link to a chat conversation.\n *\n * The hook starts empty and produces many runs over its life; each gets its\n * own `runId`, but all belong to one scope. Persistence keys on this, so\n * derive it from your own domain and keep it identical across reloads (e.g.\n * `` `video-${videoId}-start-frame` ``). It is also sent as the AG-UI thread\n * id on the wire, which the protocol requires.\n *\n * **Required whenever `persistence` is set** — an app that cannot name the\n * scope has nothing to restore to. Optional for ephemeral generations. If\n * omitted, the client mints a wire id after mount.\n */\n threadId?: string\n /**\n * Server-driven hydration handler for `persistence: true` when the\n * connection doesn't carry one (e.g. alongside `fetcher`, or a `stream()` /\n * `rpcStream()` adapter built without handlers) — typically a one-line\n * server-function call. The connection's own handler takes precedence.\n */\n hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']\n /**\n * Re-attach handler that replays a run still generating to completion on\n * mount, when the connection doesn't carry one. Without it, a restored\n * `running` snapshot surfaces as an (interrupted) error. The connection's\n * own handler takes precedence.\n */\n joinRun?: ConnectConnectionAdapter['joinRun']\n /**\n * Callback when audio is generated. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: AudioGenerationResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useGenerateAudio hook.\n *\n * @template TOutput - The output type (after optional transform)\n */\nexport interface UseGenerateAudioReturn<TOutput = AudioGenerationResult> {\n /** Trigger audio generation */\n generate: (input: AudioGenerateInput) => Promise<void>\n /** The generation result containing audio, or null */\n result: TOutput | null\n /** Whether generation is in progress */\n isLoading: boolean\n /** Current error, if any */\n error: Error | undefined\n /** Current state of the generation */\n status: GenerationClientState\n /** Abort the current generation */\n stop: () => void\n /** Clear result, error, and return to idle */\n reset: () => void\n /**\n * The id of the generation job currently running, or `null` when nothing is in\n * flight. Each call to `generate` is one job with its own id. Pass it to your\n * own endpoint to cancel or poll the provider job — `stop()` only aborts the\n * local stream, it does not stop work already running on the provider.\n */\n runId: string | null\n}\n\n/**\n * React hook for generating audio (music, sound effects) using AI models.\n *\n * Supports two transport modes:\n * - **ConnectConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom)\n * - **Fetcher** — Direct async function call\n *\n * @example\n * ```tsx\n * import { useGenerateAudio } from '@tanstack/ai-react'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function AudioGenerator() {\n * const { generate, result, isLoading, error, reset } = useGenerateAudio({\n * connection: fetchServerSentEvents('/api/generate/audio'),\n * })\n *\n * return (\n * <div>\n * <button onClick={() => generate({ prompt: 'An upbeat electronic track', duration: 10 })}>\n * Generate\n * </button>\n * {isLoading && <p>Generating...</p>}\n * {error && <p>Error: {error.message}</p>}\n * {result?.audio.url && <audio src={result.audio.url} controls />}\n * </div>\n * )\n * }\n * ```\n */\nexport function useGenerateAudio<TTransformed = void>(\n options: Omit<\n UseGenerateAudioOptions,\n 'onResult' | 'persistence' | 'threadId'\n > & {\n onResult?: (result: AudioGenerationResult) => TTransformed\n } & GenerationPersistenceOptions,\n): UseGenerateAudioReturn<\n InferGenerationOutputFromReturn<AudioGenerationResult, TTransformed>\n> {\n const devtools = {\n ...options.devtools,\n framework: 'react',\n hookName: 'useGenerateAudio',\n outputKind: 'audio' as const,\n }\n const generation = useGeneration<\n AudioGenerateInput,\n AudioGenerationResult,\n TTransformed\n >({\n ...options,\n devtools,\n reconstructResult: reconstructAudioResult,\n })\n\n return generation\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2IA,SAAgB,iBACd,SAQA;CACA,MAAM,WAAW;EACf,GAAG,QAAQ;EACX,WAAW;EACX,UAAU;EACV,YAAY;CACd;CAWA,OAVmB,cAIjB;EACA,GAAG;EACH;EACA,mBAAmB;CACrB,CAEO;AACT"}
@@ -10,10 +10,6 @@ export interface UseGenerateImageOptions<TOutput = ImageGenerationResult> {
10
10
  connection?: ConnectConnectionAdapter;
11
11
  /** Direct async function for image generation */
12
12
  fetcher?: GenerationFetcher<ImageGenerateInput, ImageGenerationResult>;
13
- /**
14
- * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
15
- */
16
- id?: string;
17
13
  /** Additional body parameters to send with connect-based adapter requests */
18
14
  body?: Record<string, any>;
19
15
  /** Display options for TanStack AI Devtools. */
@@ -37,8 +33,8 @@ export interface UseGenerateImageOptions<TOutput = ImageGenerationResult> {
37
33
  * id on the wire, which the protocol requires.
38
34
  *
39
35
  * **Required whenever `persistence` is set** — an app that cannot name the
40
- * scope has nothing to restore to. Optional for ephemeral generations, where
41
- * it falls back to `id` purely to satisfy the wire.
36
+ * scope has nothing to restore to. Optional for ephemeral generations. If
37
+ * omitted, the client mints a wire id after mount.
42
38
  */
43
39
  threadId?: string;
44
40
  /**
@@ -130,6 +126,6 @@ export interface UseGenerateImageReturn<TOutput = ImageGenerationResult> {
130
126
  * }
131
127
  * ```
132
128
  */
133
- export declare function useGenerateImage<TTransformed = void>(options: Omit<UseGenerateImageOptions, 'onResult' | 'persistence' | 'threadId' | 'id'> & {
129
+ export declare function useGenerateImage<TTransformed = void>(options: Omit<UseGenerateImageOptions, 'onResult' | 'persistence' | 'threadId'> & {
134
130
  onResult?: (result: ImageGenerationResult) => TTransformed;
135
131
  } & GenerationPersistenceOptions): UseGenerateImageReturn<InferGenerationOutputFromReturn<ImageGenerationResult, TTransformed>>;