@tanstack/ai-solid 0.7.2 → 0.8.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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChatRequestBody, UIMessage, UseChatOptions, UseChatReturn } from "./types.js";
1
+ import { ChatRequestBody, DeepPartial, UIMessage, UseChatOptions, UseChatReturn } from "./types.js";
2
2
  import { useChat } from "./use-chat.js";
3
3
  import { UseGenerationOptions, UseGenerationReturn, useGeneration } from "./use-generation.js";
4
4
  import { UseGenerateImageOptions, UseGenerateImageReturn, useGenerateImage } from "./use-generate-image.js";
@@ -8,4 +8,4 @@ import { UseTranscriptionOptions, UseTranscriptionReturn, useTranscription } fro
8
8
  import { UseSummarizeOptions, UseSummarizeReturn, useSummarize } from "./use-summarize.js";
9
9
  import { UseGenerateVideoOptions, UseGenerateVideoReturn, useGenerateVideo } from "./use-generate-video.js";
10
10
  import { AudioGenerateInput, ConnectionAdapter, FetchConnectionOptions, GenerationClientState, ImageGenerateInput, InferChatMessages, SpeechGenerateInput, SummarizeGenerateInput, TranscriptionGenerateInput, VideoGenerateInput, VideoGenerateResult, VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream } from "@tanstack/ai-client";
11
- export { type AudioGenerateInput, type ChatRequestBody, type ConnectionAdapter, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type InferChatMessages, type SpeechGenerateInput, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, 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 VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription };
11
+ export { type AudioGenerateInput, type ChatRequestBody, type ConnectionAdapter, type DeepPartial, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type InferChatMessages, type SpeechGenerateInput, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, 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 VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription };
package/dist/types.d.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import { Accessor } from "solid-js";
2
2
  import { ChatClientOptions, ChatClientState, ChatRequestBody, ConnectionStatus, MultimodalContent, UIMessage } from "@tanstack/ai-client";
3
- import { AnyClientTool, ModelMessage } from "@tanstack/ai";
3
+ import { AnyClientTool, InferSchemaType, ModelMessage, SchemaInput } from "@tanstack/ai";
4
4
 
5
5
  //#region src/types.d.ts
6
6
 
7
+ /**
8
+ * Recursive partial — every property and every nested array element is optional.
9
+ * Used to type the in-flight `partial` accessor while a structured-output
10
+ * stream is still arriving.
11
+ */
12
+ type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
7
13
  /**
8
14
  * Options for the useChat hook.
9
15
  *
@@ -17,13 +23,38 @@ import { AnyClientTool, ModelMessage } from "@tanstack/ai";
17
23
  * All other callbacks (onResponse, onChunk, onFinish, onError) are
18
24
  * passed through to the underlying ChatClient and can be used for side effects.
19
25
  *
26
+ * When `outputSchema` is supplied, the hook returns typed `partial` and `final`
27
+ * accessors. The schema is used purely for type inference; server-side
28
+ * validation still runs against the schema passed to `chat({ outputSchema })`
29
+ * on the server route.
30
+ *
20
31
  * Note: Connection and body changes will recreate the ChatClient instance.
21
32
  * To update these options, remount the component or use a key prop.
22
33
  */
23
- type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
34
+ type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
24
35
  live?: boolean;
36
+ /**
37
+ * Standard-schema-compatible schema (Zod, Valibot, ArkType, or plain JSON
38
+ * Schema). Used to infer the shape of `partial` and `final`.
39
+ */
40
+ outputSchema?: TSchema;
25
41
  };
26
- interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
42
+ /**
43
+ * Discriminated return shape: when `outputSchema` is supplied, the hook adds
44
+ * typed `partial` / `final` accessors; otherwise the return is unchanged.
45
+ */
46
+ type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools> & (TSchema extends SchemaInput ? {
47
+ /**
48
+ * Live progressively-parsed structured output. Resets on every new run.
49
+ */
50
+ partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>;
51
+ /**
52
+ * Final, schema-validated structured output. `null` until the terminal
53
+ * `structured-output.complete` event arrives. Resets on every new run.
54
+ */
55
+ final: Accessor<InferSchemaType<TSchema> | null>;
56
+ } : Record<never, never>);
57
+ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
27
58
  /**
28
59
  * Current messages in the conversation
29
60
  */
