@tanstack/ai-react 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,7 @@ A powerful, type-safe AI SDK for building AI-powered applications.
45
45
  - Headless chat state management with adapters (SSE, HTTP stream, custom)
46
46
  - Isomorphic type-safe tools with server/client execution
47
47
  - **Enhanced integration with TanStack Start** - Share implementations between AI tools and server functions
48
+ - **Observability events** - Structured, typed events for text, tools, image, speech, transcription, and video ([docs](./docs/guides/observability.md))
48
49
 
49
50
  ### <a href="https://tanstack.com/ai">Read the docs →</b></a>
50
51
 
@@ -1,6 +1,6 @@
1
1
  import { AnyClientTool, ModelMessage } from '@tanstack/ai';
2
- import { ChatClientOptions, ChatRequestBody, UIMessage } from '@tanstack/ai-client';
3
- export type { UIMessage, ChatRequestBody };
2
+ import { ChatClientOptions, ChatClientState, ChatRequestBody, UIMessage } from '@tanstack/ai-client';
3
+ export type { ChatRequestBody, UIMessage };
4
4
  /**
5
5
  * Options for the useChat hook.
6
6
  *
@@ -9,6 +9,7 @@ export type { UIMessage, ChatRequestBody };
9
9
  * - `onMessagesChange` - Managed by React state (exposed as `messages`)
10
10
  * - `onLoadingChange` - Managed by React state (exposed as `isLoading`)
11
11
  * - `onErrorChange` - Managed by React state (exposed as `error`)
12
+ * - `onStatusChange` - Managed by React state (exposed as `status`)
12
13
  *
13
14
  * All other callbacks (onResponse, onChunk, onFinish, onError) are
14
15
  * passed through to the underlying ChatClient and can be used for side effects.
@@ -16,7 +17,7 @@ export type { UIMessage, ChatRequestBody };
16
17
  * Note: Connection and body changes will recreate the ChatClient instance.
17
18
  * To update these options, remount the component or use a key prop.
18
19
  */
19
- export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'>;
20
+ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange'>;
20
21
  export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
21
22
  /**
22
23
  * Current messages in the conversation
@@ -63,6 +64,10 @@ export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any
63
64
  * Current error, if any
64
65
  */
65
66
  error: Error | undefined;
67
+ /**
68
+ * Current status of the chat client
69
+ */
70
+ status: ChatClientState;
66
71
  /**
67
72
  * Set messages manually
68
73
  */
@@ -1,5 +1,5 @@
1
- import { useId, useState, useRef, useEffect, useMemo, useCallback } from "react";
2
1
  import { ChatClient } from "@tanstack/ai-client";
2
+ import { useId, useState, useRef, useEffect, useMemo, useCallback } from "react";
3
3
  function useChat(options) {
4
4
  const hookId = useId();
5
5
  const clientId = options.id || hookId;
@@ -8,6 +8,7 @@ function useChat(options) {
8
8
  );
9
9
  const [isLoading, setIsLoading] = useState(false);
10
10
  const [error, setError] = useState(void 0);
11
+ const [status, setStatus] = useState("ready");
11
12
  const messagesRef = useRef(
12
13
  options.initialMessages || []
13
14
  );
@@ -27,8 +28,12 @@ function useChat(options) {
27
28
  body: optionsRef.current.body,
28
29
  onResponse: optionsRef.current.onResponse,
29
30
  onChunk: optionsRef.current.onChunk,
30
- onFinish: optionsRef.current.onFinish,
31
- onError: optionsRef.current.onError,
31
+ onFinish: (message) => {
32
+ optionsRef.current.onFinish?.(message);
33
+ },
34
+ onError: (error2) => {
35
+ optionsRef.current.onError?.(error2);
36
+ },
32
37
  tools: optionsRef.current.tools,
33
38
  streamProcessor: options.streamProcessor,
34
39
  onMessagesChange: (newMessages) => {
@@ -39,9 +44,15 @@ function useChat(options) {
39
44
  },
40
45
  onErrorChange: (newError) => {
41
46
  setError(newError);
47
+ },
48
+ onStatusChange: (status2) => {
49
+ setStatus(status2);
42
50
  }
43
51
  });
44
52
  }, [clientId]);
