@assistant-ui/core 0.3.18 → 0.3.19

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 (274) hide show
  1. package/dist/adapters/speech.d.ts.map +1 -1
  2. package/dist/adapters/speech.js +13 -3
  3. package/dist/adapters/speech.js.map +1 -1
  4. package/dist/adapters/thread-history.d.ts +4 -0
  5. package/dist/adapters/thread-history.d.ts.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal.d.ts +1 -1
  9. package/dist/model-context/frame/host.d.ts.map +1 -1
  10. package/dist/model-context/frame/host.js +26 -11
  11. package/dist/model-context/frame/host.js.map +1 -1
  12. package/dist/model-context/interactable-composer-metadata.js +11 -9
  13. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  14. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  15. package/dist/react/adapters/LocalStorageThreadListAdapter.js +45 -7
  16. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  17. package/dist/react/client/DataRenderers.d.ts.map +1 -1
  18. package/dist/react/client/DataRenderers.js +20 -15
  19. package/dist/react/client/DataRenderers.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts.map +1 -1
  21. package/dist/react/client/Interactables.js +11 -5
  22. package/dist/react/client/Interactables.js.map +1 -1
  23. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  24. package/dist/react/client/RemoteThreadList.js +3 -2
  25. package/dist/react/client/RemoteThreadList.js.map +1 -1
  26. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  27. package/dist/react/client/interactable-model-context.js +3 -2
  28. package/dist/react/client/interactable-model-context.js.map +1 -1
  29. package/dist/react/index.d.ts +2 -2
  30. package/dist/react/index.js +2 -2
  31. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -1
  32. package/dist/react/interactables-legacy/useAssistantInteractable.js +3 -0
  33. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
  34. package/dist/react/model-context/useInteractable.d.ts.map +1 -1
  35. package/dist/react/model-context/useInteractable.js +4 -1
  36. package/dist/react/model-context/useInteractable.js.map +1 -1
  37. package/dist/react/model-context/useInteractableVersions.js +1 -1
  38. package/dist/react/primitives/message/MessageGroupedParts.d.ts +11 -2
  39. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  40. package/dist/react/primitives/message/MessageGroupedParts.js +3 -2
  41. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  42. package/dist/react/primitives/part/PartMessages.d.ts +1 -0
  43. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  44. package/dist/react/primitives/part/PartMessages.js +1 -0
  45. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  46. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  47. package/dist/react/primitives/thread/ThreadMessages.js +6 -5
  48. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  49. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  50. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +2 -1
  51. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  52. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +2 -24
  53. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  54. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +135 -374
  55. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  56. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -0
  57. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  58. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +22 -2
  59. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  60. package/dist/react/runtimes/cloud/auiV0.d.ts +40 -15
  61. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  62. package/dist/react/runtimes/cloud/auiV0.js +62 -15
  63. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  64. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +2 -1
  65. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
  66. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +4 -0
  67. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
  68. package/dist/react/runtimes/cloud/sdkIdentity.d.ts +6 -0
  69. package/dist/react/runtimes/cloud/sdkIdentity.d.ts.map +1 -0
  70. package/dist/react/runtimes/cloud/sdkIdentity.js +9 -0
  71. package/dist/react/runtimes/cloud/sdkIdentity.js.map +1 -0
  72. package/dist/react/runtimes/external-message-converter.d.ts +12 -2
  73. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  74. package/dist/react/runtimes/external-message-converter.js +26 -61
  75. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  76. package/dist/react/runtimes/useRemoteThreadListRuntime.js +1 -1
  77. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  78. package/dist/react/utils/groupParts.d.ts +6 -1
  79. package/dist/react/utils/groupParts.d.ts.map +1 -1
  80. package/dist/react/utils/groupParts.js +6 -0
  81. package/dist/react/utils/groupParts.js.map +1 -1
  82. package/dist/react/utils/useJSONEqual.d.ts +3 -1
  83. package/dist/react/utils/useJSONEqual.d.ts.map +1 -1
  84. package/dist/react/utils/useJSONEqual.js +6 -1
  85. package/dist/react/utils/useJSONEqual.js.map +1 -1
  86. package/dist/react/utils/useJSONSchemaDependency.d.ts +7 -0
  87. package/dist/react/utils/useJSONSchemaDependency.d.ts.map +1 -0
  88. package/dist/react/utils/useJSONSchemaDependency.js +20 -0
  89. package/dist/react/utils/useJSONSchemaDependency.js.map +1 -0
  90. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  91. package/dist/runtime/api/thread-runtime.js +2 -5
  92. package/dist/runtime/api/thread-runtime.js.map +1 -1
  93. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
  94. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  95. package/dist/runtime/base/base-composer-runtime-core.js +46 -10
  96. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  97. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -0
  98. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  99. package/dist/runtime/base/base-thread-runtime-core.js +155 -64
  100. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  101. package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
  102. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  103. package/dist/runtime/queue/message-queue.d.ts +5 -0
  104. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  105. package/dist/runtime/queue/message-queue.js +61 -6
  106. package/dist/runtime/queue/message-queue.js.map +1 -1
  107. package/dist/runtime/utils/auto-status.d.ts +6 -2
  108. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  109. package/dist/runtime/utils/auto-status.js +12 -4
  110. package/dist/runtime/utils/auto-status.js.map +1 -1
  111. package/dist/runtime/utils/external-message-conversion.d.ts +16 -4
  112. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
  113. package/dist/runtime/utils/external-message-conversion.js +48 -12
  114. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  115. package/dist/runtime/utils/message-repository.js +3 -3
  116. package/dist/runtime/utils/message-repository.js.map +1 -1
  117. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  118. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  119. package/dist/runtime/utils/thread-message-like.js +4 -2
  120. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  121. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  122. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +8 -1
  123. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  124. package/dist/runtimes/internal.d.ts +1 -1
  125. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
  126. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  127. package/dist/runtimes/local/local-thread-runtime-core.js +4 -1
  128. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  129. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  130. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -1
  131. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  132. package/dist/store/clients/external-thread.d.ts.map +1 -1
  133. package/dist/store/clients/external-thread.js +241 -200
  134. package/dist/store/clients/external-thread.js.map +1 -1
  135. package/dist/store/clients/thread-tasks.d.ts +13 -0
  136. package/dist/store/clients/thread-tasks.d.ts.map +1 -0
  137. package/dist/store/clients/thread-tasks.js +69 -0
  138. package/dist/store/clients/thread-tasks.js.map +1 -0
  139. package/dist/store/index.d.ts +3 -2
  140. package/dist/store/index.js +3 -3
  141. package/dist/store/primitive-predicates.d.ts.map +1 -1
  142. package/dist/store/primitive-predicates.js +4 -4
  143. package/dist/store/primitive-predicates.js.map +1 -1
  144. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  145. package/dist/store/runtime-clients/thread-runtime-client.js +176 -128
  146. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  147. package/dist/store/scope-registration.d.ts +2 -0
  148. package/dist/store/scope-registration.d.ts.map +1 -1
  149. package/dist/store/scopes/task.d.ts +36 -0
  150. package/dist/store/scopes/task.d.ts.map +1 -0
  151. package/dist/store/scopes/task.js +0 -0
  152. package/dist/store/scopes/thread.d.ts +20 -0
  153. package/dist/store/scopes/thread.d.ts.map +1 -1
  154. package/dist/subscribable/subscribable.d.ts +2 -1
  155. package/dist/subscribable/subscribable.d.ts.map +1 -1
  156. package/dist/subscribable/subscribable.js +23 -15
  157. package/dist/subscribable/subscribable.js.map +1 -1
  158. package/dist/types/index.d.ts +2 -2
  159. package/dist/types/message.d.ts +8 -1
  160. package/dist/types/message.d.ts.map +1 -1
  161. package/dist/types/message.js.map +1 -1
  162. package/dist/utils/abortable-iterable.d.ts.map +1 -1
  163. package/dist/utils/abortable-iterable.js +30 -14
  164. package/dist/utils/abortable-iterable.js.map +1 -1
  165. package/dist/utils/getGroupStatus.d.ts +18 -2
  166. package/dist/utils/getGroupStatus.d.ts.map +1 -1
  167. package/dist/utils/getGroupStatus.js +32 -7
  168. package/dist/utils/getGroupStatus.js.map +1 -1
  169. package/dist/utils/wire-media.d.ts.map +1 -1
  170. package/dist/utils/wire-media.js +2 -2
  171. package/dist/utils/wire-media.js.map +1 -1
  172. package/package.json +6 -6
  173. package/src/adapters/speech.test.ts +236 -0
  174. package/src/adapters/speech.ts +17 -10
  175. package/src/adapters/thread-history.ts +3 -0
  176. package/src/index.ts +1 -0
  177. package/src/model-context/frame/host.test.ts +113 -1
  178. package/src/model-context/frame/host.ts +39 -12
  179. package/src/model-context/interactable-composer-metadata.test.ts +30 -0
  180. package/src/model-context/interactable-composer-metadata.ts +10 -10
  181. package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +244 -0
  182. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +104 -7
  183. package/src/react/client/DataRenderers.test.tsx +45 -0
  184. package/src/react/client/DataRenderers.ts +13 -9
  185. package/src/react/client/Interactables.test.ts +44 -1
  186. package/src/react/client/Interactables.ts +7 -1
  187. package/src/react/client/RemoteThreadList.test.ts +41 -1
  188. package/src/react/client/RemoteThreadList.ts +15 -2
  189. package/src/react/client/interactable-model-context.test.ts +44 -0
  190. package/src/react/client/interactable-model-context.ts +3 -2
  191. package/src/react/index.ts +5 -1
  192. package/src/react/interactables-legacy/useAssistantInteractable.test.tsx +132 -0
  193. package/src/react/interactables-legacy/useAssistantInteractable.ts +10 -1
  194. package/src/react/model-context/useInteractable.test.tsx +177 -0
  195. package/src/react/model-context/useInteractable.ts +3 -0
  196. package/src/react/primitive-hooks/useActionBarCopy.test.ts +0 -1
  197. package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +0 -1
  198. package/src/react/primitives/message/MessageGroupedParts.test.tsx +96 -0
  199. package/src/react/primitives/message/MessageGroupedParts.tsx +15 -4
  200. package/src/react/primitives/part/PartMessages.test.tsx +104 -1
  201. package/src/react/primitives/part/PartMessages.tsx +1 -0
  202. package/src/react/primitives/thread/ThreadMessages.test.tsx +142 -0
  203. package/src/react/primitives/thread/ThreadMessages.tsx +8 -5
  204. package/src/react/providers/ReadonlyThreadProvider.test.tsx +56 -0
  205. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +7 -1
  206. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +20 -14
  207. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +221 -1
  208. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +166 -512
  209. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +77 -0
  210. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +27 -1
  211. package/src/react/runtimes/cloud/auiV0.ts +160 -25
  212. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +21 -0
  213. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +7 -1
  214. package/src/react/runtimes/cloud/sdkIdentity.ts +9 -0
  215. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +18 -4
  216. package/src/react/runtimes/external-message-converter.test.ts +136 -1
  217. package/src/react/runtimes/external-message-converter.ts +39 -93
  218. package/src/react/runtimes/useRemoteThreadListRuntime.ts +1 -1
  219. package/src/react/utils/groupParts.ts +12 -1
  220. package/src/react/utils/useJSONEqual.ts +9 -0
  221. package/src/react/utils/useJSONSchemaDependency.ts +22 -0
  222. package/src/runtime/api/thread-runtime.test.ts +58 -0
  223. package/src/runtime/api/thread-runtime.ts +2 -4
  224. package/src/runtime/base/base-composer-runtime-core.ts +69 -16
  225. package/src/runtime/base/base-thread-runtime-core.test.ts +1008 -1
  226. package/src/runtime/base/base-thread-runtime-core.ts +222 -83
  227. package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
  228. package/src/runtime/queue/message-queue.ts +68 -6
  229. package/src/runtime/utils/auto-status.test.ts +112 -1
  230. package/src/runtime/utils/auto-status.ts +31 -5
  231. package/src/runtime/utils/external-message-conversion.test.ts +134 -0
  232. package/src/runtime/utils/external-message-conversion.ts +122 -31
  233. package/src/runtime/utils/message-repository.test.ts +49 -0
  234. package/src/runtime/utils/message-repository.ts +3 -3
  235. package/src/runtime/utils/thread-message-like.test.ts +52 -0
  236. package/src/runtime/utils/thread-message-like.ts +4 -0
  237. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +31 -1
  238. package/src/runtimes/local/local-thread-runtime-core.test.ts +21 -0
  239. package/src/runtimes/local/local-thread-runtime-core.ts +11 -1
  240. package/src/runtimes/tool-invocations/EDGE_CASES.md +2 -6
  241. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +133 -0
  242. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +10 -6
  243. package/src/store/clients/external-thread.ts +19 -1
  244. package/src/store/clients/thread-tasks.test.ts +274 -0
  245. package/src/store/clients/thread-tasks.ts +138 -0
  246. package/src/store/index.ts +8 -0
  247. package/src/store/primitive-predicates.test.ts +48 -5
  248. package/src/store/primitive-predicates.ts +7 -2
  249. package/src/store/runtime-clients/thread-runtime-client.ts +31 -1
  250. package/src/store/scope-registration.ts +2 -0
  251. package/src/store/scopes/task.ts +34 -0
  252. package/src/store/scopes/thread.ts +16 -0
  253. package/src/store/thread-tasks.test.tsx +209 -0
  254. package/src/subscribable/subscribable.test.ts +156 -1
  255. package/src/subscribable/subscribable.ts +25 -15
  256. package/src/tests/MessageRepository.test.ts +1 -5
  257. package/src/tests/auiV0Encode.test.ts +300 -0
  258. package/src/tests/base-composer-runtime-core-send.test.ts +51 -0
  259. package/src/tests/base-composer-runtime-core.test.ts +314 -2
  260. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +151 -0
  261. package/src/tests/external-thread-attachments.test.tsx +69 -0
  262. package/src/tests/external-thread-parity.test.tsx +34 -0
  263. package/src/tests/groupParts.test.ts +71 -0
  264. package/src/tests/message-queue.test.ts +253 -0
  265. package/src/tests/remote-thread-list-reactive-threadId.test.ts +51 -0
  266. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +43 -0
  267. package/src/types/index.ts +1 -0
  268. package/src/types/message.ts +8 -0
  269. package/src/utils/abortable-iterable.test.ts +115 -0
  270. package/src/utils/abortable-iterable.ts +34 -23
  271. package/src/utils/getGroupStatus.test.ts +70 -4
  272. package/src/utils/getGroupStatus.ts +35 -12
  273. package/src/utils/wire-media.test.ts +10 -0
  274. package/src/utils/wire-media.ts +6 -2
