@tanstack/ai-react 0.27.3 → 0.28.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/dist/esm/chat-ui/create-ui.d.ts +37 -4
- package/dist/esm/chat-ui/create-ui.js +94 -4
- package/dist/esm/chat-ui/create-ui.js.map +1 -1
- package/dist/esm/create-chat-hook.d.ts +8 -4
- package/dist/esm/create-chat-hook.js.map +1 -1
- package/dist/esm/types.d.ts +19 -5
- package/dist/esm/ui.d.ts +1 -1
- package/dist/esm/use-chat.d.ts +3 -1
- package/dist/esm/use-chat.js +1 -0
- package/dist/esm/use-chat.js.map +1 -1
- package/package.json +4 -4
- package/src/chat-ui/create-ui.tsx +250 -8
- package/src/create-chat-hook.ts +13 -3
- package/src/types.ts +24 -2
- package/src/ui.ts +2 -0
- package/src/use-chat.ts +11 -4
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 { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n InterruptDefinition,\n ModelMessage,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ResolvableChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\n\nimport type {\n DeepPartial,\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 TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(\n options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>,\n): UseChatReturn<TTools, TSchema, TInterrupts> {\n // The hook's identity is its `threadId`. Reload with the same `threadId`\n // restores the same conversation. `hookId` is only a React recreation key\n // when no `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 type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\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 subscribedRef = useRef(false)\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\n // Update ref synchronously during render so it's always current when useMemo runs.\n messagesRef.current = messages\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef =\n useRef<UseChatOptions<TTools, TSchema, TContext, TInterrupts>>(options)\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 // Create ChatClient instance with callbacks to sync state\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 client React may abandon.\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 ...(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.body !== undefined && { body: initialOptions.body }),\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: 'react',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => {\n // ChatClient awaits this return value. Queuing would drop the promise.\n if (!getActiveInstance()) return\n return optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onChunk?.(chunk)\n })\n },\n onFinish: (message: UIMessage<TTools>) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onFinish?.(message)\n })\n },\n onError: (error: Error) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onError?.(error)\n })\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n ...(initialOptions.interrupts !== undefined && {\n interrupts: initialOptions.interrupts,\n }),\n onCustomEvent: (eventType, data, context) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n })\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 onErrorChange: (newError: Error | undefined) => {\n runOrQueueForActiveInstance(() => {\n setError(newError)\n })\n },\n onStatusChange: (status: ChatClientState) => {\n runOrQueueForActiveInstance(() => {\n setStatus(status)\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 each wire-payload slot in its own effect so an unrelated option\n // changing doesn't re-run the others. `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT, so guard the optional\n // slots before passing them.\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\n\n useEffect(() => {\n if (options.forwardedProps !== undefined) {\n client.updateOptions({ forwardedProps: options.forwardedProps })\n }\n }, [client, options.forwardedProps])\n\n useEffect(() => {\n if (options.tools !== undefined) {\n client.updateOptions({ tools: options.tools })\n }\n }, [client, options.tools])\n\n useEffect(() => {\n client.updateOptions({ context: options.context })\n }, [client, options.context])\n\n useEffect(() => {\n if (options.queue !== undefined) {\n client.updateOptions({ queue: options.queue })\n }\n }, [client, options.queue])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n subscribedRef.current = true\n } else if (subscribedRef.current) {\n // Only tear down a subscription we actually started. Calling\n // `unsubscribe()` on initial mount (when `live` was never enabled) would\n // abort an in-flight delivery resume — `resumeInFlightRun` is kicked off\n // in the client constructor, and `unsubscribe()` cancels the shared\n // in-flight stream — so a mid-stream reload would drop its rejoin before\n // it delivers a single chunk. This is exactly why a reload froze instead\n // of continuing.\n client.unsubscribe()\n subscribedRef.current = false\n }\n }, [client, options.live])\n\n // ONLY THE VIEW ON SCREEN HOLDS A STREAM.\n //\n // A page can own many chats — 40 sandboxes, 40 conversations — and a browser\n // allows only ~6 connections per origin. One long-lived stream per chat reaches\n // that ceiling after a handful of views, and every request after it QUEUES:\n // measured, an in-page fetch took over two minutes while the same request from\n // outside the browser took 17ms. So the connection follows the view.\n //\n // Immediate, not deferred: the deferred teardown below can be skipped when the\n // same client remounts, which is right for disposal but useless for a\n // connection. `attach` is idempotent and `detach` keeps the transcript and the\n // resume pointer, so a Strict Mode remount is just detach-then-attach and the\n // run is picked straight back up from the durable log.\n useEffect(() => {\n client.attach()\n return () => {\n client.detach()\n }\n }, [client])\n\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 re-attaches 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 // Soft cleanup only: do NOT stop/unsubscribe here. React Strict Mode\n // remounts fire this cleanup then re-attach the same client one tick\n // later; calling `stop()` would abort a constructor rejoin\n // (`resumeInFlightRun`) and can wipe the durable resume pointer before\n // the first chunk. Real teardown lives in the deferred dispose path\n // below, which only runs when the client is not remounted.\n // Subscribe/unsubscribe on `options.live` is still owned by the\n // dedicated effect above for live toggles.\n const disposal = {\n client,\n timeout: setTimeout(() => {\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\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 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: any\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(\n resumeItems,\n state ?? undefined,\n )\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 // The \"active\" structured-output part is the one on the assistant message\n // that follows the latest user message. No such message exists between\n // sendMessage() and the first chunk, so partial/final naturally read as\n // cleared. Historical parts on earlier assistant messages remain available\n // via `messages` directly.\n //\n // When there is NO user message yet (e.g. `initialMessages` contains only\n // a stale assistant turn or a system prompt) we deliberately return null\n // rather than scanning historical assistants — otherwise a `final` from a\n // previous session would leak into the hook value on first render.\n const renderedMessages = client.getMessages()\n\n const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {\n let lastUserIndex = -1\n for (let i = renderedMessages.length - 1; i >= 0; i--) {\n if (renderedMessages[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {\n const m = renderedMessages[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n }, [renderedMessages])\n\n const partial = useMemo<Partial>(() => {\n if (!activeStructuredPart) return {} as Partial\n const v = activeStructuredPart.partial ?? activeStructuredPart.data\n return (v ?? {}) as Partial\n }, [activeStructuredPart])\n\n const final = useMemo<Final | null>(() => {\n if (!activeStructuredPart || activeStructuredPart.status !== 'complete') {\n return null\n }\n return activeStructuredPart.data as Final\n }, [activeStructuredPart])\n\n // The runtime shape unconditionally exposes partial/final; the public\n // return type hides them when no outputSchema was supplied. TS can't\n // structurally narrow across that conditional, so the `as` is the seam.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow\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 partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema, TInterrupts>\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 { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n InterruptDefinition,\n ModelMessage,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ResolvableChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\n\nimport type {\n DeepPartial,\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 TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n const TSubagents extends ReadonlyArray<{ name: string }> | undefined =\n undefined,\n>(\n options: UseChatOptions<TTools, TSchema, TContext, TInterrupts, TSubagents>,\n): UseChatReturn<TTools, TSchema, TInterrupts, TSubagents> {\n // The hook's identity is its `threadId`. Reload with the same `threadId`\n // restores the same conversation. `hookId` is only a React recreation key\n // when no `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 type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\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 subscribedRef = useRef(false)\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\n // Update ref synchronously during render so it's always current when useMemo runs.\n messagesRef.current = messages\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef =\n useRef<UseChatOptions<TTools, TSchema, TContext, TInterrupts, TSubagents>>(\n 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 // Create ChatClient instance with callbacks to sync state\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 client React may abandon.\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 ...(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.body !== undefined && { body: initialOptions.body }),\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: 'react',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => {\n // ChatClient awaits this return value. Queuing would drop the promise.\n if (!getActiveInstance()) return\n return optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk: StreamChunk) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onChunk?.(chunk)\n })\n },\n onFinish: (message: UIMessage<TTools>) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onFinish?.(message)\n })\n },\n onError: (error: Error) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onError?.(error)\n })\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n ...(initialOptions.interrupts !== undefined && {\n interrupts: initialOptions.interrupts,\n }),\n onCustomEvent: (eventType, data, context) => {\n runOrQueueForActiveInstance(() => {\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n })\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 onErrorChange: (newError: Error | undefined) => {\n runOrQueueForActiveInstance(() => {\n setError(newError)\n })\n },\n onStatusChange: (status: ChatClientState) => {\n runOrQueueForActiveInstance(() => {\n setStatus(status)\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 each wire-payload slot in its own effect so an unrelated option\n // changing doesn't re-run the others. `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT, so guard the optional\n // slots before passing them.\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\n\n useEffect(() => {\n if (options.forwardedProps !== undefined) {\n client.updateOptions({ forwardedProps: options.forwardedProps })\n }\n }, [client, options.forwardedProps])\n\n useEffect(() => {\n if (options.tools !== undefined) {\n client.updateOptions({ tools: options.tools })\n }\n }, [client, options.tools])\n\n useEffect(() => {\n client.updateOptions({ context: options.context })\n }, [client, options.context])\n\n useEffect(() => {\n if (options.queue !== undefined) {\n client.updateOptions({ queue: options.queue })\n }\n }, [client, options.queue])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n subscribedRef.current = true\n } else if (subscribedRef.current) {\n // Only tear down a subscription we actually started. Calling\n // `unsubscribe()` on initial mount (when `live` was never enabled) would\n // abort an in-flight delivery resume — `resumeInFlightRun` is kicked off\n // in the client constructor, and `unsubscribe()` cancels the shared\n // in-flight stream — so a mid-stream reload would drop its rejoin before\n // it delivers a single chunk. This is exactly why a reload froze instead\n // of continuing.\n client.unsubscribe()\n subscribedRef.current = false\n }\n }, [client, options.live])\n\n // ONLY THE VIEW ON SCREEN HOLDS A STREAM.\n //\n // A page can own many chats — 40 sandboxes, 40 conversations — and a browser\n // allows only ~6 connections per origin. One long-lived stream per chat reaches\n // that ceiling after a handful of views, and every request after it QUEUES:\n // measured, an in-page fetch took over two minutes while the same request from\n // outside the browser took 17ms. So the connection follows the view.\n //\n // Immediate, not deferred: the deferred teardown below can be skipped when the\n // same client remounts, which is right for disposal but useless for a\n // connection. `attach` is idempotent and `detach` keeps the transcript and the\n // resume pointer, so a Strict Mode remount is just detach-then-attach and the\n // run is picked straight back up from the durable log.\n useEffect(() => {\n client.attach()\n return () => {\n client.detach()\n }\n }, [client])\n\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 re-attaches 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 // Soft cleanup only: do NOT stop/unsubscribe here. React Strict Mode\n // remounts fire this cleanup then re-attach the same client one tick\n // later; calling `stop()` would abort a constructor rejoin\n // (`resumeInFlightRun`) and can wipe the durable resume pointer before\n // the first chunk. Real teardown lives in the deferred dispose path\n // below, which only runs when the client is not remounted.\n // Subscribe/unsubscribe on `options.live` is still owned by the\n // dedicated effect above for live toggles.\n const disposal = {\n client,\n timeout: setTimeout(() => {\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\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 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: any\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(\n resumeItems,\n state ?? undefined,\n )\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 // The \"active\" structured-output part is the one on the assistant message\n // that follows the latest user message. No such message exists between\n // sendMessage() and the first chunk, so partial/final naturally read as\n // cleared. Historical parts on earlier assistant messages remain available\n // via `messages` directly.\n //\n // When there is NO user message yet (e.g. `initialMessages` contains only\n // a stale assistant turn or a system prompt) we deliberately return null\n // rather than scanning historical assistants — otherwise a `final` from a\n // previous session would leak into the hook value on first render.\n const renderedMessages = client.getMessages()\n\n const activeStructuredPart = useMemo<StructuredOutputPart | null>(() => {\n let lastUserIndex = -1\n for (let i = renderedMessages.length - 1; i >= 0; i--) {\n if (renderedMessages[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = renderedMessages.length - 1; i > lastUserIndex; i--) {\n const m = renderedMessages[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n }, [renderedMessages])\n\n const partial = useMemo<Partial>(() => {\n if (!activeStructuredPart) return {} as Partial\n const v = activeStructuredPart.partial ?? activeStructuredPart.data\n return (v ?? {}) as Partial\n }, [activeStructuredPart])\n\n const final = useMemo<Final | null>(() => {\n if (!activeStructuredPart || activeStructuredPart.status !== 'complete') {\n return null\n }\n return activeStructuredPart.data as Final\n }, [activeStructuredPart])\n\n const subagents = useMemo(() => client.getSubagents(), [renderedMessages])\n\n // The runtime shape unconditionally exposes partial/final; the public\n // return type hides them when no outputSchema was supplied. TS can't\n // structurally narrow across that conditional, so the `as` is the seam.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- hook return shape diverges from generic UseChatReturn<TTools, TSchema> due to conditional type on TSchema; TS can't structurally narrow\n return {\n messages: renderedMessages,\n subagents,\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 partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema, TInterrupts, TSubagents>\n}\n"],"mappings":";;;;AAgCA,IAAM,mBAAmB,OAAO,OAAO,CAAC,CAAC;AACzC,IAAM,yBAAyB,OAAO,OAAO,CAAC,CAAC;AAE/C,SAAgB,QAUd,SACyD;CAIzD,MAAM,SAAS,MAAM;CACrB,MAAM,WAAW,QAAQ,YAAY;CAErC,MAAM,CAAC,UAAU,eAAe,SAC9B,QAAQ,mBAAmB,CAAC,CAC9B;CACA,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,kBAAkB,uBAAuB,SAAS,KAAK;CAC9D,MAAM,CAAC,OAAO,YAAY,SAA4B,KAAA,CAAS;CAC/D,MAAM,CAAC,QAAQ,aAAa,SAA0B,OAAO;CAC7D,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,kBAAkB,uBACvB,SAA2B,cAAc;CAC3C,MAAM,CAAC,mBAAmB,wBAAwB,SAAS,KAAK;CAChE,MAAM,CAAC,OAAO,YAAY,SAA+B,CAAC,CAAC;CAC3D,MAAM,CAAC,OAAO,YAAY,SAAwB,IAAI;CACtD,MAAM,CAAC,gBAAgB,qBAAqB,gBAEnC;EACP,YAAY;EACZ,mBAAmB;EACnB,iBAAiB;EACjB,UAAU;CACZ,EAAE;CAMF,MAAM,cAAc,OAClB,QAAQ,mBAAmB,CAAC,CAC9B;CACA,MAAM,kBAAkB,OAAO,IAAI;CACnC,MAAM,gBAAgB,OAAO,KAAK;CAClC,MAAM,kBAAkB,OAA0B,IAAI;CACtD,MAAM,yBAAyB,OAC7B,IACF;CACA,MAAM,qBAAqB,OAGjB,IAAI;CAGd,YAAY,UAAU;CAGtB,MAAM,aACJ,OACE,OACF;CACF,WAAW,UAAU;CAErB,MAAM,kBAAkB,aAAa,WAA8B;EACjE,IAAI,CAAC,QAAQ;EACb,SAAS,OAAO,gBAAgB,CAAC;EACjC,kBAAkB,OAAO,kBAAkB,CAAC;CAC9C,GAAG,CAAC,CAAC;CAGL,MAAM,EAAE,QAAQ,mBAAmB,cAAc;EAC/C,MAAM,gBAAgB,QAAQ,mBAAmB,CAAC;EAClD,gBAAgB,UAAU;EAM1B,MAAM,iBAAiB,WAAW;EAClC,MAAM,YAAY,eAAe,aAC7B,EAAE,YAAY,eAAe,WAAW,IACxC,EAAE,SAAS,eAAe,QAAQ;EAEtC,MAAM,iBAEF,EAAE,SAAS,KAAA,EAAU;EACzB,MAAM,0BAA0B;GAC9B,MAAM,kBAAkB,eAAe;GACvC,IAAI,CAAC,mBAAmB,gBAAgB,YAAY,iBAClD;GAEF,OAAO;EACT;EAKA,MAAM,sBAAsB;GAC1B,OAAO;GACP,WAAW,CAAC;EACd;EACA,MAAM,+BAA+B,aAAyB;GAC5D,IAAI,CAAC,oBAAoB,OAAO;IAC9B,oBAAoB,UAAU,KAAK,QAAQ;IAC3C;GACF;GACA,MAAM,kBAAkB,eAAe;GACvC,IAAI,CAAC,mBAAmB,gBAAgB,YAAY,iBAClD;GACF,SAAS;EACX;EACA,MAAM,WAAW,IAAI,WAA0C;GAC7D,uBAAuB;GACvB,GAAG;GACH,iBAAiB;GACjB,GAAI,OAAO,eAAe,aAAa,YACvC,eAAe,gBAAgB,OAC3B;IACE,aAAa;IACb,UAAU,eAAe;IACzB,GAAI,eAAe,YAAY,KAAA,KAAa,EAC1C,SAAS,eAAe,QAC1B;GACF,IACA,OAAO,eAAe,aAAa,YACjC,eAAe,cACf;IACE,aAAa,eAAe;IAC5B,UAAU,eAAe;GAC3B,IACA,EACE,GAAI,eAAe,aAAa,KAAA,KAAa,EAC3C,UAAU,eAAe,SAC3B,EACF;GACN,GAAI,eAAe,SAAS,KAAA,KAAa,EAAE,MAAM,eAAe,KAAK;GACrE,GAAI,eAAe,mBAAmB,KAAA,KAAa,EACjD,gBAAgB,eAAe,eACjC;GACA,GAAI,eAAe,SAAS,KAAA,KAAa,EAAE,MAAM,eAAe,KAAK;GACrE,oBAAoB,WAAW,QAAQ,eAAe;GACtD,GAAI,eAAe,0BAA0B,KAAA,KAAa,EACxD,uBAAuB,eAAe,sBACxC;GACA,GAAI,eAAe,YAAY,KAAA,KAAa,EAC1C,SAAS,eAAe,QAC1B;GACA,UAAU;IACR,GAAG,eAAe;IAClB,WAAW;IACX,UAAU;IACV,YAAY,eAAe,eAAe,eAAe;GAC3D;GACA,aAAa,aAAa;IAExB,IAAI,CAAC,kBAAkB,GAAG;IAC1B,OAAO,WAAW,QAAQ,aAAa,QAAQ;GACjD;GACA,UAAU,UAAuB;IAC/B,kCAAkC;KAChC,WAAW,QAAQ,UAAU,KAAK;IACpC,CAAC;GACH;GACA,WAAW,YAA+B;IACxC,kCAAkC;KAChC,WAAW,QAAQ,WAAW,OAAO;IACvC,CAAC;GACH;GACA,UAAU,UAAiB;IACzB,kCAAkC;KAChC,WAAW,QAAQ,UAAU,KAAK;IACpC,CAAC;GACH;GACA,GAAI,eAAe,UAAU,KAAA,KAAa,EACxC,OAAO,eAAe,MACxB;GACA,GAAI,eAAe,eAAe,KAAA,KAAa,EAC7C,YAAY,eAAe,WAC7B;GACA,gBAAgB,WAAW,MAAM,YAAY;IAC3C,kCAAkC;KAChC,WAAW,QAAQ,gBAAgB,WAAW,MAAM,OAAO;IAC7D,CAAC;GACH;GACA,GAAI,QAAQ,oBAAoB,KAAA,KAAa,EAC3C,iBAAiB,QAAQ,gBAC3B;GACA,mBAAmB,gBAA0C;IAC3D,kCAAkC;KAChC,YAAY,WAAW;KACvB,MAAM,kBAAkB,kBAAkB;KAC1C,IAAI,CAAC,iBAAiB;KACtB,oBAAoB,gBAAgB,oBAAoB,CAAC;IAC3D,CAAC;GACH;GACA,kBAAkB,iBAA0B;IAC1C,kCAAkC;KAChC,MAAM,kBAAkB,kBAAkB;KAC1C,IAAI,CAAC,iBAAiB;KACtB,aAAa,YAAY;KACzB,gBAAgB,eAAe;IACjC,CAAC;GACH;GACA,gBAAgB,aAAgC;IAC9C,kCAAkC;KAChC,SAAS,QAAQ;IACnB,CAAC;GACH;GACA,iBAAiB,WAA4B;IAC3C,kCAAkC;KAChC,UAAU,MAAM;IAClB,CAAC;GACH;GACA,uBAAuB,qBAA8B;IACnD,kCAAkC;KAChC,gBAAgB,gBAAgB;IAClC,CAAC;GACH;GACA,2BAA2B,eAAiC;IAC1D,kCAAkC;KAChC,oBAAoB,UAAU;IAChC,CAAC;GACH;GACA,4BAA4B,iBAA0B;IACpD,kCAAkC;KAChC,qBAAqB,YAAY;IACnC,CAAC;GACH;GACA,GAAI,WAAW,QAAQ,UAAU,KAAA,KAAa,EAC5C,OAAO,WAAW,QAAQ,MAC5B;GACA,gBAAgB,cAAoC;IAClD,kCAAkC;KAChC,SAAS,SAAS;IACpB,CAAC;GACH;GACA,gBAAgB,cAAc;IAC5B,kCAAkC;KAChC,SAAS,SAAS;IACpB,CAAC;GACH;GACA,sBAAsB,kBAAkB,0BAA0B;IAChE,kCAAkC;KAChC,mBAAmB,aAAa;MAC9B,GAAG;MACH,YAAY;MACZ,mBAAmB;KACrB,EAAE;IACJ,CAAC;GACH;GACA,yBAAyB,oBAAoB,YAAY;IACvD,kCAAkC;KAChC,kBAAkB,kBAAkB;KACpC,WAAW,QAAQ,yBACjB,oBACA,OACF;IACF,CAAC;GACH;EACF,CAAC;EACD,eAAe,UAAU;EACzB,OAAO;GAAE,QAAQ;GAAU,gBAAgB;EAAoB;CACjE,GAAG,CAAC,UAAU,eAAe,CAAC;CAE9B,gBAAgB;EACd,gBAAgB,UAAU;EAC1B,IAAI;GAGF,OAAO,eAAe,UAAU,SAAS,GAAG;IAC1C,IAAI,gBAAgB,YAAY,QAAQ;KACtC,eAAe,UAAU,SAAS;KAClC;IACF;IACA,eAAe,UAAU,MAAM,CAAC,GAAG;GACrC;EACF,UAAU;GAER,eAAe,QAAQ;EACzB;CACF,GAAG,CAAC,QAAQ,cAAc,CAAC;CAE3B,gBAAgB;EACd,MAAM,iBAAiB,OAAO,YAAY;EAC1C,IAAI,mBAAmB,YAAY,SACjC,YAAY,cAAc;EAE5B,oBAAoB,OAAO,oBAAoB,CAAC;CAClD,GAAG,CAAC,MAAM,CAAC;CAMX,gBAAgB;EACd,OAAO,cAAc,EAAE,MAAM,QAAQ,KAAK,CAAC;CAC7C,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;CAEzB,gBAAgB;EACd,IAAI,QAAQ,mBAAmB,KAAA,GAC7B,OAAO,cAAc,EAAE,gBAAgB,QAAQ,eAAe,CAAC;CAEnE,GAAG,CAAC,QAAQ,QAAQ,cAAc,CAAC;CAEnC,gBAAgB;EACd,IAAI,QAAQ,UAAU,KAAA,GACpB,OAAO,cAAc,EAAE,OAAO,QAAQ,MAAM,CAAC;CAEjD,GAAG,CAAC,QAAQ,QAAQ,KAAK,CAAC;CAE1B,gBAAgB;EACd,OAAO,cAAc,EAAE,SAAS,QAAQ,QAAQ,CAAC;CACnD,GAAG,CAAC,QAAQ,QAAQ,OAAO,CAAC;CAE5B,gBAAgB;EACd,IAAI,QAAQ,UAAU,KAAA,GACpB,OAAO,cAAc,EAAE,OAAO,QAAQ,MAAM,CAAC;CAEjD,GAAG,CAAC,QAAQ,QAAQ,KAAK,CAAC;CAE1B,gBAAgB;EACd,IAAI,QAAQ,MAAM;GAChB,OAAO,UAAU;GACjB,cAAc,UAAU;EAC1B,OAAO,IAAI,cAAc,SAAS;GAQhC,OAAO,YAAY;GACnB,cAAc,UAAU;EAC1B;CACF,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;CAezB,gBAAgB;EACd,OAAO,OAAO;EACd,aAAa;GACX,OAAO,OAAO;EAChB;CACF,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB;EACd,IAAI,mBAAmB,SAAS,WAAW,QAAQ;GACjD,aAAa,mBAAmB,QAAQ,OAAO;GAC/C,mBAAmB,UAAU;EAC/B;EACA,IAAI,uBAAuB,SAAS;GAClC,aAAa,uBAAuB,OAAO;GAC3C,uBAAuB,UAAU;EACnC;EACA,OAAO,cAAc;EAIrB,gBAAgB,MAAM;EAEtB,aAAa;GACX,uBAAuB,UAAU,iBAAiB;IAChD,IAAI,gBAAgB,YAAY,QAC9B,gBAAgB,UAAU;IAE5B,uBAAuB,UAAU;GACnC,GAAG,CAAC;GASJ,MAAM,WAAW;IACf;IACA,SAAS,iBAAiB;KACxB,IAAI,WAAW,QAAQ,MACrB,OAAO,YAAY;UAEnB,OAAO,KAAK;KAEd,OAAO,QAAQ;KACf,IAAI,mBAAmB,YAAY,UACjC,mBAAmB,UAAU;IAEjC,GAAG,CAAC;GACN;GACA,mBAAmB,UAAU;EAC/B;CACF,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,cAAc,YAClB,OACE,SACA,gBACG;EACH,IAAI;GACF,MAAM,OAAO,YAAY,SAAS,KAAA,GAAW,WAAW;EAC1D,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,eAAe,aAClB,OAAe;EACd,OAAO,aAAa,EAAE;CACxB,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,SAAS,YACb,OAAO,YAAsC;EAC3C,IAAI;GACF,MAAM,OAAO,OAAO,OAAO;EAC7B,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,SAAS,YAAY,YAAY;EACrC,IAAI;GACF,MAAM,OAAO,OAAO;EACtB,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,oBAAoB,YAAY,YAAY;EAChD,MAAM,OAAO,kBAAkB;EAC/B,oBAAoB,OAAO,oBAAoB,CAAC;CAClD,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,OAAO,kBAAkB;EAC7B,OAAO,KAAK;CACd,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,QAAQ,kBAAkB;EAC9B,OAAO,MAAM;EACb,gBAAgB,MAAM;CACxB,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,sBAAsB,aACzB,gBAA0C;EACzC,OAAO,oBAAoB,WAAW;CACxC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,gBAAgB,YACpB,OAAO,WAMD;EACJ,MAAM,OAAO,cAAc,MAAM;CACnC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,0BAA0B,YAC9B,OAAO,aAAgD;EACrD,MAAM,OAAO,wBAAwB,QAAQ;EAC7C,gBAAgB,MAAM;CACxB,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,mBAAmB,YACvB,OAAO,aAAwC,UAA4B;EACzE,MAAM,SAAS,MAAM,OAAO,iBAC1B,aACA,SAAS,KAAA,CACX;EACA,gBAAgB,MAAM;EACtB,OAAO;CACT,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,oBAAoB,aAEtB,eAKG;EACH,IAAI,OAAO,eAAe,WACxB,OAAO,kBAAkB,UAAU;OAEnC,OAAO,kBAAkB,UAAU;CAEvC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,mBAAmB,kBAAkB;EACzC,OAAO,iBAAiB;CAC1B,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,kBAAkB,kBAAkB;EACxC,OAAO,gBAAgB;CACzB,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,yBAAyB,aAC5B,aAAwC,UACvC,OAAO,uBAAuB,aAAa,KAAK,GAClD,CAAC,MAAM,CACT;CAYA,MAAM,mBAAmB,OAAO,YAAY;CAE5C,MAAM,uBAAuB,cAA2C;EACtE,IAAI,gBAAgB;EACpB,KAAK,IAAI,IAAI,iBAAiB,SAAS,GAAG,KAAK,GAAG,KAChD,IAAI,iBAAiB,EAAE,EAAE,SAAS,QAAQ;GACxC,gBAAgB;GAChB;EACF;EAEF,IAAI,kBAAkB,IAAI,OAAO;EACjC,KAAK,IAAI,IAAI,iBAAiB,SAAS,GAAG,IAAI,eAAe,KAAK;GAChE,MAAM,IAAI,iBAAiB;GAC3B,IAAI,GAAG,SAAS,aAAa;GAC7B,MAAM,OAAO,EAAE,MAAM,MAClB,MAAiC,EAAE,SAAS,mBAC/C;GACA,IAAI,MAAM,OAAO;EACnB;EACA,OAAO;CACT,GAAG,CAAC,gBAAgB,CAAC;CAErB,MAAM,UAAU,cAAuB;EACrC,IAAI,CAAC,sBAAsB,OAAO,CAAC;EAEnC,OADU,qBAAqB,WAAW,qBAAqB,QAClD,CAAC;CAChB,GAAG,CAAC,oBAAoB,CAAC;CAEzB,MAAM,QAAQ,cAA4B;EACxC,IAAI,CAAC,wBAAwB,qBAAqB,WAAW,YAC3D,OAAO;EAET,OAAO,qBAAqB;CAC9B,GAAG,CAAC,oBAAoB,CAAC;CAQzB,OAAO;EACL,UAAU;EACV,WARgB,cAAc,OAAO,aAAa,GAAG,CAAC,gBAAgB,CAQtE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACA;EACA;EACA;EACA,YAAY,eAAe;EAC3B,mBAAmB,eAAe;EAClC,iBAAiB,eAAe;EAChC,UAAU,eAAe;EACzB;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "React hooks for TanStack AI streaming chat, realtime voice, structured outputs, and media generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@tanstack/markdown": "^0.0.13",
|
|
61
|
-
"@tanstack/ai-client": "^0.
|
|
61
|
+
"@tanstack/ai-client": "^0.34.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@mcp-ui/client": "^7",
|
|
65
65
|
"@types/react": ">=18.0.0",
|
|
66
66
|
"react": ">=18.0.0",
|
|
67
67
|
"react-dom": ">=18.0.0",
|
|
68
|
-
"@tanstack/ai": "^0.
|
|
68
|
+
"@tanstack/ai": "^0.59.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@mcp-ui/client": {
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"react": "^19.2.3",
|
|
86
86
|
"react-dom": "^19.2.3",
|
|
87
87
|
"vite": "^8.2.1",
|
|
88
|
-
"@tanstack/ai": "0.
|
|
88
|
+
"@tanstack/ai": "0.59.0"
|
|
89
89
|
},
|
|
90
90
|
"scripts": {
|
|
91
91
|
"clean": "premove ./build ./dist",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, memo, useContext } from 'react'
|
|
1
|
+
import { createContext, memo, useContext, useMemo } from 'react'
|
|
2
2
|
import type { ComponentProps, ComponentType, Context, ReactNode } from 'react'
|
|
3
3
|
import {
|
|
4
4
|
automaticPartsForMessage,
|
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
selectMessageUI,
|
|
9
9
|
} from '@tanstack/ai-client/ui'
|
|
10
10
|
import type {
|
|
11
|
+
ChatUIApprovalToolName,
|
|
11
12
|
ChatUIData,
|
|
12
13
|
ChatUIHasNamedInterrupts,
|
|
14
|
+
ChatUIHasNamedSubagents,
|
|
13
15
|
ChatUIHasNamedTools,
|
|
14
16
|
ChatUIInterrupt,
|
|
15
17
|
ChatUIInterruptName,
|
|
@@ -22,6 +24,9 @@ import type {
|
|
|
22
24
|
ChatUISchemaOf,
|
|
23
25
|
ChatUISelectedPart,
|
|
24
26
|
ChatUISelectedPartOf,
|
|
27
|
+
ChatUISubagentName,
|
|
28
|
+
ChatUISubagentOf,
|
|
29
|
+
ChatUISubagentOptions,
|
|
25
30
|
ChatUIToolApproval,
|
|
26
31
|
ChatUIToolName,
|
|
27
32
|
ChatUIToolsOf,
|
|
@@ -29,6 +34,8 @@ import type {
|
|
|
29
34
|
import type {
|
|
30
35
|
MessagePart,
|
|
31
36
|
QueuedMessage,
|
|
37
|
+
SubagentHandle,
|
|
38
|
+
SubagentHandles,
|
|
32
39
|
ToolCallPart,
|
|
33
40
|
ToolResultPart,
|
|
34
41
|
UIMessage,
|
|
@@ -56,6 +63,7 @@ export type LayoutProps<
|
|
|
56
63
|
Messages: ComponentType
|
|
57
64
|
Interrupts: ComponentType
|
|
58
65
|
Queue: ComponentType
|
|
66
|
+
Subagents: ComponentType
|
|
59
67
|
readonly __ui?: TOptions
|
|
60
68
|
} & (TInput extends ComponentType<any> ? { Input: ComponentType } : {})
|
|
61
69
|
|
|
@@ -77,6 +85,38 @@ export type PartProps<TOptions, TKey extends ChatUIPartKey = ChatUIPartKey> = {
|
|
|
77
85
|
part: ChatUIPartOf<TOptions, TKey>
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Widgets for one subagent card. Each entry replaces the enclosing entry of
|
|
90
|
+
* the same key: the root map at top level, the outer card's map when nested.
|
|
91
|
+
* It applies to this card and the children nested in it. An unset key keeps
|
|
92
|
+
* the enclosing entry. Tool names and tool props come from that agent's
|
|
93
|
+
* `tools`.
|
|
94
|
+
*/
|
|
95
|
+
export type SubagentPartsProps<
|
|
96
|
+
TOptions,
|
|
97
|
+
TName extends ChatUISubagentName<TOptions> = ChatUISubagentName<TOptions>,
|
|
98
|
+
> = {
|
|
99
|
+
partsComponents?: ChatUIPartsComponents<
|
|
100
|
+
ChatUISubagentOptions<TOptions, TName>
|
|
101
|
+
>
|
|
102
|
+
toolsComponents?: {
|
|
103
|
+
[K in ChatUIToolName<
|
|
104
|
+
ChatUISubagentOptions<TOptions, TName>
|
|
105
|
+
>]?: ComponentType<ToolProps<ChatUISubagentOptions<TOptions, TName>, K>>
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type SubagentProps<
|
|
110
|
+
TOptions,
|
|
111
|
+
TName extends ChatUISubagentName<TOptions> = ChatUISubagentName<TOptions>,
|
|
112
|
+
> = {
|
|
113
|
+
subagent: [ChatUISubagentOf<TOptions, TName>] extends [never]
|
|
114
|
+
? SubagentHandle & { name: TName }
|
|
115
|
+
: SubagentHandles<ReadonlyArray<ChatUISubagentOf<TOptions, TName>>>
|
|
116
|
+
Parts: ComponentType<SubagentPartsProps<TOptions, TName>>
|
|
117
|
+
readonly __ui?: TOptions
|
|
118
|
+
}
|
|
119
|
+
|
|
80
120
|
export type ToolProps<
|
|
81
121
|
TOptions,
|
|
82
122
|
TName extends ChatUIToolName<TOptions> = ChatUIToolName<TOptions>,
|
|
@@ -108,7 +148,7 @@ type GenericInterruptComponents<TOptions> =
|
|
|
108
148
|
}
|
|
109
149
|
|
|
110
150
|
type ToolApprovalMap<TOptions> = {
|
|
111
|
-
[K in
|
|
151
|
+
[K in ChatUIApprovalToolName<TOptions>]?: ComponentType<
|
|
112
152
|
InterruptProps<TOptions, K & ChatUIInterruptName<TOptions>>
|
|
113
153
|
>
|
|
114
154
|
}
|
|
@@ -161,6 +201,21 @@ export type ChatUIComponents<
|
|
|
161
201
|
tools?: ToolApprovalMap<TOptions>
|
|
162
202
|
generic?: GenericInterruptComponents<TOptions>
|
|
163
203
|
}
|
|
204
|
+
}) &
|
|
205
|
+
(ChatUIHasNamedSubagents<TOptions> extends true
|
|
206
|
+
? {
|
|
207
|
+
subagentsComponents: {
|
|
208
|
+
[K in ChatUISubagentName<TOptions>]: ComponentType<
|
|
209
|
+
SubagentProps<TOptions, K>
|
|
210
|
+
>
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
: {
|
|
214
|
+
subagentsComponents?: {
|
|
215
|
+
[K in ChatUISubagentName<TOptions>]?: ComponentType<
|
|
216
|
+
SubagentProps<TOptions, K>
|
|
217
|
+
>
|
|
218
|
+
}
|
|
164
219
|
})
|
|
165
220
|
|
|
166
221
|
/** Scoped contexts, for widgets in other files or nested chat trees. */
|
|
@@ -191,7 +246,7 @@ type PartMixins<TOptions> = {
|
|
|
191
246
|
type InterruptMixins<TOptions> = {
|
|
192
247
|
[K in ChatUINamedInterruptId<TOptions>]: BoundWidget
|
|
193
248
|
} & {
|
|
194
|
-
[K in
|
|
249
|
+
[K in ChatUIApprovalToolName<TOptions>]?: BoundWidget
|
|
195
250
|
} & {
|
|
196
251
|
fallback?: BoundWidget
|
|
197
252
|
Render: BoundWidget
|
|
@@ -227,6 +282,32 @@ function queueItemEqual(
|
|
|
227
282
|
)
|
|
228
283
|
}
|
|
229
284
|
|
|
285
|
+
// The handle is one live object that the client updates in place, so a row
|
|
286
|
+
// gets its changing card fields as separate props. Messages are not one of
|
|
287
|
+
// them: `SubagentMessages` reads them from the chat context, so new child
|
|
288
|
+
// text shows without a row render.
|
|
289
|
+
type SubagentRowProps = {
|
|
290
|
+
handle: SubagentHandle
|
|
291
|
+
status: SubagentHandle['status']
|
|
292
|
+
error: SubagentHandle['error']
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function subagentListItemEqual(prev: SubagentRowProps, next: SubagentRowProps) {
|
|
296
|
+
return (
|
|
297
|
+
prev.handle.id === next.handle.id &&
|
|
298
|
+
prev.handle.stop === next.handle.stop &&
|
|
299
|
+
prev.status === next.status &&
|
|
300
|
+
prev.error === next.error
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function subagentMessagesEqual(
|
|
305
|
+
prev: { messages: SubagentHandle['messages'] },
|
|
306
|
+
next: { messages: SubagentHandle['messages'] },
|
|
307
|
+
) {
|
|
308
|
+
return prev.messages === next.messages
|
|
309
|
+
}
|
|
310
|
+
|
|
230
311
|
function selectedPartPropsEqual(
|
|
231
312
|
prev: { selected: ChatUISelectedPart },
|
|
232
313
|
next: { selected: ChatUISelectedPart },
|
|
@@ -254,6 +335,18 @@ function isSelectedPart(
|
|
|
254
335
|
return 'key' in value && 'part' in value
|
|
255
336
|
}
|
|
256
337
|
|
|
338
|
+
/** `base` with each set entry of `overrides`. An unset entry keeps `base`. */
|
|
339
|
+
function withWidgets(
|
|
340
|
+
base: Record<string, ComponentType<any> | undefined>,
|
|
341
|
+
overrides: Record<string, ComponentType<any> | undefined> | undefined,
|
|
342
|
+
) {
|
|
343
|
+
const out = { ...base }
|
|
344
|
+
for (const [key, component] of Object.entries(overrides ?? {})) {
|
|
345
|
+
if (component) out[key] = component
|
|
346
|
+
}
|
|
347
|
+
return out
|
|
348
|
+
}
|
|
349
|
+
|
|
257
350
|
function bindMap(
|
|
258
351
|
map: Record<string, ComponentType<any> | undefined> | undefined,
|
|
259
352
|
bind: (component: ComponentType<any>) => BoundWidget,
|
|
@@ -288,12 +381,14 @@ export function createChatUI<
|
|
|
288
381
|
partsComponents: parts,
|
|
289
382
|
toolsComponents: tools,
|
|
290
383
|
interruptsComponents: interrupts,
|
|
384
|
+
subagentsComponents: subagentComponents,
|
|
291
385
|
} = config as ChatUIFactoryConfig<TOptions, TInput> & {
|
|
292
386
|
toolsComponents?: Record<string, ComponentType<any> | undefined>
|
|
293
387
|
interruptsComponents?: {
|
|
294
388
|
tools?: Record<string, ComponentType<any> | undefined>
|
|
295
389
|
generic?: Record<string, ComponentType<any> | undefined>
|
|
296
390
|
}
|
|
391
|
+
subagentsComponents?: Record<string, ComponentType<any> | undefined>
|
|
297
392
|
}
|
|
298
393
|
const {
|
|
299
394
|
layout: Layout,
|
|
@@ -445,6 +540,7 @@ export function createChatUI<
|
|
|
445
540
|
Messages: Messages as ComponentType,
|
|
446
541
|
Interrupts: Interrupts as ComponentType,
|
|
447
542
|
Queue: Queue as ComponentType,
|
|
543
|
+
Subagents: Subagents as ComponentType,
|
|
448
544
|
Input: (InputComponent ?? MissingInput) as ComponentType,
|
|
449
545
|
}
|
|
450
546
|
|
|
@@ -525,6 +621,19 @@ export function createChatUI<
|
|
|
525
621
|
inlineToolNames: ReadonlyArray<string>
|
|
526
622
|
}
|
|
527
623
|
const MessageRenderContext = createContext<MessageRenderValue | null>(null)
|
|
624
|
+
const SubagentRenderContext = createContext<{
|
|
625
|
+
handle: SubagentHandle
|
|
626
|
+
/** A `Subagents` row. Its messages come from the live chat state. */
|
|
627
|
+
listed?: true
|
|
628
|
+
} | null>(null)
|
|
629
|
+
// The widgets that dispatch uses. A subagent card's `Parts` can replace
|
|
630
|
+
// entries for its subtree. Outside a card these are the factory maps.
|
|
631
|
+
type WidgetMap = Record<string, ComponentType<any> | undefined>
|
|
632
|
+
const rootWidgets: { parts: WidgetMap; tools: WidgetMap } = {
|
|
633
|
+
parts: parts as WidgetMap,
|
|
634
|
+
tools: tools ?? {},
|
|
635
|
+
}
|
|
636
|
+
const WidgetsContext = createContext(rootWidgets)
|
|
528
637
|
|
|
529
638
|
function Parts() {
|
|
530
639
|
const scope = useContext(MessageRenderContext)
|
|
@@ -588,7 +697,7 @@ export function createChatUI<
|
|
|
588
697
|
interrupts,
|
|
589
698
|
inlineToolNames: names,
|
|
590
699
|
}: {
|
|
591
|
-
message:
|
|
700
|
+
message: UIMessage
|
|
592
701
|
interrupts: ReadonlyArray<ChatUIInterrupt>
|
|
593
702
|
inlineToolNames: ReadonlyArray<string>
|
|
594
703
|
}) {
|
|
@@ -610,9 +719,10 @@ export function createChatUI<
|
|
|
610
719
|
}: {
|
|
611
720
|
selected: ChatUISelectedPart
|
|
612
721
|
}) {
|
|
722
|
+
const widgets = useContext(WidgetsContext)
|
|
613
723
|
if (selected.key === 'toolCall') {
|
|
614
724
|
const name = selected.part.name
|
|
615
|
-
const Tool = tools
|
|
725
|
+
const Tool = widgets.tools[name] as
|
|
616
726
|
| ComponentType<ToolProps<TOptions>>
|
|
617
727
|
| undefined
|
|
618
728
|
if (!Tool) {
|
|
@@ -628,9 +738,30 @@ export function createChatUI<
|
|
|
628
738
|
)
|
|
629
739
|
}
|
|
630
740
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
741
|
+
if (selected.key === 'subagent' && selected.part.type === 'subagent') {
|
|
742
|
+
const name = selected.part.subagent.name
|
|
743
|
+
const Subagent = subagentComponents?.[name] as
|
|
744
|
+
| ComponentType<SubagentProps<TOptions>>
|
|
745
|
+
| undefined
|
|
746
|
+
if (!Subagent) {
|
|
747
|
+
throw new Error(`[tanstack-ai-ui] Missing subagentsComponents.${name}`)
|
|
748
|
+
}
|
|
749
|
+
return (
|
|
750
|
+
<SubagentRenderContext.Provider
|
|
751
|
+
value={{ handle: selected.part.subagent }}
|
|
752
|
+
>
|
|
753
|
+
<Subagent
|
|
754
|
+
Parts={SubagentMessages}
|
|
755
|
+
subagent={
|
|
756
|
+
selected.part.subagent as SubagentProps<TOptions>['subagent']
|
|
757
|
+
}
|
|
758
|
+
/>
|
|
759
|
+
</SubagentRenderContext.Provider>
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const PartComponent = (widgets.parts[selected.key] ??
|
|
764
|
+
widgets.parts.fallback) as ComponentType<PartProps<TOptions>> | undefined
|
|
634
765
|
if (!PartComponent) {
|
|
635
766
|
warn(
|
|
636
767
|
`part:${selected.key}`,
|
|
@@ -746,6 +877,115 @@ export function createChatUI<
|
|
|
746
877
|
)
|
|
747
878
|
})
|
|
748
879
|
|
|
880
|
+
function readSubagentMessages() {
|
|
881
|
+
const scoped = useContext(SubagentRenderContext)
|
|
882
|
+
const chat = useChatContext()
|
|
883
|
+
if (scoped?.listed) {
|
|
884
|
+
const live = chat.subagents.find((item) => item.id === scoped.handle.id)
|
|
885
|
+
return (live ?? scoped.handle).messages
|
|
886
|
+
}
|
|
887
|
+
if (scoped) return scoped.handle.messages
|
|
888
|
+
const selected = useContext(PartContext)
|
|
889
|
+
if (selected?.key === 'subagent' && selected.part.type === 'subagent') {
|
|
890
|
+
return selected.part.subagent.messages
|
|
891
|
+
}
|
|
892
|
+
throw new Error(
|
|
893
|
+
'`Parts` must be rendered by a subagent component or `Subagents` item.',
|
|
894
|
+
)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const SubagentMessagesBody = memo(function SubagentMessagesBody({
|
|
898
|
+
messages,
|
|
899
|
+
}: {
|
|
900
|
+
messages: SubagentHandle['messages']
|
|
901
|
+
}) {
|
|
902
|
+
const chat = useChatContext()
|
|
903
|
+
const interrupts = readInterrupts(chat)
|
|
904
|
+
return (
|
|
905
|
+
<>
|
|
906
|
+
{messages.map((message) => (
|
|
907
|
+
<AutomaticParts
|
|
908
|
+
key={message.id}
|
|
909
|
+
inlineToolNames={inlineToolNames}
|
|
910
|
+
interrupts={interrupts}
|
|
911
|
+
message={message}
|
|
912
|
+
/>
|
|
913
|
+
))}
|
|
914
|
+
</>
|
|
915
|
+
)
|
|
916
|
+
}, subagentMessagesEqual)
|
|
917
|
+
|
|
918
|
+
function SubagentMessages({
|
|
919
|
+
partsComponents,
|
|
920
|
+
toolsComponents,
|
|
921
|
+
}: SubagentPartsProps<TOptions>) {
|
|
922
|
+
const messages = readSubagentMessages()
|
|
923
|
+
const outer = useContext(WidgetsContext)
|
|
924
|
+
const widgets = useMemo(
|
|
925
|
+
() =>
|
|
926
|
+
partsComponents || toolsComponents
|
|
927
|
+
? {
|
|
928
|
+
parts: withWidgets(outer.parts, partsComponents),
|
|
929
|
+
tools: withWidgets(outer.tools, toolsComponents),
|
|
930
|
+
}
|
|
931
|
+
: outer,
|
|
932
|
+
[outer, partsComponents, toolsComponents],
|
|
933
|
+
)
|
|
934
|
+
// ponytail: root `<Interrupts />` still lists an approval for a tool call
|
|
935
|
+
// inside a card, so it shows in both places. Hiding it needs `selectChatUI`
|
|
936
|
+
// to collect tool-call ids from nested subagent messages (its `callIds` is
|
|
937
|
+
// top-level only) plus the card tool names in `inlineToolNames`.
|
|
938
|
+
return (
|
|
939
|
+
<WidgetsContext.Provider value={widgets}>
|
|
940
|
+
<SubagentMessagesBody messages={messages} />
|
|
941
|
+
</WidgetsContext.Provider>
|
|
942
|
+
)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const SubagentListItem = memo(function SubagentListItem({
|
|
946
|
+
handle,
|
|
947
|
+
}: SubagentRowProps) {
|
|
948
|
+
const Subagent = subagentComponents?.[handle.name] as
|
|
949
|
+
| ComponentType<SubagentProps<TOptions>>
|
|
950
|
+
| undefined
|
|
951
|
+
if (!Subagent) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`[tanstack-ai-ui] Missing subagentsComponents.${handle.name}`,
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
return (
|
|
957
|
+
<SubagentRenderContext.Provider value={{ handle, listed: true }}>
|
|
958
|
+
<Subagent
|
|
959
|
+
Parts={SubagentMessages}
|
|
960
|
+
subagent={handle as SubagentProps<TOptions>['subagent']}
|
|
961
|
+
/>
|
|
962
|
+
</SubagentRenderContext.Provider>
|
|
963
|
+
)
|
|
964
|
+
}, subagentListItemEqual)
|
|
965
|
+
|
|
966
|
+
function Subagents({
|
|
967
|
+
children,
|
|
968
|
+
}: {
|
|
969
|
+
children?: (subagents: Array<SubagentHandle>) => ReactNode
|
|
970
|
+
} = {}) {
|
|
971
|
+
const chat = useChatContext()
|
|
972
|
+
const live = chat.subagents
|
|
973
|
+
if (children) return <>{children(live)}</>
|
|
974
|
+
if (live.length === 0) return null
|
|
975
|
+
return (
|
|
976
|
+
<>
|
|
977
|
+
{live.map((handle) => (
|
|
978
|
+
<SubagentListItem
|
|
979
|
+
key={handle.id}
|
|
980
|
+
handle={handle}
|
|
981
|
+
status={handle.status}
|
|
982
|
+
error={handle.error}
|
|
983
|
+
/>
|
|
984
|
+
))}
|
|
985
|
+
</>
|
|
986
|
+
)
|
|
987
|
+
}
|
|
988
|
+
|
|
749
989
|
return {
|
|
750
990
|
Chat,
|
|
751
991
|
Provider,
|
|
@@ -755,6 +995,8 @@ export function createChatUI<
|
|
|
755
995
|
Interrupts,
|
|
756
996
|
Interrupt,
|
|
757
997
|
Queue,
|
|
998
|
+
Subagents,
|
|
999
|
+
SubagentMessages,
|
|
758
1000
|
useChatContext,
|
|
759
1001
|
Input: InputComponent,
|
|
760
1002
|
}
|
package/src/create-chat-hook.ts
CHANGED
|
@@ -12,6 +12,7 @@ type ChatHookOverrides<
|
|
|
12
12
|
TSchema extends SchemaInput | undefined,
|
|
13
13
|
TContext,
|
|
14
14
|
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
15
|
+
TSubagents extends ReadonlyArray<{ name: string }> | undefined,
|
|
15
16
|
> = {
|
|
16
17
|
threadId?: string
|
|
17
18
|
live?: boolean
|
|
@@ -21,7 +22,8 @@ type ChatHookOverrides<
|
|
|
21
22
|
TTools,
|
|
22
23
|
TSchema,
|
|
23
24
|
TContext,
|
|
24
|
-
TInterrupts
|
|
25
|
+
TInterrupts,
|
|
26
|
+
TSubagents
|
|
25
27
|
>['initialMessages']
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -53,9 +55,17 @@ export function createChatHook<
|
|
|
53
55
|
const TInterrupts extends ReadonlyArray<
|
|
54
56
|
InterruptDefinition<any, any, any, any>
|
|
55
57
|
> = readonly [],
|
|
56
|
-
|
|
58
|
+
const TSubagents extends ReadonlyArray<{ name: string }> | undefined =
|
|
59
|
+
undefined,
|
|
60
|
+
>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts, TSubagents>) {
|
|
57
61
|
function useChat(
|
|
58
|
-
overrides?: ChatHookOverrides<
|
|
62
|
+
overrides?: ChatHookOverrides<
|
|
63
|
+
TTools,
|
|
64
|
+
TSchema,
|
|
65
|
+
TContext,
|
|
66
|
+
TInterrupts,
|
|
67
|
+
TSubagents
|
|
68
|
+
>,
|
|
59
69
|
) {
|
|
60
70
|
if (!overrides) {
|
|
61
71
|
return useUnboundChat(options)
|