@assistant-ui/react 0.14.27 → 0.14.29

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 (257) hide show
  1. package/dist/client/ExternalThread.d.ts +11 -1
  2. package/dist/client/ExternalThread.d.ts.map +1 -1
  3. package/dist/client/ExternalThread.js +758 -513
  4. package/dist/client/ExternalThread.js.map +1 -1
  5. package/dist/context/react/utils/createContextStoreHook.d.ts.map +1 -1
  6. package/dist/context/react/utils/createStateHookForRuntime.d.ts.map +1 -1
  7. package/dist/legacy-runtime/AssistantRuntimeProvider.js.map +1 -1
  8. package/dist/legacy-runtime/cloud/auiV0.d.ts +12 -1
  9. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
  10. package/dist/legacy-runtime/cloud/auiV0.js +2 -1
  11. package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
  12. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +1 -1
  13. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts.map +1 -1
  14. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +7 -6
  15. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +1 -1
  16. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +1 -1
  17. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +30 -15
  18. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
  19. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  20. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +8 -4
  21. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  22. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +1 -0
  23. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +1 -1
  24. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  25. package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
  26. package/dist/model-context/makeAssistantVisible.js +7 -1
  27. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  28. package/dist/primitives/actionBar/ActionBarCopy.d.ts.map +1 -1
  29. package/dist/primitives/actionBar/ActionBarEdit.d.ts.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
  31. package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts.map +1 -1
  32. package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts.map +1 -1
  33. package/dist/primitives/actionBar/ActionBarReload.d.ts.map +1 -1
  34. package/dist/primitives/actionBar/ActionBarRoot.d.ts.map +1 -1
  35. package/dist/primitives/actionBar/ActionBarRoot.js +3 -3
  36. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  37. package/dist/primitives/actionBar/ActionBarSpeak.d.ts.map +1 -1
  38. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
  39. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts +7 -6
  40. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
  41. package/dist/primitives/actionBar/useActionBarFloatStatus.js +10 -11
  42. package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
  43. package/dist/primitives/actionBarMore/ActionBarMoreContent.d.ts.map +1 -1
  44. package/dist/primitives/actionBarMore/ActionBarMoreContent.js.map +1 -1
  45. package/dist/primitives/actionBarMore/ActionBarMoreItem.d.ts.map +1 -1
  46. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.d.ts.map +1 -1
  47. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.d.ts.map +1 -1
  48. package/dist/primitives/assistantModal/AssistantModalAnchor.d.ts.map +1 -1
  49. package/dist/primitives/assistantModal/AssistantModalContent.d.ts.map +1 -1
  50. package/dist/primitives/assistantModal/AssistantModalContent.js.map +1 -1
  51. package/dist/primitives/assistantModal/AssistantModalTrigger.d.ts.map +1 -1
  52. package/dist/primitives/assistantModal/popoverRenderPrimitives.d.ts.map +1 -1
  53. package/dist/primitives/attachment/AttachmentRemove.d.ts.map +1 -1
  54. package/dist/primitives/attachment/AttachmentRemove.js +1 -0
  55. package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
  56. package/dist/primitives/attachment/AttachmentRoot.d.ts.map +1 -1
  57. package/dist/primitives/attachment/AttachmentThumb.d.ts.map +1 -1
  58. package/dist/primitives/attachment/AttachmentThumb.js.map +1 -1
  59. package/dist/primitives/branchPicker/BranchPickerNext.d.ts.map +1 -1
  60. package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts.map +1 -1
  61. package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
  62. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  63. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
  64. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -0
  65. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
  66. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.d.ts.map +1 -1
  67. package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
  68. package/dist/primitives/composer/ComposerAddAttachment.js +8 -2
  69. package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
  70. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  71. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -3
  72. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  73. package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
  74. package/dist/primitives/composer/ComposerDictate.d.ts.map +1 -1
  75. package/dist/primitives/composer/ComposerDictationTranscript.d.ts.map +1 -1
  76. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  77. package/dist/primitives/composer/ComposerInput.js +6 -4
  78. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  79. package/dist/primitives/composer/ComposerQuote.d.ts.map +1 -1
  80. package/dist/primitives/composer/ComposerQuote.js.map +1 -1
  81. package/dist/primitives/composer/ComposerRoot.d.ts +5 -0
  82. package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
  83. package/dist/primitives/composer/ComposerRoot.js +86 -40
  84. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  85. package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
  86. package/dist/primitives/composer/ComposerSend.js +1 -0
  87. package/dist/primitives/composer/ComposerSend.js.map +1 -1
  88. package/dist/primitives/composer/ComposerStopDictation.d.ts.map +1 -1
  89. package/dist/primitives/composer/ComposerStopDictation.js +1 -0
  90. package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
  91. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  92. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  93. package/dist/primitives/composer/trigger/TriggerPopoverBack.d.ts.map +1 -1
  94. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
  95. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  96. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
  97. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  98. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  99. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  100. package/dist/primitives/composer/trigger/detectTrigger.d.ts +1 -1
  101. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  102. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  103. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  104. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  105. package/dist/primitives/dropdownMenuRenderPrimitives.d.ts.map +1 -1
  106. package/dist/primitives/error/ErrorMessage.d.ts.map +1 -1
  107. package/dist/primitives/error/ErrorMessage.js.map +1 -1
  108. package/dist/primitives/error/ErrorRoot.d.ts.map +1 -1
  109. package/dist/primitives/message/MessageParts.js.map +1 -1
  110. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  111. package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
  112. package/dist/primitives/messagePart/MessagePartImage.d.ts.map +1 -1
  113. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  114. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  115. package/dist/primitives/messagePart/useMessagePartData.d.ts +3 -2
  116. package/dist/primitives/messagePart/useMessagePartData.d.ts.map +1 -1
  117. package/dist/primitives/messagePart/useMessagePartData.js +3 -2
  118. package/dist/primitives/messagePart/useMessagePartData.js.map +1 -1
  119. package/dist/primitives/messagePart/useMessagePartFile.d.ts +5 -4
  120. package/dist/primitives/messagePart/useMessagePartFile.d.ts.map +1 -1
  121. package/dist/primitives/messagePart/useMessagePartFile.js +11 -3
  122. package/dist/primitives/messagePart/useMessagePartFile.js.map +1 -1
  123. package/dist/primitives/messagePart/useMessagePartImage.d.ts +5 -4
  124. package/dist/primitives/messagePart/useMessagePartImage.d.ts.map +1 -1
  125. package/dist/primitives/messagePart/useMessagePartImage.js +10 -3
  126. package/dist/primitives/messagePart/useMessagePartImage.js.map +1 -1
  127. package/dist/primitives/messagePart/useMessagePartReasoning.d.ts +5 -4
  128. package/dist/primitives/messagePart/useMessagePartReasoning.d.ts.map +1 -1
  129. package/dist/primitives/messagePart/useMessagePartReasoning.js +10 -3
  130. package/dist/primitives/messagePart/useMessagePartReasoning.js.map +1 -1
  131. package/dist/primitives/messagePart/useMessagePartSource.d.ts +6 -4
  132. package/dist/primitives/messagePart/useMessagePartSource.d.ts.map +1 -1
  133. package/dist/primitives/messagePart/useMessagePartSource.js +12 -3
  134. package/dist/primitives/messagePart/useMessagePartSource.js.map +1 -1
  135. package/dist/primitives/messagePart/useMessagePartText.d.ts +6 -5
  136. package/dist/primitives/messagePart/useMessagePartText.d.ts.map +1 -1
  137. package/dist/primitives/messagePart/useMessagePartText.js +10 -3
  138. package/dist/primitives/messagePart/useMessagePartText.js.map +1 -1
  139. package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -1
  140. package/dist/primitives/queueItem/QueueItemRemove.js +1 -0
  141. package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
  142. package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -1
  143. package/dist/primitives/queueItem/QueueItemSteer.js +1 -0
  144. package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
  145. package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -1
  146. package/dist/primitives/queueItem/QueueItemText.js.map +1 -1
  147. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.d.ts.map +1 -1
  148. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
  149. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  150. package/dist/primitives/suggestion/SuggestionDescription.d.ts.map +1 -1
  151. package/dist/primitives/suggestion/SuggestionDescription.js.map +1 -1
  152. package/dist/primitives/suggestion/SuggestionTitle.d.ts.map +1 -1
  153. package/dist/primitives/suggestion/SuggestionTitle.js.map +1 -1
  154. package/dist/primitives/suggestion/SuggestionTrigger.d.ts.map +1 -1
  155. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -0
  156. package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
  157. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  158. package/dist/primitives/thread/ThreadScrollToBottom.d.ts.map +1 -1
  159. package/dist/primitives/thread/ThreadScrollToBottom.js +1 -0
  160. package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
  161. package/dist/primitives/thread/ThreadSuggestion.d.ts.map +1 -1
  162. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  163. package/dist/primitives/thread/ThreadViewport.js +53 -19
  164. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  165. package/dist/primitives/thread/ThreadViewportFooter.d.ts.map +1 -1
  166. package/dist/primitives/thread/topAnchor/createReserveObservers.d.ts.map +1 -1
  167. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.d.ts +4 -0
  168. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.d.ts.map +1 -1
  169. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js +14 -1
  170. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js.map +1 -1
  171. package/dist/primitives/thread/topAnchor/topAnchorTurn.d.ts +13 -1
  172. package/dist/primitives/thread/topAnchor/topAnchorTurn.d.ts.map +1 -1
  173. package/dist/primitives/thread/topAnchor/topAnchorTurn.js +14 -1
  174. package/dist/primitives/thread/topAnchor/topAnchorTurn.js.map +1 -1
  175. package/dist/primitives/thread/useThreadViewportAutoScroll.js +2 -1
  176. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  177. package/dist/primitives/threadList/ThreadListLoadMore.d.ts.map +1 -1
  178. package/dist/primitives/threadList/ThreadListNew.d.ts.map +1 -1
  179. package/dist/primitives/threadList/ThreadListRoot.d.ts.map +1 -1
  180. package/dist/primitives/threadList/ThreadListRoot.js.map +1 -1
  181. package/dist/primitives/threadListFocusGroup.d.ts.map +1 -1
  182. package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts.map +1 -1
  183. package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts.map +1 -1
  184. package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
  185. package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
  186. package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts.map +1 -1
  187. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  188. package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts.map +1 -1
  189. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.d.ts.map +1 -1
  190. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
  191. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.d.ts.map +1 -1
  192. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
  193. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.d.ts.map +1 -1
  194. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.d.ts.map +1 -1
  195. package/dist/tests/setup.d.ts +1 -1
  196. package/dist/utils/Primitive.d.ts.map +1 -1
  197. package/dist/utils/Primitive.js.map +1 -1
  198. package/dist/utils/createActionButton.d.ts.map +1 -1
  199. package/dist/utils/hooks/useOnResizeContent.js +1 -0
  200. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  201. package/dist/utils/hooks/useSizeHandle.js +1 -0
  202. package/dist/utils/hooks/useSizeHandle.js.map +1 -1
  203. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  204. package/dist/utils/smooth/useSmooth.js +2 -2
  205. package/dist/utils/smooth/useSmooth.js.map +1 -1
  206. package/package.json +20 -20
  207. package/src/client/ExternalThread.ts +341 -53
  208. package/src/legacy-runtime/cloud/auiV0.ts +14 -0
  209. package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +4 -1
  210. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +37 -10
  211. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +79 -0
  212. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +97 -0
  213. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +113 -2
  214. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +10 -2
  215. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +189 -0
  216. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +20 -4
  217. package/src/model-context/makeAssistantVisible.test.tsx +146 -0
  218. package/src/model-context/makeAssistantVisible.tsx +18 -1
  219. package/src/primitives/actionBar/useActionBarFloatStatus.ts +7 -5
  220. package/src/primitives/composer/ComposerAddAttachment.test.tsx +108 -0
  221. package/src/primitives/composer/ComposerAddAttachment.ts +10 -4
  222. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +3 -2
  223. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +3 -2
  224. package/src/primitives/composer/ComposerInput.test.tsx +98 -1
  225. package/src/primitives/composer/ComposerInput.tsx +10 -8
  226. package/src/primitives/composer/ComposerRoot.test.tsx +226 -5
  227. package/src/primitives/composer/ComposerRoot.tsx +50 -1
  228. package/src/primitives/messagePart/useMessagePartData.ts +3 -2
  229. package/src/primitives/messagePart/useMessagePartFile.ts +19 -7
  230. package/src/primitives/messagePart/useMessagePartImage.ts +18 -7
  231. package/src/primitives/messagePart/useMessagePartReasoning.ts +15 -6
  232. package/src/primitives/messagePart/useMessagePartSource.ts +20 -7
  233. package/src/primitives/messagePart/useMessagePartText.ts +15 -5
  234. package/src/primitives/thread/ThreadViewport.tsx +15 -0
  235. package/src/primitives/thread/topAnchor/mountTopAnchorReserve.test.ts +243 -0
  236. package/src/primitives/thread/topAnchor/mountTopAnchorReserve.ts +28 -7
  237. package/src/primitives/thread/topAnchor/topAnchorTurn.test.ts +69 -0
  238. package/src/primitives/thread/topAnchor/topAnchorTurn.ts +29 -0
  239. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +86 -1
  240. package/src/primitives/thread/useThreadViewportAutoScroll.ts +6 -1
  241. package/src/tests/DataRenderers.test.tsx +212 -0
  242. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +1 -11
  243. package/src/tests/external-thread-attachments.test.tsx +372 -0
  244. package/src/tests/external-thread-parity.test.tsx +232 -0
  245. package/src/tests/generative-ui.test.tsx +1 -46
  246. package/src/tests/messagePartSwitchRace.test.tsx +172 -0
  247. package/src/tests/messagePartTypeMismatch.test.tsx +146 -0
  248. package/src/tests/threadMessageById.test.tsx +0 -7
  249. package/src/utils/smooth/useSmooth.ts +7 -4
  250. package/src/tests/BaseComposerRuntimeCore.test.ts +0 -612
  251. package/src/tests/BaseSubscribable.test.ts +0 -54
  252. package/src/tests/DataRenderers.test.ts +0 -355
  253. package/src/tests/ExternalStoreThreadRuntimeCore.test.ts +0 -685
  254. package/src/tests/LocalThreadRuntimeCore.test.ts +0 -324
  255. package/src/tests/auiV0Encode.test.ts +0 -188
  256. package/src/tests/external-message-converter.test.ts +0 -346
  257. package/src/tests/shouldContinue.test.ts +0 -122