@@ -99,5 +130,5 @@ interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
99
130
  sessionGenerating: Accessor<boolean>;
100
131
  }
101
132
  //#endregion
102
- export { type ChatRequestBody, type UIMessage, UseChatOptions, UseChatReturn };
133
+ export { type ChatRequestBody, DeepPartial, type UIMessage, UseChatOptions, UseChatReturn };
103
134
  //# sourceMappingURL=types.d.ts.map
@@ -1,8 +1,8 @@
1
1
  import { UseChatOptions, UseChatReturn } from "./types.js";
2
- import { AnyClientTool } from "@tanstack/ai";
2
+ import { AnyClientTool, SchemaInput } from "@tanstack/ai";
3
3
 
4
4
  //#region src/use-chat.d.ts
5
- declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(options?: UseChatOptions<TTools>): UseChatReturn<TTools>;
5
+ declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined>(options?: UseChatOptions<TTools, TSchema>): UseChatReturn<TTools, TSchema>;
6
6
  //#endregion
7
7
  export { useChat };
8
8
  //# sourceMappingURL=use-chat.d.ts.map
package/dist/use-chat.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createEffect, createMemo, createSignal, createUniqueId, onCleanup } from "solid-js";
2
2
  import { ChatClient } from "@tanstack/ai-client";
3
+ import { parsePartialJSON } from "@tanstack/ai";
3
4
 
4
5
  //#region src/use-chat.ts
