@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.
@@ -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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-react",
3
- "version": "0.27.3",
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.33.2"
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.58.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.58.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 ChatUIToolName<TOptions>]?: ComponentType<
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 ChatUIToolName<TOptions>]?: BoundWidget
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: ChatUIMessages<TOptions>[number]
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?.[name as ChatUIToolName<TOptions>] as
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
- const PartComponent = (parts[selected.key] ?? parts.fallback) as
632
- | ComponentType<PartProps<TOptions>>
633
- | undefined
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
  }
@@ -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
- >(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>) {
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<TTools, TSchema, TContext, TInterrupts>,
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)