@@ -118,8 +118,9 @@ const mountList = (
118
118
  refetch?: () => Promise<void>,
119
119
  onSwitchToThread?: (id: string) => void,
120
120
  onDelete?: (id: string) => void,
121
+ providedOnThreadIdChange?: (id: string | undefined) => void,
121
122
  ) => {
122
- const onThreadIdChange = vi.fn();
123
+ const onThreadIdChange = providedOnThreadIdChange ?? vi.fn();
123
124
  const handle = createAssistantClient(
124
125
  AuiConfig({
125
126
  threads: RemoteThreadList({
@@ -1929,4 +1930,43 @@ describe("RemoteThreadList", () => {
1929
1930
  });
1930
1931
  handle.destroy();
1931
1932
  });
1933
+
1934
+ it("keeps a completed switch when onThreadIdChange throws", async () => {
1935
+ const callbackError = new Error("host callback failed");
1936
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1937
+ const onThreadIdChange = vi.fn(() => {
1938
+ throw callbackError;
1939
+ });
1940
+ const adapter = makeAdapter({
1941
+ list: vi.fn(async () => ({
1942
+ threads: [{ status: "regular" as const, remoteId: "t1", title: "One" }],
1943
+ })),
1944
+ });
1945
+ const { handle } = mountList(
1946
+ adapter,
1947
+ undefined,
1948
+ undefined,
1949
+ undefined,
1950
+ undefined,
1951
+ onThreadIdChange,
1952
+ );
1953
+ const threads = handle.getClient().threads;
1954
+ try {
1955
+ await threads.getLoadThreadsPromise();
1956
+
1957
+ flushTapSync(() => threads.switchToThread("t1"));
1958
+
1959
+ await vi.waitFor(() => {
1960
+ expect(handle.getClient().threads.getState().mainThreadId).toBe("t1");
1961
+ expect(onThreadIdChange).toHaveBeenCalledExactlyOnceWith("t1");
1962
+ expect(errorSpy).toHaveBeenCalledWith(
1963
+ "[assistant-ui] onThreadIdChange callback threw an error",
1964
+ callbackError,
1965
+ );
1966
+ });
1967
+ } finally {
1968
+ handle.destroy();
1969
+ errorSpy.mockRestore();
1970
+ }
1971
+ });
1932
1972
  });
@@ -57,6 +57,7 @@ import {
57
57
  startThreadTitleRename,
58
58
  type ThreadTitleState,
59
59
  } from "../../runtimes/remote-thread-list/title-generation";
60
+ import { invokeUserCallback } from "../../utils/invoke-user-callback";
60
61
 
61
62
  const RESOLVED_PROMISE = Promise.resolve();
62
63
 
@@ -550,7 +551,14 @@ const useRemoteThreadList = (
550
551
  (remoteId: string | undefined, emit: boolean) => {
551
552
  if (session.lastNotifiedRemoteId === remoteId) return;
552
553
  session.lastNotifiedRemoteId = remoteId;
553
- if (emit) session.onThreadIdChange?.(remoteId);
554
+ if (emit) {
555
+ invokeUserCallback(
556
+ "assistant-ui",
557
+ "onThreadIdChange",
558
+ session.onThreadIdChange,
559
+ remoteId,
560
+ );
561
+ }
554
562
  },
555
563
  [session],
556
564
  );
@@ -1255,7 +1263,12 @@ const useRemoteThreadList = (
1255
1263
  useEffect(() => {
1256
1264
  if (session.lastNotifiedRemoteId === mainRemoteId) return;
1257
1265
  session.lastNotifiedRemoteId = mainRemoteId;
1258
- onThreadIdChange?.(mainRemoteId);
1266
+ invokeUserCallback(
1267
+ "assistant-ui",
1268
+ "onThreadIdChange",
1269
+ onThreadIdChange,
1270
+ mainRemoteId,
1271
+ );
1259
1272
  }, [mainRemoteId, onThreadIdChange, session]);
1260
1273
 
1261
1274
  useEffect(() => {
@@ -241,6 +241,50 @@ describe("buildInteractableModelContext", () => {
241
241
  expect(mockGenerateId).toHaveBeenCalledOnce();
242
242
  });
243
243
 
244
+ it.each(["__proto__", "toString"])(
245
+ "mints ids for items in a prototype-named array field %s",
246
+ async (field) => {
247
+ const itemSchema = {
248
+ type: "object" as const,
249
+ properties: {
250
+ id: { type: "string" as const },
251
+ title: { type: "string" as const },
252
+ },
253
+ required: ["id", "title"],
254
+ };
255
+ const schema = {
256
+ type: "object" as const,
257
+ properties: Object.fromEntries([
258
+ [field, { type: "array" as const, items: itemSchema }],
259
+ ]),
260
+ };
261
+ const state = Object.fromEntries([[field, []]]);
262
+ const defs = { b1: def("b1", "taskBoard", state) };
263
+ const { ctx } = build(defs, new Map([["b1", schema]]));
264
+ const args = Object.fromEntries([
265
+ ["id", "b1"],
266
+ [field, { add: [{ title: "Write tests" }] }],
267
+ ]);
268
+
269
+ const result = (await ctx!.tools["update_taskBoard"]!.execute!(
270
+ args,
271
+ {} as never,
272
+ )) as {
273
+ addedItemIds: Record<string, string[]>;
274
+ };
275
+
276
+ expect(Object.getPrototypeOf(result.addedItemIds)).toBe(
277
+ Object.prototype,
278
+ );
279
+ expect(Object.hasOwn(result.addedItemIds, field)).toBe(true);
280
+ expect(result.addedItemIds[field]).toEqual(["generated-id"]);
281
+ expect(Object.hasOwn(defs.b1.state as object, field)).toBe(true);
282
+ expect((defs.b1.state as Record<string, unknown>)[field]).toEqual([
283
+ { title: "Write tests", id: "generated-id" },
284
+ ]);
285
+ },
286
+ );
287
+
244
288
  it("rejects an unknown id and lists valid ids", async () => {
245
289
  const defs = { n1: def("n1", "note"), n2: def("n2", "note") };
246
290
  const { ctx, setDefState } = build(defs);
@@ -8,6 +8,7 @@ import {
8
8
  } from "../../model-context/interactable-composer-metadata";
9
9
  import { generateId } from "../../utils/id";
10
10
  import { isRecord } from "../../utils/json/is-json";
11
+ import { nullProtoRecord } from "../../utils/record";
11
12
 
12
13
  export type PartialJSONSchema = ReturnType<typeof toJSONSchema>;
13
14
 
@@ -243,7 +244,7 @@ export function buildInteractableModelContext(
243
244
  }
244
245
  const baseline = streamBaselines.get(toolCallId);
245
246
  streamBaselines.delete(toolCallId);
246
- const addedItemIds: Record<string, string[]> = {};
247
+ const addedItemIds = nullProtoRecord<string[]>();
247
248
  setDefState(target.id, (prev) =>
248
249
  shallowMergeInteractableState(prev, partial, {
249
250
  arrayBaseline:
@@ -264,7 +265,7 @@ export function buildInteractableModelContext(
264
265
  addedItemIds?: Record<string, string[]>;
265
266
  } = { success: true, id: target.id };
266
267
  if (Object.keys(addedItemIds).length > 0) {
267
- result.addedItemIds = addedItemIds;
268
+ result.addedItemIds = { ...addedItemIds };
268
269
  }
269
270
  return result;
270
271
  },
@@ -217,8 +217,12 @@ export { useExternalStoreSharedOptions } from "./runtimes/useExternalStoreShared
217
217
  export {
218
218
  useExternalMessageConverter,
219
219
  convertExternalMessages,
220
+ createExternalMessageConversionCache,
221
+ } from "./runtimes/external-message-converter";
222
+ export type {
223
+ ExternalMessageConversionCache,
224
+ JoinStrategy,
220
225
  } from "./runtimes/external-message-converter";
221
- export type { JoinStrategy } from "./runtimes/external-message-converter";
222
226
  export { createMessageConverter } from "./runtimes/createMessageConverter";
223
227
  export {
224
228
  useStreamingTiming,
@@ -0,0 +1,132 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, renderHook, waitFor } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const mocks = vi.hoisted(() => {
7
+ const unregister = vi.fn();
8
+ const register = vi.fn(() => unregister);
9
+ return {
10
+ register,
11
+ unregister,
12
+ aui: { interactables: { register } },
13
+ };
14
+ });
15
+
16
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
17
+ ...(await importOriginal<typeof import("@assistant-ui/store")>()),
18
+ useAui: () => mocks.aui,
19
+ }));
20
+
21
+ import { useAssistantInteractable } from "./useAssistantInteractable";
22
+
23
+ afterEach(() => {
24
+ cleanup();
25
+ vi.clearAllMocks();
26
+ });
27
+
28
+ describe("useAssistantInteractable", () => {
29
+ it("refreshes the registration when its JSON schema changes", async () => {
30
+ const schemaA = {
31
+ type: "object" as const,
32
+ properties: { first: { type: "string" } },
33
+ };
34
+ const schemaB = {
35
+ type: "object" as const,
36
+ properties: { second: { type: "number" } },
37
+ };
38
+ const initialA = { first: "one" };
39
+ const initialB = { second: 2 };
40
+
41
+ const hook = renderHook(
42
+ ({ stateSchema, initialState }) =>
43
+ useAssistantInteractable("panel", {
44
+ id: "panel-1",
45
+ description: "A panel",
46
+ stateSchema,
47
+ initialState,
48
+ }),
49
+ { initialProps: { stateSchema: schemaA, initialState: initialA } },
50
+ );
51
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(1));
52
+
53
+ hook.rerender({ stateSchema: schemaB, initialState: initialB });
54
+
55
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(2));
56
+ expect(mocks.unregister).toHaveBeenCalledTimes(1);
57
+ expect(mocks.register).toHaveBeenLastCalledWith(
58
+ expect.objectContaining({ stateSchema: schemaB, initialState: initialB }),
59
+ );
60
+
61
+ hook.rerender({
62
+ stateSchema: {
63
+ type: "object",
64
+ properties: { second: { type: "number" } },
65
+ },
66
+ initialState: { second: 3 },
67
+ });
68
+ expect(mocks.register).toHaveBeenCalledTimes(2);
69
+ });
70
+
71
+ it("stabilizes equivalent rebuilt standard schemas", async () => {
72
+ const createSchema = (property: string) => ({
73
+ "~standard": {
74
+ version: 1 as const,
75
+ vendor: "test",
76
+ validate: (value: unknown) => ({ value }),
77
+ toJSONSchema: () => ({
78
+ type: "object" as const,
79
+ properties: { [property]: { type: "string" as const } },
80
+ }),
81
+ },
82
+ });
83
+ const firstSchema = createSchema("value");
84
+
85
+ const hook = renderHook(
86
+ ({ stateSchema }) =>
87
+ useAssistantInteractable("panel", {
88
+ id: "panel-1",
89
+ description: "A panel",
90
+ stateSchema,
91
+ initialState: {},
92
+ }),
93
+ { initialProps: { stateSchema: firstSchema } },
94
+ );
95
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(1));
96
+
97
+ hook.rerender({ stateSchema: createSchema("value") });
98
+ expect(mocks.register).toHaveBeenCalledTimes(1);
99
+
100
+ const changedSchema = createSchema("other");
101
+ hook.rerender({ stateSchema: changedSchema });
102
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(2));
103
+ expect(mocks.register).toHaveBeenLastCalledWith(
104
+ expect.objectContaining({ stateSchema: changedSchema }),
105
+ );
106
+ });
107
+
108
+ it("keeps unsupported standard schemas from failing during render", async () => {
109
+ const createSchema = () => ({
110
+ "~standard": {
111
+ version: 1 as const,
112
+ vendor: "test",
113
+ validate: (value: unknown) => ({ value }),
114
+ },
115
+ });
116
+
117
+ const hook = renderHook(
118
+ ({ stateSchema }) =>
119
+ useAssistantInteractable("panel", {
120
+ id: "panel-1",
121
+ description: "A panel",
122
+ stateSchema,
123
+ initialState: {},
124
+ }),
125
+ { initialProps: { stateSchema: createSchema() } },
126
+ );
127
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(1));
128
+
129
+ hook.rerender({ stateSchema: createSchema() });
130
+ expect(mocks.register).toHaveBeenCalledTimes(1);
131
+ });
132
+ });
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useId, useRef } from "react";
2
2
  import { useAui } from "@assistant-ui/store";
