@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,232 @@
1
+ // @vitest-environment jsdom
2
+
3
+ // Legacy-runtime parity: part statuses derive from the message status, and
4
+ // imperative composer call sequences observe writes before React re-renders.
5
+
6
+ import { render, waitFor } from "@testing-library/react";
7
+ import type { FC } from "react";
8
+ import { describe, it, expect, vi } from "vitest";
9
+ import { useAui, AuiProvider } from "@assistant-ui/store";
10
+ import type { ThreadMessage } from "@assistant-ui/core";
11
+ import {
12
+ ExternalThread,
13
+ type ExternalThreadProps,
14
+ type ExternalThreadMessage,
15
+ } from "../client/ExternalThread";
16
+
17
+ const renderThread = (props: ExternalThreadProps) => {
18
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
19
+ const Capture: FC = () => {
20
+ captured.aui = useAui();
21
+ return null;
22
+ };
23
+ const App: FC<{ threadProps: ExternalThreadProps }> = ({ threadProps }) => {
24
+ const aui = useAui({ thread: ExternalThread(threadProps) });
25
+ return (
26
+ <AuiProvider value={aui}>
27
+ <Capture />
28
+ </AuiProvider>
29
+ );
30
+ };
31
+ const utils = render(<App threadProps={props} />);
32
+ return {
33
+ aui: () => captured.aui!,
34
+ rerender: (next: ExternalThreadProps) =>
35
+ utils.rerender(<App threadProps={next} />),
36
+ };
37
+ };
38
+
39
+ const assistantMessage = (
40
+ status: ThreadMessage["status"],
41
+ result?: string,
42
+ ): ExternalThreadMessage =>
43
+ ({
44
+ id: "a1",
45
+ role: "assistant",
46
+ content: [
47
+ { type: "text", text: "let me check" },
48
+ {
49
+ type: "tool-call",
50
+ toolCallId: "tc1",
51
+ toolName: "probe_tool",
52
+ args: {},
53
+ argsText: "{}",
54
+ ...(result !== undefined && { result }),
55
+ },
56
+ ],
57
+ createdAt: new Date(0),
58
+ status,
59
+ metadata: { custom: {} },
60
+ }) as unknown as ExternalThreadMessage;
61
+
62
+ describe("ExternalThread part status", () => {
63
+ it("gives an unresolved tool call its message's status", () => {
64
+ const { aui } = renderThread({
65
+ messages: [assistantMessage({ type: "running" })],
66
+ isRunning: true,
67
+ });
68
+ const part = (toolCallId: string) =>
69
+ aui().thread().message({ id: "a1" }).part({ toolCallId }).getState();
70
+ expect(part("tc1").status).toEqual({ type: "running" });
71
+ });
72
+
73
+ it("marks a resolved tool call complete and streams only the last part", () => {
74
+ const { aui } = renderThread({
75
+ messages: [assistantMessage({ type: "running" }, "ok")],
76
+ isRunning: true,
77
+ });
78
+ const state = aui().thread().message({ id: "a1" }).getState();
79
+ expect(state.parts[0]!.status).toEqual({ type: "complete" });
80
+ expect(state.parts[1]!.status).toEqual({ type: "complete" });
81
+ });
82
+
83
+ it("keeps parts complete on requires-action and user messages", () => {
84
+ const { aui } = renderThread({
85
+ messages: [
86
+ {
87
+ id: "u1",
88
+ role: "user",
89
+ content: [{ type: "text", text: "hi" }],
90
+ createdAt: new Date(0),
91
+ attachments: [],
92
+ metadata: { custom: {} },
93
+ } as unknown as ExternalThreadMessage,
94
+ assistantMessage({ type: "requires-action", reason: "tool-calls" }),
95
+ ],
96
+ isRunning: false,
97
+ });
98
+ expect(
99
+ aui().thread().message({ id: "u1" }).part({ index: 0 }).getState().status,
100
+ ).toEqual({ type: "complete" });
101
+ expect(
102
+ aui()
103
+ .thread()
104
+ .message({ id: "a1" })
105
+ .part({ toolCallId: "tc1" })
106
+ .getState().status,
107
+ ).toEqual({ type: "requires-action", reason: "tool-calls" });
108
+ expect(
109
+ aui().thread().message({ id: "a1" }).part({ index: 0 }).getState().status,
110
+ ).toEqual({ type: "complete" });
111
+ });
112
+ });
113
+
114
+ describe("ExternalThread unset optional callbacks", () => {
115
+ it("throws a capability error when the callback prop is not set", () => {
116
+ const { aui } = renderThread({
117
+ messages: [
118
+ assistantMessage({ type: "requires-action", reason: "tool-calls" }),
119
+ ],
120
+ isRunning: false,
121
+ });
122
+ const part = () =>
123
+ aui().thread().message({ id: "a1" }).part({ toolCallId: "tc1" });
124
+
125
+ expect(() => part().addToolResult("ok")).toThrow(
126
+ "Runtime does not support tool results (onAddToolResult is not set).",
127
+ );
128
+ expect(() => part().resumeToolCall(undefined)).toThrow(
129
+ "Runtime does not support resuming tool calls (onResumeToolCall is not set).",
130
+ );
131
+ expect(() => aui().thread().resumeRun()).toThrow(
132
+ "Runtime does not support resuming runs (onResume is not set).",
133
+ );
134
+ expect(() => aui().thread().importExternalState({})).toThrow(
135
+ "Runtime does not support importing external states (onLoadExternalState is not set).",
136
+ );
137
+ });
138
+ });
139
+
140
+ describe("ExternalThread composer", () => {
141
+ it("dispatches a synchronous setText + send sequence", async () => {
142
+ const onNew = vi.fn();
143
+ const { aui } = renderThread({ messages: [], isRunning: false, onNew });
144
+
145
+ aui().thread().composer().setText("hello");
146
+ aui().thread().composer().send();
147
+
148
+ await waitFor(() => expect(onNew).toHaveBeenCalledTimes(1));
149
+ expect(onNew.mock.calls[0]![0].content).toEqual([
150
+ { type: "text", text: "hello" },
151
+ ]);
152
+ await waitFor(() =>
153
+ expect(aui().thread().getState().composer.text).toBe(""),
154
+ );
155
+ });
156
+
157
+ it("stamps the thread head as parentId on queue-adapter sends", async () => {
158
+ const enqueue = vi.fn();
159
+ const { aui } = renderThread({
160
+ messages: [
161
+ {
162
+ id: "u1",
163
+ role: "user",
164
+ content: [{ type: "text", text: "hi" }],
165
+ createdAt: new Date(0),
166
+ attachments: [],
167
+ metadata: { custom: {} },
168
+ } as unknown as ExternalThreadMessage,
169
+ ],
170
+ isRunning: true,
171
+ queue: {
172
+ items: [],
173
+ enqueue,
174
+ steer: vi.fn(),
175
+ remove: vi.fn(),
176
+ clear: vi.fn(),
177
+ },
178
+ });
179
+
180
+ aui().thread().composer().setText("queued");
181
+ aui().thread().composer().send();
182
+
183
+ await waitFor(() => expect(enqueue).toHaveBeenCalledTimes(1));
184
+ expect(enqueue.mock.calls[0]![0].parentId).toBe("u1");
185
+ });
186
+
187
+ it("still refuses to send an empty composer synchronously after a send", async () => {
188
+ const onNew = vi.fn();
189
+ const { aui } = renderThread({ messages: [], isRunning: false, onNew });
190
+
191
+ aui().thread().composer().send();
192
+ aui().thread().composer().setText("first");
193
+ aui().thread().composer().send();
194
+ aui().thread().composer().send();
195
+
196
+ await new Promise((resolve) => setTimeout(resolve, 0));
197
+ expect(onNew).toHaveBeenCalledTimes(1);
198
+ });
199
+ });
200
+
201
+ describe("ExternalThread duplicate message ids", () => {
202
+ const userMessage = (id: string, text: string): ExternalThreadMessage =>
203
+ ({
204
+ id,
205
+ role: "user",
206
+ content: [{ type: "text", text }],
207
+ createdAt: new Date(0),
208
+ metadata: { custom: {} },
209
+ }) as unknown as ExternalThreadMessage;
210
+
211
+ it("warns and keeps the last occurrence instead of throwing on a duplicate id", () => {
212
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
213
+ try {
214
+ const { aui } = renderThread({
215
+ messages: [
216
+ userMessage("u1", "hi"),
217
+ userMessage("dup", "stale"),
218
+ userMessage("dup", "fresh"),
219
+ ],
220
+ });
221
+
222
+ const state = aui().thread().getState();
223
+ expect(state.messages.map((m) => m.id)).toEqual(["u1", "dup"]);
224
+
225
+ const dup = aui().thread().message({ id: "dup" }).getState();
226
+ expect(dup.parts[0]).toMatchObject({ type: "text", text: "fresh" });
227
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('"dup"'));
228
+ } finally {
229
+ warn.mockRestore();
230
+ }
231
+ });
232
+ });
@@ -66,37 +66,6 @@ describe("MessagePrimitive.GenerativeUI (same-realm renderer)", () => {
66
66
  expect(out).toContain("hello world");
67
67
  });