@@ -0,0 +1,212 @@
1
+ // @vitest-environment jsdom
2
+
3
+ // Data part rendering contract: global renderers > global fallbacks >
4
+ // inline `components.data` config, exercised through the real store.
5
+
6
+ import { render, screen, waitFor } from "@testing-library/react";
7
+ import {
8
+ useEffect,
9
+ type FC,
10
+ type PropsWithChildren,
11
+ type ReactNode,
12
+ } from "react";
13
+ import { describe, expect, it } from "vitest";
14
+ import { useAui } from "@assistant-ui/store";
15
+ import { AssistantRuntimeProvider } from "../context";
16
+ import * as MessagePrimitive from "../primitives/message";
17
+ import * as ThreadPrimitive from "../primitives/thread";
18
+ import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
19
+ import {
20
+ useAssistantDataUI,
21
+ type ChatModelAdapter,
22
+ type DataMessagePartComponent,
23
+ type ThreadMessageLike,
24
+ } from "../index";
25
+
26
+ const noOpAdapter: ChatModelAdapter = {
27
+ async *run() {},
28
+ };
29
+
30
+ const dataMessages = (name: string): ThreadMessageLike[] => [
31
+ {
32
+ role: "assistant",
33
+ content: [{ type: "data", name, data: { value: 42 } }],
34
+ status: { type: "complete", reason: "stop" },
35
+ },
36
+ ];
37
+
38
+ const labeled =
39
+ (label: string): DataMessagePartComponent =>
40
+ ({ name, data }) => (
41
+ <span data-testid={label}>
42
+ {name}:{(data as { value: number }).value}
43
+ </span>
44
+ );
45
+
46
+ const RuntimeProvider: FC<
47
+ PropsWithChildren<{ messages: ThreadMessageLike[] }>
48
+ > = ({ children, messages }) => {
49
+ const runtime = useLocalRuntime(noOpAdapter, { initialMessages: messages });
50
+ return (
51
+ <AssistantRuntimeProvider runtime={runtime}>
52
+ {children}
53
+ </AssistantRuntimeProvider>
54
+ );
55
+ };
56
+
57
+ const GlobalDataUI: FC<{ name: string; render: DataMessagePartComponent }> = ({
58
+ name,
59
+ render,
60
+ }) => {
61
+ useAssistantDataUI({ name, render });
62
+ return null;
63
+ };
64
+
65
+ const GlobalFallbackDataUI: FC<{ render: DataMessagePartComponent }> = ({
66
+ render,
67
+ }) => {
68
+ const aui = useAui();
69
+ useEffect(() => aui.dataRenderers().setFallbackDataUI(render), [aui, render]);
70
+ return null;
71
+ };
72
+
73
+ const renderThread = (
74
+ messages: ThreadMessageLike[],
75
+ Message: FC,
76
+ extra?: ReactNode,
77
+ ) =>
78
+ render(
79
+ <RuntimeProvider messages={messages}>
80
+ {extra}
81
+ <ThreadPrimitive.Messages components={{ Message }} />
82
+ </RuntimeProvider>,
83
+ );
84
+
85
+ describe("data part rendering", () => {
86
+ it("renders a data part via the inline by_name config with its name and data", async () => {
87
+ const Message: FC = () => (
88
+ <MessagePrimitive.Parts
89
+ components={{ data: { by_name: { chart: labeled("inline") } } }}
90
+ />
91
+ );
92
+
93
+ renderThread(dataMessages("chart"), Message);
94
+
95
+ await waitFor(() => {
96
+ expect(screen.getByTestId("inline").textContent).toBe("chart:42");
97
+ });
98
+ });
99
+
100
+ it("uses the inline Fallback for unmatched names and renders nothing without one", async () => {
101
+ const Message: FC = () => (
102
+ <div data-testid="host">
103
+ <MessagePrimitive.Parts
104
+ components={{
105
+ data: { by_name: { other: labeled("named") } },
106
+ }}
107
+ />
108
+ </div>
109
+ );
110
+
111
+ renderThread(dataMessages("chart"), Message);
112
+ await waitFor(() => {
113
+ expect(screen.getByTestId("host").textContent).toBe("");
114
+ });
115
+
116
+ const MessageWithFallback: FC = () => (
117
+ <MessagePrimitive.Parts
118
+ components={{
119
+ data: {
120
+ by_name: { other: labeled("named") },
121
+ Fallback: labeled("inline-fallback"),
122
+ },
123
+ }}
124
+ />
125
+ );
126
+ renderThread(dataMessages("chart"), MessageWithFallback);
127
+ await waitFor(() => {
128
+ expect(screen.getByTestId("inline-fallback").textContent).toBe(
129
+ "chart:42",
130
+ );
131
+ });
132
+ });
133
+
134
+ it("prefers a globally registered renderer over the inline by_name config", async () => {
135
+ const Message: FC = () => (
136
+ <MessagePrimitive.Parts
137
+ components={{ data: { by_name: { chart: labeled("inline") } } }}
138
+ />
139
+ );
140
+
141
+ renderThread(
142
+ dataMessages("chart"),
143
+ Message,
144
+ <GlobalDataUI name="chart" render={labeled("global")} />,
145
+ );
146
+
147
+ await waitFor(() => {
148
+ expect(screen.getByTestId("global").textContent).toBe("chart:42");
149
+ });
150
+ expect(screen.queryByTestId("inline")).toBeNull();
151
+ });
152
+
153
+ it("resolves global fallbacks above inline fallbacks but below named matches", async () => {
154
+ const Message: FC = () => (
155
+ <MessagePrimitive.Parts
156
+ components={{ data: { Fallback: labeled("inline-fallback") } }}
157
+ />
158
+ );
159
+
160
+ renderThread(
161
+ dataMessages("chart"),
162
+ Message,
163
+ <GlobalFallbackDataUI render={labeled("global-fallback")} />,
164
+ );
165
+ await waitFor(() => {
166
+ expect(screen.getByTestId("global-fallback").textContent).toBe(
167
+ "chart:42",
168
+ );
169
+ });
170
+ expect(screen.queryByTestId("inline-fallback")).toBeNull();
171
+
172
+ renderThread(
173
+ dataMessages("chart"),
174
+ Message,
175
+ <>
176
+ <GlobalFallbackDataUI render={labeled("global-fallback-2")} />
177
+ <GlobalDataUI name="chart" render={labeled("global-named")} />
178
+ </>,
179
+ );
180
+ await waitFor(() => {
181
+ expect(screen.getByTestId("global-named").textContent).toBe("chart:42");
182
+ });
183
+ });
184
+
185
+ it("restores the inline renderer when the registering component unmounts", async () => {
186
+ const Message: FC = () => (
187
+ <MessagePrimitive.Parts
188
+ components={{ data: { by_name: { chart: labeled("inline") } } }}
189
+ />
190
+ );
191
+
192
+ const view = render(
193
+ <RuntimeProvider messages={dataMessages("chart")}>
194
+ <GlobalDataUI name="chart" render={labeled("global")} />
195
+ <ThreadPrimitive.Messages components={{ Message }} />
196
+ </RuntimeProvider>,
197
+ );
198
+ await waitFor(() => {
199
+ expect(screen.getByTestId("global")).toBeTruthy();
200
+ });
201
+
202
+ view.rerender(
203
+ <RuntimeProvider messages={dataMessages("chart")}>
204
+ <ThreadPrimitive.Messages components={{ Message }} />
205
+ </RuntimeProvider>,
206
+ );
207
+ await waitFor(() => {
208
+ expect(screen.getByTestId("inline").textContent).toBe("chart:42");
209
+ });
210
+ expect(screen.queryByTestId("global")).toBeNull();
211
+ });
212
+ });
@@ -67,17 +67,7 @@ const wrapInRuntimeAdapterProvider = (
67
67
  };
