@tanstack/ai-react 0.22.4 → 0.24.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.
Files changed (64) hide show
  1. package/README.md +11 -15
  2. package/dist/esm/chat-ui/chat-input.d.ts +57 -0
  3. package/dist/esm/chat-ui/chat-input.js +124 -0
  4. package/dist/esm/chat-ui/chat-input.js.map +1 -0
  5. package/dist/esm/chat-ui/chat-message.d.ts +91 -0
  6. package/dist/esm/chat-ui/chat-message.js +157 -0
  7. package/dist/esm/chat-ui/chat-message.js.map +1 -0
  8. package/dist/esm/chat-ui/chat-messages.d.ts +33 -0
  9. package/dist/esm/chat-ui/chat-messages.js +44 -0
  10. package/dist/esm/chat-ui/chat-messages.js.map +1 -0
  11. package/dist/esm/chat-ui/chat.d.ts +53 -0
  12. package/dist/esm/chat-ui/chat.js +57 -0
  13. package/dist/esm/chat-ui/chat.js.map +1 -0
  14. package/dist/esm/chat-ui/create-chat-hook.d.ts +48 -0
  15. package/dist/esm/chat-ui/create-chat-hook.js +57 -0
  16. package/dist/esm/chat-ui/create-chat-hook.js.map +1 -0
  17. package/dist/esm/chat-ui/create-ui-contexts.d.ts +29 -0
  18. package/dist/esm/chat-ui/create-ui-contexts.js +36 -0
  19. package/dist/esm/chat-ui/create-ui-contexts.js.map +1 -0
  20. package/dist/esm/chat-ui/create-ui.d.ts +153 -0
  21. package/dist/esm/chat-ui/create-ui.js +294 -0
  22. package/dist/esm/chat-ui/create-ui.js.map +1 -0
  23. package/dist/esm/chat-ui/markdown-plugins.d.ts +15 -0
  24. package/dist/esm/chat-ui/markdown-plugins.js +28 -0
  25. package/dist/esm/chat-ui/markdown-plugins.js.map +1 -0
  26. package/dist/esm/chat-ui/text-part.d.ts +57 -0
  27. package/dist/esm/chat-ui/text-part.js +46 -0
  28. package/dist/esm/chat-ui/text-part.js.map +1 -0
  29. package/dist/esm/chat-ui/thinking-part.d.ts +41 -0
  30. package/dist/esm/chat-ui/thinking-part.js +73 -0
  31. package/dist/esm/chat-ui/thinking-part.js.map +1 -0
  32. package/dist/esm/chat-ui/tool-approval.d.ts +52 -0
  33. package/dist/esm/chat-ui/tool-approval.js +83 -0
  34. package/dist/esm/chat-ui/tool-approval.js.map +1 -0
  35. package/dist/esm/chat-ui/tool-result-content.d.ts +18 -0
  36. package/dist/esm/chat-ui/tool-result-content.js +19 -0
  37. package/dist/esm/chat-ui/tool-result-content.js.map +1 -0
  38. package/dist/esm/create-chat-hook.d.ts +35 -0
  39. package/dist/esm/create-chat-hook.js +37 -0
  40. package/dist/esm/create-chat-hook.js.map +1 -0
  41. package/dist/esm/index.d.ts +3 -0
  42. package/dist/esm/index.js +3 -1
  43. package/dist/esm/ui.d.ts +10 -0
  44. package/dist/esm/ui.js +11 -0
  45. package/dist/esm/use-web-mcp-tools.d.ts +27 -0
  46. package/dist/esm/use-web-mcp-tools.js +36 -0
  47. package/dist/esm/use-web-mcp-tools.js.map +1 -0
  48. package/package.json +18 -4
  49. package/src/chat-ui/chat-input.tsx +177 -0
  50. package/src/chat-ui/chat-message.tsx +271 -0
  51. package/src/chat-ui/chat-messages.tsx +89 -0
  52. package/src/chat-ui/chat.tsx +98 -0
  53. package/src/chat-ui/create-chat-hook.tsx +103 -0
  54. package/src/chat-ui/create-ui-contexts.tsx +57 -0
  55. package/src/chat-ui/create-ui.tsx +761 -0
  56. package/src/chat-ui/markdown-plugins.ts +48 -0
  57. package/src/chat-ui/text-part.tsx +97 -0
  58. package/src/chat-ui/thinking-part.tsx +83 -0
  59. package/src/chat-ui/tool-approval.tsx +132 -0
  60. package/src/chat-ui/tool-result-content.ts +32 -0
  61. package/src/create-chat-hook.ts +67 -0
  62. package/src/index.ts +3 -0
  63. package/src/ui.ts +43 -0
  64. package/src/use-web-mcp-tools.ts +59 -0
