@tanstack/ai-preact 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -202,19 +202,20 @@ Learn more in the
202
202
 
203
203
  Official adapters include:
204
204
 
205
- | Package | Use it for |
206
- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
207
- | [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
208
- | [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
209
- | [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude |
210
- | [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
211
- | [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials |
212
- | [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
213
- | [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
214
- | [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
215
- | [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
216
- | [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
217
- | [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
205
+ | Package | Use it for |
206
+ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
207
+ | [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
208
+ | [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
209
+ | [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude |
210
+ | [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
211
+ | [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials |
212
+ | [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
213
+ | [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
214
+ | [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
215
+ | [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
216
+ | [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
217
+ | [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
218
+ | [`@tanstack/ai-cloudflare`](https://tanstack.com/ai/latest/docs/adapters/cloudflare) | Cloudflare Workers AI chat, embeddings, image, speech, transcription, and AI Gateway |
218
219
 
219
220
  The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
220
221
  `openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so
@@ -108,6 +108,14 @@ export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any
108
108
  * Whether a response is currently being generated
109
109
  */
110
110
  isLoading: boolean;
111
+ /**
112
+ * True when the last hydrate or older-page response said more messages exist.
113
+ */
114
+ hasOlderMessages: boolean;
115
+ /**
116
+ * Fetch the next older window and put it in front of the painted messages.
117
+ */
118
+ loadOlderMessages: () => Promise<void>;
111
119
  /**
112
120
  * Current error, if any
113
121
  */
@@ -9,6 +9,7 @@ function useChat(options) {
9
9
  const clientId = options.threadId ?? hookId;
10
10
  const [messages, setMessages] = useState(options.initialMessages || []);
11
11
  const [isLoading, setIsLoading] = useState(false);
12
+ const [hasOlderMessages, setHasOlderMessages] = useState(false);
12
13
  const [error, setError] = useState(void 0);
13
14
  const [status, setStatus] = useState("ready");
14
15
  const [isSubscribed, setIsSubscribed] = useState(false);
@@ -66,7 +67,11 @@ function useChat(options) {
66
67
  ...transport,
67
68
  initialMessages: messagesToUse,
68
69
  ...initialOptions.body !== void 0 && { body: initialOptions.body },
69
- ...typeof initialOptions.threadId === "string" && initialOptions.persistence ? {
70
+ ...typeof initialOptions.threadId === "string" && initialOptions.persistence === true ? {
71
+ persistence: true,
72
+ threadId: initialOptions.threadId,
73
+ ...initialOptions.history !== void 0 && { history: initialOptions.history }
74
+ } : typeof initialOptions.threadId === "string" && initialOptions.persistence ? {
70
75
  persistence: initialOptions.persistence,
71
76
  threadId: initialOptions.threadId
72
77
  } : { ...initialOptions.threadId !== void 0 && { threadId: initialOptions.threadId } },
@@ -111,6 +116,9 @@ function useChat(options) {
111
116
  onMessagesChange: (newMessages) => {
112
117
  runOrQueueForActiveInstance(() => {
113
118
  setMessages(newMessages);
119
+ const currentInstance = getActiveInstance();
120
+ if (!currentInstance) return;
121
+ setHasOlderMessages(currentInstance.getHasOlderMessages());
114
122
  });
115
123
  },
116
124
  onLoadingChange: (newIsLoading) => {
@@ -196,6 +204,7 @@ function useChat(options) {
196
204
  useEffect(() => {
197
205
  const clientMessages = client.getMessages();
198
206
  if (clientMessages !== messagesRef.current) setMessages(clientMessages);
207
+ setHasOlderMessages(client.getHasOlderMessages());
199
208
  }, [client]);
200
209
  useEffect(() => {
201
210
  client.updateOptions({
@@ -273,6 +282,10 @@ function useChat(options) {
273
282
  syncResumeState(client);
274
283
  }
275
284
  }, [client, syncResumeState]);
285
+ const loadOlderMessages = useCallback(async () => {
286
+ await client.loadOlderMessages();
287
+ setHasOlderMessages(client.getHasOlderMessages());
288
+ }, [client]);
276
289
  const stop = useCallback(() => {
277
290
  client.stop();
278
291
  }, [client]);
@@ -313,6 +326,8 @@ function useChat(options) {
313
326
  reload,
314
327
  stop,
315
328
  isLoading,
329
+ hasOlderMessages,
330
+ loadOlderMessages,
316
331
  error,
317
332
  status,
318
333
  isSubscribed,
@@ -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 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.byok !== undefined && { byok: initialOptions.byok }),\n byokProvider: () => optionsRef.current.byokProvider?.(),\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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"}
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 [hasOlderMessages, setHasOlderMessages] = 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 === true\n ? {\n persistence: true,\n threadId: initialOptions.threadId,\n ...(initialOptions.history !== undefined && {\n history: initialOptions.history,\n }),\n }\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.byok !== undefined && { byok: initialOptions.byok }),\n byokProvider: () => optionsRef.current.byokProvider?.(),\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 const currentInstance = getActiveInstance()\n if (!currentInstance) return\n setHasOlderMessages(currentInstance.getHasOlderMessages())\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 setHasOlderMessages(client.getHasOlderMessages())\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 loadOlderMessages = useCallback(async () => {\n await client.loadOlderMessages()\n setHasOlderMessages(client.getHasOlderMessages())\n }, [client])\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 hasOlderMessages,\n loadOlderMessages,\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.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Preact hooks for TanStack AI streaming chat and typed messages.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "tool-calling"
51
51
  ],
52
52
  "dependencies": {
53
- "@tanstack/ai-client": "^0.31.0"
53
+ "@tanstack/ai-client": "^0.32.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@mcp-ui/client": "^7.1.1",
@@ -59,12 +59,12 @@
59
59
  "jsdom": "^27.4.0",
60
60
  "preact": "^10.26.9",
61
61
  "vite": "^8.2.1",
62
- "@tanstack/ai": "0.53.0"
62
+ "@tanstack/ai": "0.55.0"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@mcp-ui/client": "^7",
66
66
  "preact": ">=10.11.0",
67
- "@tanstack/ai": "^0.53.0"
67
+ "@tanstack/ai": "^0.55.0"
68
68
  },
69
69
  "peerDependenciesMeta": {
70
70
  "@mcp-ui/client": {
package/src/types.ts CHANGED
@@ -196,6 +196,16 @@ export interface UseChatReturn<
196
196
  */
197
197
  isLoading: boolean
198
198
 
199
+ /**
200
+ * True when the last hydrate or older-page response said more messages exist.
201
+ */
202
+ hasOlderMessages: boolean
203
+
204
+ /**
205
+ * Fetch the next older window and put it in front of the painted messages.
206
+ */
207
+ loadOlderMessages: () => Promise<void>
208
+
199
209
  /**
200
210
  * Current error, if any
201
211
  */
package/src/use-chat.ts CHANGED
@@ -54,6 +54,7 @@ export function useChat<
54
54
  options.initialMessages || [],
55
55
  )
56
56
  const [isLoading, setIsLoading] = useState(false)
57
+ const [hasOlderMessages, setHasOlderMessages] = useState(false)
57
58
  const [error, setError] = useState<Error | undefined>(undefined)
58
59
  const [status, setStatus] = useState<ChatClientState>('ready')
59
60
  const [isSubscribed, setIsSubscribed] = useState(false)
@@ -146,16 +147,25 @@ export function useChat<
146
147
  initialMessages: messagesToUse,
147
148
  ...(initialOptions.body !== undefined && { body: initialOptions.body }),
148
149
  ...(typeof initialOptions.threadId === 'string' &&
149
- initialOptions.persistence
150
+ initialOptions.persistence === true
150
151
  ? {
151
- persistence: initialOptions.persistence,
152
+ persistence: true,
152
153
  threadId: initialOptions.threadId,
154
+ ...(initialOptions.history !== undefined && {
155
+ history: initialOptions.history,
156
+ }),
153
157
  }
154
- : {
155
- ...(initialOptions.threadId !== undefined && {
158
+ : typeof initialOptions.threadId === 'string' &&
159
+ initialOptions.persistence
160
+ ? {
161
+ persistence: initialOptions.persistence,
156
162
  threadId: initialOptions.threadId,
163
+ }
164
+ : {
165
+ ...(initialOptions.threadId !== undefined && {
166
+ threadId: initialOptions.threadId,
167
+ }),
157
168
  }),
158
- }),
159
169
  ...(initialOptions.forwardedProps !== undefined && {
160
170
  forwardedProps: initialOptions.forwardedProps,
161
171
  }),
@@ -212,6 +222,9 @@ export function useChat<
212
222
  onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
213
223
  runOrQueueForActiveInstance(() => {
214
224
  setMessages(newMessages)
225
+ const currentInstance = getActiveInstance()
226
+ if (!currentInstance) return
227
+ setHasOlderMessages(currentInstance.getHasOlderMessages())
215
228
  })
216
229
  },
217
230
  onLoadingChange: (newIsLoading: boolean) => {
@@ -306,6 +319,7 @@ export function useChat<
306
319
  if (clientMessages !== messagesRef.current) {
307
320
  setMessages(clientMessages)
308
321
  }
322
+ setHasOlderMessages(client.getHasOlderMessages())
309
323
  }, [client])
310
324
 
311
325
  // Sync body / forwardedProps changes to the client.
@@ -444,6 +458,11 @@ export function useChat<
444
458
  }
445
459
  }, [client, syncResumeState])
446
460
 
461
+ const loadOlderMessages = useCallback(async () => {
462
+ await client.loadOlderMessages()
463
+ setHasOlderMessages(client.getHasOlderMessages())
464
+ }, [client])
465
+
447
466
  const stop = useCallback(() => {
448
467
  client.stop()
449
468
  }, [client])
@@ -530,6 +549,8 @@ export function useChat<
530
549
  reload,
531
550
  stop,
532
551
  isLoading,
552
+ hasOlderMessages,
553
+ loadOlderMessages,
533
554
  error,
534
555
  status,
535
556
  isSubscribed,