@assistant-ui/core 0.3.16 → 0.3.17

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 (305) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +2 -2
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal.d.ts +2 -1
  5. package/dist/internal.js +2 -1
  6. package/dist/model-context/frame/provider.d.ts +3 -0
  7. package/dist/model-context/frame/provider.d.ts.map +1 -1
  8. package/dist/model-context/frame/provider.js +71 -21
  9. package/dist/model-context/frame/provider.js.map +1 -1
  10. package/dist/model-context/tool-call-text.d.ts +26 -0
  11. package/dist/model-context/tool-call-text.d.ts.map +1 -0
  12. package/dist/model-context/tool-call-text.js +18 -0
  13. package/dist/model-context/tool-call-text.js.map +1 -0
  14. package/dist/react/RuntimeAdapter.js +1 -1
  15. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  16. package/dist/react/adapters/LocalStorageThreadListAdapter.js +1 -1
  17. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  18. package/dist/react/client/AdaptedRemoteThread.js +1 -1
  19. package/dist/react/client/InMemoryThreadList.js +1 -1
  20. package/dist/react/client/Interactables.js +1 -1
  21. package/dist/react/client/RemoteThreadList.d.ts +15 -0
  22. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  23. package/dist/react/client/RemoteThreadList.js +240 -138
  24. package/dist/react/client/RemoteThreadList.js.map +1 -1
  25. package/dist/react/client/Tools.d.ts.map +1 -1
  26. package/dist/react/client/Tools.js +6 -2
  27. package/dist/react/client/Tools.js.map +1 -1
  28. package/dist/react/index.d.ts +2 -1
  29. package/dist/react/index.js +2 -1
  30. package/dist/react/interactables-legacy/Interactables.js +4 -4
  31. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  32. package/dist/react/model-context/toolbox.d.ts +2 -14
  33. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  34. package/dist/react/model-context/toolbox.js +1 -13
  35. package/dist/react/model-context/toolbox.js.map +1 -1
  36. package/dist/react/model-context/useAssistantContext.js +37 -7
  37. package/dist/react/model-context/useAssistantContext.js.map +1 -1
  38. package/dist/react/model-context/useAuiToolOverrides.js +31 -5
  39. package/dist/react/model-context/useAuiToolOverrides.js.map +1 -1
  40. package/dist/react/model-context/useInlineRender.d.ts.map +1 -1
  41. package/dist/react/model-context/useInlineRender.js +10 -36
  42. package/dist/react/model-context/useInlineRender.js.map +1 -1
  43. package/dist/react/primitive-hooks/useMessageError.d.ts.map +1 -1
  44. package/dist/react/primitive-hooks/useMessageError.js +2 -8
  45. package/dist/react/primitive-hooks/useMessageError.js.map +1 -1
  46. package/dist/react/primitive-hooks/useThreadListLoadMore.d.ts.map +1 -1
  47. package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -4
  48. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  49. package/dist/react/primitives/composer/ComposerAttachments.d.ts.map +1 -1
  50. package/dist/react/primitives/composer/ComposerAttachments.js +4 -3
  51. package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
  52. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  53. package/dist/react/primitives/composer/ComposerQueue.js +4 -3
  54. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
  55. package/dist/react/primitives/message/MessageAttachments.d.ts.map +1 -1
  56. package/dist/react/primitives/message/MessageAttachments.js +7 -6
  57. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  58. package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
  59. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  60. package/dist/react/primitives/message/MessageParts.js +3 -3
  61. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  62. package/dist/react/primitives/threadList/ThreadListItems.d.ts.map +1 -1
  63. package/dist/react/primitives/threadList/ThreadListItems.js +5 -4
  64. package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
  65. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +6 -4
  66. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  67. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +44 -22
  68. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  69. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +6 -5
  70. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  71. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +79 -62
  72. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  73. package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -6
  74. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  75. package/dist/react/runtimes/RemoteThreadResource.js +3 -3
  76. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  77. package/dist/react/runtimes/assistant-transport/useLatestRef.js +2 -2
  78. package/dist/react/runtimes/assistant-transport/useLatestRef.js.map +1 -1
  79. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  80. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +23 -18
  81. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  82. package/dist/react/runtimes/cloud/auiV0.d.ts +5 -1
  83. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  84. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  85. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +29 -0
  86. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -0
  87. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +159 -0
  88. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -0
  89. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts +1 -9
  90. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  91. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +16 -153
  92. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  93. package/dist/react/runtimes/external-message-converter.js +1 -1
  94. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  95. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  96. package/dist/react/runtimes/useRemoteThreadListRuntime.js +14 -6
  97. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  98. package/dist/react/types/MessagePartComponentTypes.d.ts +7 -3
  99. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  100. package/dist/react/types/scopes/tools.d.ts +2 -0
  101. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  102. package/dist/runtime/api/message-part-runtime.d.ts +2 -2
  103. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  104. package/dist/runtime/api/message-part-runtime.js +1 -1
  105. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  106. package/dist/runtime/api/thread-list-runtime.d.ts +9 -1
  107. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  108. package/dist/runtime/api/thread-list-runtime.js +5 -0
  109. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  110. package/dist/runtime/api/thread-runtime.d.ts +1 -1
  111. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -2
  112. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  113. package/dist/runtime/base/base-thread-runtime-core.js +2 -2
  114. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  115. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +14 -2
  116. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  117. package/dist/runtime/interfaces/thread-runtime-core.d.ts +15 -1
  118. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  119. package/dist/runtime/utils/auto-status.d.ts +1 -1
  120. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  121. package/dist/runtime/utils/auto-status.js +6 -2
  122. package/dist/runtime/utils/auto-status.js.map +1 -1
  123. package/dist/runtime/utils/external-message-conversion.d.ts +3 -2
  124. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
  125. package/dist/runtime/utils/external-message-conversion.js +4 -3
  126. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  127. package/dist/runtime/utils/resolveToolApprovalResponse.d.ts +6 -3
  128. package/dist/runtime/utils/resolveToolApprovalResponse.d.ts.map +1 -1
  129. package/dist/runtime/utils/resolveToolApprovalResponse.js +12 -3
  130. package/dist/runtime/utils/resolveToolApprovalResponse.js.map +1 -1
  131. package/dist/runtime/utils/thread-message-like.d.ts +2 -10
  132. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  133. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  134. package/dist/runtime/utils/tool-call-tree.d.ts +39 -0
  135. package/dist/runtime/utils/tool-call-tree.d.ts.map +1 -0
  136. package/dist/runtime/utils/tool-call-tree.js +85 -0
  137. package/dist/runtime/utils/tool-call-tree.js.map +1 -0
  138. package/dist/runtimes/external-store/external-store-adapter.d.ts +29 -2
  139. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  140. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -1
  141. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  142. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +34 -28
  143. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  144. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +1 -1
  145. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -1
  146. package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -1
  147. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
  148. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  149. package/dist/runtimes/local/local-thread-runtime-core.js +3 -1
  150. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  151. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +1 -1
  152. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  153. package/dist/runtimes/remote-thread-list/remote-thread-state.js +30 -13
  154. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  155. package/dist/runtimes/remote-thread-list/title.d.ts +9 -0
  156. package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -0
  157. package/dist/runtimes/remote-thread-list/title.js +6 -0
  158. package/dist/runtimes/remote-thread-list/title.js.map +1 -0
  159. package/dist/runtimes/remote-thread-list/types.d.ts +6 -5
  160. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  161. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +31 -8
  162. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  163. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +97 -60
  164. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  165. package/dist/store/clients/external-thread.d.ts +1 -1
  166. package/dist/store/clients/external-thread.d.ts.map +1 -1
  167. package/dist/store/clients/external-thread.js +11 -4
  168. package/dist/store/clients/external-thread.js.map +1 -1
  169. package/dist/store/clients/single-thread-list.js +1 -1
  170. package/dist/store/clients/suggestions.d.ts.map +1 -1
  171. package/dist/store/clients/suggestions.js +18 -6
  172. package/dist/store/clients/suggestions.js.map +1 -1
  173. package/dist/store/clients/thread-message-client.js +1 -1
  174. package/dist/store/clients/thread-selection-events.js +1 -1
  175. package/dist/store/index.d.ts +2 -1
  176. package/dist/store/index.js +2 -1
  177. package/dist/store/internal.d.ts +2 -2
  178. package/dist/store/internal.js +2 -2
  179. package/dist/store/primitive-predicates.d.ts +4 -1
  180. package/dist/store/primitive-predicates.d.ts.map +1 -1
  181. package/dist/store/primitive-predicates.js +9 -1
  182. package/dist/store/primitive-predicates.js.map +1 -1
  183. package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
  184. package/dist/store/runtime-clients/message-runtime-client.js +1 -1
  185. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  186. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  187. package/dist/store/runtime-clients/thread-list-runtime-client.js +124 -105
  188. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  189. package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
  190. package/dist/store/runtime-clients/useSubscribable.d.ts +3 -1
  191. package/dist/store/runtime-clients/useSubscribable.d.ts.map +1 -1
  192. package/dist/store/runtime-clients/useSubscribable.js +1 -1
  193. package/dist/store/runtime-clients/useSubscribable.js.map +1 -1
  194. package/dist/store/scopes/part.d.ts +6 -3
  195. package/dist/store/scopes/part.d.ts.map +1 -1
  196. package/dist/subscribable/subscribable.d.ts +8 -1
  197. package/dist/subscribable/subscribable.d.ts.map +1 -1
  198. package/dist/subscribable/subscribable.js +24 -16
  199. package/dist/subscribable/subscribable.js.map +1 -1
  200. package/dist/types/message.d.ts +40 -1
  201. package/dist/types/message.d.ts.map +1 -1
  202. package/dist/types/message.js +7 -1
  203. package/dist/types/message.js.map +1 -1
  204. package/package.json +12 -17
  205. package/src/index.ts +6 -1
  206. package/src/internal.ts +8 -0
  207. package/src/model-context/frame/provider.test.ts +217 -1
  208. package/src/model-context/frame/provider.ts +111 -23
  209. package/src/model-context/tool-call-text.test.ts +67 -0
  210. package/src/model-context/tool-call-text.ts +67 -0
  211. package/src/react/AssistantRuntimeProvider.test.tsx +66 -2
  212. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +1 -3
  213. package/src/react/client/RemoteThreadList.background.test.ts +384 -0
  214. package/src/react/client/RemoteThreadList.concurrent.test.tsx +274 -0
  215. package/src/react/client/RemoteThreadList.test.ts +273 -1
  216. package/src/react/client/RemoteThreadList.ts +293 -83
  217. package/src/react/client/Tools.ts +7 -1
  218. package/src/react/index.ts +4 -0
  219. package/src/react/model-context/modelContextHooks.test.tsx +131 -0
  220. package/src/react/model-context/toolbox.test.ts +9 -9
  221. package/src/react/model-context/toolbox.ts +9 -36
  222. package/src/react/model-context/useAssistantContext.ts +4 -2
  223. package/src/react/model-context/useAuiToolOverrides.ts +4 -2
  224. package/src/react/model-context/useInlineRender.test.tsx +48 -0
  225. package/src/react/model-context/useInlineRender.ts +14 -10
  226. package/src/react/primitive-hooks/useMessageError.ts +2 -19
  227. package/src/react/primitive-hooks/useThreadListLoadMore.ts +2 -3
  228. package/src/react/primitives/composer/ComposerAttachments.test.tsx +76 -0
  229. package/src/react/primitives/composer/ComposerAttachments.tsx +9 -4
  230. package/src/react/primitives/composer/ComposerQueue.test.tsx +72 -0
  231. package/src/react/primitives/composer/ComposerQueue.tsx +7 -4
  232. package/src/react/primitives/message/MessageAttachments.test.tsx +107 -37
  233. package/src/react/primitives/message/MessageAttachments.tsx +10 -7
  234. package/src/react/primitives/message/MessageGroupedParts.tsx +2 -2
  235. package/src/react/primitives/message/MessageParts.tsx +3 -3
  236. package/src/react/primitives/threadList/ThreadListItems.test.tsx +99 -0
  237. package/src/react/primitives/threadList/ThreadListItems.tsx +8 -5
  238. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +94 -1
  239. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +72 -31
  240. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +169 -0
  241. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +114 -68
  242. package/src/react/runtimes/RemoteThreadResource.ts +1 -4
  243. package/src/react/runtimes/assistant-transport/transport-scheduling.test.ts +51 -2
  244. package/src/react/runtimes/assistant-transport/useLatestRef.ts +2 -2
  245. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +45 -0
  246. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +65 -1
  247. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +42 -35
  248. package/src/react/runtimes/cloud/auiV0.ts +5 -0
  249. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +88 -0
  250. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +236 -0
  251. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +90 -1
  252. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +33 -238
  253. package/src/react/runtimes/external-message-converter.ts +1 -0
  254. package/src/react/runtimes/useRemoteThreadListRuntime.concurrent.test.tsx +216 -0
  255. package/src/react/runtimes/useRemoteThreadListRuntime.ts +24 -6
  256. package/src/react/types/MessagePartComponentTypes.ts +7 -3
  257. package/src/react/types/scopes/tools.ts +2 -0
  258. package/src/runtime/api/message-part-runtime.ts +3 -3
  259. package/src/runtime/api/thread-list-runtime.ts +25 -1
  260. package/src/runtime/base/base-thread-runtime-core.test.ts +94 -3
  261. package/src/runtime/base/base-thread-runtime-core.ts +10 -4
  262. package/src/runtime/interfaces/thread-list-runtime-core.ts +20 -1
  263. package/src/runtime/interfaces/thread-runtime-core.ts +17 -1
  264. package/src/runtime/utils/auto-status.test.ts +57 -0
  265. package/src/runtime/utils/auto-status.ts +13 -1
  266. package/src/runtime/utils/external-message-conversion.test.ts +97 -0
  267. package/src/runtime/utils/external-message-conversion.ts +17 -1
  268. package/src/runtime/utils/resolveToolApprovalResponse.test.ts +73 -0
  269. package/src/runtime/utils/resolveToolApprovalResponse.ts +27 -6
  270. package/src/runtime/utils/thread-message-like.ts +2 -10
  271. package/src/runtime/utils/tool-call-tree.test.ts +191 -0
  272. package/src/runtime/utils/tool-call-tree.ts +97 -0
  273. package/src/runtimes/external-store/external-store-adapter.ts +38 -2
  274. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +56 -33
  275. package/src/runtimes/inert/InertThreadRuntimeCore.ts +1 -1
  276. package/src/runtimes/local/local-thread-runtime-core.test.ts +29 -3
  277. package/src/runtimes/local/local-thread-runtime-core.ts +5 -1
  278. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +112 -0
  279. package/src/runtimes/remote-thread-list/remote-thread-state.ts +55 -16
  280. package/src/runtimes/remote-thread-list/title.ts +3 -0
  281. package/src/runtimes/remote-thread-list/types.ts +6 -5
  282. package/src/runtimes/tool-invocations/EDGE_CASES.md +110 -5
  283. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +750 -136
  284. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +144 -91
  285. package/src/store/clients/external-thread.ts +14 -8
  286. package/src/store/clients/suggestions.test.ts +155 -1
  287. package/src/store/clients/suggestions.ts +42 -34
  288. package/src/store/index.ts +4 -0
  289. package/src/store/internal.ts +2 -0
  290. package/src/store/primitive-predicates.ts +35 -0
  291. package/src/store/runtime-clients/thread-list-runtime-client.ts +16 -2
  292. package/src/store/runtime-clients/useSubscribable.ts +8 -2
  293. package/src/store/scopes/part.ts +6 -3
  294. package/src/subscribable/subscribable.test.ts +50 -0
  295. package/src/subscribable/subscribable.ts +33 -21
  296. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +111 -0
  297. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +78 -0
  298. package/src/tests/RemoteThreadListThreadListRuntimeCore-reload.test.ts +52 -0
  299. package/src/tests/external-store-optimistic-id.test.ts +79 -0
  300. package/src/tests/external-store-repository-instance.test.ts +166 -0
  301. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +22 -16
  302. package/src/tests/external-thread-attachments.test.tsx +85 -1
  303. package/src/tests/remote-thread-list-adapter-switch.test.ts +66 -1
  304. package/src/tests/thread-switch-events.test.tsx +397 -2
  305. package/src/types/message.ts +50 -2
