@tanstack/ai-solid 0.8.0 → 0.10.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 CHANGED
@@ -71,7 +71,10 @@ interface UseChatOptions {
71
71
  // Configuration
72
72
  initialMessages?: UIMessage[] // Starting messages
73
73
  id?: string // Unique chat ID
74
- body?: Record<string, any> // Extra data to send
74
+ threadId?: string // AG-UI thread ID (auto-generated if omitted)
75
+ forwardedProps?: Record<string, any> // Forwarded to AG-UI RunAgentInput.forwardedProps
76
+ /** @deprecated Use `forwardedProps` instead. */
77
+ body?: Record<string, any>
75
78
 
76
79
  // Callbacks
77
80
  onResponse?: (response?: Response) => void
@@ -256,7 +259,7 @@ const chat = useChat({
256
259
  'X-Custom-Header': 'value',
257
260
  },
258
261
  }),
259
- body: {
262
+ forwardedProps: {
260
263
  userId: '123',
261
264
  sessionId: 'abc',
262
265
  },
package/dist/types.d.ts CHANGED
@@ -43,22 +43,25 @@ type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema e
43
43
  * Discriminated return shape: when `outputSchema` is supplied, the hook adds
44
44
  * typed `partial` / `final` accessors; otherwise the return is unchanged.
45
45
  */
46
- type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools> & (TSchema extends SchemaInput ? {
46
+ type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools, TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown> & (TSchema extends SchemaInput ? {
47
47
  /**
48
- * Live progressively-parsed structured output. Resets on every new run.
48
+ * Live progressively-parsed structured output. Derived from the
49
+ * latest assistant message's structured-output part.
49
50
  */
50
51
  partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>;
51
52
  /**
52
- * Final, schema-validated structured output. `null` until the terminal
53
- * `structured-output.complete` event arrives. Resets on every new run.
53
+ * Final, schema-validated structured output. `null` until the latest
54
+ * assistant turn's structured-output part transitions to `complete`.
54
55
  */
55
56
  final: Accessor<InferSchemaType<TSchema> | null>;
56
57
  } : Record<never, never>);
57
- interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
58
+ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown> {
58
59
  /**
59
- * Current messages in the conversation
60
+ * Current messages in the conversation. When `outputSchema` is supplied,
61
+ * `messages()[i].parts.find(p => p.type === 'structured-output')` is typed
62
+ * by the schema — `data: T`, `partial: DeepPartial<T>`.
60
63
  */
61
- messages: Accessor<Array<UIMessage<TTools>>>;
64
+ messages: Accessor<Array<UIMessage<TTools, TData>>>;
62
65
  /**
63
66
  * Send a message and get a response.
64
67
  * Can be a simple string or multimodal content with images, audio, etc.
@@ -67,7 +70,7 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
67
70
  /**
68
71
  * Append a message to the conversation
69
72
  */
70
- append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>;
73
+ append: (message: ModelMessage | UIMessage<TTools, TData>) => Promise<void>;
71
74
  /**
72
75
  * Add the result of a client-side tool execution
73
76
  */
@@ -104,7 +107,7 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
104
107
  /**
105
108
  * Set messages manually
106
109
  */
107
- setMessages: (messages: Array<UIMessage<TTools>>) => void;
110
+ setMessages: (messages: Array<UIMessage<TTools, TData>>) => void;
108
111
  /**
109
112
  * Clear all messages
110
113
  */
package/dist/use-chat.js CHANGED
@@ -1,6 +1,5 @@
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";
4
3
 
5
4
  //#region src/use-chat.ts
6
5
  function useChat(options = {}) {
@@ -13,31 +12,15 @@ function useChat(options = {}) {
13
12
  const [isSubscribed, setIsSubscribed] = createSignal(false);
14
13
  const [connectionStatus, setConnectionStatus] = createSignal("disconnected");
15
14
  const [sessionGenerating, setSessionGenerating] = createSignal(false);
16
- const [partial, setPartial] = createSignal({});
17
- const [final, setFinal] = createSignal(null);
18
- let rawJson = "";
19
15
  const client = createMemo(() => {
20
16
  return new ChatClient({
21
17
  connection: options.connection,
22
18
  id: clientId,
23
19
  initialMessages: options.initialMessages,
24
20
  body: options.body,
21
+ forwardedProps: options.forwardedProps,
25
22
  onResponse: (response) => options.onResponse?.(response),
26
23
  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
24
  options.onChunk?.(chunk);
42
25
  },
43
26
  onFinish: (message) => {
@@ -73,8 +56,10 @@ function useChat(options = {}) {
73
56
  });
74
57
  }, [clientId]);
75
58
  createEffect(() => {
76
- const currentBody = options.body;
77
- client().updateOptions({ body: currentBody });
59
+ client().updateOptions({
60
+ body: options.body,
61
+ forwardedProps: options.forwardedProps
62
+ });
78
63
  });
79
64
  createEffect(() => {
80
65
  if (options.initialMessages && options.initialMessages.length > 0) {
@@ -115,6 +100,22 @@ function useChat(options = {}) {
115
100
  const addToolApprovalResponse = async (response) => {
116
101
  await client().addToolApprovalResponse(response);
117
102
  };
103
+ const activeStructuredPart = createMemo(() => {
104
+ const list = messages();
105
+ let lastUserIndex = -1;
106
+ for (let i = list.length - 1; i >= 0; i--) if (list[i].role === "user") {
107
+ lastUserIndex = i;
108
+ break;
109
+ }
110
+ if (lastUserIndex === -1) return null;
111
+ for (let i = list.length - 1; i > lastUserIndex; i--) {
112
+ const m = list[i];
113
+ if (m.role !== "assistant") continue;
114
+ const part = m.parts.find((p) => p.type === "structured-output");
115
+ if (part) return part;
116
+ }
117
+ return null;
118
+ });
118
119
  return {
119
120
  messages,
120
121
  sendMessage,
@@ -131,8 +132,16 @@ function useChat(options = {}) {
131
132
  clear,
132
133
  addToolResult,
133
134
  addToolApprovalResponse,
134
- partial,
135
- final
135
+ partial: createMemo(() => {
136
+ const part = activeStructuredPart();
137
+ if (!part) return {};
138
+ return part.partial ?? part.data ?? {};
139
+ }),
140
+ final: createMemo(() => {
141
+ const part = activeStructuredPart();
142
+ if (!part || part.status !== "complete") return null;
143
+ return part.data;
144
+ })
136
145
  };
137
146
  }
138
147
 
@@ -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 { 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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"}
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 {\n ChatClientState,\n ConnectionStatus,\n StructuredOutputPart,\n} 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 `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 return new ChatClient({\n connection: options.connection,\n id: clientId,\n initialMessages: options.initialMessages,\n body: options.body,\n forwardedProps: options.forwardedProps,\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 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 / 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 client().updateOptions({\n body: options.body,\n forwardedProps: options.forwardedProps,\n })\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 // 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 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":";;;;AA6BA,SAAgB,QAId,UAA2C,EAAE,EAIb;CAChC,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;CAerE,MAAM,SAAS,iBAAiB;AAC9B,SAAO,IAAI,WAAW;GACpB,YAAY,QAAQ;GACpB,IAAI;GACJ,iBAAiB,QAAQ;GACzB,MAAM,QAAQ;GACd,gBAAgB,QAAQ;GACxB,aAAa,aAAa,QAAQ,aAAa,SAAS;GACxD,UAAU,UAAuB;AAC/B,YAAQ,UAAU,MAAM;;GAE1B,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;AAKd,oBAAmB;AACjB,UAAQ,CAAC,cAAc;GACrB,MAAM,QAAQ;GACd,gBAAgB,QAAQ;GACzB,CAAC;GACF;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;;CAOlD,MAAM,uBAAuB,iBAA8C;EACzE,MAAM,OAAO,UAAU;EACvB,IAAI,gBAAgB;AACpB,OAAK,IAAI,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG,IACpC,KAAI,KAAK,GAAI,SAAS,QAAQ;AAC5B,mBAAgB;AAChB;;AAGJ,MAAI,kBAAkB,GAAI,QAAO;AACjC,OAAK,IAAI,IAAI,KAAK,SAAS,GAAG,IAAI,eAAe,KAAK;GACpD,MAAM,IAAI,KAAK;AACf,OAAI,EAAE,SAAS,YAAa;GAC5B,MAAM,OAAO,EAAE,MAAM,MAClB,MAAiC,EAAE,SAAS,oBAC9C;AACD,OAAI,KAAM,QAAO;;AAEnB,SAAO;GACP;AAiBF,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACA,SA/Bc,iBAA0B;GACxC,MAAM,OAAO,sBAAsB;AACnC,OAAI,CAAC,KAAM,QAAO,EAAE;AAEpB,UADU,KAAK,WAAW,KAAK,QAClB,EAAE;IACf;EA2BA,OAzBY,iBAA+B;GAC3C,MAAM,OAAO,sBAAsB;AACnC,OAAI,CAAC,QAAQ,KAAK,WAAW,WAAY,QAAO;AAChD,UAAO,KAAK;IACZ;EAsBD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-solid",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Solid hooks for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -31,11 +31,11 @@
31
31
  "streaming"
32
32
  ],
33
33
  "dependencies": {
34
- "@tanstack/ai-client": "0.9.2"
34
+ "@tanstack/ai-client": "0.11.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "solid-js": ">=1.9.10",
38
- "@tanstack/ai": "^0.17.0"
38
+ "@tanstack/ai": "^0.19.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@solidjs/testing-library": "^0.8.10",
@@ -47,7 +47,7 @@
47
47
  "tsdown": "^0.17.0-beta.6",
48
48
  "typescript": "5.9.3",
49
49
  "vitest": "^4.0.14",
50
- "@tanstack/ai": "0.17.0"
50
+ "@tanstack/ai": "0.19.0"
51
51
  },
52
52
  "scripts": {
53
53
  "clean": "premove ./build ./dist",
package/src/types.ts CHANGED
@@ -78,26 +78,35 @@ export type UseChatOptions<
78
78
  export type UseChatReturn<
79
79
  TTools extends ReadonlyArray<AnyClientTool> = any,
80
80
  TSchema extends SchemaInput | undefined = undefined,
81
- > = BaseUseChatReturn<TTools> &
81
+ > = BaseUseChatReturn<
82
+ TTools,
83
+ TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown
84
+ > &
82
85
  (TSchema extends SchemaInput
83
86
  ? {
84
87
  /**
85
- * Live progressively-parsed structured output. Resets on every new run.
88
+ * Live progressively-parsed structured output. Derived from the
89
+ * latest assistant message's structured-output part.
86
90
  */
87
91
  partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>
88
92
  /**
89
- * Final, schema-validated structured output. `null` until the terminal
90
- * `structured-output.complete` event arrives. Resets on every new run.
93
+ * Final, schema-validated structured output. `null` until the latest
94
+ * assistant turn's structured-output part transitions to `complete`.
91
95
  */
92
96
  final: Accessor<InferSchemaType<TSchema> | null>
93
97
  }
94
98
  : Record<never, never>)
95
99
 
96
- interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
100
+ interface BaseUseChatReturn<
101
+ TTools extends ReadonlyArray<AnyClientTool> = any,
102
+ TData = unknown,
103
+ > {
97
104
  /**
98
- * Current messages in the conversation
105
+ * Current messages in the conversation. When `outputSchema` is supplied,
106
+ * `messages()[i].parts.find(p => p.type === 'structured-output')` is typed
107
+ * by the schema — `data: T`, `partial: DeepPartial<T>`.
99
108
  */
100
- messages: Accessor<Array<UIMessage<TTools>>>
109
+ messages: Accessor<Array<UIMessage<TTools, TData>>>
101
110
 
102
111
  /**
103
112
  * Send a message and get a response.
@@ -108,7 +117,7 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
108
117
  /**
109
118
  * Append a message to the conversation
110
119
  */
111
- append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>
120
+ append: (message: ModelMessage | UIMessage<TTools, TData>) => Promise<void>
112
121
 
113
122
  /**
114
123
  * Add the result of a client-side tool execution
@@ -152,7 +161,7 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
152
161
  /**
153
162
  * Set messages manually
154
163
  */
155
- setMessages: (messages: Array<UIMessage<TTools>>) => void
164
+ setMessages: (messages: Array<UIMessage<TTools, TData>>) => void
156
165
 
157
166
  /**
158
167
  * Clear all messages
package/src/use-chat.ts CHANGED
@@ -7,8 +7,11 @@ import {
7
7
  } from 'solid-js'
8
8
 
9
9
  import { ChatClient } from '@tanstack/ai-client'
10
- import { parsePartialJSON } from '@tanstack/ai'
11
- import type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'
10
+ import type {
11
+ ChatClientState,
12
+ ConnectionStatus,
13
+ StructuredOutputPart,
14
+ } from '@tanstack/ai-client'
12
15
  import type {
13
16
  AnyClientTool,
14
17
  InferSchemaType,
@@ -47,14 +50,12 @@ export function useChat<
47
50
  createSignal<ConnectionStatus>('disconnected')
48
51
  const [sessionGenerating, setSessionGenerating] = createSignal(false)
49
52
 
50
- // Structured-output state. Runtime always tracks them — the conditional
51
- // return type hides them from callers that didn't supply `outputSchema`.
53
+ // Structured-output `partial` / `final` are derived from `messages` —
54
+ // specifically from the structured-output part on the latest assistant
55
+ // message (the one after the most recent user message). Per-turn parts
56
+ // keep history coherent without a separate reset signal.
52
57
  type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
53
58
  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
59
 
59
60
  // Create ChatClient instance with callbacks to sync state.
60
61
  // Every user-provided callback is wrapped so the LATEST `options.xxx` value
@@ -68,27 +69,9 @@ export function useChat<
68
69
  id: clientId,
69
70
  initialMessages: options.initialMessages,
70
71
  body: options.body,
72
+ forwardedProps: options.forwardedProps,
71
73
  onResponse: (response) => options.onResponse?.(response),
72
74
  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
75
  options.onChunk?.(chunk)
93
76
  },
94
77
  onFinish: (message) => {
@@ -127,11 +110,14 @@ export function useChat<
127
110
  // Connection and other options are captured at creation time
128
111
  }, [clientId])
129
112
 
130
- // Sync body changes to the client
131
- // This allows dynamic body values (like model selection) to be updated without recreating the client
113
+ // Sync body / forwardedProps changes to the client.
114
+ // Both populate the same wire payload; `forwardedProps` is preferred
115
+ // and `body` is deprecated but still supported.
132
116
  createEffect(() => {
133
- const currentBody = options.body
134
- client().updateOptions({ body: currentBody })
117
+ client().updateOptions({
118
+ body: options.body,
119
+ forwardedProps: options.forwardedProps,
120
+ })
135
121
  })
136
122
 
137
123
  // Sync initial messages on mount only
@@ -214,6 +200,44 @@ export function useChat<
214
200
  await client().addToolApprovalResponse(response)
215
201
  }
216
202
 
203
+ // The "active" structured-output part is on the assistant message after
204
+ // the latest user message. When no user message exists yet, return null
205
+ // rather than scanning history — otherwise a stale `final` from
206
+ // `initialMessages` would leak into the value on first render.
207
+ const activeStructuredPart = createMemo<StructuredOutputPart | null>(() => {
208
+ const list = messages()
209
+ let lastUserIndex = -1
210
+ for (let i = list.length - 1; i >= 0; i--) {
211
+ if (list[i]!.role === 'user') {
212
+ lastUserIndex = i
213
+ break
214
+ }
215
+ }
216
+ if (lastUserIndex === -1) return null
217
+ for (let i = list.length - 1; i > lastUserIndex; i--) {
218
+ const m = list[i]!
219
+ if (m.role !== 'assistant') continue
220
+ const part = m.parts.find(
221
+ (p): p is StructuredOutputPart => p.type === 'structured-output',
222
+ )
223
+ if (part) return part
224
+ }
225
+ return null
226
+ })
227
+
228
+ const partial = createMemo<Partial>(() => {
229
+ const part = activeStructuredPart()
230
+ if (!part) return {} as Partial
231
+ const v = part.partial ?? part.data
232
+ return (v ?? {}) as Partial
233
+ })
234
+
235
+ const final = createMemo<Final | null>(() => {
236
+ const part = activeStructuredPart()
237
+ if (!part || part.status !== 'complete') return null
238
+ return part.data as Final
239
+ })
240
+
217
241
  // partial / final are runtime-tracked unconditionally; the conditional
218
242
  // return type hides them when no `outputSchema` is supplied.
219
243
  return {