@assistant-ui/core 0.3.15 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/dist/internal.d.ts +7 -2
  2. package/dist/internal.js +8 -3
  3. package/dist/model-context/frame/host.d.ts.map +1 -1
  4. package/dist/model-context/frame/host.js +3 -2
  5. package/dist/model-context/frame/host.js.map +1 -1
  6. package/dist/model-context/frame/provider.d.ts +2 -0
  7. package/dist/model-context/frame/provider.d.ts.map +1 -1
  8. package/dist/model-context/frame/provider.js +88 -25
  9. package/dist/model-context/frame/provider.js.map +1 -1
  10. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  11. package/dist/model-context/interactable-composer-metadata.js +1 -1
  12. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  13. package/dist/model-context/registry.d.ts +2 -2
  14. package/dist/model-context/registry.d.ts.map +1 -1
  15. package/dist/model-context/registry.js +9 -28
  16. package/dist/model-context/registry.js.map +1 -1
  17. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  18. package/dist/react/client/InMemoryThreadList.js +24 -20
  19. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts.map +1 -1
  21. package/dist/react/client/Interactables.js +59 -177
  22. package/dist/react/client/Interactables.js.map +1 -1
  23. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  24. package/dist/react/client/RemoteThreadList.js +23 -56
  25. package/dist/react/client/RemoteThreadList.js.map +1 -1
  26. package/dist/react/index.d.ts +2 -1
  27. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  28. package/dist/react/interactables-legacy/Interactables.js +56 -174
  29. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  30. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  31. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  32. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  33. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  34. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  35. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  36. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  37. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  38. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +1 -1
  39. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +1 -1
  40. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  41. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +23 -55
  42. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  43. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  44. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  45. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  46. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  47. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  48. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  49. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  50. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  51. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  52. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  53. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  54. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  55. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  56. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  57. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  58. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  59. package/dist/react/runtimes/external-message-converter.js +33 -233
  60. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  61. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  62. package/dist/runtime/api/composer-runtime.js.map +1 -1
  63. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  64. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  65. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  66. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  67. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  68. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  69. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  70. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  71. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  72. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  73. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  74. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  75. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  76. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  77. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  78. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  79. package/dist/runtime/internal.d.ts +2 -1
  80. package/dist/runtime/internal.js +2 -1
  81. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  82. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  83. package/dist/runtime/queue/message-queue.js +1 -1
  84. package/dist/runtime/queue/message-queue.js.map +1 -1
  85. package/dist/runtime/queue/queue-item.d.ts +12 -0
  86. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  87. package/dist/runtime/queue/queue-item.js +6 -0
  88. package/dist/runtime/queue/queue-item.js.map +1 -0
  89. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  90. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  91. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  92. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  93. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  94. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  95. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  96. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  97. package/dist/runtime/utils/auto-status.d.ts +6 -1
  98. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  99. package/dist/runtime/utils/auto-status.js +7 -1
  100. package/dist/runtime/utils/auto-status.js.map +1 -1
  101. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  102. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  103. package/dist/runtime/utils/external-message-conversion.js +221 -0
  104. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  105. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  106. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  107. package/dist/runtime/utils/external-store-message.js +2 -1
  108. package/dist/runtime/utils/external-store-message.js.map +1 -1
  109. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  110. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  111. package/dist/runtime/utils/message-repository-session.js +130 -0
  112. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  113. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  114. package/dist/runtime/utils/message-repository.js +3 -4
  115. package/dist/runtime/utils/message-repository.js.map +1 -1
  116. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  117. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  118. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  119. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  120. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  121. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  122. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  123. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  124. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  125. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  126. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  127. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  128. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  129. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +122 -20
  130. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  131. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  132. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  133. package/dist/runtimes/local/local-thread-runtime-core.js +20 -2
  134. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  135. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  136. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  137. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  138. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  139. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  140. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  141. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  142. package/dist/store/clients/external-thread.d.ts.map +1 -1
  143. package/dist/store/clients/external-thread.js +74 -99
  144. package/dist/store/clients/external-thread.js.map +1 -1
  145. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  146. package/dist/store/clients/single-thread-list.js +1 -1
  147. package/dist/store/clients/single-thread-list.js.map +1 -1
  148. package/dist/store/clients/suggestions.d.ts.map +1 -1
  149. package/dist/store/clients/suggestions.js +16 -7
  150. package/dist/store/clients/suggestions.js.map +1 -1
  151. package/dist/store/index.d.ts +4 -2
  152. package/dist/store/scope-registration.d.ts +1 -1
  153. package/dist/store/scopes/composer.d.ts +2 -1
  154. package/dist/store/scopes/composer.d.ts.map +1 -1
  155. package/dist/store/scopes/message.d.ts +1 -1
  156. package/dist/store/scopes/queue-item.d.ts +2 -9
  157. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  158. package/dist/store/scopes/queue-item.js +0 -6
  159. package/dist/types/message.d.ts +7 -3
  160. package/dist/types/message.d.ts.map +1 -1
  161. package/dist/types/message.js.map +1 -1
  162. package/dist/utils/data-url.d.ts +14 -1
  163. package/dist/utils/data-url.d.ts.map +1 -1
  164. package/dist/utils/data-url.js +13 -1
  165. package/dist/utils/data-url.js.map +1 -1
  166. package/dist/utils/invoke-user-callback.d.ts +5 -0
  167. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  168. package/dist/utils/invoke-user-callback.js +19 -0
  169. package/dist/utils/invoke-user-callback.js.map +1 -0
  170. package/dist/utils/normalizePartStatus.js.map +1 -1
  171. package/package.json +10 -10
  172. package/src/internal.ts +12 -0
  173. package/src/model-context/frame/host.test.ts +91 -7
  174. package/src/model-context/frame/host.ts +7 -2
  175. package/src/model-context/frame/provider.test.ts +267 -12
  176. package/src/model-context/frame/provider.ts +113 -31
  177. package/src/model-context/interactable-composer-metadata.ts +1 -1
  178. package/src/model-context/registry.test.ts +44 -0
  179. package/src/model-context/registry.ts +13 -35
  180. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  181. package/src/react/client/InMemoryThreadList.ts +7 -1
  182. package/src/react/client/Interactables.ts +19 -178
  183. package/src/react/client/RemoteThreadList.test.ts +96 -0
  184. package/src/react/client/RemoteThreadList.ts +27 -59
  185. package/src/react/interactables-legacy/Interactables.ts +20 -177
  186. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  187. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  188. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +26 -56
  189. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  190. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  191. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  192. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  193. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  194. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  195. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  196. package/src/react/runtimes/external-message-converter.ts +58 -481
  197. package/src/runtime/api/composer-runtime.ts +1 -1
  198. package/src/runtime/api/message-runtime.test.ts +28 -1
  199. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  200. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  201. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  202. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  203. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  204. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  205. package/src/runtime/internal.ts +5 -0
  206. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  207. package/src/runtime/queue/message-queue.ts +1 -4
  208. package/src/runtime/queue/queue-item.ts +10 -0
  209. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  210. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  211. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  212. package/src/runtime/utils/auto-status.ts +33 -0
  213. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  214. package/src/runtime/utils/external-message-conversion.ts +494 -0
  215. package/src/runtime/utils/external-store-message.ts +2 -0
  216. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  217. package/src/runtime/utils/message-repository-session.ts +176 -0
  218. package/src/runtime/utils/message-repository.ts +7 -4
  219. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  220. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  221. package/src/runtime/utils/thread-message-like.ts +2 -0
  222. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  223. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +175 -28
  224. package/src/runtimes/local/local-thread-runtime-core.test.ts +172 -1
  225. package/src/runtimes/local/local-thread-runtime-core.ts +48 -3
  226. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  227. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  228. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  229. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  230. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  231. package/src/store/clients/external-thread.ts +16 -61
  232. package/src/store/clients/single-thread-list.ts +4 -1
  233. package/src/store/clients/suggestions.test.ts +31 -0
  234. package/src/store/clients/suggestions.ts +18 -19
  235. package/src/store/scopes/message.ts +1 -1
  236. package/src/store/scopes/queue-item.ts +2 -9
  237. package/src/tests/MessageRepository.test.ts +77 -0
  238. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  239. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  240. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +164 -0
  241. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  242. package/src/tests/external-thread-attachments.test.tsx +173 -0
  243. package/src/tests/single-thread-list-item.test.tsx +47 -0
  244. package/src/tests/thread-message-like.test.ts +39 -1
  245. package/src/types/message.ts +8 -3
  246. package/src/utils/data-url.test.ts +52 -1
  247. package/src/utils/data-url.ts +21 -0
  248. package/src/utils/invoke-user-callback.test.ts +82 -0
  249. package/src/utils/invoke-user-callback.ts +31 -0
  250. package/src/utils/normalizePartStatus.ts +1 -1
  251. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -582,3 +582,176 @@ describe("ExternalThread attachments", () => {
582
582
  vi.restoreAllMocks();
583
583
  });
