@tanstack/ai-client 0.12.0 → 0.14.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 (59) hide show
  1. package/dist/esm/chat-client.d.ts +16 -2
  2. package/dist/esm/chat-client.js +197 -31
  3. package/dist/esm/chat-client.js.map +1 -1
  4. package/dist/esm/connection-adapters.d.ts +21 -1
  5. package/dist/esm/connection-adapters.js +262 -68
  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 +51 -8
  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 +16 -2
  20. package/dist/esm/generation-types.js.map +1 -1
  21. package/dist/esm/index.d.ts +6 -4
  22. package/dist/esm/index.js +9 -3
  23. package/dist/esm/index.js.map +1 -1
  24. package/dist/esm/realtime-client.d.ts +1 -1
  25. package/dist/esm/realtime-client.js +1 -1
  26. package/dist/esm/realtime-client.js.map +1 -1
  27. package/dist/esm/realtime-types.d.ts +1 -1
  28. package/dist/esm/response-stream.d.ts +7 -0
  29. package/dist/esm/response-stream.js +32 -0
  30. package/dist/esm/response-stream.js.map +1 -0
  31. package/dist/esm/sse-parser.d.ts +1 -1
  32. package/dist/esm/sse-parser.js +5 -6
  33. package/dist/esm/sse-parser.js.map +1 -1
  34. package/dist/esm/sse-utils.d.ts +1 -0
  35. package/dist/esm/sse-utils.js +11 -0
  36. package/dist/esm/sse-utils.js.map +1 -0
  37. package/dist/esm/tool-types.d.ts +1 -1
  38. package/dist/esm/types.d.ts +23 -5
  39. package/dist/esm/types.js.map +1 -1
  40. package/dist/esm/video-generation-client.d.ts +15 -0
  41. package/dist/esm/video-generation-client.js +147 -8
  42. package/dist/esm/video-generation-client.js.map +1 -1
  43. package/package.json +7 -3
  44. package/src/chat-client.ts +250 -33
  45. package/src/connection-adapters.ts +360 -68
  46. package/src/devtools-noop.ts +205 -0
  47. package/src/devtools.ts +1866 -0
  48. package/src/events.ts +143 -12
  49. package/src/generation-client.ts +177 -10
  50. package/src/generation-types.ts +24 -5
  51. package/src/index.ts +16 -3
  52. package/src/realtime-client.ts +2 -2
  53. package/src/realtime-types.ts +1 -1
  54. package/src/response-stream.ts +42 -0
  55. package/src/sse-parser.ts +9 -7
  56. package/src/sse-utils.ts +8 -0
  57. package/src/tool-types.ts +1 -1
  58. package/src/types.ts +31 -4
  59. package/src/video-generation-client.ts +187 -12
@@ -3,8 +3,8 @@ import {
3
3
  convertSchemaToJsonSchema,
4
4
  generateMessageId,
5
5
  normalizeToUIMessage,
6
- } from '@tanstack/ai'
7
- import { DefaultChatClientEventEmitter } from './events'
6
+ } from '@tanstack/ai/client'
7
+ import { createNoOpChatDevtoolsBridge } from './devtools-noop'
8
8
  import {
9
9
  fetcherToConnectionAdapter,
10
10
  normalizeConnectionAdapter,
@@ -14,12 +14,20 @@ import type {
14
14
  ContentPart,
15
15
  ModelMessage,
16
16
  StreamChunk,
17
- } from '@tanstack/ai'
17
+ } from '@tanstack/ai/client'
18
18
  import type {
19
19
  ConnectionAdapter,
20
20
  SubscribeConnectionAdapter,
21
21
  } from './connection-adapters'
