@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
@@ -114,6 +114,53 @@ describe("groupPartByType", () => {
114
114
  ]);
115
115
  });
116
116
 
117
+ it("routes a tool call by name through its tool-call:<name> entry", () => {
118
+ const fn = groupPartByType({
119
+ "tool-call:task": ["group-subagents"],
120
+ "tool-call": ["group-tool"],
121
+ });
122
+
123
+ expect(
124
+ fn(
125
+ part({
126
+ type: "tool-call",
127
+ toolName: "task",
128
+ } as Partial<PartState>),
129
+ ),
130
+ ).toEqual(["group-subagents"]);
131
+ });
132
+
133
+ it("falls back to the plain tool-call entry for other tool names", () => {
134
+ const fn = groupPartByType({
135
+ "tool-call:task": ["group-subagents"],
136
+ "tool-call": ["group-tool"],
137
+ });
138
+
139
+ expect(
140
+ fn(
141
+ part({
142
+ type: "tool-call",
143
+ toolName: "search",
144
+ } as Partial<PartState>),
145
+ ),
146
+ ).toEqual(["group-tool"]);
147
+ });
148
+
149
+ it("routes standalone tool calls before matching a tool name entry", () => {
150
+ const fn = groupPartByType({
151
+ "standalone-tool-call": ["group-standalone"],
152
+ "tool-call:task": ["group-subagents"],
153
+ "tool-call": ["group-tool"],
154
+ });
155
+ const standaloneTask = part({
156
+ type: "tool-call",
157
+ toolName: "task",
158
+ mcp: { app: { resourceUri: "ui://task" } },
159
+ } as Partial<PartState>);
160
+
161
+ expect(fn(standaloneTask)).toEqual(["group-standalone"]);
162
+ });
163
+
117
164
  it("returns [] for part types not in the map", () => {
118
165
  const fn = groupPartByType({ reasoning: ["group-r"] });
119
166
  expect(fn(part({ type: "text" }))).toEqual([]);
@@ -254,6 +301,30 @@ describe("groupPartByType", () => {
254
301
  ];
255
302
  expect(keyA).not.toBe(keyB);
256
303
  });
304
+
305
+ it("includes tool-call name entries in an order-insensitive fingerprint", () => {
306
+ const withoutName = groupPartByType({ "tool-call": ["group-tool"] });
307
+ const a = groupPartByType({
308
+ "tool-call:task": ["group-subagents"],
309
+ "tool-call": ["group-tool"],
310
+ });
311
+ const b = groupPartByType({
312
+ "tool-call": ["group-tool"],
313
+ "tool-call:task": ["group-subagents"],
314
+ });
315
+ const withoutNameKey = (
316
+ withoutName as unknown as { [GROUPBY_MEMO_KEY]: string }
317
+ )[GROUPBY_MEMO_KEY];
318
+ const keyA = (a as unknown as { [GROUPBY_MEMO_KEY]: string })[
319
+ GROUPBY_MEMO_KEY
320
+ ];
321
+ const keyB = (b as unknown as { [GROUPBY_MEMO_KEY]: string })[
322
+ GROUPBY_MEMO_KEY
323
+ ];
324
+
325
+ expect(keyA).not.toBe(withoutNameKey);
326
+ expect(keyA).toBe(keyB);
327
+ });
257
328
  });
258
329
 
259
330
  describe("buildGroupTree idKey", () => {
@@ -435,6 +435,259 @@ describe("createMessageQueue", () => {
435
435
  });
436
436
  });
437
437
 
