@tanstack/ai-client 0.11.8 → 0.13.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.
Files changed (39) hide show
  1. package/dist/esm/chat-client.d.ts +17 -2
  2. package/dist/esm/chat-client.js +223 -37
  3. package/dist/esm/chat-client.js.map +1 -1
  4. package/dist/esm/connection-adapters.d.ts +20 -2
  5. package/dist/esm/connection-adapters.js +154 -48
  6. package/dist/esm/connection-adapters.js.map +1 -1
  7. package/dist/esm/devtools-noop.d.ts +60 -0
  8. package/dist/esm/devtools-noop.js +135 -0
  9. package/dist/esm/devtools-noop.js.map +1 -0
  10. package/dist/esm/devtools.d.ts +276 -0
  11. package/dist/esm/devtools.js +1178 -0
  12. package/dist/esm/devtools.js.map +1 -0
  13. package/dist/esm/events.d.ts +50 -7
  14. package/dist/esm/events.js +65 -20
  15. package/dist/esm/events.js.map +1 -1
  16. package/dist/esm/generation-client.d.ts +14 -0
  17. package/dist/esm/generation-client.js +135 -5
  18. package/dist/esm/generation-client.js.map +1 -1
  19. package/dist/esm/generation-types.d.ts +15 -1
  20. package/dist/esm/generation-types.js.map +1 -1
  21. package/dist/esm/index.d.ts +3 -2
  22. package/dist/esm/index.js +4 -1
  23. package/dist/esm/index.js.map +1 -1
  24. package/dist/esm/types.d.ts +78 -10
  25. package/dist/esm/types.js.map +1 -1
  26. package/dist/esm/video-generation-client.d.ts +15 -0
  27. package/dist/esm/video-generation-client.js +147 -8
  28. package/dist/esm/video-generation-client.js.map +1 -1
  29. package/package.json +8 -4
  30. package/src/chat-client.ts +285 -39
  31. package/src/connection-adapters.ts +226 -61
  32. package/src/devtools-noop.ts +205 -0
  33. package/src/devtools.ts +1866 -0
  34. package/src/events.ts +142 -11
  35. package/src/generation-client.ts +176 -9
  36. package/src/generation-types.ts +23 -4
  37. package/src/index.ts +15 -1
  38. package/src/types.ts +98 -12
  39. package/src/video-generation-client.ts +186 -11
@@ -4,8 +4,11 @@ import {
4
4
  generateMessageId,
5
5
  normalizeToUIMessage,
6
6
  } from '@tanstack/ai'
