@assistant-ui/react 0.14.16 → 0.14.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/dist/client/ExternalThread.d.ts +5 -3
  2. package/dist/client/ExternalThread.d.ts.map +1 -1
  3. package/dist/client/ExternalThread.js +745 -255
  4. package/dist/client/ExternalThread.js.map +1 -1
  5. package/dist/client/InMemoryThreadList.d.ts +1 -1
  6. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  7. package/dist/client/InMemoryThreadList.js +299 -113
  8. package/dist/client/InMemoryThreadList.js.map +1 -1
  9. package/dist/client/SingleThreadList.d.ts +1 -6
  10. package/dist/client/SingleThreadList.d.ts.map +1 -1
  11. package/dist/client/SingleThreadList.js +143 -55
  12. package/dist/client/SingleThreadList.js.map +1 -1
  13. package/dist/context/ReadonlyStore.js.map +1 -1
  14. package/dist/context/providers/MessageProvider.js +38 -5
  15. package/dist/context/providers/MessageProvider.js.map +1 -1
  16. package/dist/context/providers/ThreadViewportProvider.js +76 -20
  17. package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
  18. package/dist/context/react/ThreadViewportContext.js.map +1 -1
  19. package/dist/context/react/utils/createContextHook.js.map +1 -1
  20. package/dist/context/react/utils/createContextStoreHook.js +17 -2
  21. package/dist/context/react/utils/createContextStoreHook.js.map +1 -1
  22. package/dist/context/react/utils/createStateHookForRuntime.js.map +1 -1
  23. package/dist/context/react/utils/ensureBinding.js.map +1 -1
  24. package/dist/context/react/utils/useRuntimeState.js +18 -2
  25. package/dist/context/react/utils/useRuntimeState.js.map +1 -1
  26. package/dist/context/stores/ThreadViewport.js.map +1 -1
  27. package/dist/devtools/DevToolsHooks.js.map +1 -1
  28. package/dist/hooks/useMessageQuote.js.map +1 -1
  29. package/dist/hooks/useMessageTiming.js +4 -1
  30. package/dist/hooks/useMessageTiming.js.map +1 -1
  31. package/dist/hooks/useToolCallElapsed.d.ts +23 -0
  32. package/dist/hooks/useToolCallElapsed.d.ts.map +1 -0
  33. package/dist/hooks/useToolCallElapsed.js +72 -0
  34. package/dist/hooks/useToolCallElapsed.js.map +1 -0
  35. package/dist/index.d.ts +6 -2
  36. package/dist/index.js +5 -1
  37. package/dist/internal.js.map +1 -1
  38. package/dist/legacy-runtime/AssistantRuntimeProvider.js +46 -10
  39. package/dist/legacy-runtime/AssistantRuntimeProvider.js.map +1 -1
  40. package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
  41. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js +27 -6
  42. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js.map +1 -1
  43. package/dist/legacy-runtime/hooks/AssistantContext.js +13 -2
  44. package/dist/legacy-runtime/hooks/AssistantContext.js.map +1 -1
  45. package/dist/legacy-runtime/hooks/AttachmentContext.js +9 -1
  46. package/dist/legacy-runtime/hooks/AttachmentContext.js.map +1 -1
  47. package/dist/legacy-runtime/hooks/ComposerContext.js +9 -1
  48. package/dist/legacy-runtime/hooks/ComposerContext.js.map +1 -1
  49. package/dist/legacy-runtime/hooks/MessageContext.js +12 -2
  50. package/dist/legacy-runtime/hooks/MessageContext.js.map +1 -1
  51. package/dist/legacy-runtime/hooks/MessagePartContext.js +9 -1
  52. package/dist/legacy-runtime/hooks/MessagePartContext.js.map +1 -1
  53. package/dist/legacy-runtime/hooks/ThreadContext.js +33 -5
  54. package/dist/legacy-runtime/hooks/ThreadContext.js.map +1 -1
  55. package/dist/legacy-runtime/hooks/ThreadListItemContext.js +9 -1
  56. package/dist/legacy-runtime/hooks/ThreadListItemContext.js.map +1 -1
  57. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +3 -3
  58. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +1 -1
  59. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +71 -31
  60. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +1 -1
  61. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
  62. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +24 -16
  63. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  64. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +17 -12
  65. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +1 -1
  66. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +17 -3
  67. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js.map +1 -1
  68. package/dist/mcp-apps/McpAppRenderer.d.ts +2 -10
  69. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  70. package/dist/mcp-apps/McpAppRenderer.js +9 -8
  71. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  72. package/dist/mcp-apps/McpAppsRemoteHost.d.ts +1 -8
  73. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  74. package/dist/mcp-apps/McpAppsRemoteHost.js +6 -5
  75. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  76. package/dist/mcp-apps/app-frame.js +33 -14
  77. package/dist/mcp-apps/app-frame.js.map +1 -1
  78. package/dist/mcp-apps/bridge.js.map +1 -1
  79. package/dist/mcp-apps/types.js.map +1 -1
  80. package/dist/mcp-apps/utils.js.map +1 -1
  81. package/dist/model-context/frame/useAssistantFrameHost.js +32 -14
  82. package/dist/model-context/frame/useAssistantFrameHost.js.map +1 -1
  83. package/dist/model-context/makeAssistantVisible.js +64 -26
  84. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  85. package/dist/primitives/actionBar/ActionBarCopy.js +94 -20
  86. package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
  87. package/dist/primitives/actionBar/ActionBarEdit.js.map +1 -1
  88. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +105 -37
  89. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  90. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +60 -11
  91. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
  92. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +60 -11
  93. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
  94. package/dist/primitives/actionBar/ActionBarInteractionContext.js +3 -1
  95. package/dist/primitives/actionBar/ActionBarInteractionContext.js.map +1 -1
  96. package/dist/primitives/actionBar/ActionBarReload.js.map +1 -1
  97. package/dist/primitives/actionBar/ActionBarRoot.js +84 -25
  98. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  99. package/dist/primitives/actionBar/ActionBarSpeak.js.map +1 -1
  100. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +45 -14
  101. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  102. package/dist/primitives/actionBar/useActionBarFloatStatus.js +22 -10
  103. package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
  104. package/dist/primitives/actionBar.js.map +1 -1
  105. package/dist/primitives/actionBarMore/ActionBarMoreContent.js +44 -7
  106. package/dist/primitives/actionBarMore/ActionBarMoreContent.js.map +1 -1
  107. package/dist/primitives/actionBarMore/ActionBarMoreItem.js +28 -6
  108. package/dist/primitives/actionBarMore/ActionBarMoreItem.js.map +1 -1
  109. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +103 -36
  110. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
  111. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js +28 -6
  112. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js.map +1 -1
  113. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js +28 -6
  114. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js.map +1 -1
  115. package/dist/primitives/actionBarMore/scope.js.map +1 -1
  116. package/dist/primitives/actionBarMore.js.map +1 -1
  117. package/dist/primitives/assistantModal/AssistantModalAnchor.js +27 -6
  118. package/dist/primitives/assistantModal/AssistantModalAnchor.js.map +1 -1
  119. package/dist/primitives/assistantModal/AssistantModalContent.js +71 -10
  120. package/dist/primitives/assistantModal/AssistantModalContent.js.map +1 -1
  121. package/dist/primitives/assistantModal/AssistantModalRoot.js +93 -26
  122. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  123. package/dist/primitives/assistantModal/AssistantModalTrigger.js +27 -6
  124. package/dist/primitives/assistantModal/AssistantModalTrigger.js.map +1 -1
  125. package/dist/primitives/assistantModal/scope.js.map +1 -1
  126. package/dist/primitives/assistantModal.js.map +1 -1
  127. package/dist/primitives/attachment/AttachmentName.js +13 -1
  128. package/dist/primitives/attachment/AttachmentName.js.map +1 -1
  129. package/dist/primitives/attachment/AttachmentRemove.js +11 -4
  130. package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
  131. package/dist/primitives/attachment/AttachmentRoot.js +13 -4
  132. package/dist/primitives/attachment/AttachmentRoot.js.map +1 -1
  133. package/dist/primitives/attachment/AttachmentThumb.js +20 -9
  134. package/dist/primitives/attachment/AttachmentThumb.js.map +1 -1
  135. package/dist/primitives/attachment.js.map +1 -1
  136. package/dist/primitives/branchPicker/BranchPickerCount.js +14 -2
  137. package/dist/primitives/branchPicker/BranchPickerCount.js.map +1 -1
  138. package/dist/primitives/branchPicker/BranchPickerNext.js.map +1 -1
  139. package/dist/primitives/branchPicker/BranchPickerNumber.js +14 -2
  140. package/dist/primitives/branchPicker/BranchPickerNumber.js.map +1 -1
  141. package/dist/primitives/branchPicker/BranchPickerPrevious.js.map +1 -1
  142. package/dist/primitives/branchPicker/BranchPickerRoot.js +34 -6
  143. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  144. package/dist/primitives/branchPicker.js.map +1 -1
  145. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +16 -5
  146. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
  147. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.js +13 -4
  148. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.js.map +1 -1
  149. package/dist/primitives/chainOfThought.js.map +1 -1
  150. package/dist/primitives/composer/ComposerAddAttachment.js +37 -24
  151. package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
  152. package/dist/primitives/composer/ComposerAttachmentDropzone.js +124 -49
  153. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  154. package/dist/primitives/composer/ComposerCancel.js.map +1 -1
  155. package/dist/primitives/composer/ComposerDictate.js.map +1 -1
  156. package/dist/primitives/composer/ComposerDictationTranscript.js +32 -7
  157. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  158. package/dist/primitives/composer/ComposerInput.js +29 -29
  159. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  160. package/dist/primitives/composer/ComposerInputPluginContext.js +71 -25
  161. package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
  162. package/dist/primitives/composer/ComposerQuote.js +92 -23
  163. package/dist/primitives/composer/ComposerQuote.js.map +1 -1
  164. package/dist/primitives/composer/ComposerRoot.js +45 -11
  165. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  166. package/dist/primitives/composer/ComposerSend.js +9 -2
  167. package/dist/primitives/composer/ComposerSend.js.map +1 -1
  168. package/dist/primitives/composer/ComposerStopDictation.js +15 -5
  169. package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
  170. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  171. package/dist/primitives/composer/trigger/TriggerPopover.js +215 -75
  172. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  173. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  174. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +35 -7
  175. package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -1
  176. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +134 -28
  177. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  178. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  179. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +132 -28
  180. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  181. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -10
  182. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  183. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +126 -53
  184. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  185. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +181 -78
  186. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  187. package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -1
  188. package/dist/primitives/composer/trigger/index.js.map +1 -1
  189. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +2 -6
  190. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  191. package/dist/primitives/composer/trigger/triggerDetectionResource.js +30 -15
  192. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  193. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts +2 -17
  194. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  195. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +117 -59
  196. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  197. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts +2 -10
  198. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  199. package/dist/primitives/composer/trigger/triggerNavigationResource.js +204 -71
  200. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  201. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts +2 -10
  202. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  203. package/dist/primitives/composer/trigger/triggerSelectionResource.js +51 -14
  204. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  205. package/dist/primitives/composer.js.map +1 -1
  206. package/dist/primitives/dropdownMenuRenderPrimitives.js.map +1 -1
  207. package/dist/primitives/error/ErrorMessage.js +28 -6
  208. package/dist/primitives/error/ErrorMessage.js.map +1 -1
  209. package/dist/primitives/error/ErrorRoot.js +14 -5
  210. package/dist/primitives/error/ErrorRoot.js.map +1 -1
  211. package/dist/primitives/error.js.map +1 -1
  212. package/dist/primitives/message/MessageError.js +2 -1
  213. package/dist/primitives/message/MessageError.js.map +1 -1
  214. package/dist/primitives/message/MessageIf.js +50 -20
  215. package/dist/primitives/message/MessageIf.js.map +1 -1
  216. package/dist/primitives/message/MessageParts.js +41 -7
  217. package/dist/primitives/message/MessageParts.js.map +1 -1
  218. package/dist/primitives/message/MessagePartsGrouped.js +399 -94
  219. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  220. package/dist/primitives/message/MessageRoot.js +197 -65
  221. package/dist/primitives/message/MessageRoot.js.map +1 -1
  222. package/dist/primitives/message.js.map +1 -1
  223. package/dist/primitives/messagePart/MessagePartImage.js +15 -5
  224. package/dist/primitives/messagePart/MessagePartImage.js.map +1 -1
  225. package/dist/primitives/messagePart/MessagePartText.d.ts +5 -2
  226. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  227. package/dist/primitives/messagePart/MessagePartText.js +35 -7
  228. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  229. package/dist/primitives/messagePart/useMessagePartData.js +5 -4
  230. package/dist/primitives/messagePart/useMessagePartData.js.map +1 -1
  231. package/dist/primitives/messagePart/useMessagePartFile.js +5 -4
  232. package/dist/primitives/messagePart/useMessagePartFile.js.map +1 -1
  233. package/dist/primitives/messagePart/useMessagePartImage.js +5 -4
  234. package/dist/primitives/messagePart/useMessagePartImage.js.map +1 -1
  235. package/dist/primitives/messagePart/useMessagePartReasoning.js +5 -4
  236. package/dist/primitives/messagePart/useMessagePartReasoning.js.map +1 -1
  237. package/dist/primitives/messagePart/useMessagePartSource.js +5 -4
  238. package/dist/primitives/messagePart/useMessagePartSource.js.map +1 -1
  239. package/dist/primitives/messagePart/useMessagePartText.js +5 -4
  240. package/dist/primitives/messagePart/useMessagePartText.js.map +1 -1
  241. package/dist/primitives/messagePart.js.map +1 -1
  242. package/dist/primitives/queueItem/QueueItemRemove.js +11 -4
  243. package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
  244. package/dist/primitives/queueItem/QueueItemSteer.js +11 -4
  245. package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
  246. package/dist/primitives/queueItem/QueueItemText.js +20 -6
  247. package/dist/primitives/queueItem/QueueItemText.js.map +1 -1
  248. package/dist/primitives/queueItem.js.map +1 -1
  249. package/dist/primitives/reasoning/useScrollLock.js +61 -34
  250. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  251. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +56 -16
  252. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
  253. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +120 -59
  254. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  255. package/dist/primitives/selectionToolbar.js.map +1 -1
  256. package/dist/primitives/suggestion/SuggestionDescription.js +20 -6
  257. package/dist/primitives/suggestion/SuggestionDescription.js.map +1 -1
  258. package/dist/primitives/suggestion/SuggestionTitle.js +20 -6
  259. package/dist/primitives/suggestion/SuggestionTitle.js.map +1 -1
  260. package/dist/primitives/suggestion/SuggestionTrigger.js +39 -26
  261. package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
  262. package/dist/primitives/suggestion.js.map +1 -1
  263. package/dist/primitives/thread/ThreadEmpty.js +6 -2
  264. package/dist/primitives/thread/ThreadEmpty.js.map +1 -1
  265. package/dist/primitives/thread/ThreadIf.js +32 -10
  266. package/dist/primitives/thread/ThreadIf.js.map +1 -1
  267. package/dist/primitives/thread/ThreadRoot.js +13 -4
  268. package/dist/primitives/thread/ThreadRoot.js.map +1 -1
  269. package/dist/primitives/thread/ThreadScrollToBottom.js +24 -6
  270. package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
  271. package/dist/primitives/thread/ThreadSuggestion.js +18 -6
  272. package/dist/primitives/thread/ThreadSuggestion.js.map +1 -1
  273. package/dist/primitives/thread/ThreadViewport.js +185 -47
  274. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  275. package/dist/primitives/thread/ThreadViewportFooter.js +22 -9
  276. package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
  277. package/dist/primitives/thread/topAnchor/computeTopAnchorSlack.js.map +1 -1
  278. package/dist/primitives/thread/topAnchor/createReserveObservers.js.map +1 -1
  279. package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js.map +1 -1
  280. package/dist/primitives/thread/topAnchor/topAnchorTurn.js.map +1 -1
  281. package/dist/primitives/thread/topAnchor/topAnchorUtils.js.map +1 -1
  282. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +19 -4
  283. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
  284. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  285. package/dist/primitives/thread/useThreadViewportAutoScroll.js +21 -16
  286. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  287. package/dist/primitives/thread.js.map +1 -1
  288. package/dist/primitives/threadList/ThreadListLoadMore.js.map +1 -1
  289. package/dist/primitives/threadList/ThreadListNew.js +53 -11
  290. package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
  291. package/dist/primitives/threadList/ThreadListRoot.js +13 -4
  292. package/dist/primitives/threadList/ThreadListRoot.js.map +1 -1
  293. package/dist/primitives/threadList.js.map +1 -1
  294. package/dist/primitives/threadListItem/ThreadListItemArchive.js.map +1 -1
  295. package/dist/primitives/threadListItem/ThreadListItemDelete.js.map +1 -1
  296. package/dist/primitives/threadListItem/ThreadListItemRoot.js +26 -7
  297. package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
  298. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  299. package/dist/primitives/threadListItem/ThreadListItemUnarchive.js.map +1 -1
  300. package/dist/primitives/threadListItem.js.map +1 -1
  301. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +44 -7
  302. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
  303. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js +28 -6
  304. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js.map +1 -1
  305. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +25 -5
  306. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
  307. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js +28 -6
  308. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js.map +1 -1
  309. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +28 -6
  310. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js.map +1 -1
  311. package/dist/primitives/threadListItemMore/scope.js.map +1 -1
  312. package/dist/primitives/threadListItemMore.js.map +1 -1
  313. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  314. package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
  315. package/dist/tests/setup.js.map +1 -1
  316. package/dist/unstable/useComposerInputHistory.d.ts +30 -0
  317. package/dist/unstable/useComposerInputHistory.d.ts.map +1 -0
  318. package/dist/unstable/useComposerInputHistory.js +117 -0
  319. package/dist/unstable/useComposerInputHistory.js.map +1 -0
  320. package/dist/unstable/useMentionAdapter.js.map +1 -1
  321. package/dist/unstable/useMessageStallDetection.d.ts +29 -0
  322. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -0
  323. package/dist/unstable/useMessageStallDetection.js +69 -0
  324. package/dist/unstable/useMessageStallDetection.js.map +1 -0
  325. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  326. package/dist/utils/Primitive.js +57 -12
  327. package/dist/utils/Primitive.js.map +1 -1
  328. package/dist/utils/createActionButton.js +23 -7
  329. package/dist/utils/createActionButton.js.map +1 -1
  330. package/dist/utils/getSelectionMessageId.js.map +1 -1
  331. package/dist/utils/hooks/useManagedRef.js +16 -8
  332. package/dist/utils/hooks/useManagedRef.js.map +1 -1
  333. package/dist/utils/hooks/useMediaQuery.js +25 -10
  334. package/dist/utils/hooks/useMediaQuery.js.map +1 -1
  335. package/dist/utils/hooks/useOnResizeContent.js +29 -19
  336. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  337. package/dist/utils/hooks/useOnScrollToBottom.js +20 -4
  338. package/dist/utils/hooks/useOnScrollToBottom.js.map +1 -1
  339. package/dist/utils/hooks/useSizeHandle.js +23 -15
  340. package/dist/utils/hooks/useSizeHandle.js.map +1 -1
  341. package/dist/utils/json/is-json-equal.js.map +1 -1
  342. package/dist/utils/json/is-json.js.map +1 -1
  343. package/dist/utils/smooth/SmoothContext.js +41 -11
  344. package/dist/utils/smooth/SmoothContext.js.map +1 -1
  345. package/dist/utils/smooth/useSmooth.d.ts +40 -2
  346. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  347. package/dist/utils/smooth/useSmooth.js +52 -13
  348. package/dist/utils/smooth/useSmooth.js.map +1 -1
  349. package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
  350. package/dist/utils/useToolArgsFieldStatus.d.ts.map +1 -1
  351. package/dist/utils/useToolArgsFieldStatus.js +13 -5
  352. package/dist/utils/useToolArgsFieldStatus.js.map +1 -1
  353. package/package.json +6 -6
  354. package/src/client/ExternalThread.ts +146 -74
  355. package/src/client/InMemoryThreadList.ts +23 -21
  356. package/src/client/SingleThreadList.ts +29 -27
  357. package/src/hooks/useToolCallElapsed.ts +52 -0
  358. package/src/index.ts +19 -0
  359. package/src/mcp-apps/McpAppRenderer.tsx +5 -3
  360. package/src/mcp-apps/McpAppsRemoteHost.ts +5 -3
  361. package/src/primitives/composer/ComposerInput.test.tsx +1 -1
  362. package/src/primitives/composer/ComposerInput.tsx +3 -3
  363. package/src/primitives/composer/trigger/TriggerPopover.tsx +4 -5
  364. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +5 -3
  365. package/src/primitives/composer/trigger/triggerDetectionResource.ts +21 -21
  366. package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +5 -4
  367. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +99 -101
  368. package/src/primitives/composer/trigger/triggerNavigationResource.ts +92 -98
  369. package/src/primitives/composer/trigger/triggerSelectionResource.ts +76 -76
  370. package/src/primitives/messagePart/MessagePartText.tsx +3 -2
  371. package/src/primitives/reasoning/useScrollLock.ts +25 -2
  372. package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -0
  373. package/src/tests/external-thread-branches.test.tsx +160 -0
  374. package/src/tests/shouldContinue.test.ts +33 -0
  375. package/src/tests/toolCallTiming.test.tsx +221 -0
  376. package/src/unstable/useComposerInputHistory.test.tsx +201 -0
  377. package/src/unstable/useComposerInputHistory.ts +160 -0
  378. package/src/unstable/useMessageStallDetection.ts +91 -0
  379. package/src/utils/smooth/useSmooth.test.tsx +95 -0
  380. package/src/utils/smooth/useSmooth.ts +82 -10