@@ -0,0 +1,52 @@
1
+ import { ReactNode } from 'react';
2
+ export interface ToolApprovalProps {
3
+ /** Tool call ID */
4
+ toolCallId: string;
5
+ /** Tool name */
6
+ toolName: string;
7
+ /** Parsed tool arguments/input */
8
+ input: any;
9
+ /** Approval metadata */
10
+ approval: {
11
+ id: string;
12
+ needsApproval: boolean;
13
+ approved?: boolean;
14
+ };
15
+ /** CSS class name */
16
+ className?: string;
17
+ /** Custom render prop */
18
+ children?: (props: ToolApprovalRenderProps) => ReactNode;
19
+ }
20
+ export interface ToolApprovalRenderProps {
21
+ /** Tool name */
22
+ toolName: string;
23
+ /** Parsed input */
24
+ input: any;
25
+ /** Approve the tool call */
26
+ onApprove: () => void;
27
+ /** Deny the tool call */
28
+ onDeny: () => void;
29
+ /** Whether user has responded */
30
+ hasResponded: boolean;
31
+ /** User's decision (if responded) */
32
+ approved?: boolean;
33
+ }
34
+ /**
35
+ * @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`.
36
+ * Deprecated in 0.9.0. Removed in 1.0.0.
37
+ *
38
+ * Tool approval component - renders approve/deny buttons for tools that need approval
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * {part.approval && (
43
+ * <ToolApproval
44
+ * toolCallId={part.id}
45
+ * toolName={part.name}
46
+ * input={JSON.parse(part.arguments)}
47
+ * approval={part.approval}
48
+ * />
49
+ * )}
50
+ * ```
51
+ */
52
+ export declare function ToolApproval({ toolCallId: _, toolName, input, approval, className, children, }: ToolApprovalProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,83 @@
1
+ import { useChatContext } from "./chat.js";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ //#region src/chat-ui/tool-approval.tsx
4
+ /**
5
+ * @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`.
6
+ * Deprecated in 0.9.0. Removed in 1.0.0.
7
+ *
8
+ * Tool approval component - renders approve/deny buttons for tools that need approval
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * {part.approval && (
13
+ * <ToolApproval
14
+ * toolCallId={part.id}
15
+ * toolName={part.name}
16
+ * input={JSON.parse(part.arguments)}
17
+ * approval={part.approval}
18
+ * />
19
+ * )}
20
+ * ```
21
+ */
22
+ function ToolApproval({ toolCallId: _, toolName, input, approval, className, children }) {
23
+ const { addToolApprovalResponse } = useChatContext();
24
+ const handleApprove = () => {
25
+ addToolApprovalResponse({
26
+ id: approval.id,
27
+ approved: true
28
+ });
29
+ };
30
+ const handleDeny = () => {
31
+ addToolApprovalResponse({
32
+ id: approval.id,
33
+ approved: false
34
+ });
35
+ };
36
+ const hasResponded = approval.approved !== void 0;
37
+ const renderProps = {
38
+ toolName,
39
+ input,
40
+ onApprove: handleApprove,
41
+ onDeny: handleDeny,
42
+ hasResponded,
43
+ approved: approval.approved
44
+ };
45
+ if (children) return /* @__PURE__ */ jsx(Fragment, { children: children(renderProps) });
46
+ if (hasResponded) return /* @__PURE__ */ jsx("div", {
47
+ className,
48
+ "data-tool-approval": true,
49
+ "data-approval-status": approval.approved ? "approved" : "denied",
50
+ children: approval.approved ? "✓ Approved" : "✗ Denied"
51
+ });
52
+ return /* @__PURE__ */ jsxs("div", {
53
+ className,
54
+ "data-tool-approval": true,
55
+ "data-approval-status": "pending",
56
+ children: [
57
+ /* @__PURE__ */ jsxs("div", {
58
+ "data-approval-header": true,
59
+ children: [/* @__PURE__ */ jsx("strong", { children: toolName }), " requires approval"]
60
+ }),
61
+ /* @__PURE__ */ jsx("div", {
62
+ "data-approval-input": true,
63
+ children: /* @__PURE__ */ jsx("pre", { children: JSON.stringify(input, null, 2) })
64
+ }),
65
+ /* @__PURE__ */ jsxs("div", {
66
+ "data-approval-actions": true,
67
+ children: [/* @__PURE__ */ jsx("button", {
68
+ onClick: handleApprove,
69
+ "data-approval-approve": true,
70
+ children: "Approve"
71
+ }), /* @__PURE__ */ jsx("button", {
72
+ onClick: handleDeny,
73
+ "data-approval-deny": true,
74
+ children: "Deny"
75
+ })]
76
+ })
77
+ ]
78
+ });
79
+ }
80
+ //#endregion
81
+ export { ToolApproval };
82
+
83
+ //# sourceMappingURL=tool-approval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-approval.js","names":[],"sources":["../../../src/chat-ui/tool-approval.tsx"],"sourcesContent":["import { useChatContext } from './chat'\nimport type { ReactNode } from 'react'\n\nexport interface ToolApprovalProps {\n /** Tool call ID */\n toolCallId: string\n /** Tool name */\n toolName: string\n /** Parsed tool arguments/input */\n input: any\n /** Approval metadata */\n approval: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n /** CSS class name */\n className?: string\n /** Custom render prop */\n children?: (props: ToolApprovalRenderProps) => ReactNode\n}\n\nexport interface ToolApprovalRenderProps {\n /** Tool name */\n toolName: string\n /** Parsed input */\n input: any\n /** Approve the tool call */\n onApprove: () => void\n /** Deny the tool call */\n onDeny: () => void\n /** Whether user has responded */\n hasResponded: boolean\n /** User's decision (if responded) */\n approved?: boolean\n}\n\n/**\n * @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`.\n * Deprecated in 0.9.0. Removed in 1.0.0.\n *\n * Tool approval component - renders approve/deny buttons for tools that need approval\n *\n * @example\n * ```tsx\n * {part.approval && (\n * <ToolApproval\n * toolCallId={part.id}\n * toolName={part.name}\n * input={JSON.parse(part.arguments)}\n * approval={part.approval}\n * />\n * )}\n * ```\n */\nexport function ToolApproval({\n toolCallId: _,\n toolName,\n input,\n approval,\n className,\n children,\n}: ToolApprovalProps) {\n const { addToolApprovalResponse } = useChatContext()\n\n const handleApprove = () => {\n void addToolApprovalResponse({\n id: approval.id,\n approved: true,\n })\n }\n\n const handleDeny = () => {\n void addToolApprovalResponse({\n id: approval.id,\n approved: false,\n })\n }\n\n const hasResponded = approval.approved !== undefined\n\n const renderProps: ToolApprovalRenderProps = {\n toolName,\n input,\n onApprove: handleApprove,\n onDeny: handleDeny,\n hasResponded,\n approved: approval.approved,\n }\n\n // Render prop pattern\n if (children) {\n return <>{children(renderProps)}</>\n }\n\n // Already responded - show decision\n if (hasResponded) {\n return (\n <div\n className={className}\n data-tool-approval\n data-approval-status={approval.approved ? 'approved' : 'denied'}\n >\n {approval.approved ? '✓ Approved' : '✗ Denied'}\n </div>\n )\n }\n\n // Default approval UI\n return (\n <div\n className={className}\n data-tool-approval\n data-approval-status=\"pending\"\n >\n <div data-approval-header>\n <strong>{toolName}</strong> requires approval\n </div>\n <div data-approval-input>\n <pre>{JSON.stringify(input, null, 2)}</pre>\n </div>\n <div data-approval-actions>\n <button onClick={handleApprove} data-approval-approve>\n Approve\n </button>\n <button onClick={handleDeny} data-approval-deny>\n Deny\n </button>\n </div>\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,aAAa,EAC3B,YAAY,GACZ,UACA,OACA,UACA,WACA,YACoB;CACpB,MAAM,EAAE,4BAA4B,eAAe;CAEnD,MAAM,sBAAsB;EAC1B,wBAA6B;GAC3B,IAAI,SAAS;GACb,UAAU;EACZ,CAAC;CACH;CAEA,MAAM,mBAAmB;EACvB,wBAA6B;GAC3B,IAAI,SAAS;GACb,UAAU;EACZ,CAAC;CACH;CAEA,MAAM,eAAe,SAAS,aAAa,KAAA;CAE3C,MAAM,cAAuC;EAC3C;EACA;EACA,WAAW;EACX,QAAQ;EACR;EACA,UAAU,SAAS;CACrB;CAGA,IAAI,UACF,OAAO,oBAAA,UAAA,EAAA,UAAG,SAAS,WAAW,EAAI,CAAA;CAIpC,IAAI,cACF,OACE,oBAAC,OAAD;EACa;EACX,sBAAA;EACA,wBAAsB,SAAS,WAAW,aAAa;EAEtD,UAAA,SAAS,WAAW,eAAe;CACjC,CAAA;CAKT,OACE,qBAAC,OAAD;EACa;EACX,sBAAA;EACA,wBAAqB;EAHvB,UAAA;GAKE,qBAAC,OAAD;IAAK,wBAAA;IAAL,UAAA,CACE,oBAAC,UAAD,EAAA,UAAS,SAAiB,CAAA,GAAC,oBACxB;;GACL,oBAAC,OAAD;IAAK,uBAAA;IACH,UAAA,oBAAC,OAAD,EAAA,UAAM,KAAK,UAAU,OAAO,MAAM,CAAC,EAAO,CAAA;GACvC,CAAA;GACL,qBAAC,OAAD;IAAK,yBAAA;IAAL,UAAA,CACE,oBAAC,UAAD;KAAQ,SAAS;KAAe,yBAAA;KAAsB,UAAA;IAE9C,CAAA,GACR,oBAAC,UAAD;KAAQ,SAAS;KAAY,sBAAA;KAAmB,UAAA;IAExC,CAAA,CACL;;EACF;;AAET"}
@@ -0,0 +1,18 @@
1
+ import { UIMessage } from '../types.js';
2
+ type ToolResultPart = Extract<UIMessage['parts'][number], {
3
+ type: 'tool-result';
4
+ }>;
5
+ /** `string | Array<ContentPart>` — a tool result's raw content. */
6
+ type ToolResultContent = ToolResultPart['content'];
7
+ /**
8
+ * Reduce a tool-result part's `content` to a plain string for rendering.
9
+ *
10
+ * Tool results carry `string | Array<ContentPart>` (multimodal results are
11
+ * normalized to an array of content parts upstream). The `ChatMessage`
12
+ * renderers operate on plain strings, so array content is flattened to the
13
+ * concatenation of its text parts. Non-text parts (image, audio, video,
14
+ * document) have no string form here and are skipped — matching the
15
+ * text-extraction behavior used elsewhere for `string | Array<ContentPart>`.
16
+ */
17
+ export declare function toolResultContentToString(content: ToolResultContent): string;
18
+ export {};
@@ -0,0 +1,19 @@
1
+ //#region src/chat-ui/tool-result-content.ts
2
+ /**
3
+ * Reduce a tool-result part's `content` to a plain string for rendering.
4
+ *
5
+ * Tool results carry `string | Array<ContentPart>` (multimodal results are
6
+ * normalized to an array of content parts upstream). The `ChatMessage`
7
+ * renderers operate on plain strings, so array content is flattened to the
8
+ * concatenation of its text parts. Non-text parts (image, audio, video,
9
+ * document) have no string form here and are skipped — matching the
10
+ * text-extraction behavior used elsewhere for `string | Array<ContentPart>`.
11
+ */
12
+ function toolResultContentToString(content) {
13
+ if (typeof content === "string") return content;
14
+ return content.filter((part) => part.type === "text").map((part) => part.content).join("");
15
+ }
16
+ //#endregion
17
+ export { toolResultContentToString };
18
+
19
+ //# sourceMappingURL=tool-result-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-result-content.js","names":[],"sources":["../../../src/chat-ui/tool-result-content.ts"],"sourcesContent":["import type { UIMessage } from '../types'\n\ntype ToolResultPart = Extract<\n UIMessage['parts'][number],\n { type: 'tool-result' }\n>\n\n/** `string | Array<ContentPart>` — a tool result's raw content. */\ntype ToolResultContent = ToolResultPart['content']\n\ntype ContentPartItem = Exclude<ToolResultContent, string>[number]\n\n/**\n * Reduce a tool-result part's `content` to a plain string for rendering.\n *\n * Tool results carry `string | Array<ContentPart>` (multimodal results are\n * normalized to an array of content parts upstream). The `ChatMessage`\n * renderers operate on plain strings, so array content is flattened to the\n * concatenation of its text parts. Non-text parts (image, audio, video,\n * document) have no string form here and are skipped — matching the\n * text-extraction behavior used elsewhere for `string | Array<ContentPart>`.\n */\nexport function toolResultContentToString(content: ToolResultContent): string {\n if (typeof content === 'string') return content\n return content\n .filter(\n (part): part is Extract<ContentPartItem, { type: 'text' }> =>\n part.type === 'text',\n )\n .map((part) => part.content)\n .join('')\n}\n"],"mappings":";;;;;;;;;;;AAsBA,SAAgB,0BAA0B,SAAoC;CAC5E,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,OAAO,QACJ,QACE,SACC,KAAK,SAAS,MAClB,CAAC,CACA,KAAK,SAAS,KAAK,OAAO,CAAC,CAC3B,KAAK,EAAE;AACZ"}
@@ -0,0 +1,35 @@
1
+ import { AnyClientTool, InterruptDefinition, SchemaInput } from '@tanstack/ai/client';
2
+ import { InferredClientContext } from '@tanstack/ai-client';
3
+ import { UseChatOptions } from './types.js';
4
+ type ChatHookOverrides<TTools extends ReadonlyArray<AnyClientTool>, TSchema extends SchemaInput | undefined, TContext, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>> = {
5
+ threadId?: string;
6
+ live?: boolean;
7
+ forwardedProps?: Record<string, any>;
8
+ body?: Record<string, any>;
9
+ initialMessages?: UseChatOptions<TTools, TSchema, TContext, TInterrupts>['initialMessages'];
10
+ };
11
+ /**
12
+ * Bind chat options once at module scope. The returned `useChat` hook
13
+ * creates a chat instance from those options.
14
+ *
15
+ * Pass per-call overrides for instance keys such as `threadId`,
16
+ * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
17
+ * `interrupts`, or `outputSchema` here. Those stay on the factory options.
18
+ *
19
+ * Rename the hook at the call site if you already import `useChat` from
20
+ * `@tanstack/ai-react`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const { useChat } = createChatHook(chatOptions)
25
+ *
26
+ * function Support() {
27
+ * const chat = useChat({ threadId: 'support-1' })
28
+ * return <UI.Chat chat={chat} />
29
+ * }
30
+ * ```
31
+ */
32
+ export declare function createChatHook<const TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>, const TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>): {
33
+ useChat: (overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>) => import('./types.js').UseChatReturn<TTools, TSchema, TInterrupts>;
34
+ };
35
+ export {};
@@ -0,0 +1,37 @@
1
+ import { useChat } from "./use-chat.js";
2
+ //#region src/create-chat-hook.ts
3
+ /**
4
+ * Bind chat options once at module scope. The returned `useChat` hook
5
+ * creates a chat instance from those options.
6
+ *
7
+ * Pass per-call overrides for instance keys such as `threadId`,
8
+ * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
9
+ * `interrupts`, or `outputSchema` here. Those stay on the factory options.
10
+ *
11
+ * Rename the hook at the call site if you already import `useChat` from
12
+ * `@tanstack/ai-react`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const { useChat } = createChatHook(chatOptions)
17
+ *
18
+ * function Support() {
19
+ * const chat = useChat({ threadId: 'support-1' })
20
+ * return <UI.Chat chat={chat} />
21
+ * }
22
+ * ```
23
+ */
24
+ function createChatHook(options) {
25
+ function useChat$1(overrides) {
26
+ if (!overrides) return useChat(options);
27
+ return useChat({
28
+ ...options,
29
+ ...overrides
30
+ });
31
+ }
32
+ return { useChat: useChat$1 };
33
+ }
34
+ //#endregion
35
+ export { createChatHook };
36
+
37
+ //# sourceMappingURL=create-chat-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-chat-hook.js","names":[],"sources":["../../src/create-chat-hook.ts"],"sourcesContent":["import { useChat as useUnboundChat } from './use-chat'\nimport type {\n AnyClientTool,\n InterruptDefinition,\n SchemaInput,\n} from '@tanstack/ai/client'\nimport type { InferredClientContext } from '@tanstack/ai-client'\nimport type { UseChatOptions } from './types'\n\ntype ChatHookOverrides<\n TTools extends ReadonlyArray<AnyClientTool>,\n TSchema extends SchemaInput | undefined,\n TContext,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,\n> = {\n threadId?: string\n live?: boolean\n forwardedProps?: Record<string, any>\n body?: Record<string, any>\n initialMessages?: UseChatOptions<\n TTools,\n TSchema,\n TContext,\n TInterrupts\n >['initialMessages']\n}\n\n/**\n * Bind chat options once at module scope. The returned `useChat` hook\n * creates a chat instance from those options.\n *\n * Pass per-call overrides for instance keys such as `threadId`,\n * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,\n * `interrupts`, or `outputSchema` here. Those stay on the factory options.\n *\n * Rename the hook at the call site if you already import `useChat` from\n * `@tanstack/ai-react`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.\n *\n * @example\n * ```tsx\n * const { useChat } = createChatHook(chatOptions)\n *\n * function Support() {\n * const chat = useChat({ threadId: 'support-1' })\n * return <UI.Chat chat={chat} />\n * }\n * ```\n */\nexport function createChatHook<\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>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>) {\n function useChat(\n overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>,\n ) {\n if (!overrides) {\n return useUnboundChat(options)\n }\n return useUnboundChat({ ...options, ...overrides })\n }\n\n return { useChat }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,eAOd,SAAiE;CACjE,SAAS,UACP,WACA;EACA,IAAI,CAAC,WACH,OAAO,QAAe,OAAO;EAE/B,OAAO,QAAe;GAAE,GAAG;GAAS,GAAG;EAAU,CAAC;CACpD;CAEA,OAAO,EAAE,SAAA,UAAQ;AACnB"}
@@ -1,8 +1,11 @@
1
1
  export { useChat } from './use-chat.js';
