@assistant-ui/core 0.3.14 → 0.3.16

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 (341) hide show
  1. package/dist/adapters/voice.js +11 -0
  2. package/dist/adapters/voice.js.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/internal.d.ts +8 -2
  5. package/dist/internal.js +9 -3
  6. package/dist/model-context/frame/host.d.ts +1 -1
  7. package/dist/model-context/frame/host.d.ts.map +1 -1
  8. package/dist/model-context/frame/host.js +20 -5
  9. package/dist/model-context/frame/host.js.map +1 -1
  10. package/dist/model-context/frame/provider.d.ts +6 -0
  11. package/dist/model-context/frame/provider.d.ts.map +1 -1
  12. package/dist/model-context/frame/provider.js +126 -16
  13. package/dist/model-context/frame/provider.js.map +1 -1
  14. package/dist/model-context/frame/types.d.ts +4 -1
  15. package/dist/model-context/frame/types.d.ts.map +1 -1
  16. package/dist/model-context/frame/types.js.map +1 -1
  17. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  18. package/dist/model-context/interactable-composer-metadata.js +1 -1
  19. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  20. package/dist/model-context/registry.d.ts +2 -2
  21. package/dist/model-context/registry.d.ts.map +1 -1
  22. package/dist/model-context/registry.js +9 -28
  23. package/dist/model-context/registry.js.map +1 -1
  24. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  25. package/dist/react/client/InMemoryThreadList.js +24 -20
  26. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  27. package/dist/react/client/Interactables.d.ts.map +1 -1
  28. package/dist/react/client/Interactables.js +59 -177
  29. package/dist/react/client/Interactables.js.map +1 -1
  30. package/dist/react/client/RemoteThreadList.d.ts +1 -1
  31. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  32. package/dist/react/client/RemoteThreadList.js +117 -78
  33. package/dist/react/client/RemoteThreadList.js.map +1 -1
  34. package/dist/react/index.d.ts +2 -1
  35. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  36. package/dist/react/interactables-legacy/Interactables.js +56 -174
  37. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  38. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  39. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  40. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  41. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  42. package/dist/react/primitive-hooks/useActionBarCopy.js +7 -1
  43. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  44. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +3 -0
  45. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
  46. package/dist/react/primitive-hooks/useThreadIsEmpty.js +3 -0
  47. package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -1
  48. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +3 -0
  49. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
  50. package/dist/react/primitive-hooks/useThreadIsRunning.js +3 -0
  51. package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -1
  52. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  53. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  54. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  55. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  56. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +3 -1
  57. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  58. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +6 -0
  59. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  60. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +7 -1
  61. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  62. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +174 -64
  63. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  64. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  65. package/dist/react/runtimes/RemoteThreadResource.js +2 -0
  66. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  67. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  68. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  69. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  70. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  71. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js +1 -1
  72. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  73. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  74. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  75. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  76. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  77. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  78. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  79. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  80. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  81. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  82. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +38 -5
  83. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  84. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  85. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  86. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  87. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  88. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  89. package/dist/react/runtimes/external-message-converter.js +33 -233
  90. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  91. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  92. package/dist/runtime/api/composer-runtime.js.map +1 -1
  93. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  94. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  95. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  96. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  97. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  98. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  99. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  100. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  101. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  102. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  103. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  104. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  105. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  106. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  107. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  108. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  109. package/dist/runtime/internal.d.ts +2 -1
  110. package/dist/runtime/internal.js +2 -1
  111. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  112. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  113. package/dist/runtime/queue/message-queue.js +1 -1
  114. package/dist/runtime/queue/message-queue.js.map +1 -1
  115. package/dist/runtime/queue/queue-item.d.ts +12 -0
  116. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  117. package/dist/runtime/queue/queue-item.js +6 -0
  118. package/dist/runtime/queue/queue-item.js.map +1 -0
  119. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  120. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  121. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  122. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  123. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  124. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  125. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  126. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  127. package/dist/runtime/utils/auto-status.d.ts +6 -1
  128. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  129. package/dist/runtime/utils/auto-status.js +7 -1
  130. package/dist/runtime/utils/auto-status.js.map +1 -1
  131. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  132. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  133. package/dist/runtime/utils/external-message-conversion.js +221 -0
  134. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  135. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  136. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  137. package/dist/runtime/utils/external-store-message.js +2 -1
  138. package/dist/runtime/utils/external-store-message.js.map +1 -1
  139. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  140. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  141. package/dist/runtime/utils/message-repository-session.js +130 -0
  142. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  143. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  144. package/dist/runtime/utils/message-repository.js +30 -22
  145. package/dist/runtime/utils/message-repository.js.map +1 -1
  146. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  147. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  148. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  149. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  150. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  151. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  152. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  153. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  154. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  155. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  156. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  157. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  158. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  159. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +124 -20
  160. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  161. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +102 -0
  162. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -0
  163. package/dist/runtimes/inert/InertThreadRuntimeCore.js +156 -0
  164. package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -0
  165. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  166. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  167. package/dist/runtimes/local/local-thread-runtime-core.js +45 -8
  168. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  169. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +12 -60
  170. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  171. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +10 -145
  172. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  173. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +9 -0
  174. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -0
  175. package/dist/runtimes/remote-thread-list/adapter-changed.js +13 -0
  176. package/dist/runtimes/remote-thread-list/adapter-changed.js.map +1 -0
  177. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  178. package/dist/runtimes/remote-thread-list/empty-thread-core.js +16 -155
  179. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  180. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +2 -0
  181. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  182. package/dist/runtimes/remote-thread-list/optimistic-state.js +11 -0
  183. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  184. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  185. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  186. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  187. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  188. package/dist/runtimes/remote-thread-list/types.d.ts +3 -0
  189. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  190. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  191. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  192. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  193. package/dist/store/clients/external-thread.d.ts.map +1 -1
  194. package/dist/store/clients/external-thread.js +75 -100
  195. package/dist/store/clients/external-thread.js.map +1 -1
  196. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  197. package/dist/store/clients/single-thread-list.js +1 -1
  198. package/dist/store/clients/single-thread-list.js.map +1 -1
  199. package/dist/store/clients/suggestions.d.ts.map +1 -1
  200. package/dist/store/clients/suggestions.js +16 -7
  201. package/dist/store/clients/suggestions.js.map +1 -1
  202. package/dist/store/index.d.ts +4 -2
  203. package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
  204. package/dist/store/runtime-clients/handle-runtime-action.js +2 -0
  205. package/dist/store/runtime-clients/handle-runtime-action.js.map +1 -1
  206. package/dist/store/scope-registration.d.ts +1 -1
  207. package/dist/store/scopes/composer.d.ts +2 -1
  208. package/dist/store/scopes/composer.d.ts.map +1 -1
  209. package/dist/store/scopes/message.d.ts +1 -1
  210. package/dist/store/scopes/queue-item.d.ts +2 -9
  211. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  212. package/dist/store/scopes/queue-item.js +0 -6
  213. package/dist/types/message.d.ts +8 -4
  214. package/dist/types/message.d.ts.map +1 -1
  215. package/dist/types/message.js.map +1 -1
  216. package/dist/utils/data-url.d.ts +14 -1
  217. package/dist/utils/data-url.d.ts.map +1 -1
  218. package/dist/utils/data-url.js +13 -1
  219. package/dist/utils/data-url.js.map +1 -1
  220. package/dist/utils/invoke-user-callback.d.ts +5 -0
  221. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  222. package/dist/utils/invoke-user-callback.js +19 -0
  223. package/dist/utils/invoke-user-callback.js.map +1 -0
  224. package/dist/utils/normalizePartStatus.js.map +1 -1
  225. package/dist/utils/silent-runtime-action.d.ts +6 -0
  226. package/dist/utils/silent-runtime-action.d.ts.map +1 -0
  227. package/dist/utils/silent-runtime-action.js +7 -0
  228. package/dist/utils/silent-runtime-action.js.map +1 -0
  229. package/package.json +10 -10
  230. package/src/adapters/voice.test.ts +66 -1
  231. package/src/adapters/voice.ts +8 -0
  232. package/src/internal.ts +13 -0
  233. package/src/model-context/frame/host.test.ts +152 -9
  234. package/src/model-context/frame/host.ts +23 -5
  235. package/src/model-context/frame/provider.test.ts +601 -19
  236. package/src/model-context/frame/provider.ts +158 -20
  237. package/src/model-context/frame/types.ts +5 -0
  238. package/src/model-context/interactable-composer-metadata.ts +1 -1
  239. package/src/model-context/registry.test.ts +44 -0
  240. package/src/model-context/registry.ts +13 -35
  241. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  242. package/src/react/client/InMemoryThreadList.ts +7 -1
  243. package/src/react/client/Interactables.ts +19 -178
  244. package/src/react/client/RemoteThreadList.test.ts +292 -0
  245. package/src/react/client/RemoteThreadList.ts +119 -82
  246. package/src/react/interactables-legacy/Interactables.ts +20 -177
  247. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  248. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  249. package/src/react/primitive-hooks/useActionBarCopy.test.ts +15 -0
  250. package/src/react/primitive-hooks/useActionBarCopy.ts +13 -3
  251. package/src/react/primitive-hooks/useThreadIsEmpty.ts +3 -0
  252. package/src/react/primitive-hooks/useThreadIsRunning.ts +3 -0
  253. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +12 -0
  254. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.republish.test.ts +87 -0
  255. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +251 -76
  256. package/src/react/runtimes/RemoteThreadResource.ts +2 -0
  257. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  258. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +58 -0
  259. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.ts +3 -1
  260. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  261. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  262. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  263. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  264. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +98 -3
  265. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +68 -6
  266. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  267. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  268. package/src/react/runtimes/external-message-converter.ts +58 -481
  269. package/src/react/runtimes/useLocalRuntime.test.tsx +5 -5
  270. package/src/runtime/api/composer-runtime.ts +1 -1
  271. package/src/runtime/api/message-runtime.test.ts +28 -1
  272. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  273. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  274. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  275. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  276. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  277. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  278. package/src/runtime/internal.ts +5 -0
  279. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  280. package/src/runtime/queue/message-queue.ts +1 -4
  281. package/src/runtime/queue/queue-item.ts +10 -0
  282. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  283. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  284. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  285. package/src/runtime/utils/auto-status.ts +33 -0
  286. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  287. package/src/runtime/utils/external-message-conversion.ts +494 -0
  288. package/src/runtime/utils/external-store-message.ts +2 -0
  289. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  290. package/src/runtime/utils/message-repository-session.ts +176 -0
  291. package/src/runtime/utils/message-repository.ts +33 -22
  292. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  293. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  294. package/src/runtime/utils/thread-message-like.ts +2 -0
  295. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  296. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +179 -28
  297. package/src/runtimes/inert/InertThreadRuntimeCore.test.ts +253 -0
  298. package/src/runtimes/inert/InertThreadRuntimeCore.ts +232 -0
  299. package/src/runtimes/local/local-thread-runtime-core.test.ts +491 -1
  300. package/src/runtimes/local/local-thread-runtime-core.ts +86 -11
  301. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +17 -209
  302. package/src/runtimes/remote-thread-list/adapter-changed.ts +10 -0
  303. package/src/runtimes/remote-thread-list/empty-thread-core.ts +28 -221
  304. package/src/runtimes/remote-thread-list/optimistic-state.test.ts +18 -0
  305. package/src/runtimes/remote-thread-list/optimistic-state.ts +13 -0
  306. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  307. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  308. package/src/runtimes/remote-thread-list/types.ts +4 -0
  309. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  310. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  311. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  312. package/src/store/clients/external-thread.ts +17 -62
  313. package/src/store/clients/single-thread-list.ts +4 -1
  314. package/src/store/clients/suggestions.test.ts +31 -0
  315. package/src/store/clients/suggestions.ts +18 -19
  316. package/src/store/runtime-clients/handle-runtime-action.test.ts +40 -0
  317. package/src/store/runtime-clients/handle-runtime-action.ts +3 -0
  318. package/src/store/scopes/message.ts +1 -1
  319. package/src/store/scopes/queue-item.ts +2 -9
  320. package/src/tests/MessageRepository.test.ts +125 -0
  321. package/src/tests/RemoteThreadListThreadListRuntimeCore-errors.test.ts +2 -2
  322. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  323. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +8 -6
  324. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  325. package/src/tests/empty-thread-core.test.ts +6 -0
  326. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +184 -0
  327. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  328. package/src/tests/external-thread-attachments.test.tsx +173 -0
  329. package/src/tests/external-thread-isEmpty.test.tsx +100 -0
  330. package/src/tests/remote-thread-list-adapter-switch.test.ts +383 -0
  331. package/src/tests/single-thread-list-item.test.tsx +47 -0
  332. package/src/tests/thread-message-like.test.ts +39 -1
  333. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +67 -0
  334. package/src/types/message.ts +9 -4
  335. package/src/utils/data-url.test.ts +52 -1
  336. package/src/utils/data-url.ts +21 -0
  337. package/src/utils/invoke-user-callback.test.ts +82 -0
  338. package/src/utils/invoke-user-callback.ts +31 -0
  339. package/src/utils/normalizePartStatus.ts +1 -1
  340. package/src/utils/silent-runtime-action.ts +6 -0
  341. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -582,3 +582,176 @@ describe("ExternalThread attachments", () => {
582
582
  vi.restoreAllMocks();
583
583
  });
