@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,7 +1,8 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { beforeEach, describe, expect, it, onTestFinished, vi } from "vitest";
2
2
  import { BaseComposerRuntimeCore } from "../runtime/base/base-composer-runtime-core";
3
3
  import type { AttachmentAdapter } from "../adapters/attachment";
4
4
  import type { DictationAdapter } from "../adapters/speech";
5
+ import { WebSpeechDictationAdapter } from "../adapters/speech";
5
6
  import type { CreateAttachment, PendingAttachment } from "../types/attachment";
6
7
  import type { AppendMessage } from "../types/message";
7
8
  import type { SendOptions } from "../runtime/interfaces/composer-runtime-core";
@@ -75,6 +76,71 @@ describe("BaseComposerRuntimeCore", () => {
75
76
  expect(listener).not.toHaveBeenCalled();
76
77
  });
77
78
 
79
+ it.each(["", "Existing text"])(
80
+ "replaces the complete browser dictation preview after %j",
81
+ async (baseText) => {
82
+ vi.useFakeTimers();
83
+ onTestFinished(() => {
84
+ vi.unstubAllGlobals();
85
+ vi.useRealTimers();
86
+ });
87
+ const listeners = new Map<string, EventListener>();
88
+ class Recognition {
89
+ addEventListener(type: string, listener: EventListener) {
90
+ listeners.set(type, listener);
91
+ }
92
+ start() {}
93
+ stop() {
94
+ listeners.get("end")!(new Event("end"));
95
+ }
96
+ abort() {
97
+ this.stop();
98
+ }
99
+ }
100
+ vi.stubGlobal("window", { SpeechRecognition: Recognition });
101
+ composer.setDictationAdapter(new WebSpeechDictationAdapter());
102
+ composer.setText(baseText);
103
+ composer.startDictation();
104
+ onTestFinished(() => composer.stopDictation());
105
+ const emit = (results: [string, boolean][], resultIndex = 0) => {
106
+ listeners.get("result")!({
107
+ resultIndex,
108
+ results: results.map(([transcript, isFinal]) => ({
109
+ 0: { transcript },
110
+ isFinal,
111
+ })),
112
+ } as unknown as Event);
113
+ };
114
+ const prefix = baseText ? `${baseText} ` : "";
115
+
116
+ emit([
117
+ ["hello ", false],
118
+ ["world", false],
119
+ ]);
120
+ expect(composer.text).toBe(`${prefix}hello world`);
121
+ emit([["hello ", false]], 1);
122
+ expect(composer.text).toBe(`${prefix}hello `);
123
+ emit([]);
124
+ expect(composer.text).toBe(baseText);
125
+ emit([
126
+ ["hello ", true],
127
+ ["world", false],
128
+ ]);
129
+ expect(composer.text).toBe(`${prefix}hello world`);
130
+ emit(
131
+ [
132
+ ["hello ", true],
133
+ ["world", true],
134
+ ],
135
+ 1,
136
+ );
137
+ expect(composer.text).toBe(`${prefix}hello world`);
138
+ expect(composer.dictation?.transcript).toBeUndefined();
139
+ composer.stopDictation();
140
+ await Promise.resolve();
141
+ },
142
+ );
143
+
78
144
  it("isEmpty returns true for whitespace-only text", () => {
79
145
  composer.setText(" ");
80
146
  expect(composer.isEmpty).toBe(true);
@@ -103,6 +169,56 @@ describe("BaseComposerRuntimeCore", () => {
103
169
  expect(listener).not.toHaveBeenCalled();
104
170
  });
105
171
 
172
+ describe.each(["clearAttachments", "reset"] as const)(
173
+ "%s cleanup",
174
+ (method) => {
175
+ it.each(["first throw", "later throw", "rejection", "multiple failures"])(
176
+ "attempts all pending removals after %s and still rejects",
177
+ async (failure) => {
178
+ const error = new Error("removal failed");
179
+ const remove = vi.fn((attachment: PendingAttachment) => {
180
+ const fails =
181
+ attachment.id === (failure === "later throw" ? "two" : "one");
182
+ if (fails) {
183
+ if (failure === "rejection") return Promise.reject(error);
184
+ throw error;
185
+ }
186
+ if (failure === "multiple failures" && attachment.id === "two") {
187
+ return Promise.reject(new Error("another failure"));
188
+ }
189
+ return Promise.resolve();
190
+ });
191
+ composer.setAttachmentAdapter({
192
+ accept: "*",
193
+ add: vi.fn(),
194
+ send: vi.fn(),
195
+ remove,
196
+ });
197
+ composer.setAttachments([
198
+ makePendingAttachment("one"),
199
+ {
200
+ id: "complete",
201
+ type: "file",
202
+ name: "saved.txt",
203
+ contentType: "text/plain",
204
+ content: [],
205
+ status: { type: "complete" },
206
+ },
207
+ makePendingAttachment("two"),
208
+ makePendingAttachment("three"),
209
+ ]);
210
+
211
+ await expect(composer[method]()).rejects.toBe(error);
212
+
213
+ expect(
214
+ remove.mock.calls.map(([attachment]) => attachment.id),
215
+ ).toEqual(["one", "two", "three"]);
216
+ expect(composer.attachments).toEqual([]);
217
+ },
218
+ );
219
+ },
220
+ );
221
+
106
222
  it("reset keeps discarded text out of later dictation results", async () => {
107
223
  let emitSpeech!: (result: DictationAdapter.Result) => void;
108
224
  composer.setDictationAdapter({
@@ -226,9 +342,14 @@ describe("BaseComposerRuntimeCore", () => {
226
342
  });
227
343
 
228
344
  it("handles rejected dictation shutdowns", async () => {
345
+ const consoleError = vi
346
+ .spyOn(console, "error")
347
+ .mockImplementation(() => {});
348
+ onTestFinished(() => consoleError.mockRestore());
349
+ const stopError = new Error("shutdown failed");
229
350
  const session: DictationAdapter.Session = {
230
351
  status: { type: "running" },
231
- stop: vi.fn().mockRejectedValue(new Error("shutdown failed")),
352
+ stop: vi.fn().mockRejectedValue(stopError),
232
353
  cancel: vi.fn(),
233
354
  onSpeechStart: vi.fn(() => () => {}),
234
355
  onSpeechEnd: vi.fn(() => () => {}),
@@ -248,11 +369,202 @@ describe("BaseComposerRuntimeCore", () => {
248
369
  expect(session.stop).toHaveBeenCalledOnce();
249
370
  expect(composer.dictation).toBeUndefined();
250
371
  expect(unhandledRejection).not.toHaveBeenCalled();
372
+ expect(consoleError).toHaveBeenCalledWith(
373
+ "[assistant-ui] Dictation session stop rejected",
374
+ stopError,
375
+ );
251
376
  } finally {
252
377
  process.off("unhandledRejection", unhandledRejection);
253
378
  }
254
379
  });
255
380
 
381
+ it("handles synchronously throwing dictation shutdowns", () => {
382
+ const consoleError = vi
383
+ .spyOn(console, "error")
384
+ .mockImplementation(() => {});
385
+ onTestFinished(() => consoleError.mockRestore());
386
+ const stopError = new Error("shutdown failed");
387
+ const session: DictationAdapter.Session = {
388
+ status: { type: "running" },
389
+ stop: vi.fn(() => {
390
+ throw stopError;
391
+ }),
392
+ cancel: vi.fn(),
393
+ onSpeechStart: vi.fn(() => () => {}),
394
+ onSpeechEnd: vi.fn(() => () => {}),
395
+ onSpeech: vi.fn(() => () => {}),
396
+ };
397
+ composer.setDictationAdapter({ listen: () => session });
398
+ composer.startDictation();
399
+
400
+ expect(() => composer.stopDictation()).not.toThrow();
401
+
402
+ expect(composer.dictation).toBeUndefined();
403
+ expect(consoleError).toHaveBeenCalledWith(
404
+ "[assistant-ui] Dictation session stop threw",
405
+ stopError,
406
+ );
407
+ });
408
+
409
+ it("finishes dictation cleanup when an unsubscribe throws", async () => {
410
+ const cleanupError = new Error("cleanup failed");
411
+ const laterCleanup = vi.fn();
412
+ const consoleError = vi
413
+ .spyOn(console, "error")
414
+ .mockImplementation(() => {});
415
+ onTestFinished(() => consoleError.mockRestore());
416
+ const session: DictationAdapter.Session = {
417
+ status: { type: "running" },
418
+ stop: vi.fn().mockResolvedValue(undefined),
419
+ cancel: vi.fn(),
420
+ onSpeech: vi.fn(() => () => {
421
+ throw cleanupError;
422
+ }),
423
+ onSpeechStart: vi.fn(() => laterCleanup),
424
+ onSpeechEnd: vi.fn(() => () => {}),
425
+ };
426
+ composer.setDictationAdapter({ listen: () => session });
427
+
428
+ composer.startDictation();
429
+ composer.stopDictation();
430
+ await new Promise((resolve) => setTimeout(resolve, 0));
431
+
432
+ expect(laterCleanup).toHaveBeenCalledOnce();
433
+ expect(composer.dictation).toBeUndefined();
434
+ expect(consoleError).toHaveBeenCalledWith(
435
+ "[assistant-ui] Dictation cleanup threw",
436
+ cleanupError,
437
+ );
438
+ });
439
+
440
+ it("sends after a dictation unsubscribe throws", async () => {
441
+ const cleanupError = new Error("cleanup failed");
442
+ const cancelError = new Error("cancel failed");
443
+ const consoleError = vi
444
+ .spyOn(console, "error")
445
+ .mockImplementation(() => {});
446
+ onTestFinished(() => consoleError.mockRestore());
447
+ composer.setDictationAdapter({
448
+ listen: () => ({
449
+ status: { type: "running" },
450
+ stop: vi.fn().mockResolvedValue(undefined),
451
+ cancel: vi.fn(() => {
452
+ throw cancelError;
453
+ }),
454
+ onSpeech: () => () => {
455
+ throw cleanupError;
456
+ },
457
+ onSpeechStart: () => () => {},
458
+ onSpeechEnd: () => () => {},
459
+ }),
460
+ });
461
+ composer.setText("send me");
462
+ composer.startDictation();
463
+
464
+ await composer.send();
465
+
466
+ expect(composer.sentMessages).toHaveLength(1);
467
+ expect(composer.sentMessages[0]?.content).toEqual([
468
+ { type: "text", text: "send me" },
469
+ ]);
470
+ expect(consoleError).toHaveBeenCalledWith(
471
+ "[assistant-ui] Dictation session cancel threw",
472
+ cancelError,
473
+ );
474
+ expect(consoleError).toHaveBeenCalledWith(
475
+ "[assistant-ui] Dictation cleanup threw",
476
+ cleanupError,
477
+ );
478
+ });
479
+
480
+ it("starts replacement dictation when the old session stop throws", () => {
481
+ const consoleError = vi
482
+ .spyOn(console, "error")
483
+ .mockImplementation(() => {});
484
+ onTestFinished(() => consoleError.mockRestore());
485
+ const stopError = new Error("shutdown failed");
486
+ const firstSession: DictationAdapter.Session = {
487
+ status: { type: "running" },
488
+ stop: vi.fn(() => {
489
+ throw stopError;
490
+ }),
491
+ cancel: vi.fn(),
492
+ onSpeech: () => () => {},
493
+ onSpeechStart: () => () => {},
494
+ onSpeechEnd: () => () => {},
495
+ };
496
+ const secondSession: DictationAdapter.Session = {
497
+ ...firstSession,
498
+ stop: vi.fn().mockResolvedValue(undefined),
499
+ };
500
+ const listen = vi
501
+ .fn()
502
+ .mockReturnValueOnce(firstSession)
503
+ .mockReturnValueOnce(secondSession);
504
+ composer.setDictationAdapter({ listen });
505
+ composer.startDictation();
506
+
507
+ expect(() => composer.startDictation()).not.toThrow();
508
+
509
+ expect(listen).toHaveBeenCalledTimes(2);
510
+ expect(composer.dictation).toBeDefined();
511
+ expect(consoleError).toHaveBeenCalledWith(
512
+ "[assistant-ui] Dictation session stop threw",
513
+ stopError,
514
+ );
515
+ composer.stopDictation();
516
+ });
517
+
518
+ it("publishes cleared state when replacement session creation throws", () => {
519
+ const listenError = new Error("listen failed");
520
+ const firstSession: DictationAdapter.Session = {
521
+ status: { type: "running" },
522
+ stop: vi.fn().mockResolvedValue(undefined),
523
+ cancel: vi.fn(),
524
+ onSpeech: () => () => {},
525
+ onSpeechStart: () => () => {},
526
+ onSpeechEnd: () => () => {},
527
+ };
528
+ const listen = vi
529
+ .fn()
530
+ .mockReturnValueOnce(firstSession)
531
+ .mockImplementationOnce(() => {
532
+ throw listenError;
533
+ });
534
+ composer.setDictationAdapter({ listen });
535
+ const states: Array<string | undefined> = [];
536
+ composer.subscribe(() => states.push(composer.dictation?.status.type));
537
+ composer.startDictation();
538
+ states.length = 0;
539
+
540
+ expect(() => composer.startDictation()).toThrow(listenError);
541
+
542
+ expect(composer.dictation).toBeUndefined();
543
+ expect(states).toEqual([undefined]);
544
+ });
545
+
546
+ it("replaces dictation without publishing an intermediate cleared state", async () => {
547
+ const session = (): DictationAdapter.Session => ({
548
+ status: { type: "running" },
549
+ stop: vi.fn().mockResolvedValue(undefined),
550
+ cancel: vi.fn(),
551
+ onSpeech: () => () => {},
552
+ onSpeechStart: () => () => {},
553
+ onSpeechEnd: () => () => {},
554
+ });
555
+ composer.setDictationAdapter({ listen: session });
556
+ const states: Array<string | undefined> = [];
557
+ composer.subscribe(() => states.push(composer.dictation?.status.type));
558
+ composer.startDictation();
559
+ states.length = 0;
560
+
561
+ composer.startDictation();
562
+
563
+ expect(states).toEqual(["running"]);
564
+ composer.stopDictation();
565
+ await Promise.resolve();
566
+ });
567
+
256
568
  describe("CreateAttachment (external source)", () => {
257
569
  const makeCreateAttachment = (
258
570
  overrides?: Partial<CreateAttachment>,
@@ -4,6 +4,7 @@ import {
4
4
  hasUpcomingMessage,
5
5
  } from "../runtimes/external-store/external-store-thread-runtime-core";
6
6
  import type { ExternalStoreAdapter } from "../runtimes/external-store/external-store-adapter";
7
+ import type { RealtimeVoiceAdapter } from "../adapters/voice";
7
8
  import type { ModelContextProvider } from "../model-context/types";
8
9
  import type { AppendMessage, ThreadMessage } from "../types/message";
9
10
  import { createMessageQueue } from "../runtime/queue/message-queue";
@@ -1117,6 +1118,44 @@ describe("ExternalStoreThreadRuntimeCore adapter contract", () => {
1117
1118
  ).resolves.toBeUndefined();
1118
1119
  });
1119
1120
 
1121
+ it("emits a decision after an approval callback accepts", async () => {
1122
+ const message: ThreadMessage = {
1123
+ ...createAssistantMessage("assistant-1"),
1124
+ status: { type: "requires-action", reason: "tool-calls" },
1125
+ content: [
1126
+ {
1127
+ type: "tool-call",
1128
+ toolCallId: "tool-1",
1129
+ toolName: "review",
1130
+ args: {},
1131
+ argsText: "{}",
1132
+ approval: { id: "approval-1" },
1133
+ },
1134
+ ],
1135
+ } as ThreadMessage;
1136
+ const core = new ExternalStoreThreadRuntimeCore(
1137
+ contextProvider,
1138
+ createBaseAdapter({
1139
+ messages: [message],
1140
+ onRespondToToolApproval: () => {},
1141
+ }),
1142
+ );
1143
+ const answered = vi.fn();
1144
+ core.unstable_on("toolApprovalAnswered", answered);
1145
+
1146
+ await core.respondToToolApproval({
1147
+ approvalId: "approval-1",
1148
+ approved: true,
1149
+ });
1150
+
1151
+ expect(answered).toHaveBeenCalledWith({
1152
+ messageId: "assistant-1",
1153
+ toolCallId: "tool-1",
1154
+ toolName: "review",
1155
+ approved: true,
1156
+ });
1157
+ });
1158
+
1120
1159
  it("handles rejected onCancel callbacks", async () => {
1121
1160
  const error = new Error("cancel failed");
1122
1161
  const consoleError = vi
@@ -1262,3 +1301,115 @@ describe("ExternalStoreThreadRuntimeCore adapter contract", () => {
1262
1301
  });
1263
1302
  });
1264
1303
  });
1304
+
1305
+ describe("ExternalStoreThreadRuntimeCore voice transcripts", () => {
1306
+ const createVoiceAdapter = () => {
1307
+ let transcriptCallback:
1308
+ | ((transcript: RealtimeVoiceAdapter.TranscriptItem) => void)
1309
+ | undefined;
1310
+ const session: RealtimeVoiceAdapter.Session = {
1311
+ status: { type: "running" },
1312
+ isMuted: false,
1313
+ disconnect: vi.fn(),
1314
+ mute: vi.fn(),
1315
+ unmute: vi.fn(),
1316
+ onStatusChange: () => () => {},
1317
+ onTranscript: (callback) => {
1318
+ transcriptCallback = callback;
1319
+ return () => {
1320
+ transcriptCallback = undefined;
1321
+ };
1322
+ },
1323
+ onModeChange: () => () => {},
1324
+ onVolumeChange: () => () => {},
1325
+ };
1326
+ return {
1327
+ adapter: { connect: () => session } satisfies RealtimeVoiceAdapter,
1328
+ emitTranscript: (transcript: RealtimeVoiceAdapter.TranscriptItem) =>
1329
+ transcriptCallback?.(transcript),
1330
+ };
1331
+ };
1332
+
1333
+ const createVoiceCore = () => {
1334
+ const voiceAdapter = createVoiceAdapter();
1335
+ const onNew = vi.fn(async () => {});
1336
+ const onEdit = vi.fn(async () => {});
1337
+ const onReload = vi.fn(async () => {});
1338
+ const onResume = vi.fn(async () => {});
1339
+ const core = new ExternalStoreThreadRuntimeCore(
1340
+ createContextProvider(),
1341
+ createBaseAdapter({
1342
+ onNew,
1343
+ onEdit,
1344
+ onReload,
1345
+ onResume,
1346
+ adapters: { voice: voiceAdapter.adapter },
1347
+ }),
1348
+ );
1349
+ core.connectVoice();
1350
+ voiceAdapter.emitTranscript({
1351
+ role: "assistant",
1352
+ text: "Hello",
1353
+ isFinal: true,
1354
+ });
1355
+ return {
1356
+ core,
1357
+ onNew,
1358
+ onEdit,
1359
+ onReload,
1360
+ onResume,
1361
+ transcriptId: core.messages[0]!.id,
1362
+ };
1363
+ };
1364
+
1365
+ it("rejects an edit resubmission of a transcript before reaching the store", async () => {
1366
+ const { core, onNew, onEdit, transcriptId } = createVoiceCore();
1367
+ try {
1368
+ const edit: AppendMessage = {
1369
+ parentId: null,
1370
+ sourceId: transcriptId,
1371
+ role: "user",
1372
+ content: [{ type: "text", text: "again" }],
1373
+ attachments: [],
1374
+ metadata: { custom: {} },
1375
+ createdAt: new Date(),
1376
+ runConfig: {},
1377
+ };
1378
+
1379
+ await expect(core.append(edit)).rejects.toThrow(
1380
+ "Voice transcript messages cannot be edited",
1381
+ );
1382
+ await expect(core.append({ ...edit, startRun: false })).rejects.toThrow(
1383
+ "Voice transcript messages cannot be edited",
1384
+ );
1385
+ expect(onNew).not.toHaveBeenCalled();
1386
+ expect(onEdit).not.toHaveBeenCalled();
1387
+ } finally {
1388
+ core.disconnectVoice();
1389
+ }
1390
+ });
1391
+
1392
+ it("rejects reloading a transcript before reaching the store", async () => {
1393
+ const { core, onReload, onResume, transcriptId } = createVoiceCore();
1394
+ try {
1395
+ await expect(
1396
+ core.startRun({
1397
+ parentId: null,
1398
+ sourceId: transcriptId,
1399
+ runConfig: {},
1400
+ }),
1401
+ ).rejects.toThrow("Voice transcript messages cannot be reloaded");
1402
+ await expect(
1403
+ core.resumeRun({
1404
+ parentId: null,
1405
+ sourceId: transcriptId,
1406
+ runConfig: {},
1407
+ }),
1408
+ ).rejects.toThrow("Voice transcript messages cannot be reloaded");
1409
+ expect(onReload).not.toHaveBeenCalled();
1410
+ expect(onResume).not.toHaveBeenCalled();
1411
+ } finally {
1412
+ core.disconnectVoice();
1413
+ }
1414
+ });
1415
+ });
@@ -76,6 +76,75 @@ const renderThread = () => {
76
76
  };
77
77
 
78
78
  describe("ExternalThread attachments", () => {
79
+ describe.each(["clearAttachments", "reset"] as const)(
80
+ "%s cleanup",
81
+ (method) => {
82
+ it.each(["first throw", "later throw", "rejection", "multiple failures"])(
83
+ "attempts all pending removals after %s and still rejects",
84
+ async (failure) => {
85
+ const error = new Error("removal failed");
86
+ const remove = vi.fn((attachment: PendingAttachment) => {
87
+ const fails =
88
+ attachment.id === (failure === "later throw" ? "two" : "one");
89
+ if (fails) {
90
+ if (failure === "rejection") return Promise.reject(error);
91
+ throw error;
92
+ }
93
+ if (failure === "multiple failures" && attachment.id === "two") {
94
+ return Promise.reject(new Error("another failure"));
95
+ }
96
+ return Promise.resolve();
97
+ });
98
+ const aui = renderThreadWithProps({
99
+ attachmentAdapter: {
100
+ accept: "*",
101
+ add: async ({ file }) => ({
102
+ id: file.name,
103
+ type: "file",
104
+ name: file.name,
105
+ contentType: "text/plain",
106
+ file,
107
+ status: { type: "requires-action", reason: "composer-send" },
108
+ }),
109
+ send: vi.fn(),
110
+ remove,
111
+ },
112
+ });
113
+ const composer = () => aui().thread().composer();
114
+ await act(async () => {
115
+ await composer().addAttachment(
116
+ new File(["data"], "one", { type: "text/plain" }),
117
+ );
118
+ await composer().addAttachment({
119
+ id: "complete",
120
+ name: "saved.txt",
121
+ contentType: "text/plain",
122
+ content: [],
123
+ });
124
+ await composer().addAttachment(
125
+ new File(["data"], "two", { type: "text/plain" }),
126
+ );
127
+ await composer().addAttachment(
128
+ new File(["data"], "three", { type: "text/plain" }),
129
+ );
130
+ });
131
+ await waitFor(() =>
132
+ expect(composer().getState().attachments).toHaveLength(4),
133
+ );
134
+
135
+ await act(async () => {
136
+ await expect(composer()[method]()).rejects.toBe(error);
137
+ });
138
+
139
+ expect(
140
+ remove.mock.calls.map(([attachment]) => attachment.id),
141
+ ).toEqual(["one", "two", "three"]);
142
+ expect(composer().getState().attachments).toEqual([]);
143
+ },
144
+ );
145
+ },
146
+ );
147
+
79
148
  it("uses generated IDs unless a prepared attachment supplies one", async () => {
80
149
  const aui = renderThread();
81
150
  mockGenerateId
@@ -203,6 +203,40 @@ describe("ExternalThread part status", () => {
203
203
  });
204
204
  });
205
205
 
206
+ describe("ExternalThread tasks", () => {
207
+ it("derives nested conversations and resolves their task scope", () => {
208
+ const { aui } = renderThread({
209
+ messages: [
210
+ assistantMessageWithContent({ type: "running" }, [
211
+ {
212
+ type: "tool-call",
213
+ toolCallId: "delegate-1",
214
+ toolName: "delegate",
215
+ args: {},
216
+ argsText: "{}",
217
+ messages: [
218
+ assistantMessageWithContent(
219
+ { type: "running" },
220
+ [],
221
+ "nested-message-1",
222
+ ),
223
+ ],
224
+ },
225
+ ]),
226
+ ],
227
+ isRunning: true,
228
+ });
229
+
230
+ const [task] = aui().thread.getState().tasks;
231
+ expect(task).toMatchObject({
232
+ id: "delegate-1",
233
+ toolName: "delegate",
234
+ messageId: "a1",
235
+ });
236
+ expect(aui().thread.task({ id: "delegate-1" }).getState()).toBe(task);
237
+ });
238
+ });
239
+
206
240
  describe("ExternalThread unset optional callbacks", () => {
207
241
  it("throws a capability error when the callback prop is not set", () => {
208
242
  const { aui } = renderThread({