@tanstack/ai-solid 0.21.1 → 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.
package/dist/byok.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { ByokClient, ByokPrompt, ByokProvider, ByokProviderInit, ByokSnapshot, DefineByokOptions, EMPTY_BYOK_SNAPSHOT, KeyPreview, KeyStatus, Keyring, KeyringStorage, PasskeyStorageOptions, decryptKeyring, defaultByokStorage, defineByok, defineByokProvider, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, memoryStorage, passkeyStorage } from "@tanstack/ai-client/byok";
2
+ export { ByokClient, type ByokPrompt, type ByokProvider, type ByokProviderInit, type ByokSnapshot, type DefineByokOptions, EMPTY_BYOK_SNAPSHOT, type KeyPreview, type KeyStatus, type Keyring, type KeyringStorage, type PasskeyStorageOptions, decryptKeyring, defaultByokStorage, defineByok, defineByokProvider, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, memoryStorage, passkeyStorage };
package/dist/byok.js ADDED
@@ -0,0 +1,3 @@
1
+ import { ByokClient, EMPTY_BYOK_SNAPSHOT, decryptKeyring, defaultByokStorage, defineByok, defineByokProvider, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, memoryStorage, passkeyStorage } from "@tanstack/ai-client/byok";
2
+
3
+ export { ByokClient, EMPTY_BYOK_SNAPSHOT, decryptKeyring, defaultByokStorage, defineByok, defineByokProvider, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, memoryStorage, passkeyStorage };
package/dist/index.d.ts CHANGED
@@ -11,5 +11,5 @@ import { UseTranscriptionOptions, UseTranscriptionReturn, useTranscription } fro
11
11
  import { UseSummarizeOptions, UseSummarizeReturn, useSummarize } from "./use-summarize.js";
12
12
  import { UseGenerateVideoOptions, UseGenerateVideoReturn, useGenerateVideo } from "./use-generate-video.js";
13
13
  import { UseAudioRecorderOptions, UseAudioRecorderReturn, useAudioRecorder } from "./use-audio-recorder.js";
