@assistant-ui/core 0.3.15 → 0.3.16

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 (251) hide show
  1. package/dist/internal.d.ts +7 -2
  2. package/dist/internal.js +8 -3
  3. package/dist/model-context/frame/host.d.ts.map +1 -1
  4. package/dist/model-context/frame/host.js +3 -2
  5. package/dist/model-context/frame/host.js.map +1 -1
  6. package/dist/model-context/frame/provider.d.ts +2 -0
  7. package/dist/model-context/frame/provider.d.ts.map +1 -1
  8. package/dist/model-context/frame/provider.js +88 -25
  9. package/dist/model-context/frame/provider.js.map +1 -1
  10. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  11. package/dist/model-context/interactable-composer-metadata.js +1 -1
  12. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  13. package/dist/model-context/registry.d.ts +2 -2
  14. package/dist/model-context/registry.d.ts.map +1 -1
  15. package/dist/model-context/registry.js +9 -28
  16. package/dist/model-context/registry.js.map +1 -1
  17. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  18. package/dist/react/client/InMemoryThreadList.js +24 -20
  19. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts.map +1 -1
  21. package/dist/react/client/Interactables.js +59 -177
  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 +23 -56
  25. package/dist/react/client/RemoteThreadList.js.map +1 -1
  26. package/dist/react/index.d.ts +2 -1
  27. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  28. package/dist/react/interactables-legacy/Interactables.js +56 -174
  29. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  30. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  31. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  32. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  33. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  34. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  35. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  36. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  37. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  38. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +1 -1
  39. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +1 -1
  40. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  41. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +23 -55
  42. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  43. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  44. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  45. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  46. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  47. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  48. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  49. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  50. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  51. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  52. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  53. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  54. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  55. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  56. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  57. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  58. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  59. package/dist/react/runtimes/external-message-converter.js +33 -233
  60. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  61. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  62. package/dist/runtime/api/composer-runtime.js.map +1 -1
  63. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  64. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  65. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  66. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  67. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  68. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  69. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  70. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  71. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  72. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  73. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  74. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  75. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  76. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  77. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  78. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  79. package/dist/runtime/internal.d.ts +2 -1
  80. package/dist/runtime/internal.js +2 -1
  81. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  82. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  83. package/dist/runtime/queue/message-queue.js +1 -1
  84. package/dist/runtime/queue/message-queue.js.map +1 -1
  85. package/dist/runtime/queue/queue-item.d.ts +12 -0
  86. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  87. package/dist/runtime/queue/queue-item.js +6 -0
  88. package/dist/runtime/queue/queue-item.js.map +1 -0
  89. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  90. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  91. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  92. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  93. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  94. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  95. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  96. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  97. package/dist/runtime/utils/auto-status.d.ts +6 -1
  98. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  99. package/dist/runtime/utils/auto-status.js +7 -1
  100. package/dist/runtime/utils/auto-status.js.map +1 -1
  101. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  102. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  103. package/dist/runtime/utils/external-message-conversion.js +221 -0
  104. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  105. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  106. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  107. package/dist/runtime/utils/external-store-message.js +2 -1
  108. package/dist/runtime/utils/external-store-message.js.map +1 -1
  109. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  110. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  111. package/dist/runtime/utils/message-repository-session.js +130 -0
  112. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  113. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  114. package/dist/runtime/utils/message-repository.js +3 -4
  115. package/dist/runtime/utils/message-repository.js.map +1 -1
  116. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  117. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  118. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  119. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  120. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  121. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  122. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  123. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  124. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  125. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  126. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  127. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  128. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  129. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +122 -20
  130. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  131. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  132. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  133. package/dist/runtimes/local/local-thread-runtime-core.js +20 -2
  134. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  135. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  136. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  137. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  138. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  139. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  140. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  141. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  142. package/dist/store/clients/external-thread.d.ts.map +1 -1
  143. package/dist/store/clients/external-thread.js +74 -99
  144. package/dist/store/clients/external-thread.js.map +1 -1
  145. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  146. package/dist/store/clients/single-thread-list.js +1 -1
  147. package/dist/store/clients/single-thread-list.js.map +1 -1
  148. package/dist/store/clients/suggestions.d.ts.map +1 -1
  149. package/dist/store/clients/suggestions.js +16 -7
  150. package/dist/store/clients/suggestions.js.map +1 -1
  151. package/dist/store/index.d.ts +4 -2
  152. package/dist/store/scope-registration.d.ts +1 -1
  153. package/dist/store/scopes/composer.d.ts +2 -1
  154. package/dist/store/scopes/composer.d.ts.map +1 -1
  155. package/dist/store/scopes/message.d.ts +1 -1
  156. package/dist/store/scopes/queue-item.d.ts +2 -9
  157. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  158. package/dist/store/scopes/queue-item.js +0 -6
  159. package/dist/types/message.d.ts +7 -3
  160. package/dist/types/message.d.ts.map +1 -1
  161. package/dist/types/message.js.map +1 -1
  162. package/dist/utils/data-url.d.ts +14 -1
  163. package/dist/utils/data-url.d.ts.map +1 -1
  164. package/dist/utils/data-url.js +13 -1
  165. package/dist/utils/data-url.js.map +1 -1
  166. package/dist/utils/invoke-user-callback.d.ts +5 -0
  167. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  168. package/dist/utils/invoke-user-callback.js +19 -0
  169. package/dist/utils/invoke-user-callback.js.map +1 -0
  170. package/dist/utils/normalizePartStatus.js.map +1 -1
  171. package/package.json +10 -10
  172. package/src/internal.ts +12 -0
  173. package/src/model-context/frame/host.test.ts +91 -7
  174. package/src/model-context/frame/host.ts +7 -2
  175. package/src/model-context/frame/provider.test.ts +267 -12
  176. package/src/model-context/frame/provider.ts +113 -31
  177. package/src/model-context/interactable-composer-metadata.ts +1 -1
  178. package/src/model-context/registry.test.ts +44 -0
  179. package/src/model-context/registry.ts +13 -35
  180. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  181. package/src/react/client/InMemoryThreadList.ts +7 -1
  182. package/src/react/client/Interactables.ts +19 -178
  183. package/src/react/client/RemoteThreadList.test.ts +96 -0
  184. package/src/react/client/RemoteThreadList.ts +27 -59
  185. package/src/react/interactables-legacy/Interactables.ts +20 -177
  186. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  187. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  188. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +26 -56
  189. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  190. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  191. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  192. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  193. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  194. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  195. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  196. package/src/react/runtimes/external-message-converter.ts +58 -481
  197. package/src/runtime/api/composer-runtime.ts +1 -1
  198. package/src/runtime/api/message-runtime.test.ts +28 -1
  199. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  200. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  201. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  202. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  203. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  204. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  205. package/src/runtime/internal.ts +5 -0
  206. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  207. package/src/runtime/queue/message-queue.ts +1 -4
  208. package/src/runtime/queue/queue-item.ts +10 -0
  209. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  210. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  211. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  212. package/src/runtime/utils/auto-status.ts +33 -0
  213. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  214. package/src/runtime/utils/external-message-conversion.ts +494 -0
  215. package/src/runtime/utils/external-store-message.ts +2 -0
  216. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  217. package/src/runtime/utils/message-repository-session.ts +176 -0
  218. package/src/runtime/utils/message-repository.ts +7 -4
  219. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  220. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  221. package/src/runtime/utils/thread-message-like.ts +2 -0
  222. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  223. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +175 -28
  224. package/src/runtimes/local/local-thread-runtime-core.test.ts +172 -1
  225. package/src/runtimes/local/local-thread-runtime-core.ts +48 -3
  226. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  227. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  228. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  229. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  230. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  231. package/src/store/clients/external-thread.ts +16 -61
  232. package/src/store/clients/single-thread-list.ts +4 -1
  233. package/src/store/clients/suggestions.test.ts +31 -0
  234. package/src/store/clients/suggestions.ts +18 -19
  235. package/src/store/scopes/message.ts +1 -1
  236. package/src/store/scopes/queue-item.ts +2 -9
  237. package/src/tests/MessageRepository.test.ts +77 -0
  238. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  239. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  240. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +164 -0
  241. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  242. package/src/tests/external-thread-attachments.test.tsx +173 -0
  243. package/src/tests/single-thread-list-item.test.tsx +47 -0
  244. package/src/tests/thread-message-like.test.ts +39 -1
  245. package/src/types/message.ts +8 -3
  246. package/src/utils/data-url.test.ts +52 -1
  247. package/src/utils/data-url.ts +21 -0
  248. package/src/utils/invoke-user-callback.test.ts +82 -0
  249. package/src/utils/invoke-user-callback.ts +31 -0
  250. package/src/utils/normalizePartStatus.ts +1 -1
  251. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -99,6 +99,52 @@ describe("AssistantFrameProvider", () => {
99
99
  await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
100
100
  });
