@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
@@ -10,17 +10,27 @@ import {
10
10
  type ComponentType,
11
11
  Fragment,
12
12
  } from "react";
13
- import { create } from "zustand";
14
13
  import { useResources, withKey } from "@assistant-ui/tap";
15
14
  import type { AssistantClient } from "@assistant-ui/store";
16
15
  import { ThreadListItemRuntimeProvider } from "../providers/ThreadListItemRuntimeProvider";
17
- import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
18
- import type { ThreadListRuntimeCore } from "../../runtime/interfaces/thread-list-runtime-core";
16
+ import type {
17
+ ThreadRuntimeCore,
18
+ ThreadRuntimeEventType,
19
+ } from "../../runtime/interfaces/thread-runtime-core";
20
+ import type {
21
+ ThreadListRuntimeCore,
22
+ ThreadListRuntimeEvent,
23
+ } from "../../runtime/interfaces/thread-list-runtime-core";
19
24
  import type { Unsubscribe } from "../../types/unsubscribe";
20
- import { BaseSubscribable } from "../../subscribable/subscribable";
25
+ import {
26
+ BaseSubscribable,
27
+ WritableSubscribable,
28
+ } from "../../subscribable/subscribable";
29
+ import { useSubscribable } from "../../store/runtime-clients/useSubscribable";
21
30
  import { getThreadRuntimeCoreIsRunning } from "../../runtime/api/thread-runtime";
22
31
  import { ThreadListRuntimeImpl } from "../../runtime/api/thread-list-runtime";
23
32
  import { invalidateThreadRuntime } from "../../runtime/utils/thread-runtime-lifecycle";