2
+ export { createChatHook } from './create-chat-hook.js';
2
3
  export { useByok } from './use-byok.js';
3
4
  export { useRealtimeChat } from './use-realtime-chat.js';
4
5
  export { useMcpAppBridge } from './use-mcp-app-bridge.js';
5
6
  export type { UseMcpAppBridgeOptions } from './use-mcp-app-bridge.js';
7
+ export { useWebMCPTools } from './use-web-mcp-tools.js';
8
+ export type { UseWebMCPToolsOptions } from './use-web-mcp-tools.js';
6
9
  export type { DeepPartial, UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, QueuedMessage, SendMessageOptions, WhenBusy, QueueConfig, QueueStrategy, QueueOption, } from './types.js';
7
10
  export type { UseRealtimeChatOptions, UseRealtimeChatReturn, } from './realtime-types.js';
8
11
  export { useGeneration } from './use-generation.js';
package/dist/esm/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { useChat } from "./use-chat.js";
2
+ import { createChatHook } from "./create-chat-hook.js";
2
3
  import { useByok } from "./use-byok.js";
3
4
  import { useRealtimeChat } from "./use-realtime-chat.js";
4
5
  import { useMcpAppBridge } from "./use-mcp-app-bridge.js";
6
+ import { useWebMCPTools } from "./use-web-mcp-tools.js";
5
7
  import { useGeneration } from "./use-generation.js";
