@tanstack/ai-svelte 0.9.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
@@ -59,25 +54,6 @@ export function createChat(options) {
59
54
  forwardedProps: options.forwardedProps,
60
55
  onResponse: options.onResponse,
61
56
  onChunk: (chunk) => {
62
- if (options.outputSchema !== undefined) {
63
- if (chunk.type === 'RUN_STARTED') {
64
- rawJson = '';
65
- partial = {};
66
- final = null;
67
- }
68
- else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
69
- rawJson += chunk.delta;
70
- const progressive = parsePartialJSON(rawJson);
71
- if (progressive && typeof progressive === 'object') {
72
- partial = progressive;
73
- }
74
- }
75
- else if (chunk.type === 'CUSTOM' &&
76
- chunk.name === 'structured-output.complete') {
77
- const value = chunk.value;
78
- final = value.object;
79
- }
80
- }
81
57
  options.onChunk?.(chunk);
82
58
  },
83
59
  onFinish: (message) => {
@@ -153,6 +129,41 @@ export function createChat(options) {
153
129
  const updateForwardedProps = (newForwardedProps) => {
154
130
  client.updateOptions({ forwardedProps: newForwardedProps });
155
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);
156
167
  // Return the chat interface with reactive getters
157
168
  // Using getters allows Svelte to track reactivity without needing $ prefix
158
169
  return {
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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-svelte",
3
- "version": "0.9.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.10.0"
36
+ "@tanstack/ai-client": "0.11.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "svelte": "^5.0.0",
40
- "@tanstack/ai": "^0.18.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.18.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
@@ -88,25 +89,6 @@ export function createChat<
88
89
  forwardedProps: options.forwardedProps,
89
90
  onResponse: options.onResponse,
90
91
  onChunk: (chunk: StreamChunk) => {
91
- if (options.outputSchema !== undefined) {
92
- if (chunk.type === 'RUN_STARTED') {
93
- rawJson = ''
94
- partial = {} as Partial
95
- final = null
96
- } else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
97
- rawJson += chunk.delta
98
- const progressive = parsePartialJSON(rawJson)
99
- if (progressive && typeof progressive === 'object') {
100
- partial = progressive as Partial
101
- }
102
- } else if (
103
- chunk.type === 'CUSTOM' &&
104
- chunk.name === 'structured-output.complete'
105
- ) {
106
- const value = chunk.value as { object: unknown }
107
- final = value.object as Final
108
- }
109
- }
110
92
  options.onChunk?.(chunk)
111
93
  },
112
94
  onFinish: (message) => {
@@ -204,6 +186,44 @@ export function createChat<
204
186
  client.updateOptions({ forwardedProps: newForwardedProps })
205
187
  }
206
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
+
207
227
  // Return the chat interface with reactive getters
208
228
  // Using getters allows Svelte to track reactivity without needing $ prefix
209
229
  return {
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