33
+ import { notifyEventListeners } from "../../utils/notify-event-listeners";
24
34
  import {
25
35
  useRuntimeAdapters,
26
36
  type RuntimeAdapters,
@@ -30,6 +40,13 @@ import {
30
40
  type RemoteThreadListHook,
31
41
  } from "./RemoteThreadResource";
32
42
 
43
+ const THREAD_EVENTS = [
44
+ "runStart",
45
+ "runEnd",
46
+ "initialize",
47
+ "modelContextUpdate",
48
+ ] as const satisfies readonly ThreadRuntimeEventType[];
49
+
33
50
  type RemoteThreadListHookInstance = {
34
51
  runtime?: ThreadRuntimeCore | undefined;
35
52
  publishedGeneration?: number | undefined;
@@ -59,14 +76,14 @@ const ProviderRenderDetector: FC<{
59
76
 
60
77
  export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
61
78
  private runtimeHook: RemoteThreadListHook;
62
- private readonly adapterStore = create<AdapterSnapshot>(() => ({
79
+ private readonly adapterStore = new WritableSubscribable<AdapterSnapshot>({
63
80
  defaultAdapters: null,
64
81
  threadAdapters: new Map(),
65
- }));
66
- private readonly hostStore = create<HostSnapshot>(() => ({
82
+ });
83
+ private readonly hostStore = new WritableSubscribable<HostSnapshot>({
67
84
  threads: [],
68
85
  hookEpoch: 0,
69
- }));
86
+ });
70
87
  private readonly pendingThreadAdapters = new Map<
71
88
  string,
72
89
  RuntimeAdapters | null
@@ -148,6 +165,17 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
148
165
  return () => this.runningSubscribers.delete(callback);
149
166
  }
150
167
 
168
+ private threadEventSubscribers = new Set<
169
+ (event: ThreadListRuntimeEvent) => void
170
+ >();
171
+
172
+ public __internal_subscribeThreadEvents(
173
+ callback: (event: ThreadListRuntimeEvent) => void,
174
+ ): Unsubscribe {
175
+ this.threadEventSubscribers.add(callback);
176
+ return () => this.threadEventSubscribers.delete(callback);
177
+ }
178
+
151
179
  private _publish = (
152
180
  threadId: string,
153
181
  runtime: ThreadRuntimeCore,
@@ -170,7 +198,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
170
198
  instance.runtime = runtime;
171
199
  instance.publishedGeneration = generation;
172
200
  if (previousRuntime !== runtime) {
173
- this._trackRunning(instance);
201
+ this._trackRunning(threadId, instance);
174
202
  }
175
203
  this._notifySubscribers();
176
204
  if (previousRuntime !== undefined && previousRuntime !== runtime) {
@@ -187,7 +215,10 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
187
215
  return () => this.replacedSubscribers.delete(callback);
188
216
  }
189
217
 
190
- private _trackRunning(instance: RemoteThreadListHookInstance) {
218
+ private _trackRunning(
219
+ threadId: string,
220
+ instance: RemoteThreadListHookInstance,
221
+ ) {
191
222
  instance.unsubscribeRunning?.();
192
223
 
193
224
  const runtime = instance.runtime;
@@ -198,9 +229,23 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
198
229
  }
199
230
 
200
231
  this._setRunning(instance, getThreadRuntimeCoreIsRunning(runtime));
201
- instance.unsubscribeRunning = runtime.subscribe(() => {
202
- this._setRunning(instance, getThreadRuntimeCoreIsRunning(runtime));
203
- });
232
+ const unsubscribers = [
233
+ runtime.subscribe(() => {
234
+ this._setRunning(instance, getThreadRuntimeCoreIsRunning(runtime));
235
+ }),
236
+ ...THREAD_EVENTS.map((type) =>
237
+ runtime.unstable_on(type, () => {
238
+ notifyEventListeners(
239
+ this.threadEventSubscribers,
240
+ { threadId, type },
241
+ `Thread event "${type}"`,
242
+ );
243
+ }),
244
+ ),
245
+ ];
246
+ instance.unsubscribeRunning = () => {
247
+ for (const unsubscribe of unsubscribers) unsubscribe();
248
+ };
204
249
  }
205
250
 
206
251
  private _setRunning(
@@ -225,16 +270,14 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
225
270
  public setRuntimeHook(newRuntimeHook: RemoteThreadListHook) {
226
271
  if (this.runtimeHook === newRuntimeHook) return;
227
272
  this.runtimeHook = newRuntimeHook;
228
- this.hostStore.setState(
229
- (state) => ({ ...state, hookEpoch: state.hookEpoch + 1 }),
230
- true,
231
- );
273
+ const host = this.hostStore.getState();
274
+ this.hostStore.setState({ ...host, hookEpoch: host.hookEpoch + 1 });
232
275
  }
233
276
 
234
277
  public __internal_setDefaultAdapters(adapters: RuntimeAdapters | null) {
235
278
  const current = this.adapterStore.getState();
236
279
  if (current.defaultAdapters === adapters) return;
237
- this.adapterStore.setState({ ...current, defaultAdapters: adapters }, true);
280
+ this.adapterStore.setState({ ...current, defaultAdapters: adapters });
238
281
  }
239
282
 
240
283
  public __internal_setThreadAdapters(
@@ -245,7 +288,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
245
288
  const next = new Map(current.threadAdapters);
246
289
  if (adapters === null) next.delete(threadId);
247
290
  else next.set(threadId, adapters);
248
- this.adapterStore.setState({ ...current, threadAdapters: next }, true);
291
+ this.adapterStore.setState({ ...current, threadAdapters: next });
249
292
  }
250
293
 
251
294
  public __internal_dispose() {
@@ -255,20 +298,18 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
255
298
  }
256
299
 
257
300
  private _syncHostThreads() {
258
- this.hostStore.setState(
259
- (state) => ({
260
- ...state,
261
- threads: Array.from(this.instances.entries()).map(
262
- ([id, { generation }]) => ({ id, generation }),
263
- ),
264
- }),
265
- true,
266
- );
301
+ const host = this.hostStore.getState();
302
+ this.hostStore.setState({
303
+ ...host,
304
+ threads: Array.from(this.instances.entries()).map(
305
+ ([id, { generation }]) => ({ id, generation }),
306
+ ),
307
+ });
267
308
  }
268
309
 
269
310
  public __internal_useHost(parentClient: AssistantClient) {
270
- const { threads, hookEpoch } = this.hostStore();
271
- const adapters = this.adapterStore();
311
+ const { threads, hookEpoch } = useSubscribable(this.hostStore);
312
+ const adapters = useSubscribable(this.adapterStore);
272
313
  const runtimeHook = this.runtimeHook;
273
314
  return useResources(
274
315
  threads.map(({ id, generation }) => {
@@ -294,7 +335,7 @@ export class RemoteThreadListHookInstanceManager extends BaseSubscribable {
294
335
  public __internal_RenderThreadRuntimes: FC<{
295
336
  provider: ComponentType<PropsWithChildren>;
296
337
  }> = ({ provider }) => {
297
- this.hostStore();
338
+ useSubscribable(this.hostStore);
298
339
 
299
340
  return Array.from(this.instances.entries()).map(
300
341
  ([threadId, { generation }]) => (
@@ -0,0 +1,169 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { RemoteThreadListThreadListRuntimeCore } from "./RemoteThreadListThreadListRuntimeCore";
3
+ import {
4
+ contextProvider,
5
+ createCore,
6
+ deferred,
7
+ makeAdapter,
8
+ } from "../../tests/remote-thread-list-test-helpers";
9
+
10
+ describe("RemoteThreadListThreadListRuntimeCore switch/delete ordering", () => {
11
+ it("rejects when a thread is deleted before its runtime attaches", async () => {
12
+ const adapter = makeAdapter({
13
+ list: vi.fn(async () => ({
14
+ threads: [
15
+ {
16
+ status: "archived" as const,
17
+ remoteId: "thread-b",
18
+ externalId: "thread-b",
19
+ title: "Thread B",
20
+ },
21
+ ],
22
+ })),
23
+ });
24
+ const core = new RemoteThreadListThreadListRuntimeCore(
25
+ { adapter, runtimeHook: () => ({}) as never },
26
+ contextProvider,
27
+ );
28
+ await core.getLoadThreadsPromise();
29
+ const initialMainThreadId = core.mainThreadId;
30
+
31
+ const switchToB = core.switchToThread("thread-b");
32
+ const rejection = expect(switchToB).rejects.toThrow(
33
+ "Thread was deleted before runtime was started",
34
+ );
35
+ await core.delete("thread-b");
36
+ await rejection;
37
+
38
+ expect(adapter.unarchive).not.toHaveBeenCalled();
39
+ expect(core.getItemById("thread-b")).toBeUndefined();
40
+ expect(core.mainThreadId).toBe(initialMainThreadId);
41
+ });
42
+
43
+ it("does not select a thread deleted while its switch is unarchiving", async () => {
44
+ const unarchive = deferred<void>();
45
+ const adapter = makeAdapter({
46
+ list: vi.fn(async () => ({
47
+ threads: [
48
+ {
49
+ status: "archived" as const,
50
+ remoteId: "thread-b",
51
+ externalId: "thread-b",
52
+ title: "Thread B",
53
+ },
54
+ ],
55
+ })),
56
+ unarchive: vi.fn(() => unarchive.promise),
57
+ });
58
+ const core = createCore(adapter);
59
+ await core.getLoadThreadsPromise();
60
+ const initialMainThreadId = core.mainThreadId;
61
+
62
+ const switchToB = core.switchToThread("thread-b");
63
+ await vi.waitFor(() => {
64
+ expect(adapter.unarchive).toHaveBeenCalledWith("thread-b");
65
+ });
66
+ await core.delete("thread-b");
67
+
68
+ unarchive.resolve();
69
+ await switchToB;
70
+
71
+ expect(core.getItemById("thread-b")).toBeUndefined();
72
+ expect(core.mainThreadId).toBe(initialMainThreadId);
73
+ expect(core.getItemById(core.mainThreadId!)).toBeDefined();
74
+ });
75
+
76
+ it("does not start unarchive after initialization when the target was deleted", async () => {
77
+ const initialization = deferred<{
78
+ remoteId: string;
79
+ externalId: string | undefined;
80
+ }>();
81
+ const adapter = makeAdapter({
82
+ list: vi.fn(async () => ({
83
+ threads: [
84
+ {
85
+ status: "archived" as const,
86
+ remoteId: "thread-b",
87
+ externalId: "thread-b",
88
+ title: "Thread B",
89
+ },
90
+ ],
91
+ })),
92
+ });
93
+ const core = createCore(adapter);
94
+ await core.getLoadThreadsPromise();
95
+ const target = core.getItemById("thread-b")!;
96
+ (
97
+ target as {
98
+ initializeTask: Promise<{
99
+ remoteId: string;
100
+ externalId: string | undefined;
101
+ }>;
102
+ }
103
+ ).initializeTask = initialization.promise;
104
+ const initialMainThreadId = core.mainThreadId;
105
+
106
+ const switchToB = core.switchToThread("thread-b");
107
+ const deleteB = core.delete("thread-b");
108
+ await vi.waitFor(() => {
109
+ expect(core.getItemById("thread-b")).toBeUndefined();
110
+ });
111
+
112
+ initialization.resolve({
113
+ remoteId: "thread-b",
114
+ externalId: "thread-b",
115
+ });
116
+ await Promise.all([switchToB, deleteB]);
117
+
118
+ expect(adapter.unarchive).not.toHaveBeenCalled();
119
+ expect(core.mainThreadId).toBe(initialMainThreadId);
120
+ expect(core.getItemById(core.mainThreadId!)).toBeDefined();
121
+ });
122
+
123
+ it("does not unarchive again when the target became regular during initialization", async () => {
124
+ const initialization = deferred<{
125
+ remoteId: string;
126
+ externalId: string | undefined;
127
+ }>();
128
+ const adapter = makeAdapter({
129
+ list: vi.fn(async () => ({
130
+ threads: [
131
+ {
132
+ status: "archived" as const,
133
+ remoteId: "thread-b",
134
+ externalId: "thread-b",
135
+ title: "Thread B",
136
+ },
137
+ ],
138
+ })),
139
+ });
140
+ const core = createCore(adapter);
141
+ await core.getLoadThreadsPromise();
142
+ const target = core.getItemById("thread-b")!;
143
+ (
144
+ target as {
145
+ initializeTask: Promise<{
146
+ remoteId: string;
147
+ externalId: string | undefined;
148
+ }>;
149
+ }
150
+ ).initializeTask = initialization.promise;
151
+
152
+ const switchToB = core.switchToThread("thread-b");
153
+ await new Promise((resolve) => setTimeout(resolve, 0));
154
+ const unarchiveB = core.unarchive("thread-b");
155
+ await vi.waitFor(() => {
156
+ expect(core.getItemById("thread-b")?.status).toBe("regular");
157
+ });
158
+
159
+ initialization.resolve({
160
+ remoteId: "thread-b",
161
+ externalId: "thread-b",
162
+ });
163
+ await Promise.all([switchToB, unarchiveB]);
164
+
165
+ expect(adapter.unarchive).toHaveBeenCalledTimes(1);
166
+ expect(adapter.unarchive).toHaveBeenCalledWith("thread-b");
167
+ expect(core.mainThreadId).toBe("thread-b");
168
+ });
169
+ });
@@ -1,8 +1,16 @@
1
- import type { ThreadListRuntimeCore } from "../../runtime/interfaces/thread-list-runtime-core";
2
- import { BaseSubscribable } from "../../subscribable/subscribable";
1
+ import type {
2
+ ThreadListRuntimeCore,
3
+ ThreadListRuntimeEvent,
4
+ } from "../../runtime/interfaces/thread-list-runtime-core";
5
+ import {
6
+ BaseSubscribable,
7
+ WritableSubscribable,
8
+ } from "../../subscribable/subscribable";
9
+ import { useSubscribable } from "../../store/runtime-clients/useSubscribable";
3
10
  import { OptimisticState } from "../../runtimes/remote-thread-list/optimistic-state";
4
11
  import { EMPTY_THREAD_CORE } from "../../runtimes/remote-thread-list/empty-thread-core";
5
12
  import type {
13
+ ClassifyAccumulator,
6
14
  RemoteThreadData,
7
15
  RemoteThreadState,
8
16
  } from "../../runtimes/remote-thread-list/remote-thread-state";
@@ -24,7 +32,7 @@ import type {
24
32
  } from "../../runtimes/remote-thread-list/types";
25
33
  import { ThreadListAdapterChangedError } from "../../runtimes/remote-thread-list/adapter-changed";
26
34
  import { RemoteThreadListHookInstanceManager } from "./RemoteThreadListHookInstanceManager";
27
- import { isTitleSourceMessage } from "./RemoteThreadResource";
35
+ import { isTitleSourceMessage } from "../../runtimes/remote-thread-list/title";
28
36
  import {
29
37
  type ComponentType,
30
38
  type FC,
@@ -34,7 +42,6 @@ import {
34
42
  useId,
35
43
  } from "react";
36
44
  import { useAui } from "@assistant-ui/store";
37
- import { create } from "zustand";
38
45
  import { AssistantMessageStream } from "assistant-stream";
39
46
  import type { ModelContextProvider } from "../../model-context/types";
40
47
  import { RuntimeAdapterProvider } from "./RuntimeAdapterProvider";
@@ -126,6 +133,7 @@ export class RemoteThreadListThreadListRuntimeCore
126
133
  .optimisticUpdate({
127
134
  execute: () => this._options.adapter.list(),
128
135
  loading: (state) => {
136
+ if (generation !== this._loadGeneration) return state;
129
137
  return {
130
138
  ...state,
131
139
  isLoading: true,
@@ -147,8 +155,8 @@ export class RemoteThreadListThreadListRuntimeCore
147
155
  const fresh = classifyThreads(l.threads, {
148
156
  threadIds: [],
149
157
  archivedThreadIds: [],
150
- threadIdMap: {},
151
- threadData: {},
158
+ threadIdMap: { ...state.threadIdMap },
159
+ threadData: { ...state.threadData },
152
160
  });
153
161
  const merged = {
154
162
  ...state,
@@ -156,8 +164,8 @@ export class RemoteThreadListThreadListRuntimeCore
156
164
  cursor: normalizeCursor(l.nextCursor),
157
165
  threadIds: fresh.threadIds,
158
166
  archivedThreadIds: fresh.archivedThreadIds,
159
- threadIdMap: { ...state.threadIdMap, ...fresh.threadIdMap },
160
- threadData: { ...state.threadData, ...fresh.threadData },
167
+ threadIdMap: fresh.threadIdMap,
168
+ threadData: fresh.threadData,
161
169
  };
162
170
  return preserveMidLoadTransitions(merged, state, statusAtRequest);
163
171
  },
@@ -206,7 +214,10 @@ export class RemoteThreadListThreadListRuntimeCore
206
214
  const dedup = this._state
207
215
  .optimisticUpdate({
208
216
  execute: () => adapter.list({ after: cursor }),
209
- loading: (state) => ({ ...state, isLoadingMore: true }),
217
+ loading: (state) => {
218
+ if (generation !== this._loadGeneration) return state;
219
+ return { ...state, isLoadingMore: true };
220
+ },
210
221
  then: (state, l) => {
211
222
  if (generation !== this._loadGeneration) return state;
212
223
  if (adapter !== this._options.adapter) return state;
@@ -267,15 +278,15 @@ export class RemoteThreadListThreadListRuntimeCore
267
278
  // synchronous notify would re-enter store consumers mid-render.
268
279
  queueMicrotask(() => this._notifySubscribers());
269
280
  });
270
- this.useProvider = create(() => ({
271
- Provider: this.resolveProvider(options.adapter),
272
- }));
281
+ this.providerStore = new WritableSubscribable(
282
+ this.resolveProvider(options.adapter),
283
+ );
273
284
  this.__internal_setOptions(options);
274
285
  this.switchToNewThread();
275
286
  }
276
287
 
277
288
  private _initialThreadLoaded = false;
278
- private useProvider;
289
+ private providerStore;
279
290
 
280
291
  public __internal_setOptions(options: RemoteThreadListOptions) {
281
292
  if (this._options === options) return;
@@ -289,10 +300,7 @@ export class RemoteThreadListThreadListRuntimeCore
289
300
 
290
301
  this._options = options;
291
302
 
292
- const Provider = this.resolveProvider(options.adapter);
293
- if (Provider !== this.useProvider.getState().Provider) {
294
- this.useProvider.setState({ Provider }, true);
295
- }
303
+ this.providerStore.setState(this.resolveProvider(options.adapter));
296
304
 
297
305
  this._hookManager.setRuntimeHook(options.runtimeHook);
298
306
 
@@ -337,24 +345,11 @@ export class RemoteThreadListThreadListRuntimeCore
337
345
  threads: readonly RemoteThreadMetadata[],
338
346
  cursor: string | undefined,
339
347
  ): RemoteThreadState {
340
- const fresh = classifyThreads(threads, {
341
- threadIds: [],
342
- archivedThreadIds: [],
343
- threadIdMap: {},
344
- threadData: {},
345
- });
346
- const threadIdMap = { ...fresh.threadIdMap };
347
- const threadData = { ...fresh.threadData };
348
- const threadIds = [...fresh.threadIds];
349
- const archivedThreadIds = [...fresh.archivedThreadIds];
350
-
348
+ const carried: RemoteThreadData[] = [];
351
349
  if (state.newThreadId) {
352
350
  const mappingId = state.threadIdMap[state.newThreadId];
353
351
  const draft = mappingId ? state.threadData[mappingId] : undefined;
354
- if (draft?.status === "new") {
355
- threadIdMap[state.newThreadId] = mappingId!;
356
- threadData[mappingId!] = draft;
357
- }
352
+ if (draft?.status === "new") carried.push(draft);
358
353
  }
359
354
 
360
355
  const stale = this._staleThreadIdsOnReplace;
@@ -362,26 +357,44 @@ export class RemoteThreadListThreadListRuntimeCore
362
357
  for (const item of Object.values(state.threadData)) {
363
358
  if (stale.has(item.id)) continue;
364
359
  if (item.status !== "new" && item.remoteId === undefined) continue;
365
- const mappingId = createThreadMappingId(item.id);
366
- if (threadData[mappingId]) continue;
367
- threadIdMap[item.id] = mappingId;
368
- if (item.remoteId !== undefined) {
369
- threadIdMap[item.remoteId] = mappingId;
370
- }
371
- threadData[mappingId] = item;
372
- if (item.remoteId === undefined) continue;
373
- if (item.status === "regular" && !threadIds.includes(item.remoteId)) {
374
- threadIds.push(item.remoteId);
375
- } else if (
376
- item.status === "archived" &&
377
- !archivedThreadIds.includes(item.remoteId)
378
- ) {
379
- archivedThreadIds.push(item.remoteId);
380
- }
360
+ carried.push(item);
381
361
  }
382
362
  }
383
363
  this._staleThreadIdsOnReplace = undefined;
384
364
 
365
+ const seed: ClassifyAccumulator = {
366
+ threadIds: [],
367
+ archivedThreadIds: [],
368
+ threadIdMap: {},
369
+ threadData: {},
370
+ };
371
+ for (const item of carried) {
372
+ const mappingId = createThreadMappingId(item.id);
373
+ if (seed.threadData[mappingId]) continue;
374
+ seed.threadIdMap[item.id] = mappingId;
375
+ if (item.remoteId !== undefined) {
376
+ seed.threadIdMap[item.remoteId] = mappingId;
377
+ }
378
+ seed.threadData[mappingId] = item;
379
+ }
380
+
381
+ const { threadIds, archivedThreadIds, threadIdMap, threadData } =
382
+ classifyThreads(threads, seed);
383
+
384
+ for (const item of carried) {
385
+ if (item.remoteId === undefined) continue;
386
+ const current = threadData[createThreadMappingId(item.id)];
387
+ if (current === undefined) continue;
388
+ if (current.status === "regular" && !threadIds.includes(current.id)) {
389
+ threadIds.push(current.id);
390
+ } else if (
391
+ current.status === "archived" &&
392
+ !archivedThreadIds.includes(current.id)
393
+ ) {
394
+ archivedThreadIds.push(current.id);
395
+ }
396
+ }
397
+
385
398
  let nextState: RemoteThreadState = {
386
399
  ...state,
387
400
  isLoading: false,
@@ -555,6 +568,12 @@ export class RemoteThreadListThreadListRuntimeCore
555
568
  return this._hookManager.__internal_isThreadRunning(data.id);
556
569
  }
557
570
 
571
+ public unstable_subscribeThreadEvents(
572
+ callback: (event: ThreadListRuntimeEvent) => void,
573
+ ) {
574
+ return this._hookManager.__internal_subscribeThreadEvents(callback);
575
+ }
576
+
558
577
  public getItemById(threadIdOrRemoteId: string) {
559
578
  return getThreadData(this._state.value, threadIdOrRemoteId);
560
579
  }
@@ -629,9 +648,8 @@ export class RemoteThreadListThreadListRuntimeCore
629
648
  // A concurrent `list()` may already have placed this thread; keep that
630
649
  // position and only merge metadata. A genuinely absent thread stays
631
650
  // appended: it may live on an unloaded page, and a prepend would pin it
632
- // above newer threads permanently since `classifyThreads` skips ids it
633
- // has already seen. Filtering both arrays first still prevents
634
- // duplication or a wrong-status entry from `list()`.
651
+ // above newer threads permanently. Filtering both arrays first still
652
+ // prevents duplication or a wrong-status entry from `list()`.
635
653
  const remoteId = remoteMetadata.remoteId;
636
654
  const wasInTarget =
637
655
  remoteMetadata.status === "regular"
@@ -684,11 +702,22 @@ export class RemoteThreadListThreadListRuntimeCore
684
702
 
685
703
  if (generation !== this._switchGeneration) return;
686
704
 
687
- if (data.status === "archived" && options?.unarchive !== false) {
688
- await this.unarchive(data.id);
705
+ let current = this.getItemById(data.id);
706
+ if (current?.id !== data.id) return;
707
+
708
+ if (current.status === "archived" && options?.unarchive !== false) {
709
+ await current.initializeTask;
689
710
  if (generation !== this._switchGeneration) return;
711
+ current = this.getItemById(data.id);
712
+ if (current?.id !== data.id) return;
713
+ if (current.status === "archived") {
714
+ await this.unarchive(current.id);
715
+ if (generation !== this._switchGeneration) return;
716
+ current = this.getItemById(data.id);
717
+ if (current?.id !== data.id) return;
718
+ }
690
719
  }
691
- this._mainThreadId = data.id;
720
+ this._mainThreadId = current.id;
692
721
 
693
722
  this._notifySubscribers();
694
723
  this._notifyThreadIdChange(emitThreadIdChange);
@@ -776,21 +805,36 @@ export class RemoteThreadListThreadListRuntimeCore
776
805
  if (!data) return state;
777
806
 
778
807
  const mappingId = createThreadMappingId(threadId);
808
+ // A list() response that landed while this initialize was in flight
809
+ // could not know the remote id yet, so it may have minted its own slot
810
+ // for it; that slot collapses into this one.
811
+ const listedMappingId = state.threadIdMap[remoteId];
812
+ const orphan =
813
+ listedMappingId !== undefined && listedMappingId !== mappingId
814
+ ? state.threadData[listedMappingId]
815
+ : undefined;
816
+
817
+ const threadData = { ...state.threadData };
818
+ if (orphan !== undefined) delete threadData[listedMappingId!];
819
+ threadData[mappingId] = {
820
+ ...data,
821
+ initializeTask: Promise.resolve({ remoteId, externalId }),
822
+ remoteId,
823
+ externalId,
824
+ } as RemoteThreadData;
825
+
826
+ const rewire = (ids: readonly string[]) =>
827
+ orphan === undefined ? ids : ids.filter((id) => id !== orphan.id);
828
+
779
829
  return {
780
830
  ...state,
831
+ threadIds: rewire(state.threadIds),
832
+ archivedThreadIds: rewire(state.archivedThreadIds),
781
833
  threadIdMap: {
782
834
  ...state.threadIdMap,
783
835
  [remoteId]: mappingId,
784
836
  },
785
- threadData: {
786
- ...state.threadData,
787
- [mappingId]: {
788
- ...data,
789
- initializeTask: Promise.resolve({ remoteId, externalId }),
790
- remoteId,
791
- externalId,
792
- },
793
- },
837
+ threadData,
794
838
  };
795
839
  },
796
840
  });
@@ -1049,19 +1093,21 @@ export class RemoteThreadListThreadListRuntimeCore
1049
1093
  this._hookManager.stopThreadRuntime(data.id);
1050
1094
  }
1051
1095
 
1052
- private useBoundIds = create<string[]>(() => []);
1096
+ private boundIdsStore = new WritableSubscribable<readonly string[]>([]);
1053
1097
 
1054
1098
  public __internal_RenderComponent: FC = () => {
1055
1099
  const id = useId();
1056
1100
  useEffect(() => {
1057
- this.useBoundIds.setState((s) => [...s, id], true);
1101
+ this.boundIdsStore.setState([...this.boundIdsStore.getState(), id]);
1058
1102
  return () => {
1059
- this.useBoundIds.setState((s) => s.filter((i) => i !== id), true);
1103
+ this.boundIdsStore.setState(
1104
+ this.boundIdsStore.getState().filter((i) => i !== id),
1105
+ );
1060
1106
  };
1061
1107
  }, [id]);
1062
1108
 
1063
- const boundIds = this.useBoundIds();
1064
- const { Provider } = this.useProvider();
1109
+ const boundIds = useSubscribable(this.boundIdsStore);
1110
+ const Provider = useSubscribable(this.providerStore);
1065
1111
  const aui = useAui();
1066
1112
  const enabled = boundIds.length === 0 || boundIds[0] === id;
1067
1113