@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
@@ -4,13 +4,23 @@ import type {
4
4
  TextMessagePart,
5
5
  ReasoningMessagePart,
6
6
  MessagePartState,
7
+ MessagePartStatus,
7
8
  } from "@assistant-ui/core";
8
9
  import { useAuiState } from "@assistant-ui/store";
9
10
 
11
+ const COMPLETE_STATUS: MessagePartStatus = Object.freeze({ type: "complete" });
12
+
13
+ const EMPTY_TEXT_PART: MessagePartState & TextMessagePart = Object.freeze({
14
+ type: "text",
15
+ text: "",
16
+ status: COMPLETE_STATUS,
17
+ });
18
+
10
19
  /**
11
20
  * @deprecated Use {@link useAuiState} to select and narrow `s.part`.
12
- * Return `null` for optional rendering, or throw inside the selector to
13
- * preserve the old hook's strict behavior.
21
+ * Return `null` for optional rendering. Do not throw inside the selector:
22
+ * selectors run inside `useSyncExternalStore`'s `getSnapshot`, so a transient
23
+ * part mismatch during thread switches can unmount the React root.
14
24
  *
15
25
  * @example
16
26
  * ```tsx
@@ -23,11 +33,11 @@ import { useAuiState } from "@assistant-ui/store";
23
33
  * See the {@link https://assistant-ui.com/docs/migrations/v0-12 migration guide}.
24
34
  */
