@assistant-ui/core 0.3.1 → 0.3.2

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 (135) hide show
  1. package/dist/internal.d.ts +1 -2
  2. package/dist/internal.js +1 -2
  3. package/dist/react/AssistantProvider.d.ts +1 -1
  4. package/dist/react/AssistantProvider.d.ts.map +1 -1
  5. package/dist/react/AssistantProvider.js +35 -9
  6. package/dist/react/AssistantProvider.js.map +1 -1
  7. package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
  8. package/dist/react/AssistantRuntimeProvider.js +1 -1
  9. package/dist/react/RuntimeAdapter.js +2 -2
  10. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  11. package/dist/react/adapters/LocalStorageThreadListAdapter.js +17 -33
  12. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  13. package/dist/react/client/DataRenderers.js +1 -1
  14. package/dist/react/client/Interactables.js +1 -1
  15. package/dist/react/client/Tools.js +2 -2
  16. package/dist/react/index.d.ts +2 -1
  17. package/dist/react/index.js +2 -1
  18. package/dist/react/interactables-legacy/Interactables.js +1 -1
  19. package/dist/react/interactables-legacy/useAssistantInteractable.js +1 -1
  20. package/dist/react/interactables-legacy/useInteractableState.js +2 -2
  21. package/dist/react/model-context/useAssistantContext.js +1 -1
  22. package/dist/react/model-context/useAssistantDataUI.js +2 -2
  23. package/dist/react/model-context/useAssistantInstructions.js +2 -2
  24. package/dist/react/model-context/useAssistantTool.js +2 -2
  25. package/dist/react/model-context/useAssistantToolUI.js +2 -2
  26. package/dist/react/model-context/useAuiToolOverrides.js +1 -1
  27. package/dist/react/model-context/useInlineRender.js +1 -1
  28. package/dist/react/model-context/useInteractable.js +1 -1
  29. package/dist/react/model-context/useInteractableState.js +2 -2
  30. package/dist/react/model-context/useInteractableVersions.js +2 -2
  31. package/dist/react/model-context/useToolArgsStatus.js +2 -2
  32. package/dist/react/primitive-hooks/useActionBarCopy.js +2 -2
  33. package/dist/react/primitive-hooks/useActionBarEdit.js +2 -2
  34. package/dist/react/primitive-hooks/useActionBarFeedback.js +2 -2
  35. package/dist/react/primitive-hooks/useActionBarReload.js +2 -2
  36. package/dist/react/primitive-hooks/useActionBarSpeak.js +2 -2
  37. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +2 -2
  38. package/dist/react/primitive-hooks/useBranchPickerNext.js +2 -2
  39. package/dist/react/primitive-hooks/useBranchPickerPrevious.js +2 -2
  40. package/dist/react/primitive-hooks/useComposerAddAttachment.js +2 -2
  41. package/dist/react/primitive-hooks/useComposerCancel.js +2 -2
  42. package/dist/react/primitive-hooks/useComposerDictate.js +2 -2
  43. package/dist/react/primitive-hooks/useComposerSend.js +2 -2
  44. package/dist/react/primitive-hooks/useEditComposerCancel.js +2 -2
  45. package/dist/react/primitive-hooks/useEditComposerSend.js +2 -2
  46. package/dist/react/primitive-hooks/useMessageBranching.js +2 -2
  47. package/dist/react/primitive-hooks/useMessageReload.js +2 -2
  48. package/dist/react/primitive-hooks/useSuggestionTrigger.js +2 -2
  49. package/dist/react/primitive-hooks/useThreadListItemArchive.js +2 -2
  50. package/dist/react/primitive-hooks/useThreadListItemDelete.js +2 -2
  51. package/dist/react/primitive-hooks/useThreadListItemTrigger.js +2 -2
  52. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +2 -2
  53. package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -2
  54. package/dist/react/primitive-hooks/useThreadListNew.js +2 -2
  55. package/dist/react/primitive-hooks/useThreadMessageIds.js +1 -1
  56. package/dist/react/primitive-hooks/useVoice.js +2 -2
  57. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +2 -2
  58. package/dist/react/primitives/composer/ComposerAttachments.js +2 -2
  59. package/dist/react/primitives/composer/ComposerIf.js +1 -1
  60. package/dist/react/primitives/composer/ComposerQueue.js +1 -1
  61. package/dist/react/primitives/generativeUI/GenerativeUI.js +2 -2
  62. package/dist/react/primitives/message/MessageAttachments.js +2 -2
  63. package/dist/react/primitives/message/MessageGroupedParts.js +1 -1
  64. package/dist/react/primitives/message/MessageParts.js +2 -2
  65. package/dist/react/primitives/message/MessageQuote.js +2 -2
  66. package/dist/react/primitives/part/PartMessages.js +2 -2
  67. package/dist/react/primitives/thread/ThreadMessages.js +2 -2
  68. package/dist/react/primitives/thread/ThreadSuggestions.js +2 -2
  69. package/dist/react/primitives/threadList/ThreadListItems.js +2 -2
  70. package/dist/react/primitives/threadListItem/ThreadListItemTitle.js +1 -1
  71. package/dist/react/providers/AttachmentByIndexProvider.js +1 -1
  72. package/dist/react/providers/ChainOfThoughtByIndicesProvider.js +1 -1
  73. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +1 -1
  74. package/dist/react/providers/MessageByIdProvider.js +1 -1
  75. package/dist/react/providers/MessageByIndexProvider.js +1 -1
  76. package/dist/react/providers/PartByIndexProvider.js +1 -1
  77. package/dist/react/providers/QueueItemByIndexProvider.js +1 -1
  78. package/dist/react/providers/ReadonlyThreadProvider.js +2 -2
  79. package/dist/react/providers/SuggestionByIndexProvider.js +1 -1
  80. package/dist/react/providers/TextMessagePartProvider.js +2 -2
  81. package/dist/react/providers/ThreadListItemByIndexProvider.js +1 -1
  82. package/dist/react/providers/ThreadListItemRuntimeProvider.js +1 -1
  83. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  84. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +14 -13
  85. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  86. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
  87. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  88. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +22 -12
  89. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  90. package/dist/react/runtimes/RuntimeAdapterProvider.js +1 -1
  91. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +9 -23
  92. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  93. package/dist/react/runtimes/external-message-converter.js +1 -1
  94. package/dist/react/runtimes/useExternalStoreRuntime.js +1 -1
  95. package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -1
  96. package/dist/react/runtimes/useLocalRuntime.js +1 -1
  97. package/dist/react/runtimes/useRemoteThreadListRuntime.js +7 -13
  98. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  99. package/dist/react/utils/useJSONEqual.js +1 -1
  100. package/dist/runtime/utils/thread-message-like.js +2 -2
  101. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  102. package/dist/runtimes/remote-thread-list/types.d.ts +5 -4
  103. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  104. package/dist/store/clients/chain-of-thought-client.js +1 -1
  105. package/dist/store/clients/model-context-client.js +1 -1
  106. package/dist/store/clients/no-op-composer-client.js +1 -1
  107. package/dist/store/clients/suggestions.js +2 -2
  108. package/dist/store/clients/thread-message-client.js +2 -2
  109. package/dist/store/runtime-clients/composer-runtime-client.js +2 -2
  110. package/dist/store/runtime-clients/message-runtime-client.js +2 -2
  111. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +2 -2
  112. package/dist/store/runtime-clients/thread-list-runtime-client.js +2 -2
  113. package/dist/store/runtime-clients/thread-runtime-client.js +2 -2
  114. package/dist/store/scopes/thread.d.ts +1 -1
  115. package/dist/store/scopes/thread.d.ts.map +1 -1
  116. package/dist/utils/data-url.js +2 -2
  117. package/dist/utils/data-url.js.map +1 -1
  118. package/package.json +4 -4
  119. package/src/internal.ts +0 -7
  120. package/src/react/AssistantProvider.tsx +28 -15
  121. package/src/react/AssistantRuntimeProvider.test.tsx +101 -0
  122. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +28 -7
  123. package/src/react/index.ts +4 -0
  124. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +16 -13
  125. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +53 -10
  126. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +7 -5
  127. package/src/react/runtimes/useRemoteThreadListRuntime.ts +9 -15
  128. package/src/runtime/utils/thread-message-like.ts +2 -2
  129. package/src/runtimes/remote-thread-list/types.ts +5 -4
  130. package/src/store/scopes/thread.ts +1 -1
  131. package/src/tests/remote-thread-list-reactive-threadId.test.ts +0 -92
  132. package/src/tests/thread-message-like.test.ts +15 -0
  133. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +293 -0
  134. package/src/utils/data-url.test.ts +26 -2
  135. package/src/utils/data-url.ts +2 -2
