@assistant-ui/core 0.2.22 → 0.3.0

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 (349) hide show
  1. package/dist/adapters/thread-history.d.ts +8 -0
  2. package/dist/adapters/thread-history.d.ts.map +1 -1
  3. package/dist/internal.d.ts +3 -2
  4. package/dist/internal.js +3 -2
  5. package/dist/model-context/frame/host.d.ts.map +1 -1
  6. package/dist/model-context/frame/host.js +36 -21
  7. package/dist/model-context/frame/host.js.map +1 -1
  8. package/dist/model-context/types.d.ts.map +1 -1
  9. package/dist/model-context/types.js +13 -4
  10. package/dist/model-context/types.js.map +1 -1
  11. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  12. package/dist/react/adapters/LocalStorageThreadListAdapter.js +3 -2
  13. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  14. package/dist/react/client/Tools.d.ts.map +1 -1
  15. package/dist/react/client/Tools.js +40 -55
  16. package/dist/react/client/Tools.js.map +1 -1
  17. package/dist/react/client/interactable-model-context.js +1 -0
  18. package/dist/react/client/interactable-model-context.js.map +1 -1
  19. package/dist/react/interactables-legacy/useAssistantInteractable.js +1 -1
  20. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
  21. package/dist/react/interactables-legacy/useInteractableState.js +4 -3
  22. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -1
  23. package/dist/react/model-context/useAssistantContext.js +1 -1
  24. package/dist/react/model-context/useAssistantContext.js.map +1 -1
  25. package/dist/react/model-context/useAssistantDataUI.js +1 -1
  26. package/dist/react/model-context/useAssistantDataUI.js.map +1 -1
  27. package/dist/react/model-context/useAssistantInstructions.js +1 -1
  28. package/dist/react/model-context/useAssistantInstructions.js.map +1 -1
  29. package/dist/react/model-context/useAssistantTool.js +29 -25
  30. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  31. package/dist/react/model-context/useAssistantToolUI.js +1 -1
  32. package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
  33. package/dist/react/model-context/useAuiToolOverrides.js +1 -1
  34. package/dist/react/model-context/useAuiToolOverrides.js.map +1 -1
  35. package/dist/react/model-context/useInteractable.js +1 -1
  36. package/dist/react/model-context/useInteractable.js.map +1 -1
  37. package/dist/react/model-context/useInteractableState.js +3 -2
  38. package/dist/react/model-context/useInteractableState.js.map +1 -1
  39. package/dist/react/model-context/useInteractableVersions.js +1 -0
  40. package/dist/react/model-context/useInteractableVersions.js.map +1 -1
  41. package/dist/react/model-context/useToolArgsStatus.js +1 -0
  42. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  43. package/dist/react/primitive-hooks/useActionBarCopy.js +4 -3
  44. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  45. package/dist/react/primitive-hooks/useActionBarEdit.js +2 -1
  46. package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -1
  47. package/dist/react/primitive-hooks/useActionBarFeedback.js +3 -2
  48. package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -1
  49. package/dist/react/primitive-hooks/useActionBarReload.js +2 -1
  50. package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -1
  51. package/dist/react/primitive-hooks/useActionBarSpeak.js +2 -1
  52. package/dist/react/primitive-hooks/useActionBarSpeak.js.map +1 -1
  53. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +2 -1
  54. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js.map +1 -1
  55. package/dist/react/primitive-hooks/useBranchPickerNext.js +2 -1
  56. package/dist/react/primitive-hooks/useBranchPickerNext.js.map +1 -1
  57. package/dist/react/primitive-hooks/useBranchPickerPrevious.js +2 -1
  58. package/dist/react/primitive-hooks/useBranchPickerPrevious.js.map +1 -1
  59. package/dist/react/primitive-hooks/useComposerAddAttachment.js +2 -1
  60. package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -1
  61. package/dist/react/primitive-hooks/useComposerCancel.js +2 -1
  62. package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -1
  63. package/dist/react/primitive-hooks/useComposerDictate.js +2 -1
  64. package/dist/react/primitive-hooks/useComposerDictate.js.map +1 -1
  65. package/dist/react/primitive-hooks/useComposerSend.js +2 -1
  66. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
  67. package/dist/react/primitive-hooks/useEditComposerCancel.js +2 -1
  68. package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -1
  69. package/dist/react/primitive-hooks/useEditComposerSend.js +2 -1
  70. package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -1
  71. package/dist/react/primitive-hooks/useMessageBranching.js +3 -2
  72. package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -1
  73. package/dist/react/primitive-hooks/useMessageReload.js +2 -1
  74. package/dist/react/primitive-hooks/useMessageReload.js.map +1 -1
  75. package/dist/react/primitive-hooks/useSuggestionTrigger.js +8 -7
  76. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  77. package/dist/react/primitive-hooks/useThreadListItemArchive.js +2 -1
  78. package/dist/react/primitive-hooks/useThreadListItemArchive.js.map +1 -1
  79. package/dist/react/primitive-hooks/useThreadListItemDelete.js +2 -1
  80. package/dist/react/primitive-hooks/useThreadListItemDelete.js.map +1 -1
  81. package/dist/react/primitive-hooks/useThreadListItemTrigger.js +2 -1
  82. package/dist/react/primitive-hooks/useThreadListItemTrigger.js.map +1 -1
  83. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +2 -1
  84. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js.map +1 -1
  85. package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -1
  86. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  87. package/dist/react/primitive-hooks/useThreadListNew.js +2 -1
  88. package/dist/react/primitive-hooks/useThreadListNew.js.map +1 -1
  89. package/dist/react/primitive-hooks/useVoice.js +5 -13
  90. package/dist/react/primitive-hooks/useVoice.js.map +1 -1
  91. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  92. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
  93. package/dist/react/primitives/composer/ComposerAttachments.js +1 -1
  94. package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
  95. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  96. package/dist/react/primitives/composer/ComposerQueue.js +1 -1
  97. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
  98. package/dist/react/primitives/message/MessageAttachments.d.ts.map +1 -1
  99. package/dist/react/primitives/message/MessageAttachments.js +1 -1
  100. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  101. package/dist/react/primitives/message/MessageParts.js +84 -122
  102. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  103. package/dist/react/primitives/thread/ThreadMessages.js +1 -1
  104. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  105. package/dist/react/primitives/thread/ThreadSuggestions.js +1 -1
  106. package/dist/react/primitives/thread/ThreadSuggestions.js.map +1 -1
  107. package/dist/react/primitives/threadList/ThreadListItems.js +1 -1
  108. package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
  109. package/dist/react/providers/AttachmentByIndexProvider.js +2 -2
  110. package/dist/react/providers/AttachmentByIndexProvider.js.map +1 -1
  111. package/dist/react/providers/ChainOfThoughtByIndicesProvider.js +1 -1
  112. package/dist/react/providers/ChainOfThoughtByIndicesProvider.js.map +1 -1
  113. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
  114. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +30 -18
  115. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
  116. package/dist/react/providers/MessageByIdProvider.js +2 -2
  117. package/dist/react/providers/MessageByIdProvider.js.map +1 -1
  118. package/dist/react/providers/MessageByIndexProvider.js +2 -2
  119. package/dist/react/providers/MessageByIndexProvider.js.map +1 -1
  120. package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
  121. package/dist/react/providers/PartByIndexProvider.js +30 -18
  122. package/dist/react/providers/PartByIndexProvider.js.map +1 -1
  123. package/dist/react/providers/QueueItemByIndexProvider.js +1 -1
  124. package/dist/react/providers/QueueItemByIndexProvider.js.map +1 -1
  125. package/dist/react/providers/ReadonlyThreadProvider.js +1 -1
  126. package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
  127. package/dist/react/providers/SuggestionByIndexProvider.js +1 -1
  128. package/dist/react/providers/SuggestionByIndexProvider.js.map +1 -1
  129. package/dist/react/providers/TextMessagePartProvider.js +1 -0
  130. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  131. package/dist/react/providers/ThreadListItemByIndexProvider.js +1 -1
  132. package/dist/react/providers/ThreadListItemByIndexProvider.js.map +1 -1
  133. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +3 -3
  134. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  135. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  136. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +6 -4
  137. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  138. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +31 -1
  139. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  140. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +42 -20
  141. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  142. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -1
  143. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  144. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +1 -1
  145. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  146. package/dist/react/runtimes/cloud/auiV0.d.ts +12 -1
  147. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  148. package/dist/react/runtimes/cloud/auiV0.js +2 -1
  149. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  150. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +75 -73
  151. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  152. package/dist/react/runtimes/createRuntimeExtras.js +1 -1
  153. package/dist/react/runtimes/createRuntimeExtras.js.map +1 -1
  154. package/dist/react/runtimes/external-message-converter.js +1 -1
  155. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  156. package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -0
  157. package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -1
  158. package/dist/react/runtimes/useLocalRuntime.d.ts +0 -1
  159. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  160. package/dist/react/types/scopes/tools.d.ts +0 -6
  161. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  162. package/dist/react/utils/groupParts.d.ts +1 -3
  163. package/dist/react/utils/groupParts.d.ts.map +1 -1
  164. package/dist/react/utils/groupParts.js +1 -3
  165. package/dist/react/utils/groupParts.js.map +1 -1
  166. package/dist/runtime/api/assistant-runtime.d.ts +1 -1
  167. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  168. package/dist/runtime/api/assistant-runtime.js +1 -1
  169. package/dist/runtime/api/assistant-runtime.js.map +1 -1
  170. package/dist/runtime/api/attachment-runtime.d.ts +1 -1
  171. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  172. package/dist/runtime/api/attachment-runtime.js +1 -1
  173. package/dist/runtime/api/attachment-runtime.js.map +1 -1
  174. package/dist/runtime/api/composer-runtime.d.ts +2 -2
  175. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  176. package/dist/runtime/api/composer-runtime.js +2 -2
  177. package/dist/runtime/api/composer-runtime.js.map +1 -1
  178. package/dist/runtime/api/message-part-runtime.d.ts +4 -4
  179. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  180. package/dist/runtime/api/message-part-runtime.js +3 -3
  181. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  182. package/dist/runtime/api/message-runtime.d.ts +1 -1
  183. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  184. package/dist/runtime/api/message-runtime.js +2 -2
  185. package/dist/runtime/api/message-runtime.js.map +1 -1
  186. package/dist/runtime/api/thread-list-item-runtime.d.ts +1 -1
  187. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  188. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  189. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  190. package/dist/runtime/api/thread-list-runtime.d.ts +1 -1
  191. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  192. package/dist/runtime/api/thread-list-runtime.js +1 -1
  193. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  194. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -1
  195. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  196. package/dist/runtime/base/base-thread-runtime-core.js +1 -1
  197. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  198. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +2 -2
  199. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  200. package/dist/runtime/base/default-edit-composer-runtime-core.js +2 -2
  201. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  202. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  203. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  204. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  205. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  206. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  207. package/dist/runtime/utils/message-repository.js +4 -2
  208. package/dist/runtime/utils/message-repository.js.map +1 -1
  209. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
  210. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  211. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +1 -1
  212. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  213. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  214. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +12 -0
  215. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  216. package/dist/runtimes/internal.d.ts +2 -1
  217. package/dist/runtimes/internal.js +2 -1
  218. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -0
  219. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  220. package/dist/runtimes/local/local-thread-runtime-core.js +44 -13
  221. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  222. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +32 -27
  223. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  224. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  225. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  226. package/dist/store/clients/no-op-composer-client.js +1 -0
  227. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  228. package/dist/store/clients/runtime-adapter.js +3 -3
  229. package/dist/store/clients/runtime-adapter.js.map +1 -1
  230. package/dist/store/runtime-clients/thread-list-runtime-client.js +1 -0
  231. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  232. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  233. package/dist/store/runtime-clients/thread-runtime-client.js +17 -15
  234. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  235. package/dist/store/scopes/thread.d.ts +1 -0
  236. package/dist/store/scopes/thread.d.ts.map +1 -1
  237. package/dist/subscribable/subscribable.d.ts +3 -3
  238. package/dist/subscribable/subscribable.d.ts.map +1 -1
  239. package/dist/subscribable/subscribable.js +3 -3
  240. package/dist/subscribable/subscribable.js.map +1 -1
  241. package/package.json +8 -8
  242. package/src/adapters/thread-history.ts +8 -0
  243. package/src/internal.ts +1 -1
  244. package/src/model-context/frame/host.test.ts +47 -0
  245. package/src/model-context/frame/host.ts +46 -19
  246. package/src/model-context/types.ts +16 -6
  247. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +17 -7
  248. package/src/react/client/Tools.ts +0 -7
  249. package/src/react/interactables-legacy/useAssistantInteractable.ts +1 -1
  250. package/src/react/interactables-legacy/useInteractableState.ts +3 -3
  251. package/src/react/model-context/useAssistantContext.ts +1 -1
  252. package/src/react/model-context/useAssistantDataUI.ts +1 -1
  253. package/src/react/model-context/useAssistantInstructions.ts +1 -1
  254. package/src/react/model-context/useAssistantTool.ts +2 -2
  255. package/src/react/model-context/useAssistantToolUI.ts +1 -1
  256. package/src/react/model-context/useAuiToolOverrides.ts +1 -1
  257. package/src/react/model-context/useInteractable.ts +1 -1
  258. package/src/react/model-context/useInteractableState.ts +2 -2
  259. package/src/react/primitive-hooks/useActionBarCopy.test.ts +2 -2
  260. package/src/react/primitive-hooks/useActionBarCopy.ts +3 -3
  261. package/src/react/primitive-hooks/useActionBarEdit.ts +1 -1
  262. package/src/react/primitive-hooks/useActionBarFeedback.ts +2 -2
  263. package/src/react/primitive-hooks/useActionBarReload.ts +1 -1
  264. package/src/react/primitive-hooks/useActionBarSpeak.ts +1 -1
  265. package/src/react/primitive-hooks/useActionBarStopSpeaking.ts +1 -1
  266. package/src/react/primitive-hooks/useBranchPickerNext.ts +1 -1
  267. package/src/react/primitive-hooks/useBranchPickerPrevious.ts +1 -1
  268. package/src/react/primitive-hooks/useComposerAddAttachment.ts +1 -1
  269. package/src/react/primitive-hooks/useComposerCancel.ts +1 -1
  270. package/src/react/primitive-hooks/useComposerDictate.ts +1 -1
  271. package/src/react/primitive-hooks/useComposerSend.ts +1 -1
  272. package/src/react/primitive-hooks/useEditComposerCancel.ts +1 -1
  273. package/src/react/primitive-hooks/useEditComposerSend.ts +1 -1
  274. package/src/react/primitive-hooks/useMessageBranching.ts +2 -2
  275. package/src/react/primitive-hooks/useMessageReload.ts +1 -1
  276. package/src/react/primitive-hooks/useSuggestionTrigger.ts +9 -9
  277. package/src/react/primitive-hooks/useThreadListItemArchive.ts +1 -1
  278. package/src/react/primitive-hooks/useThreadListItemDelete.ts +1 -1
  279. package/src/react/primitive-hooks/useThreadListItemTrigger.ts +1 -1
  280. package/src/react/primitive-hooks/useThreadListItemUnarchive.ts +1 -1
  281. package/src/react/primitive-hooks/useThreadListLoadMore.ts +1 -1
  282. package/src/react/primitive-hooks/useThreadListNew.ts +1 -1
  283. package/src/react/primitive-hooks/useVoice.ts +5 -5
  284. package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -1
  285. package/src/react/primitives/composer/ComposerAttachments.tsx +1 -1
  286. package/src/react/primitives/composer/ComposerQueue.tsx +1 -3
  287. package/src/react/primitives/message/MessageAttachments.tsx +1 -3
  288. package/src/react/primitives/message/MessageParts.tsx +9 -9
  289. package/src/react/primitives/thread/ThreadMessages.tsx +1 -1
  290. package/src/react/primitives/thread/ThreadSuggestions.tsx +1 -1
  291. package/src/react/primitives/threadList/ThreadListItems.tsx +1 -1
  292. package/src/react/providers/AttachmentByIndexProvider.tsx +2 -2
  293. package/src/react/providers/ChainOfThoughtByIndicesProvider.tsx +1 -1
  294. package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +2 -19
  295. package/src/react/providers/MessageByIdProvider.tsx +2 -2
  296. package/src/react/providers/MessageByIndexProvider.tsx +2 -2
  297. package/src/react/providers/PartByIndexProvider.tsx +2 -16
  298. package/src/react/providers/QueueItemByIndexProvider.tsx +1 -1
  299. package/src/react/providers/ReadonlyThreadProvider.tsx +1 -1
  300. package/src/react/providers/SuggestionByIndexProvider.tsx +1 -1
  301. package/src/react/providers/ThreadListItemByIndexProvider.tsx +1 -1
  302. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +3 -3
  303. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +20 -6
  304. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +34 -0
  305. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +32 -7
  306. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +47 -16
  307. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +5 -1
  308. package/src/react/runtimes/cloud/auiV0.ts +14 -0
  309. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +90 -88
  310. package/src/react/runtimes/createRuntimeExtras.test.ts +1 -1
  311. package/src/react/runtimes/createRuntimeExtras.ts +1 -1
  312. package/src/react/runtimes/external-message-converter.test.ts +346 -0
  313. package/src/react/runtimes/external-message-converter.ts +1 -1
  314. package/src/react/runtimes/useLocalRuntime.test.tsx +43 -0
  315. package/src/react/types/scopes/tools.ts +0 -6
  316. package/src/react/utils/groupParts.ts +1 -7
  317. package/src/runtime/api/assistant-runtime.ts +4 -1
  318. package/src/runtime/api/attachment-runtime.ts +8 -2
  319. package/src/runtime/api/composer-runtime.ts +9 -5
  320. package/src/runtime/api/message-part-runtime.ts +10 -3
  321. package/src/runtime/api/message-runtime.ts +7 -2
  322. package/src/runtime/api/thread-list-item-runtime.ts +7 -2
  323. package/src/runtime/api/thread-list-runtime.ts +10 -2
  324. package/src/runtime/base/base-thread-runtime-core.ts +5 -1
  325. package/src/runtime/base/default-edit-composer-runtime-core.ts +14 -2
  326. package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
  327. package/src/runtime/utils/message-repository.ts +19 -13
  328. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +4 -1
  329. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +15 -0
  330. package/src/runtimes/internal.ts +3 -0
  331. package/src/runtimes/local/local-thread-runtime-core.test.ts +596 -0
  332. package/src/runtimes/local/local-thread-runtime-core.ts +85 -21
  333. package/src/runtimes/local/should-continue.test.ts +122 -0
  334. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +9 -10
  335. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +38 -32
  336. package/src/store/clients/model-context-client.test.ts +21 -1
  337. package/src/store/clients/runtime-adapter.ts +3 -3
  338. package/src/store/runtime-clients/thread-runtime-client.ts +2 -1
  339. package/src/store/scopes/thread.ts +1 -0
  340. package/src/subscribable/subscribable.ts +16 -8
  341. package/src/tests/MessageRepository.test.ts +12 -0
  342. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-position.test.ts +121 -0
  343. package/src/tests/auiV0Encode.test.ts +74 -0
  344. package/src/tests/base-composer-runtime-core.test.ts +422 -0
  345. package/src/tests/external-store-thread-list-runtime-core.test.ts +6 -6
  346. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +575 -0
  347. package/src/tests/external-store-thread-runtime-core.test.ts +55 -1
  348. package/src/tests/groupParts.test.ts +3 -48
  349. package/src/tests/OptimisticState-delete-crash.test.ts +0 -208
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import type {
3
+ RemoteThreadListResponse,
4
+ RemoteThreadMetadata,
5
+ } from "../runtimes/remote-thread-list/types";
6
+ import {
7
+ createCore,
8
+ deferred,
9
+ makeAdapter,
10
+ } from "./remote-thread-list-test-helpers";
11
+
12
+ const thread = (id: string): RemoteThreadMetadata => ({
13
+ status: "regular",
14
+ remoteId: id,
15
+ externalId: id,
16
+ title: id,
17
+ });
18
+
19
+ const archivedThread = (id: string): RemoteThreadMetadata => ({
20
+ status: "archived",
21
+ remoteId: id,
22
+ externalId: id,
23
+ title: id,
24
+ });
25
+
26
+ describe("RemoteThreadListThreadListRuntimeCore.switchToThread position", () => {
27
+ it("keeps the position a concurrent list() already gave the thread", async () => {
28
+ const TARGET = "thread-target";
29
+ const listDeferred = deferred<RemoteThreadListResponse>();
30
+ const fetchDeferred = deferred<RemoteThreadMetadata>();
31
+
32
+ const adapter = makeAdapter({
33
+ list: vi.fn(() => listDeferred.promise),
34
+ fetch: vi.fn(() => fetchDeferred.promise),
35
+ });
36
+
37
+ const core = createCore(adapter, TARGET);
38
+
39
+ const loadPromise = core.getLoadThreadsPromise();
40
+ const switchPromise = core.switchToThread(TARGET);
41
+
42
+ // `list()` resolves first and places TARGET at the top, as the most recent.
43
+ listDeferred.resolve({
44
+ threads: [thread(TARGET), thread("thread-a"), thread("thread-b")],
45
+ });
46
+ await loadPromise;
47
+
48
+ expect(core.threadIds).toEqual([TARGET, "thread-a", "thread-b"]);
49
+
50
+ fetchDeferred.resolve(thread(TARGET));
51
+ await switchPromise;
52
+
53
+ // The fetch path must not filter TARGET out of position and re-append it.
54
+ expect(core.threadIds).toEqual([TARGET, "thread-a", "thread-b"]);
55
+ });
56
+
57
+ it("appends a thread that is genuinely absent from the loaded list", async () => {
58
+ const TARGET = "thread-target";
59
+ const listDeferred = deferred<RemoteThreadListResponse>();
60
+ const fetchDeferred = deferred<RemoteThreadMetadata>();
61
+
62
+ const adapter = makeAdapter({
63
+ list: vi.fn(() => listDeferred.promise),
64
+ fetch: vi.fn(() => fetchDeferred.promise),
65
+ });
66
+
67
+ const core = createCore(adapter, TARGET);
68
+
69
+ const loadPromise = core.getLoadThreadsPromise();
70
+ const switchPromise = core.switchToThread(TARGET);
71
+
72
+ // `list()` resolves first but does not include TARGET (e.g. it is past the
73
+ // first page).
74
+ listDeferred.resolve({
75
+ threads: [thread("thread-a"), thread("thread-b")],
76
+ });
77
+ await loadPromise;
78
+
79
+ expect(core.threadIds).toEqual(["thread-a", "thread-b"]);
80
+
81
+ fetchDeferred.resolve(thread(TARGET));
82
+ await switchPromise;
83
+
84
+ // An absent thread may live on an unloaded page; appending keeps it below
85
+ // every loaded (newer) thread instead of pinning it to the top.
86
+ expect(core.threadIds).toEqual(["thread-a", "thread-b", TARGET]);
87
+ });
88
+
89
+ it("keeps the position a concurrent list() gave an archived thread", async () => {
90
+ const TARGET = "thread-target";
91
+ const listDeferred = deferred<RemoteThreadListResponse>();
92
+ const fetchDeferred = deferred<RemoteThreadMetadata>();
93
+
94
+ const adapter = makeAdapter({
95
+ list: vi.fn(() => listDeferred.promise),
96
+ fetch: vi.fn(() => fetchDeferred.promise),
97
+ });
98
+
99
+ const core = createCore(adapter, TARGET);
100
+ // Stub unarchive so auto-unarchive doesn't move the thread into `threadIds`
101
+ // via `updateStatusReducer`, hiding the position written by the fetch path.
102
+ (
103
+ core as unknown as { unarchive: (id: string) => Promise<void> }
104
+ ).unarchive = async () => {};
105
+
106
+ const loadPromise = core.getLoadThreadsPromise();
107
+ const switchPromise = core.switchToThread(TARGET);
108
+
109
+ listDeferred.resolve({
110
+ threads: [archivedThread(TARGET), archivedThread("thread-a")],
111
+ });
112
+ await loadPromise;
113
+
114
+ expect(core.archivedThreadIds).toEqual([TARGET, "thread-a"]);
115
+
116
+ fetchDeferred.resolve(archivedThread(TARGET));
117
+ await switchPromise;
118
+
119
+ expect(core.archivedThreadIds).toEqual([TARGET, "thread-a"]);
120
+ });
121
+ });
@@ -53,6 +53,35 @@ describe("auiV0Encode", () => {
53
53
  ]);