6
8
  import { useGenerateImage } from "./use-generate-image.js";
7
9
  import { useGenerateAudio } from "./use-generate-audio.js";
@@ -11,4 +13,4 @@ import { useSummarize } from "./use-summarize.js";
11
13
  import { useGenerateVideo } from "./use-generate-video.js";
12
14
  import { useAudioRecorder } from "./use-audio-recorder.js";
13
15
  import { StorageUnavailableError, createChatClientOptions, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
14
- export { StorageUnavailableError, createChatClientOptions, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useMcpAppBridge, useRealtimeChat, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
16
+ export { StorageUnavailableError, createChatClientOptions, createChatHook, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useMcpAppBridge, useRealtimeChat, useSummarize, useTranscription, useWebMCPTools, webSocket, xhrHttpStream, xhrServerSentEvents };
@@ -0,0 +1,10 @@
1
+ export { createChatUI, type ChatUIComponents, type ChatUIFactoryConfig, type ChatUIHost, type ChatUIQueueItem, type InputProps, type InterruptProps, type LayoutProps, type MessageProps, type PartProps, type QueueProps, type ToolProps, } from './chat-ui/create-ui.js';
2
+ export { createChatHook } from './chat-ui/create-chat-hook.js';
3
+ export { createChatHookContexts, type ChatUIContexts, } from './chat-ui/create-ui-contexts.js';
4
+ export { Chat, useChatContext, type ChatProps } from './chat-ui/chat.js';
5
+ export { ChatMessages, type ChatMessagesProps } from './chat-ui/chat-messages.js';
6
+ export { ChatMessage, type ChatMessageProps, type ToolCallRenderProps, } from './chat-ui/chat-message.js';
7
+ export { ChatInput, type ChatInputProps, type ChatInputRenderProps, } from './chat-ui/chat-input.js';
8
+ export { ToolApproval, type ToolApprovalProps, type ToolApprovalRenderProps, } from './chat-ui/tool-approval.js';
9
+ export { TextPart, type TextPartProps } from './chat-ui/text-part.js';
10
+ export { ThinkingPart, type ThinkingPartProps } from './chat-ui/thinking-part.js';
package/dist/esm/ui.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createChatHookContexts } from "./chat-ui/create-ui-contexts.js";
2
+ import { createChatUI } from "./chat-ui/create-ui.js";
3
+ import { createChatHook } from "./chat-ui/create-chat-hook.js";
4
+ import { Chat, useChatContext } from "./chat-ui/chat.js";
5
+ import { ThinkingPart } from "./chat-ui/thinking-part.js";
6
+ import { ChatMessage } from "./chat-ui/chat-message.js";
7
+ import { ChatMessages } from "./chat-ui/chat-messages.js";
8
+ import { ChatInput } from "./chat-ui/chat-input.js";
9
+ import { ToolApproval } from "./chat-ui/tool-approval.js";
10
+ import { TextPart } from "./chat-ui/text-part.js";
11
+ export { Chat, ChatInput, ChatMessage, ChatMessages, TextPart, ThinkingPart, ToolApproval, createChatHook, createChatHookContexts, createChatUI, useChatContext };
@@ -0,0 +1,27 @@
1
+ import { AnyClientTool, InferredClientContext, RegisterWebMCPToolsOptions } from '@tanstack/ai-client';
2
+ /** Options for the React {@link useWebMCPTools} lifecycle hook. */
3
+ export type UseWebMCPToolsOptions<TTools extends ReadonlyArray<AnyClientTool>, TContext = InferredClientContext<TTools>> = Omit<RegisterWebMCPToolsOptions<TTools, TContext>, 'signal'> & {
4
+ /** Receives an asynchronous registration failure. */
5
+ onError?: (error: unknown) => void;
6
+ };
7
+ type UseWebMCPToolsArguments<TTools extends ReadonlyArray<AnyClientTool>, TContext> = RegisterWebMCPToolsOptions<TTools, TContext> extends {
8
+ context: unknown;
9
+ } ? [options: UseWebMCPToolsOptions<TTools, TContext>] : [options?: UseWebMCPToolsOptions<TTools, TContext>];
10
+ /**
11
+ * Registers client tools with WebMCP for the lifetime of a React component.
12
+ *
13
+ * The hook replaces the registration when `tools` or `options` changes.
14
+ * Unsupported browsers and server rendering do not register tools.
15
+ *
16
+ * @param tools - The executable client tools to expose through WebMCP.
17
+ * @param options - Runtime context, per-tool options, and an error callback.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * useWebMCPTools([searchProducts], {
22
+ * toolOptions: { searchProducts: { title: 'Search products' } },
23
+ * })
24
+ * ```
25
+ */
26
+ export declare function useWebMCPTools<const TTools extends ReadonlyArray<AnyClientTool>, TContext = InferredClientContext<TTools>>(tools: TTools, ...[options]: UseWebMCPToolsArguments<TTools, TContext>): void;
27
+ export {};
@@ -0,0 +1,36 @@
1
+ import { registerWebMCPTools } from "@tanstack/ai-client";
2
+ import { useEffect } from "react";
3
+ //#region src/use-web-mcp-tools.ts
4
+ /**
5
+ * Registers client tools with WebMCP for the lifetime of a React component.
6
+ *
7
+ * The hook replaces the registration when `tools` or `options` changes.
8
+ * Unsupported browsers and server rendering do not register tools.
9
+ *
10
+ * @param tools - The executable client tools to expose through WebMCP.
11
+ * @param options - Runtime context, per-tool options, and an error callback.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * useWebMCPTools([searchProducts], {
16
+ * toolOptions: { searchProducts: { title: 'Search products' } },
17
+ * })
18
+ * ```
19
+ */
20
+ function useWebMCPTools(tools, ...[options]) {
21
+ useEffect(() => {
22
+ const controller = new AbortController();
23
+ const { onError, ...registrationOptions } = options ?? {};
24
+ registerWebMCPTools(tools, {
25
+ ...registrationOptions,
26
+ signal: controller.signal
27
+ }).catch((error) => {
28
+ if (!controller.signal.aborted) onError?.(error);
29
+ });
30
+ return () => controller.abort();
31
+ }, [tools, options]);
32
+ }
33
+ //#endregion
34
+ export { useWebMCPTools };
35
+
36
+ //# sourceMappingURL=use-web-mcp-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-web-mcp-tools.js","names":[],"sources":["../../src/use-web-mcp-tools.ts"],"sourcesContent":["import { useEffect } from 'react'\nimport { registerWebMCPTools } from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InferredClientContext,\n RegisterWebMCPToolsOptions,\n} from '@tanstack/ai-client'\n\n/** Options for the React {@link useWebMCPTools} lifecycle hook. */\nexport type UseWebMCPToolsOptions<\n TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n> = Omit<RegisterWebMCPToolsOptions<TTools, TContext>, 'signal'> & {\n /** Receives an asynchronous registration failure. */\n onError?: (error: unknown) => void\n}\n\ntype UseWebMCPToolsArguments<\n TTools extends ReadonlyArray<AnyClientTool>,\n TContext,\n> =\n RegisterWebMCPToolsOptions<TTools, TContext> extends { context: unknown }\n ? [options: UseWebMCPToolsOptions<TTools, TContext>]\n : [options?: UseWebMCPToolsOptions<TTools, TContext>]\n\n/**\n * Registers client tools with WebMCP for the lifetime of a React component.\n *\n * The hook replaces the registration when `tools` or `options` changes.\n * Unsupported browsers and server rendering do not register tools.\n *\n * @param tools - The executable client tools to expose through WebMCP.\n * @param options - Runtime context, per-tool options, and an error callback.\n *\n * @example\n * ```tsx\n * useWebMCPTools([searchProducts], {\n * toolOptions: { searchProducts: { title: 'Search products' } },\n * })\n * ```\n */\nexport function useWebMCPTools<\n const TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n>(tools: TTools, ...[options]: UseWebMCPToolsArguments<TTools, TContext>) {\n useEffect(() => {\n const controller = new AbortController()\n const { onError, ...registrationOptions } = options ?? {}\n\n registerWebMCPTools(tools, {\n ...registrationOptions,\n signal: controller.signal,\n }).catch((error) => {\n if (!controller.signal.aborted) onError?.(error)\n })\n\n return () => controller.abort()\n }, [tools, options])\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,eAGd,OAAe,GAAG,CAAC,UAAqD;CACxE,gBAAgB;EACd,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,EAAE,SAAS,GAAG,wBAAwB,WAAW,CAAC;EAExD,oBAAoB,OAAO;GACzB,GAAG;GACH,QAAQ,WAAW;EACrB,CAAC,CAAC,CAAC,OAAO,UAAU;GAClB,IAAI,CAAC,WAAW,OAAO,SAAS,UAAU,KAAK;EACjD,CAAC;EAED,aAAa,WAAW,MAAM;CAChC,GAAG,CAAC,OAAO,OAAO,CAAC;AACrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-react",
3
- "version": "0.22.4",
3
+ "version": "0.24.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",
@@ -28,6 +28,10 @@
28
28
  "./mcp-apps": {
29
29
  "types": "./dist/esm/mcp-apps.d.ts",
30
30
  "import": "./dist/esm/mcp-apps.js"
31
+ },
32
+ "./ui": {
33
+ "types": "./dist/esm/ui.d.ts",
34
+ "import": "./dist/esm/ui.js"
31
35
  }
32
36
  },
