@tanstack/ai-svelte 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.
@@ -1,5 +1,4 @@
1
1
  import { ChatClient } from '@tanstack/ai-client';
2
- import { parsePartialJSON } from '@tanstack/ai';
3
2
  /**
4
3
  * Creates a reactive chat instance for Svelte 5.
5
4
  *
@@ -42,10 +41,6 @@ export function createChat(options) {
42
41
  let isSubscribed = $state(false);
43
42
  let connectionStatus = $state('disconnected');
44
43
  let sessionGenerating = $state(false);
45
- let partial = $state({});
46
- let final = $state(null);
47
- // Raw JSON accumulator — not reactive, just a local buffer.
48
- let rawJson = '';
49
44
  // Create ChatClient instance.
50
45
  // Note: Svelte's createChat runs once per instance and `options` is captured
51
46
  // by reference. Callbacks are therefore frozen to whatever the caller passed
@@ -56,27 +51,9 @@ export function createChat(options) {
56
51
  id: clientId,
57
52
  initialMessages: options.initialMessages,
58
53
  body: options.body,
54
+ forwardedProps: options.forwardedProps,
59
55
  onResponse: options.onResponse,
60
56
  onChunk: (chunk) => {
61
- if (options.outputSchema !== undefined) {
62
- if (chunk.type === 'RUN_STARTED') {
63
- rawJson = '';
64
- partial = {};
65
- final = null;
66
- }
67
- else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
68
- rawJson += chunk.delta;
69
- const progressive = parsePartialJSON(rawJson);
70
- if (progressive && typeof progressive === 'object') {
71
- partial = progressive;
72
- }
73
- }
74
- else if (chunk.type === 'CUSTOM' &&
75
- chunk.name === 'structured-output.complete') {
76
- const value = chunk.value;
77
- final = value.object;
78
- }
79
- }
80
57
  options.onChunk?.(chunk);
81
58
  },
82
59
  onFinish: (message) => {
@@ -142,9 +119,51 @@ export function createChat(options) {
142
119
  const addToolApprovalResponse = async (response) => {
143
120
  await client.addToolApprovalResponse(response);
144
121
  };
122
+ /**
123
+ * @deprecated Use `updateForwardedProps` instead.
124
+ * Both populate the same wire payload.
125
+ */
145
126
  const updateBody = (newBody) => {
146
127
  client.updateOptions({ body: newBody });
147
128
  };
129
+ const updateForwardedProps = (newForwardedProps) => {
130
+ client.updateOptions({ forwardedProps: newForwardedProps });
131
+ };
132
+ // The "active" structured-output part is the one on the assistant message
133
+ // after the latest user message. When no user message exists yet (e.g.
134
+ // `initialMessages` carries only a stale assistant turn), we return null
135
+ // rather than scanning history — otherwise a `final` from a previous
136
+ // session would leak in on first render. Uses `$derived.by` so the
137
+ // multi-line scan re-runs whenever `messages` changes.
138
+ const activeStructuredPart = $derived.by(() => {
139
+ let lastUserIndex = -1;
140
+ for (let i = messages.length - 1; i >= 0; i--) {
141
+ if (messages[i].role === 'user') {
142
+ lastUserIndex = i;
143
+ break;
144
+ }
145
+ }
146
+ if (lastUserIndex === -1)
147
+ return null;
148
+ for (let i = messages.length - 1; i > lastUserIndex; i--) {
149
+ const m = messages[i];
150
+ if (m.role !== 'assistant')
151
+ continue;
152
+ const part = m.parts.find((p) => p.type === 'structured-output');
153
+ if (part)
154
+ return part;
155
+ }
156
+ return null;
157
+ });
158
+ const partial = $derived.by(() => {
159
+ if (!activeStructuredPart)
160
+ return {};
161
+ const v = activeStructuredPart.partial ?? activeStructuredPart.data;
162
+ return (v ?? {});
163
+ });
164
+ const final = $derived(activeStructuredPart && activeStructuredPart.status === 'complete'
165
+ ? activeStructuredPart.data
166
+ : null);
148
167
  // Return the chat interface with reactive getters
149
168
  // Using getters allows Svelte to track reactivity without needing $ prefix
150
169
  return {
@@ -184,5 +203,6 @@ export function createChat(options) {
184
203
  addToolResult,
185
204
  addToolApprovalResponse,
186
205
  updateBody,
206
+ updateForwardedProps,
187
207
  };
188
208
  }