54
54
  });
55
55
 
56
+ it("preserves a pending tool-call approval in the core cloud encoder", () => {
57
+ const encoded = auiV0Encode({
58
+ id: "m1",
59
+ createdAt: new Date("2026-03-15T00:00:00.000Z"),
60
+ role: "assistant",
61
+ status: { type: "requires-action", reason: "tool-calls" },
62
+ metadata: {
63
+ unstable_state: null,
64
+ unstable_annotations: [],
65
+ unstable_data: [],
66
+ steps: [],
67
+ custom: {},
68
+ },
69
+ content: [
70
+ {
71
+ type: "tool-call",
72
+ toolCallId: "call-1",
73
+ toolName: "send_email",
74
+ args: {},
75
+ argsText: "{}",
76
+ approval: { id: "a1" },
77
+ },
78
+ ],
79
+ });
80
+
81
+ const toolCall = encoded.content.find((p) => p.type === "tool-call");
82
+ expect(toolCall).toMatchObject({ approval: { id: "a1" } });
83
+ });
84
+
56
85
  it("preserves user attachments in the core cloud encoder", () => {
57
86
  const encoded = auiV0Encode({
58
87
  id: "m1",
@@ -129,6 +158,51 @@ describe("auiV0Encode", () => {
129
158
  });
130
159
 
131
160
  describe("auiV0Decode", () => {
161
+ it("round-trips a pending tool-call approval so a paused run stays resumable", () => {
162
+ const content = auiV0Encode({
163
+ id: "local",
164
+ createdAt: new Date("2026-03-15T00:00:00.000Z"),
165
+ role: "assistant",
166
+ status: { type: "requires-action", reason: "tool-calls" },
167
+ metadata: {
168
+ unstable_state: null,
169
+ unstable_annotations: [],
170
+ unstable_data: [],
171
+ steps: [],
172
+ custom: {},
173
+ },
174
+ content: [
175
+ {
176
+ type: "tool-call",
177
+ toolCallId: "call-1",
178
+ toolName: "send_email",
179
+ args: {},
180
+ argsText: "{}",
181
+ approval: { id: "a1" },
182
+ },
183
+ ],
184
+ });
185
+
186
+ const decoded = auiV0Decode({
187
+ id: "cloud",
188
+ parent_id: null,
189
+ height: 0,
190
+ created_at: new Date("2026-03-15T00:00:00.000Z"),
191
+ updated_at: new Date("2026-03-15T00:00:00.000Z"),
192
+ format: "aui/v0",
193
+ content: content as never,
194
+ });
195
+
196
+ if (decoded.message.role !== "assistant")
197
+ throw new Error("expected assistant");
198
+ const toolCall = decoded.message.content.find(
199
+ (p) => p.type === "tool-call",
200
+ );
201
+ expect(toolCall?.type === "tool-call" && toolCall.approval).toEqual({
202
+ id: "a1",
203
+ });
204
+ });
205
+
132
206
  it("restores user attachments from core cloud history", () => {
133
207
  const content = auiV0Encode({
134
208
  id: "local",
@@ -0,0 +1,422 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { BaseComposerRuntimeCore } from "../runtime/base/base-composer-runtime-core";
3
+ import type { AttachmentAdapter } from "../adapters/attachment";
4
+ import type { DictationAdapter } from "../adapters/speech";
5
+ import type { CreateAttachment, PendingAttachment } from "../types/attachment";
6
+ import type { AppendMessage } from "../types/message";
7
+ import type { SendOptions } from "../runtime/interfaces/composer-runtime-core";
8
+
9
+ class TestComposerCore extends BaseComposerRuntimeCore {
10
+ private _attachmentAdapter: AttachmentAdapter | undefined;
11
+ public sentMessages: Array<Omit<AppendMessage, "parentId" | "sourceId">> = [];
12
+ public sentOptions: Array<SendOptions | undefined> = [];
13
+
14
+ protected getAttachmentAdapter() {
15
+ return this._attachmentAdapter;
16
+ }
17
+ protected getDictationAdapter(): DictationAdapter | undefined {
18
+ return undefined;
19
+ }
20
+
21
+ setAttachmentAdapter(adapter: AttachmentAdapter | undefined) {
22
+ this._attachmentAdapter = adapter;
23
+ }
24
+
25
+ get canCancel() {
26
+ return false;
27
+ }
28
+
29
+ get canSend() {
30
+ return !this.isEmpty;
31
+ }
32
+
33
+ protected handleSend(
34
+ message: Omit<AppendMessage, "parentId" | "sourceId">,
35
+ options?: SendOptions,
36
+ ) {
37
+ this.sentMessages.push(message);
38
+ this.sentOptions.push(options);
39
+ }
40
+
41
+ protected handleCancel() {}
42
+ }
43
+
44
+ const makePendingAttachment = (
45
+ id: string,
46
+ name = "file.txt",
47
+ ): PendingAttachment => ({
48
+ id,
49
+ type: "file",
50
+ name,
51
+ contentType: "text/plain",
52
+ file: new File(["content"], name),
53
+ status: { type: "requires-action", reason: "composer-send" },
54
+ });
55
+
56
+ describe("BaseComposerRuntimeCore", () => {
57
+ let composer: TestComposerCore;
58
+
59
+ beforeEach(() => {
60
+ composer = new TestComposerCore();
61
+ });
62
+
63
+ it("does not notify when a set value is unchanged", () => {
64
+ composer.setText("same");
65
+ const listener = vi.fn();
66
+ composer.subscribe(listener);
67
+
68
+ composer.setText("same");
69
+ expect(listener).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it("isEmpty returns true for whitespace-only text", () => {
73
+ composer.setText(" ");
74
+ expect(composer.isEmpty).toBe(true);
75
+ });
76
+
77
+ it("reset clears text, attachments, role, runConfig, and quote", async () => {
78
+ composer.setText("hello");
79
+ composer.setRole("assistant");
80
+ composer.setRunConfig({ custom: { k: "v" } });
81
+ composer.setQuote({ text: "q", messageId: "m1" });
82
+
83
+ await composer.reset();
84
+
85
+ expect(composer.text).toBe("");
86
+ expect(composer.role).toBe("user");
87
+ expect(composer.runConfig).toEqual({});
88
+ expect(composer.quote).toBeUndefined();
89
+ expect(composer.attachments).toEqual([]);
90
+ });
91
+
92
+ it("reset does not notify when already in default state", async () => {
93
+ const listener = vi.fn();
94
+ composer.subscribe(listener);
95
+
96
+ await composer.reset();
97
+ expect(listener).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it("send includes quote in metadata and clears it", async () => {
101
+ const quote = { text: "quoted", messageId: "m1" };
102
+ composer.setText("reply");
103
+ composer.setQuote(quote);
104
+
105
+ await composer.send();
106
+
107
+ const msg = composer.sentMessages[0]!;
108
+ expect(msg.metadata).toEqual({ custom: { quote } });
109
+ expect(composer.quote).toBeUndefined();
110
+ });
111
+
112
+ it("forwards send options to handleSend", async () => {
113
+ composer.setText("a");
114
+ await composer.send();
115
+ composer.setText("b");
116
+ await composer.send({ startRun: true });
117
+ composer.setText("c");
118
+ await composer.send({ startRun: false });
119
+
120
+ expect(composer.sentOptions).toEqual([
121
+ undefined,
122
+ { startRun: true },
123
+ { startRun: false },
124
+ ]);
125
+ });
126
+
127
+ it("removeAttachment removes via adapter", async () => {
128
+ const pending = makePendingAttachment("att-1");
129
+ const adapter: AttachmentAdapter = {
130
+ accept: "*",
131
+ add: vi.fn().mockResolvedValue(pending),
132
+ remove: vi.fn().mockResolvedValue(undefined),
133
+ send: vi.fn(),
134
+ };
135
+ composer.setAttachmentAdapter(adapter);
136
+
137
+ await composer.addAttachment(new File([""], "f.txt"));
138
+ expect(composer.attachments).toHaveLength(1);
139
+
140
+ await composer.removeAttachment("att-1");
141
+ expect(composer.attachments).toHaveLength(0);
142
+ expect(adapter.remove).toHaveBeenCalledWith(pending);
143
+ });
144
+
145
+ it("removeAttachment throws for unknown id", async () => {
146
+ const adapter: AttachmentAdapter = {
147
+ accept: "*",
148
+ add: vi.fn(),
149
+ remove: vi.fn(),
150
+ send: vi.fn(),
151
+ };
152
+ composer.setAttachmentAdapter(adapter);
153
+
154
+ await expect(composer.removeAttachment("nonexistent")).rejects.toThrow(
155
+ "Attachment not found",
156
+ );
157
+ });
158
+
159
+ it("unstable_on unsubscribe stops notifications", async () => {
160
+ const callback = vi.fn();
161
+ const unsub = composer.unstable_on("send", callback);
162
+ unsub();
163
+
164
+ composer.setText("test");
165
+ await composer.send();
166
+
167
+ expect(callback).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it("attachmentAccept returns adapter accept or default", () => {
171
+ expect(composer.attachmentAccept).toBe("*");
172
+
173
+ const adapter: AttachmentAdapter = {
174
+ accept: "image/*",
175
+ add: vi.fn(),
176
+ remove: vi.fn(),
177
+ send: vi.fn(),
178
+ };
179
+ composer.setAttachmentAdapter(adapter);
180
+ expect(composer.attachmentAccept).toBe("image/*");
181
+ });
182
+
183
+ describe("CreateAttachment (external source)", () => {
184
+ const makeCreateAttachment = (
185
+ overrides?: Partial<CreateAttachment>,
186
+ ): CreateAttachment => ({
187
+ name: "external-doc.pdf",
188
+ content: [{ type: "text", text: "extracted content" }],
189
+ ...overrides,
190
+ });
191
+
192
+ it("addAttachment with CreateAttachment adds without adapter", async () => {
193
+ const att = makeCreateAttachment();
194
+ await composer.addAttachment(att);
195
+
196
+ expect(composer.attachments).toHaveLength(1);
197
+ expect(composer.attachments[0]!.name).toBe("external-doc.pdf");
198
+ expect(composer.attachments[0]!.status).toEqual({ type: "complete" });
199
+ expect(composer.attachments[0]!.type).toBe("document");
200
+ });
201
+
202
+ it("addAttachment with CreateAttachment uses provided id and type", async () => {
203
+ const att = makeCreateAttachment({
204
+ id: "custom-id",
205
+ type: "image",
206
+ contentType: "image/png",
207
+ });
208
+ await composer.addAttachment(att);
209
+
210
+ expect(composer.attachments[0]!.id).toBe("custom-id");
211
+ expect(composer.attachments[0]!.type).toBe("image");
212
+ expect(composer.attachments[0]!.contentType).toBe("image/png");
213
+ });
214
+
215
+ it("addAttachment with CreateAttachment generates id when not provided", async () => {
216
+ const att = makeCreateAttachment();
217
+ await composer.addAttachment(att);
218
+
219
+ expect(composer.attachments[0]!.id).toBeTruthy();
220
+ expect(typeof composer.attachments[0]!.id).toBe("string");
221
+ });
222
+
223
+ it("addAttachment with CreateAttachment fires attachmentAdd event", async () => {
224
+ const callback = vi.fn();
225
+ composer.unstable_on("attachmentAdd", callback);
226
+
227
+ await composer.addAttachment(makeCreateAttachment());
228
+ expect(callback).toHaveBeenCalledTimes(1);
229
+ });
230
+
231
+ it("removeAttachment on complete attachment does not require adapter", async () => {
232
+ await composer.addAttachment(makeCreateAttachment({ id: "ext-1" }));
233
+ expect(composer.attachments).toHaveLength(1);
234
+
235
+ await composer.removeAttachment("ext-1");
236
+ expect(composer.attachments).toHaveLength(0);
237
+ });
238
+
239
+ it("isEmpty returns false when external attachment present", async () => {
240
+ expect(composer.isEmpty).toBe(true);
241
+
242
+ await composer.addAttachment(makeCreateAttachment());
243
+ expect(composer.isEmpty).toBe(false);
244
+ });
245
+
246
+ it("send with mixed pending + complete attachments", async () => {
247
+ const pending = makePendingAttachment("att-pending");
248
+ const completedFromAdapter = {
249
+ id: "att-pending",
250
+ type: "file" as const,
251
+ name: "file.txt",
252
+ contentType: "text/plain",
253
+ content: [{ type: "text" as const, text: "file content" }],
254
+ status: { type: "complete" as const },
255
+ };
256
+ const adapter: AttachmentAdapter = {
257
+ accept: "*",
258
+ add: vi.fn().mockResolvedValue(pending),
259
+ remove: vi.fn(),
260
+ send: vi.fn().mockResolvedValue(completedFromAdapter),
261
+ };
262
+ composer.setAttachmentAdapter(adapter);
263
+
264
+ await composer.addAttachment(new File(["data"], "file.txt"));
265
+ await composer.addAttachment(makeCreateAttachment({ id: "ext-1" }));
266
+
267
+ expect(composer.attachments).toHaveLength(2);
268
+
269
+ composer.setText("hello");
270
+ await composer.send();
271
+
272
+ expect(composer.sentMessages).toHaveLength(1);
273
+ const msg = composer.sentMessages[0]!;
274
+ expect(msg.attachments).toHaveLength(2);
275
+ expect(adapter.send).toHaveBeenCalledTimes(1);
276
+ });
277
+
278
+ describe("adapter.accept enforcement", () => {
279
+ const makeImageAdapter = (): AttachmentAdapter => ({
280
+ accept: "image/*",
281
+ add: vi.fn(),
282
+ remove: vi.fn(),
283
+ send: vi.fn(),
284
+ });
285
+
286
+ it("rejects external attachment with contentType not matching adapter.accept", async () => {
287
+ composer.setAttachmentAdapter(makeImageAdapter());
288
+ const onError = vi.fn();
289
+ const onAdd = vi.fn();
290
+ composer.unstable_on("attachmentAddError", onError);
291
+ composer.unstable_on("attachmentAdd", onAdd);
292
+
293
+ await expect(
294
+ composer.addAttachment(
295
+ makeCreateAttachment({
296
+ name: "doc.pdf",
297
+ contentType: "application/pdf",
298
+ }),
299
+ ),
300
+ ).rejects.toThrow(/File type application\/pdf is not accepted/);
301
+
302
+ expect(composer.attachments).toHaveLength(0);
303
+ expect(onError).toHaveBeenCalledTimes(1);
304
+ expect(onError).toHaveBeenCalledWith(
305
+ expect.objectContaining({
306
+ reason: "not-accepted",
307
+ message: expect.stringContaining(
308
+ "File type application/pdf is not accepted",
309
+ ),
310
+ error: expect.any(Error),
311
+ }),
312
+ );
313
+ expect(onAdd).not.toHaveBeenCalled();
314
+ });
315
+
316
+ it("accepts external attachment with contentType matching adapter.accept", async () => {
317
+ composer.setAttachmentAdapter(makeImageAdapter());
318
+ const onError = vi.fn();
319
+
320
+ composer.unstable_on("attachmentAddError", onError);
321
+ await composer.addAttachment(
322
+ makeCreateAttachment({
323
+ name: "photo.png",
324
+ contentType: "image/png",
325
+ }),
326
+ );
327
+
328
+ expect(composer.attachments).toHaveLength(1);
329
+ expect(onError).not.toHaveBeenCalled();
330
+ });
331
+
332
+ it("accepts external attachment when only filename extension matches adapter.accept", async () => {
333
+ composer.setAttachmentAdapter({
334
+ accept: ".pdf",
335
+ add: vi.fn(),
336
+ remove: vi.fn(),
337
+ send: vi.fn(),
338
+ });
339
+
340
+ await composer.addAttachment(
341
+ makeCreateAttachment({ name: "report.pdf" }),
342
+ );
343
+
344
+ expect(composer.attachments).toHaveLength(1);
345
+ });
346
+
347
+ it("rejects external attachment when contentType missing and extension does not match", async () => {
348
+ composer.setAttachmentAdapter(makeImageAdapter());
349
+ const onError = vi.fn();
350
+ composer.unstable_on("attachmentAddError", onError);
351
+
352
+ await expect(
353
+ composer.addAttachment(makeCreateAttachment({ name: "notes.txt" })),
354
+ ).rejects.toThrow(/is not accepted/);
355
+
356
+ expect(onError).toHaveBeenCalledTimes(1);
357
+ });
358
+
359
+ it("adds external attachment without check when no adapter is configured", async () => {
360
+ await composer.addAttachment(
361
+ makeCreateAttachment({
362
+ name: "anything.xyz",
363
+ contentType: "application/x-anything",
364
+ }),
365
+ );
366
+
367
+ expect(composer.attachments).toHaveLength(1);
368
+ });
369
+
370
+ it("isolates a throwing attachmentAdd subscriber without firing attachmentAddError", async () => {
371
+ composer.setAttachmentAdapter(makeImageAdapter());
372
+ const listenerError = new Error("add subscriber boom");
373
+ const consoleError = vi
374
+ .spyOn(console, "error")
375
+ .mockImplementation(() => {});
376
+ const onError = vi.fn();
377
+ composer.unstable_on("attachmentAdd", () => {
378
+ throw listenerError;
379
+ });
380
+ composer.unstable_on("attachmentAddError", onError);
381
+
382
+ await expect(
383
+ composer.addAttachment(
384
+ makeCreateAttachment({
385
+ name: "photo.png",
386
+ contentType: "image/png",
387
+ }),
388
+ ),
389
+ ).resolves.toBeUndefined();
390
+
391
+ expect(composer.attachments).toHaveLength(1);
392
+ expect(onError).not.toHaveBeenCalled();
393
+ expect(consoleError).toHaveBeenCalledWith(
394
+ '[assistant-ui] Composer runtime "attachmentAdd" listener threw an error',
395
+ listenerError,
396
+ );
397
+ consoleError.mockRestore();
398
+ });
399
+
400
+ it("does not fire attachmentAddError when a state subscriber throws on add", async () => {
401
+ composer.setAttachmentAdapter(makeImageAdapter());
402
+ const onError = vi.fn();
403
+ composer.subscribe(() => {
404
+ throw new Error("state subscriber boom");
405
+ });
406
+ composer.unstable_on("attachmentAddError", onError);
407
+
408
+ await expect(
409
+ composer.addAttachment(
410
+ makeCreateAttachment({
411
+ name: "photo.png",
412
+ contentType: "image/png",
413
+ }),
414
+ ),
415
+ ).rejects.toThrow("state subscriber boom");
416
+
417
+ expect(composer.attachments).toHaveLength(1);
418
+ expect(onError).not.toHaveBeenCalled();
419
+ });
420
+ });
421
+ });
422
+ });
@@ -27,7 +27,7 @@ describe("ExternalStoreThreadListRuntimeCore - construction", () => {
27
27
  expect(core.getItemById(core.mainThreadId)).toBeDefined();
28
28
  });
29
29
 
30
- it("picks up mainThreadId from adapter.threadId on construction (regression: #2577)", () => {
30
+ it("picks up mainThreadId from adapter.threadId on construction", () => {
31
31
  const core = new ExternalStoreThreadListRuntimeCore(
32
32
  makeAdapter({ threadId: "thread-alpha" }),
33
33
  makeFactory(),
@@ -147,7 +147,7 @@ describe("ExternalStoreThreadListRuntimeCore - __internal_setAdapter", () => {
147
147
  expect(callback).toHaveBeenCalled();
148
148
  });
149
149
 
150
- it("synthesizes mainThreadId entry after a switch to a threadId not in the threads list (regression: #3971)", () => {
150
+ it("synthesizes mainThreadId entry after a switch to a threadId not in the threads list", () => {
151
151
  const core = new ExternalStoreThreadListRuntimeCore(
152
152
  makeAdapter({ threadId: "thread-alpha" }),
153
153
  makeFactory(),
@@ -158,7 +158,7 @@ describe("ExternalStoreThreadListRuntimeCore - __internal_setAdapter", () => {
158
158
  expect(item?.id).toBe("thread-beta");
159
159
  });
160
160
 
161
- it("does not retain stale synthesized entries across mainThreadId switches (regression: #3971)", () => {
161
+ it("does not retain stale synthesized entries across mainThreadId switches", () => {
162
162
  const core = new ExternalStoreThreadListRuntimeCore(
163
163
  makeAdapter({ threadId: "thread-alpha" }),
164
164
  makeFactory(),
@@ -190,7 +190,7 @@ describe("ExternalStoreThreadListRuntimeCore - isMain via ThreadListRuntimeImpl"
190
190
  );
191
191
  };
192
192
 
193
- it("reports isMain=true for adapter.threadId on construction (user-visible regression: #2577)", () => {
193
+ it("reports isMain=true for adapter.threadId on construction", () => {
194
194
  const impl = buildImpl({
195
195
  threadId: "thread-alpha",
196
196
  threads: [
@@ -215,7 +215,7 @@ describe("ExternalStoreThreadListRuntimeCore - isMain via ThreadListRuntimeImpl"
215
215
  expect(state.threadIds).toEqual(["thread-alpha", "thread-beta"]);
216
216
  });
217
217
 
218
- it("does not throw when adapter.threadId has no matching threads entry (regression: #3971)", () => {
218
+ it("does not throw when adapter.threadId has no matching threads entry", () => {
219
219
  const impl = buildImpl({ threadId: "thread-alpha" });
220
220
  expect(impl.mainItem.getState().id).toBe("thread-alpha");
221
221
  expect(impl.mainItem.getState().isMain).toBe(true);
@@ -234,7 +234,7 @@ describe("ExternalStoreThreadListRuntimeCore - switchToThread", () => {
234
234
  expect(onSwitchToThread).toHaveBeenCalledWith("thread-beta");
235
235
  });
236
236
 
237
- it("early-returns without calling onSwitchToThread when target equals initial threadId (regression: #2577)", async () => {
237
+ it("early-returns without calling onSwitchToThread when target equals initial threadId", async () => {
238
238
  const onSwitchToThread = vi.fn(async () => {});
239
239
  const core = new ExternalStoreThreadListRuntimeCore(
240
240
  makeAdapter({ threadId: "thread-alpha", onSwitchToThread }),