68
68
 
69
69
  describe("RemoteThreadListAdapter.unstable_Provider", () => {
70
- it("makes RuntimeAdapterProvider context visible to the runtime hook", async () => {
71
- const capture: { adapters: CapturedAdapters } = { adapters: null };
72
- const adapter = makeAdapter({
73
- unstable_Provider: wrapInRuntimeAdapterProvider(dummyHistory),
74
- });
75
- await renderAndWaitForBinder(adapter, capture);
76
-
77
- expect(capture.adapters?.history).toBe(dummyHistory);
78
- });
79
-
80
- it("preserves modelContext from the outer runtime-core RuntimeAdapterProvider", async () => {
70
+ it("makes Provider context visible to the runtime hook while preserving outer modelContext", async () => {
81
71
  const capture: { adapters: CapturedAdapters } = { adapters: null };
82
72
  const adapter = makeAdapter({
83
73
  unstable_Provider: wrapInRuntimeAdapterProvider(dummyHistory),
@@ -0,0 +1,372 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, render, waitFor } from "@testing-library/react";
4
+ import type { FC } from "react";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { AuiProvider, useAui } from "@assistant-ui/store";
7
+ import type {
8
+ ExternalThreadMessage,
9
+ ExternalThreadProps,
10
+ } from "../client/ExternalThread";
11
+ import { ExternalThread } from "../client/ExternalThread";
12
+
13
+ const renderThreadWithProps = (props: Partial<ExternalThreadProps>) => {
14
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
15
+ const Capture: FC = () => {
16
+ captured.aui = useAui();
17
+ return null;
18
+ };
19
+ const App: FC = () => {
20
+ const aui = useAui({
21
+ thread: ExternalThread({ messages: [], isRunning: false, ...props }),
22
+ });
23
+ return (
24
+ <AuiProvider value={aui}>
25
+ <Capture />
26
+ </AuiProvider>
27
+ );
28
+ };
29
+
30
+ render(<App />);
31
+ return () => captured.aui!;
32
+ };
33
+
34
+ const renderThread = () => {
35
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
36
+ const Capture: FC = () => {
37
+ captured.aui = useAui();
38
+ return null;
39
+ };
40
+ const App: FC = () => {
41
+ const aui = useAui({
42
+ thread: ExternalThread({ messages: [], isRunning: false }),
43
+ });
44
+ return (
45
+ <AuiProvider value={aui}>
46
+ <Capture />
47
+ </AuiProvider>
48
+ );
49
+ };
50
+
51
+ render(<App />);
52
+ return () => captured.aui!;
53
+ };
54
+
55
+ describe("ExternalThread attachments", () => {
56
+ it("adds prepared attachments when File is unavailable", async () => {
57
+ const aui = renderThread();
58
+ const fileConstructor = globalThis.File;
59
+ vi.stubGlobal("File", undefined);
60
+
61
+ try {
62
+ await act(async () => {
63
+ await aui()
64
+ .thread()
65
+ .composer()
66
+ .addAttachment({
67
+ name: "notes.txt",
68
+ contentType: "text/plain",
69
+ content: [{ type: "text", text: "hello" }],
70
+ });
71
+ });
72
+ } finally {
73
+ vi.stubGlobal("File", fileConstructor);
74
+ }
75
+
76
+ await waitFor(() => {
77
+ expect(aui().thread().composer().getState().attachments[0]).toMatchObject(
78
+ {
79
+ type: "document",
80
+ name: "notes.txt",
81
+ contentType: "text/plain",
82
+ content: [{ type: "text", text: "hello" }],
83
+ },
84
+ );
85
+ });
86
+ });
87
+
88
+ it("preserves foreign files that expose content", async () => {
89
+ const aui = renderThread();
90
+ const foreignFile = {
91
+ name: "photo.png",
92
+ type: "image/png",
93
+ lastModified: 0,
94
+ content: [{ type: "text", text: "implementation detail" }],
95
+ } as File;
96
+
97
+ await act(() => aui().thread().composer().addAttachment(foreignFile));
98
+
99
+ expect(aui().thread().composer().getState().attachments[0]).toMatchObject({
100
+ type: "file",
101
+ name: "photo.png",
102
+ contentType: "image/png",
103
+ file: foreignFile,
104
+ });
105
+ });
106
+
107
+ it("rejects files that do not match the adapter accept string", async () => {
108
+ const add = vi.fn(async () => ({}) as never);
109
+ const aui = renderThreadWithProps({
110
+ attachmentAdapter: {
111
+ accept: "image/*",
112
+ add,
113
+ send: async () => ({}) as never,
114
+ remove: async () => {},
115
+ },
116
+ });
117
+
118
+ await expect(
119
+ aui()
120
+ .thread()
121
+ .composer()
122
+ .addAttachment(
123
+ new File(["data"], "archive.zip", { type: "application/zip" }),
124
+ ),
125
+ ).rejects.toThrow(
126
+ "File type application/zip is not accepted. Accepted types: image/*",
127
+ );
128
+ expect(add).not.toHaveBeenCalled();
129
+ expect(aui().thread().composer().getState().attachments).toHaveLength(0);
130
+ });
131
+
132
+ it("rejects prepared attachments that do not match the adapter accept string", async () => {
133
+ const aui = renderThreadWithProps({
134
+ attachmentAdapter: {
135
+ accept: ".pdf",
136
+ add: async () => ({}) as never,
137
+ send: async () => ({}) as never,
138
+ remove: async () => {},
139
+ },
140
+ });
141
+
142
+ await expect(
143
+ aui()
144
+ .thread()
145
+ .composer()
146
+ .addAttachment({
147
+ name: "notes.txt",
148
+ contentType: "text/plain",
149
+ content: [{ type: "text", text: "hello" }],
150
+ }),
151
+ ).rejects.toThrow(
152
+ "File type text/plain is not accepted. Accepted types: .pdf",
153
+ );
154
+ expect(aui().thread().composer().getState().attachments).toHaveLength(0);
155
+ });
156
+
157
+ it("restores the draft when an attachment upload fails on send", async () => {
158
+ const consoleError = vi
159
+ .spyOn(console, "error")
160
+ .mockImplementation(() => {});
161
+ const sent: unknown[] = [];
162
+ let rejectSend!: (reason: unknown) => void;
163
+ const adapter = {
164
+ accept: "*",
165
+ add: async ({ file }: { file: File }) => ({
166
+ id: "att-1",
167
+ type: "file" as const,
168
+ name: file.name,
169
+ contentType: file.type,
170
+ file,
171
+ status: {
172
+ type: "requires-action" as const,
173
+ reason: "composer-send" as const,
174
+ },
175
+ }),
176
+ send: () =>
177
+ new Promise((_resolve, reject) => {
178
+ rejectSend = reject;
179
+ }) as never,
180
+ remove: async () => {},
181
+ };
182
+
183
+ const aui = renderThreadWithProps({
184
+ attachmentAdapter: adapter,
185
+ onNew: (message) => sent.push(message),
186
+ });
187
+
188
+ await act(() =>
189
+ aui()
190
+ .thread()
191
+ .composer()
192
+ .addAttachment(new File(["data"], "notes.txt", { type: "text/plain" })),
193
+ );
194
+ await act(async () => {
195
+ aui().thread().composer().setText("hello");
196
+ });
197
+ await act(async () => {
198
+ aui().thread().composer().send();
199
+ });
200
+
201
+ // The draft is cleared optimistically while the upload runs.
202
+ expect(aui().thread().composer().getState().text).toBe("");
203
+
204
+ await act(async () => {
205
+ rejectSend(new Error("upload failed"));
206
+ });
207
+
208
+ await waitFor(() => {
209
+ const state = aui().thread().composer().getState();
210
+ expect(state.text).toBe("hello");
211
+ expect(state.attachments).toHaveLength(1);
212
+ });
213
+ expect(sent).toHaveLength(0);
214
+ expect(consoleError).toHaveBeenCalledWith(
215
+ "Failed to send attachments",
216
+ expect.any(Error),
217
+ );
218
+ });
219
+
220
+ it.each([
221
+ [
222
+ "clearAttachments",
223
+ (c: { clearAttachments(): Promise<void> }) => c.clearAttachments(),
224
+ ],
225
+ ["reset", (c: { reset(): Promise<void> }) => c.reset()],
226
+ ] as const)(
227
+ "calls adapter.remove for pending attachments on %s",
228
+ async (_name, invoke) => {
229
+ const remove = vi.fn(async () => {});
230
+ const adapter = {
231
+ accept: "*",
232
+ add: async ({ file }: { file: File }) => ({
233
+ id: "att-1",
234
+ type: "file" as const,
235
+ name: file.name,
236
+ contentType: file.type,
237
+ file,
238
+ status: {
239
+ type: "requires-action" as const,
240
+ reason: "composer-send" as const,
241
+ },
242
+ }),
243
+ send: async () => ({}) as never,
244
+ remove,
245
+ };
246
+
247
+ const aui = renderThreadWithProps({ attachmentAdapter: adapter });
248
+
249
+ await act(() =>
250
+ aui()
251
+ .thread()
252
+ .composer()
253
+ .addAttachment(
254
+ new File(["data"], "notes.txt", { type: "text/plain" }),
255
+ ),
256
+ );
257
+
258
+ await act(() => invoke(aui().thread().composer()));
259
+
260
+ expect(remove).toHaveBeenCalledTimes(1);
261
+ expect(remove).toHaveBeenCalledWith(
262
+ expect.objectContaining({ id: "att-1" }),
263
+ );
264
+ expect(aui().thread().composer().getState().attachments).toHaveLength(0);
265
+ },
266
+ );
267
+
268
+ it("routes edit-composer attachments through the adapter", async () => {
269
+ const add = vi.fn(async ({ file }: { file: File }) => ({
270
+ id: "att-edit",
271
+ type: "file" as const,
272
+ name: file.name,
273
+ contentType: file.type,
274
+ file,
275
+ status: {
276
+ type: "requires-action" as const,
277
+ reason: "composer-send" as const,
278
+ },
279
+ }));
280
+ const aui = renderThreadWithProps({
281
+ messages: [
282
+ {
283
+ id: "u1",
284
+ role: "user",
285
+ content: [{ type: "text", text: "hi" }],
286
+ createdAt: new Date(0),
287
+ attachments: [],
288
+ metadata: { custom: {} },
289
+ } as unknown as ExternalThreadMessage,
290
+ ],
291
+ onEdit: () => {},
292
+ attachmentAdapter: {
293
+ accept: "*",
294
+ add,
295
+ send: async () => ({}) as never,
296
+ remove: async () => {},
297
+ },
298
+ });
299
+
300
+ const composer = () => aui().thread().message({ id: "u1" }).composer();
301
+ await act(async () => {
302
+ composer().beginEdit();
303
+ });
304
+ await act(() =>
305
+ composer().addAttachment(
306
+ new File(["data"], "notes.txt", { type: "text/plain" }),
307
+ ),
308
+ );
309
+
310
+ expect(add).toHaveBeenCalledTimes(1);
311
+ expect(composer().getState().attachments[0]).toMatchObject({
312
+ id: "att-edit",
313
+ name: "notes.txt",
314
+ });
315
+ });
316
+
317
+ it("merges the failed send into a draft the user already modified", async () => {
318
+ vi.spyOn(console, "error").mockImplementation(() => {});
319
+ let rejectSend!: (reason: unknown) => void;
320
+ const adapter = {
321
+ accept: "*",
322
+ add: async ({ file }: { file: File }) => ({
323
+ id: "att-1",
324
+ type: "file" as const,
325
+ name: file.name,
326
+ contentType: file.type,
327
+ file,
328
+ status: {
329
+ type: "requires-action" as const,
330
+ reason: "composer-send" as const,
331
+ },
332
+ }),
333
+ send: () =>
334
+ new Promise((_resolve, reject) => {
335
+ rejectSend = reject;
336
+ }) as never,
337
+ remove: async () => {},
338
+ };
339
+
340
+ const aui = renderThreadWithProps({
341
+ attachmentAdapter: adapter,
342
+ onNew: () => {},
343
+ });
344
+
345
+ await act(() =>
346
+ aui()
347
+ .thread()
348
+ .composer()
349
+ .addAttachment(new File(["data"], "notes.txt", { type: "text/plain" })),
350
+ );
351
+ await act(async () => {
352
+ aui().thread().composer().setText("hello");
353
+ aui().thread().composer().send();
354
+ });
355
+ await act(async () => {
356
+ aui().thread().composer().setText("meanwhile");
357
+ });
358
+
359
+ await act(async () => {
360
+ rejectSend(new Error("upload failed"));
361
+ });
362
+
363
+ const state = aui().thread().composer().getState();
364
+ expect(state.text).toBe("hello\nmeanwhile");
365
+ expect(state.attachments).toHaveLength(1);
366
+ expect(state.attachments[0]).toMatchObject({ id: "att-1" });
367
+ });
368
+
369
+ afterEach(() => {
370
+ vi.restoreAllMocks();
371
+ });
372
+ });