@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
@@ -104,6 +104,18 @@ describe("interactableToolName", () => {
104
104
  });
105
105
 
106
106
  describe("shallowMergeInteractableState", () => {
107
+ it("adds prototype-named fields as own properties", () => {
108
+ const value = { enabled: true };
109
+ const result = shallowMergeInteractableState(
110
+ { title: "Example" },
111
+ Object.fromEntries([["__proto__", value]]),
112
+ ) as Record<string, unknown>;
113
+
114
+ expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
115
+ expect(Object.hasOwn(result, "__proto__")).toBe(true);
116
+ expect(result["__proto__"]).toBe(value);
117
+ });
118
+
107
119
  it("applies array operations from a baseline", () => {
108
120
  const prev = {
109
121
  tasks: [
@@ -453,6 +465,24 @@ describe("gateInteractableComposerMetadata", () => {
453
465
  expect(gated?.interactables).toEqual([entry("a", { v: 1 })]);
454
466
  });
455
467
 
468
+ it.each(["__proto__", "toString"])(
469
+ "treats a removed prototype-named field %s as a full snapshot",
470
+ (field) => {
471
+ const known = Object.fromEntries([
472
+ [field, { enabled: true }],
473
+ ["title", "draft"],
474
+ ["stable", true],
475
+ ]);
476
+ const current = { title: "edited", stable: true };
477
+ const meta = { interactables: [entry("a", current)] };
478
+ const history = [userMsg([entry("a", known)])];
479
+
480
+ const gated = gateInteractableComposerMetadata(meta, history);
481
+
482
+ expect(gated?.interactables).toEqual([entry("a", current)]);
483
+ },
484
+ );
485
+
456
486
  it("omits an interactable the model already knows via its own update_* call", () => {
457
487
  const meta = { interactables: [entry("a", { v: 2 }, "note")] };
458
488
  const history = [
@@ -149,7 +149,7 @@ export function shallowMergeInteractableState(
149
149
  const baseline = isRecord(options?.arrayBaseline)
150
150
  ? options.arrayBaseline
151
151
  : prev;
152
- const next = { ...prev };
152
+ const next = Object.entries(prev);
153
153
  for (const [key, value] of Object.entries(partial)) {
154
154
  const baseValue = baseline[key];
155
155
  if (Array.isArray(baseValue) && isRecord(value)) {
@@ -158,12 +158,12 @@ export function shallowMergeInteractableState(
158
158
  (options.idKeyedFields === undefined || options.idKeyedFields.has(key))
159
159
  ? () => options.idFactory?.(key)
160
160
  : undefined;
161
- next[key] = applyArrayUpdate(baseValue, value, mintId);
161
+ next.push([key, applyArrayUpdate(baseValue, value, mintId)]);
162
162
  } else {
163
- next[key] = value;
163
+ next.push([key, value]);
164
164
  }
165
165
  }
166
- return next;
166
+ return Object.fromEntries(next);
167
167
  }
168
168
 
169
169
  /**
@@ -179,17 +179,17 @@ function shallowDiffInteractableState(
179
179
  ): Record<string, unknown> | undefined {
180
180
  if (!isRecord(known) || !isRecord(next)) return undefined;
181
181
  for (const key of Object.keys(known)) {
182
- if (!(key in next)) return undefined;
182
+ if (!Object.hasOwn(next, key)) return undefined;
183
183
  }
184
- const diff: Record<string, unknown> = {};
184
+ const diff: [string, unknown][] = [];
185
185
  for (const [key, value] of Object.entries(next)) {
186
- if (!(key in known) || !isJSONValueEqual(known[key], value)) {
187
- diff[key] = value;
186
+ if (!Object.hasOwn(known, key) || !isJSONValueEqual(known[key], value)) {
187
+ diff.push([key, value]);
188
188
  }
189
189
  }
190
- const changed = Object.keys(diff).length;
190
+ const changed = diff.length;
191
191
  if (changed === 0 || changed === Object.keys(next).length) return undefined;
192
- return diff;
192
+ return Object.fromEntries(diff);
193
193
  }
194
194
 
195
195
  type ToolCallLikePart = {
@@ -139,6 +139,250 @@ describe("parseStoredMessageRepository", () => {
139
139
  expect(repo.messages.map((item) => item.message.id)).toEqual(["valid"]);
140
140
  });
141
141
 
142
+ it("drops unreadable parts and attachments while keeping their messages", () => {
143
+ const attachment = {
144
+ id: "attachment-1",
145
+ type: "document",
146
+ name: "notes.txt",
147
+ status: { type: "complete" },
148
+ content: [null, { type: "text", text: "notes" }],
149
+ };
150
+ const repo = parseStoredMessageRepository(
151
+ JSON.stringify({
152
+ headId: "answer",
153
+ messages: [
154
+ {
155
+ message: {
156
+ ...storedMessage("question"),
157
+ content: [
158
+ null,
159
+ "text",
160
+ { type: 1 },
161
+ { type: "text", text: "hi" },
162
+ ],
163
+ attachments: [
164
+ null,
165
+ attachment,
166
+ { ...attachment, id: 2 },
167
+ { ...attachment, id: "uploading", status: { type: "running" } },
168
+ { ...attachment, id: "no-content", content: undefined },
169
+ { ...attachment, id: "no-name", name: undefined },
170
+ ],
171
+ },
172
+ parentId: null,
173
+ },
174
+ {
175
+ message: {
176
+ ...storedMessage("answer", "assistant"),
177
+ content: [null, { type: "future-part", value: 1 }],
178
+ },
179
+ parentId: "question",
180
+ },
181
+ ],
182
+ }),
183
+ );
184
+
185
+ const [question, answer] = repo.messages.map((item) => item.message);
186
+ expect(repo.headId).toBe("answer");
187
+ expect(question?.content).toEqual([{ type: "text", text: "hi" }]);
188
+ expect(question?.attachments).toEqual([
189
+ { ...attachment, content: [{ type: "text", text: "notes" }] },
190
+ ]);
191
+ expect(answer?.content).toEqual([{ type: "future-part", value: 1 }]);
192
+ });
193
+
194
+ it("drops known parts that are missing a required field", () => {
195
+ const parts = {
196
+ text: { type: "text", text: "hi" },
197
+ reasoning: { type: "reasoning", text: "because" },
198
+ summary: { type: "reasoning", unstable_summary: "Searching the docs" },
199
+ image: { type: "image", image: "https://example.com/a.png" },
200
+ file: { type: "file", data: "SGk=", mimeType: "text/plain" },
201
+ audio: { type: "audio", audio: { data: "SGk=", format: "mp3" } },
202
+ data: { type: "data", name: "weather", data: { sunny: true } },
203
+ url: {
204
+ type: "source",
205
+ sourceType: "url",
206
+ id: "source-1",
207
+ url: "https://example.com",
208
+ },
209
+ document: {
210
+ type: "source",
211
+ sourceType: "document",
212
+ id: "source-2",
213
+ title: "Notes",
214
+ mediaType: "text/plain",
215
+ },
216
+ generativeUI: { type: "generative-ui", spec: { root: "hi" } },
217
+ toolCall: {
218
+ type: "tool-call",
219
+ toolCallId: "call-1",
220
+ toolName: "search",
221
+ args: {},
222
+ argsText: "{}",
223
+ },
224
+ };
225
+ const repo = parseStoredMessageRepository(
226
+ JSON.stringify({
227
+ messages: [
228
+ {
229
+ message: {
230
+ ...storedMessage("assistant", "assistant"),
231
+ content: [
232
+ { type: "text" },
233
+ { ...parts.reasoning, text: 1 },
234
+ { type: "image" },
235
+ { ...parts.file, mimeType: undefined },
236
+ { type: "audio", audio: null },
237
+ { type: "data", data: {} },
238
+ { ...parts.url, url: undefined },
239
+ { ...parts.document, sourceType: "unknown" },
240
+ { type: "generative-ui" },
241
+ { ...parts.toolCall, argsText: undefined },
242
+ ...Object.values(parts),
243
+ ],
244
+ },
245
+ parentId: null,
246
+ },
247
+ ],
248
+ }),
249
+ );
250
+
251
+ expect(repo.messages[0]?.message.content).toEqual(Object.values(parts));
252
+ });
253
+
254
+ it("applies the same rules to nested tool call messages", () => {
255
+ const toolCall = {
256
+ type: "tool-call",
257
+ toolCallId: "call-1",
258
+ toolName: "delegate",
259
+ args: {},
260
+ argsText: "{}",
261
+ };
262
+ const repo = parseStoredMessageRepository(
263
+ JSON.stringify({
264
+ messages: [
265
+ {
266
+ message: {
267
+ ...storedMessage("parent", "assistant"),
268
+ content: [
269
+ {
270
+ ...toolCall,
271
+ messages: [
272
+ null,
273
+ { id: "missing-shell" },
274
+ {
275
+ ...storedMessage("nested", "assistant"),
276
+ content: [null, { type: "text", text: "nested" }],
277
+ },
278
+ ],
279
+ },
280
+ { ...toolCall, toolCallId: "call-2", messages: "invalid" },
281
+ ],
282
+ },
283
+ parentId: null,
284
+ },
285
+ ],
286
+ }),
287
+ );
288
+
289
+ expect(repo.messages[0]?.message.content).toEqual([
290
+ {
291
+ ...toolCall,
292
+ messages: [
293
+ {
294
+ ...storedMessage("nested", "assistant"),
295
+ content: [{ type: "text", text: "nested" }],
296
+ createdAt: new Date("2026-01-01T00:00:00.000Z"),
297
+ metadata: {
298
+ unstable_state: null,
299
+ unstable_annotations: [],
300
+ unstable_data: [],
301
+ steps: [],
302
+ custom: {},
303
+ },
304
+ },
305
+ ],
306
+ },
307
+ { ...toolCall, toolCallId: "call-2" },
308
+ ]);
309
+ });
310
+
311
+ it("keeps a system message when exactly one readable part remains", () => {
312
+ const repo = parseStoredMessageRepository(
313
+ JSON.stringify({
314
+ messages: [
315
+ {
316
+ message: {
317
+ ...storedMessage("unreadable", "system"),
318
+ content: [null],
319
+ },
320
+ parentId: null,
321
+ },
322
+ {
323
+ message: {
324
+ ...storedMessage("non-text", "system"),
325
+ content: [
326
+ { type: "text" },
327
+ { type: "image", image: "https://example.com/a.png" },
328
+ ],
329
+ },
330
+ parentId: null,
331
+ },
332
+ {
333
+ message: {
334
+ ...storedMessage("recovered", "system"),
335
+ content: [null, { type: "text", text: "Be brief." }],
336
+ },
337
+ parentId: null,
338
+ },
339
+ ],
340
+ }),
341
+ );
342
+
343
+ expect(
344
+ repo.messages.map(({ message }) => [message.id, message.content]),
345
+ ).toEqual([
346
+ ["non-text", [{ type: "image", image: "https://example.com/a.png" }]],
347
+ ["recovered", [{ type: "text", text: "Be brief." }]],
348
+ ]);
349
+ });
350
+
351
+ it("stops parsing nested tool call messages past the depth limit", () => {
352
+ let stored: unknown = storedMessage("leaf", "assistant");
353
+ for (let level = 0; level < 150; level += 1) {
354
+ stored = {
355
+ ...storedMessage(`level-${level}`, "assistant"),
356
+ content: [
357
+ {
358
+ type: "tool-call",
359
+ toolCallId: `call-${level}`,
360
+ toolName: "delegate",
361
+ args: {},
362
+ argsText: "{}",
363
+ messages: [stored],
364
+ },
365
+ ],
366
+ };
367
+ }
368
+
369
+ const repo = parseStoredMessageRepository(
370
+ JSON.stringify({ messages: [{ message: stored, parentId: null }] }),
371
+ );
372
+
373
+ let message = repo.messages[0]?.message;
374
+ let depth = 0;
375
+ while (message?.role === "assistant") {
376
+ const part = message.content[0];
377
+ const nested =
378
+ part?.type === "tool-call" ? part.messages?.[0] : undefined;
379
+ if (!nested) break;
380
+ message = nested;
381
+ depth += 1;
382
+ }
383
+ expect(depth).toBe(100);
384
+ });
385
+
142
386
  it("skips messages whose parent is missing, skipped, or appears later", () => {
143
387
  const repo = parseStoredMessageRepository(
144
388
  JSON.stringify({
@@ -9,12 +9,15 @@ import {
9
9
  } from "react";
10
10
  import { useAui } from "@assistant-ui/store";
11
11
  import type {
12
+ CompleteAttachment,
12
13
  RemoteThreadInitializeResponse,
13
14
  RemoteThreadListAdapter,
14
15
  RemoteThreadListResponse,
15
16
  RemoteThreadMetadata,
17
+ ThreadAssistantMessagePart,
16
18
  ThreadHistoryAdapter,
17
19
  ThreadMessage,
20
+ ThreadUserMessagePart,
18
21
  RunConfig,
19
22
  } from "../../index";
20
23
  import type {
@@ -120,7 +123,91 @@ const parseDate = (value: unknown): Date | null => {
120
123
  const isMessageRole = (value: unknown): value is ThreadMessage["role"] =>
121
124
  value === "system" || value === "user" || value === "assistant";
122
125
 
123
- const parseStoredThreadMessage = (value: unknown): ThreadMessage | null => {
126
+ const MAX_STORED_MESSAGE_DEPTH = 100;
127
+
128
+ const storedPartGuards = {
129
+ text: (part) => typeof part.text === "string",
130
+ reasoning: (part) =>
131
+ typeof part.text === "string" || typeof part.unstable_summary === "string",
132
+ image: (part) => typeof part.image === "string",
133
+ file: (part) =>
134
+ typeof part.data === "string" && typeof part.mimeType === "string",
135
+ audio: (part) =>
136
+ isRecord(part.audio) &&
137
+ typeof part.audio.data === "string" &&
138
+ typeof part.audio.format === "string",
139
+ data: (part) => typeof part.name === "string",
140
+ source: (part) =>
141
+ typeof part.id === "string" &&
142
+ (part.sourceType === "url"
143
+ ? typeof part.url === "string"
144
+ : part.sourceType === "document" &&
145
+ typeof part.title === "string" &&
146
+ typeof part.mediaType === "string"),
147
+ "generative-ui": (part) => isRecord(part.spec),
148
+ "tool-call": (part) =>
149
+ typeof part.toolCallId === "string" &&
150
+ typeof part.toolName === "string" &&
151
+ isRecord(part.args) &&
152
+ typeof part.argsText === "string",
153
+ } satisfies Record<
154
+ (ThreadUserMessagePart | ThreadAssistantMessagePart)["type"],
155
+ (part: Record<string, unknown>) => boolean
156
+ >;
157
+
158
+ const isStoredMessagePart = (
159
+ value: unknown,
160
+ ): value is Record<string, unknown> & { type: string } =>
161
+ isRecord(value) &&
162
+ typeof value.type === "string" &&
163
+ (!Object.hasOwn(storedPartGuards, value.type) ||
164
+ storedPartGuards[value.type as keyof typeof storedPartGuards](value));
165
+
166
+ const parseStoredMessageParts = (
167
+ content: unknown[],
168
+ depth: number,
169
+ ): unknown[] =>
170
+ content.flatMap((part) => {
171
+ if (!isStoredMessagePart(part)) return [];
172
+ if (part.type !== "tool-call" || part.messages === undefined) return [part];
173
+
174
+ const { messages, ...toolCall } = part;
175
+ if (!Array.isArray(messages)) return [toolCall];
176
+ return [
177
+ {
178
+ ...toolCall,
179
+ messages: messages.flatMap((item) => {
180
+ const message = parseStoredThreadMessage(item, depth + 1);
181
+ return message ? [message] : [];
182
+ }),
183
+ },
184
+ ];
185
+ });
186
+
187
+ const parseStoredAttachment = (value: unknown): CompleteAttachment | null => {
188
+ if (
189
+ !isRecord(value) ||
190
+ typeof value.id !== "string" ||
191
+ typeof value.type !== "string" ||
192
+ typeof value.name !== "string" ||
193
+ !isRecord(value.status) ||
194
+ value.status.type !== "complete" ||
195
+ !Array.isArray(value.content)
196
+ ) {
197
+ return null;
198
+ }
199
+
200
+ return {
201
+ ...value,
202
+ content: value.content.filter(isStoredMessagePart),
203
+ } as CompleteAttachment;
204
+ };
205
+
206
+ const parseStoredThreadMessage = (
207
+ value: unknown,
208
+ depth: number,
209
+ ): ThreadMessage | null => {
210
+ if (depth > MAX_STORED_MESSAGE_DEPTH) return null;
124
211
  if (!isRecord(value) || typeof value.id !== "string") return null;
125
212
  if (!isMessageRole(value.role)) return null;
126
213
  if (!Array.isArray(value.content)) return null;
@@ -143,7 +230,10 @@ const parseStoredThreadMessage = (value: unknown): ThreadMessage | null => {
143
230
  return {
144
231
  id: value.id,
145
232
  role: "assistant",
146
- content: value.content as StoredAssistantMessage["content"],
233
+ content: parseStoredMessageParts(
234
+ value.content,
235
+ depth,
236
+ ) as StoredAssistantMessage["content"],
147
237
  status: status as StoredAssistantMessage["status"],
148
238
  createdAt,
149
239
  metadata: {
@@ -185,9 +275,15 @@ const parseStoredThreadMessage = (value: unknown): ThreadMessage | null => {
185
275
  return {
186
276
  id: value.id,
187
277
  role: "user",
188
- content: value.content as StoredUserMessage["content"],
278
+ content: parseStoredMessageParts(
279
+ value.content,
280
+ depth,
281
+ ) as StoredUserMessage["content"],
189
282
  attachments: Array.isArray(value.attachments)
190
- ? (value.attachments as StoredUserMessage["attachments"])
283
+ ? value.attachments.flatMap((item) => {
284
+ const attachment = parseStoredAttachment(item);
285
+ return attachment ? [attachment] : [];
286
+ })
191
287
  : [],
192
288
  createdAt,
193
289
  metadata: {
@@ -196,12 +292,13 @@ const parseStoredThreadMessage = (value: unknown): ThreadMessage | null => {
196
292
  };
197
293
  }
198
294
 
199
- if (value.content.length !== 1) return null;
295
+ const content = parseStoredMessageParts(value.content, depth);
296
+ if (content.length !== 1) return null;
200
297
 
201
298
  return {
202
299
  id: value.id,
203
300
  role: "system",
204
- content: [value.content[0] as StoredSystemMessage["content"][0]],
301
+ content: [content[0] as StoredSystemMessage["content"][0]],
205
302
  createdAt,
206
303
  metadata: {
207
304
  custom: metadata.custom,
@@ -226,7 +323,7 @@ const parseStoredMessageRepositoryItem = (
226
323
  ): ExportedMessageRepositoryItem | null => {
227
324
  if (!isRecord(value)) return null;
228
325
 
229
- const message = parseStoredThreadMessage(value.message);
326
+ const message = parseStoredThreadMessage(value.message, 0);
230
327
  if (!message) return null;
231
328
 
232
329
  const parentId = value.parentId;
@@ -0,0 +1,45 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, cleanup, render } from "@testing-library/react";
4
+ import { useAui } from "@assistant-ui/store";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { DataRenderers } from "./DataRenderers";
7
+
8
+ type AnyClient = Record<string, any>;
9
+
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+
14
+ describe("DataRenderers", () => {
15
+ it.each(["__proto__", "constructor"])(
16
+ "registers and removes a renderer named %s",
17
+ async (name) => {
18
+ let aui!: AnyClient;
19
+ const Harness = () => {
20
+ aui = useAui({ dataRenderers: DataRenderers() } as never);
21
+ return null;
22
+ };
23
+ render(<Harness />);
24
+
25
+ let remove!: () => void;
26
+ await act(async () => {
27
+ remove = aui.dataRenderers().setDataUI(name, () => null);
28
+ await vi.waitFor(() => {
29
+ const renderers = aui.dataRenderers().getState().renderers;
30
+ expect(Object.hasOwn(renderers, name)).toBe(true);
31
+ expect(renderers[name]).toHaveLength(1);
32
+ });
33
+ });
34
+
35
+ await act(async () => {
36
+ remove();
37
+ await vi.waitFor(() =>
38
+ expect(
39
+ Object.hasOwn(aui.dataRenderers().getState().renderers, name),
40
+ ).toBe(false),
41
+ );
42
+ });
43
+ },
44
+ );
45
+ });
@@ -3,6 +3,7 @@ import { resource } from "@assistant-ui/tap";
3
3
  import type { ClientOutput } from "@assistant-ui/store";
4
4
  import type { DataRenderersState } from "../types/scopes/dataRenderers";
5
5
  import type { DataMessagePartComponent } from "../types/MessagePartComponentTypes";
6
+ import { nullProtoRecord } from "../../utils/record";
6
7
 
7
8
  /**
8
9
  * Registers renderers for `data` message parts.
@@ -13,30 +14,33 @@ import type { DataMessagePartComponent } from "../types/MessagePartComponentType
13
14
  */
14
15
  const useDataRenderers = (): ClientOutput<"dataRenderers"> => {
15
16
  const [state, setState] = useState<DataRenderersState>(() => ({
16
- renderers: {},
17
+ renderers: nullProtoRecord(),
17
18
  fallbacks: [],
18
19
  }));
19
20
 
20
21
  const setDataUI = useCallback(
21
22
  (name: string, render: DataMessagePartComponent) => {
22
23
  setState((prev) => {
24
+ const renderers = nullProtoRecord(prev.renderers);
25
+ renderers[name] = [...(renderers[name] ?? []), render];
23
26
  return {
24
27
  ...prev,
25
- renderers: {
26
- ...prev.renderers,
27
- [name]: [...(prev.renderers[name] ?? []), render],
28
- },
28
+ renderers,
29
29
  };
30
30
  });
31
31
 
32
32
  return () => {
33
33
  setState((prev) => {
34
+ const renderers = nullProtoRecord(prev.renderers);
35
+ const remaining = renderers[name]?.filter((r) => r !== render) ?? [];
36
+ if (remaining.length > 0) {
37
+ renderers[name] = remaining;
38
+ } else {
39
+ delete renderers[name];
40
+ }
34
41
  return {
35
42
  ...prev,
36
- renderers: {
37
- ...prev.renderers,
38
- [name]: prev.renderers[name]?.filter((r) => r !== render) ?? [],
39
- },
43
+ renderers,
40
44
  };
41
45
  });
42
46
  };
@@ -10,7 +10,12 @@ import type { ThreadMessage } from "../../types/message";
10
10
  const clientHolder: { client: unknown } = { client: null };
11
11
  const clientListeners = new Set<() => void>();
12
12
  let registeredModelContextProvider:
13
- | { subscribe?: (callback: () => void) => () => void }
13
+ | {
14
+ getModelContext?: () => {
15
+ tools?: Record<string, { parameters?: unknown }>;
16
+ };
17
+ subscribe?: (callback: () => void) => () => void;
18
+ }
14
19
  | undefined;
15
20
 
16
21
  const replaceClient = (client: unknown) => {
@@ -321,6 +326,44 @@ describe("Interactables registration", () => {
321
326
  expect(stateOf(root, "n1")).toBeUndefined();
322
327
  });
323
328
 
329
+ it("refreshes cached tool parameters while another anchor remains", async () => {
330
+ const schemaA = {
331
+ type: "object" as const,
332
+ properties: { first: { type: "string" } },
333
+ };
334
+ const schemaB = {
335
+ type: "object" as const,
336
+ properties: { second: { type: "number" } },
337
+ };
338
+ root = mount({ threadMessages: [createCall("n1")] });
339
+ const first = root.getValue().register(reg("n1", { stateSchema: schemaA }));
340
+ const second = root
341
+ .getValue()
342
+ .register(reg("n1", { stateSchema: schemaA }));
343
+ await flushMicrotasks();
344
+
345
+ first();
346
+ const replacement = root
347
+ .getValue()
348
+ .register(reg("n1", { stateSchema: schemaB }));
349
+
350
+ const parameters =
351
+ registeredModelContextProvider?.getModelContext?.().tools?.update_note
352
+ ?.parameters;
353
+ expect(parameters).toMatchObject({
354
+ properties: {
355
+ id: { type: "string" },
356
+ second: { type: "number" },
357
+ },
358
+ });
359
+ expect(parameters).not.toMatchObject({
360
+ properties: { first: expect.anything() },
361
+ });
362
+
363
+ second();
364
+ replacement();
365
+ });
366
+
324
367
  it("installs the update tool UI once per name and removes it with the last anchor", () => {
325
368
  const removeToolUI = vi.fn();
326
369
  const setToolUI = vi.fn(() => removeToolUI);
@@ -88,6 +88,9 @@ const useInteractablesResource = ({
88
88
 
89
89
  const subscribersRef = useRef(new Set<() => void>());
90
90
  const partialSchemaCacheRef = useRef(new Map<string, PartialJSONSchema>());
91
+ const partialSchemaSourceRef = useRef(
92
+ new Map<string, Unstable_InteractableRegistration["stateSchema"]>(),
93
+ );
91
94
  const streamBaselinesRef = useRef(
92
95
  new Map<string, { targetId: string; state: unknown }>(),
93
96
  );
@@ -409,7 +412,9 @@ const useInteractablesResource = ({
409
412
  }
410
413
 
411
414
  // The same id re-registers once per anchor (its create call + each update_*).
412
- if (!partialSchemaCacheRef.current.has(def.id)) {
415
+ if (partialSchemaSourceRef.current.get(def.id) !== def.stateSchema) {
416
+ partialSchemaSourceRef.current.set(def.id, def.stateSchema);
417
+ partialSchemaCacheRef.current.delete(def.id);
413
418
  try {
414
419
  const jsonSchema = toJSONSchema(def.stateSchema);
415
420
  partialSchemaCacheRef.current.set(
@@ -497,6 +502,7 @@ const useInteractablesResource = ({
497
502
  detachedAppStateRef.current.set(def.id, existing.state);
498
503
  }
499
504
  }
505
+ partialSchemaSourceRef.current.delete(def.id);
500
506
  partialSchemaCacheRef.current.delete(def.id);
501
507
  const definitions = nullProtoRecord(prev.definitions);
502
508
  const persistence = nullProtoRecord(prev.persistence);