33
37
  "files": [
@@ -49,17 +53,26 @@
49
53
  "media-generation"
50
54
  ],
51
55
  "dependencies": {
52
- "@tanstack/ai-client": "^0.29.2"
56
+ "react-markdown": "^10.1.0",
57
+ "rehype-highlight": "^7.0.2",
58
+ "rehype-raw": "^7.0.0",
59
+ "rehype-sanitize": "^6.0.0",
60
+ "remark-gfm": "^4.0.1",
61
+ "@tanstack/ai-client": "^0.31.0"
53
62
  },
54
63
  "peerDependencies": {
55
64
  "@mcp-ui/client": "^7",
56
65
  "@types/react": ">=18.0.0",
57
66
  "react": ">=18.0.0",
58
- "@tanstack/ai": "^0.52.0"
67
+ "react-dom": ">=18.0.0",
68
+ "@tanstack/ai": "^0.53.0"
59
69
  },
60
70
  "peerDependenciesMeta": {
61
71
  "@mcp-ui/client": {
62
72
  "optional": true
73
+ },
74
+ "react-dom": {
75
+ "optional": true
63
76
  }
64
77
  },
65
78
  "devDependencies": {
@@ -70,8 +83,9 @@
70
83
  "@vitest/coverage-v8": "4.1.10",
71
84
  "jsdom": "^27.4.0",
72
85
  "react": "^19.2.3",
86
+ "react-dom": "^19.2.3",
73
87
  "vite": "^8.2.1",
74
- "@tanstack/ai": "0.52.0"
88
+ "@tanstack/ai": "0.53.0"
75
89
  },
