@tanstack/ai-preact 0.12.3 → 0.13.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=preact&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=preact"
9
+ srcset="https://tanstack.com/api/readme/ai.png"
10
10
  />
11
11
  <img
12
- src="https://tanstack.com/api/readme/ai.png?framework=preact"
13
- alt="TanStack Preact 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,
@@ -1,5 +1,5 @@
1
- import { AnyClientTool, ModelMessage, RunAgentResumeItem, SchemaInput } from '@tanstack/ai';
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, ModelMessage, RunAgentResumeItem, SchemaInput } from '@tanstack/ai';
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
  * Options for the useChat hook.
@@ -17,7 +17,7 @@ export type { ChatRequestBody, MultimodalContent, QueueConfig, QueuedMessage, Qu
17
17
  * Note: Connection and body changes will recreate the ChatClient instance.
18
18
  * To update these options, remount the component or use a key prop.
19
19
  */
20
- export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TContext = InferredClientContext<TTools>> = DistributedOmit<ChatClientOptions<TTools, TContext>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'onQueueChange' | 'onResumeStateChange' | 'onRunIdChange' | 'context' | 'devtools' | 'id'> & {
20
+ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, 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'> & {
21
21
  live?: boolean;
22
22
  /** Display options for TanStack AI Devtools. */
23
23
  devtools?: AIDevtoolsDisplayOptions;
@@ -29,7 +29,7 @@ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TC
29
29
  */
30
30
  outputSchema?: SchemaInput;
31
31
  } & ClientContextOptionFromTools<TTools, TContext>;
32
- export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
32
+ export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> {
33
33
  /**
34
34
  * Current messages in the conversation
35
35
  */
@@ -80,14 +80,14 @@ export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any
80
80
  * it, correlate a log line).
81
81
  */
82
82
  runId: string | null;
83
- interrupts: BoundInterrupts<TTools>;
83
+ interrupts: BoundInterrupts<TTools, TInterrupts>;
84
84
  /** @deprecated Use `interrupts`. */
85
- pendingInterrupts: BoundInterrupts<TTools>;
86
- interruptErrors: ChatInterruptState<TTools>['interruptErrors'];
85
+ pendingInterrupts: BoundInterrupts<TTools, TInterrupts>;
86
+ interruptErrors: ChatInterruptState<TTools, TInterrupts>['interruptErrors'];
87
87
  resuming: boolean;
88
88
  resolveInterrupts: {
89
89
  (approved: boolean): void;
90
- (resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void;
90
+ (resolver: (interrupt: ResolvableChatInterrupt<TTools, TInterrupts>) => undefined): void;
91
91
  };
92
92
  cancelInterrupts: () => void;
93
93
  retryInterrupts: () => void;
@@ -1,4 +1,4 @@
1
1
  import { InferredClientContext } from '@tanstack/ai-client';
2
- import { AnyClientTool } from '@tanstack/ai';
2
+ import { AnyClientTool, InterruptDefinition } from '@tanstack/ai';
3
3
  import { UseChatOptions, UseChatReturn } from './types.js';
4
- export declare function useChat<const TTools extends ReadonlyArray<AnyClientTool> = any, TContext = InferredClientContext<TTools>>(options: UseChatOptions<TTools, TContext>): UseChatReturn<TTools>;
4
+ export declare function useChat<const TTools extends ReadonlyArray<AnyClientTool> = any, TContext = InferredClientContext<TTools>, const TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []>(options: UseChatOptions<TTools, TContext, TInterrupts>): UseChatReturn<TTools, TInterrupts>;
@@ -37,7 +37,7 @@ function useChat(options) {
37
37
  useEffect(() => {
38
38
  messagesRef.current = messages;
39
39
  }, [messages]);
40
- const client = useMemo(() => {
40
+ const { client, initialization } = useMemo(() => {
41
41
  const messagesToUse = options.initialMessages || [];
42
42
  isFirstMountRef.current = false;
43
43
  const initialOptions = optionsRef.current;
@@ -48,15 +48,29 @@ function useChat(options) {
48
48
  if (!currentInstance || activeClientRef.current !== currentInstance) return;
49
49
  return currentInstance;
50
50
  };
51
- const pendingInitializationErrors = [];
51
+ const initializationState = {
52
+ ready: false,
53
+ callbacks: []
54
+ };
55
+ const runOrQueueForActiveInstance = (callback) => {
56
+ if (!initializationState.ready) {
57
+ initializationState.callbacks.push(callback);
58
+ return;
59
+ }
60
+ const currentInstance = instanceHolder.current;
61
+ if (!currentInstance || activeClientRef.current !== currentInstance) return;
62
+ callback();
63
+ };
52
64
  const instance = new ChatClient({
53
65
  devtoolsBridgeFactory: createChatDevtoolsBridge,
54
66
  ...transport,
55
67
  initialMessages: messagesToUse,
56
68
  ...initialOptions.body !== void 0 && { body: initialOptions.body },
57
- ...initialOptions.threadId !== void 0 && { threadId: initialOptions.threadId },
69
+ ...typeof initialOptions.threadId === "string" && initialOptions.persistence ? {
70
+ persistence: initialOptions.persistence,
71
+ threadId: initialOptions.threadId
72
+ } : { ...initialOptions.threadId !== void 0 && { threadId: initialOptions.threadId } },
58
73
  ...initialOptions.forwardedProps !== void 0 && { forwardedProps: initialOptions.forwardedProps },
59
- ...initialOptions.persistence !== void 0 && { persistence: initialOptions.persistence },
60
74
  ...initialOptions.initialResumeSnapshot !== void 0 && { initialResumeSnapshot: initialOptions.initialResumeSnapshot },
61
75
  ...initialOptions.context !== void 0 && { context: initialOptions.context },
62
76
  devtools: {
@@ -70,89 +84,113 @@ function useChat(options) {
70
84
  return optionsRef.current.onResponse?.(response);
71
85
  },
72
86
  onChunk: (chunk) => {
73
- if (!getActiveInstance()) return;
74
- optionsRef.current.onChunk?.(chunk);
87
+ runOrQueueForActiveInstance(() => {
88
+ optionsRef.current.onChunk?.(chunk);
89
+ });
75
90
  },
76
91
  onFinish: (message) => {
77
- if (!getActiveInstance()) return;
78
- optionsRef.current.onFinish?.(message);
92
+ runOrQueueForActiveInstance(() => {
93
+ optionsRef.current.onFinish?.(message);
94
+ });
79
95
  },
80
96
  onError: (err) => {
81
- const currentInstance = instanceHolder.current;
82
- if (!currentInstance) {
83
- pendingInitializationErrors.push(err);
84
- return;
85
- }
86
- if (activeClientRef.current !== currentInstance) return;
87
- optionsRef.current.onError?.(err);
97
+ runOrQueueForActiveInstance(() => {
98
+ optionsRef.current.onError?.(err);
99
+ });
88
100
  },
89
101
  onCustomEvent: (eventType, data, context) => {
90
- if (!getActiveInstance()) return;
91
- optionsRef.current.onCustomEvent?.(eventType, data, context);
102
+ runOrQueueForActiveInstance(() => {
103
+ optionsRef.current.onCustomEvent?.(eventType, data, context);
104
+ });
92
105
  },
93
106
  ...initialOptions.tools !== void 0 && { tools: initialOptions.tools },
107
+ ...initialOptions.interrupts !== void 0 && { interrupts: initialOptions.interrupts },
94
108
  ...options.streamProcessor !== void 0 && { streamProcessor: options.streamProcessor },
95
109
  onMessagesChange: (newMessages) => {
96
- if (!getActiveInstance()) return;
97
- setMessages(newMessages);
110
+ runOrQueueForActiveInstance(() => {
111
+ setMessages(newMessages);
112
+ });
98
113
  },
99
114
  onLoadingChange: (newIsLoading) => {
100
- const currentInstance = getActiveInstance();
101
- if (!currentInstance) return;
102
- setIsLoading(newIsLoading);
103
- syncResumeState(currentInstance);
115
+ runOrQueueForActiveInstance(() => {
116
+ const currentInstance = getActiveInstance();
117
+ if (!currentInstance) return;
118
+ setIsLoading(newIsLoading);
119
+ syncResumeState(currentInstance);
120
+ });
104
121
  },
105
122
  onStatusChange: (newStatus) => {
106
- if (!getActiveInstance()) return;
107
- setStatus(newStatus);
123
+ runOrQueueForActiveInstance(() => {
124
+ setStatus(newStatus);
125
+ });
108
126
  },
109
127
  onErrorChange: (newError) => {
110
- if (!getActiveInstance()) return;
111
- setError(newError);
128
+ runOrQueueForActiveInstance(() => {
129
+ setError(newError);
130
+ });
112
131
  },
113
132
  onSubscriptionChange: (nextIsSubscribed) => {
114
- if (!getActiveInstance()) return;
115
- setIsSubscribed(nextIsSubscribed);
133
+ runOrQueueForActiveInstance(() => {
134
+ setIsSubscribed(nextIsSubscribed);
135
+ });
116
136
  },
117
137
  onConnectionStatusChange: (nextStatus) => {
118
- if (!getActiveInstance()) return;
119
- setConnectionStatus(nextStatus);
138
+ runOrQueueForActiveInstance(() => {
139
+ setConnectionStatus(nextStatus);
140
+ });
120
141
  },
121
142
  onSessionGeneratingChange: (isGenerating) => {
122
- if (!getActiveInstance()) return;
123
- setSessionGenerating(isGenerating);
143
+ runOrQueueForActiveInstance(() => {
144
+ setSessionGenerating(isGenerating);
145
+ });
124
146
  },
125
147
  ...optionsRef.current.queue !== void 0 && { queue: optionsRef.current.queue },
126
148
  onQueueChange: (nextQueue) => {
127
- if (!getActiveInstance()) return;
128
- setQueue(nextQueue);
149
+ runOrQueueForActiveInstance(() => {
150
+ setQueue(nextQueue);
151
+ });
129
152
  },
130
153
  onRunIdChange: (nextRunId) => {
131
- if (!getActiveInstance()) return;
132
- setRunId(nextRunId);
154
+ runOrQueueForActiveInstance(() => {
155
+ setRunId(nextRunId);
156
+ });
133
157
  },
134
158
  onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
135
- if (!getActiveInstance()) return;
136
- setInterruptState((current) => ({
137
- ...current,
138
- interrupts: nextPendingInterrupts,
139
- pendingInterrupts: nextPendingInterrupts
140
- }));
159
+ runOrQueueForActiveInstance(() => {
160
+ setInterruptState((current) => ({
161
+ ...current,
162
+ interrupts: nextPendingInterrupts,
163
+ pendingInterrupts: nextPendingInterrupts
164
+ }));
165
+ });
141
166
  },
142
- onInterruptStateChange: (nextInterruptState) => {
143
- if (!getActiveInstance()) return;
144
- setInterruptState(nextInterruptState);
145
- optionsRef.current.onInterruptStateChange?.(nextInterruptState);
167
+ onInterruptStateChange: (nextInterruptState, context) => {
168
+ runOrQueueForActiveInstance(() => {
169
+ setInterruptState(nextInterruptState);
170
+ optionsRef.current.onInterruptStateChange?.(nextInterruptState, context);
171
+ });
146
172
  }
147
173
  });
148
174
  instanceHolder.current = instance;
149
- activeClientRef.current = instance;
150
- for (const initializationError of pendingInitializationErrors) {
151
- if (activeClientRef.current !== instance) break;
152
- optionsRef.current.onError?.(initializationError);
153
- }
154
- return instance;
175
+ return {
176
+ client: instance,
177
+ initialization: initializationState
178
+ };
155
179
  }, [clientId, syncResumeState]);
180
+ useEffect(() => {
181
+ activeClientRef.current = client;
182
+ try {
183
+ while (initialization.callbacks.length > 0) {
184
+ if (activeClientRef.current !== client) {
185
+ initialization.callbacks.length = 0;
186
+ break;
187
+ }
188
+ initialization.callbacks.shift()?.();
189
+ }
190
+ } finally {
191
+ initialization.ready = true;
192
+ }
193
+ }, [client, initialization]);
156
194
  useEffect(() => {
157
195
  const clientMessages = client.getMessages();
158
196
  if (clientMessages !== messagesRef.current) setMessages(clientMessages);
@@ -190,7 +228,6 @@ function useChat(options) {
190
228
  clearTimeout(cleanupInvalidationRef.current);
191
229
  cleanupInvalidationRef.current = null;
192
230
  }
193
- activeClientRef.current = client;
194
231
  client.mountDevtools();
195
232
  syncResumeState(client);
196
233
  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 {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\nimport type {\n ChatClientState,\n ChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\n} from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n ModelMessage,\n RunAgentResumeItem,\n} from '@tanstack/ai'\n\nimport type {\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 TContext = InferredClientContext<TTools>,\n>(options: UseChatOptions<TTools, TContext>): UseChatReturn<TTools> {\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 client-recreation keying when no `threadId` is\n // 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 // 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 const cleanupDisposalRef = useRef<{\n client: ChatClient\n timeout: ReturnType<typeof setTimeout>\n } | null>(null)\n const optionsRef = useRef<UseChatOptions<TTools, TContext>>(options)\n\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 useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\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: 'preact',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n // Wrap every callback so the latest options are read at call time.\n // Capturing the function reference directly would freeze it to whatever\n // the parent passed on the first render.\n onResponse: (response) => {\n if (!getActiveInstance()) return\n return optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk) => {\n if (!getActiveInstance()) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message) => {\n if (!getActiveInstance()) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (err) => {\n const currentInstance = instanceHolder.current\n if (!currentInstance) {\n pendingInitializationErrors.push(err)\n return\n }\n if (activeClientRef.current !== currentInstance) return\n optionsRef.current.onError?.(err)\n },\n onCustomEvent: (eventType, data, context) => {\n if (!getActiveInstance()) return\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\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 onStatusChange: (newStatus: ChatClientState) => {\n if (!getActiveInstance()) return\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (!getActiveInstance()) return\n setError(newError)\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 (!getActiveInstance()) 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 initializationError of pendingInitializationErrors) {\n if (activeClientRef.current !== instance) break\n optionsRef.current.onError?.(initializationError)\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 body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n useEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client.updateOptions({\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n ...(options.queue !== undefined && { queue: options.queue }),\n })\n }, [\n client,\n options.body,\n options.forwardedProps,\n options.context,\n options.queue,\n ])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n // ONLY THE VIEW ON SCREEN HOLDS A STREAM. See the same effect in\n // `@tanstack/ai-react`. A page can own many chats and a browser allows only ~6\n // connections per origin, so one long-lived stream per chat starves everything\n // else once a handful of views have been open. `attach` is idempotent and\n // `detach` keeps the transcript and the resume pointer, so a remount picks the\n // run straight back up from the durable log.\n //\n // Immediate, unlike the deferred disposal below, which a remount can skip —\n // correct for disposal, useless for a connection.\n useEffect(() => {\n client.attach()\n return () => {\n client.detach()\n }\n }, [client])\n\n // Cleanup on unmount: stop any in-flight requests\n // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\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 reattaches 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 // 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 const disposal = {\n client,\n timeout: setTimeout(() => {\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 // All callback options are read through optionsRef at call time, so fresh\n // closures from each render are picked up without recreating the client.\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: unknown\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(resumeItems, state)\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 const renderedMessages = client.getMessages()\n\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 }\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 {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\nimport type {\n ChatClientState,\n ResolvableChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\n} from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InterruptDefinition,\n ModelMessage,\n RunAgentResumeItem,\n} from '@tanstack/ai'\n\nimport type {\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 TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(\n options: UseChatOptions<TTools, TContext, TInterrupts>,\n): UseChatReturn<TTools, TInterrupts> {\n // The hook's identity is its `threadId`. Reload with the same `threadId`\n // restores the same conversation. `hookId` is only a recreation key when no\n // `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 // 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 const cleanupDisposalRef = useRef<{\n client: ChatClient\n timeout: ReturnType<typeof setTimeout>\n } | null>(null)\n const optionsRef =\n useRef<UseChatOptions<TTools, TContext, TInterrupts>>(options)\n\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 useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\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 render that may never mount.\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 ...(initialOptions.body !== undefined && { body: initialOptions.body }),\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.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: 'preact',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n // Wrap every callback so the latest options are read at call time.\n // Capturing the function reference directly would freeze it to whatever\n // the parent passed on the first render.\n onResponse: (response) => {\n if (!getActiveInstance()) return\n return optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onChunk?.(chunk)\n })\n },\n onFinish: (message) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onFinish?.(message)\n })\n },\n onError: (err) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onError?.(err)\n })\n },\n onCustomEvent: (eventType, data, context) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n })\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n ...(initialOptions.interrupts !== undefined && {\n interrupts: initialOptions.interrupts,\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 onStatusChange: (newStatus: ChatClientState) => {\n runOrQueueForActiveInstance(() => {\n setStatus(newStatus)\n })\n },\n onErrorChange: (newError: Error | undefined) => {\n runOrQueueForActiveInstance(() => {\n setError(newError)\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 body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n useEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client.updateOptions({\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n ...(options.queue !== undefined && { queue: options.queue }),\n })\n }, [\n client,\n options.body,\n options.forwardedProps,\n options.context,\n options.queue,\n ])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n // ONLY THE VIEW ON SCREEN HOLDS A STREAM. See the same effect in\n // `@tanstack/ai-react`. A page can own many chats and a browser allows only ~6\n // connections per origin, so one long-lived stream per chat starves everything\n // else once a handful of views have been open. `attach` is idempotent and\n // `detach` keeps the transcript and the resume pointer, so a remount picks the\n // run straight back up from the durable log.\n //\n // Immediate, unlike the deferred disposal below, which a remount can skip —\n // correct for disposal, useless for a connection.\n useEffect(() => {\n client.attach()\n return () => {\n client.detach()\n }\n }, [client])\n\n // Cleanup on unmount: stop any in-flight requests\n // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\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 reattaches 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 // 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 const disposal = {\n client,\n timeout: setTimeout(() => {\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 // All callback options are read through optionsRef at call time, so fresh\n // closures from each render are picked up without recreating the client.\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: unknown\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(resumeItems, state)\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 const renderedMessages = client.getMessages()\n\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 }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-preact",
3
- "version": "0.12.3",
3
+ "version": "0.13.1",
4
4
  "description": "Preact hooks for TanStack AI streaming chat and typed messages.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -46,7 +46,7 @@
46
46
  "tool-calling"
47
47
  ],
48
48
  "dependencies": {
49
- "@tanstack/ai-client": "^0.23.3"
49
+ "@tanstack/ai-client": "^0.25.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@mcp-ui/client": "^7.1.1",
@@ -55,12 +55,12 @@
55
55
  "jsdom": "^27.4.0",
56
56
  "preact": "^10.26.9",
57
57
  "vite": "^8.2.1",
58
- "@tanstack/ai": "0.45.0"
58
+ "@tanstack/ai": "0.47.1"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@mcp-ui/client": "^7",
62
62
  "preact": ">=10.11.0",
63
- "@tanstack/ai": "^0.45.0"
63
+ "@tanstack/ai": "^0.47.1"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "@mcp-ui/client": {
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AnyClientTool,
3
+ InterruptDefinition,
3
4
  ModelMessage,
4
5
  RunAgentResumeItem,
5
6
  SchemaInput,
@@ -9,7 +10,7 @@ import type {
9
10
  BoundInterrupts,
10
11
  ChatClientOptions,
11
12
  ChatClientState,
12
- ChatInterrupt,
13
+ ResolvableChatInterrupt,
13
14
  ChatInterruptState,
14
15
  ChatRequestBody,
15
16
  ChatResumeState,
@@ -59,8 +60,10 @@ export type {
59
60
  export type UseChatOptions<
60
61
  TTools extends ReadonlyArray<AnyClientTool> = any,
61
62
  TContext = InferredClientContext<TTools>,
63
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
64
+ readonly [],
62
65
  > = DistributedOmit<
63
- ChatClientOptions<TTools, TContext>,
66
+ ChatClientOptions<TTools, TContext, TInterrupts>,
64
67
  | 'onMessagesChange'
65
68
  | 'onLoadingChange'
66
69
  | 'onErrorChange'
@@ -73,10 +76,6 @@ export type UseChatOptions<
73
76
  | 'onRunIdChange'
74
77
  | 'context'
75
78
  | 'devtools'
76
- // `id` is not a hook option: the hook's identity is its `threadId`, which is
77
- // also the persistence key. Persist across reloads by passing a stable
78
- // `threadId`; there is no separate id to set.
79
- | 'id'
80
79
  > & {
81
80
  live?: boolean
82
81
  /** Display options for TanStack AI Devtools. */
@@ -92,6 +91,8 @@ export type UseChatOptions<
92
91
 
93
92
  export interface UseChatReturn<
94
93
  TTools extends ReadonlyArray<AnyClientTool> = any,
94
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
95
+ readonly [],
95
96
  > {
96
97
  /**
97
98
  * Current messages in the conversation
@@ -153,14 +154,18 @@ export interface UseChatReturn<
153
154
  * it, correlate a log line).
154
155
  */
155
156
  runId: string | null
156
- interrupts: BoundInterrupts<TTools>
157
+ interrupts: BoundInterrupts<TTools, TInterrupts>
157
158
  /** @deprecated Use `interrupts`. */
158
- pendingInterrupts: BoundInterrupts<TTools>
159
- interruptErrors: ChatInterruptState<TTools>['interruptErrors']
159
+ pendingInterrupts: BoundInterrupts<TTools, TInterrupts>
160
+ interruptErrors: ChatInterruptState<TTools, TInterrupts>['interruptErrors']
160
161
  resuming: boolean
161
162
  resolveInterrupts: {
162
163
  (approved: boolean): void
163
- (resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void
164
+ (
165
+ resolver: (
166
+ interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
167
+ ) => undefined,
168
+ ): void
164
169
  }
165
170
  cancelInterrupts: () => void
166
171
  retryInterrupts: () => void
package/src/use-chat.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  } from 'preact/hooks'
11
11
  import type {
12
12
  ChatClientState,
13
- ChatInterrupt,
13
+ ResolvableChatInterrupt,
14
14
  ChatInterruptState,
15
15
  ChatResumeState,
16
16
  ConnectionStatus,
@@ -20,6 +20,7 @@ import type {
20
20
  } from '@tanstack/ai-client'
21
21
  import type {
22
22
  AnyClientTool,
23
+ InterruptDefinition,
23
24
  ModelMessage,
24
25
  RunAgentResumeItem,
25
26
  } from '@tanstack/ai'
@@ -37,11 +38,15 @@ const EMPTY_INTERRUPT_ERRORS = Object.freeze([])
37
38
  export function useChat<
38
39
  const TTools extends ReadonlyArray<AnyClientTool> = any,
39
40
  TContext = InferredClientContext<TTools>,
40
- >(options: UseChatOptions<TTools, TContext>): UseChatReturn<TTools> {
41
- // The hook's identity is its `threadId` — also the persistence key, so a
42
- // reload with the same `threadId` restores the same conversation. `hookId` is
43
- // only a stable fallback for client-recreation keying when no `threadId` is
44
- // given (an ephemeral chat), never a persistence key.
41
+ const TInterrupts extends ReadonlyArray<
42
+ InterruptDefinition<any, any, any, any>
43
+ > = readonly [],
44
+ >(
45
+ options: UseChatOptions<TTools, TContext, TInterrupts>,
46
+ ): UseChatReturn<TTools, TInterrupts> {
47
+ // The hook's identity is its `threadId`. Reload with the same `threadId`
48
+ // restores the same conversation. `hookId` is only a recreation key when no
49
+ // `threadId` is given. It is never sent on the wire.
45
50
  const hookId = useId()
46
51
  const clientId = options.threadId ?? hookId
47
52
 
@@ -58,7 +63,7 @@ export function useChat<
58
63
  const [queue, setQueue] = useState<Array<QueuedMessage>>([])
59
64
  const [runId, setRunId] = useState<string | null>(null)
60
65
  const [interruptState, setInterruptState] = useState<
61
- ChatInterruptState<TTools>
66
+ ChatInterruptState<TTools, TInterrupts>
62
67
  >(() => ({
63
68
  interrupts: EMPTY_INTERRUPTS,
64
69
  pendingInterrupts: EMPTY_INTERRUPTS,
@@ -79,7 +84,8 @@ export function useChat<
79
84
  client: ChatClient
80
85
  timeout: ReturnType<typeof setTimeout>
81
86
  } | null>(null)
82
- const optionsRef = useRef<UseChatOptions<TTools, TContext>>(options)
87
+ const optionsRef =
88
+ useRef<UseChatOptions<TTools, TContext, TInterrupts>>(options)
83
89
 
84
90
  optionsRef.current = options
85
91
 
@@ -93,7 +99,7 @@ export function useChat<
93
99
  messagesRef.current = messages
94
100
  }, [messages])
95
101
 
96
- const client = useMemo(() => {
102
+ const { client, initialization } = useMemo(() => {
97
103
  const messagesToUse = options.initialMessages || []
98
104
  isFirstMountRef.current = false
99
105
 
@@ -107,7 +113,7 @@ export function useChat<
107
113
  : { fetcher: initialOptions.fetcher }
108
114
 
109
115
  const instanceHolder: {
110
- current: ChatClient<TTools, TContext> | undefined
116
+ current: ChatClient<TTools, TContext, TInterrupts> | undefined
111
117
  } = { current: undefined }
112
118
  const getActiveInstance = () => {
113
119
  const currentInstance = instanceHolder.current
@@ -116,21 +122,43 @@ export function useChat<
116
122
  }
117
123
  return currentInstance
118
124
  }
119
- const pendingInitializationErrors: Array<Error> = []
120
- const instance = new ChatClient<TTools, TContext>({
125
+ // ChatClient may publish while its constructor is running or while async
126
+ // persistence resolves before commit. Preserve those exact notifications
127
+ // until this render commits; invoking them here would run state setters and
128
+ // user callbacks for a render that may never mount.
129
+ const initializationState = {
130
+ ready: false,
131
+ callbacks: [] as Array<() => void>,
132
+ }
133
+ const runOrQueueForActiveInstance = (callback: () => void) => {
134
+ if (!initializationState.ready) {
135
+ initializationState.callbacks.push(callback)
136
+ return
137
+ }
138
+ const currentInstance = instanceHolder.current
139
+ if (!currentInstance || activeClientRef.current !== currentInstance)
140
+ return
141
+ callback()
142
+ }
143
+ const instance = new ChatClient<TTools, TContext, TInterrupts>({
121
144
  devtoolsBridgeFactory: createChatDevtoolsBridge,
122
145
  ...transport,
123
146
  initialMessages: messagesToUse,
124
147
  ...(initialOptions.body !== undefined && { body: initialOptions.body }),
125
- ...(initialOptions.threadId !== undefined && {
126
- threadId: initialOptions.threadId,
127
- }),
148
+ ...(typeof initialOptions.threadId === 'string' &&
149
+ initialOptions.persistence
150
+ ? {
151
+ persistence: initialOptions.persistence,
152
+ threadId: initialOptions.threadId,
153
+ }
154
+ : {
155
+ ...(initialOptions.threadId !== undefined && {
156
+ threadId: initialOptions.threadId,
157
+ }),
158
+ }),
128
159
  ...(initialOptions.forwardedProps !== undefined && {
129
160
  forwardedProps: initialOptions.forwardedProps,
130
161
  }),
131
- ...(initialOptions.persistence !== undefined && {
132
- persistence: initialOptions.persistence,
133
- }),
134
162
  ...(initialOptions.initialResumeSnapshot !== undefined && {
135
163
  initialResumeSnapshot: initialOptions.initialResumeSnapshot,
136
164
  }),
@@ -151,96 +179,126 @@ export function useChat<
151
179
  return optionsRef.current.onResponse?.(response)
152
180
  },
153
181
  onChunk: (chunk) => {
154
- if (!getActiveInstance()) return
155
- optionsRef.current.onChunk?.(chunk)
182
+ runOrQueueForActiveInstance(() => {
183
+ optionsRef.current.onChunk?.(chunk)
184
+ })
156
185
  },
157
186
  onFinish: (message) => {
158
- if (!getActiveInstance()) return
159
- optionsRef.current.onFinish?.(message)
187
+ runOrQueueForActiveInstance(() => {
188
+ optionsRef.current.onFinish?.(message)
189
+ })
160
190
  },
161
191
  onError: (err) => {
162
- const currentInstance = instanceHolder.current
163
- if (!currentInstance) {
164
- pendingInitializationErrors.push(err)
165
- return
166
- }
167
- if (activeClientRef.current !== currentInstance) return
168
- optionsRef.current.onError?.(err)
192
+ runOrQueueForActiveInstance(() => {
193
+ optionsRef.current.onError?.(err)
194
+ })
169
195
  },
170
196
  onCustomEvent: (eventType, data, context) => {
171
- if (!getActiveInstance()) return
172
- optionsRef.current.onCustomEvent?.(eventType, data, context)
197
+ runOrQueueForActiveInstance(() => {
198
+ optionsRef.current.onCustomEvent?.(eventType, data, context)
199
+ })
173
200
  },
174
201
  ...(initialOptions.tools !== undefined && {
175
202
  tools: initialOptions.tools,
176
203
  }),
204
+ ...(initialOptions.interrupts !== undefined && {
205
+ interrupts: initialOptions.interrupts,
206
+ }),
177
207
  ...(options.streamProcessor !== undefined && {
178
208
  streamProcessor: options.streamProcessor,
179
209
  }),
180
210
  onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
181
- if (!getActiveInstance()) return
182
- setMessages(newMessages)
211
+ runOrQueueForActiveInstance(() => {
212
+ setMessages(newMessages)
213
+ })
183
214
  },
184
215
  onLoadingChange: (newIsLoading: boolean) => {
185
- const currentInstance = getActiveInstance()
186
- if (!currentInstance) return
187
- setIsLoading(newIsLoading)
188
- syncResumeState(currentInstance)
216
+ runOrQueueForActiveInstance(() => {
217
+ const currentInstance = getActiveInstance()
218
+ if (!currentInstance) return
219
+ setIsLoading(newIsLoading)
220
+ syncResumeState(currentInstance)
221
+ })
189
222
  },
190
223
  onStatusChange: (newStatus: ChatClientState) => {
191
- if (!getActiveInstance()) return
192
- setStatus(newStatus)
224
+ runOrQueueForActiveInstance(() => {
225
+ setStatus(newStatus)
226
+ })
193
227
  },
194
228
  onErrorChange: (newError: Error | undefined) => {
195
- if (!getActiveInstance()) return
196
- setError(newError)
229
+ runOrQueueForActiveInstance(() => {
230
+ setError(newError)
231
+ })
197
232
  },
198
233
  onSubscriptionChange: (nextIsSubscribed: boolean) => {
199
- if (!getActiveInstance()) return
200
- setIsSubscribed(nextIsSubscribed)
234
+ runOrQueueForActiveInstance(() => {
235
+ setIsSubscribed(nextIsSubscribed)
236
+ })
201
237
  },
202
238
  onConnectionStatusChange: (nextStatus: ConnectionStatus) => {
203
- if (!getActiveInstance()) return
204
- setConnectionStatus(nextStatus)
239
+ runOrQueueForActiveInstance(() => {
240
+ setConnectionStatus(nextStatus)
241
+ })
205
242
  },
206
243
  onSessionGeneratingChange: (isGenerating: boolean) => {
207
- if (!getActiveInstance()) return
208
- setSessionGenerating(isGenerating)
244
+ runOrQueueForActiveInstance(() => {
245
+ setSessionGenerating(isGenerating)
246
+ })
209
247
  },
210
248
  ...(optionsRef.current.queue !== undefined && {
211
249
  queue: optionsRef.current.queue,
212
250
  }),
213
251
  onQueueChange: (nextQueue: Array<QueuedMessage>) => {
214
- if (!getActiveInstance()) return
215
- setQueue(nextQueue)
252
+ runOrQueueForActiveInstance(() => {
253
+ setQueue(nextQueue)
254
+ })
216
255
  },
217
256
  onRunIdChange: (nextRunId) => {
218
- if (!getActiveInstance()) return
219
- setRunId(nextRunId)
257
+ runOrQueueForActiveInstance(() => {
258
+ setRunId(nextRunId)
259
+ })
220
260
  },
221
261
  onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
222
- if (!getActiveInstance()) return
223
- setInterruptState((current) => ({
224
- ...current,
225
- interrupts: nextPendingInterrupts,
226
- pendingInterrupts: nextPendingInterrupts,
227
- }))
262
+ runOrQueueForActiveInstance(() => {
263
+ setInterruptState((current) => ({
264
+ ...current,
265
+ interrupts: nextPendingInterrupts,
266
+ pendingInterrupts: nextPendingInterrupts,
267
+ }))
268
+ })
228
269
  },
229
- onInterruptStateChange: (nextInterruptState) => {
230
- if (!getActiveInstance()) return
231
- setInterruptState(nextInterruptState)
232
- optionsRef.current.onInterruptStateChange?.(nextInterruptState)
270
+ onInterruptStateChange: (nextInterruptState, context) => {
271
+ runOrQueueForActiveInstance(() => {
272
+ setInterruptState(nextInterruptState)
273
+ optionsRef.current.onInterruptStateChange?.(
274
+ nextInterruptState,
275
+ context,
276
+ )
277
+ })
233
278
  },
234
279
  })
235
280
  instanceHolder.current = instance
236
- activeClientRef.current = instance
237
- for (const initializationError of pendingInitializationErrors) {
238
- if (activeClientRef.current !== instance) break
239
- optionsRef.current.onError?.(initializationError)
240
- }
241
- return instance
281
+ return { client: instance, initialization: initializationState }
242
282
  }, [clientId, syncResumeState])
243
283
 
284
+ useEffect(() => {
285
+ activeClientRef.current = client
286
+ try {
287
+ // Keep initialization closed while draining so callbacks published by a
288
+ // queued callback are appended and delivered in the same commit.
289
+ while (initialization.callbacks.length > 0) {
290
+ if (activeClientRef.current !== client) {
291
+ initialization.callbacks.length = 0
292
+ break
293
+ }
294
+ initialization.callbacks.shift()?.()
295
+ }
296
+ } finally {
297
+ // A throw from a queued user callback must not leave the queue closed.
298
+ initialization.ready = true
299
+ }
300
+ }, [client, initialization])
301
+
244
302
  useEffect(() => {
245
303
  const clientMessages = client.getMessages()
246
304
  if (clientMessages !== messagesRef.current) {
@@ -307,7 +365,6 @@ export function useChat<
307
365
  clearTimeout(cleanupInvalidationRef.current)
308
366
  cleanupInvalidationRef.current = null
309
367
  }
310
- activeClientRef.current = client
311
368
  client.mountDevtools()
312
369
  // Delivery-durability resume is transparent: the resumable SSE connection
313
370
  // adapter reattaches via the browser's native Last-Event-ID on reconnect.
@@ -433,7 +490,11 @@ export function useChat<
433
490
 
434
491
  const resolveInterrupts = useCallback(
435
492
  (
436
- resolution: boolean | ((interrupt: ChatInterrupt<TTools>) => undefined),
493
+ resolution:
494
+ | boolean
495
+ | ((
496
+ interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
497
+ ) => undefined),
437
498
  ) => {
438
499
  if (typeof resolution === 'boolean') {
439
500
  client.resolveInterrupts(resolution)