@tanstack/ai-preact 0.18.4 → 0.19.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/index.d.ts +3 -3
- package/dist/esm/index.js +3 -3
- package/dist/esm/use-chat.js +3 -0
- package/dist/esm/use-chat.js.map +1 -1
- package/dist/esm/use-web-mcp-tools.d.ts +34 -7
- package/dist/esm/use-web-mcp-tools.js +42 -5
- package/dist/esm/use-web-mcp-tools.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +15 -2
- package/src/use-chat.ts +6 -0
- package/src/use-web-mcp-tools.ts +69 -10
package/dist/esm/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { createChatHook } from './create-chat-hook.js';
|
|
|
3
3
|
export { useByok } from './use-byok.js';
|
|
4
4
|
export { useMcpAppBridge } from './use-mcp-app-bridge.js';
|
|
5
5
|
export type { UseMcpAppBridgeOptions } from './use-mcp-app-bridge.js';
|
|
6
|
-
export { useWebMCPTools } from './use-web-mcp-tools.js';
|
|
7
|
-
export type { UseWebMCPToolsOptions } from './use-web-mcp-tools.js';
|
|
6
|
+
export { usePageWebMCPTools, useRegisterWebMCPTools, useWebMCPTools, } from './use-web-mcp-tools.js';
|
|
7
|
+
export type { UsePageWebMCPToolsOptions, UseRegisterWebMCPToolsOptions, UseWebMCPToolsOptions, } from './use-web-mcp-tools.js';
|
|
8
8
|
export type { UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, QueuedMessage, SendMessageOptions, WhenBusy, QueueConfig, QueueStrategy, QueueOption, } from './types.js';
|
|
9
|
-
export { AudioRecorder, ChatClient, InterruptManager, RealtimeClient, GenerationClient, VideoGenerationClient, GENERATION_EVENTS, reconstructImageResult, reconstructAudioResult, reconstructSpeechResult, reconstructTranscriptionResult, reconstructSummarizeResult, UnsupportedResponseStreamError, createAIDevtoolsGenerationPreview, StreamTruncatedError, DurableStreamIncompleteError, StreamReconnectLimitError, uiMessageToModelMessages, modelMessageToUIMessage, modelMessagesToUIMessages, convertMessagesToModelMessages, normalizeToUIMessage, generateMessageId, StreamProcessor, ImmediateStrategy, PunctuationStrategy, BatchStrategy, WordBoundaryStrategy, CompositeStrategy, parsePartialJSON, PartialJSONParser, defaultJSONParser, registerWebMCPTools, clientTools, webSocket, fetchServerSentEvents, localStoragePersistence, sessionStoragePersistence, indexedDBPersistence, StorageUnavailableError, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatStorageAdapter, type WebStoragePersistenceOptions, type IndexedDBPersistenceOptions, fetchHttpStream, xhrServerSentEvents, xhrHttpStream, stream, rpcStream, createChatClientOptions, createMcpAppBridge, type McpAppBridge, type CreateMcpAppBridgeOptions, type ConnectionAdapter, type ConnectConnectionAdapter, type SubscribeConnectionAdapter, type RunAgentInputContext, type FetchConnectionOptions, type XhrConnectionOptions, type InferChatMessages, } from '@tanstack/ai-client';
|
|
9
|
+
export { AudioRecorder, ChatClient, InterruptManager, RealtimeClient, GenerationClient, VideoGenerationClient, GENERATION_EVENTS, reconstructImageResult, reconstructAudioResult, reconstructSpeechResult, reconstructTranscriptionResult, reconstructSummarizeResult, UnsupportedResponseStreamError, createAIDevtoolsGenerationPreview, StreamTruncatedError, DurableStreamIncompleteError, StreamReconnectLimitError, uiMessageToModelMessages, modelMessageToUIMessage, modelMessagesToUIMessages, convertMessagesToModelMessages, normalizeToUIMessage, generateMessageId, StreamProcessor, ImmediateStrategy, PunctuationStrategy, BatchStrategy, WordBoundaryStrategy, CompositeStrategy, parsePartialJSON, PartialJSONParser, defaultJSONParser, registerWebMCPTools, getWebMCPTools, subscribeWebMCPTools, type GetWebMCPToolsOptions, type SubscribeWebMCPToolsOptions, type WebMCPPageTool, clientTools, webSocket, fetchServerSentEvents, localStoragePersistence, sessionStoragePersistence, indexedDBPersistence, StorageUnavailableError, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatStorageAdapter, type WebStoragePersistenceOptions, type IndexedDBPersistenceOptions, fetchHttpStream, xhrServerSentEvents, xhrHttpStream, stream, rpcStream, createChatClientOptions, createMcpAppBridge, type McpAppBridge, type CreateMcpAppBridgeOptions, type ConnectionAdapter, type ConnectConnectionAdapter, type SubscribeConnectionAdapter, type RunAgentInputContext, type FetchConnectionOptions, type XhrConnectionOptions, type InferChatMessages, } from '@tanstack/ai-client';
|
package/dist/esm/index.js
CHANGED
|
@@ -2,6 +2,6 @@ import { useChat } from "./use-chat.js";
|
|
|
2
2
|
import { createChatHook } from "./create-chat-hook.js";
|
|
3
3
|
import { useByok } from "./use-byok.js";
|
|
4
4
|
import { useMcpAppBridge } from "./use-mcp-app-bridge.js";
|
|
5
|
-
import { useWebMCPTools } from "./use-web-mcp-tools.js";
|
|
6
|
-
import { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
|
|
7
|
-
export { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createChatHook, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, useByok, useChat, useMcpAppBridge, useWebMCPTools, webSocket, xhrHttpStream, xhrServerSentEvents };
|
|
5
|
+
import { usePageWebMCPTools, useRegisterWebMCPTools, useWebMCPTools } from "./use-web-mcp-tools.js";
|
|
6
|
+
import { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, getWebMCPTools, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, subscribeWebMCPTools, uiMessageToModelMessages, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
|
|
7
|
+
export { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createChatHook, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, getWebMCPTools, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, subscribeWebMCPTools, uiMessageToModelMessages, useByok, useChat, useMcpAppBridge, usePageWebMCPTools, useRegisterWebMCPTools, useWebMCPTools, webSocket, xhrHttpStream, xhrServerSentEvents };
|
package/dist/esm/use-chat.js
CHANGED
|
@@ -220,6 +220,9 @@ function useChat(options) {
|
|
|
220
220
|
options.context,
|
|
221
221
|
options.queue
|
|
222
222
|
]);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (options.tools !== void 0) client.updateOptions({ tools: options.tools });
|
|
225
|
+
}, [client, options.tools]);
|
|
223
226
|
useEffect(() => {
|
|
224
227
|
if (options.live) client.subscribe();
|
|
225
228
|
else client.unsubscribe();
|
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 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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"}
|
|
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.tools !== undefined) {\n client.updateOptions({ tools: options.tools })\n }\n }, [client, options.tools])\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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"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { AnyClientTool, InferredClientContext, RegisterWebMCPToolsOptions } from '@tanstack/ai-client';
|
|
2
|
-
/** Options for the Preact {@link
|
|
3
|
-
export type
|
|
1
|
+
import { AnyClientTool, InferredClientContext, RegisterWebMCPToolsOptions, SubscribeWebMCPToolsOptions } from '@tanstack/ai-client';
|
|
2
|
+
/** Options for the Preact {@link useRegisterWebMCPTools} lifecycle hook. */
|
|
3
|
+
export type UseRegisterWebMCPToolsOptions<TTools extends ReadonlyArray<AnyClientTool>, TContext = InferredClientContext<TTools>> = Omit<RegisterWebMCPToolsOptions<TTools, TContext>, 'signal'> & {
|
|
4
4
|
/** Receives an asynchronous registration failure. */
|
|
5
5
|
onError?: (error: unknown) => void;
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
/** @deprecated Use `UseRegisterWebMCPToolsOptions`. Removed in 1.0.0. */
|
|
8
|
+
export type UseWebMCPToolsOptions<TTools extends ReadonlyArray<AnyClientTool>, TContext = InferredClientContext<TTools>> = UseRegisterWebMCPToolsOptions<TTools, TContext>;
|
|
9
|
+
type UseRegisterWebMCPToolsArguments<TTools extends ReadonlyArray<AnyClientTool>, TContext> = RegisterWebMCPToolsOptions<TTools, TContext> extends {
|
|
8
10
|
context: unknown;
|
|
9
|
-
} ? [options:
|
|
11
|
+
} ? [options: UseRegisterWebMCPToolsOptions<TTools, TContext>] : [options?: UseRegisterWebMCPToolsOptions<TTools, TContext>];
|
|
10
12
|
/**
|
|
11
13
|
* Registers client tools with WebMCP for the lifetime of a Preact component.
|
|
12
14
|
*
|
|
@@ -18,10 +20,35 @@ type UseWebMCPToolsArguments<TTools extends ReadonlyArray<AnyClientTool>, TConte
|
|
|
18
20
|
*
|
|
19
21
|
* @example
|
|
20
22
|
* ```tsx
|
|
21
|
-
*
|
|
23
|
+
* useRegisterWebMCPTools([searchProducts], {
|
|
22
24
|
* toolOptions: { searchProducts: { title: 'Search products' } },
|
|
23
25
|
* })
|
|
24
26
|
* ```
|
|
25
27
|
*/
|
|
26
|
-
export declare function
|
|
28
|
+
export declare function useRegisterWebMCPTools<const TTools extends ReadonlyArray<AnyClientTool>, TContext = InferredClientContext<TTools>>(tools: TTools, ...[options]: UseRegisterWebMCPToolsArguments<TTools, TContext>): void;
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Use `useRegisterWebMCPTools`. Removed in 1.0.0.
|
|
31
|
+
* @alias
|
|
32
|
+
*/
|
|
33
|
+
export declare const useWebMCPTools: typeof useRegisterWebMCPTools;
|
|
34
|
+
/** Options for {@link usePageWebMCPTools}. */
|
|
35
|
+
export type UsePageWebMCPToolsOptions = Omit<SubscribeWebMCPToolsOptions, 'signal'>;
|
|
36
|
+
/**
|
|
37
|
+
* Returns the WebMCP tools on the page as client tools for `useChat`.
|
|
38
|
+
*
|
|
39
|
+
* The list starts empty and updates when the page registers or removes a
|
|
40
|
+
* tool. The hook reads the latest `filter` and `onError` on each update.
|
|
41
|
+
* Unsupported browsers and server rendering return an empty array.
|
|
42
|
+
*
|
|
43
|
+
* @param options - A filter that skips tools, and an error callback.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* const pageTools = usePageWebMCPTools({
|
|
48
|
+
* filter: (tool) => tool.origin === location.origin,
|
|
49
|
+
* })
|
|
50
|
+
* const chat = useChat({ connection, tools: pageTools })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function usePageWebMCPTools(options?: UsePageWebMCPToolsOptions): Array<AnyClientTool>;
|
|
27
54
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { registerWebMCPTools } from "@tanstack/ai-client";
|
|
2
|
-
import { useEffect } from "preact/hooks";
|
|
1
|
+
import { registerWebMCPTools, subscribeWebMCPTools } from "@tanstack/ai-client";
|
|
2
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
3
3
|
//#region src/use-web-mcp-tools.ts
|
|
4
4
|
/**
|
|
5
5
|
* Registers client tools with WebMCP for the lifetime of a Preact component.
|
|
@@ -12,12 +12,12 @@ import { useEffect } from "preact/hooks";
|
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
14
|
* ```tsx
|
|
15
|
-
*
|
|
15
|
+
* useRegisterWebMCPTools([searchProducts], {
|
|
16
16
|
* toolOptions: { searchProducts: { title: 'Search products' } },
|
|
17
17
|
* })
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
function
|
|
20
|
+
function useRegisterWebMCPTools(tools, ...[options]) {
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
const controller = new AbortController();
|
|
23
23
|
const { onError, ...registrationOptions } = options ?? {};
|
|
@@ -30,7 +30,44 @@ function useWebMCPTools(tools, ...[options]) {
|
|
|
30
30
|
return () => controller.abort();
|
|
31
31
|
}, [tools, options]);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated Use `useRegisterWebMCPTools`. Removed in 1.0.0.
|
|
35
|
+
* @alias
|
|
36
|
+
*/
|
|
37
|
+
var useWebMCPTools = useRegisterWebMCPTools;
|
|
38
|
+
/**
|
|
39
|
+
* Returns the WebMCP tools on the page as client tools for `useChat`.
|
|
40
|
+
*
|
|
41
|
+
* The list starts empty and updates when the page registers or removes a
|
|
42
|
+
* tool. The hook reads the latest `filter` and `onError` on each update.
|
|
43
|
+
* Unsupported browsers and server rendering return an empty array.
|
|
44
|
+
*
|
|
45
|
+
* @param options - A filter that skips tools, and an error callback.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* const pageTools = usePageWebMCPTools({
|
|
50
|
+
* filter: (tool) => tool.origin === location.origin,
|
|
51
|
+
* })
|
|
52
|
+
* const chat = useChat({ connection, tools: pageTools })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
function usePageWebMCPTools(options) {
|
|
56
|
+
const [tools, setTools] = useState([]);
|
|
57
|
+
const optionsRef = useRef(options);
|
|
58
|
+
optionsRef.current = options;
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
subscribeWebMCPTools(setTools, {
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
filter: (tool) => optionsRef.current?.filter?.(tool) ?? true,
|
|
64
|
+
onError: (error) => optionsRef.current?.onError?.(error)
|
|
65
|
+
});
|
|
66
|
+
return () => controller.abort();
|
|
67
|
+
}, []);
|
|
68
|
+
return tools;
|
|
69
|
+
}
|
|
33
70
|
//#endregion
|
|
34
|
-
export { useWebMCPTools };
|
|
71
|
+
export { usePageWebMCPTools, useRegisterWebMCPTools, useWebMCPTools };
|
|
35
72
|
|
|
36
73
|
//# sourceMappingURL=use-web-mcp-tools.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-mcp-tools.js","names":[],"sources":["../../src/use-web-mcp-tools.ts"],"sourcesContent":["import { useEffect } from 'preact/hooks'\nimport { registerWebMCPTools } from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InferredClientContext,\n RegisterWebMCPToolsOptions,\n} from '@tanstack/ai-client'\n\n/** Options for the Preact {@link
|
|
1
|
+
{"version":3,"file":"use-web-mcp-tools.js","names":[],"sources":["../../src/use-web-mcp-tools.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'preact/hooks'\nimport { registerWebMCPTools, subscribeWebMCPTools } from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InferredClientContext,\n RegisterWebMCPToolsOptions,\n SubscribeWebMCPToolsOptions,\n} from '@tanstack/ai-client'\n\n/** Options for the Preact {@link useRegisterWebMCPTools} lifecycle hook. */\nexport type UseRegisterWebMCPToolsOptions<\n TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n> = Omit<RegisterWebMCPToolsOptions<TTools, TContext>, 'signal'> & {\n /** Receives an asynchronous registration failure. */\n onError?: (error: unknown) => void\n}\n\n/** @deprecated Use `UseRegisterWebMCPToolsOptions`. Removed in 1.0.0. */\nexport type UseWebMCPToolsOptions<\n TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n> = UseRegisterWebMCPToolsOptions<TTools, TContext>\n\ntype UseRegisterWebMCPToolsArguments<\n TTools extends ReadonlyArray<AnyClientTool>,\n TContext,\n> =\n RegisterWebMCPToolsOptions<TTools, TContext> extends { context: unknown }\n ? [options: UseRegisterWebMCPToolsOptions<TTools, TContext>]\n : [options?: UseRegisterWebMCPToolsOptions<TTools, TContext>]\n\n/**\n * Registers client tools with WebMCP for the lifetime of a Preact component.\n *\n * The hook replaces the registration when `tools` or `options` changes.\n * Unsupported browsers and server rendering do not register tools.\n *\n * @param tools - The executable client tools to expose through WebMCP.\n * @param options - Runtime context, per-tool options, and an error callback.\n *\n * @example\n * ```tsx\n * useRegisterWebMCPTools([searchProducts], {\n * toolOptions: { searchProducts: { title: 'Search products' } },\n * })\n * ```\n */\nexport function useRegisterWebMCPTools<\n const TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n>(\n tools: TTools,\n ...[options]: UseRegisterWebMCPToolsArguments<TTools, TContext>\n) {\n useEffect(() => {\n const controller = new AbortController()\n const { onError, ...registrationOptions } = options ?? {}\n\n registerWebMCPTools(tools, {\n ...registrationOptions,\n signal: controller.signal,\n }).catch((error) => {\n if (!controller.signal.aborted) onError?.(error)\n })\n\n return () => controller.abort()\n }, [tools, options])\n}\n\n/**\n * @deprecated Use `useRegisterWebMCPTools`. Removed in 1.0.0.\n * @alias\n */\nexport const useWebMCPTools = useRegisterWebMCPTools\n\n/** Options for {@link usePageWebMCPTools}. */\nexport type UsePageWebMCPToolsOptions = Omit<\n SubscribeWebMCPToolsOptions,\n 'signal'\n>\n\n/**\n * Returns the WebMCP tools on the page as client tools for `useChat`.\n *\n * The list starts empty and updates when the page registers or removes a\n * tool. The hook reads the latest `filter` and `onError` on each update.\n * Unsupported browsers and server rendering return an empty array.\n *\n * @param options - A filter that skips tools, and an error callback.\n *\n * @example\n * ```tsx\n * const pageTools = usePageWebMCPTools({\n * filter: (tool) => tool.origin === location.origin,\n * })\n * const chat = useChat({ connection, tools: pageTools })\n * ```\n */\nexport function usePageWebMCPTools(\n options?: UsePageWebMCPToolsOptions,\n): Array<AnyClientTool> {\n const [tools, setTools] = useState<Array<AnyClientTool>>([])\n const optionsRef = useRef(options)\n optionsRef.current = options\n\n useEffect(() => {\n const controller = new AbortController()\n subscribeWebMCPTools(setTools, {\n signal: controller.signal,\n filter: (tool) => optionsRef.current?.filter?.(tool) ?? true,\n onError: (error) => optionsRef.current?.onError?.(error),\n })\n return () => controller.abort()\n }, [])\n\n return tools\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,uBAId,OACA,GAAG,CAAC,UACJ;CACA,gBAAgB;EACd,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,EAAE,SAAS,GAAG,wBAAwB,WAAW,CAAC;EAExD,oBAAoB,OAAO;GACzB,GAAG;GACH,QAAQ,WAAW;EACrB,CAAC,CAAC,CAAC,OAAO,UAAU;GAClB,IAAI,CAAC,WAAW,OAAO,SAAS,UAAU,KAAK;EACjD,CAAC;EAED,aAAa,WAAW,MAAM;CAChC,GAAG,CAAC,OAAO,OAAO,CAAC;AACrB;;;;;AAMA,IAAa,iBAAiB;;;;;;;;;;;;;;;;;;AAyB9B,SAAgB,mBACd,SACsB;CACtB,MAAM,CAAC,OAAO,YAAY,SAA+B,CAAC,CAAC;CAC3D,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CAErB,gBAAgB;EACd,MAAM,aAAa,IAAI,gBAAgB;EACvC,qBAAqB,UAAU;GAC7B,QAAQ,WAAW;GACnB,SAAS,SAAS,WAAW,SAAS,SAAS,IAAI,KAAK;GACxD,UAAU,UAAU,WAAW,SAAS,UAAU,KAAK;EACzD,CAAC;EACD,aAAa,WAAW,MAAM;CAChC,GAAG,CAAC,CAAC;CAEL,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-preact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Preact hooks for TanStack AI streaming chat and typed messages.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"tool-calling"
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@tanstack/ai-client": "^0.
|
|
57
|
+
"@tanstack/ai-client": "^0.35.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@mcp-ui/client": "^7.1.1",
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
"jsdom": "^27.4.0",
|
|
64
64
|
"preact": "^10.26.9",
|
|
65
65
|
"vite": "^8.2.1",
|
|
66
|
-
"@tanstack/ai": "0.
|
|
66
|
+
"@tanstack/ai": "0.61.0"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@mcp-ui/client": "^7",
|
|
70
70
|
"preact": ">=10.11.0",
|
|
71
|
-
"@tanstack/ai": "^0.
|
|
71
|
+
"@tanstack/ai": "^0.61.0"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@mcp-ui/client": {
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,16 @@ export { createChatHook } from './create-chat-hook'
|
|
|
3
3
|
export { useByok } from './use-byok'
|
|
4
4
|
export { useMcpAppBridge } from './use-mcp-app-bridge'
|
|
5
5
|
export type { UseMcpAppBridgeOptions } from './use-mcp-app-bridge'
|
|
6
|
-
export {
|
|
7
|
-
|
|
6
|
+
export {
|
|
7
|
+
usePageWebMCPTools,
|
|
8
|
+
useRegisterWebMCPTools,
|
|
9
|
+
useWebMCPTools,
|
|
10
|
+
} from './use-web-mcp-tools'
|
|
11
|
+
export type {
|
|
12
|
+
UsePageWebMCPToolsOptions,
|
|
13
|
+
UseRegisterWebMCPToolsOptions,
|
|
14
|
+
UseWebMCPToolsOptions,
|
|
15
|
+
} from './use-web-mcp-tools'
|
|
8
16
|
export type {
|
|
9
17
|
UseChatOptions,
|
|
10
18
|
UseChatReturn,
|
|
@@ -52,6 +60,11 @@ export {
|
|
|
52
60
|
PartialJSONParser,
|
|
53
61
|
defaultJSONParser,
|
|
54
62
|
registerWebMCPTools,
|
|
63
|
+
getWebMCPTools,
|
|
64
|
+
subscribeWebMCPTools,
|
|
65
|
+
type GetWebMCPToolsOptions,
|
|
66
|
+
type SubscribeWebMCPToolsOptions,
|
|
67
|
+
type WebMCPPageTool,
|
|
55
68
|
clientTools,
|
|
56
69
|
webSocket,
|
|
57
70
|
fetchServerSentEvents,
|
package/src/use-chat.ts
CHANGED
|
@@ -344,6 +344,12 @@ export function useChat<
|
|
|
344
344
|
options.queue,
|
|
345
345
|
])
|
|
346
346
|
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
if (options.tools !== undefined) {
|
|
349
|
+
client.updateOptions({ tools: options.tools })
|
|
350
|
+
}
|
|
351
|
+
}, [client, options.tools])
|
|
352
|
+
|
|
347
353
|
useEffect(() => {
|
|
348
354
|
if (options.live) {
|
|
349
355
|
client.subscribe()
|
package/src/use-web-mcp-tools.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { useEffect } from 'preact/hooks'
|
|
2
|
-
import { registerWebMCPTools } from '@tanstack/ai-client'
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { registerWebMCPTools, subscribeWebMCPTools } from '@tanstack/ai-client'
|
|
3
3
|
import type {
|
|
4
4
|
AnyClientTool,
|
|
5
5
|
InferredClientContext,
|
|
6
6
|
RegisterWebMCPToolsOptions,
|
|
7
|
+
SubscribeWebMCPToolsOptions,
|
|
7
8
|
} from '@tanstack/ai-client'
|
|
8
9
|
|
|
9
|
-
/** Options for the Preact {@link
|
|
10
|
-
export type
|
|
10
|
+
/** Options for the Preact {@link useRegisterWebMCPTools} lifecycle hook. */
|
|
11
|
+
export type UseRegisterWebMCPToolsOptions<
|
|
11
12
|
TTools extends ReadonlyArray<AnyClientTool>,
|
|
12
13
|
TContext = InferredClientContext<TTools>,
|
|
13
14
|
> = Omit<RegisterWebMCPToolsOptions<TTools, TContext>, 'signal'> & {
|
|
@@ -15,13 +16,19 @@ export type UseWebMCPToolsOptions<
|
|
|
15
16
|
onError?: (error: unknown) => void
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
/** @deprecated Use `UseRegisterWebMCPToolsOptions`. Removed in 1.0.0. */
|
|
20
|
+
export type UseWebMCPToolsOptions<
|
|
21
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
22
|
+
TContext = InferredClientContext<TTools>,
|
|
23
|
+
> = UseRegisterWebMCPToolsOptions<TTools, TContext>
|
|
24
|
+
|
|
25
|
+
type UseRegisterWebMCPToolsArguments<
|
|
19
26
|
TTools extends ReadonlyArray<AnyClientTool>,
|
|
20
27
|
TContext,
|
|
21
28
|
> =
|
|
22
29
|
RegisterWebMCPToolsOptions<TTools, TContext> extends { context: unknown }
|
|
23
|
-
? [options:
|
|
24
|
-
: [options?:
|
|
30
|
+
? [options: UseRegisterWebMCPToolsOptions<TTools, TContext>]
|
|
31
|
+
: [options?: UseRegisterWebMCPToolsOptions<TTools, TContext>]
|
|
25
32
|
|
|
26
33
|
/**
|
|
27
34
|
* Registers client tools with WebMCP for the lifetime of a Preact component.
|
|
@@ -34,15 +41,18 @@ type UseWebMCPToolsArguments<
|
|
|
34
41
|
*
|
|
35
42
|
* @example
|
|
36
43
|
* ```tsx
|
|
37
|
-
*
|
|
44
|
+
* useRegisterWebMCPTools([searchProducts], {
|
|
38
45
|
* toolOptions: { searchProducts: { title: 'Search products' } },
|
|
39
46
|
* })
|
|
40
47
|
* ```
|
|
41
48
|
*/
|
|
42
|
-
export function
|
|
49
|
+
export function useRegisterWebMCPTools<
|
|
43
50
|
const TTools extends ReadonlyArray<AnyClientTool>,
|
|
44
51
|
TContext = InferredClientContext<TTools>,
|
|
45
|
-
>(
|
|
52
|
+
>(
|
|
53
|
+
tools: TTools,
|
|
54
|
+
...[options]: UseRegisterWebMCPToolsArguments<TTools, TContext>
|
|
55
|
+
) {
|
|
46
56
|
useEffect(() => {
|
|
47
57
|
const controller = new AbortController()
|
|
48
58
|
const { onError, ...registrationOptions } = options ?? {}
|
|
@@ -57,3 +67,52 @@ export function useWebMCPTools<
|
|
|
57
67
|
return () => controller.abort()
|
|
58
68
|
}, [tools, options])
|
|
59
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @deprecated Use `useRegisterWebMCPTools`. Removed in 1.0.0.
|
|
73
|
+
* @alias
|
|
74
|
+
*/
|
|
75
|
+
export const useWebMCPTools = useRegisterWebMCPTools
|
|
76
|
+
|
|
77
|
+
/** Options for {@link usePageWebMCPTools}. */
|
|
78
|
+
export type UsePageWebMCPToolsOptions = Omit<
|
|
79
|
+
SubscribeWebMCPToolsOptions,
|
|
80
|
+
'signal'
|
|
81
|
+
>
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the WebMCP tools on the page as client tools for `useChat`.
|
|
85
|
+
*
|
|
86
|
+
* The list starts empty and updates when the page registers or removes a
|
|
87
|
+
* tool. The hook reads the latest `filter` and `onError` on each update.
|
|
88
|
+
* Unsupported browsers and server rendering return an empty array.
|
|
89
|
+
*
|
|
90
|
+
* @param options - A filter that skips tools, and an error callback.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* const pageTools = usePageWebMCPTools({
|
|
95
|
+
* filter: (tool) => tool.origin === location.origin,
|
|
96
|
+
* })
|
|
97
|
+
* const chat = useChat({ connection, tools: pageTools })
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function usePageWebMCPTools(
|
|
101
|
+
options?: UsePageWebMCPToolsOptions,
|
|
102
|
+
): Array<AnyClientTool> {
|
|
103
|
+
const [tools, setTools] = useState<Array<AnyClientTool>>([])
|
|
104
|
+
const optionsRef = useRef(options)
|
|
105
|
+
optionsRef.current = options
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const controller = new AbortController()
|
|
109
|
+
subscribeWebMCPTools(setTools, {
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
filter: (tool) => optionsRef.current?.filter?.(tool) ?? true,
|
|
112
|
+
onError: (error) => optionsRef.current?.onError?.(error),
|
|
113
|
+
})
|
|
114
|
+
return () => controller.abort()
|
|
115
|
+
}, [])
|
|
116
|
+
|
|
117
|
+
return tools
|
|
118
|
+
}
|