76
90
  "scripts": {
77
91
  "clean": "premove ./build ./dist",
@@ -0,0 +1,177 @@
1
+ import { useRef, useState } from 'react'
2
+ import { useChatContext } from './chat'
3
+ import type { ReactNode, RefObject } from 'react'
4
+
5
+ export interface ChatInputRenderProps {
6
+ /** Current input value */
7
+ value: string
8
+ /** Set input value */
9
+ onChange: (value: string) => void
10
+ /** Submit the message */
11
+ onSubmit: () => void
12
+ /** Is the chat currently loading */
13
+ isLoading: boolean
14
+ /** Is input disabled */
15
+ disabled: boolean
16
+ /** Ref to the input element */
17
+ inputRef: RefObject<HTMLInputElement | HTMLTextAreaElement | null>
18
+ }
19
+
20
+ export interface ChatInputProps {
21
+ /** Render prop for full control */
22
+ children?: (props: ChatInputRenderProps) => ReactNode
23
+ /** CSS class name */
24
+ className?: string
25
+ /** Placeholder text */
26
+ placeholder?: string
27
+ /** Disable input */
28
+ disabled?: boolean
29
+ /** Submit on Enter (Shift+Enter for new line) */
30
+ submitOnEnter?: boolean
31
+ }
32
+
33
+ /**
34
+ * @deprecated Use `createChatUI()` and an application-owned input component.
35
+ * Deprecated in 0.9.0. Removed in 1.0.0.
36
+ *
37
+ * Chat input component - handles message input and submission
38
+ *
39
+ * Features:
40
+ * - Auto-growing textarea
41
+ * - Submit on Enter (Shift+Enter for new line)
42
+ * - Loading state management
43
+ * - Full render prop support for custom UIs
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * <Chat.Input placeholder="Type your message..." />
48
+ * ```
49
+ *
50
+ * @example Custom UI with render prop
51
+ * ```tsx
52
+ * <Chat.Input>
53
+ * {({ value, onChange, onSubmit, isLoading }) => (
54
+ * <div>
55
+ * <textarea value={value} onChange={(e) => onChange(e.target.value)} />
56
+ * <button onClick={onSubmit} disabled={isLoading}>Send</button>
57
+ * </div>
58
+ * )}
59
+ * </Chat.Input>
60
+ * ```
61
+ */
62
+ export function ChatInput({
63
+ children,
64
+ className,
65
+ placeholder = 'Type a message...',
66
+ disabled: disabledProp,
67
+ submitOnEnter = true,
68
+ }: ChatInputProps) {
69
+ const { sendMessage, isLoading } = useChatContext()
70
+ const [value, setValue] = useState('')
71
+ const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
72
+
73
+ const disabled = disabledProp || isLoading
74
+
75
+ const handleSubmit = () => {
76
+ if (!value.trim() || disabled) return
77
+ void sendMessage(value)
78
+ setValue('')
79
+ }
80
+
81
+ const renderProps: ChatInputRenderProps = {
82
+ value,
83
+ onChange: setValue,
84
+ onSubmit: handleSubmit,
85
+ isLoading,
86
+ disabled,
87
+ inputRef,
88
+ }
89
+
90
+ // Render prop pattern
91
+ if (children) {
92
+ return <>{children(renderProps)}</>
93
+ }
94
+
95
+ // Default implementation
96
+ return (
97
+ <div
98
+ className={className}
99
+ data-chat-input
100
+ style={{
101
+ display: 'flex',
102
+ gap: '0.75rem',
103
+ alignItems: 'center',
104
+ width: '100%',
105
+ }}
106
+ >
107
+ <input
108
+ ref={inputRef as React.RefObject<HTMLInputElement>}
109
+ type="text"
110
+ value={value}
111
+ onChange={(e) => setValue(e.target.value)}
112
+ onKeyDown={(e) => {
113
+ if (submitOnEnter && e.key === 'Enter') {
114
+ e.preventDefault()
115
+ handleSubmit()
116
+ }
117
+ }}
118
+ placeholder={placeholder}
119
+ disabled={disabled}
120
+ data-chat-textarea
121
+ style={{
122
+ flex: 1,
123
+ padding: '0.75rem 1rem',
124
+ fontSize: '0.875rem',
125
+ border: '1px solid rgba(255, 255, 255, 0.1)',
126
+ borderRadius: '0.75rem',
127
+ backgroundColor: 'rgba(31, 41, 55, 0.5)',
128
+ color: 'white',
129
+ outline: 'none',
130
+ transition: 'all 0.2s',
131
+ }}
132
+ onFocus={(e) => {
133
+ e.target.style.borderColor = 'rgba(249, 115, 22, 0.4)'
134
+ e.target.style.boxShadow = '0 0 0 2px rgba(249, 115, 22, 0.2)'
135
+ }}
136
+ onBlur={(e) => {
137
+ e.target.style.borderColor = 'rgba(255, 255, 255, 0.1)'
138
+ e.target.style.boxShadow = 'none'
139
+ }}
140
+ />
141
+ <button
142
+ onClick={handleSubmit}
143
+ disabled={disabled || !value.trim()}
144
+ data-chat-submit
145
+ style={{
146
+ padding: '0.75rem 1.5rem',
147
+ fontSize: '0.875rem',
148
+ fontWeight: 500,
149
+ color: 'white',
150
+ backgroundColor:
151
+ disabled || !value.trim()
152
+ ? 'rgba(107, 114, 128, 0.5)'
153
+ : 'rgb(249, 115, 22)',
154
+ border: 'none',
155
+ borderRadius: '0.75rem',
156
+ cursor: disabled || !value.trim() ? 'not-allowed' : 'pointer',
157
+ transition: 'all 0.2s',
158
+ whiteSpace: 'nowrap',
159
+ }}
160
+ onMouseEnter={(e) => {
161
+ if (!disabled && value.trim()) {
162
+ ;(e.target as HTMLButtonElement).style.backgroundColor =
163
+ 'rgb(234, 88, 12)'
164
+ }
165
+ }}
166
+ onMouseLeave={(e) => {
167
+ if (!disabled && value.trim()) {
168
+ ;(e.target as HTMLButtonElement).style.backgroundColor =
169
+ 'rgb(249, 115, 22)'
170
+ }
171
+ }}
172
+ >
173
+ {isLoading ? 'Sending...' : 'Send'}
174
+ </button>
175
+ </div>
176
+ )
177
+ }