584
584
  });
585
+
586
+ describe("cancelled edit sessions", () => {
587
+ const editSetup = (
588
+ add: (arg: { file: File }) => Promise<PendingAttachment>,
589
+ ) =>
590
+ renderThreadWithProps({
591
+ messages: [
592
+ {
593
+ id: "u1",
594
+ role: "user",
595
+ content: [{ type: "text", text: "hi" }],
596
+ createdAt: new Date(0),
597
+ attachments: [],
598
+ metadata: { custom: {} },
599
+ } as unknown as ExternalThreadMessage,
600
+ ],
601
+ onEdit: () => {},
602
+ attachmentAdapter: {
603
+ accept: "*",
604
+ add,
605
+ send: async () => ({}) as never,
606
+ remove: async () => {},
607
+ },
608
+ });
609
+
610
+ it("does not leak an in-flight add into the next edit session", async () => {
611
+ let resolveAdd!: (attachment: PendingAttachment) => void;
612
+ const aui = editSetup(
613
+ () =>
614
+ new Promise<PendingAttachment>((resolve) => {
615
+ resolveAdd = resolve;
616
+ }),
617
+ );
618
+ const composer = () => aui().thread.message({ id: "u1" }).composer();
619
+
620
+ await act(async () => {
621
+ composer().beginEdit();
622
+ });
623
+ let addPromise!: Promise<void>;
624
+ await act(async () => {
625
+ addPromise = composer().addAttachment(
626
+ new File(["data"], "notes.txt", { type: "text/plain" }),
627
+ );
628
+ });
629
+ await act(async () => {
630
+ composer().cancel();
631
+ });
632
+ await act(async () => {
633
+ composer().beginEdit();
634
+ });
635
+ await act(async () => {
636
+ resolveAdd({
637
+ id: "leak-1",
638
+ type: "document",
639
+ name: "notes.txt",
640
+ contentType: "text/plain",
641
+ file: new File(["data"], "notes.txt", { type: "text/plain" }),
642
+ status: { type: "pending", reason: "uploading", progress: 0 },
643
+ } as PendingAttachment);
644
+ await addPromise.catch(() => {});
645
+ });
646
+
647
+ expect(composer().getState().attachments).toEqual([]);
648
+ });
649
+
650
+ it("adapter-removes the session's pending attachments on cancel, sparing prefilled ones", async () => {
651
+ const remove = vi.fn(async () => {});
652
+ const aui = renderThreadWithProps({
653
+ messages: [
654
+ {
655
+ id: "u1",
656
+ role: "user",
657
+ content: [{ type: "text", text: "hi" }],
658
+ createdAt: new Date(0),
659
+ attachments: [
660
+ {
661
+ id: "prefilled-1",
662
+ type: "document",
663
+ name: "existing.txt",
664
+ contentType: "text/plain",
665
+ content: [],
666
+ status: { type: "complete" },
667
+ } as unknown as CompleteAttachment,
668
+ ],
669
+ metadata: { custom: {} },
670
+ } as unknown as ExternalThreadMessage,
671
+ ],
672
+ onEdit: () => {},
673
+ attachmentAdapter: {
674
+ accept: "*",
675
+ add: async ({ file }: { file: File }) =>
676
+ ({
677
+ id: "pending-1",
678
+ type: "document",
679
+ name: file.name,
680
+ contentType: file.type,
681
+ file,
682
+ status: { type: "pending", reason: "uploading", progress: 0 },
683
+ }) as PendingAttachment,
684
+ send: async () => ({}) as never,
685
+ remove,
686
+ },
687
+ });
688
+ const composer = () => aui().thread.message({ id: "u1" }).composer();
689
+
690
+ await act(async () => {
691
+ composer().beginEdit();
692
+ });
693
+ await act(() =>
694
+ composer().addAttachment(
695
+ new File(["data"], "notes.txt", { type: "text/plain" }),
696
+ ),
697
+ );
698
+ expect(composer().getState().attachments).toHaveLength(2);
699
+
700
+ await act(async () => {
701
+ composer().cancel();
702
+ });
703
+
704
+ await waitFor(() => expect(remove).toHaveBeenCalledTimes(1));
705
+ expect(remove).toHaveBeenCalledWith(
706
+ expect.objectContaining({ id: "pending-1" }),
707
+ );
708
+
709
+ await act(async () => {
710
+ composer().cancel();
711
+ });
712
+ expect(remove).toHaveBeenCalledTimes(1);
713
+ });
714
+
715
+ it("keeps the thread composer's in-flight add across a run cancel", async () => {
716
+ let resolveAdd!: (attachment: PendingAttachment) => void;
717
+ const aui = renderThreadWithProps({
718
+ onNew: () => {},
719
+ onCancel: () => {},
720
+ attachmentAdapter: {
721
+ accept: "*",
722
+ add: () =>
723
+ new Promise<PendingAttachment>((resolve) => {
724
+ resolveAdd = resolve;
725
+ }),
726
+ send: async () => ({}) as never,
727
+ remove: async () => {},
728
+ },
729
+ });
730
+ const composer = () => aui().thread().composer();
731
+
732
+ let addPromise!: Promise<void>;
733
+ await act(async () => {
734
+ addPromise = composer().addAttachment(
735
+ new File(["data"], "notes.txt", { type: "text/plain" }),
736
+ );
737
+ });
738
+ await act(async () => {
739
+ composer().cancel();
740
+ });
741
+ await act(async () => {
742
+ resolveAdd({
743
+ id: "draft-1",
744
+ type: "document",
745
+ name: "notes.txt",
746
+ contentType: "text/plain",
747
+ file: new File(["data"], "notes.txt", { type: "text/plain" }),
748
+ status: { type: "pending", reason: "uploading", progress: 0 },
749
+ } as PendingAttachment);
750
+ await addPromise;
751
+ });
752
+
753
+ expect(composer().getState().attachments).toMatchObject([
754
+ { id: "draft-1" },
755
+ ]);
756
+ });
757
+ });
@@ -0,0 +1,47 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, render } from "@testing-library/react";
4
+ import type { FC } from "react";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { AuiProvider, useAui } from "@assistant-ui/store";
7
+ import { ExternalThread } from "../store/clients/external-thread";
8
+ import { SingleThreadList } from "../store/clients/single-thread-list";
9
+
10
+ let aui!: ReturnType<typeof useAui>;
11
+
12
+ const Capture: FC = () => {
13
+ aui = useAui();
14
+ return null;
15
+ };
16
+
17
+ const App: FC = () => {
18
+ const value = useAui({
19
+ threads: SingleThreadList({ thread: ExternalThread({ messages: [] }) }),
20
+ });
21
+ return (
22
+ <AuiProvider value={value}>
23
+ <Capture />
24
+ </AuiProvider>
25
+ );
26
+ };
27
+
28
+ afterEach(() => {
29
+ cleanup();
30
+ });
31
+
32
+ describe("SingleThreadList item selectors", () => {
33
+ it("resolves index selectors within the regular subset only", () => {
34
+ render(<App />);
35
+
36
+ const mainId = aui.threads.item("main").getState().id;
37
+ expect(aui.threads.item({ index: 0 }).getState().id).toBe(mainId);
38
+ expect(aui.threads.item({ index: 0, archived: false }).getState().id).toBe(
39
+ mainId,
40
+ );
41
+
42
+ expect(aui.threads.getState().archivedThreadIds).toEqual([]);
43
+ expect(() => aui.threads.item({ index: 0, archived: true })).toThrow(
44
+ "unknown item selector",
45
+ );
46
+ });
47
+ });
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { fromThreadMessageLike } from "../runtime/utils/thread-message-like";
2
+ import {
3
+ fromThreadMessageLike,
4
+ type ThreadMessageLike,
5
+ } from "../runtime/utils/thread-message-like";
6
+ import type { ToolCallMessagePartMcpMetadata } from "../types/message";
3
7
 