584
584
  });
585
+
586
+ describe("cancelled edit sessions", () => {
587
+ const editSetup = (
588
+ add: (arg: { file: File }) => Promise<PendingAttachment>,
589
+ ) =>
590
+ renderThreadWithProps({
591
+ messages: [
592
+ {
593
+ id: "u1",
594
+ role: "user",
595
+ content: [{ type: "text", text: "hi" }],
596
+ createdAt: new Date(0),
597
+ attachments: [],
598
+ metadata: { custom: {} },
599
+ } as unknown as ExternalThreadMessage,
600
+ ],
601
+ onEdit: () => {},
602
+ attachmentAdapter: {
603
+ accept: "*",
604
+ add,
605
+ send: async () => ({}) as never,
606
+ remove: async () => {},
607
+ },
608
+ });
609
+
610
+ it("does not leak an in-flight add into the next edit session", async () => {
611
+ let resolveAdd!: (attachment: PendingAttachment) => void;
612
+ const aui = editSetup(
613
+ () =>
614
+ new Promise<PendingAttachment>((resolve) => {
615
+ resolveAdd = resolve;
616
+ }),
617
+ );
618
+ const composer = () => aui().thread.message({ id: "u1" }).composer();
619
+
620
+ await act(async () => {
621
+ composer().beginEdit();
622
+ });
623
+ let addPromise!: Promise<void>;
624
+ await act(async () => {
625
+ addPromise = composer().addAttachment(
626
+ new File(["data"], "notes.txt", { type: "text/plain" }),
627
+ );
628
+ });
629
+ await act(async () => {
630
+ composer().cancel();
631
+ });
632
+ await act(async () => {
633
+ composer().beginEdit();
634
+ });
635
+ await act(async () => {
636
+ resolveAdd({
637
+ id: "leak-1",
638
+ type: "document",
639
+ name: "notes.txt",
640
+ contentType: "text/plain",
641
+ file: new File(["data"], "notes.txt", { type: "text/plain" }),
642
+ status: { type: "pending", reason: "uploading", progress: 0 },
643
+ } as PendingAttachment);
644
+ await addPromise.catch(() => {});
645
+ });
646
+
647
+ expect(composer().getState().attachments).toEqual([]);
648
+ });
649
+
650
+ it("adapter-removes the session's pending attachments on cancel, sparing prefilled ones", async () => {
651
+ const remove = vi.fn(async () => {});
652
+ const aui = renderThreadWithProps({
653
+ messages: [
654
+ {
655
+ id: "u1",
656
+ role: "user",
657
+ content: [{ type: "text", text: "hi" }],
658
+ createdAt: new Date(0),
659
+ attachments: [
660
+ {
661
+ id: "prefilled-1",
662
+ type: "document",
663
+ name: "existing.txt",
664
+ contentType: "text/plain",
665
+ content: [],
666
+ status: { type: "complete" },
667
+ } as unknown as CompleteAttachment,
668
+ ],
669
+ metadata: { custom: {} },
670
+ } as unknown as ExternalThreadMessage,
671
+ ],
672
+ onEdit: () => {},
673
+ attachmentAdapter: {
674
+ accept: "*",
675
+ add: async ({ file }: { file: File }) =>
676
+ ({
677
+ id: "pending-1",
678
+ type: "document",
679
+ name: file.name,
680
+ contentType: file.type,
681
+ file,
682
+ status: { type: "pending", reason: "uploading", progress: 0 },
683
+ }) as PendingAttachment,
684
+ send: async () => ({}) as never,
685
+ remove,
686
+ },
687
+ });
688
+ const composer = () => aui().thread.message({ id: "u1" }).composer();
689
+
690
+ await act(async () => {
691
+ composer().beginEdit();
692
+ });
693
+ await act(() =>
694
+ composer().addAttachment(
695
+ new File(["data"], "notes.txt", { type: "text/plain" }),
696
+ ),
697
+ );
698
+ expect(composer().getState().attachments).toHaveLength(2);
699
+
700
+ await act(async () => {
701
+ composer().cancel();
702
+ });
703
+
704
+ await waitFor(() => expect(remove).toHaveBeenCalledTimes(1));
705
+ expect(remove).toHaveBeenCalledWith(
706
+ expect.objectContaining({ id: "pending-1" }),
707
+ );
708
+
709
+ await act(async () => {
710
+ composer().cancel();
711
+ });
712
+ expect(remove).toHaveBeenCalledTimes(1);
713
+ });
714
+
715
+ it("keeps the thread composer's in-flight add across a run cancel", async () => {
716
+ let resolveAdd!: (attachment: PendingAttachment) => void;
717
+ const aui = renderThreadWithProps({
718
+ onNew: () => {},
719
+ onCancel: () => {},
720
+ attachmentAdapter: {
721
+ accept: "*",
722
+ add: () =>
723
+ new Promise<PendingAttachment>((resolve) => {
724
+ resolveAdd = resolve;
725
+ }),
726
+ send: async () => ({}) as never,
727
+ remove: async () => {},
728
+ },
729
+ });
730
+ const composer = () => aui().thread().composer();
731
+
732
+ let addPromise!: Promise<void>;
733
+ await act(async () => {
734
+ addPromise = composer().addAttachment(
735
+ new File(["data"], "notes.txt", { type: "text/plain" }),
736
+ );
737
+ });
738
+ await act(async () => {
739
+ composer().cancel();
740
+ });
741
+ await act(async () => {
742
+ resolveAdd({
743
+ id: "draft-1",
744
+ type: "document",
745
+ name: "notes.txt",
746
+ contentType: "text/plain",
747
+ file: new File(["data"], "notes.txt", { type: "text/plain" }),
748
+ status: { type: "pending", reason: "uploading", progress: 0 },
749
+ } as PendingAttachment);
750
+ await addPromise;
751
+ });
752
+
753
+ expect(composer().getState().attachments).toMatchObject([
754
+ { id: "draft-1" },
755
+ ]);
756
+ });
757
+ });
@@ -0,0 +1,100 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { render } from "@testing-library/react";
4
+ import type { FC } from "react";
5
+ import { describe, it, expect } from "vitest";
6
+ import { AuiIf, useAui, AuiProvider } from "@assistant-ui/store";
7
+ import {
8
+ ExternalThread,
9
+ type ExternalThreadProps,
10
+ } from "../store/clients/external-thread";
11
+ import { useThreadIsEmpty } from "../react/primitive-hooks/useThreadIsEmpty";
12
+
13
+ const HookProbe: FC = () => (
14
+ <div data-testid="hook">{useThreadIsEmpty() ? "empty" : "not-empty"}</div>
15
+ );
16
+
17
+ const renderThread = (props: ExternalThreadProps) => {
18
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
19
+ const Capture: FC = () => {
20
+ captured.aui = useAui();
21
+ return null;
22
+ };
23
+ const App: FC<{ threadProps: ExternalThreadProps }> = ({ threadProps }) => {
24
+ const aui = useAui({ thread: ExternalThread(threadProps) });
25
+ return (
26
+ <AuiProvider value={aui}>
27
+ <Capture />
28
+ <HookProbe />
29
+ <AuiIf condition={(s) => s.thread.isEmpty}>
30
+ <div data-testid="aui-if">empty</div>
31
+ </AuiIf>
32
+ </AuiProvider>
33
+ );
34
+ };
35
+ const utils = render(<App threadProps={props} />);
36
+ return {
37
+ state: () => captured.aui!.thread.getState(),
38
+ hookSaysEmpty: () => utils.getByTestId("hook").textContent === "empty",
39
+ auiIfRendered: () => utils.queryByTestId("aui-if") !== null,
40
+ rerender: (next: ExternalThreadProps) =>
41
+ utils.rerender(<App threadProps={next} />),
42
+ };
43
+ };
44
+
45
+ const userMessage = (id: string) =>
46
+ ({
47
+ id,
48
+ role: "user",
49
+ content: [{ type: "text", text: "hi" }],
50
+ createdAt: new Date(0),
51
+ metadata: { custom: {} },
52
+ }) as unknown as ExternalThreadProps["messages"][number];
53
+
54
+ describe("ExternalThread isEmpty", () => {
55
+ it("is empty when there are no messages and nothing is loading", () => {
56
+ const t = renderThread({ messages: [] });
57
+ expect(t.state().isEmpty).toBe(true);
58
+ expect(t.hookSaysEmpty()).toBe(true);
59
+ expect(t.auiIfRendered()).toBe(true);
60
+ });
61
+
62
+ it("is not empty while the adapter is still loading", () => {
63
+ const t = renderThread({ messages: [], isLoading: true });
64
+ expect(t.state().isEmpty).toBe(false);
65
+ });
66
+
67
+ it("does not render empty-state consumers while loading", () => {
68
+ const t = renderThread({ messages: [], isLoading: true });
69
+ expect(t.hookSaysEmpty()).toBe(false);
70
+ expect(t.auiIfRendered()).toBe(false);
71
+ });
72
+
73
+ it("is not empty once messages arrive", () => {
74
+ const t = renderThread({ messages: [userMessage("m1")] });
75
+ expect(t.state().isEmpty).toBe(false);
76
+ });
77
+
78
+ it("becomes empty when loading finishes with no messages", () => {
79
+ const t = renderThread({ messages: [], isLoading: true });
80
+ expect(t.state().isEmpty).toBe(false);
81
+ expect(t.auiIfRendered()).toBe(false);
82
+
83
+ t.rerender({ messages: [], isLoading: false });
84
+
85
+ expect(t.state().isEmpty).toBe(true);
86
+ expect(t.hookSaysEmpty()).toBe(true);
87
+ expect(t.auiIfRendered()).toBe(true);
88
+ });
89
+
90
+ it("stays non-empty when loading finishes with messages", () => {
91
+ const t = renderThread({ messages: [], isLoading: true });
92
+ expect(t.state().isEmpty).toBe(false);
93
+
94
+ t.rerender({ messages: [userMessage("m1")], isLoading: false });
95
+
96
+ expect(t.state().isEmpty).toBe(false);
97
+ expect(t.hookSaysEmpty()).toBe(false);
98
+ expect(t.auiIfRendered()).toBe(false);
99
+ });
100
+ });
@@ -0,0 +1,383 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createCore,
4
+ deferred,
5
+ makeAdapter,
6
+ setStartThreadRuntime,
7
+ } from "./remote-thread-list-test-helpers";
8
+
9
+ const thread = (remoteId: string) => ({
10
+ remoteId,
11
+ externalId: remoteId,
12
+ status: "regular" as const,
13
+ title: remoteId,
14
+ });
15
+
16
+ describe("RemoteThreadList adapter changes", () => {
17
+ it("clears the selected thread and cached data from the previous adapter", async () => {
18
+ const adapterA = makeAdapter({
19
+ list: async () => ({ threads: [thread("thread-a")] }),
20
+ });
21
+ const listAdapterB = vi.fn(async () => ({
22
+ threads: [thread("thread-b")],
23
+ }));
24
+ const adapterB = makeAdapter({ list: listAdapterB });
25
+ const core = createCore(adapterA);
26
+ const started: string[] = [];
27
+ setStartThreadRuntime(core, async (id) => {
28
+ started.push(id);
29
+ return {};
30
+ });
31
+
32
+ await core.getLoadThreadsPromise();
33
+ await core.switchToThread("thread-a");
34
+ expect(core.getItemById(core.mainThreadId)?.remoteId).toBe("thread-a");
35
+
36
+ core.__internal_setOptions({
37
+ adapter: adapterB,
38
+ runtimeHook: () => ({}) as never,
39
+ });
40
+
41
+ await core.getLoadThreadsPromise();
42
+ expect(listAdapterB).toHaveBeenCalledTimes(1);
43
+ expect(core.threadIds).toEqual(["thread-b"]);
44
+ expect(core.getItemById("thread-a")).toBeUndefined();
45
+ expect(core.getItemById(core.mainThreadId)?.remoteId).not.toBe("thread-a");
46
+ expect(started).toContain(core.mainThreadId);
47
+
48
+ await expect(core.rename("thread-a", "leaked")).rejects.toThrow(
49
+ 'Thread "thread-a" not found',
50
+ );
51
+ expect(adapterB.rename).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it("does not resume an old adapter mutation through the new adapter", async () => {
55
+ const adapterA = makeAdapter({
56
+ list: async () => ({ threads: [thread("thread-a")] }),
57
+ });
58
+ const adapterB = makeAdapter();
59
+ const core = createCore(adapterA);
60
+
61
+ await core.getLoadThreadsPromise();
62
+ await core.switchToThread("thread-a");
63
+
64
+ const runtimeStart = deferred<unknown>();
65
+ setStartThreadRuntime(core, () => runtimeStart.promise);
66
+ const archiveTask = core.archive("thread-a");
67
+
68
+ core.__internal_setOptions({
69
+ adapter: adapterB,
70
+ runtimeHook: () => ({}) as never,
71
+ });
72
+ runtimeStart.resolve({});
73
+
74
+ await expect(archiveTask).rejects.toThrow("adapter changed");
75
+ expect(adapterA.archive).not.toHaveBeenCalled();
76
+ expect(adapterB.archive).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it("preserves an adapter failure that races an adapter change", async () => {
80
+ const unarchiveRequest = deferred<void>();
81
+ const adapterA = makeAdapter({
82
+ list: async () => ({
83
+ threads: [{ ...thread("thread-a"), status: "archived" as const }],
84
+ }),
85
+ unarchive: vi.fn(() => unarchiveRequest.promise),
86
+ });
87
+ const adapterB = makeAdapter();
88
+ const core = createCore(adapterA);
89
+
90
+ await core.getLoadThreadsPromise();
91
+ const unarchiveTask = core.unarchive("thread-a");
92
+ await vi.waitFor(() => expect(adapterA.unarchive).toHaveBeenCalledOnce());
93
+
94
+ core.__internal_setOptions({
95
+ adapter: adapterB,
96
+ runtimeHook: () => ({}) as never,
97
+ });
98
+ const failure = new Error("network error");
99
+ unarchiveRequest.reject(failure);
100
+
101
+ await expect(unarchiveTask).rejects.toBe(failure);
102
+ });
103
+
104
+ it("does not apply a late initialize to the replacement adapter state", async () => {
105
+ const initializeRequest = deferred<{
106
+ remoteId: string;
107
+ externalId: string;
108
+ }>();
109
+ const adapterA = makeAdapter({
110
+ list: async () => ({ threads: [] }),
111
+ initialize: vi.fn(() => initializeRequest.promise),
112
+ });
113
+ const adapterB = makeAdapter();
114
+ const core = createCore(adapterA);
115
+
116
+ await core.getLoadThreadsPromise();
117
+ const localId = core.newThreadId;
118
+ expect(localId).toBeDefined();
119
+ const initializeTask = core.initialize(localId!);
120
+
121
+ core.__internal_setOptions({
122
+ adapter: adapterB,
123
+ runtimeHook: () => ({}) as never,
124
+ });
125
+ initializeRequest.resolve({
126
+ remoteId: "leaked",
127
+ externalId: "leaked",
128
+ });
129
+ await expect(initializeTask).rejects.toThrow("adapter changed");
130
+ expect(core.getItemById("leaked")).toBeUndefined();
131
+ expect(core.threadIds).not.toContain("leaked");
132
+
133
+ await core.getLoadThreadsPromise();
134
+ expect(core.getItemById(core.mainThreadId)?.status).toBe("new");
135
+ expect(core.getItemById(core.mainThreadId)?.remoteId).toBeUndefined();
136
+ await expect(core.initialize(core.mainThreadId)).resolves.toEqual({
137
+ remoteId: core.mainThreadId,
138
+ externalId: core.mainThreadId,
139
+ });
140
+ });
141
+
142
+ it("selects a controlled thread that arrives on the replacement page", async () => {
143
+ const adapterA = makeAdapter({
144
+ list: async () => ({ threads: [thread("thread-a")] }),
145
+ });
146
+ const adapterB = makeAdapter({
147
+ list: async () => ({ threads: [thread("thread-b")] }),
148
+ });
149
+ const core = createCore(adapterA, "thread-a");
150
+
151
+ await core.getLoadThreadsPromise();
152
+ core.__internal_load();
153
+ await core.switchToThread("thread-a");
154
+
155
+ core.__internal_setOptions({
156
+ adapter: adapterB,
157
+ runtimeHook: () => ({}) as never,
158
+ threadId: "thread-b",
159
+ });
160
+ await core.getLoadThreadsPromise();
161
+
162
+ expect(core.getItemById(core.mainThreadId)?.remoteId).toBe("thread-b");
163
+ });
164
+
165
+ it("keeps the unsent draft runtime across an empty-list adapter swap", async () => {
166
+ const adapterA = makeAdapter();
167
+ const adapterB = makeAdapter();
168
+ const core = createCore(adapterA);
169
+ const stopped: string[] = [];
170
+ const hookManager = (
171
+ core as unknown as {
172
+ _hookManager: { stopThreadRuntime: (id: string) => void };
173
+ }
174
+ )._hookManager;
175
+ const originalStop = hookManager.stopThreadRuntime.bind(hookManager);
176
+ hookManager.stopThreadRuntime = (id: string) => {
177
+ stopped.push(id);
178
+ originalStop(id);
179
+ };
180
+
181
+ await core.getLoadThreadsPromise();
182
+ const draftId = core.mainThreadId;
183
+ expect(core.getItemById(draftId)?.status).toBe("new");
184
+
185
+ core.__internal_setOptions({
186
+ adapter: adapterB,
187
+ runtimeHook: () => ({}) as never,
188
+ });
189
+ await core.getLoadThreadsPromise();
190
+
191
+ expect(core.mainThreadId).toBe(draftId);
192
+ expect(core.getItemById(draftId)?.status).toBe("new");
193
+ expect(stopped).not.toContain(draftId);
194
+ });
195
+
196
+ it("stops runtimes for threads missing from the replacement list", async () => {
197
+ const adapterA = makeAdapter({
198
+ list: async () => ({ threads: [thread("thread-a")] }),
199
+ });
200
+ const adapterB = makeAdapter({
201
+ list: async () => ({ threads: [thread("thread-b")] }),
202
+ });
203
+ const core = createCore(adapterA);
204
+ const stopped: string[] = [];
205
+ const hookManager = (
206
+ core as unknown as {
207
+ _hookManager: { stopThreadRuntime: (id: string) => void };
208
+ }
209
+ )._hookManager;
210
+ const originalStop = hookManager.stopThreadRuntime.bind(hookManager);
211
+ hookManager.stopThreadRuntime = (id: string) => {
212
+ stopped.push(id);
213
+ originalStop(id);
214
+ };
215
+
216
+ await core.getLoadThreadsPromise();
217
+ await core.switchToThread("thread-a");
218
+
219
+ core.__internal_setOptions({
220
+ adapter: adapterB,
221
+ runtimeHook: () => ({}) as never,
222
+ });
223
+ await core.getLoadThreadsPromise();
224
+
225
+ expect(core.getItemById("thread-a")).toBeUndefined();
226
+ expect(stopped).toContain("thread-a");
227
+ expect(stopped).not.toContain(core.mainThreadId);
228
+ });
229
+
230
+ it("does not reject when a controlled thread is missing from the replacement adapter", async () => {
231
+ const adapterA = makeAdapter({
232
+ list: async () => ({ threads: [thread("thread-a")] }),
233
+ });
234
+ const adapterB = makeAdapter({
235
+ list: async () => ({ threads: [thread("thread-b")] }),
236
+ fetch: vi.fn(async () => {
237
+ throw new Error("not found");
238
+ }),
239
+ });
240
+ const core = createCore(adapterA, "thread-a");
241
+
242
+ await core.getLoadThreadsPromise();
243
+ await core.switchToThread("thread-a");
244
+
245
+ core.__internal_setOptions({
246
+ adapter: adapterB,
247
+ runtimeHook: () => ({}) as never,
248
+ threadId: "thread-a",
249
+ });
250
+ await expect(core.getLoadThreadsPromise()).resolves.toBeUndefined();
251
+ await vi.waitFor(() => {
252
+ expect(adapterB.fetch).toHaveBeenCalledWith("thread-a");
253
+ });
254
+ expect(core.getItemById("thread-a")).toBeUndefined();
255
+ expect(core.getItemById(core.mainThreadId)?.status).toBe("new");
256
+ });
257
+
258
+ it("reuses the preserved unsent draft when the selected thread is missing after swap", async () => {
259
+ const adapterA = makeAdapter({
260
+ list: async () => ({ threads: [thread("thread-a")] }),
261
+ });
262
+ const adapterB = makeAdapter({
263
+ list: async () => ({ threads: [thread("thread-b")] }),
264
+ });
265
+ const core = createCore(adapterA);
266
+
267
+ await core.getLoadThreadsPromise();
268
+ const draftId = core.newThreadId;
269
+ expect(draftId).toBeDefined();
270
+ await core.switchToThread("thread-a");
271
+ expect(core.mainThreadId).toBe("thread-a");
272
+ expect(core.newThreadId).toBe(draftId);
273
+
274
+ core.__internal_setOptions({
275
+ adapter: adapterB,
276
+ runtimeHook: () => ({}) as never,
277
+ });
278
+ await core.getLoadThreadsPromise();
279
+
280
+ expect(core.mainThreadId).toBe(draftId);
281
+ expect(core.newThreadId).toBe(draftId);
282
+ expect(core.getItemById(draftId!)?.status).toBe("new");
283
+ expect(
284
+ Object.values(core.threadItems).filter((item) => item.status === "new"),
285
+ ).toHaveLength(1);
286
+ });
287
+
288
+ it("does not send a rename through the replacement adapter while its list is pending", async () => {
289
+ const adapterA = makeAdapter({
290
+ list: async () => ({ threads: [thread("thread-a")] }),
291
+ });
292
+ const listB = deferred<{ threads: ReturnType<typeof thread>[] }>();
293
+ const adapterB = makeAdapter({
294
+ list: vi.fn(() => listB.promise),
295
+ });
296
+ const core = createCore(adapterA);
297
+
298
+ await core.getLoadThreadsPromise();
299
+ await core.switchToThread("thread-a");
300
+
301
+ core.__internal_setOptions({
302
+ adapter: adapterB,
303
+ runtimeHook: () => ({}) as never,
304
+ });
305
+ await expect(core.rename("thread-a", "leaked")).rejects.toThrow(
306
+ "adapter changed",
307
+ );
308
+ await expect(core.switchToThread("thread-a")).rejects.toThrow(
309
+ "adapter changed",
310
+ );
311
+ expect(adapterB.rename).not.toHaveBeenCalled();
312
+
313
+ listB.resolve({ threads: [thread("thread-b")] });
314
+ await core.getLoadThreadsPromise();
315
+ expect(adapterB.rename).not.toHaveBeenCalled();
316
+ expect(core.getItemById("thread-a")).toBeUndefined();
317
+ });
318
+
319
+ it("does not freeze mutations when the replacement list fails", async () => {
320
+ const adapterA = makeAdapter({
321
+ list: async () => ({ threads: [thread("thread-a")] }),
322
+ });
323
+ const adapterB = makeAdapter({
324
+ list: vi.fn(async () => {
325
+ throw new Error("network");
326
+ }),
327
+ });
328
+ const core = createCore(adapterA);
329
+ const consoleError = vi
330
+ .spyOn(console, "error")
331
+ .mockImplementation(() => {});
332
+
333
+ await core.getLoadThreadsPromise();
334
+ await core.switchToThread("thread-a");
335
+
336
+ core.__internal_setOptions({
337
+ adapter: adapterB,
338
+ runtimeHook: () => ({}) as never,
339
+ });
340
+ await core.getLoadThreadsPromise();
341
+
342
+ expect(core.getItemById("thread-a")).toBeUndefined();
343
+ expect(core.getItemById(core.mainThreadId)?.status).toBe("new");
344
+ await expect(core.switchToNewThread()).resolves.toBeUndefined();
345
+ expect(consoleError).toHaveBeenCalled();
346
+ consoleError.mockRestore();
347
+ });
348
+
349
+ it("keeps a thread initialized while the replacement list is pending", async () => {
350
+ const adapterA = makeAdapter({
351
+ list: async () => ({ threads: [thread("thread-a")] }),
352
+ });
353
+ const listB = deferred<{ threads: ReturnType<typeof thread>[] }>();
354
+ const adapterB = makeAdapter({
355
+ list: vi.fn(() => listB.promise),
356
+ initialize: vi.fn(async () => ({
357
+ remoteId: "created-on-b",
358
+ externalId: "created-on-b",
359
+ })),
360
+ });
361
+ const core = createCore(adapterA);
362
+
363
+ await core.getLoadThreadsPromise();
364
+ const draftId = core.newThreadId;
365
+ expect(draftId).toBeDefined();
366
+
367
+ core.__internal_setOptions({
368
+ adapter: adapterB,
369
+ runtimeHook: () => ({}) as never,
370
+ });
371
+ await expect(core.initialize(draftId!)).resolves.toEqual({
372
+ remoteId: "created-on-b",
373
+ externalId: "created-on-b",
374
+ });
375
+
376
+ listB.resolve({ threads: [thread("thread-b")] });
377
+ await core.getLoadThreadsPromise();
378
+
379
+ expect(core.getItemById(draftId!)?.remoteId).toBe("created-on-b");
380
+ expect(core.mainThreadId).toBe(draftId);
381
+ expect(core.threadIds).toContain("created-on-b");
382
+ });
383
+ });
@@ -0,0 +1,47 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, render } from "@testing-library/react";
4
+ import type { FC } from "react";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { AuiProvider, useAui } from "@assistant-ui/store";
7
+ import { ExternalThread } from "../store/clients/external-thread";
8
+ import { SingleThreadList } from "../store/clients/single-thread-list";
9
+
10
+ let aui!: ReturnType<typeof useAui>;
11
+
12
+ const Capture: FC = () => {
13
+ aui = useAui();
14
+ return null;
15
+ };
16
+
17
+ const App: FC = () => {
18
+ const value = useAui({
19
+ threads: SingleThreadList({ thread: ExternalThread({ messages: [] }) }),
20
+ });
21
+ return (
22
+ <AuiProvider value={value}>
23
+ <Capture />
24
+ </AuiProvider>
25
+ );
26
+ };
27
+
28
+ afterEach(() => {
29
+ cleanup();
30
+ });
31
+
32
+ describe("SingleThreadList item selectors", () => {
33
+ it("resolves index selectors within the regular subset only", () => {
34
+ render(<App />);
35
+
36
+ const mainId = aui.threads.item("main").getState().id;
37
+ expect(aui.threads.item({ index: 0 }).getState().id).toBe(mainId);
38
+ expect(aui.threads.item({ index: 0, archived: false }).getState().id).toBe(
39
+ mainId,
40
+ );
41
+
42
+ expect(aui.threads.getState().archivedThreadIds).toEqual([]);
43
+ expect(() => aui.threads.item({ index: 0, archived: true })).toThrow(
44
+ "unknown item selector",
45
+ );
46
+ });
47
+ });