68
68
 
69
- it("throws GenerativeUIRenderError for unknown components", () => {
70
- const spec: GenerativeUISpec = {
71
- root: { component: "NotAllowed", props: {} },
72
- };
73
-
74
- expect(() =>
75
- renderToStaticMarkup(
76
- <GenerativeUIRender spec={spec} components={{ Card }} />,
77
- ),
78
- ).toThrow(GenerativeUIRenderError);
79
- });
80
-
81
- it("uses Fallback when provided for unknown components", () => {
82
- const spec: GenerativeUISpec = {
83
- root: { component: "NotAllowed", props: { foo: 1 } },
84
- };
85
- const Fallback = ({ component }: any) => (
86
- <span data-fallback={component}>missing</span>
87
- );
88
-
89
- const out = renderToStaticMarkup(
90
- <GenerativeUIRender
91
- spec={spec}
92
- components={{ Card }}
93
- Fallback={Fallback}
94
- />,
95
- );
96
- expect(out).toContain('data-fallback="NotAllowed"');
97
- expect(out).toContain("missing");
98
- });
99
-
100
69
  it("handles empty / partial specs gracefully (stream-friendly)", () => {
101
70
  const out1 = renderToStaticMarkup(
102
71
  <GenerativeUIRender spec={{ root: [] }} components={{ Card }} />,
@@ -133,21 +102,7 @@ describe("MessagePrimitive.GenerativeUI (same-realm renderer)", () => {
133
102
  }
134
103
  });
135
104
 
136
- it("respects user-provided keys", () => {
137
- const spec: GenerativeUISpec = {
138
- root: [
139
- { component: "Card", props: { title: "x" }, key: "stable-1" },
140
- { component: "Card", props: { title: "y" }, key: "stable-2" },
141
- ],
142
- };
143
- expect(() =>
144
- renderToStaticMarkup(
145
- <GenerativeUIRender spec={spec} components={{ Card }} />,
146
- ),
147
- ).not.toThrow();
148
- });
149
-
150
- it("is a typed error subclass with the offending component name", () => {
105
+ it("throws a typed error naming the offending unknown component", () => {
151
106
  try {
152
107
  renderToStaticMarkup(
153
108
  <GenerativeUIRender
@@ -0,0 +1,172 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, cleanup, render, waitFor } from "@testing-library/react";
4
+ import { flushSync } from "react-dom";
5
+ import { useEffect, useState, type FC } from "react";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { useAui, useAuiState } from "@assistant-ui/store";
8
+ import { AssistantRuntimeProvider, PartByIndexProvider } from "../context";
9
+ import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
10
+ import type { ChatModelAdapter } from "../legacy-runtime/runtime-cores/local/ChatModelAdapter";
11
+ import * as ThreadPrimitive from "../primitives/thread";
12
+ import * as MessagePrimitive from "../primitives/message";
13
+ import { useMessagePartText } from "../primitives/messagePart/useMessagePartText";
14
+ import type { AssistantRuntime, ThreadMessageLike } from "../index";
15
+
16
+ const noOpAdapter: ChatModelAdapter = {
17
+ async *run() {},
18
+ };
19
+
20
+ const textMessages: readonly ThreadMessageLike[] = [
21
+ {
22
+ role: "assistant",
23
+ content: [
24
+ { type: "text", text: "hello" },
25
+ { type: "text", text: "world" },
26
+ ],
27
+ },
28
+ ];
29
+
30
+ const toolMessages: readonly ThreadMessageLike[] = [
31
+ {
32
+ role: "assistant",
33
+ content: [
34
+ { type: "tool-call", toolCallId: "tc-1", toolName: "search", args: {} },
35
+ { type: "tool-call", toolCallId: "tc-2", toolName: "search", args: {} },
36
+ ],
37
+ },
38
+ ];
39
+
40
+ const shorterTextMessages: readonly ThreadMessageLike[] = [
41
+ {
42
+ role: "assistant",
43
+ content: [{ type: "text", text: "shorter" }],
44
+ },
45
+ ];
46
+
47
+ // Re-renders a mounted text-part leaf via flushSync from a store subscriber,
48
+ // mimicking a popup library or state mirror flushing synchronously inside the
49
+ // thread-switch window, before reconciliation has unmounted the stale leaf.
50
+ const harness: {
51
+ bump: (() => void) | null;
52
+ runtime: AssistantRuntime | null;
53
+ } = { bump: null, runtime: null };
54
+
55
+ const TextLeaf: FC = () => {
56
+ const [, setTick] = useState(0);
57
+ harness.bump = () => setTick((t) => t + 1);
58
+ const part = useMessagePartText();
59
+ const aui = useAui();
60
+ useEffect(
61
+ () =>
62
+ aui.subscribe(() => {
63
+ flushSync(() => harness.bump?.());
64
+ }),
65
+ [aui],
66
+ );
67
+ return <p data-testid="text">{part.text}</p>;
68
+ };
69
+
70
+ const Message: FC = () => (
71
+ <MessagePrimitive.Parts
72
+ components={{
73
+ Text: TextLeaf,
74
+ tools: { Fallback: () => <div data-testid="tool" /> },
75
+ }}
76
+ />
77
+ );
78
+
79
+ const ChildrenMessage: FC = () => (
80
+ <MessagePrimitive.Parts>
81
+ {({ part }) => (part.type === "text" ? <TextLeaf /> : null)}
82
+ </MessagePrimitive.Parts>
83
+ );
84
+
85
+ const InvalidPartReader: FC = () => {
86
+ const part = useAuiState((s) => s.part);
87
+ return (
88
+ <p data-testid="clamped-part">
89
+ {part.type === "text" ? part.text : part.type}
90
+ </p>
91
+ );
92
+ };
93
+
94
+ const InvalidPartMessage: FC = () => (
95
+ <PartByIndexProvider index={2}>
96
+ <InvalidPartReader />
97
+ </PartByIndexProvider>
98
+ );
99
+
100
+ const App: FC<{ MessageComponent?: FC }> = ({ MessageComponent = Message }) => {
101
+ const runtime = useLocalRuntime(noOpAdapter, {
102
+ initialMessages: textMessages,
103
+ });
104
+ harness.runtime = runtime;
105
+ return (
106
+ <AssistantRuntimeProvider runtime={runtime}>
107
+ <ThreadPrimitive.Messages components={{ Message: MessageComponent }} />
108
+ </AssistantRuntimeProvider>
109
+ );
110
+ };
111
+
112
+ afterEach(() => {
113
+ cleanup();
114
+ harness.bump = null;
115
+ harness.runtime = null;
116
+ });
117
+
118
+ describe("part hooks under a thread-switch race", () => {
119
+ it("swaps every part when the incoming message has different part types", async () => {
120
+ let view!: ReturnType<typeof render>;
121
+ await act(async () => {
122
+ view = render(<App />);
123
+ });
124
+ await waitFor(() => expect(view.getAllByTestId("text")).toHaveLength(2));
125
+
126
+ await act(async () => {
127
+ harness.runtime!.thread.reset(toolMessages);
128
+ });
129
+
130
+ await waitFor(() => expect(view.getAllByTestId("tool")).toHaveLength(2));
131
+ expect(view.queryAllByTestId("text")).toHaveLength(0);
132
+ });
133
+
134
+ it.each([
135
+ ["components API", Message],
136
+ ["children API", ChildrenMessage],
137
+ ])(
138
+ "does not crash when the incoming message has fewer parts with the %s",
139
+ async (_label, MessageComponent) => {
140
+ let view!: ReturnType<typeof render>;
141
+ await act(async () => {
142
+ view = render(<App MessageComponent={MessageComponent} />);
143
+ });
144
+ await waitFor(() => expect(view.getAllByTestId("text")).toHaveLength(2));
145
+
146
+ await act(async () => {
147
+ harness.runtime!.thread.reset(shorterTextMessages);
148
+ });
149
+
150
+ await waitFor(() => expect(view.getAllByTestId("text")).toHaveLength(1));
151
+ expect(view.getByTestId("text").textContent).toBe("shorter");
152
+ },
153
+ );
154
+
155
+ it("warns and clamps an index that was never valid", async () => {
156
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
157
+ try {
158
+ let view!: ReturnType<typeof render>;
159
+ await act(async () => {
160
+ view = render(<App MessageComponent={InvalidPartMessage} />);
161
+ });
162
+ expect(warn).toHaveBeenCalledWith(
163
+ expect.stringContaining(
164
+ "useClientLookup: Clamped stale index 2 to 1 (length: 2)",
165
+ ),
166
+ );
167
+ expect(view.getByTestId("clamped-part").textContent).toBe("world");
168
+ } finally {
169
+ warn.mockRestore();
170
+ }
171
+ });
172
+ });
@@ -0,0 +1,146 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { type FC, type PropsWithChildren, type ReactNode } from "react";
4
+ import { render, screen } from "@testing-library/react";
5
+ import { describe, expect, it } from "vitest";
6
+ import { AssistantRuntimeProvider } from "../context";
7
+ import * as MessagePartPrimitive from "../primitives/messagePart";
8
+ import * as MessagePrimitive from "../primitives/message";
9
+ import * as ThreadPrimitive from "../primitives/thread";
10
+ import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
11
+ import {
12
+ useMessagePartReasoning,
13
+ useMessagePartSource,
14
+ useMessagePartFile,
15
+ type ChatModelAdapter,
16
+ type ThreadMessageLike,
17
+ } from "../index";
18
+
19
+ const noOpAdapter: ChatModelAdapter = {
20
+ async *run() {},
21
+ };
22
+
23
+ // A thread whose only assistant message has a single tool-call part at index 0.
24
+ // During a thread switch, a still-mounted text/image/reasoning/source/file
25
+ // consumer at (0, 0) re-resolves its index-derived part scope against the new
26
+ // thread and reads this tool-call part, so its selector must not throw inside
27
+ // useSyncExternalStore's getSnapshot (#5118).
28
+ const toolCallOnly: ThreadMessageLike[] = [
29
+ {
30
+ role: "assistant",
31
+ content: [
32
+ {
33
+ type: "tool-call",
34
+ toolCallId: "tc-1",
35
+ toolName: "search",
36
+ args: {},
37
+ argsText: "{}",
38
+ },
39
+ ],
40
+ status: { type: "complete", reason: "stop" },
41
+ },
42
+ ];
43
+
44
+ const RuntimeProvider: FC<
45
+ PropsWithChildren<{ messages: ThreadMessageLike[] }>
46
+ > = ({ messages, children }) => {
47
+ const runtime = useLocalRuntime(noOpAdapter, { initialMessages: messages });
48
+ return (
49
+ <AssistantRuntimeProvider runtime={runtime}>
50
+ {children}
51
+ </AssistantRuntimeProvider>
52
+ );
53
+ };
54
+
55
+ // Renders a part consumer under a Message whose parts always render it,
56
+ // regardless of the part type. This mounts the consumer inside the part scope
57
+ // at index 0, where it reads the tool-call part — the transient state a
58
+ // thread switch produces for a still-mounted consumer.
59
+ const MessageWith: FC<{ renderPart: () => ReactNode }> = ({ renderPart }) => (
60
+ <MessagePrimitive.Parts>{() => renderPart()}</MessagePrimitive.Parts>
61
+ );
62
+
63
+ const renderToolCallMessage = (renderPart: () => ReactNode) => {
64
+ const Message: FC = () => <MessageWith renderPart={renderPart} />;
65
+ render(
66
+ <RuntimeProvider messages={toolCallOnly}>
67
+ <ThreadPrimitive.Messages components={{ Message }} />
68
+ </RuntimeProvider>,
69
+ );
70
+ };
71
+
72
+ describe("part hooks tolerate a transient part-type mismatch", () => {
73
+ it("MessagePartPrimitive.Text renders empty when the part is a tool-call", async () => {
74
+ renderToolCallMessage(() => (
75
+ <MessagePartPrimitive.Text data-testid="text" />
76
+ ));
77
+
78
+ const text = await screen.findByTestId("text");
79
+ expect(text.textContent).toBe("");
80
+ });
81
+
82
+ it("MessagePartPrimitive.Image renders an empty image when the part is a tool-call", async () => {
83
+ renderToolCallMessage(() => (
84
+ <MessagePartPrimitive.Image data-testid="image" alt="" />
85
+ ));
86
+
87
+ const image = await screen.findByTestId("image");
88
+ expect(image.tagName).toBe("IMG");
89
+ });
90
+
91
+ it("useMessagePartReasoning returns an empty reasoning part when the part is a tool-call", async () => {
92
+ const Reader: FC = () => {
93
+ const part = useMessagePartReasoning();
94
+ return <span data-testid="reasoning">{part.text}</span>;
95
+ };
96
+ renderToolCallMessage(() => <Reader />);
97
+
98
+ const reasoning = await screen.findByTestId("reasoning");
99
+ expect(reasoning.textContent).toBe("");
100
+ });
101
+
102
+ it("useMessagePartSource returns an empty source part when the part is a tool-call", async () => {
103
+ const Reader: FC = () => {
104
+ const part = useMessagePartSource();
105
+ return <span data-testid="source">{part.sourceType}</span>;
106
+ };
107
+ renderToolCallMessage(() => <Reader />);
108
+
109
+ const source = await screen.findByTestId("source");
110
+ expect(source.textContent).toBe("url");
111
+ });
112
+
113
+ it("useMessagePartFile returns an empty file part when the part is a tool-call", async () => {
114
+ const Reader: FC = () => {
115
+ const part = useMessagePartFile();
116
+ return <span data-testid="file">{part.mimeType}</span>;
117
+ };
118
+ renderToolCallMessage(() => <Reader />);
119
+
120
+ const file = await screen.findByTestId("file");
121
+ expect(file.textContent).toBe("");
122
+ });
123
+
124
+ it("a real text part still renders its text", async () => {
125
+ const textMessage: ThreadMessageLike[] = [
126
+ {
127
+ role: "assistant",
128
+ content: [{ type: "text", text: "hello" }],
129
+ status: { type: "complete", reason: "stop" },
130
+ },
131
+ ];
132
+ const Message: FC = () => (
133
+ <MessageWith
134
+ renderPart={() => <MessagePartPrimitive.Text data-testid="text" />}
135
+ />
136
+ );
137
+ render(
138
+ <RuntimeProvider messages={textMessage}>
139
+ <ThreadPrimitive.Messages components={{ Message }} />
140
+ </RuntimeProvider>,
141
+ );
142
+
143
+ const text = await screen.findByTestId("text");
144
+ expect(text.textContent).toBe("hello");
145
+ });
146
+ });
@@ -215,10 +215,3 @@ describe("ThreadPrimitive.Unstable_MessageById", () => {
215
215
  expect(screen.getByTestId("by-id").textContent).toBe("first");
216
216
  });
217
217
  });
218
-
219
- describe("exports", () => {
220
- it("exposes the unstable id-keyed surface", () => {
221
- expect(typeof unstable_useThreadMessageIds).toBe("function");
222
- expect(ThreadPrimitive.Unstable_MessageById).toBeDefined();
223
- });
224
- });
@@ -58,10 +58,13 @@ class TextStreamAnimator {
58
58
  public maxCharsPerFrame: number = Infinity;
59
59
  public minCommitMs: number = 0;
60
60
 
61
- constructor(
62
- public currentText: string,
63
- private setText: (newText: string) => void,
64
- ) {}
61
+ public currentText: string;
62
+ private setText: (newText: string) => void;
63
+
64
+ constructor(currentText: string, setText: (newText: string) => void) {
65
+ this.currentText = currentText;
66
+ this.setText = setText;
67
+ }
65
68
 
66
69
  start() {
67
70
  if (this.animationFrameId !== null) return;