4
8
  const fallbackId = "test-id";
5
9
  const fallbackStatus = {
@@ -421,3 +425,37 @@ describe("fromThreadMessageLike provider metadata", () => {
421
425
  ]);
422
426
  });
423
427
  });
428
+
429
+ describe("fromThreadMessageLike MCP app metadata", () => {
430
+ it("keeps mcp on tool-call parts", () => {
431
+ const mcp: ToolCallMessagePartMcpMetadata = {
432
+ app: { resourceUri: "ui://demo/card" },
433
+ };
434
+ const message: ThreadMessageLike = {
435
+ role: "assistant",
436
+ content: [
437
+ {
438
+ type: "tool-call",
439
+ toolCallId: "call-1",
440
+ toolName: "show_card",
441
+ args: {},
442
+ argsText: "{}",
443
+ mcp,
444
+ },
445
+ ],
446
+ };
447
+
448
+ const result = fromThreadMessageLike(message, fallbackId, fallbackStatus);
449
+
450
+ expect(result.content).toEqual([
451
+ {
452
+ type: "tool-call",
453
+ toolCallId: "call-1",
454
+ toolName: "show_card",
455
+ args: {},
456
+ argsText: "{}",
457
+ mcp,
458
+ },
459
+ ]);
460
+ });
461
+ });
@@ -168,9 +168,9 @@ export type ToolApprovalOption = {
168
168
  readonly id: string;
169
169
  /**
170
170
  * Decision class. Drives approved-resolution and default rendering.
171
- * Open union: `_`-prefixed custom kinds are never auto-resolved and the
172
- * default kit skips them; they must be answered with an explicit
173
- * `approved` value (optionally alongside the `optionId`).
171
+ * Open union: `_`-prefixed custom kinds are never auto-resolved; they must
172
+ * be answered with an explicit `approved` value (optionally alongside the
173
+ * `optionId`), which is what the default kit sends when one is chosen.
174
174
  */
175
175
  readonly kind: ToolApprovalOptionKind | (string & {});
176
176
  /** Human label. Renderers supply defaults per kind when omitted. */
@@ -306,6 +306,11 @@ export type ToolCallMessagePartStatus =
306
306
  /** Reason the tool call requires action. */
307
307
  readonly reason: "tool-calls" | "interrupt";
308
308
  }
