@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
@@ -1,4 +1,8 @@
1
- import type { AppendMessage, ThreadMessage } from "../../types/message";
1
+ import type {
2
+ AppendMessage,
3
+ ThreadMessage,
4
+ ToolCallMessagePart,
5
+ } from "../../types/message";
2
6
  import type { ThreadMessageLike } from "../../runtime/utils/thread-message-like";
3
7
  import type { AttachmentAdapter } from "../../adapters/attachment";
4
8
  import type {
@@ -14,7 +18,10 @@ import type {
14
18
  ResumeRunConfig,
15
19
  ThreadSuggestion,
16
20
  } from "../../runtime/interfaces/thread-runtime-core";
17
- import type { ExportedMessageRepository } from "../../runtime/utils/message-repository";
21
+ import type {
22
+ ExportedMessageRepository,
23
+ MessageRepository,
24
+ } from "../../runtime/utils/message-repository";
18
25
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
19
26
  import type { ToolExecutionStatus } from "../tool-invocations/ToolInvocationTracker";
20
27
  import type { ExternalThreadQueueAdapter } from "../../runtime/queue/external-thread-queue-adapter";
@@ -105,6 +112,15 @@ type ExternalStoreAdapterBase<T> = {
105
112
  isLoading?: boolean | undefined;
106
113
  messages?: readonly T[];
107
114
  messageRepository?: ExportedMessageRepository;
115
+ /**
116
+ * An externally owned message repository instance. When provided, the
117
+ * thread runtime adopts it as its branch store and swaps to it atomically
118
+ * whenever a different instance is passed, so hosts that route multiple
119
+ * conversations through one runtime keep each conversation's history and
120
+ * branches isolated in its own instance. Omit it to keep the runtime's own
121
+ * repository.
122
+ */
123
+ unstable_messageRepositoryInstance?: MessageRepository | undefined;
108
124
  suggestions?: readonly ThreadSuggestion[] | undefined;
109
125
  state?: ReadonlyJSONValue | undefined;
110
126
  extras?: unknown;
@@ -207,6 +223,26 @@ type ExternalStoreAdapterBase<T> = {
207
223
  * `modelContent` populated when present.
208
224
  */
209
225
  unstable_enableToolInvocations?: boolean | undefined;
226
+ /**
227
+ * Decides whether a tool call's result is produced on the client. Only
228
+ * consulted when `unstable_enableToolInvocations` is `true`.
229
+ *
230
+ * A provider that runs tools itself answers its own calls, and its result
231
+ * arrives one or more snapshots after the call's arguments complete. In
232
+ * that window the call is complete and result-less, so a registered tool
233
+ * of the same name would otherwise execute locally and produce a result
234
+ * the provider never asked for. An adapter that can tell the two apart
235
+ * supplies this predicate; it is read once per tool call, when the call is
236
+ * first observed live.
237
+ *
238
+ * The predicate is also what licenses running a frontend tool while the
239
+ * provider's run is still open. Without it, ownership is unknown until the
240
+ * run ends, so a registered tool executes only once the run's outcome is
241
+ * known and cannot fire on a call the provider was about to answer or gate.
242
+ */
243
+ unstable_isClientToolCall?:
244
+ | ((toolCall: ToolCallMessagePart) => boolean)
245
+ | undefined;
210
246
  /**
211
247
  * Receives the current per-tool-call execution status map whenever it
212
248
  * changes. Only invoked when `unstable_enableToolInvocations` is `true`
@@ -1,3 +1,4 @@
1
+ import { shallowEqual } from "@assistant-ui/store/client";
1
2
  import type { AppendMessage, ThreadMessage } from "../../types/message";
2
3
  import type { Attachment } from "../../types/attachment";
3
4
  import type {
@@ -28,6 +29,7 @@ import {
28
29
  type ThreadMessageLike,
29
30
  } from "../../runtime/utils/thread-message-like";
30
31
  import { getThreadMessageText } from "../../utils/text";
32
+ import { shallowArrayEqual } from "../../runtime/utils/external-message-conversion";
31
33
  import type {
32
34
  RuntimeCapabilities,
33
35
  ThreadRuntimeCore,
@@ -43,6 +45,7 @@ import {
43
45
  MessageRepository,
44
46
  } from "../../runtime/utils/message-repository";
45
47
  import { generateId } from "../../utils/id";
48
+ import { walkToolCallTree } from "../../runtime/utils/tool-call-tree";
46
49
  import {
47
50
  ToolInvocationTracker,
48
51
  type ToolExecutionStatus,
@@ -57,7 +60,7 @@ import {
57
60
  const EMPTY_ARRAY: readonly ThreadSuggestion[] = Object.freeze([]);
58
61
 
59
62
  const observeAdapterCallback = (
60
- name: "onAddToolResult" | "onRespondToToolApproval" | "onCancel",
63
+ name: "onAddToolResult" | "onCancel",
61
64
  result: Promise<void> | void,
62
65
  ) => {
63
66
  void Promise.resolve(result).catch((error) => {
@@ -68,15 +71,6 @@ const observeAdapterCallback = (
68
71
  });
69
72
  };
70
73
 
71
- const shallowEqual = (a: object, b: object): boolean => {
72
- const aKeys = Object.keys(a);
73
- if (aKeys.length !== Object.keys(b).length) return false;
74
- for (const key of aKeys) {
75
- if ((a as any)[key] !== (b as any)[key]) return false;
76
- }
77
- return true;
78
- };
79
-
80
74
  export const hasUpcomingMessage = (
81
75
  isRunning: boolean,
82
76
  messages: readonly ThreadMessage[],
@@ -154,6 +148,11 @@ export class ExternalStoreThreadRuntimeCore
154
148
  // and a delete whose confirmation races a send keeps its eviction.
155
149
  private _pendingDeleteEvictions = new Set<string>();
156
150
 
151
+ // Placeholder id for the upcoming assistant message, reused across snapshot
152
+ // passes while the same tail message awaits its response so the placeholder
153
+ // keeps one identity per response.
154
+ private _optimistic: { id: string; parentId: string | null } | null = null;
155
+
157
156
  private _store!: ExternalStoreAdapter<any>;
158
157
 
159
158
  private _getInitializePromise?: () => Promise<unknown> | undefined;
@@ -246,6 +245,14 @@ export class ExternalStoreThreadRuntimeCore
246
245
  const oldStore = this._store as ExternalStoreAdapter<any> | undefined;
247
246
  this._store = store;
248
247
  const isRunning = this._getEffectiveIsRunning(store);
248
+ const repositoryInstance = store.unstable_messageRepositoryInstance;
249
+ const repositoryChanged =
250
+ repositoryInstance !== undefined &&
251
+ repositoryInstance !== this.repository;
252
+ if (repositoryChanged) {
253
+ this.repository = repositoryInstance;
254
+ this._pendingDeleteEvictions.clear();
255
+ }
249
256
  if (oldStore?.queue !== store.queue) {
250
257
  this._transformedQueue = undefined;
251
258
  store.queue?.__internal_setDispatchTransform?.((message) => {
@@ -296,6 +303,7 @@ export class ExternalStoreThreadRuntimeCore
296
303
  // Handle messageRepository
297
304
  if (
298
305
  oldStore &&
306
+ !repositoryChanged &&
299
307
  oldStore.isRunning === store.isRunning &&
300
308
  oldStore.messageRepository === store.messageRepository &&
301
309
  previousIsRunning === isRunning
@@ -308,7 +316,11 @@ export class ExternalStoreThreadRuntimeCore
308
316
  const headId =
309
317
  store.messageRepository.headId ?? incoming.at(-1)?.message.id ?? null;
310
318
 
311
- if (oldStore && oldStore.messageRepository === store.messageRepository) {
319
+ if (
320
+ oldStore &&
321
+ !repositoryChanged &&
322
+ oldStore.messageRepository === store.messageRepository
323
+ ) {
312
324
  this.repository.resetHead(headId);
313
325
  messages = this.repository.getMessages();
314
326
  } else {
@@ -333,6 +345,7 @@ export class ExternalStoreThreadRuntimeCore
333
345
  if (oldStore.convertMessage !== store.convertMessage) {
334
346
  this._converter = new ThreadMessageConverter();
335
347
  } else if (
348
+ !repositoryChanged &&
336
349
  oldStore.isRunning === store.isRunning &&
337
350
  oldStore.messages === store.messages &&
338
351
  previousIsRunning === isRunning
@@ -436,9 +449,13 @@ export class ExternalStoreThreadRuntimeCore
436
449
  // (prior placeholders, mid-run id-swap siblings); export() never persists them.
437
450
  let optimisticId: string | null = null;
438
451
  if (hasUpcomingMessage(isRunning, messages)) {
439
- optimisticId = generateId();
452
+ const parentId = messages.at(-1)?.id ?? null;
453
+ if (this._optimistic?.parentId !== parentId) {
454
+ this._optimistic = { id: generateId(), parentId };
455
+ }
456
+ optimisticId = this._optimistic.id;
440
457
  this.repository.addOrUpdateMessage(
441
- messages.at(-1)?.id ?? null,
458
+ parentId,
442
459
  fromThreadMessageLike(
443
460
  { role: "assistant", content: [], metadata: { isOptimistic: true } },
444
461
  optimisticId,
@@ -447,10 +464,20 @@ export class ExternalStoreThreadRuntimeCore
447
464
  );
448
465
  }
449
466
 
467
+ if (optimisticId === null) this._optimistic = null;
450
468
  this.repository.resetHead(optimisticId ?? messages.at(-1)?.id ?? null);
451
469
 
452
- this._messages = this.repository.getMessages();
470
+ const messagesSnapshot = this.repository.getMessages();
471
+ if (
472
+ !this._messages ||
473
+ !shallowArrayEqual(this._messages, messagesSnapshot)
474
+ ) {
475
+ this._messages = messagesSnapshot;
476
+ }
453
477
 
478
+ if (repositoryChanged) {
479
+ this._runTrackerUpdate(() => this._toolInvocations?.reset());
480
+ }
454
481
  this._runTrackerUpdate(() => this._driveToolInvocations());
455
482
 
456
483
  this._notifySubscribers();
@@ -529,6 +556,7 @@ export class ExternalStoreThreadRuntimeCore
529
556
  }
530
557
  },
531
558
  },
559
+ (toolCall) => this._store.unstable_isClientToolCall?.(toolCall),
532
560
  );
533
561
  }
534
562
 
@@ -557,17 +585,9 @@ export class ExternalStoreThreadRuntimeCore
557
585
  private _findMessageIdForToolCall(toolCallId: string): string | undefined {
558
586
  if (this._messagesForToolCallIndex !== this._messages) {
559
587
  this._toolCallToMessageId.clear();
560
- const visit = (messages: readonly ThreadMessage[]): void => {
561
- for (const message of messages) {
562
- if (!Array.isArray(message.content)) continue;
563
- for (const part of message.content) {
564
- if (!part || part.type !== "tool-call") continue;
565
- this._toolCallToMessageId.set(part.toolCallId, message.id);
566
- if (part.messages) visit(part.messages);
567
- }
568
- }
569
- };
570
- visit(this._messages);
588
+ for (const { part, messageId } of walkToolCallTree(this._messages)) {
589
+ this._toolCallToMessageId.set(part.toolCallId, messageId);
590
+ }
571
591
  this._messagesForToolCallIndex = this._messages;
572
592
  }
573
593
  return this._toolCallToMessageId.get(toolCallId);
@@ -671,7 +691,7 @@ export class ExternalStoreThreadRuntimeCore
671
691
  // user messages — matches the satellites' historical opt-in cancel
672
692
  // behavior, which is now built in.
673
693
  if (message.startRun ?? message.role === "user") {
674
- await this._toolInvocations?.abort();
694
+ await this._toolInvocations?.abort({ discardPending: true });
675
695
  }
676
696
  if (!isThreadRuntimeGenerationCurrent(this, generation)) return;
677
697
 
@@ -772,7 +792,7 @@ export class ExternalStoreThreadRuntimeCore
772
792
  // Auto-abort in-flight client-side tool executions when a run reloads;
773
793
  // any results that land afterward would target a turn that no longer
774
794
  // exists. See `append` above for full rationale.
775
- await this._toolInvocations?.abort();
795
+ await this._toolInvocations?.abort({ discardPending: true });
776
796
 
777
797
  await this._store.onReload(config.parentId, config);
778
798
  }
@@ -826,7 +846,7 @@ export class ExternalStoreThreadRuntimeCore
826
846
  // Abort any in-flight client-side tool executions. Fire-and-forget —
827
847
  // the abort resolves once executions settle, but we don't gate the
828
848
  // cancel on it.
829
- void this._toolInvocations?.abort();
849
+ void this._toolInvocations?.abort({ discardPending: true });
830
850
 
831
851
  // Before the run is aborted, so the settle it produces keeps the pending
832
852
  // items instead of dispatching the next one at the moment the user
@@ -936,13 +956,16 @@ export class ExternalStoreThreadRuntimeCore
936
956
  );
937
957
  }
938
958
 
939
- public respondToToolApproval(options: RespondToToolApprovalOptions) {
959
+ public respondToToolApproval(
960
+ options: RespondToToolApprovalOptions,
961
+ ): Promise<void> {
940
962
  if (!this._store.onRespondToToolApproval)
941
963
  throw new Error("Runtime does not support tool approvals.");
942
- observeAdapterCallback(
943
- "onRespondToToolApproval",
944
- this._store.onRespondToToolApproval(options),
945
- );
964
+ try {
965
+ return Promise.resolve(this._store.onRespondToToolApproval(options));
966
+ } catch (error) {
967
+ return Promise.reject(error);
968
+ }
946
969
  }
947
970
 
948
971
  public override reset(initialMessages?: readonly ThreadMessageLike[]) {
@@ -136,7 +136,7 @@ export abstract class InertThreadRuntimeCore
136
136
  throw this.error;
137
137
  }
138
138
 
139
- respondToToolApproval(): void {
139
+ respondToToolApproval(): Promise<void> {
140
140
  throw this.error;
141
141
  }
142
142
 
@@ -5,7 +5,7 @@ import type {
5
5
  ChatModelRunOptions,
6
6
  ChatModelRunResult,
7
7
  } from "../../runtime/utils/chat-model-adapter";
8
- import type { AppendMessage } from "../../types/message";
8
+ import type { AppendMessage, ToolCallMessagePart } from "../../types/message";
9
9
  import type { LocalRuntimeOptionsBase } from "./local-runtime-options";
10
10
  import {
11
11
  ExportedMessageRepository,
@@ -61,7 +61,7 @@ const userMessage = (text: string): AppendMessage => ({
61
61
 
62
62
  const toolCallPart = (
63
63
  toolName: string,
64
- approval?: { id: string; resolution?: "cancelled" | "expired" },
64
+ approval?: ToolCallMessagePart["approval"],
65
65
  ) => ({
66
66
  type: "tool-call" as const,
67
67
  toolCallId: `call-${toolName}`,
@@ -73,7 +73,7 @@ const toolCallPart = (
73
73
 
74
74
  const toolCallResult = (
75
75
  toolName: string,
76
- approval?: { id: string; resolution?: "cancelled" | "expired" },
76
+ approval?: ToolCallMessagePart["approval"],
77
77
  ): ChatModelRunResult => ({
78
78
  content: [toolCallPart(toolName, approval)],
79
79
  status: { type: "requires-action", reason: "tool-calls" },
@@ -485,6 +485,32 @@ describe("LocalThreadRuntimeCore tool approvals", () => {
485
485
  });
486
486
  });
487
487
 
488
+ it("records a free-form answer alongside the decision", async () => {
489
+ const { thread, runs } = createApprovalThread(
490
+ toolCallResult("send_email", { id: "a1", display: "text" }),
491
+ );
492
+
493
+ await thread.append(userMessage("send an email"));
494
+ await flush();
495
+
496
+ await thread.respondToToolApproval({
497
+ approvalId: "a1",
498
+ approved: true,
499
+ text: "Quarterly update",
500
+ });
501
+ await flush();
502
+
503
+ const toolCall = runs[1]!
504
+ .unstable_getMessage()
505
+ .content.find((part) => part.type === "tool-call");
506
+ expect(toolCall?.approval).toEqual({
507
+ id: "a1",
508
+ display: "text",
509
+ approved: true,
510
+ text: "Quarterly update",
511
+ });
512
+ });
513
+
488
514
  it("treats a terminal resolution as non-pending and continues the run", async () => {
489
515
  const { thread, runs } = createApprovalThread(
490
516
  toolCallResult("deploy", { id: "a1", resolution: "expired" }),
@@ -900,8 +900,9 @@ export class LocalThreadRuntimeCore
900
900
  approvalId,
901
901
  approved,
902
902
  optionId,
903
+ text,
903
904
  reason,
904
- }: RespondToToolApprovalOptions) {
905
+ }: RespondToToolApprovalOptions): Promise<void> {
905
906
  let message = this.repository
906
907
  .getMessages()
907
908
  .findLast(
@@ -944,6 +945,7 @@ export class LocalThreadRuntimeCore
944
945
  ...targetApproval,
945
946
  approved,
946
947
  ...(optionId != null && { optionId }),
948
+ ...(text != null && { text }),
947
949
  ...(reason != null && { reason }),
948
950
  };
949
951
  if (approved) return { ...c, approval };
@@ -968,5 +970,7 @@ export class LocalThreadRuntimeCore
968
970
  } else {
969
971
  this._persistPausedMessage(parentId, message);
970
972
  }
973
+
974
+ return Promise.resolve();
971
975
  }
972
976
  }
@@ -1,9 +1,54 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
+ classifyThreads,
3
4
  createEmptyRemoteThreadState,
4
5
  createThreadMappingId,
5
6
  seedNewThread,
7
+ updateStatusReducer,
6
8
  } from "./remote-thread-state";
9
+ import type {
10
+ RemoteThreadData,
11
+ RemoteThreadState,
12
+ } from "./remote-thread-state";
13
+
14
+ const initializedDraft = () => {
15
+ const seeded = seedNewThread(createEmptyRemoteThreadState());
16
+ const regular = updateStatusReducer(seeded.state, seeded.id, "regular");
17
+ const mappingId = regular.threadIdMap[seeded.id]!;
18
+ const initializeTask = Promise.resolve({
19
+ remoteId: "remote-1",
20
+ externalId: "remote-1",
21
+ });
22
+ return {
23
+ id: seeded.id,
24
+ mappingId,
25
+ initializeTask,
26
+ state: {
27
+ ...regular,
28
+ threadIdMap: { ...regular.threadIdMap, "remote-1": mappingId },
29
+ threadData: {
30
+ ...regular.threadData,
31
+ [mappingId]: {
32
+ ...regular.threadData[mappingId]!,
33
+ remoteId: "remote-1",
34
+ externalId: "remote-1",
35
+ initializeTask,
36
+ } as RemoteThreadData,
37
+ },
38
+ } as RemoteThreadState,
39
+ };
40
+ };
41
+
42
+ const expectOneSlotPerIdentity = (state: RemoteThreadState) => {
43
+ const remoteIds = Object.values(state.threadData)
44
+ .map((data) => data.remoteId)
45
+ .filter((remoteId) => remoteId !== undefined);
46
+ expect(new Set(remoteIds).size).toBe(remoteIds.length);
47
+
48
+ for (const id of [...state.threadIds, ...state.archivedThreadIds]) {
49
+ expect(state.threadData[state.threadIdMap[id]!]?.id).toBe(id);
50
+ }
51
+ };
7
52
 
8
53
  describe("remote thread state", () => {
9
54
  it("creates an empty state", () => {
@@ -34,4 +79,71 @@ describe("remote thread state", () => {
34
79
  );
35
80
  expect(Object.keys(second.state.threadData)).toEqual([first.id, second.id]);
36
81
  });
82
+
83
+ it("refreshes the local slot when a listed thread already has a mapping", () => {
84
+ const draft = initializedDraft();
85
+
86
+ const merged = classifyThreads(
87
+ [
88
+ {
89
+ status: "regular",
90
+ remoteId: "remote-1",
91
+ externalId: "remote-1",
92
+ title: "from the server",
93
+ },
94
+ ],
95
+ {
96
+ threadIds: [],
97
+ archivedThreadIds: [],
98
+ threadIdMap: { ...draft.state.threadIdMap },
99
+ threadData: { ...draft.state.threadData },
100
+ },
101
+ );
102
+
103
+ expect(Object.keys(merged.threadData)).toEqual([draft.mappingId]);
104
+ expect(merged.threadIds).toEqual([draft.id]);
105
+ expect(merged.threadIdMap["remote-1"]).toBe(draft.mappingId);
106
+ expect(merged.threadData[draft.mappingId]?.id).toBe(draft.id);
107
+ expect(merged.threadData[draft.mappingId]?.title).toBe("from the server");
108
+ expect(merged.threadData[draft.mappingId]?.localOrigin).toBe(true);
109
+ const refreshed = merged.threadData[draft.mappingId]!;
110
+ expect(
111
+ refreshed.status === "new" ? undefined : refreshed.initializeTask,
112
+ ).toBe(draft.initializeTask);
113
+ expectOneSlotPerIdentity({ ...draft.state, ...merged });
114
+ });
115
+
116
+ it("lists a repeated remote id once", () => {
117
+ const listed = classifyThreads(
118
+ [
119
+ { status: "regular", remoteId: "a", externalId: "a", title: "first" },
120
+ { status: "regular", remoteId: "a", externalId: "a", title: "second" },
121
+ ],
122
+ {
123
+ threadIds: [],
124
+ archivedThreadIds: [],
125
+ threadIdMap: {},
126
+ threadData: {},
127
+ },
128
+ );
129
+
130
+ expect(listed.threadIds).toEqual(["a"]);
131
+ expect(listed.threadData[createThreadMappingId("a")]?.title).toBe("second");
132
+ });
133
+
134
+ it("deletes every alias of an identity, by either id", () => {
135
+ for (const target of ["remote-1", "local"] as const) {
136
+ const draft = initializedDraft();
137
+ const deleted = updateStatusReducer(
138
+ draft.state,
139
+ target === "local" ? draft.id : target,
140
+ "deleted",
141
+ );
142
+
143
+ expect(deleted.threadData).toEqual({});
144
+ expect(deleted.threadIdMap).toEqual({});
145
+ expect(deleted.threadIds).toEqual([]);
146
+ expect(deleted.archivedThreadIds).toEqual([]);
147
+ }
148
+ });
37
149
  });
@@ -47,26 +47,29 @@ export const LOCAL_THREAD_ID_PREFIX = "__LOCALID_";
47
47
  export const normalizeCursor = (c: string | undefined): string | undefined =>
48
48
  c || undefined;
49
49
 
50
- type ClassifyAccumulator = {
50
+ export type ClassifyAccumulator = {
51
51
  threadIds: string[];
52
52
  archivedThreadIds: string[];
53
53
  threadIdMap: Record<string, THREAD_MAPPING_ID>;
54
54
  threadData: Record<THREAD_MAPPING_ID, RemoteThreadData>;
55
55
  };
56
56
 
57
+ // A slot's `id` is fixed when it is minted: the hook instance manager keys live
58
+ // thread runtimes by it, so renaming a slot would detach the runtime the user is
59
+ // chatting in. A listed thread whose `remoteId` already maps to a slot therefore
60
+ // refreshes that slot instead of minting a second one, and a slot minted by a
61
+ // list() that raced ahead of the remote id collapses when `initialize()` settles.
62
+ // `threadIds`/`archivedThreadIds` carry slot ids, never remote ids.
57
63
  export const classifyThreads = (
58
64
  threads: readonly RemoteThreadMetadata[],
59
65
  acc: ClassifyAccumulator,
60
66
  ): ClassifyAccumulator => {
61
- for (const thread of threads) {
62
- if (acc.threadIdMap[thread.remoteId] !== undefined) continue;
67
+ const listed = new Set([...acc.threadIds, ...acc.archivedThreadIds]);
63
68
 
69
+ for (const thread of threads) {
64
70
  switch (thread.status) {
65
71
  case "regular":
66
- acc.threadIds.push(thread.remoteId);
67
- break;
68
72
  case "archived":
69
- acc.archivedThreadIds.push(thread.remoteId);
70
73
  break;
71
74
  default: {
72
75
  const _exhaustiveCheck: never = thread.status;
@@ -74,20 +77,52 @@ export const classifyThreads = (
74
77
  }
75
78
  }
76
79
 
77
- const mappingId = createThreadMappingId(thread.remoteId);
80
+ const existingMappingId = acc.threadIdMap[thread.remoteId];
81
+ const existing =
82
+ existingMappingId !== undefined
83
+ ? acc.threadData[existingMappingId]
84
+ : undefined;
85
+ const id = existing?.id ?? thread.remoteId;
86
+ const mappingId = existingMappingId ?? createThreadMappingId(id);
87
+ const existingTask =
88
+ existing !== undefined && existing.status !== "new"
89
+ ? existing.initializeTask
90
+ : undefined;
91
+
92
+ if (!listed.has(id)) {
93
+ listed.add(id);
94
+ if (thread.status === "regular") {
95
+ acc.threadIds.push(id);
96
+ } else {
97
+ acc.archivedThreadIds.push(id);
98
+ }
99
+ } else if (existing !== undefined && existing.status !== thread.status) {
100
+ if (thread.status === "regular") {
101
+ acc.archivedThreadIds = acc.archivedThreadIds.filter((t) => t !== id);
102
+ acc.threadIds.push(id);
103
+ } else {
104
+ acc.threadIds = acc.threadIds.filter((t) => t !== id);
105
+ acc.archivedThreadIds.push(id);
106
+ }
107
+ }
108
+
109
+ acc.threadIdMap[id] = mappingId;
78
110
  acc.threadIdMap[thread.remoteId] = mappingId;
79
111
  acc.threadData[mappingId] = {
80
- id: thread.remoteId,
112
+ ...(existing?.localOrigin === true ? { localOrigin: true as const } : {}),
113
+ id,
81
114
  remoteId: thread.remoteId,
82
115
  externalId: thread.externalId,
83
116
  status: thread.status,
84
117
  title: thread.title,
85
118
  lastMessageAt: thread.lastMessageAt,
86
119
  custom: thread.custom,
87
- initializeTask: Promise.resolve({
88
- remoteId: thread.remoteId,
89
- externalId: thread.externalId,
90
- }),
120
+ initializeTask:
121
+ existingTask ??
122
+ Promise.resolve({
123
+ remoteId: thread.remoteId,
124
+ externalId: thread.externalId,
125
+ }),
91
126
  };
92
127
  }
93
128
  return acc;
@@ -238,7 +273,7 @@ export const updateStatusReducer = (
238
273
  const data = getThreadData(state, threadIdOrRemoteId);
239
274
  if (!data) return state;
240
275
 
241
- const { id, remoteId, status: lastStatus } = data;
276
+ const { id, status: lastStatus } = data;
242
277
  if (lastStatus === newStatus) return state;
243
278
 
244
279
  const newState = { ...state };
@@ -273,16 +308,20 @@ export const updateStatusReducer = (
273
308
  newState.archivedThreadIds = [id, ...newState.archivedThreadIds];
274
309
  break;
275
310
 
276
- case "deleted":
311
+ case "deleted": {
312
+ const mappingId = state.threadIdMap[threadIdOrRemoteId]!;
277
313
  newState.threadData = Object.fromEntries(
278
- Object.entries(newState.threadData).filter(([key]) => key !== id),
314
+ Object.entries(newState.threadData).filter(
315
+ ([key]) => key !== mappingId,
316
+ ),
279
317
  );
280
318
  newState.threadIdMap = Object.fromEntries(
281
319
  Object.entries(newState.threadIdMap).filter(
282
- ([key]) => key !== id && key !== remoteId,
320
+ ([, value]) => value !== mappingId,
283
321
  ),
284
322
  );
285
323
  break;
324
+ }
286
325
 
287
326
  default: {
288
327
  const _exhaustiveCheck: never = newStatus;
@@ -0,0 +1,3 @@
1
+ export const isTitleSourceMessage = (message: {
2
+ status?: { type: string } | undefined;
3
+ }) => message.status?.type !== "running";
@@ -78,11 +78,12 @@ export type RemoteThreadListAdapter = {
78
78
  unstable_Provider?: RemoteThreadListProviderComponent | undefined;
79
79
 
80
80
  /**
81
- * Hook the `RemoteThreadList` store entry calls once for the main-thread
82
- * slot, then provides to the `thread` factory. This is not mounted per
83
- * listed thread. `useRemoteThreadListRuntime` also calls it when
84
- * `unstable_Provider` is omitted. Resolve `threadListItem` lazily on each
85
- * adapter call; do not capture it at hook mount. The hook must keep a
81
+ * Hook the `RemoteThreadList` store entry calls once per mounted thread
82
+ * body (the main-thread slot by default; every started thread with
83
+ * `backgroundThreads`), then provides to the `thread` factory. This is not
84
+ * mounted per listed thread. `useRemoteThreadListRuntime` also calls it
85
+ * when `unstable_Provider` is omitted. Resolve `threadListItem` lazily on
86
+ * each adapter call; do not capture it at hook mount. The hook must keep a
86
87
  * stable hook count across adapter swaps; a different count throws.
87
88
  * Memoize the returned object.
88
89
  *