438
+ describe("synchronous dispatch failures", () => {
439
+ it("restores a message when the driver throws", () => {
440
+ const error = new Error("dispatch failed");
441
+ const run = vi.fn(() => {
442
+ throw error;
443
+ });
444
+ const { adapter } = createMessageQueue({ run });
445
+
446
+ expect(() => adapter.enqueue(msg("first"))).toThrow(error);
447
+ expect(prompts(adapter.items)).toEqual(["first"]);
448
+
449
+ run.mockImplementation(() => undefined);
450
+ adapter.enqueue(msg("second"));
451
+
452
+ expect(run).toHaveBeenCalledTimes(2);
453
+ expect(run).toHaveBeenLastCalledWith(
454
+ expect.objectContaining({ content: [{ type: "text", text: "first" }] }),
455
+ { steer: false },
456
+ );
457
+ expect(prompts(adapter.items)).toEqual(["second"]);
458
+ });
459
+
460
+ it("does not restore work after the driver starts its run", () => {
461
+ const error = new Error("dispatch failed");
462
+ let fail = true;
463
+ let controller!: ReturnType<typeof createMessageQueue>;
464
+ const run = vi.fn(() => {
465
+ if (!fail) return;
466
+ controller.notifyBusy();
467
+ throw error;
468
+ });
469
+ controller = createMessageQueue({ run });
470
+
471
+ expect(() => controller.adapter.enqueue(msg("first"))).toThrow(error);
472
+ expect(prompts(controller.adapter.items)).toEqual([]);
473
+
474
+ fail = false;
475
+ controller.adapter.enqueue(msg("second"));
476
+ expect(run).toHaveBeenCalledOnce();
477
+
478
+ controller.notifyIdle();
479
+ expect(run).toHaveBeenCalledTimes(2);
480
+ expect(run).toHaveBeenLastCalledWith(
481
+ expect.objectContaining({
482
+ content: [{ type: "text", text: "second" }],
483
+ }),
484
+ { steer: false },
485
+ );
486
+ expect(prompts(controller.adapter.items)).toEqual([]);
487
+ });
488
+
489
+ it("restores a message when its dispatch transform throws", () => {
490
+ const error = new Error("transform failed");
491
+ const run = vi.fn();
492
+ const { adapter } = createMessageQueue({ run });
493
+ adapter.__internal_setDispatchTransform(() => {
494
+ throw error;
495
+ });
496
+
497
+ expect(() => adapter.enqueue(msg("first"))).toThrow(error);
498
+ expect(run).not.toHaveBeenCalled();
499
+ expect(prompts(adapter.items)).toEqual(["first"]);
500
+
501
+ adapter.__internal_setDispatchTransform((message) => message);
502
+ adapter.enqueue(msg("second"));
503
+
504
+ expect(run).toHaveBeenCalledWith(
505
+ expect.objectContaining({ content: [{ type: "text", text: "first" }] }),
506
+ { steer: false },
507
+ );
508
+ expect(prompts(adapter.items)).toEqual(["second"]);
509
+ });
510
+
511
+ it("restores a steer when cancellation throws", () => {
512
+ const error = new Error("cancel failed");
513
+ const run = vi.fn();
514
+ const cancel = vi.fn(() => {
515
+ throw error;
516
+ });
517
+ const { adapter, notifyIdle } = createMessageQueue({ run, cancel });
518
+
519
+ adapter.enqueue(msg("active"));
520
+ expect(() => adapter.steer(msg("urgent"))).toThrow(error);
521
+ expect(prompts(adapter.steerItems)).toEqual(["urgent"]);
522
+
523
+ notifyIdle();
524
+ expect(run).toHaveBeenCalledOnce();
525
+
526
+ cancel.mockImplementation(() => undefined);
527
+ adapter.enqueue(msg("later"));
528
+ expect(run).toHaveBeenLastCalledWith(
529
+ expect.objectContaining({
530
+ content: [{ type: "text", text: "urgent" }],
531
+ }),
532
+ { steer: false },
533
+ );
534
+ expect(prompts(adapter.items)).toEqual(["later"]);
535
+ });
536
+
537
+ it("restores a steer when its dispatch transform throws", () => {
538
+ const error = new Error("transform failed");
539
+ const run = vi.fn();
540
+ const cancel = vi.fn();
541
+ const { adapter, notifyIdle } = createMessageQueue({ run, cancel });
542
+
543
+ adapter.enqueue(msg("active"));
544
+ adapter.__internal_setDispatchTransform(() => {
545
+ throw error;
546
+ });
547
+
548
+ expect(() => adapter.steer(msg("urgent"))).toThrow(error);
549
+ expect(cancel).toHaveBeenCalledOnce();
550
+ expect(prompts(adapter.steerItems)).toEqual(["urgent"]);
551
+
552
+ notifyIdle();
553
+ adapter.__internal_setDispatchTransform((message) => message);
554
+ adapter.enqueue(msg("later"));
555
+
556
+ expect(run).toHaveBeenLastCalledWith(
557
+ expect.objectContaining({
558
+ content: [{ type: "text", text: "urgent" }],
559
+ }),
560
+ { steer: false },
561
+ );
562
+ expect(prompts(adapter.items)).toEqual(["later"]);
563
+ });
564
+
565
+ it("restores a steer when its replacement run throws", () => {
566
+ const error = new Error("steer failed");
567
+ let failSteer = true;
568
+ const run = vi.fn(
569
+ (_message: AppendMessage, options: { steer: boolean }) => {
570
+ if (options.steer && failSteer) throw error;
571
+ },
572
+ );
573
+ const { adapter, notifyIdle } = createMessageQueue({
574
+ run,
575
+ cancel: vi.fn(),
576
+ });
577
+
578
+ adapter.enqueue(msg("active"));
579
+ expect(() => adapter.steer(msg("urgent"))).toThrow(error);
580
+ expect(prompts(adapter.steerItems)).toEqual(["urgent"]);
581
+
582
+ failSteer = false;
583
+ notifyIdle();
584
+ expect(run).toHaveBeenCalledTimes(2);
585
+
586
+ adapter.enqueue(msg("later"));
587
+ expect(run).toHaveBeenLastCalledWith(
588
+ expect.objectContaining({
589
+ content: [{ type: "text", text: "urgent" }],
590
+ }),
591
+ { steer: false },
592
+ );
593
+ expect(prompts(adapter.items)).toEqual(["later"]);
594
+ });
595
+
596
+ it("recovers when cancellation settles before the replacement throws", () => {
597
+ const error = new Error("steer failed");
598
+ let failSteer = true;
599
+ let controller!: ReturnType<typeof createMessageQueue>;
600
+ controller = createMessageQueue({
601
+ run: (_message, options) => {
602
+ if (options.steer && failSteer) throw error;
603
+ },
604
+ cancel: () => controller.notifyIdle(),
605
+ });
606
+
607
+ controller.adapter.enqueue(msg("active"));
608
+ expect(() => controller.adapter.steer(msg("urgent"))).toThrow(error);
609
+ expect(prompts(controller.adapter.steerItems)).toEqual(["urgent"]);
610
+
611
+ failSteer = false;
612
+ controller.adapter.enqueue(msg("later"));
613
+
614
+ expect(prompts(controller.adapter.steerItems)).toEqual([]);
615
+ expect(prompts(controller.adapter.items)).toEqual(["later"]);
616
+ });
617
+
618
+ it("does not restore a steer after its replacement run starts", () => {
619
+ const error = new Error("steer failed");
620
+ let failSteer = true;
621
+ let controller!: ReturnType<typeof createMessageQueue>;
622
+ const run = vi.fn(
623
+ (_message: AppendMessage, options: { steer: boolean }) => {
624
+ if (!options.steer || !failSteer) return;
625
+ controller.notifyBusy();
626
+ throw error;
627
+ },
628
+ );
629
+ controller = createMessageQueue({
630
+ run,
631
+ cancel: () => controller.notifyIdle(),
632
+ });
633
+
634
+ controller.adapter.enqueue(msg("active"));
635
+ expect(() => controller.adapter.steer(msg("urgent"))).toThrow(error);
636
+ expect(prompts(controller.adapter.steerItems)).toEqual([]);
637
+
638
+ failSteer = false;
639
+ controller.adapter.enqueue(msg("later"));
640
+ expect(run).toHaveBeenCalledTimes(2);
641
+
642
+ controller.notifyIdle();
643
+ expect(run).toHaveBeenCalledTimes(3);
644
+ expect(run).toHaveBeenLastCalledWith(
645
+ expect.objectContaining({
646
+ content: [{ type: "text", text: "later" }],
647
+ }),
648
+ { steer: false },
649
+ );
650
+ expect(prompts(controller.adapter.items)).toEqual([]);
651
+ });
652
+
653
+ it("restores an unanchored move when its replacement run throws", () => {
654
+ const error = new Error("steer failed");
655
+ let failSteer = true;
656
+ const run = vi.fn(
657
+ (_message: AppendMessage, options: { steer: boolean }) => {
658
+ if (options.steer && failSteer) throw error;
659
+ },
660
+ );
661
+ const { adapter, notifyIdle } = createMessageQueue({
662
+ run,
663
+ cancel: vi.fn(),
664
+ });
665
+
666
+ adapter.enqueue(msg("active"));
667
+ adapter.enqueue(msg("first"));
668
+ adapter.enqueue(msg("second"));
669
+ adapter.enqueue(msg("moved"));
670
+ const movedId = adapter.items[2]!.id;
671
+
672
+ expect(() => adapter.move(movedId, { lane: "steer" })).toThrow(error);
673
+ expect(adapter.steerItems).toHaveLength(0);
674
+ expect(prompts(adapter.items)).toEqual(["first", "second", "moved"]);
675
+ expect(adapter.items[2]?.id).toBe(movedId);
676
+
677
+ failSteer = false;
678
+ notifyIdle();
679
+ adapter.enqueue(msg("later"));
680
+
681
+ expect(run).toHaveBeenLastCalledWith(
682
+ expect.objectContaining({
683
+ content: [{ type: "text", text: "first" }],
684
+ }),
685
+ { steer: false },
686
+ );
687
+ expect(prompts(adapter.items)).toEqual(["second", "moved", "later"]);
688
+ });
689
+ });
690
+
438
691
  it("notifyCancelled keeps items and pauses advance until the next send", () => {
439
692
  const run = vi.fn();
440
693
  const { adapter, notifyIdle, notifyCancelled } = createMessageQueue({
@@ -88,4 +88,55 @@ describe("onThreadIdChange", () => {
88
88
  expect(emitted).not.toBe(localId);
89
89
  }
90
90
  });
91
+
92
+ it("does not reject a completed switch when the callback throws", async () => {
93
+ const callbackError = new Error("host callback failed");
94
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
95
+ try {
96
+ const cb = vi.fn(() => {
97
+ throw callbackError;
98
+ });
99
+ const { core } = createCoreWithCallback(cb);
100
+ await flush();
101
+
102
+ await expect(core.switchToThread("existing-1")).resolves.toBeUndefined();
103
+
104
+ expect(core.mainThreadId).toBe("existing-1");
105
+ expect(cb).toHaveBeenCalledExactlyOnceWith("existing-1");
106
+ expect(errorSpy).toHaveBeenCalledWith(
107
+ "[assistant-ui] onThreadIdChange callback threw an error",
108
+ callbackError,
109
+ );
110
+
111
+ await expect(core.switchToThread("existing-2")).resolves.toBeUndefined();
112
+ expect(core.mainThreadId).toBe("existing-2");
113
+ expect(cb).toHaveBeenLastCalledWith("existing-2");
114
+ } finally {
115
+ errorSpy.mockRestore();
116
+ }
117
+ });
118
+
119
+ it("contains a rejected callback promise", async () => {
120
+ const callbackError = new Error("async host callback failed");
121
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
122
+ try {
123
+ const cb = vi.fn(async () => {
124
+ throw callbackError;
125
+ });
126
+ const { core } = createCoreWithCallback(cb);
127
+ await flush();
128
+
129
+ await expect(core.switchToThread("existing-1")).resolves.toBeUndefined();
130
+ await vi.waitFor(() => {
131
+ expect(errorSpy).toHaveBeenCalledWith(
132
+ "[assistant-ui] onThreadIdChange callback threw an error",
133
+ callbackError,
134
+ );
135
+ });
136
+
137
+ expect(core.mainThreadId).toBe("existing-1");
138
+ } finally {
139
+ errorSpy.mockRestore();
140
+ }
141
+ });
91
142
  });
@@ -269,6 +269,49 @@ describe("useRemoteThreadListRuntime controlled threadId", () => {
269
269
  expect(onThreadIdChange).toHaveBeenLastCalledWith(undefined);
270
270
  });
271
271
 
272
+ it("contains rejected callback promises on runtime-initiated switches", async () => {
273
+ const callbackError = new Error("async host callback failed");
274
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
275
+ const adapter = makeAdapter();
276
+ const onThreadIdChange = vi.fn(async () => {
277
+ throw callbackError;
278
+ });
279
+ const runtimeRef: RuntimeRef = { current: null };
280
+
281
+ const { unmount } = render(
282
+ <ControlledRuntime
283
+ adapter={adapter}
284
+ threadId="thread-a"
285
+ onThreadIdChange={onThreadIdChange}
286
+ runtimeRef={runtimeRef}
287
+ />,
288
+ );
289
+
290
+ try {
291
+ await waitForRemoteThread(runtimeRef, "thread-a");
292
+
293
+ await act(async () => {
294
+ await expect(
295
+ runtimeRef.current!.threads.switchToThread("thread-b"),
296
+ ).resolves.toBeUndefined();
297
+ });
298
+
299
+ await waitFor(() => {
300
+ expect(errorSpy).toHaveBeenCalledWith(
301
+ "[assistant-ui] onThreadIdChange callback threw an error",
302
+ callbackError,
303
+ );
304
+ });
305
+ expect(onThreadIdChange).toHaveBeenCalledExactlyOnceWith("thread-b");
306
+ expect(runtimeRef.current!.threads.mainItem.getState().remoteId).toBe(
307
+ "thread-b",
308
+ );
309
+ } finally {
310
+ unmount();
311
+ errorSpy.mockRestore();
312
+ }
313
+ });
314
+
272
315
  it("does not retain suppression after an initial switch fails", async () => {
273
316
  let threadAFetchCount = 0;
274
317
  const adapter = makeAdapter({
@@ -23,6 +23,7 @@ export type {
23
23
  MessageStatus,
24
24
  // Thread messages
25
25
  MessageTiming,
26
+ MessageModality,
26
27
  ThreadStep,
27
28
  ThreadSystemMessage,
28
29
  ThreadUserMessage,
@@ -395,6 +395,8 @@ export type MessageTiming = {
395
395
  readonly toolCallCount: number;
396
396
  };
397
397
 
398
+ export type MessageModality = "voice";
399
+
398
400
  export type ThreadStep = {
399
401
  readonly messageId?: string;
400
402
  readonly usage?:
@@ -420,6 +422,7 @@ export type ThreadSystemMessage = MessageCommonProps & {
420
422
  readonly steps?: undefined;
421
423
  readonly submittedFeedback?: undefined;
422
424
  readonly timing?: undefined;
425
+ readonly modality?: undefined;
423
426
  readonly custom: Record<string, unknown>;
424
427
  };
425
428
  };
@@ -436,6 +439,8 @@ export type ThreadUserMessage = MessageCommonProps & {
436
439
  readonly submittedFeedback?: undefined;
437
440
  readonly timing?: undefined;
438
441
  readonly isOptimistic?: boolean;
442
+ /** Set when the message was produced by a voice session rather than typed. */
443
+ readonly modality?: MessageModality;
439
444
  readonly custom: Record<string, unknown>;
440
445
  };
441
446
  };
@@ -456,6 +461,8 @@ export type ThreadAssistantMessage = MessageCommonProps & {
456
461
  * once off the head branch and are never persisted.
457
462
  */
458
463
  readonly isOptimistic?: boolean;
464
+ /** Set when the message was produced by a voice session rather than typed. */
465
+ readonly modality?: MessageModality;
459
466
  readonly custom: Record<string, unknown>;
460
467
  };
461
468
  };
@@ -470,6 +477,7 @@ type BaseThreadMessage = {
470
477
  readonly submittedFeedback?: { readonly type: "positive" | "negative" };
471
478
  readonly timing?: MessageTiming;
472
479
  readonly isOptimistic?: boolean;
480
+ readonly modality?: MessageModality;
473
481
  readonly custom: Record<string, unknown>;
474
482
  };
475
483
  readonly attachments?: ThreadUserMessage["attachments"];
@@ -16,6 +16,121 @@ const collect = async <T>(iterable: AsyncIterable<T>) => {
16
16
  };
17
17
 
18
18
  describe("abortableIterable", () => {
19
+ it.each([false, true])(
20
+ "releases the opening abort listener on rejection=%s",
21
+ async (fails) => {
22
+ const controller = new AbortController();
23
+ const added = vi.spyOn(controller.signal, "addEventListener");
24
+ const removed = vi.spyOn(controller.signal, "removeEventListener");
25
+ const source: AsyncIterable<number> = {
26
+ async *[Symbol.asyncIterator]() {
27
+ yield 1;
28
+ },
29
+ };
30
+ const opening = openAbortableIterable(
31
+ fails ? Promise.reject(new Error("opening failed")) : source,
32
+ controller.signal,
33
+ );
34
+ if (fails) await expect(opening).rejects.toThrow("opening failed");
35
+ else await expect(opening).resolves.toBe(source);
36
+ expect(added).toHaveBeenCalledTimes(1);
37
+ expect(removed).toHaveBeenCalledWith("abort", added.mock.calls[0]![1]);
38
+ },
39
+ );
40
+
41
+ it("finalizes once when an aborted read later rejects", async () => {
42
+ let rejectRead!: (error: unknown) => void;
43
+ const read = new Promise<IteratorResult<number>>((_, reject) => {
44
+ rejectRead = reject;
45
+ });
46
+ const finalize = vi.fn(async () => ({
47
+ done: true as const,
48
+ value: undefined,
49
+ }));
50
+ const source: AsyncIterable<number> = {
51
+ [Symbol.asyncIterator]: () => ({ next: () => read, return: finalize }),
52
+ };
53
+ const controller = new AbortController();
54
+ const iterator = abortableIterable(source, controller.signal)[
55
+ Symbol.asyncIterator
56
+ ]();
57
+ const pending = iterator.next();
58
+ controller.abort();
59
+ await expect(pending).resolves.toEqual({ done: true, value: undefined });
60
+ expect(finalize).toHaveBeenCalledTimes(1);
61
+ rejectRead(new Error("late failure"));
62
+ await read.catch(() => {});
63
+ await iterator.return?.();
64
+ expect(finalize).toHaveBeenCalledTimes(1);
65
+ });
66
+
67
+ it("releases the listener and finalizes after a synchronous read failure", async () => {
68
+ const controller = new AbortController();
69
+ const added = vi.spyOn(controller.signal, "addEventListener");
70
+ const removed = vi.spyOn(controller.signal, "removeEventListener");
71
+ const error = new Error("read failed");
72
+ const finalize = vi.fn(async () => ({
73
+ done: true as const,
74
+ value: undefined,
75
+ }));
76
+ const source: AsyncIterable<number> = {
77
+ [Symbol.asyncIterator]: () => ({
78
+ next: () => {
79
+ throw error;
80
+ },
81
+ return: finalize,
82
+ }),
83
+ };
84
+ await expect(
85
+ collect(abortableIterable(source, controller.signal)),
86
+ ).rejects.toBe(error);
87
+ expect(removed).toHaveBeenCalledWith("abort", added.mock.calls[0]![1]);
88
+ expect(finalize).toHaveBeenCalledTimes(1);
89
+ controller.abort();
90
+ expect(finalize).toHaveBeenCalledTimes(1);
91
+ });
92
+
93
+ it("does not read or finalize again after the consumer returns", async () => {
94
+ const next = vi.fn(async () => ({ done: false as const, value: 1 }));
95
+ const finalize = vi.fn(async () => ({
96
+ done: true as const,
97
+ value: undefined,
98
+ }));
99
+ const source: AsyncIterable<number> = {
100
+ [Symbol.asyncIterator]: () => ({ next, return: finalize }),
101
+ };
102
+ const iterator = abortableIterable(source, new AbortController().signal)[
103
+ Symbol.asyncIterator
104
+ ]();
105
+ await iterator.return?.();
106
+ await iterator.return?.();
107
+ await expect(iterator.next()).resolves.toEqual({
108
+ done: true,
109
+ value: undefined,
110
+ });
111
+ expect(next).not.toHaveBeenCalled();
112
+ expect(finalize).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it("does not finalize a naturally exhausted source", async () => {
116
+ const next = vi.fn(async () => ({ done: true as const, value: undefined }));
117
+ const finalize = vi.fn(async () => ({
118
+ done: true as const,
119
+ value: undefined,
120
+ }));
121
+ const source: AsyncIterable<number> = {
122
+ [Symbol.asyncIterator]: () => ({ next, return: finalize }),
123
+ };
124
+ const iterator = abortableIterable(source, new AbortController().signal)[
125
+ Symbol.asyncIterator
126
+ ]();
127
+ await iterator.next();
128
+ await iterator.next();
129
+ await iterator.return?.();
130
+ expect(next).toHaveBeenCalledTimes(1);
131
+ expect(finalize).not.toHaveBeenCalled();
132
+ });
133
+
19
134
  it("settles an opening stream on abort and finalizes a late iterable", async () => {
20
135
  const opened = deferred<AsyncIterable<number>>();
21
136
  const finalize = vi.fn(async () => ({