53
+ useEffect(() => {
54
+ client.updateOptions({ body: options.body });
55
+ }, [client, options.body]);
45
56
  useEffect(() => {
46
57
  if (options.initialMessages && options.initialMessages.length > 0) {
47
58
  if (messages.length === 0) {
@@ -101,6 +112,7 @@ function useChat(options) {
101
112
  stop,
102
113
  isLoading,
103
114
  error,
115
+ status,
104
116
  setMessages: setMessagesManually,
105
117
  clear,
106
118
  addToolResult,
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport { ChatClient } from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\n\nimport type { UIMessage, UseChatOptions, UseChatReturn } from './types'\n\nexport function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools>,\n): UseChatReturn<TTools> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n\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\n // Update ref whenever messages change\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n // Note: Options are captured at client creation time.\n // The connection adapter can use functions for dynamic values (url, headers, etc.)\n // which are evaluated lazily on each request.\n const client = useMemo(() => {\n // On first mount, use initialMessages. On subsequent recreations, preserve existing messages.\n const messagesToUse = isFirstMountRef.current\n ? options.initialMessages || []\n : messagesRef.current\n\n isFirstMountRef.current = false\n\n return new ChatClient({\n connection: optionsRef.current.connection,\n id: clientId,\n initialMessages: messagesToUse,\n body: optionsRef.current.body,\n onResponse: optionsRef.current.onResponse,\n onChunk: optionsRef.current.onChunk,\n onFinish: optionsRef.current.onFinish,\n onError: optionsRef.current.onError,\n tools: optionsRef.current.tools,\n streamProcessor: options.streamProcessor,\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n })\n }, [clientId])\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n useEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages.length === 0) {\n client.setMessagesManually(options.initialMessages)\n }\n }\n }, []) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Cleanup on unmount: stop any in-flight requests\n // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\n useEffect(() => {\n return () => {\n // Stop any active generation when component unmounts or client changes\n client.stop()\n }\n }, [client])\n\n // Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall)\n // are captured at client creation time. Changes to these callbacks require\n // remounting the component or changing the connection to recreate the client.\n\n const sendMessage = useCallback(\n async (content: string) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\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 },\n [client],\n )\n\n return {\n messages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\nimport type { ChatClientState } from '@tanstack/ai-client'\n\nimport type { UIMessage, UseChatOptions, UseChatReturn } from './types'\n\nexport function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools>,\n): UseChatReturn<TTools> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n\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\n // Update ref whenever messages change\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools>>(options)\n optionsRef.current = options\n\n // Create ChatClient instance with callbacks to sync state\n // Note: Options are captured at client creation time.\n // The connection adapter can use functions for dynamic values (url, headers, etc.)\n // which are evaluated lazily on each request.\n const client = useMemo(() => {\n // On first mount, use initialMessages. On subsequent recreations, preserve existing messages.\n const messagesToUse = isFirstMountRef.current\n ? options.initialMessages || []\n : messagesRef.current\n\n isFirstMountRef.current = false\n\n return new ChatClient({\n connection: optionsRef.current.connection,\n id: clientId,\n initialMessages: messagesToUse,\n body: optionsRef.current.body,\n onResponse: optionsRef.current.onResponse,\n onChunk: optionsRef.current.onChunk,\n onFinish: (message: UIMessage<TTools>) => {\n optionsRef.current.onFinish?.(message)\n },\n onError: (error: Error) => {\n optionsRef.current.onError?.(error)\n },\n tools: optionsRef.current.tools,\n streamProcessor: options.streamProcessor,\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onStatusChange: (status: ChatClientState) => {\n setStatus(status)\n },\n })\n }, [clientId])\n\n // Sync body changes to the client\n // This allows dynamic body values (like model selection) to be updated without recreating the client\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n useEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages.length === 0) {\n client.setMessagesManually(options.initialMessages)\n }\n }\n }, []) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Cleanup on unmount: stop any in-flight requests\n // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\n useEffect(() => {\n return () => {\n // Stop any active generation when component unmounts or client changes\n client.stop()\n }\n }, [client])\n\n // Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall)\n // are captured at client creation time. Changes to these callbacks require\n // remounting the component or changing the connection to recreate the client.\n\n const sendMessage = useCallback(\n async (content: string) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\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 },\n [client],\n )\n\n return {\n messages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n }\n}\n"],"names":["error","status"],"mappings":";;AAOO,SAAS,QACd,SACuB;AACvB,QAAM,SAAS,MAAA;AACf,QAAM,WAAW,QAAQ,MAAM;AAE/B,QAAM,CAAC,UAAU,WAAW,IAAI;AAAA,IAC9B,QAAQ,mBAAmB,CAAA;AAAA,EAAC;AAE9B,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA4B,MAAS;AAC/D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA0B,OAAO;AAG7D,QAAM,cAAc;AAAA,IAClB,QAAQ,mBAAmB,CAAA;AAAA,EAAC;AAE9B,QAAM,kBAAkB,OAAO,IAAI;AAGnC,YAAU,MAAM;AACd,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,QAAQ,CAAC;AAGb,QAAM,aAAa,OAA+B,OAAO;AACzD,aAAW,UAAU;AAMrB,QAAM,SAAS,QAAQ,MAAM;AAE3B,UAAM,gBAAgB,gBAAgB,UAClC,QAAQ,mBAAmB,CAAA,IAC3B,YAAY;AAEhB,oBAAgB,UAAU;AAE1B,WAAO,IAAI,WAAW;AAAA,MACpB,YAAY,WAAW,QAAQ;AAAA,MAC/B,IAAI;AAAA,MACJ,iBAAiB;AAAA,MACjB,MAAM,WAAW,QAAQ;AAAA,MACzB,YAAY,WAAW,QAAQ;AAAA,MAC/B,SAAS,WAAW,QAAQ;AAAA,MAC5B,UAAU,CAAC,YAA+B;AACxC,mBAAW,QAAQ,WAAW,OAAO;AAAA,MACvC;AAAA,MACA,SAAS,CAACA,WAAiB;AACzB,mBAAW,QAAQ,UAAUA,MAAK;AAAA,MACpC;AAAA,MACA,OAAO,WAAW,QAAQ;AAAA,MAC1B,iBAAiB,QAAQ;AAAA,MACzB,kBAAkB,CAAC,gBAA0C;AAC3D,oBAAY,WAAW;AAAA,MACzB;AAAA,MACA,iBAAiB,CAAC,iBAA0B;AAC1C,qBAAa,YAAY;AAAA,MAC3B;AAAA,MACA,eAAe,CAAC,aAAgC;AAC9C,iBAAS,QAAQ;AAAA,MACnB;AAAA,MACA,gBAAgB,CAACC,YAA4B;AAC3C,kBAAUA,OAAM;AAAA,MAClB;AAAA,IAAA,CACD;AAAA,EACH,GAAG,CAAC,QAAQ,CAAC;AAIb,YAAU,MAAM;AACd,WAAO,cAAc,EAAE,MAAM,QAAQ,MAAM;AAAA,EAC7C,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;AAKzB,YAAU,MAAM;AACd,QAAI,QAAQ,mBAAmB,QAAQ,gBAAgB,SAAS,GAAG;AAEjE,UAAI,SAAS,WAAW,GAAG;AACzB,eAAO,oBAAoB,QAAQ,eAAe;AAAA,MACpD;AAAA,IACF;AAAA,EACF,GAAG,CAAA,CAAE;AAML,YAAU,MAAM;AACd,WAAO,MAAM;AAEX,aAAO,KAAA;AAAA,IACT;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAMX,QAAM,cAAc;AAAA,IAClB,OAAO,YAAoB;AACzB,YAAM,OAAO,YAAY,OAAO;AAAA,IAClC;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,SAAS;AAAA,IACb,OAAO,YAAsC;AAC3C,YAAM,OAAO,OAAO,OAAO;AAAA,IAC7B;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,SAAS,YAAY,YAAY;AACrC,UAAM,OAAO,OAAA;AAAA,EACf,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,OAAO,YAAY,MAAM;AAC7B,WAAO,KAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,QAAQ,YAAY,MAAM;AAC9B,WAAO,MAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,sBAAsB;AAAA,IAC1B,CAAC,gBAA0C;AACzC,aAAO,oBAAoB,WAAW;AAAA,IACxC;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,gBAAgB;AAAA,IACpB,OAAO,WAMD;AACJ,YAAM,OAAO,cAAc,MAAM;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,0BAA0B;AAAA,IAC9B,OAAO,aAAgD;AACrD,YAAM,OAAO,wBAAwB,QAAQ;AAAA,IAC/C;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-react",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "React hooks for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -31,12 +31,12 @@
31
31
  "streaming"
32
32
  ],
33
33
  "dependencies": {
34
- "@tanstack/ai-client": "0.2.2"
34
+ "@tanstack/ai-client": "0.4.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@types/react": ">=18.0.0",
38
38
  "react": ">=18.0.0",
39
- "@tanstack/ai": "^0.2.2"
39
+ "@tanstack/ai": "^0.3.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@testing-library/react": "^16.3.0",
@@ -45,7 +45,7 @@
45
45
  "jsdom": "^27.2.0",
46
46
  "react": "^19.2.3",
47
47
  "vite": "^7.2.7",
48
- "@tanstack/ai": "0.2.2"
48
+ "@tanstack/ai": "0.3.1"
49
49
  },
50
50
  "scripts": {
51
51
  "clean": "premove ./build ./dist",
package/src/types.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
2
2
  import type {
3
3
  ChatClientOptions,
4
+ ChatClientState,
4
5
  ChatRequestBody,
5
6
  UIMessage,
6
7
  } from '@tanstack/ai-client'
7
8
 
8
9
  // Re-export types from ai-client
9
- export type { UIMessage, ChatRequestBody }
10
+ export type { ChatRequestBody, UIMessage }
10
11
 
11
12
  /**
12
13
  * Options for the useChat hook.
@@ -16,6 +17,7 @@ export type { UIMessage, ChatRequestBody }
16
17
  * - `onMessagesChange` - Managed by React state (exposed as `messages`)
17
18
  * - `onLoadingChange` - Managed by React state (exposed as `isLoading`)
18
19
  * - `onErrorChange` - Managed by React state (exposed as `error`)
20
+ * - `onStatusChange` - Managed by React state (exposed as `status`)
19
21
  *
20
22
  * All other callbacks (onResponse, onChunk, onFinish, onError) are
21
23
  * passed through to the underlying ChatClient and can be used for side effects.
@@ -26,7 +28,7 @@ export type { UIMessage, ChatRequestBody }
26
28
  export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
27
29
  Omit<
28
30
  ChatClientOptions<TTools>,
29
- 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'
31
+ 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange'
30
32
  >
31
33
 
32
34
  export interface UseChatReturn<
@@ -86,6 +88,11 @@ export interface UseChatReturn<
86
88
  */
87
89
  error: Error | undefined
88
90
 
91
+ /**
92
+ * Current status of the chat client
93
+ */
94
+ status: ChatClientState
95
+
89
96
  /**
90
97
  * Set messages manually
91
98
  */
package/src/use-chat.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
2
1
  import { ChatClient } from '@tanstack/ai-client'
2
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
3
3
  import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
4
+ import type { ChatClientState } from '@tanstack/ai-client'
4
5
 
5
6
  import type { UIMessage, UseChatOptions, UseChatReturn } from './types'
6
7
 
@@ -15,6 +16,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
15
16
  )
16
17
  const [isLoading, setIsLoading] = useState(false)
17
18
  const [error, setError] = useState<Error | undefined>(undefined)
19
+ const [status, setStatus] = useState<ChatClientState>('ready')
18
20
 
19
21
  // Track current messages in a ref to preserve them when client is recreated
20
22
  const messagesRef = useRef<Array<UIMessage<TTools>>>(
@@ -50,8 +52,12 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
50
52
  body: optionsRef.current.body,
51
53
  onResponse: optionsRef.current.onResponse,
52
54
  onChunk: optionsRef.current.onChunk,
53
- onFinish: optionsRef.current.onFinish,
54
- onError: optionsRef.current.onError,
55
+ onFinish: (message: UIMessage<TTools>) => {
56
+ optionsRef.current.onFinish?.(message)
57
+ },
58
+ onError: (error: Error) => {
59
+ optionsRef.current.onError?.(error)
60
+ },
55
61
  tools: optionsRef.current.tools,
56
62
  streamProcessor: options.streamProcessor,
57
63
  onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
@@ -63,9 +69,18 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
63
69
  onErrorChange: (newError: Error | undefined) => {
64
70
  setError(newError)
65
71
  },
72
+ onStatusChange: (status: ChatClientState) => {
73
+ setStatus(status)
74
+ },
66
75
  })
67
76
  }, [clientId])
68
77
 
78
+ // Sync body changes to the client
79
+ // This allows dynamic body values (like model selection) to be updated without recreating the client
80
+ useEffect(() => {
81
+ client.updateOptions({ body: options.body })
82
+ }, [client, options.body])
83
+
69
84
  // Sync initial messages on mount only
70
85
  // Note: initialMessages are passed to ChatClient constructor, but we also
71
86
  // set them here to ensure React state is in sync
@@ -154,6 +169,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
154
169
  stop,
155
170
  isLoading,
156
171
  error,
172
+ status,
157
173
  setMessages: setMessagesManually,
158
174
  clear,
159
175
  addToolResult,