@@ -2,6 +2,7 @@ import { useMemo } from "react";
2
2
  import { resource, withKey } from "@assistant-ui/tap";
3
3
  import type { ClientOutput } from "@assistant-ui/store";
4
4
  import { useClientLookup } from "@assistant-ui/store/client";
5
+ import { shallowEqual } from "@assistant-ui/store/internal";
5
6
  import type { SuggestionsState } from "../scopes/suggestions";
6
7
  import type { SuggestionState } from "../scopes/suggestion";
7
8
  import type { ThreadSuggestion } from "../../runtime/interfaces/thread-runtime-core";
@@ -10,6 +11,31 @@ export type SuggestionConfig =
10
11
  | string
11
12
  | { title: string; label: string; prompt: string };
12
13
 
14
+ const useStableSuggestionsState = (
15
+ next: SuggestionsState,
16
+ ): SuggestionsState => {
17
+ const cell = useMemo(() => ({}) as { state?: SuggestionsState }, []);
18
+ const previous = cell.state;
19
+
20
+ const suggestions = next.suggestions.map((suggestion, index) => {
21
+ const previousSuggestion = previous?.suggestions[index];
22
+ return previousSuggestion && shallowEqual(previousSuggestion, suggestion)
23
+ ? previousSuggestion
24
+ : suggestion;
25
+ });
26
+ const state =
27
+ previous &&
28
+ previous.suggestions.length === suggestions.length &&
29
+ suggestions.every(
30
+ (suggestion, index) => suggestion === previous.suggestions[index],
31
+ )
32
+ ? previous
33
+ : { suggestions };
34
+
35
+ cell.state = state;
36
+ return state;
37
+ };
38
+
13
39
  const useSuggestionClient = (
14
40
  state: SuggestionState,
15
41
  ): ClientOutput<"suggestion"> => {
@@ -21,8 +47,9 @@ const useSuggestionClient = (
21
47
  const SuggestionClient = resource(useSuggestionClient);
22
48
 
23
49
  const useSuggestionsClient = (
24
- state: SuggestionsState,
50
+ nextState: SuggestionsState,
25
51
  ): ClientOutput<"suggestions"> => {
52
+ const state = useStableSuggestionsState(nextState);
26
53
  const suggestionClients = useClientLookup(
27
54
  state.suggestions.map((suggestion, index) =>
28
55
  withKey(index, SuggestionClient(suggestion), [suggestion]),
@@ -40,27 +67,13 @@ const useSuggestionsClient = (
40
67
  const useStaticSuggestions = (
41
68
  suggestions?: SuggestionConfig[],
42
69
  ): ClientOutput<"suggestions"> => {
43
- const state = useMemo<SuggestionsState>(
44
- () => ({
45
- suggestions: (suggestions ?? []).map((s) => {
46
- if (typeof s === "string") {
47
- return {
48
- title: s,
49
- label: "",
50
- prompt: s,
51
- };
52
- }
53
- return {
54
- title: s.title,
55
- label: s.label,
56
- prompt: s.prompt,
57
- };
58
- }),
59
- }),
60
- [suggestions],
61
- );
62
-
63
- return useSuggestionsClient(state);
70
+ return useSuggestionsClient({
71
+ suggestions: (suggestions ?? []).map((s) =>
72
+ typeof s === "string"
73
+ ? { title: s, label: "", prompt: s }
74
+ : { title: s.title, label: s.label, prompt: s.prompt },
75
+ ),
76
+ });
64
77
  };
65
78
 
66
79
  export const Suggestions = resource(useStaticSuggestions);
@@ -68,18 +81,13 @@ export const Suggestions = resource(useStaticSuggestions);
68
81
  const useThreadSuggestions = (
69
82
  suggestions: readonly ThreadSuggestion[],
70
83
  ): ClientOutput<"suggestions"> => {
71
- const state = useMemo<SuggestionsState>(
72
- () => ({
73
- suggestions: suggestions.map((s) => ({
74
- title: s.title ?? s.prompt,
75
- label: s.label ?? "",
76
- prompt: s.prompt,
77
- })),
78
- }),
79
- [suggestions],
80
- );
81
-
82
- return useSuggestionsClient(state);
84
+ return useSuggestionsClient({
85
+ suggestions: suggestions.map((s) => ({
86
+ title: s.title ?? s.prompt,
87
+ label: s.label ?? "",
88
+ prompt: s.prompt,
89
+ })),
90
+ });
83
91
  };
84
92
 
85
93
  export const ThreadSuggestions = resource(useThreadSuggestions);
@@ -108,6 +108,10 @@ export {
108
108
  createRuntimeExtrasBrand,
109
109
  type RuntimeExtrasBrand,
110
110
  } from "../runtime/utils/runtime-extras-brand";
111
+ export {
112
+ resolveToolCallText,
113
+ type ToolCallText,
114
+ } from "../model-context/tool-call-text";
111
115
  export { defineToolkit } from "../react/model-context/define-toolkit";
112
116
  export {
113
117
  defineMcpToolkit,
@@ -15,7 +15,9 @@ export {
15
15
  composerCancelDisabled,
16
16
  composerInputDisabled,
17
17
  composerSendDisabled,
18
+ messageErrorText,
18
19
  suggestionTriggerDisabled,
20
+ threadListLoadMoreDisabled,
19
21
  } from "./primitive-predicates";
20
22
  export { isDevelopment } from "./env";
21
23
  export { useThreadSelectionEvents } from "./clients/thread-selection-events";
@@ -1,3 +1,7 @@
1
+ import type {
2
+ ReadonlyJSONArray,
3
+ ReadonlyJSONObject,
4
+ } from "assistant-stream/utils";
1
5
  import type { AssistantState } from "@assistant-ui/store";
2
6
 
3
7
  /**
@@ -41,3 +45,34 @@ export const suggestionTriggerDisabled = (
41
45
  ): boolean =>
42
46
  s.thread.isDisabled ||
43
47
  (send && s.thread.isRunning && !s.thread.capabilities.queue);
48
+
49
+ export const messageErrorText = (
50
+ s: AssistantState,
51
+ ):
52
+ | string
53
+ | number
54
+ | boolean
55
+ | ReadonlyJSONObject
56
+ | ReadonlyJSONArray
57
+ | undefined => {
58
+ if (
59
+ s.message.status?.type !== "incomplete" ||
60
+ s.message.status.reason !== "error"
61
+ ) {
62
+ return undefined;
63
+ }
64
+ const error = s.message.status.error;
65
+ if (typeof error === "string") return error;
66
+ if (
67
+ typeof error === "object" &&
68
+ error !== null &&
69
+ "message" in error &&
70
+ typeof error.message === "string"
71
+ ) {
72
+ return error.message;
73
+ }
74
+ return error ?? "An error occurred";
75
+ };
76
+
77
+ export const threadListLoadMoreDisabled = (s: AssistantState): boolean =>
78
+ !s.threads.hasMore || s.threads.isLoading || s.threads.isLoadingMore;
@@ -1,7 +1,11 @@
1
- import { useMemo } from "react";
1
+ import { useEffect, useMemo } from "react";
2
2
  import { useResource, withKey, resource } from "@assistant-ui/tap";
3
3
  import type { ClientOutput } from "@assistant-ui/store";
4
- import { useClientLookup, useClientResource } from "@assistant-ui/store/client";
4
+ import {
5
+ useAssistantEmit,
6
+ useClientLookup,
7
+ useClientResource,
8
+ } from "@assistant-ui/store/client";
5
9
  import { useThreadSelectionEvents } from "../clients/thread-selection-events";
6
10
  import type { ThreadListRuntime } from "../../runtime/api/thread-list-runtime";
7
11
  import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
@@ -44,6 +48,16 @@ const useThreadListClient = ({
44
48
  const runtimeState = useSubscribable(runtime);
45
49
  useThreadSelectionEvents(runtimeState.mainThreadId);
46
50
 
51
+ const emit = useAssistantEmit();
52
+ useEffect(
53
+ () =>
54
+ runtime.unstable_subscribeThreadEvents(({ threadId, type }) => {
55
+ if (threadId === runtime.getState().mainThreadId) return;
56
+ emit(`thread.${type}`, { threadId });
57
+ }),
58
+ [runtime, emit],
59
+ );
60
+
47
61
  const main = useClientResource(
48
62
  ThreadClient({
49
63
  runtime: runtime.main,
@@ -2,7 +2,13 @@ import { useSyncExternalStore } from "react";
2
2
  import type { SubscribableWithState } from "../../subscribable/subscribable";
3
3
 
4
4
  export const useSubscribable = <T>(
5
- subscribable: Omit<SubscribableWithState<T, any>, "path">,
5
+ subscribable: Omit<SubscribableWithState<T, any>, "path"> & {
6
+ getServerSnapshot?: () => T;
7
+ },
6
8
  ) => {
7
- return useSyncExternalStore(subscribable.subscribe, subscribable.getState);
9
+ return useSyncExternalStore(
10
+ subscribable.subscribe,
11
+ subscribable.getState,
12
+ subscribable.getServerSnapshot,
13
+ );
8
14
  };
@@ -29,10 +29,13 @@ export type PartMethods = {
29
29
  resumeToolCall(payload: unknown): void;
30
30
  /**
31
31
  * Respond to a server-side tool approval gate. The approval id is read from
32
- * the part. Accepts a boolean decision or the id of one of the approval's
33
- * options.
32
+ * the part. Accepts a boolean decision, the id of one of the approval's
33
+ * options, or a free-form answer when the request asks for one.
34
+ *
35
+ * Resolves once the runtime has accepted the response, and rejects when it
36
+ * could not be recorded, so a renderer can leave its controls retryable.
34
37
  */
35
- respondToToolApproval(response: ToolApprovalResponse): void;
38
+ respondToToolApproval(response: ToolApprovalResponse): Promise<void>;
36
39
  __internal_getRuntime?(): MessagePartRuntime;
37
40
  };
38
41
 
@@ -32,6 +32,30 @@ const createBinding = (initialState: TestState) => {
32
32
  };
33
33
  };
34
34
 
35
+ // Mirrors runtime bindings such as `getThreadListState`, which assemble a new
36
+ // object on every read while the underlying values keep their identity.
37
+ const createRebuildingBinding = (initialState: TestState) => {
38
+ let state = initialState;
39
+ const subscribers = new Set<() => void>();
40
+
41
+ const binding: SubscribableWithState<TestState, null> = {
42
+ path: null,
43
+ getState: () => ({ ...state }),
44
+ subscribe: (callback) => {
45
+ subscribers.add(callback);
46
+ return () => subscribers.delete(callback);
47
+ },
48
+ };
49
+
50
+ return {
51
+ binding,
52
+ update(nextState: TestState) {
53
+ state = nextState;
54
+ for (const callback of subscribers) callback();
55
+ },
56
+ };
57
+ };
58
+
35
59
  describe("ShallowMemoizeSubject", () => {
36
60
  it("notifies subscribers when a state key is removed", () => {
37
61
  const source = createBinding({
@@ -131,4 +155,30 @@ describe("LazyMemoizeSubject", () => {
131
155
  expect(subject.getState()).toEqual({ status: "ready" });
132
156
  reconnect();
133
157
  });
158
+
159
+ it("keeps one reference while unconnected and the state is unchanged", () => {
160
+ // A consumer that reads without subscribing (React's useSyncExternalStore
161
+ // calls getSnapshot outside the subscription) must not see a new object on
162
+ // every call: React compares snapshots with Object.is, so a fresh reference
163
+ // reads as a change and re-renders forever.
164
+ const source = createRebuildingBinding({ status: "ready" });
165
+ const subject = new LazyMemoizeSubject(source.binding);
166
+
167
+ const first = subject.getState();
168
+
169
+ expect(subject.getState()).toBe(first);
170
+ expect(subject.getState()).toBe(first);
171
+ });
172
+
173
+ it("returns a new reference once the unchanged state actually changes", () => {
174
+ const source = createRebuildingBinding({ status: "ready" });
175
+ const subject = new LazyMemoizeSubject(source.binding);
176
+
177
+ const first = subject.getState();
178
+ source.update({ status: "done" });
179
+
180
+ const second = subject.getState();
181
+ expect(second).not.toBe(first);
182
+ expect(second).toEqual({ status: "done" });
183
+ });
134
184
  });
@@ -1,3 +1,4 @@
1
+ import { shallowEqual } from "@assistant-ui/store/client";
1
2
  import type { Unsubscribe } from "../types/unsubscribe";
2
3
  import { notifyEventListeners } from "../utils/notify-event-listeners";
3
4
 
@@ -57,25 +58,10 @@ export const notifySubscribers = <TArgs extends unknown[]>(
57
58
  }
58
59
  };
59
60
 
60
- function shallowEqual<T extends object>(
61
- objA: T | undefined,
62
- objB: T | undefined,
63
- ) {
64
- if (objA === undefined && objB === undefined) return true;
65
- if (objA === undefined) return false;
66
- if (objB === undefined) return false;
67
-
68
- const keysA = Object.keys(objA);
69
- if (keysA.length !== Object.keys(objB).length) return false;
70
-
71
- for (const key of keysA) {
72
- const valueA = objA[key as keyof T];
73
- const valueB = objB[key as keyof T];
74
- if (!Object.is(valueA, valueB)) return false;
75
- }
76
-
77
- return true;
78
- }
61
+ const shallowEqualOrUndefined = <T extends object>(
62
+ a: T | undefined,
63
+ b: T | undefined,
64
+ ) => (a === undefined || b === undefined ? a === b : shallowEqual(a, b));
79
65
 
80
66
  export class BaseSubscribable {
81
67
  private _subscribers = new Set<() => void>();
@@ -99,6 +85,32 @@ export class BaseSubscribable {
99
85
  }
100
86
  }
101
87
 
88
+ export class WritableSubscribable<TState> extends BaseSubscribable {
89
+ private _state: TState;
90
+
91
+ constructor(state: TState) {
92
+ super();
93
+ this._state = state;
94
+ this.subscribe = this.subscribe.bind(this);
95
+ this.getState = this.getState.bind(this);
96
+ // Hydration has to agree with what the server rendered, so the server
97
+ // snapshot stays at the creation-time state rather than following writes.
98
+ this.getServerSnapshot = () => state;
99
+ }
100
+
101
+ public getState(): TState {
102
+ return this._state;
103
+ }
104
+
105
+ public getServerSnapshot: () => TState;
106
+
107
+ public setState(state: TState): void {
108
+ if (Object.is(state, this._state)) return;
109
+ this._state = state;
110
+ this._notifySubscribers();
111
+ }
112
+ }
113
+
102
114
  // lazy connect/disconnect: only opens upstream subscription while it has subscribers
103
115
  export abstract class BaseSubject {
104
116
  private _subscriptions = new Set<(payload?: unknown) => void>();
@@ -168,7 +180,7 @@ export class ShallowMemoizeSubject<TState extends object, TPath>
168
180
  private _syncState() {
169
181
  const state = this.binding.getState();
170
182
  if (state === SKIP_UPDATE) return false;
171
- if (shallowEqual(state, this._previousState)) return false;
183
+ if (shallowEqualOrUndefined(state, this._previousState)) return false;
172
184
  this._previousState = state;
173
185
  return true;
174
186
  }
@@ -209,7 +221,7 @@ export class LazyMemoizeSubject<TState extends object, TPath>
209
221
  if (
210
222
  newState !== SKIP_UPDATE &&
211
223
  (this._previousState === undefined ||
212
- !shallowEqual(newState, this._previousState))
224
+ !shallowEqualOrUndefined(newState, this._previousState))
213
225
  ) {
214
226
  this._previousState = newState;
215
227
  }
@@ -36,6 +36,117 @@ describe("RemoteThreadListThreadListRuntimeCore load race", () => {
36
36
  expect(core.threadIds[0]).toBe(newId);
37
37
  expect(core.threadIds).toContain("t1");
38
38
  });
39
+
40
+ it("keeps one slot when the list reports the thread just initialized", async () => {
41
+ const listDeferred = deferred<ListResult>();
42
+ const adapter = makeAdapter({
43
+ list: vi.fn(() => listDeferred.promise),
44
+ initialize: vi.fn(async () => ({
45
+ remoteId: "remote-1",
46
+ externalId: "remote-1",
47
+ })),
48
+ });
49
+ const core = createCore(adapter);
50
+
51
+ const loadPromise = core.getLoadThreadsPromise();
52
+ await core.switchToNewThread();
53
+ const localId = core.newThreadId!;
54
+ await core.initialize(localId);
55
+
56
+ listDeferred.resolve({
57
+ threads: [
58
+ { status: "regular", remoteId: "remote-1", externalId: "remote-1" },
59
+ ],
60
+ });
61
+ await loadPromise;
62
+
63
+ expect(Object.keys(core.threadItems)).toEqual([localId]);
64
+ expect(core.threadIds).toEqual([localId]);
65
+ expect(core.getItemById("remote-1")?.id).toBe(localId);
66
+ expect(core.mainThreadId).toBe(localId);
67
+
68
+ await core.delete("remote-1");
69
+
70
+ expect(core.getItemById(localId)).toBeUndefined();
71
+ expect(core.getItemById("remote-1")).toBeUndefined();
72
+ expect(core.threadIds).toEqual([]);
73
+ });
74
+
75
+ it("collapses a slot the list minted while initialize was in flight", async () => {
76
+ const listDeferred = deferred<ListResult>();
77
+ const initializeDeferred = deferred<{
78
+ remoteId: string;
79
+ externalId: string;
80
+ }>();
81
+ const adapter = makeAdapter({
82
+ list: vi.fn(() => listDeferred.promise),
83
+ initialize: vi.fn(() => initializeDeferred.promise),
84
+ });
85
+ const core = createCore(adapter);
86
+
87
+ const loadPromise = core.getLoadThreadsPromise();
88
+ await core.switchToNewThread();
89
+ const localId = core.newThreadId!;
90
+ const initializePromise = core.initialize(localId);
91
+
92
+ listDeferred.resolve({
93
+ threads: [
94
+ { status: "regular", remoteId: "remote-1", externalId: "remote-1" },
95
+ ],
96
+ });
97
+ await loadPromise;
98
+
99
+ initializeDeferred.resolve({
100
+ remoteId: "remote-1",
101
+ externalId: "remote-1",
102
+ });
103
+ await initializePromise;
104
+
105
+ expect(Object.keys(core.threadItems)).toEqual([localId]);
106
+ expect(core.threadIds).toEqual([localId]);
107
+ expect(core.getItemById("remote-1")?.id).toBe(localId);
108
+
109
+ await core.delete("remote-1");
110
+
111
+ expect(core.getItemById(localId)).toBeUndefined();
112
+ expect(core.getItemById("remote-1")).toBeUndefined();
113
+ expect(core.threadIds).toEqual([]);
114
+ });
115
+
116
+ it("does not leave the collapsed slot in both lists when the race reported it archived", async () => {
117
+ const listDeferred = deferred<ListResult>();
118
+ const initializeDeferred = deferred<{
119
+ remoteId: string;
120
+ externalId: string;
121
+ }>();
122
+ const adapter = makeAdapter({
123
+ list: vi.fn(() => listDeferred.promise),
124
+ initialize: vi.fn(() => initializeDeferred.promise),
125
+ });
126
+ const core = createCore(adapter);
127
+
128
+ const loadPromise = core.getLoadThreadsPromise();
129
+ await core.switchToNewThread();
130
+ const localId = core.newThreadId!;
131
+ const initializePromise = core.initialize(localId);
132
+
133
+ listDeferred.resolve({
134
+ threads: [
135
+ { status: "archived", remoteId: "remote-1", externalId: "remote-1" },
136
+ ],
137
+ });
138
+ await loadPromise;
139
+
140
+ initializeDeferred.resolve({
141
+ remoteId: "remote-1",
142
+ externalId: "remote-1",
143
+ });
144
+ await initializePromise;
145
+
146
+ expect(Object.keys(core.threadItems)).toEqual([localId]);
147
+ expect(core.threadIds).toEqual([localId]);
148
+ expect(core.archivedThreadIds).toEqual([]);
149
+ });
39
150
  });
40
151
 
41
152
  describe("preserveMidLoadTransitions", () => {
@@ -195,6 +195,30 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
195
195
  expect(core.hasMore).toBe(false);
196
196
  });
197
197
 
198
+ it("clears loadingMore when the superseded request never settles", async () => {
199
+ const listFn = vi
200
+ .fn<ListFn>()
201
+ .mockResolvedValueOnce({
202
+ threads: [{ status: "regular", remoteId: "a", externalId: "a" }],
203
+ nextCursor: "c1",
204
+ })
205
+ .mockReturnValueOnce(new Promise<never>(() => {}))
206
+ .mockResolvedValueOnce({
207
+ threads: [
208
+ { status: "regular", remoteId: "fresh", externalId: "fresh" },
209
+ ],
210
+ });
211
+ const adapter = makeAdapter({ list: listFn });
212
+ const core = createCore(adapter);
213
+
214
+ await core.getLoadThreadsPromise();
215
+ void core.loadMore();
216
+ await core.reload();
217
+
218
+ expect(core.threadIds).toEqual(["fresh"]);
219
+ expect(core.isLoadingMore).toBe(false);
220
+ });
221
+
198
222
  it("releases the dedup handle after a rejection so retries can proceed", async () => {
199
223
  vi.spyOn(console, "error").mockImplementation(() => {});
200
224
  const listFn = vi
@@ -241,6 +265,60 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
241
265
  expect(core.threadIds).toEqual(["a", "b"]);
242
266
  });
243
267
 
268
+ it("refreshes a thread repeated on a later page instead of dropping it", async () => {
269
+ const listFn = vi
270
+ .fn<ListFn>()
271
+ .mockResolvedValueOnce({
272
+ threads: [
273
+ { status: "regular", remoteId: "a", externalId: "a", title: "old" },
274
+ ],
275
+ nextCursor: "c1",
276
+ })
277
+ .mockResolvedValueOnce({
278
+ threads: [
279
+ {
280
+ status: "regular",
281
+ remoteId: "a",
282
+ externalId: "a",
283
+ title: "updated",
284
+ },
285
+ { status: "regular", remoteId: "b", externalId: "b", title: "New" },
286
+ ],
287
+ });
288
+ const core = createCore(makeAdapter({ list: listFn }));
289
+
290
+ await core.getLoadThreadsPromise();
291
+ await core.loadMore();
292
+
293
+ expect(core.threadIds).toEqual(["a", "b"]);
294
+ expect(core.getItemById("a")?.title).toBe("updated");
295
+ });
296
+
297
+ it("moves a thread whose status flips between pages", async () => {
298
+ const listFn = vi
299
+ .fn<ListFn>()
300
+ .mockResolvedValueOnce({
301
+ threads: [
302
+ { status: "regular", remoteId: "a", externalId: "a", title: "A" },
303
+ { status: "regular", remoteId: "b", externalId: "b", title: "B" },
304
+ ],
305
+ nextCursor: "c1",
306
+ })
307
+ .mockResolvedValueOnce({
308
+ threads: [
309
+ { status: "archived", remoteId: "a", externalId: "a", title: "A" },
310
+ ],
311
+ });
312
+ const core = createCore(makeAdapter({ list: listFn }));
313
+
314
+ await core.getLoadThreadsPromise();
315
+ await core.loadMore();
316
+
317
+ expect(core.threadIds).toEqual(["b"]);
318
+ expect(core.archivedThreadIds).toEqual(["a"]);
319
+ expect(core.getItemById("a")?.status).toBe("archived");
320
+ });
321
+
244
322
  it("__internal_setOptions clears cursor and dedup handles on adapter swap, then refetches via the new adapter", async () => {
245
323
  const firstAdapter = makeAdapter({
246
324
  list: vi.fn<ListFn>(async () => ({
@@ -95,6 +95,58 @@ describe("RemoteThreadListThreadListRuntimeCore.reload", () => {
95
95
  expect(core.threadIds).not.toContain("stale");
96
96
  });
97
97
 
98
+ it("clears loading when the superseded request never settles", async () => {
99
+ const listFn = vi
100
+ .fn<() => Promise<RemoteThreadListResponse>>()
101
+ .mockReturnValueOnce(new Promise<never>(() => {}))
102
+ .mockResolvedValueOnce({
103
+ threads: [
104
+ {
105
+ status: "regular",
106
+ remoteId: "fresh",
107
+ externalId: "fresh",
108
+ title: "Fresh",
109
+ },
110
+ ],
111
+ });
112
+ const adapter = makeAdapter({ list: listFn });
113
+ const core = createCore(adapter);
114
+
115
+ void core.getLoadThreadsPromise();
116
+ await core.reload();
117
+
118
+ expect(core.threadIds).toEqual(["fresh"]);
119
+ expect(core.isLoading).toBe(false);
120
+ });
121
+
122
+ it("clears loading once the fresh list arrives, before the superseded request settles", async () => {
123
+ const first = deferred<RemoteThreadListResponse>();
124
+ const listFn = vi
125
+ .fn<() => Promise<RemoteThreadListResponse>>()
126
+ .mockReturnValueOnce(first.promise)
127
+ .mockResolvedValueOnce({
128
+ threads: [
129
+ {
130
+ status: "regular",
131
+ remoteId: "fresh",
132
+ externalId: "fresh",
133
+ title: "Fresh",
134
+ },
135
+ ],
136
+ });
137
+ const core = createCore(makeAdapter({ list: listFn }));
138
+
139
+ void core.getLoadThreadsPromise();
140
+ await core.reload();
141
+
142
+ expect(core.threadIds).toEqual(["fresh"]);
143
+ expect(core.isLoading).toBe(false);
144
+
145
+ first.resolve({ threads: [] });
146
+ await Promise.resolve();
147
+ expect(core.isLoading).toBe(false);
148
+ });
149
+
98
150
  it("recovers after a failed initial load", async () => {
99
151
  vi.spyOn(console, "error").mockImplementation(() => {});
100
152
  const listFn = vi