101
101
 
102
+ it("defaults to the current origin", async () => {
103
+ const execute = vi.fn(async () => "result");
104
+ AssistantFrameProvider.addModelContextProvider({
105
+ getModelContext: () => ({
106
+ tools: { sensitiveTool: { execute } },
107
+ }),
108
+ });
109
+
110
+ expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
111
+ expect.anything(),
112
+ window.location.origin,
113
+ );
114
+
115
+ dispatchToolCall("https://untrusted.example");
116
+ expect(execute).not.toHaveBeenCalled();
117
+
118
+ dispatchToolCall(window.location.origin);
119
+ await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
120
+ });
121
+
122
+ it("reports a failure even when the thrown error has an empty message", async () => {
123
+ const execute = vi.fn(async () => {
124
+ throw new Error();
125
+ });
126
+ AssistantFrameProvider.addModelContextProvider({
127
+ getModelContext: () => ({
128
+ tools: {
129
+ sensitiveTool: { execute },
130
+ },
131
+ }),
132
+ });
133
+
134
+ dispatchToolCall(window.location.origin);
135
+
136
+ await vi.waitFor(() => {
137
+ const frame = (
138
+ parentWindow.postMessage as ReturnType<typeof vi.fn>
139
+ ).mock.calls
140
+ .map(([data]) => data)
141
+ .find((data) => data?.message?.type === "tool-result");
142
+ expect(frame).toBeDefined();
143
+ expect(frame.message).toHaveProperty("error");
144
+ expect(frame.message).not.toHaveProperty("result");
145
+ });
146
+ });
147
+
102
148
  it("aborts in-flight tool calls when the parent cancels them", async () => {
103
149
  let toolSignal: AbortSignal | undefined;
104
150
  const execute = vi.fn(
@@ -119,10 +165,10 @@ describe("AssistantFrameProvider", () => {
119
165
  }),
120
166
  });
121
167
 
122
- dispatchToolCall("*");
168
+ dispatchToolCall(window.location.origin);
123
169
  await vi.waitFor(() => expect(toolSignal).toBeDefined());
124
170
 
125
- dispatchToolCancel("*");
171
+ dispatchToolCancel(window.location.origin);
126
172
 
127
173
  expect(toolSignal?.aborted).toBe(true);
128
174
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -155,11 +201,11 @@ describe("AssistantFrameProvider", () => {
155
201
  getModelContext: () => ({ tools: { sensitiveTool: { execute } } }),
156
202
  });
157
203
 
158
- dispatchToolCall("*", parentWindow, "tool-a");
159
- dispatchToolCall("*", parentWindow, "tool-b");
204
+ dispatchToolCall(window.location.origin, parentWindow, "tool-a");
205
+ dispatchToolCall(window.location.origin, parentWindow, "tool-b");
160
206
  await vi.waitFor(() => expect(signals.size).toBe(2));
161
207
 
162
- dispatchToolCancel("*", parentWindow, "tool-a");
208
+ dispatchToolCancel(window.location.origin, parentWindow, "tool-a");
163
209
 
164
210
  expect(signals.get("tool-a")?.aborted).toBe(true);
165
211
  expect(signals.get("tool-b")?.aborted).toBe(false);
@@ -183,9 +229,9 @@ describe("AssistantFrameProvider", () => {
183
229
  getModelContext: () => ({ tools: { sensitiveTool: { execute } } }),
184
230
  });
185
231
 
186
- dispatchToolCall("*", parentWindow, "duplicate");
232
+ dispatchToolCall(window.location.origin, parentWindow, "duplicate");
187
233
  await vi.waitFor(() => expect(signals).toHaveLength(1));
188
- dispatchToolCall("*", parentWindow, "duplicate");
234
+ dispatchToolCall(window.location.origin, parentWindow, "duplicate");
189
235
  await vi.waitFor(() => expect(signals).toHaveLength(2));
190
236
 
191
237
  expect(signals[0]?.aborted).toBe(true);
@@ -210,7 +256,7 @@ describe("AssistantFrameProvider", () => {
210
256
  getModelContext: () => ({ tools: { sensitiveTool: { execute } } }),
211
257
  });
212
258
 
213
- dispatchToolCall("*");
259
+ dispatchToolCall(window.location.origin);
214
260
  await vi.waitFor(() => expect(toolSignal).toBeDefined());
215
261
 
216
262
  AssistantFrameProvider.dispose();
@@ -225,7 +271,7 @@ describe("AssistantFrameProvider", () => {
225
271
  error: "AssistantFrameProvider has been disposed",
226
272
  },
227
273
  },
228
- { targetOrigin: "*" },
274
+ { targetOrigin: window.location.origin },
229
275
  );