22
- 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'
23
31
  import type {
24
32
  ChatClientOptions,
25
33
  ChatClientState,
@@ -64,8 +72,15 @@ export class ChatClient {
64
72
  private status: ChatClientState = 'ready'
65
73
  private connectionStatus: ConnectionStatus = 'disconnected'
66
74
  private abortController: AbortController | null = null
67
- private readonly events: ChatClientEventEmitter
68
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
69
84
  private currentStreamId: string | null = null
70
85
  private currentMessageId: string | null = null
71
86
  private readonly postStreamActions: Array<() => Promise<void>> = []
@@ -83,6 +98,7 @@ export class ChatClient {
83
98
  private draining = false
84
99
  private sessionGenerating = false
85
100
  private readonly activeRunIds = new Set<string>()
101
+ private devtoolsMounted = false
86
102
 
87
103
  private readonly callbacksRef: {
88
104
  current: {
@@ -116,7 +132,6 @@ export class ChatClient {
116
132
  this.bodyOption = options.body || {}
117
133
  this.forwardedPropsOption = options.forwardedProps || {}
118
134
  this.connection = normalizeConnectionAdapter(resolveTransport(options))
119
- this.events = new DefaultChatClientEventEmitter(this.uniqueId)
120
135
 
121
136
  // Build client tools map
122
137
  this.clientToolsRef = { current: new Map() }
@@ -126,6 +141,11 @@ export class ChatClient {
126
141
  }
127
142
  }
128
143
 
144
+ this.devtoolsBridge = (
145
+ options.devtoolsBridgeFactory ?? createNoOpChatDevtoolsBridge
146
+ )(this.buildDevtoolsBridgeOptions(options.devtools))
147
+ this.events = this.devtoolsBridge.events
148
+
129
149
  this.callbacksRef = {
130
150
  current: {
131
151
  onResponse: options.onResponse || (() => {}),
@@ -200,9 +220,41 @@ export class ChatClient {
200
220
  this.currentStreamId,
201
221
  messageId,
202
222
  content,
223
+ undefined,
203
224
  )
204
225
  }
205
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
+ },
206
258
  onToolCallStateChange: (
207
259
  messageId: string,
208
260
  toolCallId: string,
@@ -239,24 +291,36 @@ export class ChatClient {
239
291
  const clientTool = this.clientToolsRef.current.get(args.toolName)
240
292
  const executeFunc = clientTool?.execute
241
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()
242
300
  // Create and track the execution promise
243
301
  const executionPromise = (async () => {
244
302
  try {
245
303
  const output = await executeFunc(args.input)
246
- await this.addToolResult({
247
- toolCallId: args.toolCallId,
248
- tool: args.toolName,
249
- output,
250
- state: 'output-available',
251
- })
304
+ await this.addToolResultInternal(
305
+ {
306
+ toolCallId: args.toolCallId,
307
+ tool: args.toolName,
308
+ output,
309
+ state: 'output-available',
310
+ },
311
+ runEventContext,
312
+ )
252
313
  } catch (error: any) {
253
- await this.addToolResult({
254
- toolCallId: args.toolCallId,
255
- tool: args.toolName,
256
- output: null,
257
- state: 'output-error',
258
- errorText: error.message,
259
- })
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
+ )
260
324
  } finally {
261
325
  // Remove from pending when complete
262
326
  this.pendingToolExecutions.delete(args.toolCallId)
@@ -273,16 +337,20 @@ export class ChatClient {
273
337
  input: any
274
338
  approvalId: string
275
339
  }) => {
276
- if (this.currentStreamId) {
277
- this.events.approvalRequested(
278
- this.currentStreamId,
279
- this.currentMessageId || '',
280
- args.toolCallId,
281
- args.toolName,
282
- args.input,
283
- args.approvalId,
284
- )
285
- }
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
+ )
286
354
  },
287
355
  onCustomEvent: (
288
356
  eventType: string,
@@ -293,8 +361,15 @@ export class ChatClient {
293
361
  },
294
362
  },
295
363
  })
364
+ }
296
365
 
297
- this.events.clientCreated(this.processor.getMessages().length)
366
+ mountDevtools(): void {
367
+ if (this.devtoolsMounted) {
368
+ return
369
+ }
370
+
371
+ this.devtoolsMounted = true
372
+ this.devtoolsBridge.mountWithTools(this.processor.getMessages().length)
298
373
  }
299
374
 
300
375
  private generateUniqueId(prefix: string): string {
@@ -310,22 +385,26 @@ export class ChatClient {
310
385
  private setStatus(status: ChatClientState): void {
311
386
  this.status = status
312
387
  this.callbacksRef.current.onStatusChange(status)
388
+ this.devtoolsBridge.emitSnapshot()
313
389
  }
314
390
 
315
391
  private setIsSubscribed(isSubscribed: boolean): void {
316
392
  this.isSubscribed = isSubscribed
317
393
  this.callbacksRef.current.onSubscriptionChange(isSubscribed)
394
+ this.devtoolsBridge.emitSnapshot()
318
395
  }
319
396
 
320
397
  private setConnectionStatus(status: ConnectionStatus): void {
321
398
  this.connectionStatus = status
322
399
  this.callbacksRef.current.onConnectionStatusChange(status)
400
+ this.devtoolsBridge.emitSnapshot()
323
401
  }
324
402
 
325
403
  private setSessionGenerating(isGenerating: boolean): void {
326
404
  if (this.sessionGenerating === isGenerating) return
327
405
  this.sessionGenerating = isGenerating
328
406
  this.callbacksRef.current.onSessionGeneratingChange(isGenerating)
407
+ this.devtoolsBridge.emitSnapshot()
329
408
  }
330
409
 
331
410
  private resetSessionGenerating(): void {
@@ -339,6 +418,57 @@ export class ChatClient {
339
418
  this.events.errorChanged(error?.message || null)
340
419
  }
341
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
+
342
472
  private abortSubscriptionLoop(): void {
343
473
  this.subscriptionAbortController?.abort()
344
474
  this.subscriptionAbortController = null
@@ -428,11 +558,12 @@ export class ChatClient {
428
558
  this.setConnectionStatus('connected')
429
559
  }
430
560
  this.callbacksRef.current.onChunk(chunk)
431
- this.processor.processChunk(chunk)
432
561
  if (chunk.type === 'RUN_STARTED') {
433
562
  this.activeRunIds.add(chunk.runId)
434
563
  this.setSessionGenerating(true)
435
564
  }
565
+ this.devtoolsBridge.observeChunk(chunk)
566
+ this.processor.processChunk(chunk)
436
567
  // RUN_FINISHED / RUN_ERROR signal run completion — resolve processing
437
568
  // (redundant if onStreamEnd already resolved it, harmless)
438
569
  if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {
@@ -529,6 +660,7 @@ export class ChatClient {
529
660
  content: string | MultimodalContent,
530
661
  body?: Record<string, any>,
531
662
  ): Promise<void> {
663
+ this.mountDevtools()
532
664
  const emptyMessage = typeof content === 'string' && !content.trim()
533
665
  if (emptyMessage || this.isLoading) {
534
666
  return
@@ -567,6 +699,7 @@ export class ChatClient {
567
699
  * Append a message and stream the response
568
700
  */
569
701
  async append(message: UIMessage | ModelMessage): Promise<void> {
702
+ this.mountDevtools()
570
703
  // Normalize the message to ensure it has id and createdAt
571
704
  const normalizedMessage = normalizeToUIMessage(message, generateMessageId)
572
705
 
@@ -584,6 +717,7 @@ export class ChatClient {
584
717
  // Add to messages
585
718
  const messages = this.processor.getMessages()
586
719
  this.processor.setMessages([...messages, uiMessage])
720
+ this.devtoolsBridge.emitSnapshot()
587
721
 
588
722
  // If stream is in progress, queue the response for after it ends
589
723
  if (this.isLoading) {
@@ -621,6 +755,8 @@ export class ChatClient {
621
755
  // Reset pending tool executions for the new stream
622
756
  this.pendingToolExecutions.clear()
623
757
  let streamCompletedSuccessfully = false
758
+ let activeDevtoolsRunId: string | null = null
759
+ let runTerminalEventEmitted = false
624
760
 
625
761
  try {
626
762
  // Get UIMessages with parts (preserves approval state and client tool results)
@@ -658,6 +794,7 @@ export class ChatClient {
658
794
 
659
795
  // Generate stream ID — assistant message will be created by stream events
660
796
  this.currentStreamId = this.generateUniqueId('stream')
797
+ this.devtoolsBridge.setCurrentStreamId(this.currentStreamId)
661
798
  this.currentMessageId = null
662
799
 
663
800
  // Reset processor stream state for new response — prevents stale
@@ -693,6 +830,19 @@ export class ChatClient {
693
830
  ),
694
831
  forwardedProps: { ...mergedBody },
695
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()
696
846
 
697
847
  // Send through normalized connection (pushes chunks to subscription queue)
698
848
  await this.connection.send(messages, mergedBody, signal, runContext)
@@ -709,6 +859,15 @@ export class ChatClient {
709
859
  // A RUN_ERROR from the stream transitions status to error.
710
860
  // Do not treat this stream as a successful completion.
711
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
+ }
712
871
  return false
713
872
  }
714
873
 
@@ -723,10 +882,27 @@ export class ChatClient {
723
882
  } catch (err) {
724
883
  if (err instanceof Error) {
725
884
  if (err.name === 'AbortError') {
885
+ if (activeDevtoolsRunId) {
886
+ this.devtoolsBridge.emitRunLifecycle(
887
+ 'run:cancelled',
888
+ activeDevtoolsRunId,
889
+ 'cancelled',
890
+ )
891
+ runTerminalEventEmitted = true
892
+ }
726
893
  return false
727
894
  }
728
895
  if (generation === this.streamGeneration) {
729
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
+ }
730
906
  }
731
907
  }
732
908
  } finally {
@@ -735,11 +911,28 @@ export class ChatClient {
735
911
  // clobber the new stream's abortController or isLoading state.
736
912
  if (generation === this.streamGeneration) {
737
913
  this.currentStreamId = null
914
+ this.devtoolsBridge.setCurrentStreamId(null)
738
915
  this.currentMessageId = null
739
916
  this.abortController = null
740
917
  this.setIsLoading(false)
741
918
  this.pendingMessageBody = undefined // Ensure it's cleared even on error
742
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
+
743
936
  // Drain any actions that were queued while the stream was in progress
744
937
  await this.drainPostStreamActions()
745
938
 
@@ -811,7 +1004,7 @@ export class ChatClient {
811
1004
 
812
1005
  // Find the last user message
813
1006
  const lastUserMessageIndex = messages.findLastIndex(
814
- (m: UIMessage) => m.role === 'user',
1007
+ (m) => m.role === 'user',
815
1008
  )
816
1009
 
817
1010
  if (lastUserMessageIndex === -1) return
@@ -825,6 +1018,7 @@ export class ChatClient {
825
1018
 
826
1019
  // Remove all messages after the last user message
827
1020
  this.processor.removeMessagesAfter(lastUserMessageIndex)
1021
+ this.devtoolsBridge.emitSnapshot()
828
1022
 
829
1023
  // Resend
830
1024
  await this.streamResponse()
@@ -857,11 +1051,25 @@ export class ChatClient {
857
1051
  state?: 'output-available' | 'output-error'
858
1052
  errorText?: string
859
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> {
860
1067
  this.events.toolResultAdded(
861
1068
  result.toolCallId,
862
1069
  result.tool,
863
1070
  result.output,
864
1071
  result.state || 'output-available',
1072
+ context,
865
1073
  )
866
1074
 
867
1075
  // Add result via processor
@@ -912,6 +1120,7 @@ export class ChatClient {
912
1120
 
913
1121
  // Add response via processor
914
1122
  this.processor.addToolApprovalResponse(response.id, response.approved)
1123
+ this.devtoolsBridge.emitSnapshot()
915
1124
 
916
1125
  // If stream is in progress, queue continuation check for after it ends
917
1126
  if (this.isLoading) {
@@ -1051,6 +1260,7 @@ export class ChatClient {
1051
1260
  */
1052
1261
  setMessagesManually(messages: Array<UIMessage>): void {
1053
1262
  this.processor.setMessages(messages)
1263
+ this.devtoolsBridge.emitSnapshot()
1054
1264
  }
1055
1265
 
1056
1266
  /**
@@ -1104,7 +1314,7 @@ export class ChatClient {
1104
1314
  }
1105
1315
  // Replace each slot independently so callers can update one without
1106
1316
  // wiping the other. (Passing `undefined` for either field is a "leave
1107
- // unchanged" signal — to clear a slot, pass an empty object `{}`.)
1317
+ // unchanged" signal - to clear a slot, pass an empty object `{}`.)
1108
1318
  if (options.body !== undefined) {
1109
1319
  this.bodyOption = options.body
1110
1320
  }
@@ -1116,6 +1326,7 @@ export class ChatClient {
1116
1326
  for (const tool of options.tools) {
1117
1327
  this.clientToolsRef.current.set(tool.name, tool)
1118
1328
  }
1329
+ this.devtoolsBridge.notifyToolsChanged()
1119
1330
  }
1120
1331
  if (options.onResponse !== undefined) {
1121
1332
  this.callbacksRef.current.onResponse = options.onResponse
@@ -1145,4 +1356,10 @@ export class ChatClient {
1145
1356
  this.callbacksRef.current.onCustomEvent = options.onCustomEvent
1146
1357
  }
1147
1358
  }
1359
+
1360
+ dispose(): void {
1361
+ this.unsubscribe()
1362
+ this.devtoolsBridge.dispose()
1363
+ this.devtoolsMounted = false
1364
+ }
1148
1365
  }