@@ -1,10 +1,10 @@
1
1
  //#region src/utils/data-url.ts
2
2
  const httpUrlPattern = /^https?:\/\//i;
3
3
  function parseDataUrl(value) {
4
- const match = value.match(/^data:([^;,]+)(?:;[^;,]+)*;base64,(.+)$/);
4
+ const match = value.match(/^data:([^;,]+)(?:;[^;,]+)*;base64,(.*)$/i);
5
5
  if (!match) return null;
6
6
  return {
7
- mimeType: match[1],
7
+ mimeType: match[1].toLowerCase(),
8
8
  data: match[2]
9
9
  };
10
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"data-url.js","names":["httpUrlPattern","parseDataUrl","value","mimeType","data","match"],"sources":["../../src/utils/data-url.ts"],"sourcesContent":["export const httpUrlPattern = /^https?:\\/\\//i;\n\nexport function parseDataUrl(\n value: string,\n): { mimeType: string; data: string } | null {\n const match = value.match(/^data:([^;,]+)(?:;[^;,]+)*;base64,(.+)$/);\n if (!match) return null;\n return { mimeType: match[1]!, data: match[2]! };\n}\n"],"mappings":";AAAA,MAAaA,iBAAiB;AAE9B,SAAgBC,aACdC,OAC2C;CAC3C,MAAMG,QAAQH,MAAMG,MAAM,yCAAyC;CACnE,IAAI,CAACA,OAAO,OAAO;CACnB,OAAO;EAAEF,UAAUE,MAAM;EAAKD,MAAMC,MAAM;CAAI;AAChD"}
1
+ {"version":3,"file":"data-url.js","names":["httpUrlPattern","parseDataUrl","value","mimeType","data","match","toLowerCase"],"sources":["../../src/utils/data-url.ts"],"sourcesContent":["export const httpUrlPattern = /^https?:\\/\\//i;\n\nexport function parseDataUrl(\n value: string,\n): { mimeType: string; data: string } | null {\n const match = value.match(/^data:([^;,]+)(?:;[^;,]+)*;base64,(.*)$/i);\n if (!match) return null;\n return { mimeType: match[1]!.toLowerCase(), data: match[2]! };\n}\n"],"mappings":";AAAA,MAAaA,iBAAiB;AAE9B,SAAgBC,aACdC,OAC2C;CAC3C,MAAMG,QAAQH,MAAMG,MAAM,0CAA0C;CACpE,IAAI,CAACA,OAAO,OAAO;CACnB,OAAO;EAAEF,UAAUE,MAAM,EAAE,CAAEC,YAAY;EAAGF,MAAMC,MAAM;CAAI;AAC9D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/core",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Framework-agnostic core runtime for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "sideEffects": false,
61
61
  "dependencies": {
62
- "assistant-stream": "^0.3.30",
62
+ "assistant-stream": "^0.3.31",
63
63
  "nanoid": "^6.0.0"
64
64
  },
65
65
  "optionalDevDependencies": {
@@ -95,8 +95,8 @@
95
95
  "react-dom": "^19.2.8",
96
96
  "vitest": "^4.1.10",
97
97
  "zustand": "^5.0.14",
98
- "@assistant-ui/store": "0.3.1",
99
- "@assistant-ui/tap": "0.9.7",
98
+ "@assistant-ui/store": "0.3.2",
99
+ "@assistant-ui/tap": "0.9.8",
100
100
  "@assistant-ui/vite": "0.0.11",
101
101
  "@assistant-ui/x-buildutils": "0.0.20",
102
102
  "assistant-cloud": "0.1.37"
package/src/internal.ts CHANGED
@@ -58,12 +58,5 @@ export { isJSONValue, isRecord } from "./utils/json/is-json";
58
58
  // outbound part conversion lives in one place.
59
59
  export { httpUrlPattern, parseDataUrl } from "./utils/data-url";
60
60
 
61
- // Runtime extras helper for external-store adapters. Internal because the
62
- // tap-native runtime path replaces the `thread.extras` side-channel it wraps.
63
- export {
64
- createRuntimeExtras,
65
- type RuntimeExtras,
66
- } from "./react/runtimes/createRuntimeExtras";
67
-
68
61
  export * from "./runtime/internal";
69
62
  export * from "./runtimes/internal";
@@ -19,24 +19,37 @@ export type AssistantProviderBaseProps = PropsWithChildren<{
19
19
  aui?: AssistantClient | null;
20
20
  }>;
21
21
 
22
+ const AssistantProviderInner: FC<
23
+ PropsWithChildren<{ runtime: AssistantRuntime }>
24
+ > = ({ runtime, children }) => {
25
+ // The runtime has a stable identity but mutates in place: its options are
26
+ // pushed in by an unconditional effect inside <RenderComponent />, so that
27
+ // element must be re-created every commit for React to re-render it and
28
+ // re-run the effect. React Compiler caches <RenderComponent /> on the
29
+ // stable RenderComponent type, which silences the effect and stops option
30
+ // changes (e.g. unstable_enableMessageQueue) from reaching the runtime.
31
+ "use no memo";
32
+ const aui = useAui({ threads: RuntimeAdapter(runtime) });
33
+ const RenderComponent = getRenderComponent(runtime);
34
+ return (
35
+ <AuiProvider value={aui}>
36
+ {RenderComponent && <RenderComponent />}
37
+ {children}
38
+ </AuiProvider>
39
+ );
40
+ };
41
+
22
42
  export const AssistantProviderBase: FC<AssistantProviderBaseProps> = memo(
23
- ({ runtime, aui: parent = null, children }) => {
24
- // The runtime has a stable identity but mutates in place: its options are
25
- // pushed in by an unconditional effect inside <RenderComponent />, so that
26
- // element must be re-created every commit for React to re-render it and
27
- // re-run the effect. React Compiler caches <RenderComponent /> on the
28
- // stable RenderComponent type, which silences the effect and stops option
29
- // changes (e.g. unstable_enableMessageQueue) from reaching the runtime.
30
- "use no memo";
31
- const aui = useAui({ threads: RuntimeAdapter(runtime) }, { parent });
32
- const RenderComponent = getRenderComponent(runtime);
43
+ ({ runtime, aui = null, children }) => {
33
44
  const inner = (
34
- <AuiProvider value={aui}>
35
- {RenderComponent && <RenderComponent />}
45
+ <AssistantProviderInner runtime={runtime}>
36
46
  {children}
37
- </AuiProvider>
47
+ </AssistantProviderInner>
48
+ );
49
+ return aui ? (
50
+ <AuiProvider value={aui}>{inner}</AuiProvider>
51
+ ) : (
52
+ <AuiProvider value={null}>{inner}</AuiProvider>
38
53
  );
39
- if (!parent) return inner;
40
- return <AuiProvider value={parent}>{inner}</AuiProvider>;
41
54
  },
42
55
  );
@@ -0,0 +1,101 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useEffect } from "react";
5
+ import { cleanup, render } from "@testing-library/react";
6
+ import { afterEach, describe, expect, it } from "vitest";
7
+ import { resource } from "@assistant-ui/tap";
8
+ import { AuiProvider, useAui, type AssistantClient } from "@assistant-ui/store";
9
+ import type { ChatModelAdapter } from "../runtime/utils/chat-model-adapter";
10
+ import { AssistantRuntimeProvider } from "./AssistantRuntimeProvider";
11
+ import { useLocalRuntime } from "./runtimes/useLocalRuntime";
12
+
13
+ type AnyClient = Record<string, any>;
14
+
15
+ const chatModel: ChatModelAdapter = {
16
+ run: async () => ({ content: [] }),
17
+ };
18
+
19
+ const makeCounterClient = (log?: string[]) => {
20
+ const useCounterClient = () => {
21
+ useEffect(() => {
22
+ log?.push("parent tap effect");
23
+ }, []);
24
+ return { getState: () => ({ count: 0 }) };
25
+ };
26
+ return resource(useCounterClient);
27
+ };
28
+
29
+ afterEach(() => {
30
+ cleanup();
31
+ });
32
+
33
+ describe("AssistantRuntimeProvider aui composition", () => {
34
+ it("isolates from ambient context by default", () => {
35
+ let aui!: AnyClient;
36
+ const Consumer = () => {
37
+ aui = useAui();
38
+ return null;
39
+ };
40
+
41
+ const Ambient = ({ children }: { children: ReactNode }) => {
42
+ const ambient = useAui({
43
+ parentScope: makeCounterClient()(),
44
+ } as unknown as useAui.Props);
45
+ return <AuiProvider value={ambient}>{children}</AuiProvider>;
46
+ };
47
+
48
+ const App = () => {
49
+ const runtime = useLocalRuntime(chatModel);
50
+ return (
51
+ <AssistantRuntimeProvider runtime={runtime}>
52
+ <Consumer />
53
+ </AssistantRuntimeProvider>
54
+ );
55
+ };
56
+
57
+ render(
58
+ <Ambient>
59
+ <App />
60
+ </Ambient>,
61
+ );
62
+
63
+ expect(aui.threads).toBeDefined();
64
+ expect(() => aui.parentScope.getState()).toThrow(
65
+ /The current scope does not have a "parentScope" property/,
66
+ );
67
+ });
68
+
69
+ it("inherits a prop-passed parent and mounts its effects", () => {
70
+ const log: string[] = [];
71
+ let aui!: AnyClient;
72
+ const Consumer = () => {
73
+ aui = useAui();
74
+ useEffect(() => {
75
+ log.push("consumer effect");
76
+ }, []);
77
+ return null;
78
+ };
79
+
80
+ const App = ({ parent }: { parent: AssistantClient }) => {
81
+ const runtime = useLocalRuntime(chatModel);
82
+ return (
83
+ <AssistantRuntimeProvider runtime={runtime} aui={parent}>
84
+ <Consumer />
85
+ </AssistantRuntimeProvider>
86
+ );
87
+ };
88
+
89
+ const Root = () => {
90
+ const parent = useAui({
91
+ parentScope: makeCounterClient(log)(),
92
+ } as unknown as useAui.Props);
93
+ return <App parent={parent} />;
94
+ };
95
+
96
+ render(<Root />);
97
+
98
+ expect(aui.parentScope.getState()).toEqual({ count: 0 });
99
+ expect(log).toEqual(["parent tap effect", "consumer effect"]);
100
+ });
101
+ });
@@ -1,5 +1,12 @@
1
1
  import { type AssistantStream, createAssistantStream } from "assistant-stream";
2
- import { type FC, type PropsWithChildren, useMemo } from "react";
2
+ import {
3
+ type FC,
4
+ type PropsWithChildren,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
3
10
  import { useAui } from "@assistant-ui/store";
4
11
  import type {
5
12
  RemoteThreadInitializeResponse,
@@ -276,22 +283,26 @@ export const parseStoredMessageRepository = (
276
283
 
277
284
  class AsyncStorageHistoryAdapter implements ThreadHistoryAdapter {
278
285
  private storage: AsyncStorageLike;
279
- private aui: ReturnType<typeof useAui>;
286
+ private getAui: () => ReturnType<typeof useAui>;
280
287
  private prefix: string;
281
288
  private mutationQueue: KeyedMutationQueue;
282
289
 
283
290
  constructor(
284
291
  storage: AsyncStorageLike,
285
- aui: ReturnType<typeof useAui>,
292
+ getAui: () => ReturnType<typeof useAui>,
286
293
  prefix: string,
287
294
  mutationQueue: KeyedMutationQueue,
288
295
  ) {
289
296
  this.storage = storage;
290
- this.aui = aui;
297
+ this.getAui = getAui;
291
298
  this.prefix = prefix;
292
299
  this.mutationQueue = mutationQueue;
293
300
  }
294
301
 
302
+ private get aui(): ReturnType<typeof useAui> {
303
+ return this.getAui();
304
+ }
305
+
295
306
  private _messagesKey(remoteId: string) {
296
307
  return `${this.prefix}messages:${remoteId}`;
297
308
  }
@@ -334,9 +345,19 @@ const createHistoryProvider = (
334
345
  ): FC<PropsWithChildren> => {
335
346
  const Provider: FC<PropsWithChildren> = ({ children }) => {
336
347
  const aui = useAui();
337
- const history = useMemo(
338
- () => new AsyncStorageHistoryAdapter(storage, aui, prefix, mutationQueue),
339
- [aui],
348
+ // Not useEffectEvent: history adapter methods run during render (SSR load).
349
+ const auiRef = useRef(aui);
350
+ useEffect(() => {
351
+ auiRef.current = aui;
352
+ });
353
+ const [history] = useState(
354
+ () =>
355
+ new AsyncStorageHistoryAdapter(
356
+ storage,
357
+ () => auiRef.current,
358
+ prefix,
359
+ mutationQueue,
360
+ ),
340
361
  );
341
362
  const adapters = useMemo(() => ({ history }), [history]);
342
363
 
@@ -226,6 +226,10 @@ export {
226
226
  type StreamingTimingOptions,
227
227
  type StreamingTimingState,
228
228
  } from "./runtimes/useStreamingTiming";
229
+ export {
230
+ createRuntimeExtras,
231
+ type RuntimeExtras,
232
+ } from "./runtimes/createRuntimeExtras";
229
233
  export { RemoteThreadListHookInstanceManager } from "./runtimes/RemoteThreadListHookInstanceManager";
230
234
  export { RemoteThreadListThreadListRuntimeCore } from "./runtimes/RemoteThreadListThreadListRuntimeCore";
231
235
  export { useRemoteThreadListRuntime } from "./runtimes/useRemoteThreadListRuntime";
@@ -4,6 +4,7 @@ import {
4
4
  useCallback,
5
5
  useRef,
6
6
  useEffect,
7
+ useEffectEvent,
7
8
  useLayoutEffect,
8
9
  memo,
9
10
  type PropsWithChildren,
@@ -142,23 +143,25 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
142
143
  }
143
144
  }, [threadBinding]);
144
145
 
145
- useEffect(() => {
146
- hasInitializedRef.current = false;
147
- return runtime.threads.main.unstable_on("initialize", () => {
148
- if (hasInitializedRef.current) return;
146
+ const handleInitialize = useEffectEvent(() => {
147
+ if (hasInitializedRef.current) return;
149
148
 
150
- const state = aui.threadListItem.getState();
151
- if (state.status !== "new") return;
152
- hasInitializedRef.current = true;
149
+ const state = aui.threadListItem.getState();
150
+ if (state.status !== "new") return;
151
+ hasInitializedRef.current = true;
153
152
 
154
- initPromiseRef.current = aui.threadListItem.initialize();
153
+ initPromiseRef.current = aui.threadListItem.initialize();
155
154
 
156
- const dispose = runtime.thread.unstable_on("runEnd", () => {
157
- dispose();
158
- aui.threadListItem.generateTitle();
159
- });
155
+ const dispose = runtime.thread.unstable_on("runEnd", () => {
156
+ dispose();
157
+ aui.threadListItem.generateTitle();
160
158
  });
161
- }, [runtime, aui]);
159
+ });
160
+
161
+ useEffect(() => {
162
+ hasInitializedRef.current = false;
163
+ return runtime.threads.main.unstable_on("initialize", handleInitialize);
164
+ }, [runtime]);
162
165
 
163
166
  return <>{children}</>;
164
167
  };
@@ -200,6 +200,10 @@ export class RemoteThreadListThreadListRuntimeCore
200
200
 
201
201
  const adapterChanged =
202
202
  this._options !== undefined && this._options.adapter !== options.adapter;
203
+ const controlledThreadIdChanged =
204
+ this._initialThreadLoaded &&
205
+ this._options !== undefined &&
206
+ this._options.threadId !== options.threadId;
203
207
 
204
208
  this._options = options;
205
209
 
@@ -219,15 +223,25 @@ export class RemoteThreadListThreadListRuntimeCore
219
223
  cursor: undefined,
220
224
  });
221
225
  }
226
+
227
+ if (controlledThreadIdChanged) {
228
+ this._switchToThreadFromProp(options.threadId).catch(() => {});
229
+ }
222
230
  }
223
231
 
224
232
  public __internal_load() {
225
233
  this.getLoadThreadsPromise(); // begin loading on initial bind
234
+ if (this._initialThreadLoaded) return;
235
+ this._initialThreadLoaded = true;
236
+
226
237
  const startThreadId =
227
238
  this._options.threadId ?? this._options.initialThreadId;
228
- if (!this._initialThreadLoaded && startThreadId) {
229
- this._initialThreadLoaded = true;
230
- this.switchToThread(startThreadId).catch(() => {});
239
+ if (startThreadId !== undefined) {
240
+ const switchTask =
241
+ this._options.threadId !== undefined
242
+ ? this._switchToThreadFromProp(startThreadId)
243
+ : this.switchToThread(startThreadId);
244
+ switchTask.catch(() => {});
231
245
  }
232
246
  }
233
247
 
@@ -279,11 +293,13 @@ export class RemoteThreadListThreadListRuntimeCore
279
293
 
280
294
  private _lastNotifiedThreadId: string | undefined = undefined;
281
295
 
282
- private _notifyThreadIdChange() {
296
+ private _notifyThreadIdChange(emit = true) {
283
297
  const threadId = this._mainThreadRemoteId;
284
298
  if (this._lastNotifiedThreadId === threadId) return;
285
299
  this._lastNotifiedThreadId = threadId;
286
- this._options.onThreadIdChange?.(threadId);
300
+ if (emit) {
301
+ this._options.onThreadIdChange?.(threadId);
302
+ }
287
303
  }
288
304
 
289
305
  public getMainThreadRuntimeCore() {
@@ -312,9 +328,22 @@ export class RemoteThreadListThreadListRuntimeCore
312
328
  public switchToThread(
313
329
  threadIdOrRemoteId: string,
314
330
  options?: { unarchive?: boolean },
331
+ ): Promise<void> {
332
+ return this._startSwitchToThread(threadIdOrRemoteId, options, true);
333
+ }
334
+
335
+ private _startSwitchToThread(
336
+ threadIdOrRemoteId: string,
337
+ options: { unarchive?: boolean } | undefined,
338
+ emitThreadIdChange: boolean,
315
339
  ): Promise<void> {
316
340
  const generation = ++this._switchGeneration;
317
- const task = this._switchToThread(threadIdOrRemoteId, options, generation);
341
+ const task = this._switchToThread(
342
+ threadIdOrRemoteId,
343
+ options,
344
+ generation,
345
+ emitThreadIdChange,
346
+ );
318
347
  this._switchTask = task;
319
348
  return task;
320
349
  }
@@ -323,6 +352,7 @@ export class RemoteThreadListThreadListRuntimeCore
323
352
  threadIdOrRemoteId: string,
324
353
  options: { unarchive?: boolean } | undefined,
325
354
  generation: number,
355
+ emitThreadIdChange: boolean,
326
356
  ): Promise<void> {
327
357
  let data = this.getItemById(threadIdOrRemoteId);
328
358
 
@@ -418,17 +448,30 @@ export class RemoteThreadListThreadListRuntimeCore
418
448
  this._mainThreadId = data.id;
419
449
 
420
450
  this._notifySubscribers();
421
- this._notifyThreadIdChange();
451
+ this._notifyThreadIdChange(emitThreadIdChange);
422
452
  }
423
453
 
424
454
  public switchToNewThread(): Promise<void> {
455
+ return this._startSwitchToNewThread(true);
456
+ }
457
+
458
+ private _switchToThreadFromProp(threadId: string | undefined): Promise<void> {
459
+ return threadId !== undefined
460
+ ? this._startSwitchToThread(threadId, undefined, false)
461
+ : this._startSwitchToNewThread(false);
462
+ }
463
+
464
+ private _startSwitchToNewThread(emitThreadIdChange: boolean): Promise<void> {
425
465
  const generation = ++this._switchGeneration;
426
- const task = this._switchToNewThread(generation);
466
+ const task = this._switchToNewThread(generation, emitThreadIdChange);
427
467
  this._switchTask = task;
428
468
  return task;
429
469
  }
430
470
 
431
- private async _switchToNewThread(generation: number): Promise<void> {
471
+ private async _switchToNewThread(
472
+ generation: number,
473
+ emitThreadIdChange: boolean,
474
+ ): Promise<void> {
432
475
  // an initialization transaction is in progress, wait for it to settle
433
476
  while (
434
477
  this._state.baseValue.newThreadId !== undefined &&
@@ -467,7 +510,7 @@ export class RemoteThreadListThreadListRuntimeCore
467
510
  });
468
511
  }
469
512
 
470
- return this._switchToThread(id, undefined, generation);
513
+ return this._switchToThread(id, undefined, generation, emitThreadIdChange);
471
514
  }
472
515
 
473
516
  public initialize = async (threadId: string) => {
@@ -22,18 +22,18 @@ const globalPersistence = new WeakMap<
22
22
 
23
23
  class AssistantCloudThreadHistoryAdapter implements ThreadHistoryAdapter {
24
24
  private cloudRef: RefObject<AssistantCloud>;
25
- private auiRef: RefObject<AssistantClient>;
25
+ private getAui: () => AssistantClient;
26
26
 
27
27
  constructor(
28
28
  cloudRef: RefObject<AssistantCloud>,
29
- auiRef: RefObject<AssistantClient>,
29
+ getAui: () => AssistantClient,
30
30
  ) {
31
31
  this.cloudRef = cloudRef;
32
- this.auiRef = auiRef;
32
+ this.getAui = getAui;
33
33
  }
34
34
 
35
35
  private get aui(): AssistantClient {
36
- return this.auiRef.current;
36
+ return this.getAui();
37
37
  }
38
38
 
39
39
  private get _persistence(): CloudMessagePersistence {
@@ -816,12 +816,14 @@ export function useAssistantCloudThreadHistoryAdapter(
816
816
  cloudRef: RefObject<AssistantCloud>,
817
817
  ): ThreadHistoryAdapter {
818
818
  const aui = useAui();
819
+ // Not useEffectEvent: history adapter methods run during render (SSR load).
819
820
  const auiRef = useRef(aui);
820
821
  useEffect(() => {
821
822
  auiRef.current = aui;
822
823
  });
823
824
  const [adapter] = useState(
824
- () => new AssistantCloudThreadHistoryAdapter(cloudRef, auiRef),
825
+ () =>
826
+ new AssistantCloudThreadHistoryAdapter(cloudRef, () => auiRef.current),
825
827
  );
826
828
  return adapter;
827
829
  }
@@ -51,8 +51,7 @@ export const useRemoteThreadListRuntime = (
51
51
  const runtimeHookRef = useRef(options.runtimeHook);
52
52
  runtimeHookRef.current = options.runtimeHook;
53
53
 
54
- // threadId/initialThreadId only affect the constructor; capture once via ref
55
- const startThreadIdRef = useRef(options.threadId ?? options.initialThreadId);
54
+ const initialThreadIdRef = useRef(options.initialThreadId);
56
55
 
57
56
  const stableRuntimeHook = useCallback(() => {
58
57
  return runtimeHookRef.current();
@@ -66,11 +65,17 @@ export const useRemoteThreadListRuntime = (
66
65
  () => ({
67
66
  adapter: options.adapter,
68
67
  allowNesting: options.allowNesting,
69
- initialThreadId: startThreadIdRef.current,
68
+ threadId: options.threadId,
69
+ initialThreadId: initialThreadIdRef.current,
70
70
  runtimeHook: stableRuntimeHook,
71
71
  onThreadIdChange,
72
72
  }),
73
- [options.adapter, options.allowNesting, stableRuntimeHook],
73
+ [
74
+ options.adapter,
75
+ options.allowNesting,
76
+ options.threadId,
77
+ stableRuntimeHook,
78
+ ],
74
79
  );
75
80
 
76
81
  const aui = useAui();
@@ -91,16 +96,5 @@ export const useRemoteThreadListRuntime = (
91
96
 
92
97
  const runtime = useRemoteThreadListRuntimeImpl(stableOptions);
93
98
 
94
- const prevThreadIdRef = useRef(options.threadId);
95
- useEffect(() => {
96
- if (options.threadId === prevThreadIdRef.current) return;
97
- prevThreadIdRef.current = options.threadId;
98
- if (options.threadId) {
99
- runtime.threads.switchToThread(options.threadId).catch(() => {});
100
- } else {
101
- runtime.threads.switchToNewThread().catch(() => {});
102
- }
103
- }, [runtime, options.threadId]);
104
-
105
99
  return runtime;
106
100
  };
@@ -131,12 +131,12 @@ export const fromThreadMessageLike = (
131
131
  }: ImageMessagePart): ImageMessagePart | null => {
132
132
  if (typeof image !== "string") return null;
133
133
  const dataUri = image.match(
134
- /^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)$/,
134
+ /^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)$/i,
135
135
  );
136
136
  if (dataUri) {
137
137
  return { ...rest, image };
138
138
  }
139
- if (/^(https:\/\/|blob:)/.test(image)) {
139
+ if (/^(https:\/\/|blob:)/i.test(image)) {
140
140
  return { ...rest, image };
141
141
  }
142
142
  console.warn(`Invalid image data format detected`);
@@ -78,10 +78,11 @@ export type RemoteThreadListOptions = {
78
78
  threadId?: string | undefined;
79
79
 
80
80
  /**
81
- * Called whenever the active thread's canonical (remote) ID changes, so the
82
- * value can be treated as a managed/controlled variable (e.g. synced to a
83
- * URL query param). Together with `threadId` this forms the controlled
84
- * pattern: `threadId` in, `onThreadIdChange` out.
81
+ * Called whenever the runtime changes the active thread's canonical (remote)
82
+ * ID, so the value can be treated as a managed/controlled variable (e.g.
83
+ * synced to a URL query param). Changes initiated by the controlled
84
+ * `threadId` option are not echoed back. Together these options form the
85
+ * controlled pattern: `threadId` in, `onThreadIdChange` out.
85
86
  *
86
87
  * Only the settled remote ID is emitted: while a freshly created thread is
87
88
  * still optimistic (no remote ID yet) the value is `undefined`, and the real
@@ -108,7 +108,7 @@ export type ThreadMethods = {
108
108
  * @param initialMessages - Optional array of initial messages to populate the thread
109
109
  */
110
110
  reset(initialMessages?: readonly ThreadMessageLike[]): void;
111
- importExternalState?(state: unknown): void;
111
+ importExternalState(state: unknown): void;
112
112
  message(selector: { id: string } | { index: number }): MessageMethods;
113
113
  /** @deprecated This API is still under active development and might change without notice. */
114
114
  stopSpeaking(): void;