309
+ | {
310
+ readonly type: "incomplete";
311
+ readonly reason: "tool-calls";
312
+ readonly error?: ReadonlyJSONValue;
313
+ }
309
314
  | MessagePartStatus;
310
315
 
311
316
  export type MessageStatus =
@@ -1,5 +1,9 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { httpUrlPattern, parseDataUrl } from "./data-url";
2
+ import {
3
+ httpUrlPattern,
4
+ parseDataUrl,
5
+ resolveFilePartSource,
6
+ } from "./data-url";
3
7
 
4
8
  describe("parseDataUrl", () => {
5
9
  it("parses a base64 data URL", () => {
@@ -72,3 +76,50 @@ describe("httpUrlPattern", () => {
72
76
  },
73
77
  );
74
78
  });
79
+
80
+ describe("resolveFilePartSource", () => {
81
+ it("uses an explicit url source type for opaque values", () => {
82
+ expect(
83
+ resolveFilePartSource({
84
+ data: "provider-file-id",
85
+ mimeType: "application/pdf",
86
+ sourceType: "url",
87
+ }),
88
+ ).toEqual({ kind: "url", url: "provider-file-id" });
89
+ });
90
+
91
+ it("uses http URLs as url sources", () => {
92
+ expect(
93
+ resolveFilePartSource({
94
+ data: "https://example.com/file.pdf",
95
+ mimeType: "application/pdf",
96
+ }),
97
+ ).toEqual({ kind: "url", url: "https://example.com/file.pdf" });
98
+ });
99
+
100
+ it("decodes data URLs and uses their media type", () => {
101
+ expect(
102
+ resolveFilePartSource({
103
+ data: "data:image/png;base64,aGVsbG8=",
104
+ mimeType: "application/octet-stream",
105
+ }),
106
+ ).toEqual({
107
+ kind: "data",
108
+ data: "aGVsbG8=",
109
+ mimeType: "image/png",
110
+ });
111
+ });
112
+
113
+ it("passes through opaque data with its declared media type", () => {
114
+ expect(
115
+ resolveFilePartSource({
116
+ data: "provider-file-id",
117
+ mimeType: "application/pdf",
118
+ }),
119
+ ).toEqual({
120
+ kind: "data",
121
+ data: "provider-file-id",
122
+ mimeType: "application/pdf",
123
+ });
124
+ });
125
+ });
@@ -1,5 +1,9 @@
1
1
  export const httpUrlPattern = /^https?:\/\//i;
2
2
 
3
+ export type FilePartSource =
4
+ | { kind: "url"; url: string }
5
+ | { kind: "data"; data: string; mimeType: string };
6
+
3
7
  export function parseDataUrl(
4
8
  value: string,
5
9
  ): { mimeType: string; data: string } | null {
@@ -8,6 +12,23 @@ export function parseDataUrl(
8
12
  return { mimeType: match[1]!.toLowerCase(), data: match[2]! };
9
13
  }
10
14
 
15
+ export const resolveFilePartSource = (part: {
16
+ data: string;
17
+ mimeType: string;
18
+ sourceType?: string | undefined;
19
+ }): FilePartSource => {
20
+ if (part.sourceType === "url" || httpUrlPattern.test(part.data)) {
21
+ return { kind: "url", url: part.data };
22
+ }
23
+
24
+ const parsed = parseDataUrl(part.data);
25
+ return {
26
+ kind: "data",
27
+ data: parsed?.data ?? part.data,
28
+ mimeType: parsed?.mimeType ?? part.mimeType,
29
+ };
30
+ };
31
+
11
32
  /**
12
33
  * Whether a payload is something `new URL()` accepts. Adapters that place a
13
34
  * `FileMessagePart` payload into a url-typed wire field use this to decide
@@ -0,0 +1,82 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { invokeUserCallback } from "./invoke-user-callback";
3
+
4
+ describe("invokeUserCallback", () => {
5
+ afterEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ it("invokes a callback synchronously with its arguments", () => {
10
+ const callback = vi.fn((left: number, right: number) => left + right);
11
+
12
+ expect(invokeUserCallback("test", "onSuccess", callback, 2, 3)).toBe(
13
+ undefined,
14
+ );
15
+ expect(callback).toHaveBeenCalledOnce();
16
+ expect(callback).toHaveBeenCalledWith(2, 3);
17
+ });
18
+
19
+ it("reports and swallows a synchronous throw", () => {
20
+ const error = new Error("sync failure");
21
+ const consoleError = vi
22
+ .spyOn(console, "error")
23
+ .mockImplementation(() => {});
24
+
25
+ expect(() =>
26
+ invokeUserCallback("test", "onError", () => {
27
+ throw error;
28
+ }),
29
+ ).not.toThrow();
30
+ expect(consoleError).toHaveBeenCalledOnce();
31
+ expect(consoleError).toHaveBeenCalledWith(
32
+ "[test] onError callback threw an error",
33
+ error,
34
+ );
35
+ });
36
+
37
+ it("reports an asynchronous rejection and resolves", async () => {
38
+ const error = new Error("async failure");
39
+ const consoleError = vi
40
+ .spyOn(console, "error")
41
+ .mockImplementation(() => {});
42
+
43
+ await expect(
44
+ invokeUserCallback("test", "onError", async () => {
45
+ throw error;
46
+ }),
47
+ ).resolves.toBeUndefined();
48
+ expect(consoleError).toHaveBeenCalledOnce();
49
+ expect(consoleError).toHaveBeenCalledWith(
50
+ "[test] onError callback threw an error",
51
+ error,
52
+ );
53
+ });
54
+
55
+ it("resolves only after an asynchronous callback settles", async () => {
56
+ let settleCallback: (() => void) | undefined;
57
+ const callbackPromise = new Promise<void>((resolve) => {
58
+ settleCallback = resolve;
59
+ });
60
+ let settled = false;
61
+
62
+ const result = invokeUserCallback(
63
+ "test",
64
+ "onFinish",
65
+ () => callbackPromise,
66
+ );
67
+ void result?.then(() => {
68
+ settled = true;
69
+ });
70
+
71
+ await Promise.resolve();
72
+ expect(settled).toBe(false);
73
+
74
+ settleCallback?.();
75
+ await result;
76
+ expect(settled).toBe(true);
77
+ });
78
+
79
+ it("does nothing when the callback is undefined", () => {
80
+ expect(invokeUserCallback("test", "onMissing", undefined)).toBeUndefined();
81
+ });
82
+ });
@@ -0,0 +1,31 @@
1
+ export function invokeUserCallback<TArgs extends readonly unknown[]>(
2
+ tag: string,
3
+ name: string,
4
+ callback: ((...args: TArgs) => unknown) | undefined,
5
+ ...args: TArgs
6
+ ): void | Promise<void> {
7
+ if (!callback) return;
8
+
9
+ const reportError = (error: unknown) => {
10
+ try {
11
+ console.error(`[${tag}] ${name} callback threw an error`, error);
12
+ } catch {}
13
+ };
14
+
15
+ try {
16
+ const result = callback(...args);
17
+ if (
18
+ result !== null &&
19
+ (typeof result === "object" || typeof result === "function") &&
20
+ "then" in result &&
21
+ typeof result.then === "function"
22
+ ) {
23
+ return Promise.resolve(result).then(
24
+ () => undefined,
25
+ (error) => reportError(error),
26
+ );
27
+ }
28
+ } catch (error) {
29
+ reportError(error);
30
+ }
31
+ }
@@ -65,7 +65,7 @@ export const toMessagePartStatus = (
65
65
 
66
66
  if (part.type === "tool-call") {
67
67
  if (part.result === undefined) {
68
- return message.status as ToolCallMessagePartStatus;
68
+ return message.status;
69
69
  } else {
70
70
  return COMPLETE_STATUS;
71
71
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"queue-item.js","names":["FileMessagePart","TextMessagePart","QueuePlacement","QueueItemState","id","prompt","parts","EMPTY_QUEUE_ITEMS","Object","freeze","QueueItemMethods","getState","steer","move","placement","remove","QueueItemMeta","source","query","type","index","QueueItemClientSchema","methods","meta"],"sources":["../../../src/store/scopes/queue-item.ts"],"sourcesContent":["import type { FileMessagePart, TextMessagePart } from \"../../types/message\";\nimport type { QueuePlacement } from \"../../runtime/queue/external-thread-queue-adapter\";\n\nexport type QueueItemState = {\n readonly id: string;\n /** @deprecated Derive from the text parts of `parts` instead. Removal after 2026-11-05. */\n readonly prompt: string;\n readonly parts: readonly (FileMessagePart | TextMessagePart)[];\n};\n\nexport const EMPTY_QUEUE_ITEMS: readonly QueueItemState[] = Object.freeze([]);\n\nexport type QueueItemMethods = {\n getState(): QueueItemState;\n /** @deprecated Use `move({ lane: \"steer\", insertAfter: null })` instead. Removal after 2026-11-05. */\n steer(): void;\n move(placement: QueuePlacement): void;\n remove(): void;\n};\n\nexport type QueueItemMeta = {\n source: \"composer\";\n query: { type: \"index\"; index: number } | { type: \"id\"; id: string };\n};\n\nexport type QueueItemClientSchema = {\n methods: QueueItemMethods;\n meta: QueueItemMeta;\n};\n"],"mappings":";AAUA,MAAaO,oBAA+CC,OAAOC,OAAO,CAAA,CAAE"}