25
35
  export const useMessagePartText = () => {
36
+ // Runs inside useSyncExternalStore's getSnapshot, where a throw tears down
37
+ // the React root; the module-level frozen sentinel keeps snapshots stable.
26
38
  const text = useAuiState((s) => {
27
39
  if (s.part.type !== "text" && s.part.type !== "reasoning")
28
- throw new Error(
29
- "MessagePartText can only be used inside text or reasoning message parts.",
30
- );
40
+ return EMPTY_TEXT_PART;
31
41
 
32
42
  return s.part as MessagePartState &
33
43
  (TextMessagePart | ReasoningMessagePart);
@@ -23,6 +23,7 @@ import { useTopAnchorReserve } from "./topAnchor/useTopAnchorReserve";
23
23
  import {
24
24
  getActiveTopAnchorAnchorId,
25
25
  getActiveTopAnchorTargetId,
26
+ isTopAnchorTurnValid,
26
27
  } from "./topAnchor/topAnchorTurn";
27
28
 
28
29
  export namespace ThreadPrimitiveViewport {
@@ -110,11 +111,25 @@ const useTopAnchorTurn = (enabled: boolean) => {
110
111
  if (!enabled) return undefined;
111
112
  return getActiveTopAnchorTargetId(s.thread);
112
113
  });
114
+ const topAnchorTurn = useThreadViewport((s) => s.topAnchorTurn);
113
115
  const activeTurn = useMemo(() => {
114
116
  if (!activeAnchorId || !activeTargetId) return null;
115
117
  return { anchorId: activeAnchorId, targetId: activeTargetId };
116
118
  }, [activeAnchorId, activeTargetId]);
117
119
 
120
+ const topAnchorTurnIsValid = useAuiState(
121
+ (s) =>
122
+ enabled &&
123
+ !!topAnchorTurn &&
124
+ isTopAnchorTurnValid(topAnchorTurn, s.thread.messages),
125
+ );
126
+
127
+ useLayoutEffect(() => {
128
+ if (!topAnchorTurn || topAnchorTurnIsValid) return;
129
+
130
+ threadViewportStore.getState().setTopAnchorTurn(null);
131
+ }, [threadViewportStore, topAnchorTurn, topAnchorTurnIsValid]);
132
+
118
133
  useLayoutEffect(() => {
119
134
  if (!activeTurn) return;
120
135
 
@@ -43,6 +43,7 @@ const makeStore = (state: ReturnType<TopAnchorStore["getState"]>) => {
43
43
  };
44
44
 
45
45
  const numericClamp = { tallerThan: 160, visibleHeight: 96 };
46
+ const activeTopAnchorTurn = { anchorId: "user-1", targetId: "assistant-1" };
46
47
 
47
48
  describe("mountTopAnchorReserve", () => {
48
49
  beforeEach(() => {
@@ -76,6 +77,7 @@ describe("mountTopAnchorReserve", () => {
76
77
  turnAnchor: "top",
77
78
  element: { viewport, anchor, target },
78
79
  targetConfig: numericClamp,
80
+ topAnchorTurn: activeTopAnchorTurn,
79
81
  });
80
82
 
81
83
  mountTopAnchorReserve(store);
@@ -90,6 +92,245 @@ describe("mountTopAnchorReserve", () => {
90
92
  expect(reserve.style.height).toBe("60px");
91
93
  });
92
94
 
95
+ it("preserves the reserve across a transient between-turns anchor gap", () => {
96
+ const viewport = document.createElement("div");
97
+ const anchor = document.createElement("div");
98
+ const target = document.createElement("div");
99
+ const reserveHost = document.createElement("div");
100
+ reserveHost.append(target);
101
+ document.body.append(reserveHost);
102
+
103
+ defineReadonlyNumber(viewport, "offsetTop", 0);
104
+ defineReadonlyNumber(viewport, "clientHeight", 400);
105
+ defineReadonlyNumber(viewport, "scrollHeight", 560);
106
+ defineReadonlyNumber(anchor, "offsetTop", 220);
107
+ defineReadonlyNumber(anchor, "offsetHeight", 64);
108
+ viewport.scrollTo = vi.fn();
109
+
110
+ const { store, setState } = makeStore({
111
+ turnAnchor: "top",
112
+ element: { viewport, anchor, target },
113
+ targetConfig: numericClamp,
114
+ topAnchorTurn: activeTopAnchorTurn,
115
+ });
116
+
117
+ const unmount = mountTopAnchorReserve(store);
118
+ vi.runOnlyPendingTimers();
119
+
120
+ const reserve = reserveHost.querySelector(
121
+ "[data-aui-top-anchor-reserve]",
122
+ ) as HTMLElement;
123
+ const optimisticUserMessage = document.createElement("div");
124
+ reserveHost.append(optimisticUserMessage);
125
+ const appendSpy = vi.spyOn(reserveHost, "append");
126
+
127
+ setState({
128
+ turnAnchor: "top",
129
+ element: { viewport, anchor: null, target: null },
130
+ targetConfig: null,
131
+ topAnchorTurn: activeTopAnchorTurn,
132
+ });
133
+ vi.runOnlyPendingTimers();
134
+
135
+ expect(reserve.isConnected).toBe(true);
136
+ expect(reserve.style.height).toBe("60px");
137
+ expect(reserve.previousElementSibling).toBe(optimisticUserMessage);
138
+ expect(appendSpy).toHaveBeenCalledTimes(1);
139
+
140
+ setState({
141
+ turnAnchor: "top",
142
+ element: { viewport, anchor: null, target: null },
143
+ targetConfig: null,
144
+ topAnchorTurn: activeTopAnchorTurn,
145
+ });
146
+ vi.runOnlyPendingTimers();
147
+
148
+ expect(appendSpy).toHaveBeenCalledTimes(1);
149
+
150
+ const nextAnchor = document.createElement("div");
151
+ const nextTarget = document.createElement("div");
152
+ reserveHost.append(nextTarget);
153
+ defineReadonlyNumber(nextAnchor, "offsetTop", 236);
154
+ defineReadonlyNumber(nextAnchor, "offsetHeight", 64);
155
+
156
+ setState({
157
+ turnAnchor: "top",
158
+ element: { viewport, anchor: nextAnchor, target: nextTarget },
159
+ targetConfig: numericClamp,
160
+ topAnchorTurn: activeTopAnchorTurn,
161
+ });
162
+ vi.runOnlyPendingTimers();
163
+
164
+ expect(reserve.previousElementSibling).toBe(nextTarget);
165
+
166
+ unmount();
167
+ expect(reserve.isConnected).toBe(false);
168
+ });
169
+
170
+ it("removes the reserve when the active turn is no longer valid", () => {
171
+ const viewport = document.createElement("div");
172
+ const anchor = document.createElement("div");
173
+ const target = document.createElement("div");
174
+ const reserveHost = document.createElement("div");
175
+ reserveHost.append(target);
176
+ document.body.append(reserveHost);
177
+
178
+ defineReadonlyNumber(viewport, "offsetTop", 0);
179
+ defineReadonlyNumber(viewport, "clientHeight", 400);
180
+ defineReadonlyNumber(viewport, "scrollHeight", 560);
181
+ defineReadonlyNumber(anchor, "offsetTop", 220);
182
+ defineReadonlyNumber(anchor, "offsetHeight", 64);
183
+ viewport.scrollTo = vi.fn();
184
+
185
+ const { store, setState } = makeStore({
186
+ turnAnchor: "top",
187
+ element: { viewport, anchor, target },
188
+ targetConfig: numericClamp,
189
+ topAnchorTurn: activeTopAnchorTurn,
190
+ });
191
+
192
+ mountTopAnchorReserve(store);
193
+ vi.runOnlyPendingTimers();
194
+
195
+ const reserve = reserveHost.querySelector(
196
+ "[data-aui-top-anchor-reserve]",
197
+ ) as HTMLElement;
198
+
199
+ setState({
200
+ turnAnchor: "top",
201
+ element: { viewport, anchor: null, target: null },
202
+ targetConfig: null,
203
+ topAnchorTurn: null,
204
+ });
205
+ vi.runOnlyPendingTimers();
206
+
207
+ expect(reserve.isConnected).toBe(false);
208
+ expect(reserve.style.height).toBe("0px");
209
+ });
210
+
211
+ it("removes the reserve when only the registered anchor unmounts", () => {
212
+ const viewport = document.createElement("div");
213
+ const anchor = document.createElement("div");
214
+ const target = document.createElement("div");
215
+ const reserveHost = document.createElement("div");
216
+ reserveHost.append(target);
217
+ document.body.append(reserveHost);
218
+
219
+ defineReadonlyNumber(viewport, "offsetTop", 0);
220
+ defineReadonlyNumber(viewport, "clientHeight", 400);
221
+ defineReadonlyNumber(viewport, "scrollHeight", 560);
222
+ defineReadonlyNumber(anchor, "offsetTop", 220);
223
+ defineReadonlyNumber(anchor, "offsetHeight", 64);
224
+ viewport.scrollTo = vi.fn();
225
+
226
+ const { store, setState } = makeStore({
227
+ turnAnchor: "top",
228
+ element: { viewport, anchor, target },
229
+ targetConfig: numericClamp,
230
+ topAnchorTurn: activeTopAnchorTurn,
231
+ });
232
+
233
+ mountTopAnchorReserve(store);
234
+ vi.runOnlyPendingTimers();
235
+
236
+ const reserve = reserveHost.querySelector(
237
+ "[data-aui-top-anchor-reserve]",
238
+ ) as HTMLElement;
239
+
240
+ setState({
241
+ turnAnchor: "top",
242
+ element: { viewport, anchor: null, target },
243
+ targetConfig: numericClamp,
244
+ topAnchorTurn: activeTopAnchorTurn,
245
+ });
246
+ vi.runOnlyPendingTimers();
247
+
248
+ expect(reserve.isConnected).toBe(false);
249
+ expect(reserve.style.height).toBe("0px");
250
+ });
251
+
252
+ it("removes the reserve when the viewport is unavailable", () => {
253
+ const viewport = document.createElement("div");
254
+ const anchor = document.createElement("div");
255
+ const target = document.createElement("div");
256
+ const reserveHost = document.createElement("div");
257
+ reserveHost.append(target);
258
+ document.body.append(reserveHost);
259
+
260
+ defineReadonlyNumber(viewport, "offsetTop", 0);
261
+ defineReadonlyNumber(viewport, "clientHeight", 400);
262
+ defineReadonlyNumber(viewport, "scrollHeight", 560);
263
+ defineReadonlyNumber(anchor, "offsetTop", 220);
264
+ defineReadonlyNumber(anchor, "offsetHeight", 64);
265
+ viewport.scrollTo = vi.fn();
266
+
267
+ const { store, setState } = makeStore({
268
+ turnAnchor: "top",
269
+ element: { viewport, anchor, target },
270
+ targetConfig: numericClamp,
271
+ topAnchorTurn: activeTopAnchorTurn,
272
+ });
273
+
274
+ mountTopAnchorReserve(store);
275
+ vi.runOnlyPendingTimers();
276
+
277
+ const reserve = reserveHost.querySelector(
278
+ "[data-aui-top-anchor-reserve]",
279
+ ) as HTMLElement;
280
+
281
+ setState({
282
+ turnAnchor: "top",
283
+ element: { viewport: null, anchor: null, target: null },
284
+ targetConfig: null,
285
+ topAnchorTurn: null,
286
+ });
287
+ vi.runOnlyPendingTimers();
288
+
289
+ expect(reserve.isConnected).toBe(false);
290
+ expect(reserve.style.height).toBe("0px");
291
+ });
292
+
293
+ it("removes the reserve when top anchoring is disabled", () => {
294
+ const viewport = document.createElement("div");
295
+ const anchor = document.createElement("div");
296
+ const target = document.createElement("div");
297
+ const reserveHost = document.createElement("div");
298
+ reserveHost.append(target);
299
+ document.body.append(reserveHost);
300
+
301
+ defineReadonlyNumber(viewport, "offsetTop", 0);
302
+ defineReadonlyNumber(viewport, "clientHeight", 400);
303
+ defineReadonlyNumber(viewport, "scrollHeight", 560);
304
+ defineReadonlyNumber(anchor, "offsetTop", 220);
305
+ defineReadonlyNumber(anchor, "offsetHeight", 64);
306
+ viewport.scrollTo = vi.fn();
307
+
308
+ const { store, setState } = makeStore({
309
+ turnAnchor: "top",
310
+ element: { viewport, anchor, target },
311
+ targetConfig: numericClamp,
312
+ topAnchorTurn: activeTopAnchorTurn,
313
+ });
314
+
315
+ mountTopAnchorReserve(store);
316
+ vi.runOnlyPendingTimers();
317
+
318
+ const reserve = reserveHost.querySelector(
319
+ "[data-aui-top-anchor-reserve]",
320
+ ) as HTMLElement;
321
+
322
+ setState({
323
+ turnAnchor: "bottom",
324
+ element: { viewport, anchor: null, target: null },
325
+ targetConfig: null,
326
+ topAnchorTurn: null,
327
+ });
328
+ vi.runOnlyPendingTimers();
329
+
330
+ expect(reserve.isConnected).toBe(false);
331
+ expect(reserve.style.height).toBe("0px");
332
+ });
333
+
93
334
  it("does not repeat the smooth top-anchor scroll for the same message", () => {
94
335
  const viewport = document.createElement("div");
95
336
  const anchor = document.createElement("div");
@@ -108,6 +349,7 @@ describe("mountTopAnchorReserve", () => {
108
349
  turnAnchor: "top",
109
350
  element: { viewport, anchor, target },
110
351
  targetConfig: numericClamp,
352
+ topAnchorTurn: activeTopAnchorTurn,
111
353
  });
112
354
 
113
355
  mountTopAnchorReserve(store);
@@ -123,6 +365,7 @@ describe("mountTopAnchorReserve", () => {
123
365
  turnAnchor: "top",
124
366
  element: { viewport, anchor, target },
125
367
  targetConfig: numericClamp,
368
+ topAnchorTurn: activeTopAnchorTurn,
126
369
  });
127
370
  vi.runOnlyPendingTimers();
128
371
 
@@ -29,6 +29,10 @@ export type TopAnchorStore = {
29
29
  tallerThan: number;
30
30
  visibleHeight: number;
31
31
  } | null;
32
+ topAnchorTurn: {
33
+ readonly anchorId: string;
34
+ readonly targetId: string;
35
+ } | null;
32
36
  };
33
37
  subscribe(fn: () => void): () => void;
34
38
  };
@@ -62,13 +66,30 @@ export const mountTopAnchorReserve = (store: TopAnchorStore) => {
62
66
  const { viewport, anchor, target } = state.element;
63
67
  const clamp = state.targetConfig;
64
68
 
65
- if (
66
- state.turnAnchor !== "top" ||
67
- !viewport ||
68
- !anchor ||
69
- !target ||
70
- !clamp
71
- ) {
69
+ if (state.turnAnchor !== "top" || !viewport) {
70
+ observers.disconnect();
71
+ if (reserve) {
72
+ setReserveHeight(reserve, 0);
73
+ reserve.remove();
74
+ }
75
+ return;
76
+ }
77
+
78
+ if (!anchor && !target && !clamp && state.topAnchorTurn) {
79
+ // ThreadViewport clears this state once the stored pair stops being the
80
+ // trailing turn (followed at most by pending user messages), so reaching
81
+ // here means the anchor gap is transient and the next run is imminent.
82
+ observers.disconnect();
83
+ if (
84
+ reserve?.parentElement &&
85
+ reserve.parentElement.lastElementChild !== reserve
86
+ ) {
87
+ reserve.parentElement.append(reserve);
88
+ }
89
+ return;
90
+ }
91
+
92
+ if (!anchor || !target || !clamp) {
72
93
  observers.disconnect();
73
94
  if (reserve) {
74
95
  setReserveHeight(reserve, 0);
@@ -3,6 +3,7 @@ import {
3
3
  getActiveTopAnchorAnchorId,
4
4
  getActiveTopAnchorTargetId,
5
5
  getActiveTopAnchorTurn,
6
+ isTopAnchorTurnValid,
6
7
  } from "./topAnchorTurn";
7
8
 
8
9
  describe("topAnchorTurn", () => {
@@ -43,4 +44,72 @@ describe("topAnchorTurn", () => {
43
44
 
44
45
  expect(getActiveTopAnchorTurn({ isRunning: true, messages })).toBe(null);
45
46
  });
47
+
48
+ it("keeps a completed turn valid while it remains the trailing turn", () => {
49
+ const turn = { anchorId: "user-2", targetId: "assistant-2" };
50
+
51
+ expect(
52
+ isTopAnchorTurnValid(turn, [
53
+ { id: "assistant-1", role: "assistant" },
54
+ { id: "user-2", role: "user" },
55
+ { id: "assistant-2", role: "assistant" },
56
+ ]),
57
+ ).toBe(true);
58
+ });
59
+
60
+ it("keeps a turn valid across the between-turns gap with pending user messages", () => {
61
+ const turn = { anchorId: "user-2", targetId: "assistant-2" };
62
+
63
+ expect(
64
+ isTopAnchorTurnValid(turn, [
65
+ { id: "user-2", role: "user" },
66
+ { id: "assistant-2", role: "assistant" },
67
+ { id: "user-3", role: "user" },
68
+ { id: "user-4", role: "user" },
69
+ ]),
70
+ ).toBe(true);
71
+ });
72
+
73
+ it.each([
74
+ [
75
+ "thread switch",
76
+ [
77
+ { id: "other-user", role: "user" },
78
+ { id: "other-assistant", role: "assistant" },
79
+ ],
80
+ ],
81
+ ["new chat", []],
82
+ ["last-turn deletion", [{ id: "older-assistant", role: "assistant" }]],
83
+ [
84
+ "branch swap",
85
+ [
86
+ { id: "branch-user", role: "user" },
87
+ { id: "branch-assistant", role: "assistant" },
88
+ ],
89
+ ],
90
+ [
91
+ "an externally synced completed turn",
92
+ [
93
+ { id: "user-2", role: "user" },
94
+ { id: "assistant-2", role: "assistant" },
95
+ { id: "user-3", role: "user" },
96
+ { id: "assistant-3", role: "assistant" },
97
+ ],
98
+ ],
99
+ [
100
+ "a resync separating the pair",
101
+ [
102
+ { id: "user-2", role: "user" },
103
+ { id: "inserted-assistant", role: "assistant" },
104
+ { id: "assistant-2", role: "assistant" },
105
+ ],
106
+ ],
107
+ ])("invalidates a stored turn after %s", (_transition, messages) => {
108
+ expect(
109
+ isTopAnchorTurnValid(
110
+ { anchorId: "user-2", targetId: "assistant-2" },
111
+ messages,
112
+ ),
113
+ ).toBe(false);
114
+ });
46
115
  });
@@ -5,6 +5,35 @@ type TopAnchorTurnMessage = {
5
5
  readonly role: string;
6
6
  };
7
7
 
8
+ export type TopAnchorTurn = {
9
+ readonly anchorId: string;
10
+ readonly targetId: string;
11
+ };
12
+
13
+ /**
14
+ * A stored turn stays valid only while it is still the trailing turn: the
15
+ * anchor immediately precedes the target, and anything after the target is a
16
+ * pending user message awaiting the next run. An assistant message after the
17
+ * target means the turn was superseded (e.g. an external store synced in a
18
+ * turn completed elsewhere), so the reserve must be torn down, not preserved.
19
+ */
20
+ export const isTopAnchorTurnValid = (
21
+ turn: TopAnchorTurn | null,
22
+ messages: readonly TopAnchorTurnMessage[],
23
+ ) => {
24
+ if (!turn) return false;
25
+
26
+ const targetIndex = messages.findIndex(
27
+ (message) => message.id === turn.targetId,
28
+ );
29
+ if (targetIndex < 1) return false;
30
+
31
+ return (
32
+ messages[targetIndex - 1]?.id === turn.anchorId &&
33
+ messages.slice(targetIndex + 1).every((message) => message.role === "user")
34
+ );
35
+ };
36
+
8
37
  export const getActiveTopAnchorTurn = ({
9
38
  isRunning,
10
39
  messages,
@@ -11,6 +11,7 @@ import {
11
11
  vi,
12
12
  } from "vitest";
13
13
  import { useEffect, useState, type FC, type PropsWithChildren } from "react";
14
+ import { useAuiState } from "@assistant-ui/store";
14
15
  import { AssistantRuntimeProvider } from "../../context";
15
16
  import * as MessagePrimitive from "../message";
16
17
  import { ThreadPrimitiveMessages } from "./ThreadMessages";
@@ -39,6 +40,7 @@ const getMaxScrollTop = (element: Element) =>
39
40
  Math.max(0, element.scrollHeight - element.clientHeight);
40
41
 
41
42
  let forceShortViewportMeasurement = false;
43
+ let viewportMeasurementOffset = 0;
42
44
  const resizeObserverCallbacks = new Set<ResizeObserverCallback>();
43
45
 
44
46
  class TestResizeObserver {
@@ -111,7 +113,9 @@ beforeAll(() => {
111
113
  if (this.getAttribute("data-testid") !== "viewport") return 0;
112
114
  if (forceShortViewportMeasurement) return this.clientHeight;
113
115
  return (
114
- document.querySelectorAll('[data-testid="thread-message"]').length * 80
116
+ document.querySelectorAll('[data-testid="thread-message"]').length *
117
+ 80 +
118
+ viewportMeasurementOffset
115
119
  );
116
120
  },
117
121
  });
@@ -127,6 +131,7 @@ beforeAll(() => {
127
131
 
128
132
  afterEach(() => {
129
133
  forceShortViewportMeasurement = false;
134
+ viewportMeasurementOffset = 0;
130
135
  resizeObserverCallbacks.clear();
131
136
  cleanup();
132
137
  });
@@ -174,6 +179,11 @@ const BottomAnchorThread = () => (
174
179
  </ThreadPrimitiveRoot>
175
180
  );
176
181
 
182
+ const RunState = () => {
183
+ const isRunning = useAuiState((s) => s.thread.isRunning);
184
+ return <output data-testid="run-state">{String(isRunning)}</output>;
185
+ };
186
+
177
187
  const SyncRuntimeProvider: FC<PropsWithChildren> = ({ children }) => {
178
188
  const runtime = useLocalRuntime(adapter, { initialMessages: messages });
179
189
 
@@ -302,6 +312,81 @@ describe("useThreadViewportAutoScroll", () => {
302
312
  scrollToSpy.mockRestore();
303
313
  });
304
314
 
315
+ it("defers auto-scroll to an active top anchor only while the run is active", async () => {
316
+ let releaseRun!: () => void;
317
+ const runGate = new Promise<void>((resolve) => {
318
+ releaseRun = resolve;
319
+ });
320
+ const deferredAdapter: ChatModelAdapter = {
321
+ async *run() {
322
+ await runGate;
323
+ yield { content: [{ type: "text", text: "done" }] };
324
+ },
325
+ };
326
+ const scrollToSpy = vi.spyOn(HTMLElement.prototype, "scrollTo");
327
+
328
+ let runtime: ReturnType<typeof useLocalRuntime> | null = null;
329
+ const Harness: FC = () => {
330
+ runtime = useLocalRuntime(deferredAdapter, { initialMessages: messages });
331
+ return (
332
+ <AssistantRuntimeProvider runtime={runtime}>
333
+ <Thread autoScroll scrollToBottomOnInitialize={false} />
334
+ <RunState />
335
+ </AssistantRuntimeProvider>
336
+ );
337
+ };
338
+
339
+ render(<Harness />);
340
+
341
+ act(() => {
342
+ void runtime!.thread.append({
343
+ role: "user",
344
+ content: [{ type: "text", text: "hello" }],
345
+ });
346
+ });
347
+
348
+ await waitFor(() => {
349
+ expect(
350
+ document.querySelector("[data-aui-top-anchor-user]"),
351
+ ).not.toBeNull();
352
+ });
353
+
354
+ scrollToSpy.mockClear();
355
+ viewportMeasurementOffset = 1;
356
+ act(notifyResizeObservers);
357
+
358
+ expect(
359
+ scrollToSpy.mock.calls.map(
360
+ (call) => (call[0] as ScrollToOptions).behavior,
361
+ ),
362
+ ).not.toContain("instant");
363
+
364
+ await act(async () => {
365
+ releaseRun();
366
+ });
367
+ await waitFor(() => {
368
+ expect(screen.getByTestId("run-state").textContent).toBe("false");
369
+ });
370
+ expect(document.querySelector("[data-aui-top-anchor-user]")).not.toBeNull();
371
+
372
+ const viewport = getViewport();
373
+ act(() => {
374
+ viewport.scrollTop = getMaxScrollTop(viewport);
375
+ viewport.dispatchEvent(new Event("scroll"));
376
+ });
377
+ scrollToSpy.mockClear();
378
+ viewportMeasurementOffset = 2;
379
+ act(notifyResizeObservers);
380
+
381
+ expect(
382
+ scrollToSpy.mock.calls.map(
383
+ (call) => (call[0] as ScrollToOptions).behavior,
384
+ ),
385
+ ).toContain("instant");
386
+
387
+ scrollToSpy.mockRestore();
388
+ });
389
+
305
390
  it("does not scroll initial messages when initialize scrolling is disabled", async () => {
306
391
  render(
307
392
  <SyncRuntimeProvider>
@@ -50,6 +50,7 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
50
50
  }: useThreadViewportAutoScroll.Options): RefCallback<TElement> => {
51
51
  const divRef = useRef<TElement>(null);
52
52
  const hasMessages = useAuiState((s) => s.thread.messages.length > 0);
53
+ const isRunning = useAuiState((s) => s.thread.isRunning);
53
54
  const initializeScrollRequestedRef = useRef(false);
54
55
  const scheduledFrameRef = useRef<number | null>(null);
55
56
 
@@ -172,7 +173,11 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
172
173
  scrollingToBottomBehaviorRef.current = null;
173
174
  } else if (scrollBehavior) {
174
175
  scrollToBottom(scrollBehavior);
175
- } else if (autoScroll && threadViewportStore.getState().isAtBottom) {
176
+ } else if (
177
+ autoScroll &&
178
+ !(isRunning && hasActiveTopAnchor()) &&
179
+ threadViewportStore.getState().isAtBottom
180
+ ) {
176
181
  scrollToBottom("instant");
177
182
  }
178
183