@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
@@ -1 +1 @@
1
- {"version":3,"file":"wire-media.js","names":["dataUrlMediaType","isParsableUrl","parseDataUrl","detectImageMediaType","resolveImageMediaType","image","contentType","startsWith","declared","parsed","payload","data","undefined","sniffed","resolveFileMediaType","mimeType","toMediaWireUrl","mediaType"],"sources":["../../src/utils/wire-media.ts"],"sourcesContent":["import { dataUrlMediaType, isParsableUrl, parseDataUrl } from \"./data-url\";\nimport { detectImageMediaType } from \"./image-media-type\";\n\n/**\n * The media type to declare for an `ImageMessagePart`, which carries none of\n * its own: an attachment's content type, then the payload's data URL\n * declaration, then its leading bytes, then `image/png`.\n *\n * The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK\n * for a url source and whenever inline bytes fail to sniff, so it fails exactly\n * where a fallback is needed.\n */\nexport function resolveImageMediaType(\n image: string,\n contentType?: string | undefined,\n): string {\n if (contentType?.startsWith(\"image/\")) return contentType;\n\n const declared = dataUrlMediaType(image);\n if (declared?.startsWith(\"image/\")) return declared;\n\n // Read through a data URL envelope too, so a generic one such as\n // `application/octet-stream` does not mask the format. A url of any other\n // scheme has no local bytes to read.\n const parsed = parseDataUrl(image);\n const payload = parsed?.data ?? (isParsableUrl(image) ? undefined : image);\n if (payload !== undefined) {\n const sniffed = detectImageMediaType(payload);\n if (sniffed) return sniffed;\n }\n\n return \"image/png\";\n}\n\n/**\n * The media type to declare for a `FileMessagePart`: its own `mimeType`, then\n * the payload's data URL declaration, then `application/octet-stream`.\n *\n * `mimeType` is a plain string, and an adapter reading `file.type` on a file\n * the OS cannot type yields `\"\"`, so the declared value is not always present.\n */\nexport function resolveFileMediaType(\n data: string,\n mimeType?: string | undefined,\n): string {\n return mimeType || dataUrlMediaType(data) || \"application/octet-stream\";\n}\n\n/**\n * A payload placed into a wire field that is contractually a url.\n *\n * Two hazards drive this. A consumer may hand the value to an unguarded\n * `new URL()`, so a payload that is not a url has to be wrapped; and a data\n * URL's own media type wins over a separately declared one downstream, so a\n * base64 envelope that disagrees with `mediaType` is rebuilt around the same\n * bytes. Everything else passes through byte for byte: an envelope that agrees,\n * a url of any other scheme, and a percent-encoded data URL, whose declaration\n * cannot be corrected without transcoding the payload and is authoritative for\n * the bytes it carries anyway.\n */\nexport function toMediaWireUrl(payload: string, mediaType: string): string {\n const parsed = parseDataUrl(payload);\n if (parsed) {\n return parsed.mimeType === mediaType\n ? payload\n : `data:${mediaType};base64,${parsed.data}`;\n }\n if (isParsableUrl(payload)) return payload;\n return `data:${mediaType};base64,${payload}`;\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgBI,sBACdC,OACAC,aACQ;CACR,IAAIA,aAAaC,WAAW,QAAQ,GAAG,OAAOD;CAE9C,MAAME,WAAWR,iBAAiBK,KAAK;CACvC,IAAIG,UAAUD,WAAW,QAAQ,GAAG,OAAOC;CAM3C,MAAME,UADSR,aAAaG,KACZI,CAAM,EAAEE,SAASV,cAAcI,KAAK,IAAIO,KAAAA,IAAYP;CACpE,IAAIK,YAAYE,KAAAA,GAAW;EACzB,MAAMC,UAAUV,qBAAqBO,OAAO;EAC5C,IAAIG,SAAS,OAAOA;CACtB;CAEA,OAAO;AACT;;;;;;;;AASA,SAAgBC,qBACdH,MACAI,UACQ;CACR,OAAOA,YAAYf,iBAAiBW,IAAI,KAAK;AAC/C;;;;;;;;;;;;;AAcA,SAAgBK,eAAeN,SAAiBO,WAA2B;CACzE,MAAMR,SAASP,aAAaQ,OAAO;CACnC,IAAID,QACF,OAAOA,OAAOM,aAAaE,YACvBP,UACA,QAAQO,UAAS,UAAWR,OAAOE;CAEzC,IAAIV,cAAcS,OAAO,GAAG,OAAOA;CACnC,OAAO,QAAQO,UAAS,UAAWP;AACrC"}
1
+ {"version":3,"file":"wire-media.js","names":["dataUrlMediaType","isParsableUrl","parseDataUrl","detectImageMediaType","resolveImageMediaType","image","contentType","startsWith","includes","declared","parsed","payload","data","undefined","sniffed","resolveFileMediaType","mimeType","toMediaWireUrl","mediaType"],"sources":["../../src/utils/wire-media.ts"],"sourcesContent":["import { dataUrlMediaType, isParsableUrl, parseDataUrl } from \"./data-url\";\nimport { detectImageMediaType } from \"./image-media-type\";\n\n/**\n * The media type to declare for an `ImageMessagePart`, which carries none of\n * its own: an attachment's content type, then the payload's data URL\n * declaration, then its leading bytes, then `image/png`.\n *\n * The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK\n * for a url source and whenever inline bytes fail to sniff, so it fails exactly\n * where a fallback is needed.\n */\nexport function resolveImageMediaType(\n image: string,\n contentType?: string | undefined,\n): string {\n if (contentType?.startsWith(\"image/\") && !contentType.includes(\"*\")) {\n return contentType;\n }\n\n const declared = dataUrlMediaType(image);\n if (declared?.startsWith(\"image/\") && !declared.includes(\"*\")) {\n return declared;\n }\n\n // Read through a data URL envelope too, so a generic one such as\n // `application/octet-stream` does not mask the format. A url of any other\n // scheme has no local bytes to read.\n const parsed = parseDataUrl(image);\n const payload = parsed?.data ?? (isParsableUrl(image) ? undefined : image);\n if (payload !== undefined) {\n const sniffed = detectImageMediaType(payload);\n if (sniffed) return sniffed;\n }\n\n return \"image/png\";\n}\n\n/**\n * The media type to declare for a `FileMessagePart`: its own `mimeType`, then\n * the payload's data URL declaration, then `application/octet-stream`.\n *\n * `mimeType` is a plain string, and an adapter reading `file.type` on a file\n * the OS cannot type yields `\"\"`, so the declared value is not always present.\n */\nexport function resolveFileMediaType(\n data: string,\n mimeType?: string | undefined,\n): string {\n return mimeType || dataUrlMediaType(data) || \"application/octet-stream\";\n}\n\n/**\n * A payload placed into a wire field that is contractually a url.\n *\n * Two hazards drive this. A consumer may hand the value to an unguarded\n * `new URL()`, so a payload that is not a url has to be wrapped; and a data\n * URL's own media type wins over a separately declared one downstream, so a\n * base64 envelope that disagrees with `mediaType` is rebuilt around the same\n * bytes. Everything else passes through byte for byte: an envelope that agrees,\n * a url of any other scheme, and a percent-encoded data URL, whose declaration\n * cannot be corrected without transcoding the payload and is authoritative for\n * the bytes it carries anyway.\n */\nexport function toMediaWireUrl(payload: string, mediaType: string): string {\n const parsed = parseDataUrl(payload);\n if (parsed) {\n return parsed.mimeType === mediaType\n ? payload\n : `data:${mediaType};base64,${parsed.data}`;\n }\n if (isParsableUrl(payload)) return payload;\n return `data:${mediaType};base64,${payload}`;\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgBI,sBACdC,OACAC,aACQ;CACR,IAAIA,aAAaC,WAAW,QAAQ,KAAK,CAACD,YAAYE,SAAS,GAAG,GAChE,OAAOF;CAGT,MAAMG,WAAWT,iBAAiBK,KAAK;CACvC,IAAII,UAAUF,WAAW,QAAQ,KAAK,CAACE,SAASD,SAAS,GAAG,GAC1D,OAAOC;CAOT,MAAME,UADST,aAAaG,KACZK,CAAM,EAAEE,SAASX,cAAcI,KAAK,IAAIQ,KAAAA,IAAYR;CACpE,IAAIM,YAAYE,KAAAA,GAAW;EACzB,MAAMC,UAAUX,qBAAqBQ,OAAO;EAC5C,IAAIG,SAAS,OAAOA;CACtB;CAEA,OAAO;AACT;;;;;;;;AASA,SAAgBC,qBACdH,MACAI,UACQ;CACR,OAAOA,YAAYhB,iBAAiBY,IAAI,KAAK;AAC/C;;;;;;;;;;;;;AAcA,SAAgBK,eAAeN,SAAiBO,WAA2B;CACzE,MAAMR,SAASR,aAAaS,OAAO;CACnC,IAAID,QACF,OAAOA,OAAOM,aAAaE,YACvBP,UACA,QAAQO,UAAS,UAAWR,OAAOE;CAEzC,IAAIX,cAAcU,OAAO,GAAG,OAAOA;CACnC,OAAO,QAAQO,UAAS,UAAWP;AACrC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/core",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "Framework-agnostic core runtime for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "sideEffects": false,
61
61
  "dependencies": {
62
- "assistant-stream": "^0.3.42",
62
+ "assistant-stream": "^0.3.43",
63
63
  "nanoid": "^6.0.1"
64
64
  },
65
65
  "optionalDevDependencies": {
@@ -70,7 +70,7 @@
70
70
  "@assistant-ui/tap": "^0.9.17",
71
71
  "@types/react": "*",
72
72
  "react": "^18 || ^19",
73
- "assistant-cloud": "^0.2.0"
73
+ "assistant-cloud": "^0.2.1"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@types/react": {
@@ -87,14 +87,14 @@
87
87
  "@assistant-ui/store": "0.3.13",
88
88
  "@assistant-ui/tap": "0.9.17",
89
89
  "@assistant-ui/vite": "0.0.17",
90
- "@assistant-ui/x-buildutils": "0.0.26",
90
+ "@assistant-ui/x-buildutils": "0.0.27",
91
91
  "@testing-library/react": "^16.3.3",
92
92
  "@types/react": "^19.2.18",
93
- "assistant-cloud": "0.2.0",
93
+ "assistant-cloud": "0.2.1",
94
94
  "jsdom": "^30.0.1",
95
95
  "react": "^19.2.8",
96
96
  "react-dom": "^19.2.8",
97
- "vitest": "^4.1.11"
97
+ "vitest": "^5.0.0"
98
98
  },
99
99
  "publishConfig": {
100
100
  "access": "public",
@@ -7,6 +7,62 @@ afterEach(() => {
7
7
  });
8
8
 
9
9
  describe("WebSpeechSynthesisAdapter", () => {
10
+ const stubSpeechSynthesis = () => {
11
+ const utterances: EventTarget[] = [];
12
+ class MockSpeechSynthesisUtterance extends EventTarget {
13
+ constructor() {
14
+ super();
15
+ utterances.push(this);
16
+ }
17
+ }
18
+ const cancel = vi.fn();
19
+ vi.stubGlobal("SpeechSynthesisUtterance", MockSpeechSynthesisUtterance);
20
+ vi.stubGlobal("window", { speechSynthesis: { speak: vi.fn(), cancel } });
21
+ return { utterances, cancel };
22
+ };
23
+
24
+ it.each(["end", "error"])(
25
+ "does not cancel newer playback through a handle that received %s",
26
+ (event) => {
27
+ const { utterances, cancel } = stubSpeechSynthesis();
28
+ const adapter = new WebSpeechSynthesisAdapter();
29
+ const old = adapter.speak("old");
30
+ utterances[0]!.dispatchEvent(new Event(event));
31
+ const endedStatus = old.status;
32
+ const current = adapter.speak("current");
33
+
34
+ old.cancel();
35
+ old.cancel();
36
+
37
+ expect(cancel).not.toHaveBeenCalled();
38
+ expect(old.status).toBe(endedStatus);
39
+ expect(current.status).toEqual({ type: "running" });
40
+ },
41
+ );
42
+
43
+ it("cancels active playback once and leaves later playback alone", () => {
44
+ const { cancel } = stubSpeechSynthesis();
45
+ const adapter = new WebSpeechSynthesisAdapter();
46
+ const old = adapter.speak("old");
47
+ const onChange = vi.fn();
48
+ old.subscribe(onChange);
49
+
50
+ old.cancel();
51
+
52
+ expect(cancel).toHaveBeenCalledOnce();
53
+ expect(old.status).toMatchObject({ type: "ended", reason: "cancelled" });
54
+ expect(onChange).toHaveBeenCalledOnce();
55
+
56
+ const current = adapter.speak("current");
57
+ old.cancel();
58
+
59
+ expect(cancel).toHaveBeenCalledOnce();
60
+ expect(onChange).toHaveBeenCalledOnce();
61
+ expect(current.status).toEqual({ type: "running" });
62
+ current.cancel();
63
+ expect(cancel).toHaveBeenCalledTimes(2);
64
+ });
65
+
10
66
  it("isolates a late subscriber that throws after the utterance ended", async () => {
11
67
  const listeners = new Map<string, EventListener>();
12
68
  class MockSpeechSynthesisUtterance {
@@ -109,6 +165,186 @@ describe("WebSpeechDictationAdapter", () => {
109
165
  return listeners;
110
166
  };
111
167
 
168
+ const emitResults = (
169
+ listeners: Map<string, EventListener>,
170
+ results: [transcript: string, isFinal: boolean][],
171
+ resultIndex = 0,
172
+ ) => {
173
+ listeners.get("result")!({
174
+ resultIndex,
175
+ results: results.map(([transcript, isFinal]) => ({
176
+ 0: { transcript },
177
+ isFinal,
178
+ })),
179
+ } as unknown as Event);
180
+ };
181
+
182
+ it("publishes the entire interim suffix when only its last result changes", () => {
183
+ const listeners = stubSpeechRecognition();
184
+ const session = new WebSpeechDictationAdapter().listen();
185
+ const onSpeech = vi.fn();
186
+ session.onSpeech(onSpeech);
187
+
188
+ emitResults(listeners, [
189
+ ["hello ", false],
190
+ ["world", false],
191
+ ]);
192
+ emitResults(
193
+ listeners,
194
+ [
195
+ ["hello ", false],
196
+ ["there", false],
197
+ ],
198
+ 1,
199
+ );
200
+
201
+ expect(onSpeech.mock.calls).toEqual([
202
+ [{ transcript: "hello world", isFinal: false }],
203
+ [{ transcript: "hello there", isFinal: false }],
204
+ ]);
205
+ });
206
+
207
+ it("publishes partial and complete retractions with no changed results", () => {
208
+ const listeners = stubSpeechRecognition();
209
+ const session = new WebSpeechDictationAdapter().listen();
210
+ const onSpeech = vi.fn();
211
+ session.onSpeech(onSpeech);
212
+
213
+ emitResults(listeners, [
214
+ ["hello ", false],
215
+ ["world", false],
216
+ ]);
217
+ emitResults(listeners, [["hello ", false]], 1);
218
+ emitResults(listeners, []);
219
+ emitResults(listeners, []);
220
+
221
+ expect(onSpeech.mock.calls).toEqual([
222
+ [{ transcript: "hello world", isFinal: false }],
223
+ [{ transcript: "hello ", isFinal: false }],
224
+ [{ transcript: "", isFinal: false }],
225
+ ]);
226
+ });
227
+
228
+ it("delivers changed final results once before the remaining interim suffix", () => {
229
+ const listeners = stubSpeechRecognition();
230
+ const session = new WebSpeechDictationAdapter().listen();
231
+ const onSpeech = vi.fn();
232
+ const onEnd = vi.fn();
233
+ session.onSpeech(onSpeech);
234
+ session.onSpeechEnd(onEnd);
235
+
236
+ emitResults(listeners, [
237
+ ["hello ", true],
238
+ ["wor", false],
239
+ ["ld", false],
240
+ ]);
241
+ emitResults(
242
+ listeners,
243
+ [
244
+ ["hello ", true],
245
+ ["world", true],
246
+ ],
247
+ 1,
248
+ );
249
+ listeners.get("end")!(new Event("end"));
250
+
251
+ expect(onSpeech.mock.calls).toEqual([
252
+ [{ transcript: "hello ", isFinal: true }],
253
+ [{ transcript: "world", isFinal: false }],
254
+ [{ transcript: "world", isFinal: true }],
255
+ ]);
256
+ expect(onEnd).toHaveBeenCalledExactlyOnceWith({
257
+ transcript: "hello world",
258
+ });
259
+ });
260
+
261
+ it("clears an interim suffix while retaining earlier final results", () => {
262
+ const listeners = stubSpeechRecognition();
263
+ const session = new WebSpeechDictationAdapter().listen();
264
+ const onSpeech = vi.fn();
265
+ session.onSpeech(onSpeech);
266
+
267
+ emitResults(listeners, [
268
+ ["hello ", true],
269
+ ["world", false],
270
+ ]);
271
+ emitResults(listeners, [["hello ", true]], 1);
272
+
273
+ expect(onSpeech.mock.calls).toEqual([
274
+ [{ transcript: "hello ", isFinal: true }],
275
+ [{ transcript: "world", isFinal: false }],
276
+ [{ transcript: "", isFinal: false }],
277
+ ]);
278
+ });
279
+
280
+ it("keeps final-only delivery free of empty interim updates", () => {
281
+ const listeners = stubSpeechRecognition();
282
+ const session = new WebSpeechDictationAdapter().listen();
283
+ const onSpeech = vi.fn();
284
+ session.onSpeech(onSpeech);
285
+
286
+ emitResults(listeners, [["hello", true]]);
287
+ emitResults(listeners, [["hello", true]], 1);
288
+
289
+ expect(onSpeech).toHaveBeenCalledExactlyOnceWith({
290
+ transcript: "hello",
291
+ isFinal: true,
292
+ });
293
+ });
294
+
295
+ it("does not drop an unconsumed final when resultIndex skips it", () => {
296
+ const listeners = stubSpeechRecognition();
297
+ const session = new WebSpeechDictationAdapter().listen();
298
+ const onSpeech = vi.fn();
299
+ const onEnd = vi.fn();
300
+ session.onSpeech(onSpeech);
301
+ session.onSpeechEnd(onEnd);
302
+
303
+ emitResults(listeners, [["hello ", false]]);
304
+ emitResults(
305
+ listeners,
306
+ [
307
+ ["hello ", true],
308
+ ["world", false],
309
+ ],
310
+ 1,
311
+ );
312
+ listeners.get("end")!(new Event("end"));
313
+
314
+ expect(onSpeech.mock.calls).toEqual([
315
+ [{ transcript: "hello ", isFinal: false }],
316
+ [{ transcript: "hello ", isFinal: true }],
317
+ [{ transcript: "world", isFinal: false }],
318
+ ]);
319
+ expect(onEnd).toHaveBeenCalledExactlyOnceWith({ transcript: "hello " });
320
+ });
321
+
322
+ it("does not reread historical finalized results during interim updates", () => {
323
+ const listeners = stubSpeechRecognition();
324
+ new WebSpeechDictationAdapter().listen();
325
+ const readFinal = vi.fn(() => ({ transcript: "word " }));
326
+ const finals = Array.from({ length: 1_000 }, () => ({
327
+ get 0() {
328
+ return readFinal();
329
+ },
330
+ isFinal: true,
331
+ }));
332
+ listeners.get("result")!({
333
+ resultIndex: 0,
334
+ results: finals,
335
+ } as unknown as Event);
336
+ expect(readFinal).toHaveBeenCalledTimes(1_000);
337
+
338
+ for (const transcript of ["hel", "hello", "hello world"]) {
339
+ listeners.get("result")!({
340
+ resultIndex: finals.length,
341
+ results: [...finals, { 0: { transcript }, isFinal: false }],
342
+ } as unknown as Event);
343
+ }
344
+
345
+ expect(readFinal).toHaveBeenCalledTimes(1_000);
346
+ });
347
+
112
348
  it("continues notifying dictation listeners when one throws", () => {
113
349
  const listeners = stubSpeechRecognition();
114
350
  const listenerError = new Error("listener failed");
@@ -76,6 +76,7 @@ export class WebSpeechSynthesisAdapter implements SpeechSynthesisAdapter {
76
76
  const res: SpeechSynthesisAdapter.Utterance = {
77
77
  status: { type: "running" },
78
78
  cancel: () => {
79
+ if (res.status.type === "ended") return;
79
80
  window.speechSynthesis.cancel();
80
81
  handleEnd("cancelled");
81
82
  },
@@ -205,6 +206,8 @@ export class WebSpeechDictationAdapter implements DictationAdapter {
205
206
  >();
206
207
 
207
208
  let finalTranscript = "";
209
+ let hasInterimTranscript = false;
210
+ let firstInterimIndex = 0;
208
211
 
209
212
  const session: DictationAdapter.Session = {
210
213
  status: { type: "starting" },
@@ -267,32 +270,36 @@ export class WebSpeechDictationAdapter implements DictationAdapter {
267
270
 
268
271
  recognition.addEventListener("result", (event) => {
269
272
  const speechEvent = event as unknown as SpeechRecognitionEvent;
273
+ let interimTranscript = "";
270
274
 
271
- for (
272
- let i = speechEvent.resultIndex;
273
- i < speechEvent.results.length;
274
- i++
275
- ) {
275
+ for (let i = firstInterimIndex; i < speechEvent.results.length; i++) {
276
276
  const result = speechEvent.results[i];
277
277
  if (!result) continue;
278
278
 
279
279
  const transcript = result[0]?.transcript ?? "";
280
280
 
281
281
  if (result.isFinal) {
282
+ firstInterimIndex = i + 1;
282
283
  finalTranscript += transcript;
284
+ hasInterimTranscript = false;
283
285
  notifyEventListeners(
284
286
  speechCallbacks,
285
287
  () => ({ transcript, isFinal: true }),
286
288
  "Dictation",
287
289
  );
288
290
  } else {
289
- notifyEventListeners(
290
- speechCallbacks,
291
- () => ({ transcript, isFinal: false }),
292
- "Dictation",
293
- );
291
+ interimTranscript += transcript;
294
292
  }
295
293
  }
294
+
295
+ if (interimTranscript || hasInterimTranscript) {
296
+ hasInterimTranscript = interimTranscript.length > 0;
297
+ notifyEventListeners(
298
+ speechCallbacks,
299
+ () => ({ transcript: interimTranscript, isFinal: false }),
300
+ "Dictation",
301
+ );
302
+ }
296
303
  });
297
304
 
298
305
  recognition.addEventListener("speechend", () => {
@@ -7,6 +7,7 @@ import type {
7
7
  ExportedMessageRepositoryItem,
8
8
  } from "../runtime/utils/message-repository";
9
9
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
10
+ import type { ThreadMessage } from "../types";
10
11
 
11
12
  export interface MessageStorageEntry<TPayload> {
12
13
  id: string;
@@ -52,6 +53,8 @@ export type GenericThreadHistoryAdapter<TMessage> = {
52
53
  options?: {
53
54
  durationMs?: number;
54
55
  stepTimestamps?: { start_ms: number; end_ms: number }[];
56
+ /** The thread message the items were persisted from; its status and timing complete a report the stored format cannot carry. */
57
+ message?: ThreadMessage;
55
58
  },
56
59
  ): void;
57
60
  };
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export type {
40
40
  MessageStatus,
41
41
  // Thread messages
42
42
  MessageTiming,
43
+ MessageModality,
43
44
  ThreadStep,
44
45
  ThreadSystemMessage,
45
46
  ThreadUserMessage,
@@ -74,9 +74,31 @@ beforeEach(() => {
74
74
  afterEach(() => {
75
75
  vi.useRealTimers();
76
76
  vi.unstubAllGlobals();
77
+ vi.restoreAllMocks();
77
78
  });
78
79
 
79
80
  describe("AssistantFrameHost", () => {
81
+ it("does not install its message listener when initialization fails", () => {
82
+ const addEventListener = vi.fn();
83
+ const removeEventListener = vi.fn();
84
+ vi.stubGlobal("window", {
85
+ addEventListener,
86
+ removeEventListener,
87
+ location: { origin: DEFAULT_ORIGIN },
88
+ });
89
+ const error = new Error("postMessage failed");
90
+ const iframeWindow = {
91
+ postMessage: vi.fn(() => {
92
+ throw error;
93
+ }),
94
+ } as unknown as Window;
95
+
96
+ expect(() => new AssistantFrameHost(iframeWindow)).toThrow(error);
97
+
98
+ expect(addEventListener).not.toHaveBeenCalled();
99
+ expect(removeEventListener).not.toHaveBeenCalled();
100
+ });
101
+
80
102
  it("defaults to the current origin", () => {
81
103
  const { host, postMessage } = createHost();
82
104
 
@@ -212,8 +234,83 @@ describe("AssistantFrameHost", () => {
212
234
  expect(vi.getTimerCount()).toBe(0);
213
235
  });
214
236
 
237
+ it("settles every pending tool call when cancellation posting fails", async () => {
238
+ const { execute, host, postMessage } = createHost();
239
+ const consoleError = vi
240
+ .spyOn(console, "error")
241
+ .mockImplementation(() => undefined);
242
+ const firstAbortController = new AbortController();
243
+ const secondAbortController = new AbortController();
244
+ const firstRemoveEventListener = vi.spyOn(
245
+ firstAbortController.signal,
246
+ "removeEventListener",
247
+ );
248
+ const secondRemoveEventListener = vi.spyOn(
249
+ secondAbortController.signal,
250
+ "removeEventListener",
251
+ );
252
+ const firstRejected = vi.fn();
253
+ const secondRejected = vi.fn();
254
+
255
+ void execute(
256
+ {},
257
+ { ...executionContext, abortSignal: firstAbortController.signal },
258
+ ).catch(firstRejected);
259
+ void execute(
260
+ {},
261
+ { ...executionContext, abortSignal: secondAbortController.signal },
262
+ ).catch(secondRejected);
263
+
264
+ const firstTransportError = new Error("first tool cancellation failed");
265
+ const secondTransportError = new Error("second tool cancellation failed");
266
+ let cancellationCount = 0;
267
+ postMessage.mockImplementation((data) => {
268
+ if (data.message.type !== "tool-cancel") return;
269
+ cancellationCount += 1;
270
+ throw cancellationCount === 1
271
+ ? firstTransportError
272
+ : secondTransportError;
273
+ });
274
+
275
+ expect(() => host.dispose()).toThrow(firstTransportError);
276
+ await Promise.resolve();
277
+
278
+ expect(firstRejected).toHaveBeenCalledWith(
279
+ expect.objectContaining({
280
+ message: "AssistantFrameHost has been disposed",
281
+ }),
282
+ );
283
+ expect(secondRejected).toHaveBeenCalledWith(
284
+ expect.objectContaining({
285
+ message: "AssistantFrameHost has been disposed",
286
+ }),
287
+ );
288
+ expect(
289
+ postMessage.mock.calls.filter(
290
+ ([data]) => data.message.type === "tool-cancel",
291
+ ),
292
+ ).toHaveLength(2);
293
+ expect(firstRemoveEventListener).toHaveBeenCalledWith(
294
+ "abort",
295
+ expect.any(Function),
296
+ );
297
+ expect(secondRemoveEventListener).toHaveBeenCalledWith(
298
+ "abort",
299
+ expect.any(Function),
300
+ );
301
+ expect(vi.getTimerCount()).toBe(0);
302
+ expect(consoleError).toHaveBeenCalledWith(
303
+ "[assistant-ui] AssistantFrameHost tool cancellation could not be sent.",
304
+ secondTransportError,
305
+ );
306
+ expect(() => host.dispose()).not.toThrow();
307
+ });
308
+
215
309
  it("rejects pending tool calls when execution is aborted", async () => {
216
310
  const { execute, getToolCallId, host, postMessage } = createHost();
311
+ const consoleError = vi
312
+ .spyOn(console, "error")
313
+ .mockImplementation(() => undefined);
217
314
  const abortController = new AbortController();
218
315
  const abortError = new Error("Run cancelled");
219
316
  abortError.name = "AbortError";
@@ -229,8 +326,13 @@ describe("AssistantFrameHost", () => {
229
326
  const onRejected = vi.fn();
230
327
  void result.catch(onRejected);
231
328
  const toolCallId = getToolCallId();
329
+ postMessage.mockImplementation((data) => {
330
+ if (data.message.type === "tool-cancel") {
331
+ throw new Error("tool cancellation failed");
332
+ }
333
+ });
232
334
 
233
- abortController.abort(abortError);
335
+ expect(() => abortController.abort(abortError)).not.toThrow();
234
336
  await Promise.resolve();
235
337
 
236
338
  expect(onRejected).toHaveBeenCalledWith(abortError);
@@ -241,17 +343,26 @@ describe("AssistantFrameHost", () => {
241
343
  },
242
344
  DEFAULT_ORIGIN,
243
345
  );
346
+ expect(consoleError).toHaveBeenCalledOnce();
244
347
  expect(vi.getTimerCount()).toBe(0);
245
348
  host.dispose();
246
349
  });
247
350
 
248
351
  it("cancels tool calls when they time out", async () => {
249
352
  const { execute, getToolCallId, host, postMessage } = createHost();
353
+ const consoleError = vi
354
+ .spyOn(console, "error")
355
+ .mockImplementation(() => undefined);
250
356
  const result = Promise.resolve(execute({}, executionContext));
251
357
  const toolCallId = getToolCallId();
252
358
  const rejection = expect(result).rejects.toThrow(
253
359
  'Tool call "search" timed out',
254
360
  );
361
+ postMessage.mockImplementation((data) => {
362
+ if (data.message.type === "tool-cancel") {
363
+ throw new Error("tool cancellation failed");
364
+ }
365
+ });
255
366
 
256
367
  await vi.advanceTimersByTimeAsync(30000);
257
368
 
@@ -263,6 +374,7 @@ describe("AssistantFrameHost", () => {
263
374
  },
264
375
  DEFAULT_ORIGIN,
265
376
  );
377
+ expect(consoleError).toHaveBeenCalledOnce();
266
378
  expect(vi.getTimerCount()).toBe(0);
267
379
  host.dispose();
268
380
  });
@@ -12,6 +12,13 @@ import {
12
12
 
13
13
  const getDefaultTargetOrigin = () => window.location.origin;
14
14
 
15
+ const logCancellationFailure = (error: unknown) => {
16
+ console.error(
17
+ "[assistant-ui] AssistantFrameHost tool cancellation could not be sent.",
18
+ error,
19
+ );
20
+ };
21
+
15
22
  /**
16
23
  * Deserializes tools from JSON Schema format back to Tool objects
17
24
  */
@@ -73,9 +80,10 @@ export class AssistantFrameHost implements ModelContextProvider {
73
80
  this._targetOrigin = targetOrigin;
74
81
 
75
82
  this.handleMessage = this.handleMessage.bind(this);
76
- window.addEventListener("message", this.handleMessage);
77
-
83
+ // A posted message cannot be delivered before this constructor returns, so
84
+ // requesting first means a failed request leaves no listener to clean up.
78
85
  this.requestContext();
86
+ window.addEventListener("message", this.handleMessage);
79
87
  }
80
88
 
81
89
  private handleMessage(event: MessageEvent) {
@@ -208,14 +216,21 @@ export class AssistantFrameHost implements ModelContextProvider {
208
216
  });
209
217
  }
210
218
 
211
- private cancelToolCall(id: string) {
212
- this._iframeWindow.postMessage(
213
- {
214
- channel: FRAME_MESSAGE_CHANNEL,
215
- message: { type: "tool-cancel", id } satisfies FrameMessage,
216
- },
217
- this._targetOrigin,
218
- );
219
+ private cancelToolCall(
220
+ id: string,
221
+ onError: (error: unknown) => void = logCancellationFailure,
222
+ ) {
223
+ try {
224
+ this._iframeWindow.postMessage(
225
+ {
226
+ channel: FRAME_MESSAGE_CHANNEL,
227
+ message: { type: "tool-cancel", id } satisfies FrameMessage,
228
+ },
229
+ this._targetOrigin,
230
+ );
231
+ } catch (error) {
232
+ onError(error);
233
+ }
219
234
  }
220
235
 
221
236
  private requestContext() {
@@ -248,10 +263,22 @@ export class AssistantFrameHost implements ModelContextProvider {
248
263
  window.removeEventListener("message", this.handleMessage);
249
264
  this._subscribers.clear();
250
265
  const error = new Error("AssistantFrameHost has been disposed");
266
+ let cancellationFailed = false;
267
+ let cancellationError: unknown;
268
+
251
269
  for (const [id, pending] of this._pendingRequests) {
252
- this.cancelToolCall(id);
270
+ this._pendingRequests.delete(id);
271
+ this.cancelToolCall(id, (error) => {
272
+ if (!cancellationFailed) {
273
+ cancellationFailed = true;
274
+ cancellationError = error;
275
+ } else {
276
+ logCancellationFailure(error);
277
+ }
278
+ });
253
279
  pending.reject(error);
254
280
  }
255
- this._pendingRequests.clear();
281
+
282
+ if (cancellationFailed) throw cancellationError;
256
283
  }
257
284
  }