7
- import { DefaultChatClientEventEmitter } from './events'
8
- import { normalizeConnectionAdapter } from './connection-adapters'
7
+ import { createNoOpChatDevtoolsBridge } from './devtools-noop'
8
+ import {
9
+ fetcherToConnectionAdapter,
10
+ normalizeConnectionAdapter,
11
+ } from './connection-adapters'
9
12
  import type {
10
13
  AnyClientTool,
11
14
  ContentPart,
@@ -16,10 +19,19 @@ import type {
16
19
  ConnectionAdapter,
17
20
  SubscribeConnectionAdapter,
18
21
  } from './connection-adapters'
19
- import type { ChatClientEventEmitter } from './events'
22
+ import type {
23
+ ChatClientEventEmitter,
24
+ ChatClientRunEventContext,
25
+ } from './events'
26
+ import type {
27
+ AIDevtoolsChatSnapshot,
28
+ ChatDevtoolsBridge,
29
+ ChatDevtoolsBridgeOptions,
30
+ } from './devtools'
20
31
  import type {
21
32
  ChatClientOptions,
22
33
  ChatClientState,
34
+ ChatFetcher,
23
35
  ConnectionStatus,
24
36
  MessagePart,
25
37
  MultimodalContent,
@@ -27,6 +39,21 @@ import type {
27
39
  UIMessage,
28
40
  } from './types'
29
41
 
42
+ function resolveTransport(transport: {
43
+ connection?: ConnectionAdapter
44
+ fetcher?: ChatFetcher
45
+ }): ConnectionAdapter {
46
+ const { connection, fetcher } = transport
47
+ if (connection && fetcher) {
48
+ throw new Error(
49
+ 'ChatClient: pass either `connection` or `fetcher`, not both.',
50
+ )
51
+ }
52
+ if (connection) return connection
53
+ if (fetcher) return fetcherToConnectionAdapter(fetcher)
54
+ throw new Error('ChatClient: either `connection` or `fetcher` is required.')
55
+ }
56
+
30
57
  export class ChatClient {
31
58
  private readonly processor: StreamProcessor
32
59
  private connection: SubscribeConnectionAdapter
@@ -45,8 +72,15 @@ export class ChatClient {
45
72
  private status: ChatClientState = 'ready'
46
73
  private connectionStatus: ConnectionStatus = 'disconnected'
47
74
  private abortController: AbortController | null = null
48
- private readonly events: ChatClientEventEmitter
49
75
  private readonly clientToolsRef: { current: Map<string, AnyClientTool> }
76
+ private readonly devtoolsBridge: ChatDevtoolsBridge
77
+ /**
78
+ * Alias for `this.events`. The bridge installs an
79
+ * emitter that auto-attaches run/thread context and auto-emits a
80
+ * snapshot after every event, so chat-client only ever calls
81
+ * `this.events.X(...)` exactly like it did before devtools landed.
82
+ */
83
+ private readonly events: ChatClientEventEmitter
50
84
  private currentStreamId: string | null = null
51
85
  private currentMessageId: string | null = null
52
86
  private readonly postStreamActions: Array<() => Promise<void>> = []
@@ -64,6 +98,7 @@ export class ChatClient {
64
98
  private draining = false
65
99
  private sessionGenerating = false
66
100
  private readonly activeRunIds = new Set<string>()
101
+ private devtoolsMounted = false
67
102
 
68
103
  private readonly callbacksRef: {
69
104
  current: {
@@ -96,8 +131,7 @@ export class ChatClient {
96
131
  // winning on key collision.
97
132
  this.bodyOption = options.body || {}
98
133
  this.forwardedPropsOption = options.forwardedProps || {}
99
- this.connection = normalizeConnectionAdapter(options.connection)
100
- this.events = new DefaultChatClientEventEmitter(this.uniqueId)
134
+ this.connection = normalizeConnectionAdapter(resolveTransport(options))
101
135
 
102
136
  // Build client tools map
103
137
  this.clientToolsRef = { current: new Map() }
@@ -107,6 +141,11 @@ export class ChatClient {
107
141
  }
108
142
  }
109
143
 
144
+ this.devtoolsBridge = (
145
+ options.devtoolsBridgeFactory ?? createNoOpChatDevtoolsBridge
146
+ )(this.buildDevtoolsBridgeOptions(options.devtools))
147
+ this.events = this.devtoolsBridge.events
148
+
110
149
  this.callbacksRef = {
111
150
  current: {
112
151
  onResponse: options.onResponse || (() => {}),
@@ -181,9 +220,41 @@ export class ChatClient {
181
220
  this.currentStreamId,
182
221
  messageId,
183
222
  content,
223
+ undefined,
184
224
  )
185
225
  }
186
226
  },
227
+ onStructuredOutputChange: (args) => {
228
+ const streamId = this.devtoolsBridge.resolveStreamId()
229
+ const eventName =
230
+ args.phase === 'start'
231
+ ? 'structured-output:started'
232
+ : args.phase === 'complete'
233
+ ? 'structured-output:completed'
234
+ : args.phase === 'error'
235
+ ? 'structured-output:errored'
236
+ : 'structured-output:updated'
237
+
238
+ this.currentMessageId = args.messageId
239
+ this.events.structuredOutputChanged(
240
+ eventName,
241
+ streamId,
242
+ args.messageId,
243
+ {
244
+ status: args.status,
245
+ raw: args.raw,
246
+ ...(args.partial !== undefined ? { partial: args.partial } : {}),
247
+ ...(args.data !== undefined ? { data: args.data } : {}),
248
+ ...(args.reasoning !== undefined
249
+ ? { reasoning: args.reasoning }
250
+ : {}),
251
+ ...(args.errorMessage !== undefined
252
+ ? { errorMessage: args.errorMessage }
253
+ : {}),
254
+ ...(args.delta !== undefined ? { delta: args.delta } : {}),
255
+ },
256
+ )
257
+ },
187
258
  onToolCallStateChange: (
188
259
  messageId: string,
189
260
  toolCallId: string,
@@ -220,24 +291,36 @@ export class ChatClient {
220
291
  const clientTool = this.clientToolsRef.current.get(args.toolName)
221
292
  const executeFunc = clientTool?.execute
222
293
  if (executeFunc) {
294
+ // Capture the run context at execution-start so a tool whose
295
+ // result lands AFTER the originating run finishes still reports
296
+ // back against the originating run, not whatever run is
297
+ // current when the result emits.
298
+ const runEventContext =
299
+ this.devtoolsBridge.getCurrentRunEventContext()
223
300
  // Create and track the execution promise
224
301
  const executionPromise = (async () => {
225
302
  try {
226
303
  const output = await executeFunc(args.input)
227
- await this.addToolResult({
228
- toolCallId: args.toolCallId,
229
- tool: args.toolName,
230
- output,
231
- state: 'output-available',
232
- })
304
+ await this.addToolResultInternal(
305
+ {
306
+ toolCallId: args.toolCallId,
307
+ tool: args.toolName,
308
+ output,
309
+ state: 'output-available',
310
+ },
311
+ runEventContext,
312
+ )
233
313
  } catch (error: any) {
234
- await this.addToolResult({
235
- toolCallId: args.toolCallId,
236
- tool: args.toolName,
237
- output: null,
238
- state: 'output-error',
239
- errorText: error.message,
240
- })
314
+ await this.addToolResultInternal(
315
+ {
316
+ toolCallId: args.toolCallId,
317
+ tool: args.toolName,
318
+ output: null,
319
+ state: 'output-error',
320
+ errorText: error.message,
321
+ },
322
+ runEventContext,
323
+ )
241
324
  } finally {
242
325
  // Remove from pending when complete
243
326
  this.pendingToolExecutions.delete(args.toolCallId)
@@ -254,16 +337,20 @@ export class ChatClient {
254
337
  input: any
255
338
  approvalId: string
256
339
  }) => {
257
- if (this.currentStreamId) {
258
- this.events.approvalRequested(
259
- this.currentStreamId,
260
- this.currentMessageId || '',
261
- args.toolCallId,
262
- args.toolName,
263
- args.input,
264
- args.approvalId,
265
- )
266
- }
340
+ const streamId = this.devtoolsBridge.resolveStreamId()
341
+ const messageIdForApproval =
342
+ this.findMessageIdForToolCall(args.toolCallId) ??
343
+ this.currentMessageId ??
344
+ ''
345
+
346
+ this.events.approvalRequested(
347
+ streamId,
348
+ messageIdForApproval,
349
+ args.toolCallId,
350
+ args.toolName,
351
+ args.input,
352
+ args.approvalId,
353
+ )
267
354
  },
268
355
  onCustomEvent: (
269
356
  eventType: string,
@@ -274,8 +361,15 @@ export class ChatClient {
274
361
  },
275
362
  },
276
363
  })
364
+ }
365
+
366
+ mountDevtools(): void {
367
+ if (this.devtoolsMounted) {
368
+ return
369
+ }
277
370
 
278
- this.events.clientCreated(this.processor.getMessages().length)
371
+ this.devtoolsMounted = true
372
+ this.devtoolsBridge.mountWithTools(this.processor.getMessages().length)
279
373
  }
280
374
 
281
375
  private generateUniqueId(prefix: string): string {
@@ -291,22 +385,26 @@ export class ChatClient {
291
385
  private setStatus(status: ChatClientState): void {
292
386
  this.status = status
293
387
  this.callbacksRef.current.onStatusChange(status)
388
+ this.devtoolsBridge.emitSnapshot()
294
389
  }
295
390
 
296
391
  private setIsSubscribed(isSubscribed: boolean): void {
297
392
  this.isSubscribed = isSubscribed
298
393
  this.callbacksRef.current.onSubscriptionChange(isSubscribed)
394
+ this.devtoolsBridge.emitSnapshot()
299
395
  }
300
396
 
301
397
  private setConnectionStatus(status: ConnectionStatus): void {
302
398
  this.connectionStatus = status
303
399
  this.callbacksRef.current.onConnectionStatusChange(status)
400
+ this.devtoolsBridge.emitSnapshot()
304
401
  }
305
402
 
306
403
  private setSessionGenerating(isGenerating: boolean): void {
307
404
  if (this.sessionGenerating === isGenerating) return
308
405
  this.sessionGenerating = isGenerating
309
406
  this.callbacksRef.current.onSessionGeneratingChange(isGenerating)
407
+ this.devtoolsBridge.emitSnapshot()
310
408
  }
311
409
 
312
410
  private resetSessionGenerating(): void {
@@ -320,6 +418,57 @@ export class ChatClient {
320
418
  this.events.errorChanged(error?.message || null)
321
419
  }
322
420
 
421
+ private buildDevtoolsBridgeOptions(
422
+ devtools: ChatClientOptions['devtools'],
423
+ ): ChatDevtoolsBridgeOptions {
424
+ return {
425
+ hookId: this.uniqueId,
426
+ clientId: this.uniqueId,
427
+ threadId: this.threadId,
428
+ metadata: {
429
+ hookName: devtools?.hookName ?? 'useChat',
430
+ outputKind: devtools?.outputKind ?? 'chat',
431
+ ...(devtools?.framework ? { framework: devtools.framework } : {}),
432
+ ...(devtools?.name ? { name: devtools.name } : {}),
433
+ },
434
+ getSnapshot: () => this.getDevtoolsSnapshot(),
435
+ getTools: () => this.clientToolsRef.current.values(),
436
+ getMessages: () => this.processor.getMessages(),
437
+ setMessages: (messages: Array<UIMessage>) => {
438
+ this.processor.setMessages(messages)
439
+ },
440
+ addToolResult: (toolCallId, output, errorText) => {
441
+ this.processor.addToolResult(toolCallId, output, errorText)
442
+ },
443
+ generateId: (prefix) => this.generateUniqueId(prefix),
444
+ }
445
+ }
446
+
447
+ private getDevtoolsSnapshot(): AIDevtoolsChatSnapshot {
448
+ return {
449
+ messages: this.processor.getMessages(),
450
+ status: this.status,
451
+ isLoading: this.isLoading,
452
+ isSubscribed: this.isSubscribed,
453
+ connectionStatus: this.connectionStatus,
454
+ sessionGenerating: this.sessionGenerating,
455
+ activeRunIds: Array.from(this.activeRunIds),
456
+ ...(this.error ? { error: this.error.message } : {}),
457
+ }
458
+ }
459
+
460
+ private findMessageIdForToolCall(toolCallId: string): string | undefined {
461
+ const messages = this.processor.getMessages()
462
+ for (const message of messages) {
463
+ const match = message.parts.find(
464
+ (part: MessagePart): part is ToolCallPart =>
465
+ part.type === 'tool-call' && part.id === toolCallId,
466
+ )
467
+ if (match) return message.id
468
+ }
469
+ return undefined
470
+ }
471
+
323
472
  private abortSubscriptionLoop(): void {
324
473
  this.subscriptionAbortController?.abort()
325
474
  this.subscriptionAbortController = null
@@ -409,11 +558,12 @@ export class ChatClient {
409
558
  this.setConnectionStatus('connected')
410
559
  }
411
560
  this.callbacksRef.current.onChunk(chunk)
412
- this.processor.processChunk(chunk)
413
561
  if (chunk.type === 'RUN_STARTED') {
414
562
  this.activeRunIds.add(chunk.runId)
415
563
  this.setSessionGenerating(true)
416
564
  }
565
+ this.devtoolsBridge.observeChunk(chunk)
566
+ this.processor.processChunk(chunk)
417
567
  // RUN_FINISHED / RUN_ERROR signal run completion — resolve processing
418
568
  // (redundant if onStreamEnd already resolved it, harmless)
419
569
  if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {
@@ -422,9 +572,9 @@ export class ChatClient {
422
572
  // both so a RUN_ERROR with a runId only clears that run, not every
423
573
  // active run in the session.
424
574
  const runId =
425
- chunk.type === 'RUN_FINISHED'
575
+ 'runId' in chunk && typeof chunk.runId === 'string'
426
576
  ? chunk.runId
427
- : (chunk as { runId?: string }).runId
577
+ : undefined
428
578
  if (runId) {
429
579
  this.activeRunIds.delete(runId)
430
580
  } else if (chunk.type === 'RUN_ERROR') {
@@ -510,6 +660,7 @@ export class ChatClient {
510
660
  content: string | MultimodalContent,
511
661
  body?: Record<string, any>,
512
662
  ): Promise<void> {
663
+ this.mountDevtools()
513
664
  const emptyMessage = typeof content === 'string' && !content.trim()
514
665
  if (emptyMessage || this.isLoading) {
515
666
  return
@@ -548,6 +699,7 @@ export class ChatClient {
548
699
  * Append a message and stream the response
549
700
  */
550
701
  async append(message: UIMessage | ModelMessage): Promise<void> {
702
+ this.mountDevtools()
551
703
  // Normalize the message to ensure it has id and createdAt
552
704
  const normalizedMessage = normalizeToUIMessage(message, generateMessageId)
553
705
 
@@ -565,6 +717,7 @@ export class ChatClient {
565
717
  // Add to messages
566
718
  const messages = this.processor.getMessages()
567
719
  this.processor.setMessages([...messages, uiMessage])
720
+ this.devtoolsBridge.emitSnapshot()
568
721
 
569
722
  // If stream is in progress, queue the response for after it ends
570
723
  if (this.isLoading) {
@@ -602,6 +755,8 @@ export class ChatClient {
602
755
  // Reset pending tool executions for the new stream
603
756
  this.pendingToolExecutions.clear()
604
757
  let streamCompletedSuccessfully = false
758
+ let activeDevtoolsRunId: string | null = null
759
+ let runTerminalEventEmitted = false
605
760
 
606
761
  try {
607
762
  // Get UIMessages with parts (preserves approval state and client tool results)
@@ -639,6 +794,7 @@ export class ChatClient {
639
794
 
640
795
  // Generate stream ID — assistant message will be created by stream events
641
796
  this.currentStreamId = this.generateUniqueId('stream')
797
+ this.devtoolsBridge.setCurrentStreamId(this.currentStreamId)
642
798
  this.currentMessageId = null
643
799
 
644
800
  // Reset processor stream state for new response — prevents stale
@@ -674,6 +830,19 @@ export class ChatClient {
674
830
  ),
675
831
  forwardedProps: { ...mergedBody },
676
832
  }
833
+ this.devtoolsBridge.beginRun(runContext.runId, this.threadId)
834
+ activeDevtoolsRunId = runContext.runId
835
+ this.devtoolsBridge.emitRunLifecycle(
836
+ 'run:created',
837
+ runContext.runId,
838
+ 'created',
839
+ )
840
+ this.devtoolsBridge.emitRunLifecycle(
841
+ 'run:started',
842
+ runContext.runId,
843
+ 'started',
844
+ )
845
+ this.devtoolsBridge.emitSnapshot()
677
846
 
678
847
  // Send through normalized connection (pushes chunks to subscription queue)
679
848
  await this.connection.send(messages, mergedBody, signal, runContext)
@@ -690,6 +859,15 @@ export class ChatClient {
690
859
  // A RUN_ERROR from the stream transitions status to error.
691
860
  // Do not treat this stream as a successful completion.
692
861
  if (this.status === 'error') {
862
+ if (activeDevtoolsRunId) {
863
+ this.devtoolsBridge.emitRunLifecycle(
864
+ 'run:errored',
865
+ activeDevtoolsRunId,
866
+ 'errored',
867
+ this.error ? { error: this.error.message } : {},
868
+ )
869
+ runTerminalEventEmitted = true
870
+ }
693
871
  return false
694
872
  }
695
873
 
@@ -704,10 +882,27 @@ export class ChatClient {
704
882
  } catch (err) {
705
883
  if (err instanceof Error) {
706
884
  if (err.name === 'AbortError') {
885
+ if (activeDevtoolsRunId) {
886
+ this.devtoolsBridge.emitRunLifecycle(
887
+ 'run:cancelled',
888
+ activeDevtoolsRunId,
889
+ 'cancelled',
890
+ )
891
+ runTerminalEventEmitted = true
892
+ }
707
893
  return false
708
894
  }
709
895
  if (generation === this.streamGeneration) {
710
896
  this.reportStreamError(err)
897
+ if (activeDevtoolsRunId) {
898
+ this.devtoolsBridge.emitRunLifecycle(
899
+ 'run:errored',
900
+ activeDevtoolsRunId,
901
+ 'errored',
902
+ { error: err.message },
903
+ )
904
+ runTerminalEventEmitted = true
905
+ }
711
906
  }
712
907
  }
713
908
  } finally {
@@ -716,11 +911,28 @@ export class ChatClient {
716
911
  // clobber the new stream's abortController or isLoading state.
717
912
  if (generation === this.streamGeneration) {
718
913
  this.currentStreamId = null
914
+ this.devtoolsBridge.setCurrentStreamId(null)
719
915
  this.currentMessageId = null
720
916
  this.abortController = null
721
917
  this.setIsLoading(false)
722
918
  this.pendingMessageBody = undefined // Ensure it's cleared even on error
723
919
 
920
+ if (activeDevtoolsRunId && !runTerminalEventEmitted) {
921
+ if (streamCompletedSuccessfully) {
922
+ this.devtoolsBridge.emitRunLifecycle(
923
+ 'run:completed',
924
+ activeDevtoolsRunId,
925
+ 'completed',
926
+ )
927
+ } else if (signal.aborted) {
928
+ this.devtoolsBridge.emitRunLifecycle(
929
+ 'run:cancelled',
930
+ activeDevtoolsRunId,
931
+ 'cancelled',
932
+ )
933
+ }
934
+ }
935
+
724
936
  // Drain any actions that were queued while the stream was in progress
725
937
  await this.drainPostStreamActions()
726
938
 
@@ -792,7 +1004,7 @@ export class ChatClient {
792
1004
 
793
1005
  // Find the last user message
794
1006
  const lastUserMessageIndex = messages.findLastIndex(
795
- (m: UIMessage) => m.role === 'user',
1007
+ (m) => m.role === 'user',
796
1008
  )
797
1009
 
798
1010
  if (lastUserMessageIndex === -1) return
@@ -806,6 +1018,7 @@ export class ChatClient {
806
1018
 
807
1019
  // Remove all messages after the last user message
808
1020
  this.processor.removeMessagesAfter(lastUserMessageIndex)
1021
+ this.devtoolsBridge.emitSnapshot()
809
1022
 
810
1023
  // Resend
811
1024
  await this.streamResponse()
@@ -838,11 +1051,25 @@ export class ChatClient {
838
1051
  state?: 'output-available' | 'output-error'
839
1052
  errorText?: string
840
1053
  }): Promise<void> {
1054
+ await this.addToolResultInternal(result)
1055
+ }
1056
+
1057
+ private async addToolResultInternal(
1058
+ result: {
1059
+ toolCallId: string
1060
+ tool: string
1061
+ output: any
1062
+ state?: 'output-available' | 'output-error'
1063
+ errorText?: string
1064
+ },
1065
+ context?: ChatClientRunEventContext,
1066
+ ): Promise<void> {
841
1067
  this.events.toolResultAdded(
842
1068
  result.toolCallId,
843
1069
  result.tool,
844
1070
  result.output,
845
1071
  result.state || 'output-available',
1072
+ context,
846
1073
  )
847
1074
 
848
1075
  // Add result via processor
@@ -893,6 +1120,7 @@ export class ChatClient {
893
1120
 
894
1121
  // Add response via processor
895
1122
  this.processor.addToolApprovalResponse(response.id, response.approved)
1123
+ this.devtoolsBridge.emitSnapshot()
896
1124
 
897
1125
  // If stream is in progress, queue continuation check for after it ends
898
1126
  if (this.isLoading) {
@@ -964,9 +1192,13 @@ export class ChatClient {
964
1192
  */
965
1193
  private shouldAutoSend(): boolean {
966
1194
  const messages = this.processor.getMessages()
967
- const lastAssistant = messages.findLast((m) => m.role === 'assistant')
1195
+ const lastAssistant = messages.findLast(
1196
+ (m: UIMessage) => m.role === 'assistant',
1197
+ )
968
1198
  if (!lastAssistant) return false
969
- const hasToolCalls = lastAssistant.parts.some((p) => p.type === 'tool-call')
1199
+ const hasToolCalls = lastAssistant.parts.some(
1200
+ (p: MessagePart) => p.type === 'tool-call',
1201
+ )
970
1202
  if (!hasToolCalls) return false
971
1203
  return this.processor.areAllToolsComplete()
972
1204
  }
@@ -1028,6 +1260,7 @@ export class ChatClient {
1028
1260
  */
1029
1261
  setMessagesManually(messages: Array<UIMessage>): void {
1030
1262
  this.processor.setMessages(messages)
1263
+ this.devtoolsBridge.emitSnapshot()
1031
1264
  }
1032
1265
 
1033
1266
  /**
@@ -1035,6 +1268,7 @@ export class ChatClient {
1035
1268
  */
1036
1269
  updateOptions(options: {
1037
1270
  connection?: ConnectionAdapter
1271
+ fetcher?: ChatFetcher
1038
1272
  /** @deprecated Use `forwardedProps` instead. */
1039
1273
  body?: Record<string, any>
1040
1274
  forwardedProps?: Record<string, any>
@@ -1052,7 +1286,7 @@ export class ChatClient {
1052
1286
  context: { toolCallId?: string },
1053
1287
  ) => void
1054
1288
  }): void {
1055
- if (options.connection !== undefined) {
1289
+ if (options.connection !== undefined || options.fetcher !== undefined) {
1056
1290
  const wasSubscribed = this.isSubscribed
1057
1291
 
1058
1292
  if (this.isLoading) {
@@ -1067,7 +1301,12 @@ export class ChatClient {
1067
1301
  this.resetSessionGenerating()
1068
1302
  this.setIsSubscribed(false)
1069
1303
  this.setConnectionStatus('disconnected')
1070
- this.connection = normalizeConnectionAdapter(options.connection)
1304
+ this.connection = normalizeConnectionAdapter(
1305
+ resolveTransport({
1306
+ connection: options.connection,
1307
+ fetcher: options.fetcher,
1308
+ }),
1309
+ )
1071
1310
 
1072
1311
  if (wasSubscribed) {
1073
1312
  this.subscribe()
@@ -1075,7 +1314,7 @@ export class ChatClient {
1075
1314
  }
1076
1315
  // Replace each slot independently so callers can update one without
1077
1316
  // wiping the other. (Passing `undefined` for either field is a "leave
1078
- // unchanged" signal — to clear a slot, pass an empty object `{}`.)
1317
+ // unchanged" signal - to clear a slot, pass an empty object `{}`.)
1079
1318
  if (options.body !== undefined) {
1080
1319
  this.bodyOption = options.body
1081
1320
  }
@@ -1087,6 +1326,7 @@ export class ChatClient {
1087
1326
  for (const tool of options.tools) {
1088
1327
  this.clientToolsRef.current.set(tool.name, tool)
1089
1328
  }
1329
+ this.devtoolsBridge.notifyToolsChanged()
1090
1330
  }
1091
1331
  if (options.onResponse !== undefined) {
1092
1332
  this.callbacksRef.current.onResponse = options.onResponse
@@ -1116,4 +1356,10 @@ export class ChatClient {
1116
1356
  this.callbacksRef.current.onCustomEvent = options.onCustomEvent
1117
1357
  }
1118
1358
  }
1359
+
1360
+ dispose(): void {
1361
+ this.unsubscribe()
1362
+ this.devtoolsBridge.dispose()
1363
+ this.devtoolsMounted = false
1364
+ }
1119
1365
  }