@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 +14 -5
- package/dist/esm/types.d.ts +8 -8
- package/dist/esm/use-chat.d.ts +2 -2
- package/dist/esm/use-chat.js +91 -54
- package/dist/esm/use-chat.js.map +1 -1
- package/package.json +4 -4
- package/src/types.ts +15 -10
- package/src/use-chat.ts +130 -69
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?
|
|
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
|
|
9
|
+
srcset="https://tanstack.com/api/readme/ai.png"
|
|
10
10
|
/>
|
|
11
11
|
<img
|
|
12
|
-
src="https://tanstack.com/api/readme/ai.png
|
|
13
|
-
alt="TanStack
|
|
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
|
|
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,
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AnyClientTool, ModelMessage, RunAgentResumeItem, SchemaInput } from '@tanstack/ai';
|
|
2
|
-
import { AIDevtoolsDisplayOptions, BoundInterrupts, ChatClientOptions, ChatClientState,
|
|
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'
|
|
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:
|
|
90
|
+
(resolver: (interrupt: ResolvableChatInterrupt<TTools, TInterrupts>) => undefined): void;
|
|
91
91
|
};
|
|
92
92
|
cancelInterrupts: () => void;
|
|
93
93
|
retryInterrupts: () => void;
|
package/dist/esm/use-chat.d.ts
CHANGED
|
@@ -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>;
|
package/dist/esm/use-chat.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
87
|
+
runOrQueueForActiveInstance(() => {
|
|
88
|
+
optionsRef.current.onChunk?.(chunk);
|
|
89
|
+
});
|
|
75
90
|
},
|
|
76
91
|
onFinish: (message) => {
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
runOrQueueForActiveInstance(() => {
|
|
93
|
+
optionsRef.current.onFinish?.(message);
|
|
94
|
+
});
|
|
79
95
|
},
|
|
80
96
|
onError: (err) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
97
|
-
|
|
110
|
+
runOrQueueForActiveInstance(() => {
|
|
111
|
+
setMessages(newMessages);
|
|
112
|
+
});
|
|
98
113
|
},
|
|
99
114
|
onLoadingChange: (newIsLoading) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
runOrQueueForActiveInstance(() => {
|
|
116
|
+
const currentInstance = getActiveInstance();
|
|
117
|
+
if (!currentInstance) return;
|
|
118
|
+
setIsLoading(newIsLoading);
|
|
119
|
+
syncResumeState(currentInstance);
|
|
120
|
+
});
|
|
104
121
|
},
|
|
105
122
|
onStatusChange: (newStatus) => {
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
runOrQueueForActiveInstance(() => {
|
|
124
|
+
setStatus(newStatus);
|
|
125
|
+
});
|
|
108
126
|
},
|
|
109
127
|
onErrorChange: (newError) => {
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
runOrQueueForActiveInstance(() => {
|
|
129
|
+
setError(newError);
|
|
130
|
+
});
|
|
112
131
|
},
|
|
113
132
|
onSubscriptionChange: (nextIsSubscribed) => {
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
runOrQueueForActiveInstance(() => {
|
|
134
|
+
setIsSubscribed(nextIsSubscribed);
|
|
135
|
+
});
|
|
116
136
|
},
|
|
117
137
|
onConnectionStatusChange: (nextStatus) => {
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
runOrQueueForActiveInstance(() => {
|
|
139
|
+
setConnectionStatus(nextStatus);
|
|
140
|
+
});
|
|
120
141
|
},
|
|
121
142
|
onSessionGeneratingChange: (isGenerating) => {
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
runOrQueueForActiveInstance(() => {
|
|
144
|
+
setSessionGenerating(isGenerating);
|
|
145
|
+
});
|
|
124
146
|
},
|
|
125
147
|
...optionsRef.current.queue !== void 0 && { queue: optionsRef.current.queue },
|
|
126
148
|
onQueueChange: (nextQueue) => {
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
runOrQueueForActiveInstance(() => {
|
|
150
|
+
setQueue(nextQueue);
|
|
151
|
+
});
|
|
129
152
|
},
|
|
130
153
|
onRunIdChange: (nextRunId) => {
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
runOrQueueForActiveInstance(() => {
|
|
155
|
+
setRunId(nextRunId);
|
|
156
|
+
});
|
|
133
157
|
},
|
|
134
158
|
onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
runOrQueueForActiveInstance(() => {
|
|
160
|
+
setInterruptState((current) => ({
|
|
161
|
+
...current,
|
|
162
|
+
interrupts: nextPendingInterrupts,
|
|
163
|
+
pendingInterrupts: nextPendingInterrupts
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
141
166
|
},
|
|
142
|
-
onInterruptStateChange: (nextInterruptState) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 () => {
|
package/dist/esm/use-chat.js.map
CHANGED
|
@@ -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":";;;;AAiCA,IAAM,mBAAmB,OAAO,OAAO,CAAC,CAAC;AACzC,IAAM,yBAAyB,OAAO,OAAO,CAAC,CAAC;AAE/C,SAAgB,QAGd,SAAkE;CAKlE,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;CAGF,MAAM,cAAc,OAClB,QAAQ,mBAAmB,CAAC,CAC9B;CACA,MAAM,kBAAkB,OAAO,IAAI;CACnC,MAAM,kBAAkB,OAA0B,IAAI;CACtD,MAAM,yBAAyB,OAC7B,IACF;CACA,MAAM,qBAAqB,OAGjB,IAAI;CACd,MAAM,aAAa,OAAyC,OAAO;CAEnE,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;CAEL,gBAAgB;EACd,YAAY,UAAU;CACxB,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,SAAS,cAAc;EAC3B,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;EACA,MAAM,8BAA4C,CAAC;EACnD,MAAM,WAAW,IAAI,WAA6B;GAChD,uBAAuB;GACvB,GAAG;GACH,iBAAiB;GACjB,GAAI,eAAe,SAAS,KAAA,KAAa,EAAE,MAAM,eAAe,KAAK;GACrE,GAAI,eAAe,aAAa,KAAA,KAAa,EAC3C,UAAU,eAAe,SAC3B;GACA,GAAI,eAAe,mBAAmB,KAAA,KAAa,EACjD,gBAAgB,eAAe,eACjC;GACA,GAAI,eAAe,gBAAgB,KAAA,KAAa,EAC9C,aAAa,eAAe,YAC9B;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;GAIA,aAAa,aAAa;IACxB,IAAI,CAAC,kBAAkB,GAAG;IAC1B,OAAO,WAAW,QAAQ,aAAa,QAAQ;GACjD;GACA,UAAU,UAAU;IAClB,IAAI,CAAC,kBAAkB,GAAG;IAC1B,WAAW,QAAQ,UAAU,KAAK;GACpC;GACA,WAAW,YAAY;IACrB,IAAI,CAAC,kBAAkB,GAAG;IAC1B,WAAW,QAAQ,WAAW,OAAO;GACvC;GACA,UAAU,QAAQ;IAChB,MAAM,kBAAkB,eAAe;IACvC,IAAI,CAAC,iBAAiB;KACpB,4BAA4B,KAAK,GAAG;KACpC;IACF;IACA,IAAI,gBAAgB,YAAY,iBAAiB;IACjD,WAAW,QAAQ,UAAU,GAAG;GAClC;GACA,gBAAgB,WAAW,MAAM,YAAY;IAC3C,IAAI,CAAC,kBAAkB,GAAG;IAC1B,WAAW,QAAQ,gBAAgB,WAAW,MAAM,OAAO;GAC7D;GACA,GAAI,eAAe,UAAU,KAAA,KAAa,EACxC,OAAO,eAAe,MACxB;GACA,GAAI,QAAQ,oBAAoB,KAAA,KAAa,EAC3C,iBAAiB,QAAQ,gBAC3B;GACA,mBAAmB,gBAA0C;IAC3D,IAAI,CAAC,kBAAkB,GAAG;IAC1B,YAAY,WAAW;GACzB;GACA,kBAAkB,iBAA0B;IAC1C,MAAM,kBAAkB,kBAAkB;IAC1C,IAAI,CAAC,iBAAiB;IACtB,aAAa,YAAY;IACzB,gBAAgB,eAAe;GACjC;GACA,iBAAiB,cAA+B;IAC9C,IAAI,CAAC,kBAAkB,GAAG;IAC1B,UAAU,SAAS;GACrB;GACA,gBAAgB,aAAgC;IAC9C,IAAI,CAAC,kBAAkB,GAAG;IAC1B,SAAS,QAAQ;GACnB;GACA,uBAAuB,qBAA8B;IACnD,IAAI,CAAC,kBAAkB,GAAG;IAC1B,gBAAgB,gBAAgB;GAClC;GACA,2BAA2B,eAAiC;IAC1D,IAAI,CAAC,kBAAkB,GAAG;IAC1B,oBAAoB,UAAU;GAChC;GACA,4BAA4B,iBAA0B;IACpD,IAAI,CAAC,kBAAkB,GAAG;IAC1B,qBAAqB,YAAY;GACnC;GACA,GAAI,WAAW,QAAQ,UAAU,KAAA,KAAa,EAC5C,OAAO,WAAW,QAAQ,MAC5B;GACA,gBAAgB,cAAoC;IAClD,IAAI,CAAC,kBAAkB,GAAG;IAC1B,SAAS,SAAS;GACpB;GACA,gBAAgB,cAAc;IAC5B,IAAI,CAAC,kBAAkB,GAAG;IAC1B,SAAS,SAAS;GACpB;GACA,sBAAsB,kBAAkB,0BAA0B;IAChE,IAAI,CAAC,kBAAkB,GAAG;IAC1B,mBAAmB,aAAa;KAC9B,GAAG;KACH,YAAY;KACZ,mBAAmB;IACrB,EAAE;GACJ;GACA,yBAAyB,uBAAuB;IAC9C,IAAI,CAAC,kBAAkB,GAAG;IAC1B,kBAAkB,kBAAkB;IACpC,WAAW,QAAQ,yBAAyB,kBAAkB;GAChE;EACF,CAAC;EACD,eAAe,UAAU;EACzB,gBAAgB,UAAU;EAC1B,KAAK,MAAM,uBAAuB,6BAA6B;GAC7D,IAAI,gBAAgB,YAAY,UAAU;GAC1C,WAAW,QAAQ,UAAU,mBAAmB;EAClD;EACA,OAAO;CACT,GAAG,CAAC,UAAU,eAAe,CAAC;CAE9B,gBAAgB;EACd,MAAM,iBAAiB,OAAO,YAAY;EAC1C,IAAI,mBAAmB,YAAY,SACjC,YAAY,cAAc;CAE9B,GAAG,CAAC,MAAM,CAAC;CAKX,gBAAgB;EAGd,OAAO,cAAc;GACnB,MAAM,QAAQ;GACd,GAAI,QAAQ,mBAAmB,KAAA,KAAa,EAC1C,gBAAgB,QAAQ,eAC1B;GACA,SAAS,QAAQ;GACjB,GAAI,QAAQ,UAAU,KAAA,KAAa,EAAE,OAAO,QAAQ,MAAM;EAC5D,CAAC;CACH,GAAG;EACD;EACA,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;CACV,CAAC;CAED,gBAAgB;EACd,IAAI,QAAQ,MACV,OAAO,UAAU;OAEjB,OAAO,YAAY;CAEvB,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;CAWzB,gBAAgB;EACd,OAAO,OAAO;EACd,aAAa;GACX,OAAO,OAAO;EAChB;CACF,GAAG,CAAC,MAAM,CAAC;CAMX,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,gBAAgB,UAAU;EAC1B,OAAO,cAAc;EAIrB,gBAAgB,MAAM;EAEtB,aAAa;GACX,uBAAuB,UAAU,iBAAiB;IAChD,IAAI,gBAAgB,YAAY,QAC9B,gBAAgB,UAAU;IAE5B,uBAAuB,UAAU;GACnC,GAAG,CAAC;GAKJ,IAAI,WAAW,QAAQ,MACrB,OAAO,YAAY;QAEnB,OAAO,KAAK;GAEd,MAAM,WAAW;IACf;IACA,SAAS,iBAAiB;KACxB,OAAO,QAAQ;KACf,IAAI,mBAAmB,YAAY,UACjC,mBAAmB,UAAU;IAEjC,GAAG,CAAC;GACN;GACA,mBAAmB,UAAU;EAC/B;CACF,GAAG,CAAC,QAAQ,eAAe,CAAC;CAI5B,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,iBAAiB,aAAa,KAAK;EAC/D,gBAAgB,MAAM;EACtB,OAAO;CACT,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,oBAAoB,aAEtB,eACG;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;CAIA,OAAO;EACL,UAHuB,OAAO,YAGpB;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;CACF;AACF"}
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 =
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
126
|
-
|
|
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
|
-
|
|
155
|
-
|
|
182
|
+
runOrQueueForActiveInstance(() => {
|
|
183
|
+
optionsRef.current.onChunk?.(chunk)
|
|
184
|
+
})
|
|
156
185
|
},
|
|
157
186
|
onFinish: (message) => {
|
|
158
|
-
|
|
159
|
-
|
|
187
|
+
runOrQueueForActiveInstance(() => {
|
|
188
|
+
optionsRef.current.onFinish?.(message)
|
|
189
|
+
})
|
|
160
190
|
},
|
|
161
191
|
onError: (err) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
182
|
-
|
|
211
|
+
runOrQueueForActiveInstance(() => {
|
|
212
|
+
setMessages(newMessages)
|
|
213
|
+
})
|
|
183
214
|
},
|
|
184
215
|
onLoadingChange: (newIsLoading: boolean) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
224
|
+
runOrQueueForActiveInstance(() => {
|
|
225
|
+
setStatus(newStatus)
|
|
226
|
+
})
|
|
193
227
|
},
|
|
194
228
|
onErrorChange: (newError: Error | undefined) => {
|
|
195
|
-
|
|
196
|
-
|
|
229
|
+
runOrQueueForActiveInstance(() => {
|
|
230
|
+
setError(newError)
|
|
231
|
+
})
|
|
197
232
|
},
|
|
198
233
|
onSubscriptionChange: (nextIsSubscribed: boolean) => {
|
|
199
|
-
|
|
200
|
-
|
|
234
|
+
runOrQueueForActiveInstance(() => {
|
|
235
|
+
setIsSubscribed(nextIsSubscribed)
|
|
236
|
+
})
|
|
201
237
|
},
|
|
202
238
|
onConnectionStatusChange: (nextStatus: ConnectionStatus) => {
|
|
203
|
-
|
|
204
|
-
|
|
239
|
+
runOrQueueForActiveInstance(() => {
|
|
240
|
+
setConnectionStatus(nextStatus)
|
|
241
|
+
})
|
|
205
242
|
},
|
|
206
243
|
onSessionGeneratingChange: (isGenerating: boolean) => {
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
215
|
-
|
|
252
|
+
runOrQueueForActiveInstance(() => {
|
|
253
|
+
setQueue(nextQueue)
|
|
254
|
+
})
|
|
216
255
|
},
|
|
217
256
|
onRunIdChange: (nextRunId) => {
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
runOrQueueForActiveInstance(() => {
|
|
258
|
+
setRunId(nextRunId)
|
|
259
|
+
})
|
|
220
260
|
},
|
|
221
261
|
onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
262
|
+
runOrQueueForActiveInstance(() => {
|
|
263
|
+
setInterruptState((current) => ({
|
|
264
|
+
...current,
|
|
265
|
+
interrupts: nextPendingInterrupts,
|
|
266
|
+
pendingInterrupts: nextPendingInterrupts,
|
|
267
|
+
}))
|
|
268
|
+
})
|
|
228
269
|
},
|
|
229
|
-
onInterruptStateChange: (nextInterruptState) => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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:
|
|
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)
|