@tanstack/ai-react 0.24.0 → 0.26.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 +14 -13
- package/dist/esm/chat-ui/text-part.d.ts +19 -24
- package/dist/esm/chat-ui/text-part.js +16 -15
- package/dist/esm/chat-ui/text-part.js.map +1 -1
- package/dist/esm/types.d.ts +8 -0
- package/dist/esm/use-chat.js +16 -1
- package/dist/esm/use-chat.js.map +1 -1
- package/package.json +5 -9
- package/src/chat-ui/text-part.tsx +36 -38
- package/src/types.ts +10 -0
- package/src/use-chat.ts +26 -5
- package/dist/esm/chat-ui/markdown-plugins.d.ts +0 -15
- package/dist/esm/chat-ui/markdown-plugins.js +0 -28
- package/dist/esm/chat-ui/markdown-plugins.js.map +0 -1
- package/src/chat-ui/markdown-plugins.ts +0 -48
package/README.md
CHANGED
|
@@ -202,19 +202,20 @@ Learn more in the
|
|
|
202
202
|
|
|
203
203
|
Official adapters include:
|
|
204
204
|
|
|
205
|
-
| Package | Use it for
|
|
206
|
-
| ------------------------------------------------------------------------------------ |
|
|
207
|
-
| [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking
|
|
208
|
-
| [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools
|
|
209
|
-
| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude
|
|
210
|
-
| [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation
|
|
211
|
-
| [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials
|
|
212
|
-
| [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models
|
|
213
|
-
| [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime
|
|
214
|
-
| [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference
|
|
215
|
-
| [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects
|
|
216
|
-
| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR
|
|
217
|
-
| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models
|
|
205
|
+
| Package | Use it for |
|
|
206
|
+
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
207
|
+
| [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
|
|
208
|
+
| [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
|
|
209
|
+
| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude |
|
|
210
|
+
| [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
|
|
211
|
+
| [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials |
|
|
212
|
+
| [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
|
|
213
|
+
| [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
|
|
214
|
+
| [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
|
|
215
|
+
| [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
|
|
216
|
+
| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
|
|
217
|
+
| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
|
|
218
|
+
| [`@tanstack/ai-cloudflare`](https://tanstack.com/ai/latest/docs/adapters/cloudflare) | Cloudflare Workers AI chat, embeddings, image, speech, transcription, and AI Gateway |
|
|
218
219
|
|
|
219
220
|
The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
|
|
220
221
|
`openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { CodeHighlighter, MarkdownExtension } from '@tanstack/markdown';
|
|
2
|
+
import { MarkdownComponents } from '@tanstack/markdown/react';
|
|
3
3
|
export interface TextPartProps {
|
|
4
4
|
/** The text content to render */
|
|
5
5
|
content: string;
|
|
@@ -12,29 +12,24 @@ export interface TextPartProps {
|
|
|
12
12
|
/** Additional className for assistant messages (also used for system messages) */
|
|
13
13
|
assistantClassName?: string;
|
|
14
14
|
/**
|
|
15
|
-
* Additional
|
|
16
|
-
*
|
|
15
|
+
* Additional TanStack Markdown extensions, appended after the built-in
|
|
16
|
+
* streaming extension.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
extensions?: Array<MarkdownExtension>;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* is true, replaces the entire chain.
|
|
20
|
+
* Synchronous code highlighter. Its output is inserted as trusted HTML,
|
|
21
|
+
* so use only a highlighter that escapes source text (for example
|
|
22
|
+
* `createTanStackMarkdownHighlighter` from `@tanstack/highlight/markdown`).
|
|
24
23
|
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
components?:
|
|
28
|
-
/**
|
|
29
|
-
* Drop the built-in plugin defaults entirely. The consumer becomes
|
|
30
|
-
* responsible for syntax highlighting, GFM, raw HTML handling, and
|
|
31
|
-
* sanitization. Use with care — disabling defaults removes the
|
|
32
|
-
* built-in XSS sanitizer.
|
|
33
|
-
*/
|
|
34
|
-
disableDefaultPlugins?: boolean;
|
|
24
|
+
highlighter?: CodeHighlighter;
|
|
25
|
+
/** Replace intrinsic elements (e.g. custom `a`, `img`) by tag name. */
|
|
26
|
+
components?: MarkdownComponents;
|
|
35
27
|
}
|
|
36
28
|
/**
|
|
37
|
-
* TextPart component - renders markdown text with
|
|
29
|
+
* TextPart component - renders markdown text with TanStack Markdown.
|
|
30
|
+
*
|
|
31
|
+
* Raw HTML is escaped and executable URLs are removed, so AI output can be
|
|
32
|
+
* rendered without a separate sanitizer.
|
|
38
33
|
*
|
|
39
34
|
* @example Standalone usage
|
|
40
35
|
* ```tsx
|
|
@@ -47,11 +42,11 @@ export interface TextPartProps {
|
|
|
47
42
|
* />
|
|
48
43
|
* ```
|
|
49
44
|
*
|
|
50
|
-
* @example
|
|
45
|
+
* @example Syntax highlighting
|
|
51
46
|
* ```tsx
|
|
52
|
-
* import
|
|
47
|
+
* import { highlightMarkdownCode } from './markdown-highlighter.js'
|
|
53
48
|
*
|
|
54
|
-
* <TextPart content={content}
|
|
49
|
+
* <TextPart content={content} highlighter={highlightMarkdownCode} />
|
|
55
50
|
* ```
|
|
56
51
|
*/
|
|
57
|
-
export declare function TextPart({ content, role, className, userClassName, assistantClassName,
|
|
52
|
+
export declare function TextPart({ content, role, className, userClassName, assistantClassName, extensions, highlighter, components, }: TextPartProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { resolveMarkdownPlugins } from "./markdown-plugins.js";
|
|
2
1
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import
|
|
2
|
+
import { Markdown } from "@tanstack/markdown/react";
|
|
3
|
+
import { streamingMarkdownExtension } from "@tanstack/markdown/extensions/streaming";
|
|
4
4
|
//#region src/chat-ui/text-part.tsx
|
|
5
|
+
var DEFAULT_EXTENSIONS = [streamingMarkdownExtension()];
|
|
5
6
|
/**
|
|
6
|
-
* TextPart component - renders markdown text with
|
|
7
|
+
* TextPart component - renders markdown text with TanStack Markdown.
|
|
8
|
+
*
|
|
9
|
+
* Raw HTML is escaped and executable URLs are removed, so AI output can be
|
|
10
|
+
* rendered without a separate sanitizer.
|
|
7
11
|
*
|
|
8
12
|
* @example Standalone usage
|
|
9
13
|
* ```tsx
|
|
@@ -16,25 +20,22 @@ import ReactMarkdown from "react-markdown";
|
|
|
16
20
|
* />
|
|
17
21
|
* ```
|
|
18
22
|
*
|
|
19
|
-
* @example
|
|
23
|
+
* @example Syntax highlighting
|
|
20
24
|
* ```tsx
|
|
21
|
-
* import
|
|
25
|
+
* import { highlightMarkdownCode } from './markdown-highlighter'
|
|
22
26
|
*
|
|
23
|
-
* <TextPart content={content}
|
|
27
|
+
* <TextPart content={content} highlighter={highlightMarkdownCode} />
|
|
24
28
|
* ```
|
|
25
29
|
*/
|
|
26
|
-
function TextPart({ content, role, className = "", userClassName = "", assistantClassName = "",
|
|
30
|
+
function TextPart({ content, role, className = "", userClassName = "", assistantClassName = "", extensions, highlighter, components }) {
|
|
27
31
|
const combinedClassName = [className, role === "user" ? userClassName : role === "assistant" ? assistantClassName : ""].filter(Boolean).join(" ");
|
|
28
|
-
const resolved = resolveMarkdownPlugins({
|
|
29
|
-
remarkPlugins,
|
|
30
|
-
rehypePlugins,
|
|
31
|
-
disableDefaultPlugins
|
|
32
|
-
});
|
|
33
32
|
return /* @__PURE__ */ jsx("div", {
|
|
34
33
|
className: combinedClassName || void 0,
|
|
35
|
-
children: /* @__PURE__ */ jsx(
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
children: /* @__PURE__ */ jsx(Markdown, {
|
|
35
|
+
extensions: extensions ? [...DEFAULT_EXTENSIONS, ...extensions] : DEFAULT_EXTENSIONS,
|
|
36
|
+
frontmatter: false,
|
|
37
|
+
headingIds: false,
|
|
38
|
+
highlighter,
|
|
38
39
|
components,
|
|
39
40
|
children: content
|
|
40
41
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-part.js","names":[],"sources":["../../../src/chat-ui/text-part.tsx"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"text-part.js","names":[],"sources":["../../../src/chat-ui/text-part.tsx"],"sourcesContent":["import { Markdown } from '@tanstack/markdown/react'\nimport { streamingMarkdownExtension } from '@tanstack/markdown/extensions/streaming'\nimport type { CodeHighlighter, MarkdownExtension } from '@tanstack/markdown'\nimport type { MarkdownComponents } from '@tanstack/markdown/react'\n\nconst DEFAULT_EXTENSIONS: Array<MarkdownExtension> = [\n streamingMarkdownExtension(),\n]\n\nexport interface TextPartProps {\n /** The text content to render */\n content: string\n /** The role of the message (user, assistant, or system) - optional for standalone use */\n role?: 'user' | 'assistant' | 'system'\n /** Base className applied to all text parts */\n className?: string\n /** Additional className for user messages */\n userClassName?: string\n /** Additional className for assistant messages (also used for system messages) */\n assistantClassName?: string\n /**\n * Additional TanStack Markdown extensions, appended after the built-in\n * streaming extension.\n */\n extensions?: Array<MarkdownExtension>\n /**\n * Synchronous code highlighter. Its output is inserted as trusted HTML,\n * so use only a highlighter that escapes source text (for example\n * `createTanStackMarkdownHighlighter` from `@tanstack/highlight/markdown`).\n */\n highlighter?: CodeHighlighter\n /** Replace intrinsic elements (e.g. custom `a`, `img`) by tag name. */\n components?: MarkdownComponents\n}\n\n/**\n * TextPart component - renders markdown text with TanStack Markdown.\n *\n * Raw HTML is escaped and executable URLs are removed, so AI output can be\n * rendered without a separate sanitizer.\n *\n * @example Standalone usage\n * ```tsx\n * <TextPart\n * content=\"Hello **world**!\"\n * role=\"user\"\n * className=\"p-4 rounded\"\n * userClassName=\"bg-blue-500\"\n * assistantClassName=\"bg-gray-500\"\n * />\n * ```\n *\n * @example Syntax highlighting\n * ```tsx\n * import { highlightMarkdownCode } from './markdown-highlighter'\n *\n * <TextPart content={content} highlighter={highlightMarkdownCode} />\n * ```\n */\nexport function TextPart({\n content,\n role,\n className = '',\n userClassName = '',\n assistantClassName = '',\n extensions,\n highlighter,\n components,\n}: TextPartProps) {\n const roleClassName =\n role === 'user'\n ? userClassName\n : role === 'assistant'\n ? assistantClassName\n : ''\n const combinedClassName = [className, roleClassName].filter(Boolean).join(' ')\n\n return (\n <div className={combinedClassName || undefined}>\n <Markdown\n extensions={\n extensions\n ? [...DEFAULT_EXTENSIONS, ...extensions]\n : DEFAULT_EXTENSIONS\n }\n frontmatter={false}\n headingIds={false}\n highlighter={highlighter}\n components={components}\n >\n {content}\n </Markdown>\n </div>\n )\n}\n"],"mappings":";;;;AAKA,IAAM,qBAA+C,CACnD,2BAA2B,CAC7B;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,SAAS,EACvB,SACA,MACA,YAAY,IACZ,gBAAgB,IAChB,qBAAqB,IACrB,YACA,aACA,cACgB;CAOhB,MAAM,oBAAoB,CAAC,WALzB,SAAS,SACL,gBACA,SAAS,cACP,qBACA,EAC2C,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CAE7E,OACE,oBAAC,OAAD;EAAK,WAAW,qBAAqB,KAAA;EACnC,UAAA,oBAAC,UAAD;GACE,YACE,aACI,CAAC,GAAG,oBAAoB,GAAG,UAAU,IACrC;GAEN,aAAa;GACb,YAAY;GACC;GACD;GAEX,UAAA;EACO,CAAA;CACP,CAAA;AAET"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -155,6 +155,14 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, T
|
|
|
155
155
|
* Whether a response is currently being generated
|
|
156
156
|
*/
|
|
157
157
|
isLoading: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* True when the last hydrate or older-page response said more messages exist.
|
|
160
|
+
*/
|
|
161
|
+
hasOlderMessages: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Fetch the next older window and put it in front of the painted messages.
|
|
164
|
+
*/
|
|
165
|
+
loadOlderMessages: () => Promise<void>;
|
|
158
166
|
/**
|
|
159
167
|
* Current error, if any
|
|
160
168
|
*/
|
package/dist/esm/use-chat.js
CHANGED
|
@@ -9,6 +9,7 @@ function useChat(options) {
|
|
|
9
9
|
const clientId = options.threadId ?? hookId;
|
|
10
10
|
const [messages, setMessages] = useState(options.initialMessages || []);
|
|
11
11
|
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
const [hasOlderMessages, setHasOlderMessages] = useState(false);
|
|
12
13
|
const [error, setError] = useState(void 0);
|
|
13
14
|
const [status, setStatus] = useState("ready");
|
|
14
15
|
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
@@ -64,7 +65,11 @@ function useChat(options) {
|
|
|
64
65
|
devtoolsBridgeFactory: createChatDevtoolsBridge,
|
|
65
66
|
...transport,
|
|
66
67
|
initialMessages: messagesToUse,
|
|
67
|
-
...typeof initialOptions.threadId === "string" && initialOptions.persistence ? {
|
|
68
|
+
...typeof initialOptions.threadId === "string" && initialOptions.persistence === true ? {
|
|
69
|
+
persistence: true,
|
|
70
|
+
threadId: initialOptions.threadId,
|
|
71
|
+
...initialOptions.history !== void 0 && { history: initialOptions.history }
|
|
72
|
+
} : typeof initialOptions.threadId === "string" && initialOptions.persistence ? {
|
|
68
73
|
persistence: initialOptions.persistence,
|
|
69
74
|
threadId: initialOptions.threadId
|
|
70
75
|
} : { ...initialOptions.threadId !== void 0 && { threadId: initialOptions.threadId } },
|
|
@@ -110,6 +115,9 @@ function useChat(options) {
|
|
|
110
115
|
onMessagesChange: (newMessages) => {
|
|
111
116
|
runOrQueueForActiveInstance(() => {
|
|
112
117
|
setMessages(newMessages);
|
|
118
|
+
const currentInstance = getActiveInstance();
|
|
119
|
+
if (!currentInstance) return;
|
|
120
|
+
setHasOlderMessages(currentInstance.getHasOlderMessages());
|
|
113
121
|
});
|
|
114
122
|
},
|
|
115
123
|
onLoadingChange: (newIsLoading) => {
|
|
@@ -195,6 +203,7 @@ function useChat(options) {
|
|
|
195
203
|
useEffect(() => {
|
|
196
204
|
const clientMessages = client.getMessages();
|
|
197
205
|
if (clientMessages !== messagesRef.current) setMessages(clientMessages);
|
|
206
|
+
setHasOlderMessages(client.getHasOlderMessages());
|
|
198
207
|
}, [client]);
|
|
199
208
|
useEffect(() => {
|
|
200
209
|
client.updateOptions({ body: options.body });
|
|
@@ -278,6 +287,10 @@ function useChat(options) {
|
|
|
278
287
|
syncResumeState(client);
|
|
279
288
|
}
|
|
280
289
|
}, [client, syncResumeState]);
|
|
290
|
+
const loadOlderMessages = useCallback(async () => {
|
|
291
|
+
await client.loadOlderMessages();
|
|
292
|
+
setHasOlderMessages(client.getHasOlderMessages());
|
|
293
|
+
}, [client]);
|
|
281
294
|
const stop = useCallback(() => {
|
|
282
295
|
client.stop();
|
|
283
296
|
}, [client]);
|
|
@@ -342,6 +355,8 @@ function useChat(options) {
|
|
|
342
355
|
reload,
|
|
343
356
|
stop,
|
|
344
357
|
isLoading,
|
|
358
|
+
hasOlderMessages,
|
|
359
|
+
loadOlderMessages,
|
|
345
360
|
error,
|
|
346
361
|
status,
|
|
347
362
|
isSubscribed,
|
package/dist/esm/use-chat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat.js","names":[],"sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type {\n AnyClientTool,\n InferSchemaType,\n InterruptDefinition,\n ModelMessage,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type {\n ChatClientState,\n ResolvableChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\n\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nconst EMPTY_INTERRUPTS = Object.freeze([])\nconst EMPTY_INTERRUPT_ERRORS = Object.freeze([])\n\nexport function useChat<\n const TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(\n options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>,\n): UseChatReturn<TTools, TSchema, TInterrupts> {\n // The hook's identity is its `threadId`. Reload with the same `threadId`\n // restores the same conversation. `hookId` is only a React recreation key\n // when no `threadId` is given. It is never sent on the wire.\n const hookId = useId()\n const clientId = options.threadId ?? hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [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\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 })\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 }, [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 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 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":";;;;AAgCA,IAAM,mBAAmB,OAAO,OAAO,CAAC,CAAC;AACzC,IAAM,yBAAyB,OAAO,OAAO,CAAC,CAAC;AAE/C,SAAgB,QAQd,SAC6C;CAI7C,MAAM,SAAS,MAAM;CACrB,MAAM,WAAW,QAAQ,YAAY;CAErC,MAAM,CAAC,UAAU,eAAe,SAC9B,QAAQ,mBAAmB,CAAC,CAC9B;CACA,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,OAAO,YAAY,SAA4B,KAAA,CAAS;CAC/D,MAAM,CAAC,QAAQ,aAAa,SAA0B,OAAO;CAC7D,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,kBAAkB,uBACvB,SAA2B,cAAc;CAC3C,MAAM,CAAC,mBAAmB,wBAAwB,SAAS,KAAK;CAChE,MAAM,CAAC,OAAO,YAAY,SAA+B,CAAC,CAAC;CAC3D,MAAM,CAAC,OAAO,YAAY,SAAwB,IAAI;CACtD,MAAM,CAAC,gBAAgB,qBAAqB,gBAEnC;EACP,YAAY;EACZ,mBAAmB;EACnB,iBAAiB;EACjB,UAAU;CACZ,EAAE;CAMF,MAAM,cAAc,OAClB,QAAQ,mBAAmB,CAAC,CAC9B;CACA,MAAM,kBAAkB,OAAO,IAAI;CACnC,MAAM,gBAAgB,OAAO,KAAK;CAClC,MAAM,kBAAkB,OAA0B,IAAI;CACtD,MAAM,yBAAyB,OAC7B,IACF;CACA,MAAM,qBAAqB,OAGjB,IAAI;CAGd,YAAY,UAAU;CAGtB,MAAM,aACJ,OAA+D,OAAO;CACxE,WAAW,UAAU;CAErB,MAAM,kBAAkB,aAAa,WAA8B;EACjE,IAAI,CAAC,QAAQ;EACb,SAAS,OAAO,gBAAgB,CAAC;EACjC,kBAAkB,OAAO,kBAAkB,CAAC;CAC9C,GAAG,CAAC,CAAC;CAGL,MAAM,EAAE,QAAQ,mBAAmB,cAAc;EAC/C,MAAM,gBAAgB,QAAQ,mBAAmB,CAAC;EAClD,gBAAgB,UAAU;EAM1B,MAAM,iBAAiB,WAAW;EAClC,MAAM,YAAY,eAAe,aAC7B,EAAE,YAAY,eAAe,WAAW,IACxC,EAAE,SAAS,eAAe,QAAQ;EAEtC,MAAM,iBAEF,EAAE,SAAS,KAAA,EAAU;EACzB,MAAM,0BAA0B;GAC9B,MAAM,kBAAkB,eAAe;GACvC,IAAI,CAAC,mBAAmB,gBAAgB,YAAY,iBAClD;GAEF,OAAO;EACT;EAKA,MAAM,sBAAsB;GAC1B,OAAO;GACP,WAAW,CAAC;EACd;EACA,MAAM,+BAA+B,aAAyB;GAC5D,IAAI,CAAC,oBAAoB,OAAO;IAC9B,oBAAoB,UAAU,KAAK,QAAQ;IAC3C;GACF;GACA,MAAM,kBAAkB,eAAe;GACvC,IAAI,CAAC,mBAAmB,gBAAgB,YAAY,iBAClD;GACF,SAAS;EACX;EACA,MAAM,WAAW,IAAI,WAA0C;GAC7D,uBAAuB;GACvB,GAAG;GACH,iBAAiB;GACjB,GAAI,OAAO,eAAe,aAAa,YACvC,eAAe,cACX;IACE,aAAa,eAAe;IAC5B,UAAU,eAAe;GAC3B,IACA,EACE,GAAI,eAAe,aAAa,KAAA,KAAa,EAC3C,UAAU,eAAe,SAC3B,EACF;GACJ,GAAI,eAAe,SAAS,KAAA,KAAa,EAAE,MAAM,eAAe,KAAK;GACrE,GAAI,eAAe,mBAAmB,KAAA,KAAa,EACjD,gBAAgB,eAAe,eACjC;GACA,GAAI,eAAe,SAAS,KAAA,KAAa,EAAE,MAAM,eAAe,KAAK;GACrE,oBAAoB,WAAW,QAAQ,eAAe;GACtD,GAAI,eAAe,0BAA0B,KAAA,KAAa,EACxD,uBAAuB,eAAe,sBACxC;GACA,GAAI,eAAe,YAAY,KAAA,KAAa,EAC1C,SAAS,eAAe,QAC1B;GACA,UAAU;IACR,GAAG,eAAe;IAClB,WAAW;IACX,UAAU;IACV,YAAY,eAAe,eAAe,eAAe;GAC3D;GACA,aAAa,aAAa;IAExB,IAAI,CAAC,kBAAkB,GAAG;IAC1B,OAAO,WAAW,QAAQ,aAAa,QAAQ;GACjD;GACA,UAAU,UAAuB;IAC/B,kCAAkC;KAChC,WAAW,QAAQ,UAAU,KAAK;IACpC,CAAC;GACH;GACA,WAAW,YAA+B;IACxC,kCAAkC;KAChC,WAAW,QAAQ,WAAW,OAAO;IACvC,CAAC;GACH;GACA,UAAU,UAAiB;IACzB,kCAAkC;KAChC,WAAW,QAAQ,UAAU,KAAK;IACpC,CAAC;GACH;GACA,GAAI,eAAe,UAAU,KAAA,KAAa,EACxC,OAAO,eAAe,MACxB;GACA,GAAI,eAAe,eAAe,KAAA,KAAa,EAC7C,YAAY,eAAe,WAC7B;GACA,gBAAgB,WAAW,MAAM,YAAY;IAC3C,kCAAkC;KAChC,WAAW,QAAQ,gBAAgB,WAAW,MAAM,OAAO;IAC7D,CAAC;GACH;GACA,GAAI,QAAQ,oBAAoB,KAAA,KAAa,EAC3C,iBAAiB,QAAQ,gBAC3B;GACA,mBAAmB,gBAA0C;IAC3D,kCAAkC;KAChC,YAAY,WAAW;IACzB,CAAC;GACH;GACA,kBAAkB,iBAA0B;IAC1C,kCAAkC;KAChC,MAAM,kBAAkB,kBAAkB;KAC1C,IAAI,CAAC,iBAAiB;KACtB,aAAa,YAAY;KACzB,gBAAgB,eAAe;IACjC,CAAC;GACH;GACA,gBAAgB,aAAgC;IAC9C,kCAAkC;KAChC,SAAS,QAAQ;IACnB,CAAC;GACH;GACA,iBAAiB,WAA4B;IAC3C,kCAAkC;KAChC,UAAU,MAAM;IAClB,CAAC;GACH;GACA,uBAAuB,qBAA8B;IACnD,kCAAkC;KAChC,gBAAgB,gBAAgB;IAClC,CAAC;GACH;GACA,2BAA2B,eAAiC;IAC1D,kCAAkC;KAChC,oBAAoB,UAAU;IAChC,CAAC;GACH;GACA,4BAA4B,iBAA0B;IACpD,kCAAkC;KAChC,qBAAqB,YAAY;IACnC,CAAC;GACH;GACA,GAAI,WAAW,QAAQ,UAAU,KAAA,KAAa,EAC5C,OAAO,WAAW,QAAQ,MAC5B;GACA,gBAAgB,cAAoC;IAClD,kCAAkC;KAChC,SAAS,SAAS;IACpB,CAAC;GACH;GACA,gBAAgB,cAAc;IAC5B,kCAAkC;KAChC,SAAS,SAAS;IACpB,CAAC;GACH;GACA,sBAAsB,kBAAkB,0BAA0B;IAChE,kCAAkC;KAChC,mBAAmB,aAAa;MAC9B,GAAG;MACH,YAAY;MACZ,mBAAmB;KACrB,EAAE;IACJ,CAAC;GACH;GACA,yBAAyB,oBAAoB,YAAY;IACvD,kCAAkC;KAChC,kBAAkB,kBAAkB;KACpC,WAAW,QAAQ,yBACjB,oBACA,OACF;IACF,CAAC;GACH;EACF,CAAC;EACD,eAAe,UAAU;EACzB,OAAO;GAAE,QAAQ;GAAU,gBAAgB;EAAoB;CACjE,GAAG,CAAC,UAAU,eAAe,CAAC;CAE9B,gBAAgB;EACd,gBAAgB,UAAU;EAC1B,IAAI;GAGF,OAAO,eAAe,UAAU,SAAS,GAAG;IAC1C,IAAI,gBAAgB,YAAY,QAAQ;KACtC,eAAe,UAAU,SAAS;KAClC;IACF;IACA,eAAe,UAAU,MAAM,CAAC,GAAG;GACrC;EACF,UAAU;GAER,eAAe,QAAQ;EACzB;CACF,GAAG,CAAC,QAAQ,cAAc,CAAC;CAE3B,gBAAgB;EACd,MAAM,iBAAiB,OAAO,YAAY;EAC1C,IAAI,mBAAmB,YAAY,SACjC,YAAY,cAAc;CAE9B,GAAG,CAAC,MAAM,CAAC;CAMX,gBAAgB;EACd,OAAO,cAAc,EAAE,MAAM,QAAQ,KAAK,CAAC;CAC7C,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;CAEzB,gBAAgB;EACd,IAAI,QAAQ,mBAAmB,KAAA,GAC7B,OAAO,cAAc,EAAE,gBAAgB,QAAQ,eAAe,CAAC;CAEnE,GAAG,CAAC,QAAQ,QAAQ,cAAc,CAAC;CAEnC,gBAAgB;EACd,IAAI,QAAQ,UAAU,KAAA,GACpB,OAAO,cAAc,EAAE,OAAO,QAAQ,MAAM,CAAC;CAEjD,GAAG,CAAC,QAAQ,QAAQ,KAAK,CAAC;CAE1B,gBAAgB;EACd,OAAO,cAAc,EAAE,SAAS,QAAQ,QAAQ,CAAC;CACnD,GAAG,CAAC,QAAQ,QAAQ,OAAO,CAAC;CAE5B,gBAAgB;EACd,IAAI,QAAQ,UAAU,KAAA,GACpB,OAAO,cAAc,EAAE,OAAO,QAAQ,MAAM,CAAC;CAEjD,GAAG,CAAC,QAAQ,QAAQ,KAAK,CAAC;CAE1B,gBAAgB;EACd,IAAI,QAAQ,MAAM;GAChB,OAAO,UAAU;GACjB,cAAc,UAAU;EAC1B,OAAO,IAAI,cAAc,SAAS;GAQhC,OAAO,YAAY;GACnB,cAAc,UAAU;EAC1B;CACF,GAAG,CAAC,QAAQ,QAAQ,IAAI,CAAC;CAezB,gBAAgB;EACd,OAAO,OAAO;EACd,aAAa;GACX,OAAO,OAAO;EAChB;CACF,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB;EACd,IAAI,mBAAmB,SAAS,WAAW,QAAQ;GACjD,aAAa,mBAAmB,QAAQ,OAAO;GAC/C,mBAAmB,UAAU;EAC/B;EACA,IAAI,uBAAuB,SAAS;GAClC,aAAa,uBAAuB,OAAO;GAC3C,uBAAuB,UAAU;EACnC;EACA,OAAO,cAAc;EAIrB,gBAAgB,MAAM;EAEtB,aAAa;GACX,uBAAuB,UAAU,iBAAiB;IAChD,IAAI,gBAAgB,YAAY,QAC9B,gBAAgB,UAAU;IAE5B,uBAAuB,UAAU;GACnC,GAAG,CAAC;GASJ,MAAM,WAAW;IACf;IACA,SAAS,iBAAiB;KACxB,IAAI,WAAW,QAAQ,MACrB,OAAO,YAAY;UAEnB,OAAO,KAAK;KAEd,OAAO,QAAQ;KACf,IAAI,mBAAmB,YAAY,UACjC,mBAAmB,UAAU;IAEjC,GAAG,CAAC;GACN;GACA,mBAAmB,UAAU;EAC/B;CACF,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,cAAc,YAClB,OACE,SACA,gBACG;EACH,IAAI;GACF,MAAM,OAAO,YAAY,SAAS,KAAA,GAAW,WAAW;EAC1D,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,eAAe,aAClB,OAAe;EACd,OAAO,aAAa,EAAE;CACxB,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,SAAS,YACb,OAAO,YAAsC;EAC3C,IAAI;GACF,MAAM,OAAO,OAAO,OAAO;EAC7B,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,SAAS,YAAY,YAAY;EACrC,IAAI;GACF,MAAM,OAAO,OAAO;EACtB,UAAU;GACR,gBAAgB,MAAM;EACxB;CACF,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,OAAO,kBAAkB;EAC7B,OAAO,KAAK;CACd,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,QAAQ,kBAAkB;EAC9B,OAAO,MAAM;EACb,gBAAgB,MAAM;CACxB,GAAG,CAAC,QAAQ,eAAe,CAAC;CAE5B,MAAM,sBAAsB,aACzB,gBAA0C;EACzC,OAAO,oBAAoB,WAAW;CACxC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,gBAAgB,YACpB,OAAO,WAMD;EACJ,MAAM,OAAO,cAAc,MAAM;CACnC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,0BAA0B,YAC9B,OAAO,aAAgD;EACrD,MAAM,OAAO,wBAAwB,QAAQ;EAC7C,gBAAgB,MAAM;CACxB,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,mBAAmB,YACvB,OAAO,aAAwC,UAA4B;EACzE,MAAM,SAAS,MAAM,OAAO,iBAC1B,aACA,SAAS,KAAA,CACX;EACA,gBAAgB,MAAM;EACtB,OAAO;CACT,GACA,CAAC,QAAQ,eAAe,CAC1B;CAEA,MAAM,oBAAoB,aAEtB,eAKG;EACH,IAAI,OAAO,eAAe,WACxB,OAAO,kBAAkB,UAAU;OAEnC,OAAO,kBAAkB,UAAU;CAEvC,GACA,CAAC,MAAM,CACT;CAEA,MAAM,mBAAmB,kBAAkB;EACzC,OAAO,iBAAiB;CAC1B,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,kBAAkB,kBAAkB;EACxC,OAAO,gBAAgB;CACzB,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,yBAAyB,aAC5B,aAAwC,UACvC,OAAO,uBAAuB,aAAa,KAAK,GAClD,CAAC,MAAM,CACT;CAYA,MAAM,mBAAmB,OAAO,YAAY;CAE5C,MAAM,uBAAuB,cAA2C;EACtE,IAAI,gBAAgB;EACpB,KAAK,IAAI,IAAI,iBAAiB,SAAS,GAAG,KAAK,GAAG,KAChD,IAAI,iBAAiB,EAAE,EAAE,SAAS,QAAQ;GACxC,gBAAgB;GAChB;EACF;EAEF,IAAI,kBAAkB,IAAI,OAAO;EACjC,KAAK,IAAI,IAAI,iBAAiB,SAAS,GAAG,IAAI,eAAe,KAAK;GAChE,MAAM,IAAI,iBAAiB;GAC3B,IAAI,GAAG,SAAS,aAAa;GAC7B,MAAM,OAAO,EAAE,MAAM,MAClB,MAAiC,EAAE,SAAS,mBAC/C;GACA,IAAI,MAAM,OAAO;EACnB;EACA,OAAO;CACT,GAAG,CAAC,gBAAgB,CAAC;CAErB,MAAM,UAAU,cAAuB;EACrC,IAAI,CAAC,sBAAsB,OAAO,CAAC;EAEnC,OADU,qBAAqB,WAAW,qBAAqB,QAClD,CAAC;CAChB,GAAG,CAAC,oBAAoB,CAAC;CAEzB,MAAM,QAAQ,cAA4B;EACxC,IAAI,CAAC,wBAAwB,qBAAqB,WAAW,YAC3D,OAAO;EAET,OAAO,qBAAqB;CAC9B,GAAG,CAAC,oBAAoB,CAAC;CAMzB,OAAO;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACA;EACA;EACA;EACA,YAAY,eAAe;EAC3B,mBAAmB,eAAe;EAClC,iBAAiB,eAAe;EAChC,UAAU,eAAe;EACzB;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|
|
@@ -53,19 +53,15 @@
|
|
|
53
53
|
"media-generation"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"
|
|
57
|
-
"
|
|
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"
|
|
56
|
+
"@tanstack/markdown": "^0.0.13",
|
|
57
|
+
"@tanstack/ai-client": "^0.32.0"
|
|
62
58
|
},
|
|
63
59
|
"peerDependencies": {
|
|
64
60
|
"@mcp-ui/client": "^7",
|
|
65
61
|
"@types/react": ">=18.0.0",
|
|
66
62
|
"react": ">=18.0.0",
|
|
67
63
|
"react-dom": ">=18.0.0",
|
|
68
|
-
"@tanstack/ai": "^0.
|
|
64
|
+
"@tanstack/ai": "^0.55.0"
|
|
69
65
|
},
|
|
70
66
|
"peerDependenciesMeta": {
|
|
71
67
|
"@mcp-ui/client": {
|
|
@@ -85,7 +81,7 @@
|
|
|
85
81
|
"react": "^19.2.3",
|
|
86
82
|
"react-dom": "^19.2.3",
|
|
87
83
|
"vite": "^8.2.1",
|
|
88
|
-
"@tanstack/ai": "0.
|
|
84
|
+
"@tanstack/ai": "0.55.0"
|
|
89
85
|
},
|
|
90
86
|
"scripts": {
|
|
91
87
|
"clean": "premove ./build ./dist",
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
1
|
+
import { Markdown } from '@tanstack/markdown/react'
|
|
2
|
+
import { streamingMarkdownExtension } from '@tanstack/markdown/extensions/streaming'
|
|
3
|
+
import type { CodeHighlighter, MarkdownExtension } from '@tanstack/markdown'
|
|
4
|
+
import type { MarkdownComponents } from '@tanstack/markdown/react'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_EXTENSIONS: Array<MarkdownExtension> = [
|
|
7
|
+
streamingMarkdownExtension(),
|
|
8
|
+
]
|
|
5
9
|
|
|
6
10
|
export interface TextPartProps {
|
|
7
11
|
/** The text content to render */
|
|
@@ -15,30 +19,25 @@ export interface TextPartProps {
|
|
|
15
19
|
/** Additional className for assistant messages (also used for system messages) */
|
|
16
20
|
assistantClassName?: string
|
|
17
21
|
/**
|
|
18
|
-
* Additional
|
|
19
|
-
*
|
|
20
|
-
*/
|
|
21
|
-
remarkPlugins?: PluggableList
|
|
22
|
-
/**
|
|
23
|
-
* Additional rehype plugins. Inserted between the built-in
|
|
24
|
-
* `rehypeRaw`/`rehypeHighlight` and the trailing `rehypeSanitize`
|
|
25
|
-
* so sanitization always runs last. When `disableDefaultPlugins`
|
|
26
|
-
* is true, replaces the entire chain.
|
|
22
|
+
* Additional TanStack Markdown extensions, appended after the built-in
|
|
23
|
+
* streaming extension.
|
|
27
24
|
*/
|
|
28
|
-
|
|
29
|
-
/** react-markdown `components` overrides (e.g. custom `a`, `code`). */
|
|
30
|
-
components?: Components
|
|
25
|
+
extensions?: Array<MarkdownExtension>
|
|
31
26
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* built-in XSS sanitizer.
|
|
27
|
+
* Synchronous code highlighter. Its output is inserted as trusted HTML,
|
|
28
|
+
* so use only a highlighter that escapes source text (for example
|
|
29
|
+
* `createTanStackMarkdownHighlighter` from `@tanstack/highlight/markdown`).
|
|
36
30
|
*/
|
|
37
|
-
|
|
31
|
+
highlighter?: CodeHighlighter
|
|
32
|
+
/** Replace intrinsic elements (e.g. custom `a`, `img`) by tag name. */
|
|
33
|
+
components?: MarkdownComponents
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
/**
|
|
41
|
-
* TextPart component - renders markdown text with
|
|
37
|
+
* TextPart component - renders markdown text with TanStack Markdown.
|
|
38
|
+
*
|
|
39
|
+
* Raw HTML is escaped and executable URLs are removed, so AI output can be
|
|
40
|
+
* rendered without a separate sanitizer.
|
|
42
41
|
*
|
|
43
42
|
* @example Standalone usage
|
|
44
43
|
* ```tsx
|
|
@@ -51,11 +50,11 @@ export interface TextPartProps {
|
|
|
51
50
|
* />
|
|
52
51
|
* ```
|
|
53
52
|
*
|
|
54
|
-
* @example
|
|
53
|
+
* @example Syntax highlighting
|
|
55
54
|
* ```tsx
|
|
56
|
-
* import
|
|
55
|
+
* import { highlightMarkdownCode } from './markdown-highlighter'
|
|
57
56
|
*
|
|
58
|
-
* <TextPart content={content}
|
|
57
|
+
* <TextPart content={content} highlighter={highlightMarkdownCode} />
|
|
59
58
|
* ```
|
|
60
59
|
*/
|
|
61
60
|
export function TextPart({
|
|
@@ -64,10 +63,9 @@ export function TextPart({
|
|
|
64
63
|
className = '',
|
|
65
64
|
userClassName = '',
|
|
66
65
|
assistantClassName = '',
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
extensions,
|
|
67
|
+
highlighter,
|
|
69
68
|
components,
|
|
70
|
-
disableDefaultPlugins,
|
|
71
69
|
}: TextPartProps) {
|
|
72
70
|
const roleClassName =
|
|
73
71
|
role === 'user'
|
|
@@ -77,21 +75,21 @@ export function TextPart({
|
|
|
77
75
|
: ''
|
|
78
76
|
const combinedClassName = [className, roleClassName].filter(Boolean).join(' ')
|
|
79
77
|
|
|
80
|
-
const resolved = resolveMarkdownPlugins({
|
|
81
|
-
remarkPlugins,
|
|
82
|
-
rehypePlugins,
|
|
83
|
-
disableDefaultPlugins,
|
|
84
|
-
})
|
|
85
|
-
|
|
86
78
|
return (
|
|
87
79
|
<div className={combinedClassName || undefined}>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
<Markdown
|
|
81
|
+
extensions={
|
|
82
|
+
extensions
|
|
83
|
+
? [...DEFAULT_EXTENSIONS, ...extensions]
|
|
84
|
+
: DEFAULT_EXTENSIONS
|
|
85
|
+
}
|
|
86
|
+
frontmatter={false}
|
|
87
|
+
headingIds={false}
|
|
88
|
+
highlighter={highlighter}
|
|
91
89
|
components={components}
|
|
92
90
|
>
|
|
93
91
|
{content}
|
|
94
|
-
</
|
|
92
|
+
</Markdown>
|
|
95
93
|
</div>
|
|
96
94
|
)
|
|
97
95
|
}
|
package/src/types.ts
CHANGED
|
@@ -263,6 +263,16 @@ interface BaseUseChatReturn<
|
|
|
263
263
|
*/
|
|
264
264
|
isLoading: boolean
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* True when the last hydrate or older-page response said more messages exist.
|
|
268
|
+
*/
|
|
269
|
+
hasOlderMessages: boolean
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Fetch the next older window and put it in front of the painted messages.
|
|
273
|
+
*/
|
|
274
|
+
loadOlderMessages: () => Promise<void>
|
|
275
|
+
|
|
266
276
|
/**
|
|
267
277
|
* Current error, if any
|
|
268
278
|
*/
|
package/src/use-chat.ts
CHANGED
|
@@ -53,6 +53,7 @@ export function useChat<
|
|
|
53
53
|
options.initialMessages || [],
|
|
54
54
|
)
|
|
55
55
|
const [isLoading, setIsLoading] = useState(false)
|
|
56
|
+
const [hasOlderMessages, setHasOlderMessages] = useState(false)
|
|
56
57
|
const [error, setError] = useState<Error | undefined>(undefined)
|
|
57
58
|
const [status, setStatus] = useState<ChatClientState>('ready')
|
|
58
59
|
const [isSubscribed, setIsSubscribed] = useState(false)
|
|
@@ -149,16 +150,25 @@ export function useChat<
|
|
|
149
150
|
...transport,
|
|
150
151
|
initialMessages: messagesToUse,
|
|
151
152
|
...(typeof initialOptions.threadId === 'string' &&
|
|
152
|
-
initialOptions.persistence
|
|
153
|
+
initialOptions.persistence === true
|
|
153
154
|
? {
|
|
154
|
-
persistence:
|
|
155
|
+
persistence: true,
|
|
155
156
|
threadId: initialOptions.threadId,
|
|
157
|
+
...(initialOptions.history !== undefined && {
|
|
158
|
+
history: initialOptions.history,
|
|
159
|
+
}),
|
|
156
160
|
}
|
|
157
|
-
:
|
|
158
|
-
|
|
161
|
+
: typeof initialOptions.threadId === 'string' &&
|
|
162
|
+
initialOptions.persistence
|
|
163
|
+
? {
|
|
164
|
+
persistence: initialOptions.persistence,
|
|
159
165
|
threadId: initialOptions.threadId,
|
|
166
|
+
}
|
|
167
|
+
: {
|
|
168
|
+
...(initialOptions.threadId !== undefined && {
|
|
169
|
+
threadId: initialOptions.threadId,
|
|
170
|
+
}),
|
|
160
171
|
}),
|
|
161
|
-
}),
|
|
162
172
|
...(initialOptions.body !== undefined && { body: initialOptions.body }),
|
|
163
173
|
...(initialOptions.forwardedProps !== undefined && {
|
|
164
174
|
forwardedProps: initialOptions.forwardedProps,
|
|
@@ -214,6 +224,9 @@ export function useChat<
|
|
|
214
224
|
onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
|
|
215
225
|
runOrQueueForActiveInstance(() => {
|
|
216
226
|
setMessages(newMessages)
|
|
227
|
+
const currentInstance = getActiveInstance()
|
|
228
|
+
if (!currentInstance) return
|
|
229
|
+
setHasOlderMessages(currentInstance.getHasOlderMessages())
|
|
217
230
|
})
|
|
218
231
|
},
|
|
219
232
|
onLoadingChange: (newIsLoading: boolean) => {
|
|
@@ -308,6 +321,7 @@ export function useChat<
|
|
|
308
321
|
if (clientMessages !== messagesRef.current) {
|
|
309
322
|
setMessages(clientMessages)
|
|
310
323
|
}
|
|
324
|
+
setHasOlderMessages(client.getHasOlderMessages())
|
|
311
325
|
}, [client])
|
|
312
326
|
|
|
313
327
|
// Sync each wire-payload slot in its own effect so an unrelated option
|
|
@@ -465,6 +479,11 @@ export function useChat<
|
|
|
465
479
|
}
|
|
466
480
|
}, [client, syncResumeState])
|
|
467
481
|
|
|
482
|
+
const loadOlderMessages = useCallback(async () => {
|
|
483
|
+
await client.loadOlderMessages()
|
|
484
|
+
setHasOlderMessages(client.getHasOlderMessages())
|
|
485
|
+
}, [client])
|
|
486
|
+
|
|
468
487
|
const stop = useCallback(() => {
|
|
469
488
|
client.stop()
|
|
470
489
|
}, [client])
|
|
@@ -601,6 +620,8 @@ export function useChat<
|
|
|
601
620
|
reload,
|
|
602
621
|
stop,
|
|
603
622
|
isLoading,
|
|
623
|
+
hasOlderMessages,
|
|
624
|
+
loadOlderMessages,
|
|
604
625
|
error,
|
|
605
626
|
status,
|
|
606
627
|
isSubscribed,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Options as ReactMarkdownOptions } from 'react-markdown';
|
|
2
|
-
export type PluggableList = NonNullable<ReactMarkdownOptions['remarkPlugins']>;
|
|
3
|
-
export declare const DEFAULT_REMARK_PLUGINS: PluggableList;
|
|
4
|
-
export declare const DEFAULT_REHYPE_PLUGINS_BEFORE_USER: PluggableList;
|
|
5
|
-
export declare const DEFAULT_REHYPE_PLUGINS_AFTER_USER: PluggableList;
|
|
6
|
-
export interface ResolveMarkdownPluginsOptions {
|
|
7
|
-
remarkPlugins?: PluggableList;
|
|
8
|
-
rehypePlugins?: PluggableList;
|
|
9
|
-
disableDefaultPlugins?: boolean;
|
|
10
|
-
}
|
|
11
|
-
export interface ResolvedMarkdownPlugins {
|
|
12
|
-
remarkPlugins: PluggableList;
|
|
13
|
-
rehypePlugins: PluggableList;
|
|
14
|
-
}
|
|
15
|
-
export declare function resolveMarkdownPlugins(options: ResolveMarkdownPluginsOptions): ResolvedMarkdownPlugins;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import remarkGfm from "remark-gfm";
|
|
2
|
-
import rehypeRaw from "rehype-raw";
|
|
3
|
-
import rehypeSanitize from "rehype-sanitize";
|
|
4
|
-
import rehypeHighlight from "rehype-highlight";
|
|
5
|
-
//#region src/chat-ui/markdown-plugins.ts
|
|
6
|
-
var DEFAULT_REMARK_PLUGINS = [remarkGfm];
|
|
7
|
-
var DEFAULT_REHYPE_PLUGINS_BEFORE_USER = [rehypeRaw, rehypeHighlight];
|
|
8
|
-
var DEFAULT_REHYPE_PLUGINS_AFTER_USER = [rehypeSanitize];
|
|
9
|
-
function resolveMarkdownPlugins(options) {
|
|
10
|
-
const userRemark = options.remarkPlugins ?? [];
|
|
11
|
-
const userRehype = options.rehypePlugins ?? [];
|
|
12
|
-
if (options.disableDefaultPlugins) return {
|
|
13
|
-
remarkPlugins: [...userRemark],
|
|
14
|
-
rehypePlugins: [...userRehype]
|
|
15
|
-
};
|
|
16
|
-
return {
|
|
17
|
-
remarkPlugins: [...DEFAULT_REMARK_PLUGINS, ...userRemark],
|
|
18
|
-
rehypePlugins: [
|
|
19
|
-
...DEFAULT_REHYPE_PLUGINS_BEFORE_USER,
|
|
20
|
-
...userRehype,
|
|
21
|
-
...DEFAULT_REHYPE_PLUGINS_AFTER_USER
|
|
22
|
-
]
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
//#endregion
|
|
26
|
-
export { DEFAULT_REHYPE_PLUGINS_AFTER_USER, DEFAULT_REHYPE_PLUGINS_BEFORE_USER, DEFAULT_REMARK_PLUGINS, resolveMarkdownPlugins };
|
|
27
|
-
|
|
28
|
-
//# sourceMappingURL=markdown-plugins.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-plugins.js","names":[],"sources":["../../../src/chat-ui/markdown-plugins.ts"],"sourcesContent":["import remarkGfm from 'remark-gfm'\nimport rehypeRaw from 'rehype-raw'\nimport rehypeSanitize from 'rehype-sanitize'\nimport rehypeHighlight from 'rehype-highlight'\nimport type { Options as ReactMarkdownOptions } from 'react-markdown'\n\nexport type PluggableList = NonNullable<ReactMarkdownOptions['remarkPlugins']>\n\nexport const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm]\nexport const DEFAULT_REHYPE_PLUGINS_BEFORE_USER: PluggableList = [\n rehypeRaw,\n rehypeHighlight,\n]\nexport const DEFAULT_REHYPE_PLUGINS_AFTER_USER: PluggableList = [rehypeSanitize]\n\nexport interface ResolveMarkdownPluginsOptions {\n remarkPlugins?: PluggableList\n rehypePlugins?: PluggableList\n disableDefaultPlugins?: boolean\n}\n\nexport interface ResolvedMarkdownPlugins {\n remarkPlugins: PluggableList\n rehypePlugins: PluggableList\n}\n\nexport function resolveMarkdownPlugins(\n options: ResolveMarkdownPluginsOptions,\n): ResolvedMarkdownPlugins {\n const userRemark = options.remarkPlugins ?? []\n const userRehype = options.rehypePlugins ?? []\n\n if (options.disableDefaultPlugins) {\n return {\n remarkPlugins: [...userRemark],\n rehypePlugins: [...userRehype],\n }\n }\n\n return {\n remarkPlugins: [...DEFAULT_REMARK_PLUGINS, ...userRemark],\n rehypePlugins: [\n ...DEFAULT_REHYPE_PLUGINS_BEFORE_USER,\n ...userRehype,\n ...DEFAULT_REHYPE_PLUGINS_AFTER_USER,\n ],\n }\n}\n"],"mappings":";;;;;AAQA,IAAa,yBAAwC,CAAC,SAAS;AAC/D,IAAa,qCAAoD,CAC/D,WACA,eACF;AACA,IAAa,oCAAmD,CAAC,cAAc;AAa/E,SAAgB,uBACd,SACyB;CACzB,MAAM,aAAa,QAAQ,iBAAiB,CAAC;CAC7C,MAAM,aAAa,QAAQ,iBAAiB,CAAC;CAE7C,IAAI,QAAQ,uBACV,OAAO;EACL,eAAe,CAAC,GAAG,UAAU;EAC7B,eAAe,CAAC,GAAG,UAAU;CAC/B;CAGF,OAAO;EACL,eAAe,CAAC,GAAG,wBAAwB,GAAG,UAAU;EACxD,eAAe;GACb,GAAG;GACH,GAAG;GACH,GAAG;EACL;CACF;AACF"}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import remarkGfm from 'remark-gfm'
|
|
2
|
-
import rehypeRaw from 'rehype-raw'
|
|
3
|
-
import rehypeSanitize from 'rehype-sanitize'
|
|
4
|
-
import rehypeHighlight from 'rehype-highlight'
|
|
5
|
-
import type { Options as ReactMarkdownOptions } from 'react-markdown'
|
|
6
|
-
|
|
7
|
-
export type PluggableList = NonNullable<ReactMarkdownOptions['remarkPlugins']>
|
|
8
|
-
|
|
9
|
-
export const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm]
|
|
10
|
-
export const DEFAULT_REHYPE_PLUGINS_BEFORE_USER: PluggableList = [
|
|
11
|
-
rehypeRaw,
|
|
12
|
-
rehypeHighlight,
|
|
13
|
-
]
|
|
14
|
-
export const DEFAULT_REHYPE_PLUGINS_AFTER_USER: PluggableList = [rehypeSanitize]
|
|
15
|
-
|
|
16
|
-
export interface ResolveMarkdownPluginsOptions {
|
|
17
|
-
remarkPlugins?: PluggableList
|
|
18
|
-
rehypePlugins?: PluggableList
|
|
19
|
-
disableDefaultPlugins?: boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ResolvedMarkdownPlugins {
|
|
23
|
-
remarkPlugins: PluggableList
|
|
24
|
-
rehypePlugins: PluggableList
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function resolveMarkdownPlugins(
|
|
28
|
-
options: ResolveMarkdownPluginsOptions,
|
|
29
|
-
): ResolvedMarkdownPlugins {
|
|
30
|
-
const userRemark = options.remarkPlugins ?? []
|
|
31
|
-
const userRehype = options.rehypePlugins ?? []
|
|
32
|
-
|
|
33
|
-
if (options.disableDefaultPlugins) {
|
|
34
|
-
return {
|
|
35
|
-
remarkPlugins: [...userRemark],
|
|
36
|
-
rehypePlugins: [...userRehype],
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
remarkPlugins: [...DEFAULT_REMARK_PLUGINS, ...userRemark],
|
|
42
|
-
rehypePlugins: [
|
|
43
|
-
...DEFAULT_REHYPE_PLUGINS_BEFORE_USER,
|
|
44
|
-
...userRehype,
|
|
45
|
-
...DEFAULT_REHYPE_PLUGINS_AFTER_USER,
|
|
46
|
-
],
|
|
47
|
-
}
|
|
48
|
-
}
|