5
6
  function useChat(options = {}) {
@@ -12,6 +13,9 @@ function useChat(options = {}) {
12
13
  const [isSubscribed, setIsSubscribed] = createSignal(false);
13
14
  const [connectionStatus, setConnectionStatus] = createSignal("disconnected");
14
15
  const [sessionGenerating, setSessionGenerating] = createSignal(false);
16
+ const [partial, setPartial] = createSignal({});
17
+ const [final, setFinal] = createSignal(null);
18
+ let rawJson = "";
15
19
  const client = createMemo(() => {
16
20
  return new ChatClient({
17
21
  connection: options.connection,
@@ -19,7 +23,23 @@ function useChat(options = {}) {
19
23
  initialMessages: options.initialMessages,
20
24
  body: options.body,
21
25
  onResponse: (response) => options.onResponse?.(response),
22
- onChunk: (chunk) => options.onChunk?.(chunk),
26
+ onChunk: (chunk) => {
27
+ if (options.outputSchema !== void 0) {
28
+ if (chunk.type === "RUN_STARTED") {
29
+ rawJson = "";
30
+ setPartial({});
31
+ setFinal(null);
32
+ } else if (chunk.type === "TEXT_MESSAGE_CONTENT" && chunk.delta) {
33
+ rawJson += chunk.delta;
34
+ const progressive = parsePartialJSON(rawJson);
35
+ if (progressive && typeof progressive === "object") setPartial(() => progressive);
36
+ } else if (chunk.type === "CUSTOM" && chunk.name === "structured-output.complete") {
37
+ const value = chunk.value;
38
+ setFinal(() => value.object);
39
+ }
40
+ }
41
+ options.onChunk?.(chunk);
42
+ },
23
43
  onFinish: (message) => {
24
44
  options.onFinish?.(message);
25
45
  },
@@ -110,7 +130,9 @@ function useChat(options = {}) {
110
130
  setMessages: setMessagesManually,
111
131
  clear,
112
132
  addToolResult,
113
- addToolApprovalResponse
133
+ addToolApprovalResponse,
134
+ partial,
135
+ final
114
136
  };
115
137
  }
116
138
 
@@ -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} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\nimport type {\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools> = {} as UseChatOptions<TTools>,\n): UseChatReturn<TTools> {\n const hookId = createUniqueId()\n const clientId = options.id || 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\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 return new ChatClient({\n connection: options.connection,\n id: clientId,\n initialMessages: options.initialMessages,\n body: options.body,\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk) => options.onChunk?.(chunk),\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\n streamProcessor: options.streamProcessor,\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\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 })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n }, [clientId])\n\n // Sync body changes to the client\n // This allows dynamic body values (like model selection) to be updated without recreating the client\n createEffect(() => {\n const currentBody = options.body\n client().updateOptions({ body: currentBody })\n })\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n createEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages().length === 0) {\n client().setMessagesManually(options.initialMessages)\n }\n }\n }) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // 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 // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\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 (content: string | MultimodalContent) => {\n await client().sendMessage(content)\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n await client().append(message)\n }\n\n const reload = async () => {\n await client().reload()\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\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 }\n\n return {\n messages,\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 }\n}\n"],"mappings":";;;;AAkBA,SAAgB,QACd,UAAkC,EAAE,EACb;CACvB,MAAM,SAAS,gBAAgB;CAC/B,MAAM,WAAW,QAAQ,MAAM;CAE/B,MAAM,CAAC,UAAU,eAAe,aAC9B,QAAQ,mBAAmB,EAAE,CAC9B;CACD,MAAM,CAAC,WAAW,gBAAgB,aAAa,MAAM;CACrD,MAAM,CAAC,OAAO,YAAY,aAAgC,OAAU;CACpE,MAAM,CAAC,QAAQ,aAAa,aAA8B,QAAQ;CAClE,MAAM,CAAC,cAAc,mBAAmB,aAAa,MAAM;CAC3D,MAAM,CAAC,kBAAkB,uBACvB,aAA+B,eAAe;CAChD,MAAM,CAAC,mBAAmB,wBAAwB,aAAa,MAAM;CAQrE,MAAM,SAAS,iBAAiB;AAC9B,SAAO,IAAI,WAAW;GACpB,YAAY,QAAQ;GACpB,IAAI;GACJ,iBAAiB,QAAQ;GACzB,MAAM,QAAQ;GACd,aAAa,aAAa,QAAQ,aAAa,SAAS;GACxD,UAAU,UAAU,QAAQ,UAAU,MAAM;GAC5C,WAAW,YAAY;AACrB,YAAQ,WAAW,QAAQ;;GAE7B,UAAU,QAAQ;AAChB,YAAQ,UAAU,IAAI;;GAExB,OAAO,QAAQ;GACf,gBAAgB,WAAW,MAAM,YAC/B,QAAQ,gBAAgB,WAAW,MAAM,QAAQ;GACnD,iBAAiB,QAAQ;GACzB,mBAAmB,gBAA0C;AAC3D,gBAAY,YAAY;;GAE1B,kBAAkB,iBAA0B;AAC1C,iBAAa,aAAa;;GAE5B,iBAAiB,cAA+B;AAC9C,cAAU,UAAU;;GAEtB,gBAAgB,aAAgC;AAC9C,aAAS,SAAS;;GAEpB,uBAAuB,qBAA8B;AACnD,oBAAgB,iBAAiB;;GAEnC,2BAA2B,eAAiC;AAC1D,wBAAoB,WAAW;;GAEjC,4BAA4B,iBAA0B;AACpD,yBAAqB,aAAa;;GAErC,CAAC;IAGD,CAAC,SAAS,CAAC;AAId,oBAAmB;EACjB,MAAM,cAAc,QAAQ;AAC5B,UAAQ,CAAC,cAAc,EAAE,MAAM,aAAa,CAAC;GAC7C;AAKF,oBAAmB;AACjB,MAAI,QAAQ,mBAAmB,QAAQ,gBAAgB,SAAS,GAE9D;OAAI,UAAU,CAAC,WAAW,EACxB,SAAQ,CAAC,oBAAoB,QAAQ,gBAAgB;;GAGzD;AAGF,KAAI,QAAQ,KACV,SAAQ,CAAC,WAAW;KAEpB,SAAQ,CAAC,aAAa;AAGxB,oBAAmB;AACjB,MAAI,QAAQ,KACV,SAAQ,CAAC,WAAW;MAEpB,SAAQ,CAAC,aAAa;GAExB;AAGF,iBAAgB;AACd,MAAI,QAAQ,KACV,SAAQ,CAAC,aAAa;MAEtB,SAAQ,CAAC,MAAM;GAEjB;CAKF,MAAM,cAAc,OAAO,YAAwC;AACjE,QAAM,QAAQ,CAAC,YAAY,QAAQ;;CAGrC,MAAM,SAAS,OAAO,YAA8C;AAClE,QAAM,QAAQ,CAAC,OAAO,QAAQ;;CAGhC,MAAM,SAAS,YAAY;AACzB,QAAM,QAAQ,CAAC,QAAQ;;CAGzB,MAAM,aAAa;AACjB,UAAQ,CAAC,MAAM;;CAGjB,MAAM,cAAc;AAClB,UAAQ,CAAC,OAAO;;CAGlB,MAAM,uBAAuB,gBAA0C;AACrE,UAAQ,CAAC,oBAAoB,YAAY;;CAG3C,MAAM,gBAAgB,OAAO,WAMvB;AACJ,QAAM,QAAQ,CAAC,cAAc,OAAO;;CAGtC,MAAM,0BAA0B,OAAO,aAGjC;AACJ,QAAM,QAAQ,CAAC,wBAAwB,SAAS;;AAGlD,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACD"}
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} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport { parsePartialJSON } from '@tanstack/ai'\nimport type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n>(\n options: UseChatOptions<TTools, TSchema> = {} as UseChatOptions<\n TTools,\n TSchema\n >,\n): UseChatReturn<TTools, TSchema> {\n const hookId = createUniqueId()\n const clientId = options.id || 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\n // Structured-output state. Runtime always tracks them — the conditional\n // return type hides them from callers that didn't supply `outputSchema`.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n const [partial, setPartial] = createSignal<Partial>({} as Partial)\n const [final, setFinal] = createSignal<Final | null>(null)\n // Raw JSON accumulator — kept outside the signal to avoid extra re-runs.\n let rawJson = ''\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 return new ChatClient({\n connection: options.connection,\n id: clientId,\n initialMessages: options.initialMessages,\n body: options.body,\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n if (options.outputSchema !== undefined) {\n if (chunk.type === 'RUN_STARTED') {\n rawJson = ''\n setPartial({} as Partial)\n setFinal(null)\n } else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {\n rawJson += chunk.delta\n const progressive = parsePartialJSON(rawJson)\n if (progressive && typeof progressive === 'object') {\n setPartial(() => progressive as Partial)\n }\n } else if (\n chunk.type === 'CUSTOM' &&\n chunk.name === 'structured-output.complete'\n ) {\n const value = chunk.value as { object: unknown }\n setFinal(() => value.object as Final)\n }\n }\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 onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\n streamProcessor: options.streamProcessor,\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\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 })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n }, [clientId])\n\n // Sync body changes to the client\n // This allows dynamic body values (like model selection) to be updated without recreating the client\n createEffect(() => {\n const currentBody = options.body\n client().updateOptions({ body: currentBody })\n })\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n createEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages().length === 0) {\n client().setMessagesManually(options.initialMessages)\n }\n }\n }) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // 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 // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\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 (content: string | MultimodalContent) => {\n await client().sendMessage(content)\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n await client().append(message)\n }\n\n const reload = async () => {\n await client().reload()\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\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 }\n\n // partial / final are runtime-tracked unconditionally; the conditional\n // return type hides them when no `outputSchema` is supplied.\n return {\n messages,\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 partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-solid",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Solid hooks for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -31,14 +31,15 @@
31
31
  "streaming"
32
32
  ],
33
33
  "dependencies": {
34
- "@tanstack/ai-client": "0.9.1"
34
+ "@tanstack/ai-client": "0.9.2"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "solid-js": ">=1.9.10",
38
- "@tanstack/ai": "^0.16.0"
38
+ "@tanstack/ai": "^0.17.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@solidjs/testing-library": "^0.8.10",
42
+ "@standard-schema/spec": "^1.1.0",
42
43
  "@types/node": "^24.10.1",
43
44
  "@vitest/coverage-v8": "4.0.14",
44
45
  "jsdom": "^27.2.0",
@@ -46,7 +47,7 @@
46
47
  "tsdown": "^0.17.0-beta.6",
47
48
  "typescript": "5.9.3",
48
49
  "vitest": "^4.0.14",
49
- "@tanstack/ai": "0.16.0"
50
+ "@tanstack/ai": "0.17.0"
50
51
  },
51
52
  "scripts": {
52
53
  "clean": "premove ./build ./dist",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { useChat } from './use-chat'
2
2
  export type {
3
+ DeepPartial,
3
4
  UseChatOptions,
4
5
  UseChatReturn,
5
6
  UIMessage,
package/src/types.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
1
+ import type {
2
+ AnyClientTool,
3
+ InferSchemaType,
4
+ ModelMessage,
5
+ SchemaInput,
6
+ } from '@tanstack/ai'
2
7
  import type {
3
8
  ChatClientOptions,
4
9
  ChatClientState,
@@ -12,6 +17,18 @@ import type { Accessor } from 'solid-js'
12
17
  // Re-export types from ai-client
13
18
  export type { ChatRequestBody, MultimodalContent, UIMessage }
14
19
 
20
+ /**
21
+ * Recursive partial — every property and every nested array element is optional.
22
+ * Used to type the in-flight `partial` accessor while a structured-output
23
+ * stream is still arriving.
24
+ */
25
+ export type DeepPartial<T> =
26
+ T extends ReadonlyArray<infer U>
27
+ ? Array<DeepPartial<U>>
28
+ : T extends object
29
+ ? { [K in keyof T]?: DeepPartial<T[K]> }
30
+ : T
31
+
15
32
  /**
16
33
  * Options for the useChat hook.
17
34
  *
@@ -25,26 +42,58 @@ export type { ChatRequestBody, MultimodalContent, UIMessage }
25
42
  * All other callbacks (onResponse, onChunk, onFinish, onError) are
26
43
  * passed through to the underlying ChatClient and can be used for side effects.
27
44
  *
45
+ * When `outputSchema` is supplied, the hook returns typed `partial` and `final`
46
+ * accessors. The schema is used purely for type inference; server-side
47
+ * validation still runs against the schema passed to `chat({ outputSchema })`
48
+ * on the server route.
49
+ *
28
50
  * Note: Connection and body changes will recreate the ChatClient instance.
29
51
  * To update these options, remount the component or use a key prop.
30
52
  */
31
- export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
32
- Omit<
33
- ChatClientOptions<TTools>,
34
- | 'onMessagesChange'
35
- | 'onLoadingChange'
36
- | 'onErrorChange'
37
- | 'onStatusChange'
38
- | 'onSubscriptionChange'
39
- | 'onConnectionStatusChange'
40
- | 'onSessionGeneratingChange'
41
- > & {
42
- live?: boolean
43
- }
44
-
45
- export interface UseChatReturn<
53
+ export type UseChatOptions<
54
+ TTools extends ReadonlyArray<AnyClientTool> = any,
55
+ TSchema extends SchemaInput | undefined = undefined,
56
+ > = Omit<
57
+ ChatClientOptions<TTools>,
58
+ | 'onMessagesChange'
59
+ | 'onLoadingChange'
60
+ | 'onErrorChange'
61
+ | 'onStatusChange'
62
+ | 'onSubscriptionChange'
63
+ | 'onConnectionStatusChange'
64
+ | 'onSessionGeneratingChange'
65
+ > & {
66
+ live?: boolean
67
+ /**
68
+ * Standard-schema-compatible schema (Zod, Valibot, ArkType, or plain JSON
69
+ * Schema). Used to infer the shape of `partial` and `final`.
70
+ */
71
+ outputSchema?: TSchema
72
+ }
73
+
74
+ /**
75
+ * Discriminated return shape: when `outputSchema` is supplied, the hook adds
76
+ * typed `partial` / `final` accessors; otherwise the return is unchanged.
77
+ */
78
+ export type UseChatReturn<
46
79
  TTools extends ReadonlyArray<AnyClientTool> = any,
47
- > {
80
+ TSchema extends SchemaInput | undefined = undefined,
81
+ > = BaseUseChatReturn<TTools> &
82
+ (TSchema extends SchemaInput
83
+ ? {
84
+ /**
85
+ * Live progressively-parsed structured output. Resets on every new run.
86
+ */
87
+ partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>
88
+ /**
89
+ * Final, schema-validated structured output. `null` until the terminal
90
+ * `structured-output.complete` event arrives. Resets on every new run.
91
+ */
92
+ final: Accessor<InferSchemaType<TSchema> | null>
93
+ }
94
+ : Record<never, never>)
95
+
96
+ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
48
97
  /**
49
98
  * Current messages in the conversation
50
99
  */
package/src/use-chat.ts CHANGED
@@ -7,18 +7,32 @@ import {
7
7
  } from 'solid-js'
8
8
 
9
9
  import { ChatClient } from '@tanstack/ai-client'
10
+ import { parsePartialJSON } from '@tanstack/ai'
10
11
  import type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'
11
- import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
12
12
  import type {
13
+ AnyClientTool,
14
+ InferSchemaType,
15
+ ModelMessage,
16
+ SchemaInput,
17
+ StreamChunk,
18
+ } from '@tanstack/ai'
19
+ import type {
20
+ DeepPartial,
13
21
  MultimodalContent,
14
22
  UIMessage,
15
23
  UseChatOptions,
16
24
  UseChatReturn,
17
25
  } from './types'
18
26
 
19
- export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
20
- options: UseChatOptions<TTools> = {} as UseChatOptions<TTools>,
21
- ): UseChatReturn<TTools> {
27
+ export function useChat<
28
+ TTools extends ReadonlyArray<AnyClientTool> = any,
29
+ TSchema extends SchemaInput | undefined = undefined,
30
+ >(
31
+ options: UseChatOptions<TTools, TSchema> = {} as UseChatOptions<
32
+ TTools,
33
+ TSchema
34
+ >,
35
+ ): UseChatReturn<TTools, TSchema> {
22
36
  const hookId = createUniqueId()
23
37
  const clientId = options.id || hookId
24
38
 
@@ -33,6 +47,15 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
33
47
  createSignal<ConnectionStatus>('disconnected')
34
48
  const [sessionGenerating, setSessionGenerating] = createSignal(false)
35
49
 
50
+ // Structured-output state. Runtime always tracks them — the conditional
51
+ // return type hides them from callers that didn't supply `outputSchema`.
52
+ type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
53
+ type Final = InferSchemaType<NonNullable<TSchema>>
54
+ const [partial, setPartial] = createSignal<Partial>({} as Partial)
55
+ const [final, setFinal] = createSignal<Final | null>(null)
56
+ // Raw JSON accumulator — kept outside the signal to avoid extra re-runs.
57
+ let rawJson = ''
58
+
36
59
  // Create ChatClient instance with callbacks to sync state.
37
60
  // Every user-provided callback is wrapped so the LATEST `options.xxx` value
38
61
  // is read at call time. Direct assignment would freeze the callback to the
@@ -46,7 +69,28 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
46
69
  initialMessages: options.initialMessages,
47
70
  body: options.body,
48
71
  onResponse: (response) => options.onResponse?.(response),
49
- onChunk: (chunk) => options.onChunk?.(chunk),
72
+ onChunk: (chunk: StreamChunk) => {
73
+ if (options.outputSchema !== undefined) {
74
+ if (chunk.type === 'RUN_STARTED') {
75
+ rawJson = ''
76
+ setPartial({} as Partial)
77
+ setFinal(null)
78
+ } else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
79
+ rawJson += chunk.delta
80
+ const progressive = parsePartialJSON(rawJson)
81
+ if (progressive && typeof progressive === 'object') {
82
+ setPartial(() => progressive as Partial)
83
+ }
84
+ } else if (
85
+ chunk.type === 'CUSTOM' &&
86
+ chunk.name === 'structured-output.complete'
87
+ ) {
88
+ const value = chunk.value as { object: unknown }
89
+ setFinal(() => value.object as Final)
90
+ }
91
+ }
92
+ options.onChunk?.(chunk)
93
+ },
50
94
  onFinish: (message) => {
51
95
  options.onFinish?.(message)
52
96
  },
@@ -170,6 +214,8 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
170
214
  await client().addToolApprovalResponse(response)
171
215
  }
172
216
 
217
+ // partial / final are runtime-tracked unconditionally; the conditional
218
+ // return type hides them when no `outputSchema` is supplied.
173
219
  return {
174
220
  messages,
175
221
  sendMessage,
@@ -186,5 +232,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
186
232
  clear,
187
233
  addToolResult,
188
234
  addToolApprovalResponse,
189
- }
235
+ partial,
236
+ final,
237
+ } as unknown as UseChatReturn<TTools, TSchema>
190
238
  }