package/dist/types.d.ts CHANGED
@@ -43,24 +43,27 @@ export type CreateChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any,
43
43
  * typed `partial` / `final` reactive getters; otherwise the return is
44
44
  * unchanged.
45
45
  */
46
- export type CreateChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseCreateChatReturn<TTools> & (TSchema extends SchemaInput ? {
46
+ export type CreateChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseCreateChatReturn<TTools, TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown> & (TSchema extends SchemaInput ? {
47
47
  /**
48
48
  * Live progressively-parsed structured output (reactive getter).
49
- * Resets on every new run.
49
+ * Derived from the latest assistant message's structured-output part.
50
50
  */
51
51
  readonly partial: DeepPartial<InferSchemaType<TSchema>>;
52
52
  /**
53
53
  * Final, schema-validated structured output (reactive getter). `null`
54
- * until the terminal `structured-output.complete` event arrives.
55
- * Resets on every new run.
54
+ * until the latest assistant turn's structured-output part transitions
55
+ * to `complete`.
56
56
  */
57
57
  readonly final: InferSchemaType<TSchema> | null;
58
58
  } : Record<never, never>);
59
- interface BaseCreateChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
59
+ interface BaseCreateChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown> {
60
60
  /**
61
- * Current messages in the conversation (reactive getter)
61
+ * Current messages in the conversation (reactive getter). When
62
+ * `outputSchema` is supplied, `messages[i].parts.find(p => p.type ===
63
+ * 'structured-output')` is typed by the schema — `data: T`,
64
+ * `partial: DeepPartial<T>`.
62
65
  */
63
- readonly messages: Array<UIMessage<TTools>>;
66
+ readonly messages: Array<UIMessage<TTools, TData>>;
64
67
  /**
65
68
  * Send a message and get a response.
66
69
  * Can be a simple string or multimodal content with images, audio, etc.
@@ -69,7 +72,7 @@ interface BaseCreateChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any
69
72
  /**
70
73
  * Append a message to the conversation
71
74
  */
72
- append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>;
75
+ append: (message: ModelMessage | UIMessage<TTools, TData>) => Promise<void>;
73
76
  /**
74
77
  * Add the result of a client-side tool execution
75
78
  */
@@ -106,7 +109,7 @@ interface BaseCreateChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any
106
109
  /**
107
110
  * Set messages manually
108
111
  */
109
- setMessages: (messages: Array<UIMessage<TTools>>) => void;
112
+ setMessages: (messages: Array<UIMessage<TTools, TData>>) => void;
110
113
  /**
111
114
  * Clear all messages
112
115
  */
@@ -131,7 +134,13 @@ interface BaseCreateChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any
131
134
  */
132
135
  readonly sessionGenerating: boolean;
133
136
  /**
134
- * Update the body sent with requests (e.g., for changing model selection)
137
+ * @deprecated Use `updateForwardedProps` instead. Both populate the
138
+ * same wire payload; `updateBody` is retained for backward compatibility.
135
139
  */
136
140
  updateBody: (body: Record<string, any>) => void;
141
+ /**
142
+ * Update the AG-UI `forwardedProps` sent with requests (e.g., for
143
+ * changing model selection or other client-driven options).
144
+ */
145
+ updateForwardedProps: (forwardedProps: Record<string, any>) => void;
137
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-svelte",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Svelte bindings for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -33,11 +33,11 @@
33
33
  "streaming"
34
34
  ],
35
35
  "dependencies": {
36
- "@tanstack/ai-client": "0.9.2"
36
+ "@tanstack/ai-client": "0.11.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "svelte": "^5.0.0",
40
- "@tanstack/ai": "^0.17.0"
40
+ "@tanstack/ai": "^0.19.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@standard-schema/spec": "^1.1.0",
@@ -49,8 +49,8 @@
49
49
  "svelte": "^5.20.0",
50
50
  "svelte-check": "^4.2.0",
51
51
  "typescript": "5.9.3",
52
- "vite": "^7.2.7",
53
- "@tanstack/ai": "0.17.0"
52
+ "vite": "^7.3.3",
53
+ "@tanstack/ai": "0.19.0"
54
54
  },
55
55
  "scripts": {
56
56
  "clean": "rm -rf ./dist",
@@ -1,6 +1,9 @@
1
1
  import { ChatClient } from '@tanstack/ai-client'
2
- import { parsePartialJSON } from '@tanstack/ai'
3
- import type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'
2
+ import type {
3
+ ChatClientState,
4
+ ConnectionStatus,
5
+ StructuredOutputPart,
6
+ } from '@tanstack/ai-client'
4
7
  import type {
5
8
  AnyClientTool,
6
9
  InferSchemaType,
@@ -66,14 +69,12 @@ export function createChat<
66
69
  let connectionStatus = $state<ConnectionStatus>('disconnected')
67
70
  let sessionGenerating = $state(false)
68
71
 
69
- // Structured-output state. Runtime always tracks them — the conditional
70
- // return type hides them when no `outputSchema` is supplied.
72
+ // Structured-output `partial` / `final` are derived from `messages` —
73
+ // specifically from the structured-output part on the latest assistant
74
+ // message (the one after the most recent user message). Per-turn parts
75
+ // keep history coherent without a separate reset signal.
71
76
  type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
72
77
  type Final = InferSchemaType<NonNullable<TSchema>>
73
- let partial = $state<Partial>({} as Partial)
74
- let final = $state<Final | null>(null)
75
- // Raw JSON accumulator — not reactive, just a local buffer.
76
- let rawJson = ''
77
78
 
78
79
  // Create ChatClient instance.
79
80
  // Note: Svelte's createChat runs once per instance and `options` is captured
@@ -85,27 +86,9 @@ export function createChat<
85
86
  id: clientId,
86
87
  initialMessages: options.initialMessages,
87
88
  body: options.body,
89
+ forwardedProps: options.forwardedProps,
88
90
  onResponse: options.onResponse,
89
91
  onChunk: (chunk: StreamChunk) => {
90
- if (options.outputSchema !== undefined) {
91
- if (chunk.type === 'RUN_STARTED') {
92
- rawJson = ''
93
- partial = {} as Partial
94
- final = null
95
- } else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
96
- rawJson += chunk.delta
97
- const progressive = parsePartialJSON(rawJson)
98
- if (progressive && typeof progressive === 'object') {
99
- partial = progressive as Partial
100
- }
101
- } else if (
102
- chunk.type === 'CUSTOM' &&
103
- chunk.name === 'structured-output.complete'
104
- ) {
105
- const value = chunk.value as { object: unknown }
106
- final = value.object as Final
107
- }
108
- }
109
92
  options.onChunk?.(chunk)
110
93
  },
111
94
  onFinish: (message) => {
@@ -191,10 +174,56 @@ export function createChat<
191
174
  await client.addToolApprovalResponse(response)
192
175
  }
193
176
 
177
+ /**
178
+ * @deprecated Use `updateForwardedProps` instead.
179
+ * Both populate the same wire payload.
180
+ */
194
181
  const updateBody = (newBody: Record<string, any>) => {
195
182
  client.updateOptions({ body: newBody })
196
183
  }
197
184
 
185
+ const updateForwardedProps = (newForwardedProps: Record<string, any>) => {
186
+ client.updateOptions({ forwardedProps: newForwardedProps })
187
+ }
188
+
189
+ // The "active" structured-output part is the one on the assistant message
190
+ // after the latest user message. When no user message exists yet (e.g.
191
+ // `initialMessages` carries only a stale assistant turn), we return null
192
+ // rather than scanning history — otherwise a `final` from a previous
193
+ // session would leak in on first render. Uses `$derived.by` so the
194
+ // multi-line scan re-runs whenever `messages` changes.
195
+ const activeStructuredPart: StructuredOutputPart | null = $derived.by(() => {
196
+ let lastUserIndex = -1
197
+ for (let i = messages.length - 1; i >= 0; i--) {
198
+ if (messages[i]!.role === 'user') {
199
+ lastUserIndex = i
200
+ break
201
+ }
202
+ }
203
+ if (lastUserIndex === -1) return null
204
+ for (let i = messages.length - 1; i > lastUserIndex; i--) {
205
+ const m = messages[i]!
206
+ if (m.role !== 'assistant') continue
207
+ const part = m.parts.find(
208
+ (p): p is StructuredOutputPart => p.type === 'structured-output',
209
+ )
210
+ if (part) return part
211
+ }
212
+ return null
213
+ })
214
+
215
+ const partial: Partial = $derived.by(() => {
216
+ if (!activeStructuredPart) return {} as Partial
217
+ const v = activeStructuredPart.partial ?? activeStructuredPart.data
218
+ return (v ?? {}) as Partial
219
+ })
220
+
221
+ const final: Final | null = $derived(
222
+ activeStructuredPart && activeStructuredPart.status === 'complete'
223
+ ? (activeStructuredPart.data as Final)
224
+ : null,
225
+ )
226
+
198
227
  // Return the chat interface with reactive getters
199
228
  // Using getters allows Svelte to track reactivity without needing $ prefix
200
229
  return {
@@ -234,5 +263,6 @@ export function createChat<
234
263
  addToolResult,
235
264
  addToolApprovalResponse,
236
265
  updateBody,
266
+ updateForwardedProps,
237
267
  } as unknown as CreateChatReturn<TTools, TSchema>
238
268
  }
package/src/types.ts CHANGED
@@ -78,18 +78,21 @@ export type CreateChatOptions<
78
78
  export type CreateChatReturn<
79
79
  TTools extends ReadonlyArray<AnyClientTool> = any,
80
80
  TSchema extends SchemaInput | undefined = undefined,
81
- > = BaseCreateChatReturn<TTools> &
81
+ > = BaseCreateChatReturn<
82
+ TTools,
83
+ TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown
84
+ > &
82
85
  (TSchema extends SchemaInput
83
86
  ? {
84
87
  /**
85
88
  * Live progressively-parsed structured output (reactive getter).
86
- * Resets on every new run.
89
+ * Derived from the latest assistant message's structured-output part.
87
90
  */
88
91
  readonly partial: DeepPartial<InferSchemaType<TSchema>>
89
92
  /**
90
93
  * Final, schema-validated structured output (reactive getter). `null`
91
- * until the terminal `structured-output.complete` event arrives.
92
- * Resets on every new run.
94
+ * until the latest assistant turn's structured-output part transitions
95
+ * to `complete`.
93
96
  */
94
97
  readonly final: InferSchemaType<TSchema> | null
95
98
  }
@@ -97,11 +100,15 @@ export type CreateChatReturn<
97
100
 
98
101
  interface BaseCreateChatReturn<
99
102
  TTools extends ReadonlyArray<AnyClientTool> = any,
103
+ TData = unknown,
100
104
  > {
101
105
  /**
102
- * Current messages in the conversation (reactive getter)
106
+ * Current messages in the conversation (reactive getter). When
107
+ * `outputSchema` is supplied, `messages[i].parts.find(p => p.type ===
108
+ * 'structured-output')` is typed by the schema — `data: T`,
109
+ * `partial: DeepPartial<T>`.
103
110
  */
104
- readonly messages: Array<UIMessage<TTools>>
111
+ readonly messages: Array<UIMessage<TTools, TData>>
105
112
 
106
113
  /**
107
114
  * Send a message and get a response.
@@ -112,7 +119,7 @@ interface BaseCreateChatReturn<
112
119
  /**
113
120
  * Append a message to the conversation
114
121
  */
115
- append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>
122
+ append: (message: ModelMessage | UIMessage<TTools, TData>) => Promise<void>
116
123
 
117
124
  /**
118
125
  * Add the result of a client-side tool execution
@@ -156,7 +163,7 @@ interface BaseCreateChatReturn<
156
163
  /**
157
164
  * Set messages manually
158
165
  */
159
- setMessages: (messages: Array<UIMessage<TTools>>) => void
166
+ setMessages: (messages: Array<UIMessage<TTools, TData>>) => void
160
167
 
161
168
  /**
162
169
  * Clear all messages
@@ -183,9 +190,15 @@ interface BaseCreateChatReturn<
183
190
  */
184
191
  readonly sessionGenerating: boolean
185
192
  /**
186
- * Update the body sent with requests (e.g., for changing model selection)
193
+ * @deprecated Use `updateForwardedProps` instead. Both populate the
194
+ * same wire payload; `updateBody` is retained for backward compatibility.
187
195
  */
188
196
  updateBody: (body: Record<string, any>) => void
197
+ /**
198
+ * Update the AG-UI `forwardedProps` sent with requests (e.g., for
199
+ * changing model selection or other client-driven options).
200
+ */
201
+ updateForwardedProps: (forwardedProps: Record<string, any>) => void
189
202
  }
190
203
 
191
204
  // Note: createChatClientOptions and InferChatMessages are now in @tanstack/ai-client