230
276
  await new Promise((resolve) => setTimeout(resolve, 0));
231
277
  const toolResults = vi
@@ -312,7 +358,185 @@ describe("AssistantFrameProvider", () => {
312
358
  );
313
359
  });
314
360
 
315
- it("resets the origin policy after every provider unsubscribes", () => {
361
+ it("rolls back a provider when registration fails", () => {
362
+ const execute = vi.fn(async () => "result");
363
+ expect(() =>
364
+ AssistantFrameProvider.addModelContextProvider(
365
+ {
366
+ getModelContext: () => ({
367
+ tools: { sensitiveTool: { execute } },
368
+ }),
369
+ subscribe: () => {
370
+ throw new Error("subscribe failed");
371
+ },
372
+ },
373
+ "https://first.example",
374
+ ),
375
+ ).toThrow("subscribe failed");
376
+
377
+ dispatchToolCall("https://first.example");
378
+ expect(execute).not.toHaveBeenCalled();
379
+
380
+ expect(() =>
381
+ AssistantFrameProvider.addModelContextProvider(
382
+ { getModelContext: () => ({}) },
383
+ "https://second.example",
384
+ ),
385
+ ).not.toThrow();
386
+ });
387
+
388
+ it("keeps an existing registration when the same provider fails to register again", async () => {
389
+ const execute = vi.fn(async () => "result");
390
+ const firstUnsubscribe = vi.fn();
391
+ let subscriptionCount = 0;
392
+ const provider = {
393
+ getModelContext: () => ({
394
+ tools: { sensitiveTool: { execute } },
395
+ }),
396
+ subscribe: () => {
397
+ subscriptionCount += 1;
398
+ if (subscriptionCount === 1) return firstUnsubscribe;
399
+ throw new Error("second subscribe failed");
400
+ },
401
+ };
402
+ const releaseFirst = AssistantFrameProvider.addModelContextProvider(
403
+ provider,
404
+ "https://parent.example",
405
+ );
406
+
407
+ expect(() =>
408
+ AssistantFrameProvider.addModelContextProvider(
409
+ provider,
410
+ "https://parent.example",
411
+ ),
412
+ ).toThrow("second subscribe failed");
413
+ expect(firstUnsubscribe).not.toHaveBeenCalled();
414
+
415
+ dispatchToolCall("https://parent.example");
416
+
417
+ await vi.waitFor(() => expect(execute).toHaveBeenCalledOnce());
418
+ releaseFirst();
419
+ expect(firstUnsubscribe).toHaveBeenCalledOnce();
420
+ });
421
+
422
+ it("merges a provider registered more than once only once", () => {
423
+ const provider = {
424
+ getModelContext: () => ({ system: "shared system" }),
425
+ };
426
+
427
+ AssistantFrameProvider.addModelContextProvider(provider);
428
+ AssistantFrameProvider.addModelContextProvider(provider);
429
+
430
+ expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
431
+ {
432
+ channel: FRAME_MESSAGE_CHANNEL,
433
+ message: {
434
+ type: "model-context-update",
435
+ context: {
436
+ system: "shared system",
437
+ tools: {},
438
+ },
439
+ },
440
+ },
441
+ window.location.origin,
442
+ );
443
+ });
444
+
445
+ it("releases a subscription when the initial broadcast fails", () => {
446
+ const unsubscribe = vi.fn();
447
+ expect(() =>
448
+ AssistantFrameProvider.addModelContextProvider(
449
+ {
450
+ getModelContext: () => {
451
+ throw new Error("context failed");
452
+ },
453
+ subscribe: () => unsubscribe,
454
+ },
455
+ "https://first.example",
456
+ ),
457
+ ).toThrow("context failed");
458
+
459
+ expect(unsubscribe).toHaveBeenCalledOnce();
460
+ expect(() =>
461
+ AssistantFrameProvider.addModelContextProvider(
462
+ { getModelContext: () => ({}) },
463
+ "https://second.example",
464
+ ),
465
+ ).not.toThrow();
466
+ });
467
+
468
+ it("reports rollback cleanup failures without replacing the original error", () => {
469
+ const contextError = new Error("context failed");
470
+ const unsubscribeError = new Error("unsubscribe failed");
471
+ const consoleError = vi
472
+ .spyOn(console, "error")
473
+ .mockImplementation(() => undefined);
474
+
475
+ expect(() =>
476
+ AssistantFrameProvider.addModelContextProvider({
477
+ getModelContext: () => {
478
+ throw contextError;
479
+ },
480
+ subscribe: () => () => {
481
+ throw unsubscribeError;
482
+ },
483
+ }),
484
+ ).toThrow(contextError);
485
+
486
+ expect(consoleError).toHaveBeenCalledWith(unsubscribeError);
487
+ });
488
+
489
+ it("cleans up provider state when its unsubscribe throws", () => {
490
+ const unsubscribe = vi.fn(() => {
491
+ throw new Error("unsubscribe failed");
492
+ });
493
+ const release = AssistantFrameProvider.addModelContextProvider(
494
+ {
495
+ getModelContext: () => ({}),
496
+ subscribe: () => unsubscribe,
497
+ },
498
+ "https://first.example",
499
+ );
500
+
501
+ expect(release).toThrow("unsubscribe failed");
502
+ expect(unsubscribe).toHaveBeenCalledOnce();
503
+ expect(() =>
504
+ AssistantFrameProvider.addModelContextProvider(
505
+ { getModelContext: () => ({}) },
506
+ "https://second.example",
507
+ ),
508
+ ).not.toThrow();
509
+ });
510
+
511
+ it("finishes disposal when a provider unsubscribe throws", () => {
512
+ const error = new Error("unsubscribe failed");
513
+ const firstUnsubscribe = vi.fn(() => {
514
+ throw error;
515
+ });
516
+ const secondUnsubscribe = vi.fn();
517
+ AssistantFrameProvider.addModelContextProvider({
518
+ getModelContext: () => ({}),
519
+ subscribe: () => firstUnsubscribe,
520
+ });
521
+ AssistantFrameProvider.addModelContextProvider({
522
+ getModelContext: () => ({}),
523
+ subscribe: () => secondUnsubscribe,
524
+ });
525
+
526
+ expect(() => AssistantFrameProvider.dispose()).toThrow(error);
527
+ expect(firstUnsubscribe).toHaveBeenCalledOnce();
528
+ expect(secondUnsubscribe).toHaveBeenCalledOnce();
529
+
530
+ expect(() =>
531
+ AssistantFrameProvider.addModelContextProvider(
532
+ { getModelContext: () => ({}) },
533
+ "https://new.example",
534
+ ),
535
+ ).not.toThrow();
536
+ expect(window.addEventListener).toHaveBeenCalledTimes(2);
537
+ });
538
+
539
+ it("returns to the same-origin policy after every provider unsubscribes", () => {
316
540
  const unsubscribe = AssistantFrameProvider.addModelContextProvider(
317
541
  { getModelContext: () => ({}) },
318
542
  "https://first.example",
@@ -322,7 +546,7 @@ describe("AssistantFrameProvider", () => {
322
546
 
323
547
  expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
324
548
  expect.anything(),
325
- "*",
549
+ window.location.origin,
326
550
  );
327
551
 
328
552
  expect(() =>
@@ -360,7 +584,7 @@ describe("AssistantFrameProvider", () => {
360
584
 
361
585
  expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
362
586
  expect.anything(),
363
- "*",
587
+ window.location.origin,
364
588
  );
365
589
  });
366
590
 
@@ -382,6 +606,37 @@ describe("AssistantFrameProvider", () => {
382
606
  );
383
607
  });
384
608
 
609
+ it("returns to the same-origin policy after a wildcard provider unsubscribes", () => {
610
+ const unsubscribe = AssistantFrameProvider.addModelContextProvider(
611
+ { getModelContext: () => ({}) },
612
+ "*",
613
+ );
614
+
615
+ unsubscribe();
616
+
617
+ expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
618
+ expect.anything(),
619
+ window.location.origin,
620
+ );
621
+ });
622
+
623
+ it("allows opting back into a wildcard policy after every provider unsubscribes", () => {
624
+ const unsubscribe = AssistantFrameProvider.addModelContextProvider({
625
+ getModelContext: () => ({}),
626
+ });
627
+ unsubscribe();
628
+
629
+ AssistantFrameProvider.addModelContextProvider(
630
+ { getModelContext: () => ({}) },
631
+ "*",
632
+ );
633
+
634
+ expect(parentWindow.postMessage).toHaveBeenLastCalledWith(
635
+ expect.anything(),
636
+ "*",
637
+ );
638
+ });
639
+
385
640
  it("keeps a shared strict origin after one provider unsubscribes", async () => {
386
641
  const unsubscribeFirst = AssistantFrameProvider.addModelContextProvider(
387
642
  { getModelContext: () => ({}) },
@@ -29,22 +29,22 @@ const serializeModelContext = (
29
29
  }),
30
30
  });
31
31
 
32
+ const getDefaultTargetOrigin = () => window.location.origin;
33
+
32
34
  export class AssistantFrameProvider {
33
35
  private static _instance: AssistantFrameProvider | null = null;
34
36
 
35
- private _providers = new Set<ModelContextProvider>();
36
- private _providerUnsubscribes = new Map<
37
- ModelContextProvider,
38
- Unsubscribe | undefined
39
- >();
37
+ private _providers = new Map<symbol, ModelContextProvider>();
38
+ private _providerUnsubscribes = new Map<symbol, Unsubscribe | undefined>();
40
39
  private _activeToolCalls = new Map<
41
40
  string,
42
41
  { abortController: AbortController; event: MessageEvent }
43
42
  >();
44
43
  private _targetOrigin: string;
45
44
  private _strictRegistrations = 0;
45
+ private _wildcardRegistrations = 0;
46
46
 
47
- private constructor(targetOrigin: string = "*") {
47
+ private constructor(targetOrigin: string = getDefaultTargetOrigin()) {
48
48
  this._targetOrigin = targetOrigin;
49
49
  this.handleMessage = this.handleMessage.bind(this);
50
50
  window.addEventListener("message", this.handleMessage);
@@ -63,8 +63,17 @@ export class AssistantFrameProvider {
63
63
  return AssistantFrameProvider._instance;
64
64
  }
65
65
 
66
- private reconcileTargetOrigin(targetOrigin: string = "*") {
67
- if (targetOrigin === "*" || targetOrigin === this._targetOrigin) return;
66
+ private reconcileTargetOrigin(
67
+ targetOrigin: string = getDefaultTargetOrigin(),
68
+ ) {
69
+ if (targetOrigin === this._targetOrigin) return;
70
+
71
+ if (this._providers.size === 0) {
72
+ this._targetOrigin = targetOrigin;
73
+ return;
74
+ }
75
+
76
+ if (targetOrigin === "*") return;
68
77
 
69
78
  if (this._targetOrigin === "*") {
70
79
  this._targetOrigin = targetOrigin;
@@ -141,7 +150,7 @@ export class AssistantFrameProvider {
141
150
  this.sendMessage(event, {
142
151
  type: "tool-result",
143
152
  id: message.id,
144
- ...(error ? { error } : { result }),
153
+ ...(error !== undefined ? { error } : { result }),
145
154
  });
146
155
  }
147
156
 
@@ -160,7 +169,7 @@ export class AssistantFrameProvider {
160
169
  }
161
170
 
162
171
  private getModelContext(): ModelContext {
163
- const contexts = Array.from(this._providers).map((p) =>
172
+ const contexts = Array.from(new Set(this._providers.values())).map((p) =>
164
173
  p.getModelContext(),
165
174
  );
166
175
 
@@ -191,34 +200,87 @@ export class AssistantFrameProvider {
191
200
  }
192
201
  }
193
202
 
203
+ private removeProvider(id: symbol, origin: string): Unsubscribe | undefined {
204
+ this._providers.delete(id);
205
+ const unsubscribe = this._providerUnsubscribes.get(id);
206
+ this._providerUnsubscribes.delete(id);
207
+ if (origin === "*") {
208
+ this._wildcardRegistrations -= 1;
209
+ if (
210
+ this._wildcardRegistrations === 0 &&
211
+ this._strictRegistrations === 0
212
+ ) {
213
+ this._targetOrigin = getDefaultTargetOrigin();
214
+ }
215
+ } else {
216
+ this._strictRegistrations -= 1;
217
+ if (this._strictRegistrations === 0) {
218
+ this._targetOrigin =
219
+ this._wildcardRegistrations > 0 ? "*" : getDefaultTargetOrigin();
220
+ }
221
+ }
222
+ return unsubscribe;
223
+ }
224
+
194
225
  static addModelContextProvider(
195
226
  provider: ModelContextProvider,
196
227
  targetOrigin?: string,
197
228
  ): Unsubscribe {
198
- const origin = targetOrigin ?? "*";
229
+ const origin = targetOrigin ?? getDefaultTargetOrigin();
199
230
  const instance = AssistantFrameProvider.getInstance(origin);
200
- instance._providers.add(provider);
201
- if (origin !== "*") instance._strictRegistrations += 1;
202
-
203
- const unsubscribe = provider.subscribe?.(() => instance.broadcastUpdate());
204
- if (unsubscribe) {
205
- instance._providerUnsubscribes.set(provider, unsubscribe);
231
+ const id = Symbol();
232
+ instance._providers.set(id, provider);
233
+ if (origin === "*") {
234
+ instance._wildcardRegistrations += 1;
235
+ } else {
236
+ instance._strictRegistrations += 1;
206
237
  }
207
238
 
208
- instance.broadcastUpdate();
239
+ try {
240
+ const unsubscribe = provider.subscribe?.(() =>
241
+ instance.broadcastUpdate(),
242
+ );
243
+ if (unsubscribe) {
244
+ instance._providerUnsubscribes.set(id, unsubscribe);
245
+ }
246
+
247
+ instance.broadcastUpdate();
248
+ } catch (error) {
249
+ const unsubscribe = instance.removeProvider(id, origin);
250
+ // Rollback failures must not replace the registration error.
251
+ try {
252
+ unsubscribe?.();
253
+ } catch (unsubscribeError) {
254
+ console.error(unsubscribeError);
255
+ }
256
+ try {
257
+ instance.broadcastUpdate();
258
+ } catch (broadcastError) {
259
+ console.error(broadcastError);
260
+ }
261
+ throw error;
262
+ }
209
263
 
210
264
  let released = false;
211
265
  return () => {
212
266
  if (released) return;
213
267
  released = true;
214
- instance._providers.delete(provider);
215
- instance._providerUnsubscribes.get(provider)?.();
216
- instance._providerUnsubscribes.delete(provider);
217
- if (origin !== "*") {
218
- instance._strictRegistrations -= 1;
219
- if (instance._strictRegistrations === 0) instance._targetOrigin = "*";
268
+ const unsubscribe = instance.removeProvider(id, origin);
269
+ let unsubscribeFailed = false;
270
+ let unsubscribeError: unknown;
271
+ try {
272
+ unsubscribe?.();
273
+ } catch (error) {
274
+ unsubscribeFailed = true;
275
+ unsubscribeError = error;
220
276
  }
221
- instance.broadcastUpdate();
277
+ try {
278
+ instance.broadcastUpdate();
279
+ } catch (error) {
280
+ if (!unsubscribeFailed) throw error;
281
+ console.error(error);
282
+ }
283
+ if (unsubscribeFailed) throw unsubscribeError;
222
284
  };
223
285
  }
224
286
 
@@ -227,20 +289,40 @@ export class AssistantFrameProvider {
227
289
  const instance = AssistantFrameProvider._instance;
228
290
  window.removeEventListener("message", instance.handleMessage);
229
291
 
230
- instance._providerUnsubscribes.forEach((unsubscribe) => unsubscribe?.());
292
+ let cleanupFailed = false;
293
+ let cleanupError: unknown;
294
+ const runCleanup = (cleanup: () => void) => {
295
+ try {
296
+ cleanup();
297
+ } catch (error) {
298
+ if (cleanupFailed) {
299
+ console.error(error);
300
+ } else {
301
+ cleanupFailed = true;
302
+ cleanupError = error;
303
+ }
304
+ }
305
+ };
306
+
307
+ instance._providerUnsubscribes.forEach((unsubscribe) => {
308
+ if (unsubscribe) runCleanup(unsubscribe);
309
+ });
231
310
  instance._providerUnsubscribes.clear();
232
311
  instance._providers.clear();
233
312
  instance._activeToolCalls.forEach(({ abortController, event }, id) => {
234
- abortController.abort();
235
- instance.sendMessage(event, {
236
- type: "tool-result",
237
- id,
238
- error: "AssistantFrameProvider has been disposed",
313
+ runCleanup(() => {
314
+ abortController.abort();
315
+ instance.sendMessage(event, {
316
+ type: "tool-result",
317
+ id,
318
+ error: "AssistantFrameProvider has been disposed",
319
+ });
239
320
  });
240
321
  });
241
322
  instance._activeToolCalls.clear();
242
323
 
243
324
  AssistantFrameProvider._instance = null;
325
+ if (cleanupFailed) throw cleanupError;
244
326
  }
245
327
  }
246
328
  }
@@ -33,7 +33,7 @@ type SnapshotCarrierMessage = {
33
33
  * `metadata.custom.interactables`, or `undefined` if none. This is the read
34
34
  * half of the snapshot channel — integrations use it to surface interactable
35
35
  * state to the model (see `unstable_injectInteractableContext` in
36
- * `@assistant-ui/react-ai-sdk` for the AI SDK implementation).
36
+ * `@assistant-ui/ai-sdk` for the AI SDK implementation).
37
37
  *
38
38
  * @deprecated Unstable / Experimental (not actually removed).
39
39
  */
@@ -128,4 +128,48 @@ describe("ModelContextRegistry", () => {
128
128
  expect(() => publishUpdate()).toThrow(error);
129
129
  expect(laterSubscriber).toHaveBeenCalledTimes(1);
130
130
  });
131
+
132
+ it("notifies subscribers exactly once when a provider changes", () => {
133
+ const registry = new ModelContextRegistry();
134
+ const callbacks = new Set<() => void>();
135
+ const subscriber = vi.fn();
136
+
137
+ registry.addProvider({
138
+ getModelContext: () => ({ system: "provider instructions" }),
139
+ subscribe: (callback) => {
140
+ callbacks.add(callback);
141
+ return () => callbacks.delete(callback);
142
+ },
143
+ });
144
+ registry.subscribe(subscriber);
145
+
146
+ for (const callback of callbacks) {
147
+ callback();
148
+ }
149
+
150
+ expect(subscriber).toHaveBeenCalledTimes(1);
151
+ });
152
+
153
+ it("removes a provider's context and subscription through the addProvider handle", () => {
154
+ const registry = new ModelContextRegistry();
155
+ const unsubscribe = vi.fn();
156
+
157
+ const handle = registry.addProvider({
158
+ getModelContext: () => ({ system: "provider instructions" }),
159
+ subscribe: () => unsubscribe,
160
+ });
161
+ expect(registry.getModelContext().system).toContain(
162
+ "provider instructions",
163
+ );
164
+
165
+ const subscriber = vi.fn();
166
+ registry.subscribe(subscriber);
167
+ handle.remove();
168
+
169
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
170
+ expect(registry.getModelContext().system ?? "").not.toContain(
171
+ "provider instructions",
172
+ );
173
+ expect(subscriber).toHaveBeenCalledTimes(1);
174
+ });
131
175
  });