@@ -38,103 +38,97 @@ export type TriggerNavigationResourceOutput = {
38
38
  * Computes categories, items, search results, and navigation state from the
39
39
  * adapter + current query. Pure derivation — no side effects on the composer.
40
40
  */
41
- export const TriggerNavigationResource = resource(
42
- function TriggerNavigationResource({
43
- adapter,
44
- query,
45
- open,
46
- }: {
47
- adapter: Unstable_TriggerAdapter | undefined;
48
- query: string;
49
- open: boolean;
50
- }): TriggerNavigationResourceOutput {
51
- const [activeCategoryId, setActiveCategoryId] = useState<string | null>(
52
- null,
53
- );
54
-
55
- useEffect(() => {
56
- if (!open) setActiveCategoryId(null);
57
- }, [open]);
58
-
59
- const categories = useMemo<readonly Unstable_TriggerCategory[]>(() => {
60
- if (!open || !adapter) return [];
61
- return adapter.categories();
62
- }, [open, adapter]);
63
-
64
- const effectiveActiveCategoryId = open ? activeCategoryId : null;
65
-
66
- const allItems = useMemo<readonly Unstable_TriggerItem[]>(() => {
67
- if (!effectiveActiveCategoryId || !adapter) return [];
68
- return adapter.categoryItems(effectiveActiveCategoryId);
69
- }, [effectiveActiveCategoryId, adapter]);
70
-
71
- const searchResults = useMemo<
72
- readonly Unstable_TriggerItem[] | null
73
- >(() => {
74
- if (!open || !adapter || effectiveActiveCategoryId) return null;
75
- // If categories exist and query is empty, show categories first (not search)
76
- if (!query && categories.length > 0) return null;
77
- if (adapter.search) return adapter.search(query);
78
-
79
- // fallback: no adapter.search
80
- const all: Unstable_TriggerItem[] = [];
81
- const lower = query.toLowerCase();
82
- for (const cat of categories) {
83
- for (const item of adapter.categoryItems(cat.id)) {
84
- if (matchesQuery(item, lower)) {
85
- all.push(item);
86
- }
41
+ const useTriggerNavigationResource = ({
42
+ adapter,
43
+ query,
44
+ open,
45
+ }: {
46
+ adapter: Unstable_TriggerAdapter | undefined;
47
+ query: string;
48
+ open: boolean;
49
+ }): TriggerNavigationResourceOutput => {
50
+ const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
51
+
52
+ useEffect(() => {
53
+ if (!open) setActiveCategoryId(null);
54
+ }, [open]);
55
+
56
+ const categories = useMemo<readonly Unstable_TriggerCategory[]>(() => {
57
+ if (!open || !adapter) return [];
58
+ return adapter.categories();
59
+ }, [open, adapter]);
60
+
61
+ const effectiveActiveCategoryId = open ? activeCategoryId : null;
62
+
63
+ const allItems = useMemo<readonly Unstable_TriggerItem[]>(() => {
64
+ if (!effectiveActiveCategoryId || !adapter) return [];
65
+ return adapter.categoryItems(effectiveActiveCategoryId);
66
+ }, [effectiveActiveCategoryId, adapter]);
67
+
68
+ const searchResults = useMemo<readonly Unstable_TriggerItem[] | null>(() => {
69
+ if (!open || !adapter || effectiveActiveCategoryId) return null;
70
+ // If categories exist and query is empty, show categories first (not search)
71
+ if (!query && categories.length > 0) return null;
72
+ if (adapter.search) return adapter.search(query);
73
+
74
+ // fallback: no adapter.search
75
+ const all: Unstable_TriggerItem[] = [];
76
+ const lower = query.toLowerCase();
77
+ for (const cat of categories) {
78
+ for (const item of adapter.categoryItems(cat.id)) {
79
+ if (matchesQuery(item, lower)) {
80
+ all.push(item);
87
81
  }
88
82
  }
89
- return all;
90
- }, [open, adapter, query, effectiveActiveCategoryId, categories]);
91
-
92
- const isSearchMode = searchResults !== null;
93
-
94
- const filteredCategories = useMemo(() => {
95
- if (isSearchMode) return [];
96
- if (!query) return categories;
97
- const lower = query.toLowerCase();
98
- return categories.filter((cat) =>
99
- cat.label.toLowerCase().includes(lower),
100
- );
101
- }, [categories, query, isSearchMode]);
102
-
103
- const filteredItems = useMemo(() => {
104
- if (isSearchMode) return searchResults ?? [];
105
- if (!query) return allItems;
106
- const lower = query.toLowerCase();
107
- return allItems.filter((item) => matchesQuery(item, lower));
108
- }, [allItems, query, isSearchMode, searchResults]);
109
-
110
- const navigableList = useMemo(() => {
111
- if (isSearchMode) return searchResults ?? [];
112
- if (effectiveActiveCategoryId) return filteredItems;
113
- return filteredCategories;
114
- }, [
115
- isSearchMode,
116
- searchResults,
117
- effectiveActiveCategoryId,
118
- filteredItems,
119
- filteredCategories,
120
- ]);
121
-
122
- const selectCategory = useEffectEvent((categoryId: string) => {
123
- setActiveCategoryId(categoryId);
124
- });
125
-
126
- const goBack = useEffectEvent(() => {
127
- setActiveCategoryId(null);
128
- });
129
-
130
- return {
131
- categories: filteredCategories,
132
- items: filteredItems,
133
- isSearchMode,
134
- activeCategoryId: effectiveActiveCategoryId,
135
- navigableList,
136
- selectCategory,
137
- goBack,
138
- };
139
- },
140
- );
83
+ }
84
+ return all;
85
+ }, [open, adapter, query, effectiveActiveCategoryId, categories]);
86
+
87
+ const isSearchMode = searchResults !== null;
88
+
89
+ const filteredCategories = useMemo(() => {
90
+ if (isSearchMode) return [];
91
+ if (!query) return categories;
92
+ const lower = query.toLowerCase();
93
+ return categories.filter((cat) => cat.label.toLowerCase().includes(lower));
94
+ }, [categories, query, isSearchMode]);
95
+
96
+ const filteredItems = useMemo(() => {
97
+ if (isSearchMode) return searchResults ?? [];
98
+ if (!query) return allItems;
99
+ const lower = query.toLowerCase();
100
+ return allItems.filter((item) => matchesQuery(item, lower));
101
+ }, [allItems, query, isSearchMode, searchResults]);
102
+
103
+ const navigableList = useMemo(() => {
104
+ if (isSearchMode) return searchResults ?? [];
105
+ if (effectiveActiveCategoryId) return filteredItems;
106
+ return filteredCategories;
107
+ }, [
108
+ isSearchMode,
109
+ searchResults,
110
+ effectiveActiveCategoryId,
111
+ filteredItems,
112
+ filteredCategories,
113
+ ]);
114
+
115
+ const selectCategory = useEffectEvent((categoryId: string) => {
116
+ setActiveCategoryId(categoryId);
117
+ });
118
+
119
+ const goBack = useEffectEvent(() => {
120
+ setActiveCategoryId(null);
121
+ });
122
+
123
+ return {
124
+ categories: filteredCategories,
125
+ items: filteredItems,
126
+ isSearchMode,
127
+ activeCategoryId: effectiveActiveCategoryId,
128
+ navigableList,
129
+ selectCategory,
130
+ goBack,
131
+ };
132
+ };
133
+
134
+ export const TriggerNavigationResource = resource(useTriggerNavigationResource);
@@ -33,91 +33,91 @@ export type TriggerSelectionResourceOutput = {
33
33
  };
34
34
 
35
35
  /** Owns composer text mutation + behavior dispatch on item selection. */
36
- export const TriggerSelectionResource = resource(
37
- function TriggerSelectionResource({
38
- behavior,
39
- trigger,
40
- aui,
41
- triggerChar,
42
- setCursorPosition,
43
- onSelected,
44
- }: {
45
- behavior: TriggerBehavior | undefined;
46
- trigger: DetectedTrigger | null;
47
- aui: AssistantClient;
48
- triggerChar: string;
49
- setCursorPosition: (pos: number) => void;
50
- /** Called after a successful selection so the parent can reset nav state. */
51
- onSelected: () => void;
52
- }): TriggerSelectionResourceOutput {
53
- // Select-item override: lets Lexical's DirectivePlugin intercept selection
54
- // and drive its own node insertion.
55
- const selectItemOverrideRef = useRef<SelectItemOverride | null>(null);
36
+ const useTriggerSelectionResource = ({
37
+ behavior,
38
+ trigger,
39
+ aui,
40
+ triggerChar,
41
+ setCursorPosition,
42
+ onSelected,
43
+ }: {
44
+ behavior: TriggerBehavior | undefined;
45
+ trigger: DetectedTrigger | null;
46
+ aui: AssistantClient;
47
+ triggerChar: string;
48
+ setCursorPosition: (pos: number) => void;
49
+ /** Called after a successful selection so the parent can reset nav state. */
50
+ onSelected: () => void;
51
+ }): TriggerSelectionResourceOutput => {
52
+ // Select-item override: lets Lexical's DirectivePlugin intercept selection
53
+ // and drive its own node insertion.
54
+ const selectItemOverrideRef = useRef<SelectItemOverride | null>(null);
56
55
 
57
- const registerSelectItemOverride = useEffectEvent(
58
- (fn: SelectItemOverride) => {
59
- selectItemOverrideRef.current = fn;
60
- return () => {
61
- if (selectItemOverrideRef.current === fn) {
62
- selectItemOverrideRef.current = null;
63
- }
64
- };
65
- },
66
- );
56
+ const registerSelectItemOverride = useEffectEvent(
57
+ (fn: SelectItemOverride) => {
58
+ selectItemOverrideRef.current = fn;
59
+ return () => {
60
+ if (selectItemOverrideRef.current === fn) {
61
+ selectItemOverrideRef.current = null;
62
+ }
63
+ };
64
+ },
65
+ );
67
66
 
68
- const selectItem = useEffectEvent((item: Unstable_TriggerItem) => {
69
- if (!trigger || !behavior) return;
67
+ const selectItem = useEffectEvent((item: Unstable_TriggerItem) => {
68
+ if (!trigger || !behavior) return;
70
69
 
71
- if (selectItemOverrideRef.current?.(item)) {
72
- onSelected();
73
- return;
74
- }
70
+ if (selectItemOverrideRef.current?.(item)) {
71
+ onSelected();
72
+ return;
73
+ }
75
74
 
76
- const currentText = aui.composer().getState().text;
77
- const before = currentText.slice(0, trigger.offset);
78
- const after = currentText.slice(
79
- trigger.offset + triggerChar.length + trigger.query.length,
80
- );
75
+ const currentText = aui.composer().getState().text;
76
+ const before = currentText.slice(0, trigger.offset);
77
+ const after = currentText.slice(
78
+ trigger.offset + triggerChar.length + trigger.query.length,
79
+ );
80
+
81
+ const insertDirective = () => {
82
+ const directive = behavior.formatter.serialize(item);
83
+ aui
84
+ .composer()
85
+ .setText(
86
+ before + directive + (after.startsWith(" ") ? after : ` ${after}`),
87
+ );
88
+ };
81
89
 
82
- const insertDirective = () => {
83
- const directive = behavior.formatter.serialize(item);
90
+ if (behavior.kind === "directive") {
91
+ insertDirective();
92
+ behavior.onInserted?.(item);
93
+ } else {
94
+ if (behavior.removeOnExecute) {
84
95
  aui
85
96
  .composer()
86
- .setText(
87
- before + directive + (after.startsWith(" ") ? after : ` ${after}`),
88
- );
89
- };
90
-
91
- if (behavior.kind === "directive") {
92
- insertDirective();
93
- behavior.onInserted?.(item);
97
+ .setText(before + (after.startsWith(" ") ? after.slice(1) : after));
94
98
  } else {
95
- if (behavior.removeOnExecute) {
96
- aui
97
- .composer()
98
- .setText(before + (after.startsWith(" ") ? after.slice(1) : after));
99
- } else {
100
- // Leave directive chip in the composer as an audit trail
101
- insertDirective();
102
- }
103
- behavior.onExecute(item);
99
+ // Leave directive chip in the composer as an audit trail
100
+ insertDirective();
104
101
  }
102
+ behavior.onExecute(item);
103
+ }
105
104
 
106
- onSelected();
107
- });
105
+ onSelected();
106
+ });
108
107
 
109
- const close = useEffectEvent(() => {
110
- onSelected();
111
- // Move cursor before the trigger so trigger detection deactivates
112
- if (trigger) {
113
- setCursorPosition(trigger.offset);
114
- }
115
- });
108
+ const close = useEffectEvent(() => {
109
+ onSelected();
110
+ // Move cursor before the trigger so trigger detection deactivates
111
+ if (trigger) {
112
+ setCursorPosition(trigger.offset);
113
+ }
114
+ });
116
115
 
117
- return {
118
- selectItem,
119
- close,
120
- registerSelectItemOverride,
121
- };
122
- },
123
- );
116
+ return {
117
+ selectItem,
118
+ close,
119
+ registerSelectItemOverride,
120
+ };
121
+ };
122
+
123
+ export const TriggerSelectionResource = resource(useTriggerSelectionResource);
@@ -8,7 +8,7 @@ import {
8
8
  type ElementType,
9
9
  } from "react";
10
10
  import { useMessagePartText } from "./useMessagePartText";
11
- import { useSmooth } from "../../utils/smooth/useSmooth";
11
+ import { useSmooth, type SmoothOptions } from "../../utils/smooth/useSmooth";
12
12
 
13
13
  export namespace MessagePartPrimitiveText {
14
14
  export type Element = ComponentRef<typeof Primitive.span>;
@@ -19,9 +19,10 @@ export namespace MessagePartPrimitiveText {
19
19
  /**
20
20
  * Whether to enable smooth text streaming animation.
21
21
  * When enabled, text appears with a typing effect as it streams in.
22
+ * Pass a `SmoothOptions` object to tune the reveal rate.
22
23
  * @default true
23
24
  */
24
- smooth?: boolean;
25
+ smooth?: boolean | SmoothOptions;
25
26
  /**
26
27
  * The HTML element or React component to render as.
27
28
  * @default "span"
@@ -64,21 +64,44 @@ export const useScrollLock = <T extends HTMLElement = HTMLElement>(
64
64
  const scrollPosition = scrollContainer.scrollTop;
65
65
  const scrollbarWidth = scrollContainer.style.scrollbarWidth;
66
66
 
67
+ // Hiding the scrollbar collapses its gutter on classic scrollbars, which
68
+ // shifts centered content horizontally; compensate with padding on the
69
+ // side the scrollbar occupies (the left side in RTL).
70
+ const computed = getComputedStyle(scrollContainer);
71
+ const paddingSide =
72
+ computed.direction === "rtl" ? "paddingLeft" : "paddingRight";
73
+ const previousPadding = scrollContainer.style[paddingSide];
74
+ const scrollbarSize =
75
+ scrollContainer.offsetWidth -
76
+ scrollContainer.clientWidth -
77
+ parseFloat(computed.borderLeftWidth) -
78
+ parseFloat(computed.borderRightWidth);
79
+
67
80
  scrollContainer.style.scrollbarWidth = "none";
81
+ if (scrollbarSize > 0) {
82
+ scrollContainer.style[paddingSide] = `${
83
+ parseFloat(computed[paddingSide]) + scrollbarSize
84
+ }px`;
85
+ }
86
+
87
+ const restoreStyles = () => {
88
+ scrollContainer.style.scrollbarWidth = scrollbarWidth;
89
+ scrollContainer.style[paddingSide] = previousPadding;
90
+ };
68
91
 
69
92
  const resetPosition = () => (scrollContainer.scrollTop = scrollPosition);
70
93
  scrollContainer.addEventListener("scroll", resetPosition);
71
94
 
72
95
  const timeoutId = setTimeout(() => {
73
96
  scrollContainer.removeEventListener("scroll", resetPosition);
74
- scrollContainer.style.scrollbarWidth = scrollbarWidth;
97
+ restoreStyles();
75
98
  cleanupRef.current = null;
76
99
  }, animationDuration);
77
100
 
78
101
  cleanupRef.current = () => {
79
102
  clearTimeout(timeoutId);
80
103
  scrollContainer.removeEventListener("scroll", resetPosition);
81
- scrollContainer.style.scrollbarWidth = scrollbarWidth;
104
+ restoreStyles();
82
105
  };
83
106
  }, [animationDuration, animatedElementRef]);
84
107
 
@@ -180,9 +180,17 @@ export const useThreadViewportAutoScroll = <TElement extends HTMLElement>({
180
180
  });
181
181
 
182
182
  const scrollRef = useManagedRef<HTMLElement>((el) => {
183
+ // A pointer gesture invalidates pending bottom-scroll intent; otherwise an
184
+ // intent kept alive by a non-overflowing thread (see handleScroll) hijacks
185
+ // the next content growth, e.g. expanding a collapsible tool call.
186
+ const cancelPendingScrollToBottom = () => {
187
+ scrollingToBottomBehaviorRef.current = null;
188
+ };
183
189
  el.addEventListener("scroll", handleScroll);
190
+ el.addEventListener("pointerdown", cancelPendingScrollToBottom);
184
191
  return () => {
185
192
  el.removeEventListener("scroll", handleScroll);
193
+ el.removeEventListener("pointerdown", cancelPendingScrollToBottom);
186
194
  };
187
195
  });
188
196
 
@@ -0,0 +1,160 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { render } from "@testing-library/react";
4
+ import type { FC } from "react";
5
+ import { describe, it, expect, vi } from "vitest";
6
+ import { useAui, AuiProvider } from "@assistant-ui/store";
7
+ import type {
8
+ ExternalThreadBranchAdapter,
9
+ ThreadMessage,
10
+ } from "@assistant-ui/core";
11
+ import {
12
+ ExternalThread,
13
+ type ExternalThreadProps,
14
+ } from "../client/ExternalThread";
15
+
16
+ const message = (id: string, role: "user" | "assistant"): ThreadMessage =>
17
+ ({
18
+ id,
19
+ role,
20
+ content: [{ type: "text", text: `text of ${id}` }],
21
+ createdAt: new Date(1718000000000),
22
+ ...(role === "assistant"
23
+ ? { status: { type: "complete", reason: "stop" } }
24
+ : { attachments: [] }),
25
+ metadata: { custom: {} },
26
+ }) as ThreadMessage;
27
+
28
+ const renderThread = (props: ExternalThreadProps) => {
29
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
30
+ const Capture: FC = () => {
31
+ captured.aui = useAui();
32
+ return null;
33
+ };
34
+ const App: FC<{ threadProps: ExternalThreadProps }> = ({ threadProps }) => {
35
+ const aui = useAui({ thread: ExternalThread(threadProps) });
36
+ return (
37
+ <AuiProvider value={aui}>
38
+ <Capture />
39
+ </AuiProvider>
40
+ );
41
+ };
42
+ const utils = render(<App threadProps={props} />);
43
+ return {
44
+ aui: () => captured.aui!,
45
+ rerender: (next: ExternalThreadProps) =>
46
+ utils.rerender(<App threadProps={next} />),
47
+ };
48
+ };
49
+
50
+ const baseProps = (
51
+ branches?: ExternalThreadBranchAdapter,
52
+ ): ExternalThreadProps => ({
53
+ messages: [message("u1", "user"), message("a2", "assistant")],
54
+ isRunning: false,
55
+ ...(branches ? { branches } : {}),
56
+ });
57
+
58
+ const adapterFor = (
59
+ ids: readonly string[],
60
+ switchToBranch = vi.fn(),
61
+ ): ExternalThreadBranchAdapter => ({
62
+ getBranches: (messageId) => (ids.includes(messageId) ? ids : []),
63
+ switchToBranch,
64
+ });
65
+
66
+ describe("ExternalThread branches", () => {
67
+ it("defaults to single-branch state without an adapter", () => {
68
+ const { aui } = renderThread(baseProps());
69
+ const state = aui().thread().getState();
70
+ expect(state.messages[1]!.branchNumber).toBe(1);
71
+ expect(state.messages[1]!.branchCount).toBe(1);
72
+ expect(state.capabilities.switchToBranch).toBe(false);
73
+ expect(() =>
74
+ aui().thread().message({ index: 1 }).switchToBranch({ position: "next" }),
75
+ ).not.toThrow();
76
+ });
77
+
78
+ it("derives branchNumber and branchCount from the adapter", () => {
79
+ const { aui } = renderThread(baseProps(adapterFor(["a1", "a2", "a3"])));
80
+ const state = aui().thread().getState();
81
+ expect(state.messages[1]!.branchNumber).toBe(2);
82
+ expect(state.messages[1]!.branchCount).toBe(3);
83
+ expect(state.capabilities.switchToBranch).toBe(true);
84
+ });
85
+
86
+ it("resolves previous and next to sibling ids and no-ops at the edges", () => {
87
+ const switchToBranch = vi.fn();
88
+ const { aui } = renderThread(
89
+ baseProps(adapterFor(["a1", "a2", "a3"], switchToBranch)),
90
+ );
91
+ const msg = () => aui().thread().message({ index: 1 });
92
+
93
+ msg().switchToBranch({ position: "previous" });
94
+ expect(switchToBranch).toHaveBeenLastCalledWith("a1");
95
+ msg().switchToBranch({ position: "next" });
96
+ expect(switchToBranch).toHaveBeenLastCalledWith("a3");
97
+
98
+ switchToBranch.mockClear();
99
+ const edge = renderThread({
100
+ messages: [message("u1", "user"), message("a1", "assistant")],
101
+ isRunning: false,
102
+ branches: adapterFor(["a1", "a2"], switchToBranch),
103
+ });
104
+ edge.aui().thread().message({ index: 1 }).switchToBranch({
105
+ position: "previous",
106
+ });
107
+ expect(switchToBranch).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it("forwards an explicit branchId unvalidated but ignores self-switches", () => {
111
+ const switchToBranch = vi.fn();
112
+ const { aui } = renderThread(
113
+ baseProps(adapterFor(["a1", "a2"], switchToBranch)),
114
+ );
115
+ const msg = () => aui().thread().message({ index: 1 });
116
+
117
+ msg().switchToBranch({ branchId: "not-in-the-list" });
118
+ expect(switchToBranch).toHaveBeenLastCalledWith("not-in-the-list");
119
+
120
+ switchToBranch.mockClear();
121
+ msg().switchToBranch({ branchId: "a2" });
122
+ expect(switchToBranch).not.toHaveBeenCalled();
123
+ });
124
+
125
+ it("falls back to single-branch state when getBranches omits the own id", () => {
126
+ const switchToBranch = vi.fn();
127
+ const { aui } = renderThread(
128
+ baseProps({
129
+ getBranches: () => ["b1", "b2"],
130
+ switchToBranch,
131
+ }),
132
+ );
133
+ const state = aui().thread().getState();
134
+ expect(state.messages[1]!.branchNumber).toBe(1);
135
+ expect(state.messages[1]!.branchCount).toBe(1);
136
+
137
+ aui().thread().message({ index: 1 }).switchToBranch({ position: "next" });
138
+ expect(switchToBranch).not.toHaveBeenCalled();
139
+ });
140
+
141
+ it("keeps message state identity across adapter recreation with equal values", () => {
142
+ const messages = [message("u1", "user"), message("a2", "assistant")];
143
+ const { aui, rerender } = renderThread({
144
+ messages,
145
+ isRunning: false,
146
+ branches: adapterFor(["a1", "a2", "a3"]),
147
+ });
148
+ const before = aui().thread().getState().messages[1];
149
+
150
+ rerender({
151
+ messages,
152
+ isRunning: false,
153
+ branches: adapterFor(["a1", "a2", "a3"]),
154
+ });
155
+ const after = aui().thread().getState().messages[1];
156
+
157
+ expect(after!.branchNumber).toBe(2);
158
+ expect(before!.parts[0]).toBe(after!.parts[0]);
159
+ });
160
+ });
@@ -86,4 +86,37 @@ describe("shouldContinue", () => {
86
86
  });
87
87
  expect(shouldContinue(msg, undefined)).toBe(true);
88
88
  });
89
+
90
+ it("returns false while a tool call has a pending approval", () => {
91
+ const msg = makeMessage({
92
+ status: { type: "requires-action", reason: "tool-calls" },
93
+ content: [{ ...toolCall("deploy"), approval: { id: "a1" } }],
94
+ });
95
+ expect(shouldContinue(msg, undefined)).toBe(false);
96
+ expect(shouldContinue(msg, ["human-approval"])).toBe(false);
97
+ });
98
+
99
+ it("returns true when a decided approval has no result", () => {
100
+ const msg = makeMessage({
101
+ status: { type: "requires-action", reason: "tool-calls" },
102
+ content: [
103
+ { ...toolCall("deploy"), approval: { id: "a1", approved: true } },
104
+ ],
105
+ });
106
+ expect(shouldContinue(msg, undefined)).toBe(true);
107
+ expect(shouldContinue(msg, ["human-approval"])).toBe(true);
108
+ });
109
+
110
+ it("exempts approval-gated tool calls from the human tool result requirement", () => {
111
+ const msg = makeMessage({
112
+ status: { type: "requires-action", reason: "tool-calls" },
113
+ content: [
114
+ {
115
+ ...toolCall("human-approval"),
116
+ approval: { id: "a1", approved: true },
117
+ },
118
+ ],
119
+ });
120
+ expect(shouldContinue(msg, ["human-approval"])).toBe(true);
121
+ });
89
122
  });