14
- import { AudioGenerateInput, ChatClientPersistence, ChatPersistedState, ChatPersistenceOption, ChatStorageAdapter, ConnectConnectionAdapter, ConnectionAdapter, FetchConnectionOptions, GenerationClientState, ImageGenerateInput, IndexedDBPersistenceOptions, InferChatMessages, RunAgentInputContext, SpeechGenerateInput, StorageUnavailableError, SubscribeConnectionAdapter, SummarizeGenerateInput, TranscriptionGenerateInput, VideoGenerateInput, VideoGenerateResult, VideoStatusInfo, WebSocketConnectionOptions, WebStoragePersistenceOptions, XhrConnectionOptions, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
15
- export { type AudioGenerateInput, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatRequestBody, type ChatStorageAdapter, type ConnectConnectionAdapter, type ConnectionAdapter, type DeepPartial, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type IndexedDBPersistenceOptions, type InferChatMessages, type QueueConfig, type QueueOption, type QueueStrategy, type QueuedMessage, type RunAgentInputContext, type SendMessageOptions, type SpeechGenerateInput, StorageUnavailableError, type SubscribeConnectionAdapter, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseAudioRecorderOptions, type UseAudioRecorderReturn, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type UseWebMCPToolsOptions, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, type WebSocketConnectionOptions, type WebStoragePersistenceOptions, type WhenBusy, type XhrConnectionOptions, createChatClientOptions, createChatHook, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, useWebMCPTools, webSocket, xhrHttpStream, xhrServerSentEvents };
14
+ import { AudioGenerateInput, AudioRecorder, BatchStrategy, ChatClient, ChatClientPersistence, ChatPersistedState, ChatPersistenceOption, ChatStorageAdapter, CompositeStrategy, ConnectConnectionAdapter, ConnectionAdapter, CreateMcpAppBridgeOptions, DurableStreamIncompleteError, FetchConnectionOptions, GENERATION_EVENTS, GenerationClient, GenerationClientState, ImageGenerateInput, ImmediateStrategy, IndexedDBPersistenceOptions, InferChatMessages, InterruptManager, McpAppBridge, PartialJSONParser, PunctuationStrategy, RealtimeClient, RunAgentInputContext, SpeechGenerateInput, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, SubscribeConnectionAdapter, SummarizeGenerateInput, TranscriptionGenerateInput, UnsupportedResponseStreamError, VideoGenerateInput, VideoGenerateResult, VideoGenerationClient, VideoStatusInfo, WebSocketConnectionOptions, WebStoragePersistenceOptions, WordBoundaryStrategy, XhrConnectionOptions, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
15
+ export { type AudioGenerateInput, AudioRecorder, BatchStrategy, ChatClient, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatRequestBody, type ChatStorageAdapter, CompositeStrategy, type ConnectConnectionAdapter, type ConnectionAdapter, type CreateMcpAppBridgeOptions, type DeepPartial, DurableStreamIncompleteError, type FetchConnectionOptions, GENERATION_EVENTS, GenerationClient, type GenerationClientState, type ImageGenerateInput, ImmediateStrategy, type IndexedDBPersistenceOptions, type InferChatMessages, InterruptManager, type McpAppBridge, PartialJSONParser, PunctuationStrategy, type QueueConfig, type QueueOption, type QueueStrategy, type QueuedMessage, RealtimeClient, type RunAgentInputContext, type SendMessageOptions, type SpeechGenerateInput, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, type SubscribeConnectionAdapter, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, UnsupportedResponseStreamError, type UseAudioRecorderOptions, type UseAudioRecorderReturn, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type UseWebMCPToolsOptions, type VideoGenerateInput, type VideoGenerateResult, VideoGenerationClient, type VideoStatusInfo, type WebSocketConnectionOptions, type WebStoragePersistenceOptions, type WhenBusy, WordBoundaryStrategy, type XhrConnectionOptions, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createChatHook, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, useWebMCPTools, webSocket, xhrHttpStream, xhrServerSentEvents };
package/dist/index.js CHANGED
@@ -10,6 +10,6 @@ import { useTranscription } from "./use-transcription.js";
10
10
  import { useSummarize } from "./use-summarize.js";
11
11
  import { useGenerateVideo } from "./use-generate-video.js";
12
12
  import { useAudioRecorder } from "./use-audio-recorder.js";
13
- import { StorageUnavailableError, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
13
+ import { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
14
14
 
15
- export { StorageUnavailableError, createChatClientOptions, createChatHook, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, useWebMCPTools, webSocket, xhrHttpStream, xhrServerSentEvents };
15
+ export { AudioRecorder, BatchStrategy, ChatClient, CompositeStrategy, DurableStreamIncompleteError, GENERATION_EVENTS, GenerationClient, ImmediateStrategy, InterruptManager, PartialJSONParser, PunctuationStrategy, RealtimeClient, StorageUnavailableError, StreamProcessor, StreamReconnectLimitError, StreamTruncatedError, UnsupportedResponseStreamError, VideoGenerationClient, WordBoundaryStrategy, clientTools, convertMessagesToModelMessages, createAIDevtoolsGenerationPreview, createChatClientOptions, createChatHook, createMcpAppBridge, defaultJSONParser, fetchHttpStream, fetchServerSentEvents, generateMessageId, indexedDBPersistence, localStoragePersistence, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, parsePartialJSON, reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult, registerWebMCPTools, rpcStream, sessionStoragePersistence, stream, uiMessageToModelMessages, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, useWebMCPTools, webSocket, xhrHttpStream, xhrServerSentEvents };
package/dist/types.d.ts CHANGED
@@ -141,6 +141,14 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool$1> = any,
141
141
  * Whether a response is currently being generated
142
142
  */
143
143
  isLoading: Accessor<boolean>;
144
+ /**
145
+ * True when the last hydrate or older-page response said more messages exist.
146
+ */
147
+ hasOlderMessages: Accessor<boolean>;
148
+ /**
149
+ * Fetch the next older window and put it in front of the painted messages.
150
+ */
151
+ loadOlderMessages: () => Promise<void>;
144
152
  /**
145
153
  * Current error, if any
146
154
  */
package/dist/use-chat.js CHANGED
@@ -10,6 +10,7 @@ function useChat(options = {}) {
10
10
  const clientId = options.threadId ?? hookId;
11
11
  const [messages, setMessages] = createSignal(options.initialMessages || []);
12
12
  const [isLoading, setIsLoading] = createSignal(false);
13
+ const [hasOlderMessages, setHasOlderMessages] = createSignal(false);
13
14
  const [error, setError] = createSignal(void 0);
14
15
  const [status, setStatus] = createSignal("ready");
15
16
  const [isSubscribed, setIsSubscribed] = createSignal(false);
@@ -28,11 +29,15 @@ function useChat(options = {}) {
28
29
  setInterruptState(client().getInterruptState());
29
30
  };
30
31
  const client = createMemo(() => {
31
- return new ChatClient({
32
+ const instance = new ChatClient({
32
33
  devtoolsBridgeFactory: createChatDevtoolsBridge,
33
34
  ...options.connection ? { connection: options.connection } : { fetcher: options.fetcher },
34
35
  ...options.initialMessages !== void 0 && { initialMessages: options.initialMessages },
35
- ...typeof options.threadId === "string" && options.persistence ? {
36
+ ...typeof options.threadId === "string" && options.persistence === true ? {
37
+ persistence: true,
38
+ threadId: options.threadId,
39
+ ...options.history !== void 0 && { history: options.history }
40
+ } : typeof options.threadId === "string" && options.persistence ? {
36
41
  persistence: options.persistence,
37
42
  threadId: options.threadId
38
43
  } : { ...options.threadId !== void 0 && { threadId: options.threadId } },
@@ -64,6 +69,7 @@ function useChat(options = {}) {
64
69
  ...options.streamProcessor !== void 0 && { streamProcessor: options.streamProcessor },
65
70
  onMessagesChange: (newMessages) => {
66
71
  setMessages(newMessages);
72
+ setHasOlderMessages(instance.getHasOlderMessages());
67
73
  },
68
74
  onLoadingChange: (newIsLoading) => {
69
75
  setIsLoading(newIsLoading);
@@ -103,8 +109,10 @@ function useChat(options = {}) {
103
109
  options.onInterruptStateChange?.(nextInterruptState, context);
104
110
  }
105
111
  });
112
+ return instance;
106
113
  }, [clientId]);
107
114
  setMessages(client().getMessages());
115
+ setHasOlderMessages(client().getHasOlderMessages());
108
116
  syncResumeState();
109
117
  createEffect(() => {
110
118
  client().updateOptions({
@@ -152,6 +160,10 @@ function useChat(options = {}) {
152
160
  syncResumeState();
153
161
  }
154
162
  };
163
+ const loadOlderMessages = async () => {
164
+ await client().loadOlderMessages();
165
+ setHasOlderMessages(client().getHasOlderMessages());
166
+ };
155
167
  const stop = () => {
156
168
  client().stop();
157
169
  };
@@ -214,6 +226,8 @@ function useChat(options = {}) {
214
226
  reload,
215
227
  stop,
216
228
  isLoading,
229
+ hasOlderMessages,
230
+ loadOlderMessages,
217
231
  error,
218
232
  status,
219
233
  isSubscribed,
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n onCleanup,\n onMount,\n} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\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'\nimport type {\n AnyClientTool,\n InterruptDefinition,\n InferSchemaType,\n ModelMessage,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\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<\n TTools,\n TSchema,\n TContext,\n TInterrupts\n > = {} as 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 recreation key when no\n // `threadId` is given. It is never sent on the wire.\n const hookId = createUniqueId()\n const clientId = options.threadId ?? hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n const [status, setStatus] = createSignal<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = createSignal(false)\n const [connectionStatus, setConnectionStatus] =\n createSignal<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = createSignal(false)\n const [queue, setQueue] = createSignal<Array<QueuedMessage>>([])\n const [runId, setRunId] = createSignal<string | null>(null)\n const [interruptState, setInterruptState] = createSignal<\n ChatInterruptState<TTools, TInterrupts>\n >({\n interrupts: EMPTY_INTERRUPTS,\n pendingInterrupts: EMPTY_INTERRUPTS,\n interruptErrors: EMPTY_INTERRUPT_ERRORS,\n resuming: false,\n })\n\n const syncResumeState = () => {\n setRunId(client().getCurrentRunId())\n setInterruptState(client().getInterruptState())\n }\n\n // Structured-output `partial` / `final` are derived from `messages` —\n // specifically from the structured-output part on the latest assistant\n // message (the one after the most recent user message). Per-turn parts\n // keep history coherent without a separate reset signal.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Create ChatClient instance with callbacks to sync state.\n // Every user-provided callback is wrapped so the LATEST `options.xxx` value\n // is read at call time. Direct assignment would freeze the callback to the\n // reference we saw at creation; the wrapper lets reactive `options` or\n // in-place mutations propagate. When the user clears a callback (sets it to\n // undefined), `?.` no-ops.\n const client = createMemo(() => {\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 transport = options.connection\n ? { connection: options.connection }\n : { fetcher: options.fetcher }\n return new ChatClient<TTools, TContext, TInterrupts>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n ...(options.initialMessages !== undefined && {\n initialMessages: options.initialMessages,\n }),\n ...(typeof options.threadId === 'string' && options.persistence\n ? {\n persistence: options.persistence,\n threadId: options.threadId,\n }\n : {\n ...(options.threadId !== undefined && {\n threadId: options.threadId,\n }),\n }),\n ...(options.initialResumeSnapshot !== undefined && {\n initialResumeSnapshot: options.initialResumeSnapshot,\n }),\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n ...(options.byok !== undefined && { byok: options.byok }),\n byokProvider: () => options.byokProvider?.(),\n ...(options.context !== undefined && { context: options.context }),\n devtools: {\n ...options.devtools,\n framework: 'solid',\n hookName: 'useChat',\n outputKind: options.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n options.onChunk?.(chunk)\n },\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n ...(options.interrupts !== undefined && {\n interrupts: options.interrupts,\n }),\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n syncResumeState()\n },\n onStatusChange: (newStatus: ChatClientState) => {\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n ...(options.queue !== undefined && { queue: options.queue }),\n onQueueChange: (nextQueue: Array<QueuedMessage>) => {\n setQueue(nextQueue)\n },\n onRunIdChange: (nextRunId) => {\n setRunId(nextRunId)\n },\n onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {\n setInterruptState((current) => ({\n ...current,\n interrupts: nextPendingInterrupts,\n pendingInterrupts: nextPendingInterrupts,\n }))\n },\n onInterruptStateChange: (nextInterruptState, context) => {\n setInterruptState(nextInterruptState)\n options.onInterruptStateChange?.(nextInterruptState, context)\n },\n })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n }, [clientId])\n\n setMessages(client().getMessages())\n syncResumeState()\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n createEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client().updateOptions({\n ...(options.body !== undefined && { body: options.body }),\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n ...(options.queue !== undefined && { queue: options.queue }),\n })\n })\n\n // Apply initial live mode immediately on hook creation.\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n\n createEffect(() => {\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n })\n\n onMount(() => {\n // START TAILING HERE, not in the constructor. A client is idle until a view\n // attaches it, so a client that gets built and thrown away never opens a\n // connection — an unreachable stream would hold one of the browser's ~6\n // connections per origin until the page reloaded.\n client().attach()\n client().mountDevtools()\n // Delivery-durability resume is transparent: the resumable SSE connection\n // adapter reattaches via the browser's native Last-Event-ID on reconnect.\n // We only seed interrupt (state) resume from the client here.\n syncResumeState()\n })\n\n // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n // Release the connection first: `detach` is the counterpart of the `attach`\n // above, and it keeps the transcript and resume pointer so a later mount can\n // pick the run back up from the durable log.\n client().detach()\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\n client().dispose()\n })\n\n // Callback options are read through `options.xxx` at call time, so reactive\n // or mutated options propagate without recreating the client.\n\n const sendMessage = async (\n content: string | MultimodalContent,\n sendOptions?: SendMessageOptions,\n ) => {\n try {\n await client().sendMessage(content, undefined, sendOptions)\n } finally {\n syncResumeState()\n }\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n try {\n await client().append(message)\n } finally {\n syncResumeState()\n }\n }\n\n const reload = async () => {\n try {\n await client().reload()\n } finally {\n syncResumeState()\n }\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n syncResumeState()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = 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\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n syncResumeState()\n }\n\n const cancelQueued = (id: string) => client().cancelQueued(id)\n\n const resumeInterrupts = async (\n resumeItems: Array<RunAgentResumeItem>,\n state?: ChatResumeState,\n ) => {\n const result = await client().resumeInterrupts(resumeItems, state)\n syncResumeState()\n return result\n }\n\n const resolveInterrupts = (\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\n const cancelInterrupts = () => {\n client().cancelInterrupts()\n }\n\n const retryInterrupts = () => {\n client().retryInterrupts()\n }\n\n const resumeInterruptsUnsafe = (\n resumeItems: Array<RunAgentResumeItem>,\n state?: ChatResumeState,\n ) => client().resumeInterruptsUnsafe(resumeItems, state)\n\n const interrupts = () => interruptState().interrupts\n const pendingInterrupts = () => interruptState().pendingInterrupts\n const interruptErrors = () => interruptState().interruptErrors\n const resuming = () => interruptState().resuming\n\n // The \"active\" structured-output part is on the assistant message after\n // the latest user message. When no user message exists yet, return null\n // rather than scanning history — otherwise a stale `final` from\n // `initialMessages` would leak into the value on first render.\n const activeStructuredPart = createMemo<StructuredOutputPart | null>(() => {\n const list = messages()\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = list.length - 1; i > lastUserIndex; i--) {\n const m = list[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 })\n\n const partial = createMemo<Partial>(() => {\n const part = activeStructuredPart()\n if (!part) return {} as Partial\n const v = part.partial ?? part.data\n return (v ?? {}) as Partial\n })\n\n const final = createMemo<Final | null>(() => {\n const part = activeStructuredPart()\n if (!part || part.status !== 'complete') return null\n return part.data as Final\n })\n\n // partial / final are runtime-tracked unconditionally; the conditional\n // return type hides them when no `outputSchema` is supplied.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- primitive return shape diverges from generic UseChatReturn<TTools, TSchema>; TS can't structurally narrow the conditional partial/final fields\n return {\n messages,\n queue,\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 cancelQueued,\n runId,\n interrupts,\n pendingInterrupts,\n interruptErrors,\n resuming,\n resolveInterrupts,\n cancelInterrupts,\n retryInterrupts,\n resumeInterruptsUnsafe,\n resumeInterrupts,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema, TInterrupts>\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n onCleanup,\n onMount,\n} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\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'\nimport type {\n AnyClientTool,\n InterruptDefinition,\n InferSchemaType,\n ModelMessage,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\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<\n TTools,\n TSchema,\n TContext,\n TInterrupts\n > = {} as 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 recreation key when no\n // `threadId` is given. It is never sent on the wire.\n const hookId = createUniqueId()\n const clientId = options.threadId ?? hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [hasOlderMessages, setHasOlderMessages] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n const [status, setStatus] = createSignal<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = createSignal(false)\n const [connectionStatus, setConnectionStatus] =\n createSignal<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = createSignal(false)\n const [queue, setQueue] = createSignal<Array<QueuedMessage>>([])\n const [runId, setRunId] = createSignal<string | null>(null)\n const [interruptState, setInterruptState] = createSignal<\n ChatInterruptState<TTools, TInterrupts>\n >({\n interrupts: EMPTY_INTERRUPTS,\n pendingInterrupts: EMPTY_INTERRUPTS,\n interruptErrors: EMPTY_INTERRUPT_ERRORS,\n resuming: false,\n })\n\n const syncResumeState = () => {\n setRunId(client().getCurrentRunId())\n setInterruptState(client().getInterruptState())\n }\n\n // Structured-output `partial` / `final` are derived from `messages` —\n // specifically from the structured-output part on the latest assistant\n // message (the one after the most recent user message). Per-turn parts\n // keep history coherent without a separate reset signal.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Create ChatClient instance with callbacks to sync state.\n // Every user-provided callback is wrapped so the LATEST `options.xxx` value\n // is read at call time. Direct assignment would freeze the callback to the\n // reference we saw at creation; the wrapper lets reactive `options` or\n // in-place mutations propagate. When the user clears a callback (sets it to\n // undefined), `?.` no-ops.\n const client = createMemo(() => {\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 transport = options.connection\n ? { connection: options.connection }\n : { fetcher: options.fetcher }\n const instance = new ChatClient<TTools, TContext, TInterrupts>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n ...(options.initialMessages !== undefined && {\n initialMessages: options.initialMessages,\n }),\n ...(typeof options.threadId === 'string' && options.persistence === true\n ? {\n persistence: true,\n threadId: options.threadId,\n ...(options.history !== undefined && {\n history: options.history,\n }),\n }\n : typeof options.threadId === 'string' && options.persistence\n ? {\n persistence: options.persistence,\n threadId: options.threadId,\n }\n : {\n ...(options.threadId !== undefined && {\n threadId: options.threadId,\n }),\n }),\n ...(options.initialResumeSnapshot !== undefined && {\n initialResumeSnapshot: options.initialResumeSnapshot,\n }),\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n ...(options.byok !== undefined && { byok: options.byok }),\n byokProvider: () => options.byokProvider?.(),\n ...(options.context !== undefined && { context: options.context }),\n devtools: {\n ...options.devtools,\n framework: 'solid',\n hookName: 'useChat',\n outputKind: options.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n options.onChunk?.(chunk)\n },\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n ...(options.interrupts !== undefined && {\n interrupts: options.interrupts,\n }),\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n setHasOlderMessages(instance.getHasOlderMessages())\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n syncResumeState()\n },\n onStatusChange: (newStatus: ChatClientState) => {\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n ...(options.queue !== undefined && { queue: options.queue }),\n onQueueChange: (nextQueue: Array<QueuedMessage>) => {\n setQueue(nextQueue)\n },\n onRunIdChange: (nextRunId) => {\n setRunId(nextRunId)\n },\n onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {\n setInterruptState((current) => ({\n ...current,\n interrupts: nextPendingInterrupts,\n pendingInterrupts: nextPendingInterrupts,\n }))\n },\n onInterruptStateChange: (nextInterruptState, context) => {\n setInterruptState(nextInterruptState)\n options.onInterruptStateChange?.(nextInterruptState, context)\n },\n })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n return instance\n }, [clientId])\n\n setMessages(client().getMessages())\n setHasOlderMessages(client().getHasOlderMessages())\n syncResumeState()\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n createEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client().updateOptions({\n ...(options.body !== undefined && { body: options.body }),\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n ...(options.queue !== undefined && { queue: options.queue }),\n })\n })\n\n // Apply initial live mode immediately on hook creation.\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n\n createEffect(() => {\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n })\n\n onMount(() => {\n // START TAILING HERE, not in the constructor. A client is idle until a view\n // attaches it, so a client that gets built and thrown away never opens a\n // connection — an unreachable stream would hold one of the browser's ~6\n // connections per origin until the page reloaded.\n client().attach()\n client().mountDevtools()\n // Delivery-durability resume is transparent: the resumable SSE connection\n // adapter reattaches via the browser's native Last-Event-ID on reconnect.\n // We only seed interrupt (state) resume from the client here.\n syncResumeState()\n })\n\n // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n // Release the connection first: `detach` is the counterpart of the `attach`\n // above, and it keeps the transcript and resume pointer so a later mount can\n // pick the run back up from the durable log.\n client().detach()\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\n client().dispose()\n })\n\n // Callback options are read through `options.xxx` at call time, so reactive\n // or mutated options propagate without recreating the client.\n\n const sendMessage = async (\n content: string | MultimodalContent,\n sendOptions?: SendMessageOptions,\n ) => {\n try {\n await client().sendMessage(content, undefined, sendOptions)\n } finally {\n syncResumeState()\n }\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n try {\n await client().append(message)\n } finally {\n syncResumeState()\n }\n }\n\n const reload = async () => {\n try {\n await client().reload()\n } finally {\n syncResumeState()\n }\n }\n\n const loadOlderMessages = async () => {\n await client().loadOlderMessages()\n setHasOlderMessages(client().getHasOlderMessages())\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n syncResumeState()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = 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\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n syncResumeState()\n }\n\n const cancelQueued = (id: string) => client().cancelQueued(id)\n\n const resumeInterrupts = async (\n resumeItems: Array<RunAgentResumeItem>,\n state?: ChatResumeState,\n ) => {\n const result = await client().resumeInterrupts(resumeItems, state)\n syncResumeState()\n return result\n }\n\n const resolveInterrupts = (\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\n const cancelInterrupts = () => {\n client().cancelInterrupts()\n }\n\n const retryInterrupts = () => {\n client().retryInterrupts()\n }\n\n const resumeInterruptsUnsafe = (\n resumeItems: Array<RunAgentResumeItem>,\n state?: ChatResumeState,\n ) => client().resumeInterruptsUnsafe(resumeItems, state)\n\n const interrupts = () => interruptState().interrupts\n const pendingInterrupts = () => interruptState().pendingInterrupts\n const interruptErrors = () => interruptState().interruptErrors\n const resuming = () => interruptState().resuming\n\n // The \"active\" structured-output part is on the assistant message after\n // the latest user message. When no user message exists yet, return null\n // rather than scanning history — otherwise a stale `final` from\n // `initialMessages` would leak into the value on first render.\n const activeStructuredPart = createMemo<StructuredOutputPart | null>(() => {\n const list = messages()\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = list.length - 1; i > lastUserIndex; i--) {\n const m = list[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 })\n\n const partial = createMemo<Partial>(() => {\n const part = activeStructuredPart()\n if (!part) return {} as Partial\n const v = part.partial ?? part.data\n return (v ?? {}) as Partial\n })\n\n const final = createMemo<Final | null>(() => {\n const part = activeStructuredPart()\n if (!part || part.status !== 'complete') return null\n return part.data as Final\n })\n\n // partial / final are runtime-tracked unconditionally; the conditional\n // return type hides them when no `outputSchema` is supplied.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- primitive return shape diverges from generic UseChatReturn<TTools, TSchema>; TS can't structurally narrow the conditional partial/final fields\n return {\n messages,\n queue,\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 cancelQueued,\n runId,\n interrupts,\n pendingInterrupts,\n interruptErrors,\n resuming,\n resolveInterrupts,\n cancelInterrupts,\n retryInterrupts,\n resumeInterruptsUnsafe,\n resumeInterrupts,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema, TInterrupts>\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { Accessor } from "solid-js";
2
2
  import { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, GenerationPersistenceOptions, InferGenerationOutputFromReturn, VideoGenerateInput, VideoGenerateResult, VideoStatusInfo } from "@tanstack/ai-client";
3
- import { StreamChunk } from "@tanstack/ai";
4
3
  import { ByokClient } from "@tanstack/ai-client/byok";
4
+ import { StreamChunk } from "@tanstack/ai";
5
5
  import { ProviderId } from "@tanstack/ai/byok";
6
6
 
7
7
  //#region src/use-generate-video.d.ts
@@ -1,7 +1,7 @@
1
1
  import { Accessor } from "solid-js";
2
2
  import { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, GenerationPersistenceOptions, GenerationRestoredResult, InferGenerationOutputFromReturn } from "@tanstack/ai-client";
3
- import { StreamChunk } from "@tanstack/ai";
4
3
  import { ByokClient } from "@tanstack/ai-client/byok";
4
+ import { StreamChunk } from "@tanstack/ai";
5
5
  import { ProviderId } from "@tanstack/ai/byok";
6
6
 
7
7
  //#region src/use-generation.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-solid",
3
- "version": "0.21.1",
3
+ "version": "0.24.0",
4
4
  "description": "Solid hooks for TanStack AI streaming chat, structured outputs, and media generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -29,6 +29,10 @@
29
29
  "solid": "./src/ui.ts",
30
30
  "types": "./src/ui.ts",
31
31
  "import": "./src/ui.ts"
32
+ },
33
+ "./byok": {
34
+ "types": "./dist/byok.d.ts",
35
+ "import": "./dist/byok.js"
32
36
  }
33
37
  },
34
38
  "files": [
@@ -50,16 +54,12 @@
50
54
  "media-generation"
51
55
  ],
52
56
  "dependencies": {
53
- "rehype-highlight": "^7.0.2",
54
- "rehype-raw": "^7.0.0",
55
- "rehype-sanitize": "^6.0.0",
56
- "remark-gfm": "^4.0.1",
57
- "solid-markdown": "^2.1.0",
58
- "@tanstack/ai-client": "^0.31.1"
57
+ "@tanstack/markdown": "^0.0.13",
58
+ "@tanstack/ai-client": "^0.32.1"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "solid-js": ">=1.9.10",
62
- "@tanstack/ai": "^0.54.0"
62
+ "@tanstack/ai": "^0.55.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@solidjs/testing-library": "^0.8.10",
@@ -73,7 +73,7 @@
73
73
  "vite": "^8.2.1",
74
74
  "vite-plugin-solid": "^2.11.14",
75
75
  "vitest": "^4.1.10",
76
- "@tanstack/ai": "0.54.0"
76
+ "@tanstack/ai": "0.55.0"
77
77
  },
78
78
  "scripts": {
79
79
  "clean": "premove ./build ./dist",
package/src/byok.ts ADDED
@@ -0,0 +1,25 @@
1
+ export {
2
+ ByokClient,
3
+ EMPTY_BYOK_SNAPSHOT,
4
+ decryptKeyring,
5
+ defaultByokStorage,
6
+ defineByok,
7
+ defineByokProvider,
8
+ deriveAesKey,
9
+ encryptKeyring,
10
+ isPasskeyStorageSupported,
11
+ memoryStorage,
12
+ passkeyStorage,
13
+ } from '@tanstack/ai-client/byok'
14
+ export type {
15
+ ByokPrompt,
16
+ ByokProvider,
17
+ ByokProviderInit,
18
+ ByokSnapshot,
19
+ DefineByokOptions,
20
+ KeyPreview,
21
+ KeyStatus,
22
+ Keyring,
23
+ KeyringStorage,
24
+ PasskeyStorageOptions,
25
+ } from '@tanstack/ai-client/byok'
@@ -1,7 +1,10 @@
1
- import { SolidMarkdown } from 'solid-markdown'
2
- import { resolveMarkdownPlugins } from './markdown-plugins'
3
- import type { SolidMarkdownComponents } from 'solid-markdown'
4
- import type { PluggableList } from './markdown-plugins'
1
+ import { renderHtml } from '@tanstack/markdown/html'
2
+ import { streamingMarkdownExtension } from '@tanstack/markdown/extensions/streaming'
3
+ import type { CodeHighlighter, MarkdownExtension } from '@tanstack/markdown'
4
+
5
+ const DEFAULT_EXTENSIONS: Array<MarkdownExtension> = [
6
+ streamingMarkdownExtension(),
7
+ ]
5
8
 
6
9
  export interface TextPartProps {
7
10
  /** The text content to render */
@@ -14,30 +17,30 @@ export interface TextPartProps {
14
17
  userClass?: string
15
18
  /** Additional class for assistant messages (also used for system messages) */
16
19
  assistantClass?: string
17
- /** Additional remark plugins, appended after the defaults. */
18
- remarkPlugins?: PluggableList
19
20
  /**
20
- * Additional rehype plugins. Inserted before the trailing
21
- * `rehypeSanitize` so sanitization always runs last.
21
+ * Additional TanStack Markdown extensions, appended after the built-in
22
+ * streaming extension.
22
23
  */
23
- rehypePlugins?: PluggableList
24
- /** solid-markdown `components` overrides. */
25
- components?: SolidMarkdownComponents
24
+ extensions?: Array<MarkdownExtension>
26
25
  /**
27
- * Drop the built-in plugin defaults entirely. Disables the XSS
28
- * sanitizer; the caller becomes responsible for sanitization.
26
+ * Synchronous code highlighter. Its output is inserted as trusted HTML,
27
+ * so use only a highlighter that escapes source text (for example
28
+ * `createTanStackMarkdownHighlighter` from `@tanstack/highlight/markdown`).
29
29
  */
30
- disableDefaultPlugins?: boolean
30
+ highlighter?: CodeHighlighter
31
31
  }
32
32
 
33
33
  /**
34
- * TextPart component - renders markdown text with syntax highlighting.
34
+ * TextPart component - renders markdown text with TanStack Markdown.
35
+ *
36
+ * Raw HTML is escaped and executable URLs are removed, so AI output can be
37
+ * rendered without a separate sanitizer.
35
38
  *
36
- * @example Add a markdown plugin (e.g. CJK bold/emphasis support)
39
+ * @example Syntax highlighting
37
40
  * ```tsx
38
- * import remarkCjkFriendly from 'remark-cjk-friendly'
41
+ * import { highlightMarkdownCode } from './markdown-highlighter'
39
42
  *
40
- * <TextPart content={content} remarkPlugins={[remarkCjkFriendly]} />
43
+ * <TextPart content={content} highlighter={highlightMarkdownCode} />
41
44
  * ```
42
45
  */
43
46
  export function TextPart(props: TextPartProps) {
@@ -50,22 +53,16 @@ export function TextPart(props: TextPartProps) {
50
53
  const combinedClass = () =>
51
54
  [props.class ?? '', roleClass()].filter(Boolean).join(' ')
52
55
 
53
- const resolved = () =>
54
- resolveMarkdownPlugins({
55
- remarkPlugins: props.remarkPlugins,
56
- rehypePlugins: props.rehypePlugins,
57
- disableDefaultPlugins: props.disableDefaultPlugins,
56
+ // TanStack Markdown escapes this HTML string.
57
+ const html = () =>
58
+ renderHtml(props.content, {
59
+ extensions: props.extensions
60
+ ? [...DEFAULT_EXTENSIONS, ...props.extensions]
61
+ : DEFAULT_EXTENSIONS,
62
+ frontmatter: false,
63
+ headingIds: false,
64
+ highlighter: props.highlighter,
58
65
  })
59
66
 
60
- return (
61
- <div class={combinedClass() || undefined}>
62
- <SolidMarkdown
63
- remarkPlugins={resolved().remarkPlugins}
64
- rehypePlugins={resolved().rehypePlugins}
65
- components={props.components}
66
- >
67
- {props.content}
68
- </SolidMarkdown>
69
- </div>
70
- )
67
+ return <div class={combinedClass() || undefined} innerHTML={html()} />
71
68
  }
package/src/index.ts CHANGED
@@ -65,6 +65,43 @@ export type {
65
65
 
66
66
  // Re-export from ai-client for convenience
67
67
  export {
68
+ AudioRecorder,
69
+ ChatClient,
70
+ InterruptManager,
71
+ RealtimeClient,
72
+ GenerationClient,
73
+ VideoGenerationClient,
74
+ GENERATION_EVENTS,
75
+ reconstructImageResult,
76
+ reconstructAudioResult,
77
+ reconstructSpeechResult,
78
+ reconstructTranscriptionResult,
79
+ reconstructSummarizeResult,
80
+ UnsupportedResponseStreamError,
81
+ createAIDevtoolsGenerationPreview,
82
+ StreamTruncatedError,
83
+ DurableStreamIncompleteError,
84
+ StreamReconnectLimitError,
85
+ uiMessageToModelMessages,
86
+ modelMessageToUIMessage,
87
+ modelMessagesToUIMessages,
88
+ convertMessagesToModelMessages,
89
+ normalizeToUIMessage,
90
+ generateMessageId,
91
+ StreamProcessor,
92
+ ImmediateStrategy,
93
+ PunctuationStrategy,
94
+ BatchStrategy,
95
+ WordBoundaryStrategy,
96
+ CompositeStrategy,
97
+ parsePartialJSON,
98
+ PartialJSONParser,
99
+ defaultJSONParser,
100
+ registerWebMCPTools,
101
+ clientTools,
102
+ createMcpAppBridge,
103
+ type McpAppBridge,
104
+ type CreateMcpAppBridgeOptions,
68
105
  fetchServerSentEvents,
69
106
  localStoragePersistence,
70
107
  sessionStoragePersistence,
package/src/types.ts CHANGED
@@ -251,6 +251,16 @@ interface BaseUseChatReturn<
251
251
  */
252
252
  isLoading: Accessor<boolean>
253
253
 
254
+ /**
255
+ * True when the last hydrate or older-page response said more messages exist.
256
+ */
257
+ hasOlderMessages: Accessor<boolean>
258
+
259
+ /**
260
+ * Fetch the next older window and put it in front of the painted messages.
261
+ */
262
+ loadOlderMessages: () => Promise<void>
263
+
254
264
  /**
255
265
  * Current error, if any
256
266
  */
package/src/use-chat.ts CHANGED
@@ -65,6 +65,7 @@ export function useChat<
65
65
  options.initialMessages || [],
66
66
  )
67
67
  const [isLoading, setIsLoading] = createSignal(false)
68
+ const [hasOlderMessages, setHasOlderMessages] = createSignal(false)
68
69
  const [error, setError] = createSignal<Error | undefined>(undefined)
69
70
  const [status, setStatus] = createSignal<ChatClientState>('ready')
70
71
  const [isSubscribed, setIsSubscribed] = createSignal(false)
@@ -108,22 +109,30 @@ export function useChat<
108
109
  const transport = options.connection
109
110
  ? { connection: options.connection }
110
111
  : { fetcher: options.fetcher }
111
- return new ChatClient<TTools, TContext, TInterrupts>({
112
+ const instance = new ChatClient<TTools, TContext, TInterrupts>({
112
113
  devtoolsBridgeFactory: createChatDevtoolsBridge,
113
114
  ...transport,
114
115
  ...(options.initialMessages !== undefined && {
115
116
  initialMessages: options.initialMessages,
116
117
  }),
117
- ...(typeof options.threadId === 'string' && options.persistence
118
+ ...(typeof options.threadId === 'string' && options.persistence === true
118
119
  ? {
119
- persistence: options.persistence,
120
+ persistence: true,
120
121
  threadId: options.threadId,
122
+ ...(options.history !== undefined && {
123
+ history: options.history,
124
+ }),
121
125
  }
122
- : {
123
- ...(options.threadId !== undefined && {
126
+ : typeof options.threadId === 'string' && options.persistence
127
+ ? {
128
+ persistence: options.persistence,
124
129
  threadId: options.threadId,
130
+ }
131
+ : {
132
+ ...(options.threadId !== undefined && {
133
+ threadId: options.threadId,
134
+ }),
125
135
  }),
126
- }),
127
136
  ...(options.initialResumeSnapshot !== undefined && {
128
137
  initialResumeSnapshot: options.initialResumeSnapshot,
129
138
  }),
@@ -161,6 +170,7 @@ export function useChat<
161
170
  }),
162
171
  onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
163
172
  setMessages(newMessages)
173
+ setHasOlderMessages(instance.getHasOlderMessages())
164
174
  },
165
175
  onLoadingChange: (newIsLoading: boolean) => {
166
176
  setIsLoading(newIsLoading)
@@ -202,9 +212,11 @@ export function useChat<
202
212
  })
203
213
  // Only recreate when clientId changes
204
214
  // Connection and other options are captured at creation time
215
+ return instance
205
216
  }, [clientId])
206
217
 
207
218
  setMessages(client().getMessages())
219
+ setHasOlderMessages(client().getHasOlderMessages())
208
220
  syncResumeState()
209
221
 
210
222
  // Sync body / forwardedProps changes to the client.
@@ -295,6 +307,11 @@ export function useChat<
295
307
  }
296
308
  }
297
309
 
310
+ const loadOlderMessages = async () => {
311
+ await client().loadOlderMessages()
312
+ setHasOlderMessages(client().getHasOlderMessages())
313
+ }
314
+
298
315
  const stop = () => {
299
316
  client().stop()
300
317
  }
@@ -418,6 +435,8 @@ export function useChat<
418
435
  reload,
419
436
  stop,
420
437
  isLoading,
438
+ hasOlderMessages,
439
+ loadOlderMessages,
421
440
  error,
422
441
  status,
423
442
  isSubscribed,
@@ -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 { SolidMarkdownOptions } from 'solid-markdown'
6
-
7
- export type PluggableList = SolidMarkdownOptions['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
- }