3
+ import { useJSONSchemaDependency } from "../utils/useJSONSchemaDependency";
3
4
  import type { InteractableStateSchema } from "./scopes";
4
5
 
5
6
  /**
@@ -38,6 +39,7 @@ export const useAssistantInteractable = (
38
39
 
39
40
  const stateSchemaRef = useRef(config.stateSchema);
40
41
  stateSchemaRef.current = config.stateSchema;
42
+ const normalizedStateSchema = useJSONSchemaDependency(config.stateSchema);
41
43
  const initialStateRef = useRef(config.initialState);
42
44
  initialStateRef.current = config.initialState;
43
45
 
@@ -50,7 +52,14 @@ export const useAssistantInteractable = (
50
52
  initialState: initialStateRef.current,
51
53
  selected: config.selected,
52
54
  });
53
- }, [aui, id, name, config.description, config.selected]);
55
+ }, [
56
+ aui,
57
+ id,
58
+ name,
59
+ config.description,
60
+ normalizedStateSchema,
61
+ config.selected,
62
+ ]);
54
63
 
55
64
  return id;
56
65
  };
@@ -0,0 +1,177 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, renderHook, waitFor } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const mocks = vi.hoisted(() => {
7
+ const unregister = vi.fn();
8
+ const register = vi.fn(() => unregister);
9
+ return {
10
+ register,
11
+ unregister,
12
+ aui: { unstable_interactables: { register } },
13
+ state: { optional: { part: undefined }, thread: { messages: [] } },
14
+ methods: {
15
+ setState: vi.fn(),
16
+ isPending: false,
17
+ error: undefined,
18
+ flush: vi.fn(async () => undefined),
19
+ },
20
+ };
21
+ });
22
+
23
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
24
+ ...(await importOriginal<typeof import("@assistant-ui/store")>()),
25
+ useAui: () => mocks.aui,
26
+ useAuiState: (selector: (state: typeof mocks.state) => unknown) =>
27
+ selector(mocks.state),
28
+ }));
29
+
30
+ vi.mock("./useInteractableState", async (importOriginal) => ({
31
+ ...(await importOriginal<typeof import("./useInteractableState")>()),
32
+ unstable_useInteractableState: () => [undefined, mocks.methods],
33
+ }));
34
+
35
+ import { unstable_useInteractable } from "./useInteractable";
36
+
37
+ afterEach(() => {
38
+ cleanup();
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ describe("unstable_useInteractable", () => {
43
+ it("refreshes the registration when its JSON schema changes", async () => {
44
+ const schemaA = {
45
+ type: "object" as const,
46
+ properties: { first: { type: "string" } },
47
+ };
48
+ const schemaB = {
49
+ type: "object" as const,
50
+ properties: { second: { type: "number" } },
51
+ };
52
+ const initialA = { first: "one" };
53
+ const initialB = { second: 2 };
54
+
55
+ const hook = renderHook(
56
+ ({ stateSchema, initialState }) =>
57
+ unstable_useInteractable("panel", {
58
+ id: "panel-1",
59
+ description: "A panel",
60
+ stateSchema,
61
+ initialState,
62
+ }),
63
+ { initialProps: { stateSchema: schemaA, initialState: initialA } },
64
+ );
65
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(1));
66
+
67
+ hook.rerender({ stateSchema: schemaB, initialState: initialB });
68
+
69
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(2));
70
+ expect(mocks.unregister).toHaveBeenCalledTimes(1);
71
+ expect(mocks.register).toHaveBeenLastCalledWith(
72
+ expect.objectContaining({ stateSchema: schemaB, initialState: initialB }),
73
+ );
74
+
75
+ hook.rerender({
76
+ stateSchema: {
77
+ type: "object",
78
+ properties: { second: { type: "number" } },
79
+ },
80
+ initialState: { second: 3 },
81
+ });
82
+ expect(mocks.register).toHaveBeenCalledTimes(2);
83
+ });
84
+
85
+ it("stabilizes equivalent rebuilt standard schemas", async () => {
86
+ const createSchema = (property: string) => ({
87
+ "~standard": {
88
+ version: 1 as const,
89
+ vendor: "test",
90
+ validate: (value: unknown) => ({ value }),
91
+ toJSONSchema: () => ({
92
+ type: "object" as const,
93
+ properties: { [property]: { type: "string" as const } },
94
+ }),
95
+ },
96
+ });
97
+ const firstSchema = createSchema("value");
98
+
99
+ const hook = renderHook(
100
+ ({ stateSchema }) =>
101
+ unstable_useInteractable("panel", {
102
+ id: "panel-1",
103
+ description: "A panel",
104
+ stateSchema,
105
+ initialState: {},
106
+ }),
107
+ { initialProps: { stateSchema: firstSchema } },
108
+ );
109
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(1));
110
+
111
+ hook.rerender({ stateSchema: createSchema("value") });
112
+ expect(mocks.register).toHaveBeenCalledTimes(1);
113
+
114
+ const changedSchema = createSchema("other");
115
+ hook.rerender({ stateSchema: changedSchema });
116
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(2));
117
+ expect(mocks.register).toHaveBeenLastCalledWith(
118
+ expect.objectContaining({ stateSchema: changedSchema }),
119
+ );
120
+ });
121
+
122
+ it("keeps unsupported standard schemas from failing during render", async () => {
123
+ const createSchema = () => ({
124
+ "~standard": {
125
+ version: 1 as const,
126
+ vendor: "test",
127
+ validate: (value: unknown) => ({ value }),
128
+ },
129
+ });
130
+
131
+ const hook = renderHook(
132
+ ({ stateSchema }) =>
133
+ unstable_useInteractable("panel", {
134
+ id: "panel-1",
135
+ description: "A panel",
136
+ stateSchema,
137
+ initialState: {},
138
+ }),
139
+ { initialProps: { stateSchema: createSchema() } },
140
+ );
141
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(1));
142
+
143
+ hook.rerender({ stateSchema: createSchema() });
144
+ expect(mocks.register).toHaveBeenCalledTimes(1);
145
+ });
146
+
147
+ it("converts a referentially stable schema only once", async () => {
148
+ const toJSONSchema = vi.fn(() => ({
149
+ type: "object" as const,
150
+ properties: { value: { type: "string" as const } },
151
+ }));
152
+ const stateSchema = {
153
+ "~standard": {
154
+ version: 1 as const,
155
+ vendor: "test",
156
+ validate: (value: unknown) => ({ value }),
157
+ toJSONSchema,
158
+ },
159
+ };
160
+
161
+ const hook = renderHook(
162
+ ({ initialState }) =>
163
+ unstable_useInteractable("panel", {
164
+ id: "panel-1",
165
+ description: "A panel",
166
+ stateSchema,
167
+ initialState,
168
+ }),
169
+ { initialProps: { initialState: { value: "first" } } },
170
+ );
171
+ await waitFor(() => expect(mocks.register).toHaveBeenCalledTimes(1));
172
+
173
+ hook.rerender({ initialState: { value: "second" } });
174
+ expect(toJSONSchema).toHaveBeenCalledTimes(1);
175
+ expect(mocks.register).toHaveBeenCalledTimes(1);
176
+ });
177
+ });
@@ -10,6 +10,7 @@ import {
10
10
  } from "../../model-context/interactable-composer-metadata";
11
11
  import { unstable_useInteractableState as useInteractableState } from "./useInteractableState";
12
12
  import { useJSONEqual } from "../utils/useJSONEqual";
13
+ import { useJSONSchemaDependency } from "../utils/useJSONSchemaDependency";
13
14
 
14
15
  /**
15
16
  * The state type described by an interactable's `stateSchema`. Resolves the
@@ -116,6 +117,7 @@ const useInteractable = <TSchema extends Unstable_InteractableStateSchema>(
116
117
 
117
118
  const stateSchemaRef = useRef(config.stateSchema);
118
119
  stateSchemaRef.current = config.stateSchema;
120
+ const normalizedStateSchema = useJSONSchemaDependency(config.stateSchema);
119
121
  const initialStateRef = useRef(config.initialState);
120
122
  initialStateRef.current = config.initialState;
121
123
 
@@ -138,6 +140,7 @@ const useInteractable = <TSchema extends Unstable_InteractableStateSchema>(
138
140
  id,
139
141
  name,
140
142
  config.description,
143
+ normalizedStateSchema,
141
144
  internalScope,
142
145
  config.updateRender,
143
146
  ]);
@@ -45,7 +45,6 @@ import { useActionBarCopy } from "./useActionBarCopy";
45
45
 
46
46
  afterEach(() => {
47
47
  cleanup();
48
- vi.clearAllMocks();
49
48
  vi.useRealTimers();
50
49
  mocks.currentAui = mocks.aui;
51
50
  });
@@ -47,7 +47,6 @@ import { useSuggestionTrigger } from "./useSuggestionTrigger";
47
47
 
48
48
  afterEach(() => {
49
49
  cleanup();
50
- vi.clearAllMocks();
51
50
  mocks.state.thread.isDisabled = false;
52
51
  mocks.state.thread.isRunning = false;
53
52
  mocks.state.thread.capabilities = { queue: false };
@@ -0,0 +1,96 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import type { ThreadMessageLike } from "../../../runtime/utils/thread-message-like";
5
+ import { AssistantRuntimeProvider } from "../../AssistantRuntimeProvider";
6
+ import { ThreadPrimitiveMessages } from "../thread/ThreadMessages";
7
+ import { useExternalStoreRuntime } from "../../runtimes/useExternalStoreRuntime";
8
+ import { groupPartByType } from "../../utils/groupParts";
9
+ import { MessagePrimitiveGroupedParts } from "./MessageGroupedParts";
10
+
11
+ type Msg = {
12
+ id: string;
13
+ content: readonly {
14
+ type: "tool-call";
15
+ toolCallId: string;
16
+ toolName: string;
17
+ args: {};
18
+ result?: { ok: true };
19
+ }[];
20
+ };
21
+
22
+ const task = (toolCallId: string, hasResult: boolean) => ({
23
+ type: "tool-call" as const,
24
+ toolCallId,
25
+ toolName: "task",
26
+ args: {},
27
+ ...(hasResult ? { result: { ok: true } } : {}),
28
+ });
29
+
30
+ const convertMessage = (message: Msg): ThreadMessageLike => ({
31
+ id: message.id,
32
+ role: "assistant",
33
+ content: message.content,
34
+ });
35
+
36
+ afterEach(cleanup);
37
+
38
+ describe("MessagePrimitive.GroupedParts", () => {
39
+ it("passes status counts to a tool-name group", () => {
40
+ let group:
41
+ | {
42
+ counts: MessagePrimitiveGroupedParts.GroupCounts;
43
+ indices: readonly number[];
44
+ }
45
+ | undefined;
46
+
47
+ const GroupedParts = () => (
48
+ <MessagePrimitiveGroupedParts
49
+ groupBy={groupPartByType({
50
+ "tool-call:task": ["group-subagents"],
51
+ })}
52
+ >
53
+ {({ part, children }) => {
54
+ if (part.type === "group-subagents") {
55
+ group = { counts: part.counts, indices: part.indices };
56
+ return children;
57
+ }
58
+ return null;
59
+ }}
60
+ </MessagePrimitiveGroupedParts>
61
+ );
62
+
63
+ const App = () => {
64
+ const runtime = useExternalStoreRuntime<Msg>({
65
+ messages: [
66
+ {
67
+ id: "assistant-1",
68
+ content: [
69
+ task("task-1", true),
70
+ task("task-2", true),
71
+ task("task-3", false),
72
+ ],
73
+ },
74
+ ],
75
+ isRunning: true,
76
+ convertMessage,
77
+ onNew: async () => {},
78
+ });
79
+ return (
80
+ <AssistantRuntimeProvider runtime={runtime}>
81
+ <ThreadPrimitiveMessages components={{ Message: GroupedParts }} />
82
+ </AssistantRuntimeProvider>
83
+ );
84
+ };
85
+
86
+ render(<App />);
87
+
88
+ expect(group?.counts).toEqual({
89
+ running: 1,
90
+ complete: 2,
91
+ incomplete: 0,
92
+ requiresAction: 0,
93
+ });
94
+ expect(group?.indices).toHaveLength(3);
95
+ });
96
+ });