@tanstack/ai-preact 0.12.4 → 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/dist/esm/types.d.ts +8 -8
- package/dist/esm/use-chat.d.ts +2 -2
- package/dist/esm/use-chat.js +87 -52
- package/dist/esm/use-chat.js.map +1 -1
- package/package.json +4 -4
- package/src/types.ts +15 -6
- package/src/use-chat.ts +116 -59
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,7 +48,19 @@ 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,
|
|
@@ -72,89 +84,113 @@ function useChat(options) {
|
|
|
72
84
|
return optionsRef.current.onResponse?.(response);
|
|
73
85
|
},
|
|
74
86
|
onChunk: (chunk) => {
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
runOrQueueForActiveInstance(() => {
|
|
88
|
+
optionsRef.current.onChunk?.(chunk);
|
|
89
|
+
});
|
|
77
90
|
},
|
|
78
91
|
onFinish: (message) => {
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
runOrQueueForActiveInstance(() => {
|
|
93
|
+
optionsRef.current.onFinish?.(message);
|
|
94
|
+
});
|
|
81
95
|
},
|
|
82
96
|
onError: (err) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (activeClientRef.current !== currentInstance) return;
|
|
89
|
-
optionsRef.current.onError?.(err);
|
|
97
|
+
runOrQueueForActiveInstance(() => {
|
|
98
|
+
optionsRef.current.onError?.(err);
|
|
99
|
+
});
|
|
90
100
|
},
|
|
91
101
|
onCustomEvent: (eventType, data, context) => {
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
runOrQueueForActiveInstance(() => {
|
|
103
|
+
optionsRef.current.onCustomEvent?.(eventType, data, context);
|
|
104
|
+
});
|
|
94
105
|
},
|
|
95
106
|
...initialOptions.tools !== void 0 && { tools: initialOptions.tools },
|
|
107
|
+
...initialOptions.interrupts !== void 0 && { interrupts: initialOptions.interrupts },
|
|
96
108
|
...options.streamProcessor !== void 0 && { streamProcessor: options.streamProcessor },
|
|
97
109
|
onMessagesChange: (newMessages) => {
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
runOrQueueForActiveInstance(() => {
|
|
111
|
+
setMessages(newMessages);
|
|
112
|
+
});
|
|
100
113
|
},
|
|
101
114
|
onLoadingChange: (newIsLoading) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
runOrQueueForActiveInstance(() => {
|
|
116
|
+
const currentInstance = getActiveInstance();
|
|
117
|
+
if (!currentInstance) return;
|
|
118
|
+
setIsLoading(newIsLoading);
|
|
119
|
+
syncResumeState(currentInstance);
|
|
120
|
+
});
|
|
106
121
|
},
|
|
107
122
|
onStatusChange: (newStatus) => {
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
runOrQueueForActiveInstance(() => {
|
|
124
|
+
setStatus(newStatus);
|
|
125
|
+
});
|
|
110
126
|
},
|
|
111
127
|
onErrorChange: (newError) => {
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
runOrQueueForActiveInstance(() => {
|
|
129
|
+
setError(newError);
|
|
130
|
+
});
|
|
114
131
|
},
|
|
115
132
|
onSubscriptionChange: (nextIsSubscribed) => {
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
runOrQueueForActiveInstance(() => {
|
|
134
|
+
setIsSubscribed(nextIsSubscribed);
|
|
135
|
+
});
|
|
118
136
|
},
|
|
119
137
|
onConnectionStatusChange: (nextStatus) => {
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
runOrQueueForActiveInstance(() => {
|
|
139
|
+
setConnectionStatus(nextStatus);
|
|
140
|
+
});
|
|
122
141
|
},
|
|
123
142
|
onSessionGeneratingChange: (isGenerating) => {
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
runOrQueueForActiveInstance(() => {
|
|
144
|
+
setSessionGenerating(isGenerating);
|
|
145
|
+
});
|
|
126
146
|
},
|
|
127
147
|
...optionsRef.current.queue !== void 0 && { queue: optionsRef.current.queue },
|
|
128
148
|
onQueueChange: (nextQueue) => {
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
runOrQueueForActiveInstance(() => {
|
|
150
|
+
setQueue(nextQueue);
|
|
151
|
+
});
|
|
131
152
|
},
|
|
132
153
|
onRunIdChange: (nextRunId) => {
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
runOrQueueForActiveInstance(() => {
|
|
155
|
+
setRunId(nextRunId);
|
|
156
|
+
});
|
|
135
157
|
},
|
|
136
158
|
onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
runOrQueueForActiveInstance(() => {
|
|
160
|
+
setInterruptState((current) => ({
|
|
161
|
+
...current,
|
|
162
|
+
interrupts: nextPendingInterrupts,
|
|
163
|
+
pendingInterrupts: nextPendingInterrupts
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
143
166
|
},
|
|
144
|
-
onInterruptStateChange: (nextInterruptState) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
onInterruptStateChange: (nextInterruptState, context) => {
|
|
168
|
+
runOrQueueForActiveInstance(() => {
|
|
169
|
+
setInterruptState(nextInterruptState);
|
|
170
|
+
optionsRef.current.onInterruptStateChange?.(nextInterruptState, context);
|
|
171
|
+
});
|
|
148
172
|
}
|
|
149
173
|
});
|
|
150
174
|
instanceHolder.current = instance;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
return instance;
|
|
175
|
+
return {
|
|
176
|
+
client: instance,
|
|
177
|
+
initialization: initializationState
|
|
178
|
+
};
|
|
157
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]);
|
|
158
194
|
useEffect(() => {
|
|
159
195
|
const clientMessages = client.getMessages();
|
|
160
196
|
if (clientMessages !== messagesRef.current) setMessages(clientMessages);
|
|
@@ -192,7 +228,6 @@ function useChat(options) {
|
|
|
192
228
|
clearTimeout(cleanupInvalidationRef.current);
|
|
193
229
|
cleanupInvalidationRef.current = null;
|
|
194
230
|
}
|
|
195
|
-
activeClientRef.current = client;
|
|
196
231
|
client.mountDevtools();
|
|
197
232
|
syncResumeState(client);
|
|
198
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`. 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>\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 ...(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 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.
|
|
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'
|
|
@@ -88,6 +91,8 @@ export type UseChatOptions<
|
|
|
88
91
|
|
|
89
92
|
export interface UseChatReturn<
|
|
90
93
|
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
94
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
|
|
95
|
+
readonly [],
|
|
91
96
|
> {
|
|
92
97
|
/**
|
|
93
98
|
* Current messages in the conversation
|
|
@@ -149,14 +154,18 @@ export interface UseChatReturn<
|
|
|
149
154
|
* it, correlate a log line).
|
|
150
155
|
*/
|
|
151
156
|
runId: string | null
|
|
152
|
-
interrupts: BoundInterrupts<TTools>
|
|
157
|
+
interrupts: BoundInterrupts<TTools, TInterrupts>
|
|
153
158
|
/** @deprecated Use `interrupts`. */
|
|
154
|
-
pendingInterrupts: BoundInterrupts<TTools>
|
|
155
|
-
interruptErrors: ChatInterruptState<TTools>['interruptErrors']
|
|
159
|
+
pendingInterrupts: BoundInterrupts<TTools, TInterrupts>
|
|
160
|
+
interruptErrors: ChatInterruptState<TTools, TInterrupts>['interruptErrors']
|
|
156
161
|
resuming: boolean
|
|
157
162
|
resolveInterrupts: {
|
|
158
163
|
(approved: boolean): void
|
|
159
|
-
(
|
|
164
|
+
(
|
|
165
|
+
resolver: (
|
|
166
|
+
interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
|
|
167
|
+
) => undefined,
|
|
168
|
+
): void
|
|
160
169
|
}
|
|
161
170
|
cancelInterrupts: () => void
|
|
162
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,7 +38,12 @@ 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
|
+
const TInterrupts extends ReadonlyArray<
|
|
42
|
+
InterruptDefinition<any, any, any, any>
|
|
43
|
+
> = readonly [],
|
|
44
|
+
>(
|
|
45
|
+
options: UseChatOptions<TTools, TContext, TInterrupts>,
|
|
46
|
+
): UseChatReturn<TTools, TInterrupts> {
|
|
41
47
|
// The hook's identity is its `threadId`. Reload with the same `threadId`
|
|
42
48
|
// restores the same conversation. `hookId` is only a recreation key when no
|
|
43
49
|
// `threadId` is given. It is never sent on the wire.
|
|
@@ -57,7 +63,7 @@ export function useChat<
|
|
|
57
63
|
const [queue, setQueue] = useState<Array<QueuedMessage>>([])
|
|
58
64
|
const [runId, setRunId] = useState<string | null>(null)
|
|
59
65
|
const [interruptState, setInterruptState] = useState<
|
|
60
|
-
ChatInterruptState<TTools>
|
|
66
|
+
ChatInterruptState<TTools, TInterrupts>
|
|
61
67
|
>(() => ({
|
|
62
68
|
interrupts: EMPTY_INTERRUPTS,
|
|
63
69
|
pendingInterrupts: EMPTY_INTERRUPTS,
|
|
@@ -78,7 +84,8 @@ export function useChat<
|
|
|
78
84
|
client: ChatClient
|
|
79
85
|
timeout: ReturnType<typeof setTimeout>
|
|
80
86
|
} | null>(null)
|
|
81
|
-
const optionsRef =
|
|
87
|
+
const optionsRef =
|
|
88
|
+
useRef<UseChatOptions<TTools, TContext, TInterrupts>>(options)
|
|
82
89
|
|
|
83
90
|
optionsRef.current = options
|
|
84
91
|
|
|
@@ -92,7 +99,7 @@ export function useChat<
|
|
|
92
99
|
messagesRef.current = messages
|
|
93
100
|
}, [messages])
|
|
94
101
|
|
|
95
|
-
const client = useMemo(() => {
|
|
102
|
+
const { client, initialization } = useMemo(() => {
|
|
96
103
|
const messagesToUse = options.initialMessages || []
|
|
97
104
|
isFirstMountRef.current = false
|
|
98
105
|
|
|
@@ -106,7 +113,7 @@ export function useChat<
|
|
|
106
113
|
: { fetcher: initialOptions.fetcher }
|
|
107
114
|
|
|
108
115
|
const instanceHolder: {
|
|
109
|
-
current: ChatClient<TTools, TContext> | undefined
|
|
116
|
+
current: ChatClient<TTools, TContext, TInterrupts> | undefined
|
|
110
117
|
} = { current: undefined }
|
|
111
118
|
const getActiveInstance = () => {
|
|
112
119
|
const currentInstance = instanceHolder.current
|
|
@@ -115,8 +122,25 @@ export function useChat<
|
|
|
115
122
|
}
|
|
116
123
|
return currentInstance
|
|
117
124
|
}
|
|
118
|
-
|
|
119
|
-
|
|
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>({
|
|
120
144
|
devtoolsBridgeFactory: createChatDevtoolsBridge,
|
|
121
145
|
...transport,
|
|
122
146
|
initialMessages: messagesToUse,
|
|
@@ -155,96 +179,126 @@ export function useChat<
|
|
|
155
179
|
return optionsRef.current.onResponse?.(response)
|
|
156
180
|
},
|
|
157
181
|
onChunk: (chunk) => {
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
runOrQueueForActiveInstance(() => {
|
|
183
|
+
optionsRef.current.onChunk?.(chunk)
|
|
184
|
+
})
|
|
160
185
|
},
|
|
161
186
|
onFinish: (message) => {
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
runOrQueueForActiveInstance(() => {
|
|
188
|
+
optionsRef.current.onFinish?.(message)
|
|
189
|
+
})
|
|
164
190
|
},
|
|
165
191
|
onError: (err) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
if (activeClientRef.current !== currentInstance) return
|
|
172
|
-
optionsRef.current.onError?.(err)
|
|
192
|
+
runOrQueueForActiveInstance(() => {
|
|
193
|
+
optionsRef.current.onError?.(err)
|
|
194
|
+
})
|
|
173
195
|
},
|
|
174
196
|
onCustomEvent: (eventType, data, context) => {
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
runOrQueueForActiveInstance(() => {
|
|
198
|
+
optionsRef.current.onCustomEvent?.(eventType, data, context)
|
|
199
|
+
})
|
|
177
200
|
},
|
|
178
201
|
...(initialOptions.tools !== undefined && {
|
|
179
202
|
tools: initialOptions.tools,
|
|
180
203
|
}),
|
|
204
|
+
...(initialOptions.interrupts !== undefined && {
|
|
205
|
+
interrupts: initialOptions.interrupts,
|
|
206
|
+
}),
|
|
181
207
|
...(options.streamProcessor !== undefined && {
|
|
182
208
|
streamProcessor: options.streamProcessor,
|
|
183
209
|
}),
|
|
184
210
|
onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
|
|
185
|
-
|
|
186
|
-
|
|
211
|
+
runOrQueueForActiveInstance(() => {
|
|
212
|
+
setMessages(newMessages)
|
|
213
|
+
})
|
|
187
214
|
},
|
|
188
215
|
onLoadingChange: (newIsLoading: boolean) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
runOrQueueForActiveInstance(() => {
|
|
217
|
+
const currentInstance = getActiveInstance()
|
|
218
|
+
if (!currentInstance) return
|
|
219
|
+
setIsLoading(newIsLoading)
|
|
220
|
+
syncResumeState(currentInstance)
|
|
221
|
+
})
|
|
193
222
|
},
|
|
194
223
|
onStatusChange: (newStatus: ChatClientState) => {
|
|
195
|
-
|
|
196
|
-
|
|
224
|
+
runOrQueueForActiveInstance(() => {
|
|
225
|
+
setStatus(newStatus)
|
|
226
|
+
})
|
|
197
227
|
},
|
|
198
228
|
onErrorChange: (newError: Error | undefined) => {
|
|
199
|
-
|
|
200
|
-
|
|
229
|
+
runOrQueueForActiveInstance(() => {
|
|
230
|
+
setError(newError)
|
|
231
|
+
})
|
|
201
232
|
},
|
|
202
233
|
onSubscriptionChange: (nextIsSubscribed: boolean) => {
|
|
203
|
-
|
|
204
|
-
|
|
234
|
+
runOrQueueForActiveInstance(() => {
|
|
235
|
+
setIsSubscribed(nextIsSubscribed)
|
|
236
|
+
})
|
|
205
237
|
},
|
|
206
238
|
onConnectionStatusChange: (nextStatus: ConnectionStatus) => {
|
|
207
|
-
|
|
208
|
-
|
|
239
|
+
runOrQueueForActiveInstance(() => {
|
|
240
|
+
setConnectionStatus(nextStatus)
|
|
241
|
+
})
|
|
209
242
|
},
|
|
210
243
|
onSessionGeneratingChange: (isGenerating: boolean) => {
|
|
211
|
-
|
|
212
|
-
|
|
244
|
+
runOrQueueForActiveInstance(() => {
|
|
245
|
+
setSessionGenerating(isGenerating)
|
|
246
|
+
})
|
|
213
247
|
},
|
|
214
248
|
...(optionsRef.current.queue !== undefined && {
|
|
215
249
|
queue: optionsRef.current.queue,
|
|
216
250
|
}),
|
|
217
251
|
onQueueChange: (nextQueue: Array<QueuedMessage>) => {
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
runOrQueueForActiveInstance(() => {
|
|
253
|
+
setQueue(nextQueue)
|
|
254
|
+
})
|
|
220
255
|
},
|
|
221
256
|
onRunIdChange: (nextRunId) => {
|
|
222
|
-
|
|
223
|
-
|
|
257
|
+
runOrQueueForActiveInstance(() => {
|
|
258
|
+
setRunId(nextRunId)
|
|
259
|
+
})
|
|
224
260
|
},
|
|
225
261
|
onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
262
|
+
runOrQueueForActiveInstance(() => {
|
|
263
|
+
setInterruptState((current) => ({
|
|
264
|
+
...current,
|
|
265
|
+
interrupts: nextPendingInterrupts,
|
|
266
|
+
pendingInterrupts: nextPendingInterrupts,
|
|
267
|
+
}))
|
|
268
|
+
})
|
|
232
269
|
},
|
|
233
|
-
onInterruptStateChange: (nextInterruptState) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
270
|
+
onInterruptStateChange: (nextInterruptState, context) => {
|
|
271
|
+
runOrQueueForActiveInstance(() => {
|
|
272
|
+
setInterruptState(nextInterruptState)
|
|
273
|
+
optionsRef.current.onInterruptStateChange?.(
|
|
274
|
+
nextInterruptState,
|
|
275
|
+
context,
|
|
276
|
+
)
|
|
277
|
+
})
|
|
237
278
|
},
|
|
238
279
|
})
|
|
239
280
|
instanceHolder.current = instance
|
|
240
|
-
|
|
241
|
-
for (const initializationError of pendingInitializationErrors) {
|
|
242
|
-
if (activeClientRef.current !== instance) break
|
|
243
|
-
optionsRef.current.onError?.(initializationError)
|
|
244
|
-
}
|
|
245
|
-
return instance
|
|
281
|
+
return { client: instance, initialization: initializationState }
|
|
246
282
|
}, [clientId, syncResumeState])
|
|
247
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
|
+
|
|
248
302
|
useEffect(() => {
|
|
249
303
|
const clientMessages = client.getMessages()
|
|
250
304
|
if (clientMessages !== messagesRef.current) {
|
|
@@ -311,7 +365,6 @@ export function useChat<
|
|
|
311
365
|
clearTimeout(cleanupInvalidationRef.current)
|
|
312
366
|
cleanupInvalidationRef.current = null
|
|
313
367
|
}
|
|
314
|
-
activeClientRef.current = client
|
|
315
368
|
client.mountDevtools()
|
|
316
369
|
// Delivery-durability resume is transparent: the resumable SSE connection
|
|
317
370
|
// adapter reattaches via the browser's native Last-Event-ID on reconnect.
|
|
@@ -437,7 +490,11 @@ export function useChat<
|
|
|
437
490
|
|
|
438
491
|
const resolveInterrupts = useCallback(
|
|
439
492
|
(
|
|
440
|
-
resolution:
|
|
493
|
+
resolution:
|
|
494
|
+
| boolean
|
|
495
|
+
| ((
|
|
496
|
+
interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
|
|
497
|
+
) => undefined),
|
|
441
498
|
) => {
|
|
442
499
|
if (typeof resolution === 'boolean') {
|
|
443
500
|
client.resolveInterrupts(resolution)
|