@tanstack/ai-react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tanner Linsley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ <div align="center">
2
+ <img src="./media/header_ai.png" >
3
+ </div>
4
+
5
+ <br />
6
+
7
+ <div align="center">
8
+ <a href="https://npmjs.com/package/@tanstack/ai" target="\_parent">
9
+ <img alt="" src="https://img.shields.io/npm/dm/@tanstack/ai.svg" />
10
+ </a>
11
+ <a href="https://github.com/TanStack/ai" target="\_parent">
12
+ <img alt="" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" alt="GitHub stars" />
13
+ </a>
14
+ <a href="https://bundlephobia.com/result?p=@tanstack/ai@latest" target="\_parent">
15
+ <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/ai@latest" />
16
+ </a>
17
+ </div>
18
+
19
+ <div align="center">
20
+ <a href="#badge">
21
+ <img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
22
+ </a>
23
+ <a href="#badge">
24
+ <img src="https://img.shields.io/github/v/release/tanstack/ai" alt="Release"/>
25
+ </a>
26
+ <a href="https://twitter.com/tan_stack">
27
+ <img src="https://img.shields.io/twitter/follow/tan_stack.svg?style=social" alt="Follow @TanStack"/>
28
+ </a>
29
+ </div>
30
+
31
+ <div align="center">
32
+
33
+ ### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
34
+ </div>
35
+
36
+ # TanStack AI
37
+
38
+ A powerful, type-safe AI SDK for building AI-powered applications.
39
+
40
+ - Provider-agnostic adapters (OpenAI, Anthropic, Gemini, Ollama, etc.)
41
+ - **Multimodal content support** - Send images, audio, video, and documents
42
+ - Chat completion, streaming, and agent loop strategies
43
+ - Headless chat state management with adapters (SSE, HTTP stream, custom)
44
+ - Isomorphic type-safe tools with server/client execution
45
+ - **Enhanced integration with TanStack Start** - Share implementations between AI tools and server functions
46
+
47
+ ### <a href="https://tanstack.com/ai">Read the docs →</b></a>
48
+
49
+ ## Bonus: TanStack Start Integration
50
+
51
+ TanStack AI works with **any** framework (Next.js, Express, Remix, etc.).
52
+
53
+ **With TanStack Start**, you get a bonus: share implementations between AI tools and server functions with `createServerFnTool`:
54
+
55
+ ```typescript
56
+ import { createServerFnTool } from '@tanstack/ai-react'
57
+
58
+ // Define once, get AI tool AND server function (TanStack Start only)
59
+ const getProducts = createServerFnTool({
60
+ name: 'getProducts',
61
+ inputSchema: z.object({ query: z.string() }),
62
+ execute: async ({ query }) => db.products.search(query),
63
+ })
64
+
65
+ // Use in AI chat
66
+ chat({ tools: [getProducts.server] })
67
+
68
+ // Call directly from components (no API endpoint needed!)
69
+ const products = await getProducts.serverFn({ query: 'laptop' })
70
+ ```
71
+
72
+ No duplicate logic, full type safety, automatic validation. The `serverFn` feature requires TanStack Start. See [docs](https://tanstack.com/ai) for details.
73
+
74
+ ## Get Involved
75
+
76
+ - We welcome issues and pull requests!
77
+ - Participate in [GitHub discussions](https://github.com/TanStack/ai/discussions)
78
+ - Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ)
79
+ - See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions
80
+
81
+ ## Partners
82
+
83
+ <table align="center">
84
+ <tr>
85
+ <td>
86
+ <a href="https://www.coderabbit.ai/?via=tanstack&dub_id=aCcEEdAOqqutX6OS" >
87
+ <picture>
88
+ <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-CMcuvjEy.svg" height="40" />
89
+ <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" />
90
+ <img src="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" alt="CodeRabbit" />
91
+ </picture>
92
+ </a>
93
+ </td>
94
+ <td>
95
+ <a href="https://www.cloudflare.com?utm_source=tanstack">
96
+ <picture>
97
+ <source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-DQDB7UaL.svg" height="60" />
98
+ <source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" />
99
+ <img src="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" alt="Cloudflare" />
100
+ </picture>
101
+ </a>
102
+ </td>
103
+ </tr>
104
+ </table>
105
+
106
+ <div align="center">
107
+ <img src="./media/partner_logo.svg" alt="AI & you?" height="65">
108
+ <p>
109
+ We're looking for TanStack AI Partners to join our mission! Partner with us to push the boundaries of TanStack AI and build amazing things together.
110
+ </p>
111
+ <a href="mailto:partners@tanstack.com?subject=TanStack AI Partnership"><b>LET'S CHAT</b></a>
112
+ </div>
113
+
114
+ ## Explore the TanStack Ecosystem
115
+
116
+ - <a href="https://github.com/tanstack/config"><b>TanStack Config</b></a> – Tooling for JS/TS packages
117
+ - <a href="https://github.com/tanstack/db"><b>TanStack DB</b></a> – Reactive sync client store
118
+ - <a href="https://github.com/tanstack/devtools">TanStack Devtools</a> – Unified devtools panel
119
+ - <a href="https://github.com/tanstack/form"><b>TanStack Form</b></a> – Type‑safe form state
120
+ - <a href="https://github.com/tanstack/pacer"><b>TanStack Pacer</b></a> – Debouncing, throttling, batching
121
+ - <a href="https://github.com/tanstack/query"><b>TanStack Query</b></a> – Async state & caching
122
+ - <a href="https://github.com/tanstack/ranger"><b>TanStack Ranger</b></a> – Range & slider primitives
123
+ - <a href="https://github.com/tanstack/router"><b>TanStack Router</b></a> – Type‑safe routing, caching & URL state
124
+ - <a href="https://github.com/tanstack/router"><b>TanStack Start</b></a> – Full‑stack SSR & streaming
125
+ - <a href="https://github.com/tanstack/store"><b>TanStack Store</b></a> – Reactive data store
126
+ - <a href="https://github.com/tanstack/table"><b>TanStack Table</b></a> – Headless datagrids
127
+ - <a href="https://github.com/tanstack/virtual"><b>TanStack Virtual</b></a> – Virtualized rendering
128
+
129
+ … and more at <a href="https://tanstack.com"><b>TanStack.com »</b></a>
130
+
131
+ <!-- USE THE FORCE LUKE -->
@@ -0,0 +1,3 @@
1
+ export { useChat } from './use-chat.js';
2
+ export type { UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, } from './types.js';
3
+ export { fetchServerSentEvents, fetchHttpStream, stream, createChatClientOptions, type ConnectionAdapter, type FetchConnectionOptions, type InferChatMessages, } from '@tanstack/ai-client';
@@ -0,0 +1,10 @@
1
+ import { useChat } from "./use-chat.js";
2
+ import { createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream } from "@tanstack/ai-client";
3
+ export {
4
+ createChatClientOptions,
5
+ fetchHttpStream,
6
+ fetchServerSentEvents,
7
+ stream,
8
+ useChat
9
+ };
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,74 @@
1
+ import { AnyClientTool, ModelMessage } from '@tanstack/ai';
2
+ import { ChatClientOptions, ChatRequestBody, UIMessage } from '@tanstack/ai-client';
3
+ export type { UIMessage, ChatRequestBody };
4
+ /**
5
+ * Options for the useChat hook.
6
+ *
7
+ * This extends ChatClientOptions but omits the state change callbacks that are
8
+ * managed internally by React state:
9
+ * - `onMessagesChange` - Managed by React state (exposed as `messages`)
10
+ * - `onLoadingChange` - Managed by React state (exposed as `isLoading`)
11
+ * - `onErrorChange` - Managed by React state (exposed as `error`)
12
+ *
13
+ * All other callbacks (onResponse, onChunk, onFinish, onError) are
14
+ * passed through to the underlying ChatClient and can be used for side effects.
15
+ *
16
+ * Note: Connection and body changes will recreate the ChatClient instance.
17
+ * To update these options, remount the component or use a key prop.
18
+ */
19
+ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'>;
20
+ export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
21
+ /**
22
+ * Current messages in the conversation
23
+ */
24
+ messages: Array<UIMessage<TTools>>;
25
+ /**
26
+ * Send a message and get a response
27
+ */
28
+ sendMessage: (content: string) => Promise<void>;
29
+ /**
30
+ * Append a message to the conversation
31
+ */
32
+ append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>;
33
+ /**
34
+ * Add the result of a client-side tool execution
35
+ */
36
+ addToolResult: (result: {
37
+ toolCallId: string;
38
+ tool: string;
39
+ output: any;
40
+ state?: 'output-available' | 'output-error';
41
+ errorText?: string;
42
+ }) => Promise<void>;
43
+ /**
44
+ * Respond to a tool approval request
45
+ */
46
+ addToolApprovalResponse: (response: {
47
+ id: string;
48
+ approved: boolean;
49
+ }) => Promise<void>;
50
+ /**
51
+ * Reload the last assistant message
52
+ */
53
+ reload: () => Promise<void>;
54
+ /**
55
+ * Stop the current response generation
56
+ */
57
+ stop: () => void;
58
+ /**
59
+ * Whether a response is currently being generated
60
+ */
61
+ isLoading: boolean;
62
+ /**
63
+ * Current error, if any
64
+ */
65
+ error: Error | undefined;
66
+ /**
67
+ * Set messages manually
68
+ */
69
+ setMessages: (messages: Array<UIMessage<TTools>>) => void;
70
+ /**
71
+ * Clear all messages
72
+ */
73
+ clear: () => void;
74
+ }
@@ -0,0 +1,3 @@
1
+ import { AnyClientTool } from '@tanstack/ai';
2
+ import { UseChatOptions, UseChatReturn } from './types.js';
3
+ export declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(options: UseChatOptions<TTools>): UseChatReturn<TTools>;
@@ -0,0 +1,115 @@
1
+ import { useId, useState, useRef, useEffect, useMemo, useCallback } from "react";
2
+ import { ChatClient } from "@tanstack/ai-client";
3
+ function useChat(options) {
4
+ const hookId = useId();
5
+ const clientId = options.id || hookId;
6
+ const [messages, setMessages] = useState(
7
+ options.initialMessages || []
8
+ );
9
+ const [isLoading, setIsLoading] = useState(false);
10
+ const [error, setError] = useState(void 0);
11
+ const messagesRef = useRef(
12
+ options.initialMessages || []
13
+ );
14
+ const isFirstMountRef = useRef(true);
15
+ const optionsRef = useRef(options);
16
+ useEffect(() => {
17
+ messagesRef.current = messages;
18
+ }, [messages]);
19
+ optionsRef.current = options;
20
+ const client = useMemo(() => {
21
+ const messagesToUse = isFirstMountRef.current ? options.initialMessages || [] : messagesRef.current;
22
+ isFirstMountRef.current = false;
23
+ return new ChatClient({
24
+ connection: optionsRef.current.connection,
25
+ id: clientId,
26
+ initialMessages: messagesToUse,
27
+ body: optionsRef.current.body,
28
+ onResponse: optionsRef.current.onResponse,
29
+ onChunk: optionsRef.current.onChunk,
30
+ onFinish: optionsRef.current.onFinish,
31
+ onError: optionsRef.current.onError,
32
+ tools: optionsRef.current.tools,
33
+ streamProcessor: options.streamProcessor,
34
+ onMessagesChange: (newMessages) => {
35
+ setMessages(newMessages);
36
+ },
37
+ onLoadingChange: (newIsLoading) => {
38
+ setIsLoading(newIsLoading);
39
+ },
40
+ onErrorChange: (newError) => {
41
+ setError(newError);
42
+ }
43
+ });
44
+ }, [clientId]);
45
+ useEffect(() => {
46
+ if (options.initialMessages && options.initialMessages.length > 0) {
47
+ if (messages.length === 0) {
48
+ client.setMessagesManually(options.initialMessages);
49
+ }
50
+ }
51
+ }, []);
52
+ useEffect(() => {
53
+ return () => {
54
+ if (isLoading) {
55
+ client.stop();
56
+ }
57
+ };
58
+ }, [client, isLoading]);
59
+ const sendMessage = useCallback(
60
+ async (content) => {
61
+ await client.sendMessage(content);
62
+ },
63
+ [client]
64
+ );
65
+ const append = useCallback(
66
+ async (message) => {
67
+ await client.append(message);
68
+ },
69
+ [client]
70
+ );
71
+ const reload = useCallback(async () => {
72
+ await client.reload();
73
+ }, [client]);
74
+ const stop = useCallback(() => {
75
+ client.stop();
76
+ }, [client]);
77
+ const clear = useCallback(() => {
78
+ client.clear();
79
+ }, [client]);
80
+ const setMessagesManually = useCallback(
81
+ (newMessages) => {
82
+ client.setMessagesManually(newMessages);
83
+ },
84
+ [client]
85
+ );
86
+ const addToolResult = useCallback(
87
+ async (result) => {
88
+ await client.addToolResult(result);
89
+ },
90
+ [client]
91
+ );
92
+ const addToolApprovalResponse = useCallback(
93
+ async (response) => {
94
+ await client.addToolApprovalResponse(response);
95
+ },
96
+ [client]
97
+ );
98
+ return {
99
+ messages,
100
+ sendMessage,
101
+ append,
102
+ reload,
103
+ stop,
104
+ isLoading,
105
+ error,
106
+ setMessages: setMessagesManually,
107
+ clear,
108
+ addToolResult,
109
+ addToolApprovalResponse
110
+ };
111
+ }
112
+ export {
113
+ useChat
114
+ };
115
+ //# sourceMappingURL=use-chat.js.map
@@ -0,0 +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 // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools>>(options)\n\n // Update ref whenever messages change\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\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 useEffect(() => {\n return () => {\n // Stop any active generation when component unmounts\n if (isLoading) {\n client.stop()\n }\n }\n }, [client, isLoading])\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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"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@tanstack/ai-react",
3
+ "version": "0.0.1",
4
+ "description": "React hooks for TanStack AI",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/TanStack/ai.git",
10
+ "directory": "packages/typescript/ai-react"
11
+ },
12
+ "type": "module",
13
+ "module": "./dist/esm/index.js",
14
+ "types": "./dist/esm/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/esm/index.d.ts",
18
+ "import": "./dist/esm/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "keywords": [
26
+ "ai",
27
+ "react",
28
+ "hooks",
29
+ "tanstack",
30
+ "chat",
31
+ "streaming"
32
+ ],
33
+ "dependencies": {
34
+ "@tanstack/ai": "0.0.1",
35
+ "@tanstack/ai-client": "0.0.1"
36
+ },
37
+ "devDependencies": {
38
+ "@testing-library/react": "^16.3.0",
39
+ "@types/react": "^19.2.7",
40
+ "@vitest/coverage-v8": "4.0.14",
41
+ "jsdom": "^27.2.0",
42
+ "vite": "^7.2.4",
43
+ "zod": "^4.1.13"
44
+ },
45
+ "peerDependencies": {
46
+ "@types/react": ">=18.0.0",
47
+ "react": ">=18.0.0",
48
+ "@tanstack/ai": "0.0.1",
49
+ "@tanstack/ai-client": "0.0.1"
50
+ },
51
+ "scripts": {
52
+ "clean": "premove ./build ./dist",
53
+ "lint:fix": "eslint ./src --fix",
54
+ "test:eslint": "eslint ./src",
55
+ "test:lib": "vitest run",
56
+ "test:lib:dev": "pnpm test:lib --watch",
57
+ "test:types": "tsc",
58
+ "test:build": "publint --strict",
59
+ "build": "vite build"
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ export { useChat } from './use-chat'
2
+ export type {
3
+ UseChatOptions,
4
+ UseChatReturn,
5
+ UIMessage,
6
+ ChatRequestBody,
7
+ } from './types'
8
+
9
+ // Re-export from ai-client for convenience
10
+ export {
11
+ fetchServerSentEvents,
12
+ fetchHttpStream,
13
+ stream,
14
+ createChatClientOptions,
15
+ type ConnectionAdapter,
16
+ type FetchConnectionOptions,
17
+ type InferChatMessages,
18
+ } from '@tanstack/ai-client'
package/src/types.ts ADDED
@@ -0,0 +1,101 @@
1
+ import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
2
+ import type {
3
+ ChatClientOptions,
4
+ ChatRequestBody,
5
+ UIMessage,
6
+ } from '@tanstack/ai-client'
7
+
8
+ // Re-export types from ai-client
9
+ export type { UIMessage, ChatRequestBody }
10
+
11
+ /**
12
+ * Options for the useChat hook.
13
+ *
14
+ * This extends ChatClientOptions but omits the state change callbacks that are
15
+ * managed internally by React state:
16
+ * - `onMessagesChange` - Managed by React state (exposed as `messages`)
17
+ * - `onLoadingChange` - Managed by React state (exposed as `isLoading`)
18
+ * - `onErrorChange` - Managed by React state (exposed as `error`)
19
+ *
20
+ * All other callbacks (onResponse, onChunk, onFinish, onError) are
21
+ * passed through to the underlying ChatClient and can be used for side effects.
22
+ *
23
+ * Note: Connection and body changes will recreate the ChatClient instance.
24
+ * To update these options, remount the component or use a key prop.
25
+ */
26
+ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
27
+ Omit<
28
+ ChatClientOptions<TTools>,
29
+ 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'
30
+ >
31
+
32
+ export interface UseChatReturn<
33
+ TTools extends ReadonlyArray<AnyClientTool> = any,
34
+ > {
35
+ /**
36
+ * Current messages in the conversation
37
+ */
38
+ messages: Array<UIMessage<TTools>>
39
+
40
+ /**
41
+ * Send a message and get a response
42
+ */
43
+ sendMessage: (content: string) => Promise<void>
44
+
45
+ /**
46
+ * Append a message to the conversation
47
+ */
48
+ append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>
49
+
50
+ /**
51
+ * Add the result of a client-side tool execution
52
+ */
53
+ addToolResult: (result: {
54
+ toolCallId: string
55
+ tool: string
56
+ output: any
57
+ state?: 'output-available' | 'output-error'
58
+ errorText?: string
59
+ }) => Promise<void>
60
+
61
+ /**
62
+ * Respond to a tool approval request
63
+ */
64
+ addToolApprovalResponse: (response: {
65
+ id: string // approval.id, not toolCallId
66
+ approved: boolean
67
+ }) => Promise<void>
68
+
69
+ /**
70
+ * Reload the last assistant message
71
+ */
72
+ reload: () => Promise<void>
73
+
74
+ /**
75
+ * Stop the current response generation
76
+ */
77
+ stop: () => void
78
+
79
+ /**
80
+ * Whether a response is currently being generated
81
+ */
82
+ isLoading: boolean
83
+
84
+ /**
85
+ * Current error, if any
86
+ */
87
+ error: Error | undefined
88
+
89
+ /**
90
+ * Set messages manually
91
+ */
92
+ setMessages: (messages: Array<UIMessage<TTools>>) => void
93
+
94
+ /**
95
+ * Clear all messages
96
+ */
97
+ clear: () => void
98
+ }
99
+
100
+ // Note: createChatClientOptions and InferChatMessages are now in @tanstack/ai-client
101
+ // and re-exported from there for convenience
@@ -0,0 +1,162 @@
1
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
2
+ import { ChatClient } from '@tanstack/ai-client'
3
+ import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
4
+
5
+ import type { UIMessage, UseChatOptions, UseChatReturn } from './types'
6
+
7
+ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
8
+ options: UseChatOptions<TTools>,
9
+ ): UseChatReturn<TTools> {
10
+ const hookId = useId()
11
+ const clientId = options.id || hookId
12
+
13
+ const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(
14
+ options.initialMessages || [],
15
+ )
16
+ const [isLoading, setIsLoading] = useState(false)
17
+ const [error, setError] = useState<Error | undefined>(undefined)
18
+
19
+ // Track current messages in a ref to preserve them when client is recreated
20
+ const messagesRef = useRef<Array<UIMessage<TTools>>>(
21
+ options.initialMessages || [],
22
+ )
23
+ const isFirstMountRef = useRef(true)
24
+
25
+ // Track current options in a ref to avoid recreating client when options change
26
+ const optionsRef = useRef<UseChatOptions<TTools>>(options)
27
+
28
+ // Update ref whenever messages change
29
+ useEffect(() => {
30
+ messagesRef.current = messages
31
+ }, [messages])
32
+
33
+ optionsRef.current = options
34
+
35
+ // Create ChatClient instance with callbacks to sync state
36
+ // Note: Options are captured at client creation time.
37
+ // The connection adapter can use functions for dynamic values (url, headers, etc.)
38
+ // which are evaluated lazily on each request.
39
+ const client = useMemo(() => {
40
+ // On first mount, use initialMessages. On subsequent recreations, preserve existing messages.
41
+ const messagesToUse = isFirstMountRef.current
42
+ ? options.initialMessages || []
43
+ : messagesRef.current
44
+
45
+ isFirstMountRef.current = false
46
+
47
+ return new ChatClient({
48
+ connection: optionsRef.current.connection,
49
+ id: clientId,
50
+ initialMessages: messagesToUse,
51
+ body: optionsRef.current.body,
52
+ onResponse: optionsRef.current.onResponse,
53
+ onChunk: optionsRef.current.onChunk,
54
+ onFinish: optionsRef.current.onFinish,
55
+ onError: optionsRef.current.onError,
56
+ tools: optionsRef.current.tools,
57
+ streamProcessor: options.streamProcessor,
58
+ onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
59
+ setMessages(newMessages)
60
+ },
61
+ onLoadingChange: (newIsLoading: boolean) => {
62
+ setIsLoading(newIsLoading)
63
+ },
64
+ onErrorChange: (newError: Error | undefined) => {
65
+ setError(newError)
66
+ },
67
+ })
68
+ }, [clientId])
69
+
70
+ // Sync initial messages on mount only
71
+ // Note: initialMessages are passed to ChatClient constructor, but we also
72
+ // set them here to ensure React state is in sync
73
+ useEffect(() => {
74
+ if (options.initialMessages && options.initialMessages.length > 0) {
75
+ // Only set if current messages are empty (initial state)
76
+ if (messages.length === 0) {
77
+ client.setMessagesManually(options.initialMessages)
78
+ }
79
+ }
80
+ }, []) // Only run on mount - initialMessages are handled by ChatClient constructor
81
+
82
+ // Cleanup on unmount: stop any in-flight requests
83
+ useEffect(() => {
84
+ return () => {
85
+ // Stop any active generation when component unmounts
86
+ if (isLoading) {
87
+ client.stop()
88
+ }
89
+ }
90
+ }, [client, isLoading])
91
+
92
+ // Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall)
93
+ // are captured at client creation time. Changes to these callbacks require
94
+ // remounting the component or changing the connection to recreate the client.
95
+
96
+ const sendMessage = useCallback(
97
+ async (content: string) => {
98
+ await client.sendMessage(content)
99
+ },
100
+ [client],
101
+ )
102
+
103
+ const append = useCallback(
104
+ async (message: ModelMessage | UIMessage) => {
105
+ await client.append(message)
106
+ },
107
+ [client],
108
+ )
109
+
110
+ const reload = useCallback(async () => {
111
+ await client.reload()
112
+ }, [client])
113
+
114
+ const stop = useCallback(() => {
115
+ client.stop()
116
+ }, [client])
117
+
118
+ const clear = useCallback(() => {
119
+ client.clear()
120
+ }, [client])
121
+
122
+ const setMessagesManually = useCallback(
123
+ (newMessages: Array<UIMessage<TTools>>) => {
124
+ client.setMessagesManually(newMessages)
125
+ },
126
+ [client],
127
+ )
128
+
129
+ const addToolResult = useCallback(
130
+ async (result: {
131
+ toolCallId: string
132
+ tool: string
133
+ output: any
134
+ state?: 'output-available' | 'output-error'
135
+ errorText?: string
136
+ }) => {
137
+ await client.addToolResult(result)
138
+ },
139
+ [client],
140
+ )
141
+
142
+ const addToolApprovalResponse = useCallback(
143
+ async (response: { id: string; approved: boolean }) => {
144
+ await client.addToolApprovalResponse(response)
145
+ },
146
+ [client],
147
+ )
148
+
149
+ return {
150
+ messages,
151
+ sendMessage,
152
+ append,
153
+ reload,
154
+ stop,
155
+ isLoading,
156
+ error,
157
+ setMessages: setMessagesManually,
158
+ clear,
159
+ addToolResult,
160
+ addToolApprovalResponse,
161
+ }
162
+ }