@assistant-ui/core 0.3.17 → 0.3.19

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 (470) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/attachment.js +5 -4
  3. package/dist/adapters/attachment.js.map +1 -1
  4. package/dist/adapters/speech.d.ts.map +1 -1
  5. package/dist/adapters/speech.js +13 -3
  6. package/dist/adapters/speech.js.map +1 -1
  7. package/dist/adapters/thread-history.d.ts +4 -0
  8. package/dist/adapters/thread-history.d.ts.map +1 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal.d.ts +4 -3
  12. package/dist/internal.js +3 -2
  13. package/dist/model-context/frame/host.d.ts.map +1 -1
  14. package/dist/model-context/frame/host.js +35 -14
  15. package/dist/model-context/frame/host.js.map +1 -1
  16. package/dist/model-context/frame/provider.d.ts +2 -0
  17. package/dist/model-context/frame/provider.d.ts.map +1 -1
  18. package/dist/model-context/frame/provider.js +30 -7
  19. package/dist/model-context/frame/provider.js.map +1 -1
  20. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
  21. package/dist/model-context/interactable-composer-metadata.js +27 -18
  22. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  23. package/dist/model-context/registry.d.ts.map +1 -1
  24. package/dist/model-context/registry.js +3 -5
  25. package/dist/model-context/registry.js.map +1 -1
  26. package/dist/model-context/types.d.ts.map +1 -1
  27. package/dist/model-context/types.js +5 -4
  28. package/dist/model-context/types.js.map +1 -1
  29. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  30. package/dist/react/adapters/LocalStorageThreadListAdapter.js +45 -7
  31. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  32. package/dist/react/client/DataRenderers.d.ts.map +1 -1
  33. package/dist/react/client/DataRenderers.js +20 -15
  34. package/dist/react/client/DataRenderers.js.map +1 -1
  35. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  36. package/dist/react/client/InMemoryThreadList.js +62 -56
  37. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  38. package/dist/react/client/Interactables.d.ts.map +1 -1
  39. package/dist/react/client/Interactables.js +35 -32
  40. package/dist/react/client/Interactables.js.map +1 -1
  41. package/dist/react/client/RemoteThreadList.d.ts +1 -0
  42. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  43. package/dist/react/client/RemoteThreadList.js +154 -143
  44. package/dist/react/client/RemoteThreadList.js.map +1 -1
  45. package/dist/react/client/Tools.d.ts.map +1 -1
  46. package/dist/react/client/Tools.js +18 -16
  47. package/dist/react/client/Tools.js.map +1 -1
  48. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  49. package/dist/react/client/interactable-model-context.js +3 -2
  50. package/dist/react/client/interactable-model-context.js.map +1 -1
  51. package/dist/react/index.d.ts +2 -2
  52. package/dist/react/index.js +2 -2
  53. package/dist/react/interactables-legacy/Interactables.js +242 -121
  54. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  55. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -1
  56. package/dist/react/interactables-legacy/useAssistantInteractable.js +3 -0
  57. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
  58. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
  59. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +11 -9
  60. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -1
  61. package/dist/react/model-context/makeAssistantTool.d.ts +2 -2
  62. package/dist/react/model-context/makeAssistantTool.js +1 -1
  63. package/dist/react/model-context/makeAssistantTool.js.map +1 -1
  64. package/dist/react/model-context/useAssistantTool.d.ts +2 -2
  65. package/dist/react/model-context/useAssistantTool.js +1 -1
  66. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  67. package/dist/react/model-context/useInteractable.d.ts.map +1 -1
  68. package/dist/react/model-context/useInteractable.js +4 -1
  69. package/dist/react/model-context/useInteractable.js.map +1 -1
  70. package/dist/react/model-context/useInteractableVersions.js +1 -1
  71. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
  72. package/dist/react/model-context/useToolArgsStatus.js +2 -1
  73. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  74. package/dist/react/primitives/message/MessageGroupedParts.d.ts +11 -2
  75. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  76. package/dist/react/primitives/message/MessageGroupedParts.js +3 -2
  77. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  78. package/dist/react/primitives/part/PartMessages.d.ts +1 -0
  79. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  80. package/dist/react/primitives/part/PartMessages.js +1 -0
  81. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  82. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  83. package/dist/react/primitives/thread/ThreadMessages.js +6 -5
  84. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  85. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  86. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +16 -8
  87. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  88. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -1
  89. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  90. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +105 -104
  91. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  92. package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -0
  93. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  94. package/dist/react/runtimes/RemoteThreadResource.js +13 -11
  95. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  96. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +5 -24
  97. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  98. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +354 -226
  99. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  100. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -0
  101. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  102. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +22 -2
  103. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  104. package/dist/react/runtimes/cloud/auiV0.d.ts +40 -15
  105. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  106. package/dist/react/runtimes/cloud/auiV0.js +62 -15
  107. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  108. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +2 -1
  109. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
  110. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +6 -1
  111. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
  112. package/dist/react/runtimes/cloud/sdkIdentity.d.ts +6 -0
  113. package/dist/react/runtimes/cloud/sdkIdentity.d.ts.map +1 -0
  114. package/dist/react/runtimes/cloud/sdkIdentity.js +9 -0
  115. package/dist/react/runtimes/cloud/sdkIdentity.js.map +1 -0
  116. package/dist/react/runtimes/external-message-converter.d.ts +12 -2
  117. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  118. package/dist/react/runtimes/external-message-converter.js +26 -61
  119. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  120. package/dist/react/runtimes/useExternalStoreRuntime.js +75 -46
  121. package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -1
  122. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  123. package/dist/react/runtimes/useRemoteThreadListRuntime.js +64 -24
  124. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  125. package/dist/react/utils/groupParts.d.ts +6 -1
  126. package/dist/react/utils/groupParts.d.ts.map +1 -1
  127. package/dist/react/utils/groupParts.js +6 -0
  128. package/dist/react/utils/groupParts.js.map +1 -1
  129. package/dist/react/utils/useJSONEqual.d.ts +3 -1
  130. package/dist/react/utils/useJSONEqual.d.ts.map +1 -1
  131. package/dist/react/utils/useJSONEqual.js +6 -1
  132. package/dist/react/utils/useJSONEqual.js.map +1 -1
  133. package/dist/react/utils/useJSONSchemaDependency.d.ts +7 -0
  134. package/dist/react/utils/useJSONSchemaDependency.d.ts.map +1 -0
  135. package/dist/react/utils/useJSONSchemaDependency.js +20 -0
  136. package/dist/react/utils/useJSONSchemaDependency.js.map +1 -0
  137. package/dist/runtime/api/thread-list-item-runtime.d.ts +7 -3
  138. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  139. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  140. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  141. package/dist/runtime/api/thread-list-runtime.d.ts +3 -0
  142. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  143. package/dist/runtime/api/thread-list-runtime.js +18 -14
  144. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  145. package/dist/runtime/api/thread-runtime.d.ts +4 -0
  146. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  147. package/dist/runtime/api/thread-runtime.js +3 -6
  148. package/dist/runtime/api/thread-runtime.js.map +1 -1
  149. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
  150. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  151. package/dist/runtime/base/base-composer-runtime-core.js +62 -22
  152. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  153. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -0
  154. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  155. package/dist/runtime/base/base-thread-runtime-core.js +161 -69
  156. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  157. package/dist/runtime/interfaces/composer-runtime-core.d.ts +12 -5
  158. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  159. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +4 -1
  160. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  161. package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
  162. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  163. package/dist/runtime/queue/message-queue.d.ts +5 -0
  164. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  165. package/dist/runtime/queue/message-queue.js +61 -6
  166. package/dist/runtime/queue/message-queue.js.map +1 -1
  167. package/dist/runtime/utils/auto-status.d.ts +6 -2
  168. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  169. package/dist/runtime/utils/auto-status.js +12 -4
  170. package/dist/runtime/utils/auto-status.js.map +1 -1
  171. package/dist/runtime/utils/external-message-conversion.d.ts +16 -4
  172. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
  173. package/dist/runtime/utils/external-message-conversion.js +49 -13
  174. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  175. package/dist/runtime/utils/message-repository.d.ts +1 -0
  176. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  177. package/dist/runtime/utils/message-repository.js +18 -9
  178. package/dist/runtime/utils/message-repository.js.map +1 -1
  179. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  180. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  181. package/dist/runtime/utils/thread-message-like.js +5 -3
  182. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  183. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  184. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +21 -27
  185. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  186. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  187. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +8 -1
  188. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  189. package/dist/runtimes/internal.d.ts +3 -3
  190. package/dist/runtimes/internal.js +2 -2
  191. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
  192. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  193. package/dist/runtimes/local/local-thread-runtime-core.js +4 -1
  194. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  195. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +18 -1
  196. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  197. package/dist/runtimes/remote-thread-list/remote-thread-state.js +130 -42
  198. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  199. package/dist/runtimes/remote-thread-list/title-generation.d.ts +49 -0
  200. package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -0
  201. package/dist/runtimes/remote-thread-list/title-generation.js +197 -0
  202. package/dist/runtimes/remote-thread-list/title-generation.js.map +1 -0
  203. package/dist/runtimes/remote-thread-list/title.d.ts +3 -1
  204. package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -1
  205. package/dist/runtimes/remote-thread-list/title.js +9 -1
  206. package/dist/runtimes/remote-thread-list/title.js.map +1 -1
  207. package/dist/runtimes/remote-thread-list/types.d.ts +10 -0
  208. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  209. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  210. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -1
  211. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  212. package/dist/store/clients/external-thread.d.ts.map +1 -1
  213. package/dist/store/clients/external-thread.js +260 -203
  214. package/dist/store/clients/external-thread.js.map +1 -1
  215. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  216. package/dist/store/clients/model-context-client.js +2 -1
  217. package/dist/store/clients/model-context-client.js.map +1 -1
  218. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  219. package/dist/store/clients/single-thread-list.js +1 -0
  220. package/dist/store/clients/single-thread-list.js.map +1 -1
  221. package/dist/store/clients/suggestions.d.ts.map +1 -1
  222. package/dist/store/clients/suggestions.js +5 -4
  223. package/dist/store/clients/suggestions.js.map +1 -1
  224. package/dist/store/clients/thread-tasks.d.ts +13 -0
  225. package/dist/store/clients/thread-tasks.d.ts.map +1 -0
  226. package/dist/store/clients/thread-tasks.js +69 -0
  227. package/dist/store/clients/thread-tasks.js.map +1 -0
  228. package/dist/store/index.d.ts +3 -2
  229. package/dist/store/index.js +3 -3
  230. package/dist/store/primitive-predicates.d.ts.map +1 -1
  231. package/dist/store/primitive-predicates.js +4 -4
  232. package/dist/store/primitive-predicates.js.map +1 -1
  233. package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -0
  234. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  235. package/dist/store/runtime-clients/composer-runtime-client.js +97 -61
  236. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  237. package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -0
  238. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  239. package/dist/store/runtime-clients/message-runtime-client.js +221 -144
  240. package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
  241. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  242. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  243. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  244. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  245. package/dist/store/runtime-clients/thread-list-runtime-client.js +54 -52
  246. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  247. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  248. package/dist/store/runtime-clients/thread-runtime-client.js +246 -126
  249. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  250. package/dist/store/scope-registration.d.ts +2 -0
  251. package/dist/store/scope-registration.d.ts.map +1 -1
  252. package/dist/store/scopes/composer.d.ts +10 -2
  253. package/dist/store/scopes/composer.d.ts.map +1 -1
  254. package/dist/store/scopes/message.d.ts +30 -1
  255. package/dist/store/scopes/message.d.ts.map +1 -1
  256. package/dist/store/scopes/task.d.ts +36 -0
  257. package/dist/store/scopes/task.d.ts.map +1 -0
  258. package/dist/store/scopes/task.js +0 -0
  259. package/dist/store/scopes/thread-list-item.d.ts +2 -2
  260. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  261. package/dist/store/scopes/thread.d.ts +28 -0
  262. package/dist/store/scopes/thread.d.ts.map +1 -1
  263. package/dist/store/scopes/threads.d.ts +1 -0
  264. package/dist/store/scopes/threads.d.ts.map +1 -1
  265. package/dist/subscribable/subscribable.d.ts +2 -1
  266. package/dist/subscribable/subscribable.d.ts.map +1 -1
  267. package/dist/subscribable/subscribable.js +23 -15
  268. package/dist/subscribable/subscribable.js.map +1 -1
  269. package/dist/types/attachment.d.ts +2 -1
  270. package/dist/types/attachment.d.ts.map +1 -1
  271. package/dist/types/attachment.js +2 -1
  272. package/dist/types/attachment.js.map +1 -1
  273. package/dist/types/index.d.ts +2 -2
  274. package/dist/types/message.d.ts +8 -1
  275. package/dist/types/message.d.ts.map +1 -1
  276. package/dist/types/message.js.map +1 -1
  277. package/dist/utils/abortable-iterable.d.ts +19 -0
  278. package/dist/utils/abortable-iterable.d.ts.map +1 -0
  279. package/dist/utils/abortable-iterable.js +88 -0
  280. package/dist/utils/abortable-iterable.js.map +1 -0
  281. package/dist/utils/composite-context-provider.d.ts.map +1 -1
  282. package/dist/utils/composite-context-provider.js +20 -2
  283. package/dist/utils/composite-context-provider.js.map +1 -1
  284. package/dist/utils/getGroupStatus.d.ts +18 -2
  285. package/dist/utils/getGroupStatus.d.ts.map +1 -1
  286. package/dist/utils/getGroupStatus.js +32 -7
  287. package/dist/utils/getGroupStatus.js.map +1 -1
  288. package/dist/utils/record.d.ts +11 -0
  289. package/dist/utils/record.d.ts.map +1 -0
  290. package/dist/utils/record.js +12 -0
  291. package/dist/utils/record.js.map +1 -0
  292. package/dist/utils/wire-media.d.ts.map +1 -1
  293. package/dist/utils/wire-media.js +2 -2
  294. package/dist/utils/wire-media.js.map +1 -1
  295. package/package.json +11 -11
  296. package/src/adapters/attachment.test.ts +50 -1
  297. package/src/adapters/attachment.ts +7 -4
  298. package/src/adapters/speech.test.ts +236 -0
  299. package/src/adapters/speech.ts +17 -10
  300. package/src/adapters/thread-history.ts +3 -0
  301. package/src/index.ts +2 -0
  302. package/src/internal.ts +4 -0
  303. package/src/model-context/frame/host.test.ts +137 -1
  304. package/src/model-context/frame/host.ts +49 -16
  305. package/src/model-context/frame/provider.test.ts +71 -0
  306. package/src/model-context/frame/provider.ts +37 -7
  307. package/src/model-context/interactable-composer-metadata.test.ts +58 -0
  308. package/src/model-context/interactable-composer-metadata.ts +19 -13
  309. package/src/model-context/registry.test.ts +12 -0
  310. package/src/model-context/registry.ts +3 -2
  311. package/src/model-context/types.test.ts +25 -0
  312. package/src/model-context/types.ts +9 -4
  313. package/src/react/RuntimeAdapter.test.ts +34 -0
  314. package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +244 -0
  315. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +104 -7
  316. package/src/react/client/DataRenderers.test.tsx +45 -0
  317. package/src/react/client/DataRenderers.ts +13 -9
  318. package/src/react/client/InMemoryThreadList.ts +1 -0
  319. package/src/react/client/Interactables.test.ts +72 -1
  320. package/src/react/client/Interactables.ts +22 -14
  321. package/src/react/client/RemoteThreadList.background.test.ts +148 -6
  322. package/src/react/client/RemoteThreadList.concurrent.test.tsx +42 -1
  323. package/src/react/client/RemoteThreadList.test.ts +386 -4
  324. package/src/react/client/RemoteThreadList.ts +181 -112
  325. package/src/react/client/Tools.ts +22 -11
  326. package/src/react/client/interactable-model-context.test.ts +44 -0
  327. package/src/react/client/interactable-model-context.ts +3 -2
  328. package/src/react/index.ts +5 -1
  329. package/src/react/interactables-legacy/Interactables.ts +16 -16
  330. package/src/react/interactables-legacy/useAssistantInteractable.test.tsx +132 -0
  331. package/src/react/interactables-legacy/useAssistantInteractable.ts +10 -1
  332. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +9 -10
  333. package/src/react/model-context/makeAssistantTool.ts +2 -2
  334. package/src/react/model-context/useAssistantTool.ts +2 -2
  335. package/src/react/model-context/useInteractable.test.tsx +177 -0
  336. package/src/react/model-context/useInteractable.ts +3 -0
  337. package/src/react/model-context/useToolArgsStatus.test.tsx +30 -0
  338. package/src/react/model-context/useToolArgsStatus.ts +2 -1
  339. package/src/react/primitive-hooks/useActionBarCopy.test.ts +0 -1
  340. package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +0 -1
  341. package/src/react/primitives/message/MessageGroupedParts.test.tsx +96 -0
  342. package/src/react/primitives/message/MessageGroupedParts.tsx +15 -4
  343. package/src/react/primitives/part/PartMessages.test.tsx +104 -1
  344. package/src/react/primitives/part/PartMessages.tsx +1 -0
  345. package/src/react/primitives/thread/ThreadMessages.test.tsx +142 -0
  346. package/src/react/primitives/thread/ThreadMessages.tsx +8 -5
  347. package/src/react/providers/ReadonlyThreadProvider.test.tsx +56 -0
  348. package/src/react/runtimes/RemoteThreadListHookInstanceManager.destroy.test.tsx +172 -0
  349. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +33 -0
  350. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +7 -1
  351. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +28 -5
  352. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.load-error.test.ts +132 -0
  353. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +105 -0
  354. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +143 -112
  355. package/src/react/runtimes/RemoteThreadResource.test.ts +9 -0
  356. package/src/react/runtimes/RemoteThreadResource.ts +19 -12
  357. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +20 -14
  358. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +703 -6
  359. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +492 -402
  360. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +77 -0
  361. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +27 -1
  362. package/src/react/runtimes/cloud/auiV0.ts +160 -25
  363. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +36 -0
  364. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +8 -1
  365. package/src/react/runtimes/cloud/sdkIdentity.ts +9 -0
  366. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +18 -4
  367. package/src/react/runtimes/external-message-converter.test.ts +136 -1
  368. package/src/react/runtimes/external-message-converter.ts +39 -93
  369. package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +41 -0
  370. package/src/react/runtimes/useExternalStoreRuntime.ts +10 -4
  371. package/src/react/runtimes/useLocalRuntime.test.tsx +20 -0
  372. package/src/react/runtimes/useRemoteThreadListRuntime.load-error.test.tsx +108 -0
  373. package/src/react/runtimes/useRemoteThreadListRuntime.ts +34 -1
  374. package/src/react/utils/groupParts.ts +12 -1
  375. package/src/react/utils/useJSONEqual.ts +9 -0
  376. package/src/react/utils/useJSONSchemaDependency.ts +22 -0
  377. package/src/runtime/api/message-runtime.test.ts +75 -0
  378. package/src/runtime/api/thread-list-item-runtime.ts +10 -3
  379. package/src/runtime/api/thread-list-runtime.ts +21 -11
  380. package/src/runtime/api/thread-runtime.test.ts +136 -0
  381. package/src/runtime/api/thread-runtime.ts +10 -5
  382. package/src/runtime/base/base-composer-runtime-core.ts +105 -24
  383. package/src/runtime/base/base-thread-runtime-core.test.ts +1063 -2
  384. package/src/runtime/base/base-thread-runtime-core.ts +228 -87
  385. package/src/runtime/interfaces/composer-runtime-core.ts +10 -5
  386. package/src/runtime/interfaces/thread-list-runtime-core.ts +5 -1
  387. package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
  388. package/src/runtime/queue/message-queue.ts +68 -6
  389. package/src/runtime/utils/auto-status.test.ts +112 -1
  390. package/src/runtime/utils/auto-status.ts +31 -5
  391. package/src/runtime/utils/external-message-conversion.test.ts +202 -0
  392. package/src/runtime/utils/external-message-conversion.ts +123 -32
  393. package/src/runtime/utils/message-repository.test.ts +49 -0
  394. package/src/runtime/utils/message-repository.ts +32 -20
  395. package/src/runtime/utils/thread-message-like.test.ts +52 -0
  396. package/src/runtime/utils/thread-message-like.ts +5 -1
  397. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +18 -13
  398. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +31 -1
  399. package/src/runtimes/internal.ts +1 -0
  400. package/src/runtimes/local/local-thread-runtime-core.test.ts +21 -0
  401. package/src/runtimes/local/local-thread-runtime-core.ts +11 -1
  402. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +128 -2
  403. package/src/runtimes/remote-thread-list/remote-thread-state.ts +182 -42
  404. package/src/runtimes/remote-thread-list/title-generation.test.ts +602 -0
  405. package/src/runtimes/remote-thread-list/title-generation.ts +329 -0
  406. package/src/runtimes/remote-thread-list/title.test.ts +62 -0
  407. package/src/runtimes/remote-thread-list/title.ts +13 -0
  408. package/src/runtimes/remote-thread-list/types.ts +10 -0
  409. package/src/runtimes/tool-invocations/EDGE_CASES.md +2 -6
  410. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +133 -0
  411. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +10 -6
  412. package/src/store/clients/external-thread.ts +67 -4
  413. package/src/store/clients/model-context-client.ts +5 -1
  414. package/src/store/clients/single-thread-list.ts +1 -0
  415. package/src/store/clients/suggestions.test.ts +40 -0
  416. package/src/store/clients/suggestions.ts +13 -10
  417. package/src/store/clients/thread-tasks.test.ts +274 -0
  418. package/src/store/clients/thread-tasks.ts +138 -0
  419. package/src/store/index.ts +8 -0
  420. package/src/store/primitive-predicates.test.ts +48 -5
  421. package/src/store/primitive-predicates.ts +7 -2
  422. package/src/store/runtime-clients/composer-runtime-client.ts +42 -9
  423. package/src/store/runtime-clients/message-runtime-client.ts +54 -6
  424. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +4 -2
  425. package/src/store/runtime-clients/thread-list-runtime-client.ts +1 -0
  426. package/src/store/runtime-clients/thread-runtime-client.ts +89 -11
  427. package/src/store/scope-registration.ts +2 -0
  428. package/src/store/scopes/composer.ts +16 -4
  429. package/src/store/scopes/message.ts +14 -0
  430. package/src/store/scopes/task.ts +34 -0
  431. package/src/store/scopes/thread-list-item.ts +5 -2
  432. package/src/store/scopes/thread.ts +20 -0
  433. package/src/store/scopes/threads.ts +1 -0
  434. package/src/store/thread-tasks.test.tsx +209 -0
  435. package/src/subscribable/subscribable.test.ts +156 -1
  436. package/src/subscribable/subscribable.ts +25 -15
  437. package/src/tests/MessageRepository.test.ts +117 -5
  438. package/src/tests/OptimisticState-list-race.test.ts +1 -0
  439. package/src/tests/RemoteThreadListThreadListRuntimeCore-initialize-adapter-change.test.ts +38 -0
  440. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +47 -0
  441. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +89 -0
  442. package/src/tests/attachment-adapters.test.ts +14 -0
  443. package/src/tests/auiV0Encode.test.ts +300 -0
  444. package/src/tests/base-composer-runtime-core-send.test.ts +51 -0
  445. package/src/tests/base-composer-runtime-core.test.ts +353 -2
  446. package/src/tests/external-store-thread-list-runtime-core.test.ts +83 -0
  447. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +178 -18
  448. package/src/tests/external-thread-attachments.test.tsx +208 -0
  449. package/src/tests/external-thread-parity.test.tsx +57 -0
  450. package/src/tests/groupParts.test.ts +71 -0
  451. package/src/tests/message-queue.test.ts +253 -0
  452. package/src/tests/remote-thread-list-isLoading.test.ts +1 -0
  453. package/src/tests/remote-thread-list-reactive-threadId.test.ts +51 -0
  454. package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
  455. package/src/tests/thread-switch-events.test.tsx +78 -0
  456. package/src/tests/tools-scope-migration.test.tsx +28 -0
  457. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +43 -0
  458. package/src/types/attachment.ts +4 -0
  459. package/src/types/index.ts +1 -0
  460. package/src/types/message.ts +8 -0
  461. package/src/utils/abortable-iterable.test.ts +328 -0
  462. package/src/utils/abortable-iterable.ts +104 -0
  463. package/src/utils/composite-context-provider.test.ts +25 -0
  464. package/src/utils/composite-context-provider.ts +23 -2
  465. package/src/utils/getGroupStatus.test.ts +70 -4
  466. package/src/utils/getGroupStatus.ts +35 -12
  467. package/src/utils/record.test.ts +48 -0
  468. package/src/utils/record.ts +11 -0
  469. package/src/utils/wire-media.test.ts +10 -0
  470. package/src/utils/wire-media.ts +6 -2
@@ -18,6 +18,7 @@ import type { ModelContext } from "../../model-context/types";
18
18
  import type { MessageMethods, MessageState } from "./message";
19
19
  import type { ComposerMethods, ComposerState } from "./composer";
20
20
  import type { SuggestionsMethods } from "./suggestions";
21
+ import type { TaskMethods, TaskState } from "./task";
21
22
 
22
23
  export type ThreadState = {
23
24
  /**
@@ -44,6 +45,10 @@ export type ThreadState = {
44
45
  * The messages in the currently selected branch of the thread.
45
46
  */
46
47
  readonly messages: readonly MessageState[];
48
+ /**
49
+ * Child work derived from the thread's tool calls: every tool call that carries a nested conversation, in document order with nested tasks after their parent. The array keeps its identity while no task changed.
50
+ */
51
+ readonly tasks: readonly TaskState[];
47
52
  /**
48
53
  * The thread state.
49
54
  * @deprecated This feature is experimental
@@ -76,6 +81,10 @@ export type ThreadMethods = {
76
81
  * The suggestions shown for this thread.
77
82
  */
78
83
  suggestions(): SuggestionsMethods;
84
+ /**
85
+ * Access a task by index or toolCallId; an id resolves the first task with that toolCallId in document order.
86
+ */
87
+ task(selector: { index: number } | { id: string }): TaskMethods;
79
88
  /**
80
89
  * Append a new message to the thread.
81
90
  *
@@ -144,6 +153,13 @@ export type ThreadMeta = {
144
153
  };
145
154
 
146
155
  export type ThreadEvents = {
156
+ "thread.toolApprovalAnswered": {
157
+ threadId: string;
158
+ messageId: string;
159
+ toolCallId: string;
160
+ toolName: string;
161
+ approved: boolean;
162
+ };
147
163
  /**
148
164
  * A run started on this thread. Also observable as `isRunning` flipping to
149
165
  * `true` in thread state.
@@ -155,6 +171,10 @@ export type ThreadEvents = {
155
171
  * state.
156
172
  */
157
173
  "thread.runEnd": { threadId: string };
174
+ /** The user stopped the run in progress on this thread. */
175
+ "thread.cancelRun": { threadId: string };
176
+ /** The user started a voice session on this thread. */
177
+ "thread.voiceStarted": { threadId: string };
158
178
  /**
159
179
  * The thread transitioned from new to initialized. Fires before the first
160
180
  * message is added, so read thread state via `useAuiState` rather than
@@ -9,6 +9,7 @@ export type ThreadsState = {
9
9
  readonly mainThreadId: string;
10
10
  readonly newThreadId: string | null;
11
11
  readonly isLoading: boolean;
12
+ readonly loadError: unknown;
12
13
  readonly isLoadingMore: boolean;
13
14
  readonly hasMore: boolean;
14
15
  readonly threadIds: readonly string[];
@@ -0,0 +1,209 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, render, waitFor } from "@testing-library/react";
4
+ import { useAui, useAuiState } from "@assistant-ui/store";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import type { ThreadMessage } from "../types/message";
7
+ import { AssistantRuntimeProvider } from "../react/AssistantRuntimeProvider";
8
+ import { useExternalStoreRuntime } from "../react/runtimes/useExternalStoreRuntime";
9
+ import type { TaskState } from "./scopes/task";
10
+
11
+ const taskMessage = (
12
+ nestedStatus: { type: "running" } | { type: "complete"; reason: "stop" },
13
+ result?: unknown,
14
+ ) =>
15
+ ({
16
+ id: "message-1",
17
+ role: "assistant",
18
+ createdAt: new Date(0),
19
+ content: [
20
+ {
21
+ type: "tool-call",
22
+ toolCallId: "delegate-1",
23
+ toolName: "delegate",
24
+ args: { topic: "research" },
25
+ argsText: '{"topic":"research"}',
26
+ ...(result !== undefined ? { result } : undefined),
27
+ messages: [
28
+ {
29
+ id: "nested-message-1",
30
+ role: "assistant",
31
+ createdAt: new Date(0),
32
+ content: [],
33
+ status: nestedStatus,
34
+ metadata: {
35
+ unstable_state: {},
36
+ unstable_annotations: [],
37
+ unstable_data: [],
38
+ steps: [],
39
+ custom: {},
40
+ },
41
+ },
42
+ ],
43
+ },
44
+ ],
45
+ status: nestedStatus,
46
+ metadata: {
47
+ unstable_state: {},
48
+ unstable_annotations: [],
49
+ unstable_data: [],
50
+ steps: [],
51
+ custom: {},
52
+ },
53
+ }) as ThreadMessage;
54
+
55
+ afterEach(cleanup);
56
+
57
+ describe("thread tasks", () => {
58
+ it("exposes task state and accessors through an external-store runtime", async () => {
59
+ const captured: {
60
+ tasks?: readonly TaskState[];
61
+ byId?: TaskState;
62
+ byIndex?: TaskState;
63
+ } = {};
64
+
65
+ const Probe = () => {
66
+ const tasks = useAuiState((s) => s.thread.tasks);
67
+ const aui = useAui();
68
+ captured.tasks = tasks;
69
+ if (tasks.length > 0) {
70
+ captured.byId = aui.thread.task({ id: "delegate-1" }).getState();
71
+ captured.byIndex = aui.thread.task({ index: 0 }).getState();
72
+ }
73
+ return null;
74
+ };
75
+
76
+ const App = ({ messages }: { messages: readonly ThreadMessage[] }) => {
77
+ const runtime = useExternalStoreRuntime({
78
+ messages,
79
+ convertMessage: (message) => message,
80
+ onNew: async () => {},
81
+ });
82
+ return (
83
+ <AssistantRuntimeProvider runtime={runtime}>
84
+ <Probe />
85
+ </AssistantRuntimeProvider>
86
+ );
87
+ };
88
+
89
+ const view = render(<App messages={[taskMessage({ type: "running" })]} />);
90
+
91
+ await waitFor(() => expect(captured.tasks).toHaveLength(1));
92
+ expect(captured.tasks?.[0]).toMatchObject({
93
+ status: { type: "running" },
94
+ toolName: "delegate",
95
+ });
96
+ expect(captured.byId).toMatchObject({ messageId: "message-1" });
97
+ expect(captured.byIndex).toBe(captured.byId);
98
+
99
+ view.rerender(
100
+ <App
101
+ messages={[taskMessage({ type: "complete", reason: "stop" }, "done")]}
102
+ />,
103
+ );
104
+
105
+ await waitFor(() =>
106
+ expect(captured.tasks?.[0]?.status).toEqual({ type: "complete" }),
107
+ );
108
+ expect(captured.tasks?.[0]).toMatchObject({
109
+ toolName: "delegate",
110
+ messageId: "message-1",
111
+ result: "done",
112
+ });
113
+ expect(captured.tasks?.[0]?.messages).toHaveLength(1);
114
+ });
115
+
116
+ it("survives nested payloads that repeat ids and resolves the first task in document order", async () => {
117
+ const nested = (id: string, toolCallId: string) =>
118
+ ({
119
+ id,
120
+ role: "assistant",
121
+ createdAt: new Date(0),
122
+ content: [
123
+ {
124
+ type: "tool-call",
125
+ toolCallId,
126
+ toolName: "search",
127
+ args: {},
128
+ argsText: "{}",
129
+ result: "ok",
130
+ messages: [],
131
+ },
132
+ ],
133
+ status: { type: "complete", reason: "stop" },
134
+ metadata: {
135
+ unstable_state: {},
136
+ unstable_annotations: [],
137
+ unstable_data: [],
138
+ steps: [],
139
+ custom: {},
140
+ },
141
+ }) as ThreadMessage;
142
+ const message = {
143
+ id: "message-1",
144
+ role: "assistant",
145
+ createdAt: new Date(0),
146
+ content: [
147
+ {
148
+ type: "tool-call",
149
+ toolCallId: "delegate-a",
150
+ toolName: "delegate",
151
+ args: {},
152
+ argsText: "{}",
153
+ result: "ok",
154
+ messages: [nested("nested", "call_1")],
155
+ },
156
+ {
157
+ type: "tool-call",
158
+ toolCallId: "delegate-b",
159
+ toolName: "delegate",
160
+ args: {},
161
+ argsText: "{}",
162
+ result: "ok",
163
+ messages: [nested("nested", "call_1")],
164
+ },
165
+ ],
166
+ status: { type: "complete", reason: "stop" },
167
+ metadata: {
168
+ unstable_state: {},
169
+ unstable_annotations: [],
170
+ unstable_data: [],
171
+ steps: [],
172
+ custom: {},
173
+ },
174
+ } as ThreadMessage;
175
+ const captured: { ids?: readonly string[]; first?: TaskState } = {};
176
+ const Probe = () => {
177
+ const tasks = useAuiState((s) => s.thread.tasks);
178
+ const aui = useAui();
179
+ captured.ids = tasks.map((task) => task.id);
180
+ if (tasks.length === 4) {
181
+ captured.first = aui.thread.task({ id: "call_1" }).getState();
182
+ }
183
+ return null;
184
+ };
185
+ const App = () => {
186
+ const runtime = useExternalStoreRuntime({
187
+ messages: [message],
188
+ convertMessage: (m) => m,
189
+ onNew: async () => {},
190
+ });
191
+ return (
192
+ <AssistantRuntimeProvider runtime={runtime}>
193
+ <Probe />
194
+ </AssistantRuntimeProvider>
195
+ );
196
+ };
197
+
198
+ render(<App />);
199
+
200
+ await waitFor(() => expect(captured.ids).toHaveLength(4));
201
+ expect(captured.ids).toEqual([
202
+ "delegate-a",
203
+ "call_1",
204
+ "delegate-b",
205
+ "call_1",
206
+ ]);
207
+ expect(captured.first).toMatchObject({ parentTaskId: "delegate-a" });
208
+ });
209
+ });
@@ -1,6 +1,12 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import type { SubscribableWithState } from "./subscribable";
3
- import { LazyMemoizeSubject, ShallowMemoizeSubject } from "./subscribable";
3
+ import {
4
+ EventSubscriptionSubject,
5
+ LazyMemoizeSubject,
6
+ NestedSubscriptionSubject,
7
+ runCleanups,
8
+ ShallowMemoizeSubject,
9
+ } from "./subscribable";
4
10
 
5
11
  type TestState = {
6
12
  status: string;
@@ -56,6 +62,57 @@ const createRebuildingBinding = (initialState: TestState) => {
56
62
  };
57
63
  };
58
64
 
65
+ describe("runCleanups", () => {
66
+ it("runs every cleanup before rethrowing an error", () => {
67
+ const cleanupError = new Error("cleanup failed");
68
+ const laterCleanup = vi.fn();
69
+
70
+ expect(() =>
71
+ runCleanups([
72
+ () => {
73
+ throw cleanupError;
74
+ },
75
+ laterCleanup,
76
+ ]),
77
+ ).toThrow(cleanupError);
78
+ expect(laterCleanup).toHaveBeenCalledOnce();
79
+ });
80
+
81
+ it("preserves every error when multiple cleanups throw", () => {
82
+ const firstError = new Error("first cleanup failed");
83
+ const secondError = new Error("second cleanup failed");
84
+ const consoleError = vi
85
+ .spyOn(console, "error")
86
+ .mockImplementation(() => {});
87
+
88
+ try {
89
+ let thrown: unknown;
90
+ try {
91
+ runCleanups([
92
+ () => {
93
+ throw firstError;
94
+ },
95
+ () => {
96
+ throw secondError;
97
+ },
98
+ ]);
99
+ } catch (error) {
100
+ thrown = error;
101
+ }
102
+
103
+ expect(thrown).toBeInstanceOf(AggregateError);
104
+ expect((thrown as AggregateError).errors).toEqual([
105
+ firstError,
106
+ secondError,
107
+ ]);
108
+ expect(consoleError).toHaveBeenNthCalledWith(1, firstError);
109
+ expect(consoleError).toHaveBeenNthCalledWith(2, secondError);
110
+ } finally {
111
+ consoleError.mockRestore();
112
+ }
113
+ });
114
+ });
115
+
59
116
  describe("ShallowMemoizeSubject", () => {
60
117
  it("notifies subscribers when a state key is removed", () => {
61
118
  const source = createBinding({
@@ -110,6 +167,28 @@ describe("ShallowMemoizeSubject", () => {
110
167
  expect(subject.getState()).toEqual({ status: "ready" });
111
168
  expect(subscriber).not.toHaveBeenCalled();
112
169
  });
170
+
171
+ it("reconnects after the previous connection cleanup throws", () => {
172
+ let subscribeCount = 0;
173
+ const cleanupError = new Error("cleanup failed");
174
+ const binding: SubscribableWithState<TestState, null> = {
175
+ path: null,
176
+ getState: () => ({ status: "ready" }),
177
+ subscribe: () => {
178
+ subscribeCount += 1;
179
+ return () => {
180
+ throw cleanupError;
181
+ };
182
+ },
183
+ };
184
+ const subject = new ShallowMemoizeSubject(binding);
185
+
186
+ const unsubscribe = subject.subscribe(() => {});
187
+ expect(() => unsubscribe()).toThrow(cleanupError);
188
+
189
+ subject.subscribe(() => {});
190
+ expect(subscribeCount).toBe(2);
191
+ });
113
192
  });
114
193
 
115
194
  describe("LazyMemoizeSubject", () => {
@@ -182,3 +261,79 @@ describe("LazyMemoizeSubject", () => {
182
261
  expect(second).toEqual({ status: "done" });
183
262
  });
184
263
  });
264
+
265
+ describe("nested subscription swaps", () => {
266
+ it("connects the next nested source when the previous cleanup throws", () => {
267
+ const cleanupError = new Error("cleanup failed");
268
+ let outerUpdate!: () => void;
269
+ let nextUpdate!: () => void;
270
+ const previous = {
271
+ subscribe: () => () => {
272
+ throw cleanupError;
273
+ },
274
+ };
275
+ const nextSubscribe = vi.fn((callback: () => void) => {
276
+ nextUpdate = callback;
277
+ return () => {};
278
+ });
279
+ const next = { subscribe: nextSubscribe };
280
+ let current = previous;
281
+ const subject = new NestedSubscriptionSubject({
282
+ path: null,
283
+ getState: () => current,
284
+ subscribe: (callback) => {
285
+ outerUpdate = callback;
286
+ return () => {};
287
+ },
288
+ });
289
+ const listener = vi.fn();
290
+ subject.subscribe(listener);
291
+
292
+ current = next;
293
+ expect(() => outerUpdate()).toThrow(cleanupError);
294
+ expect(nextSubscribe).toHaveBeenCalledOnce();
295
+ expect(listener).toHaveBeenCalledOnce();
296
+
297
+ nextUpdate();
298
+ expect(listener).toHaveBeenCalledTimes(2);
299
+ });
300
+
301
+ it("connects the next event source when the previous cleanup throws", () => {
302
+ const cleanupError = new Error("cleanup failed");
303
+ let outerUpdate!: () => void;
304
+ let nextEvent!: (payload?: unknown) => void;
305
+ const previous = {
306
+ unstable_on: () => () => {
307
+ throw cleanupError;
308
+ },
309
+ };
310
+ const nextSubscribe = vi.fn(
311
+ (_event: string, callback: (payload?: unknown) => void) => {
312
+ nextEvent = callback;
313
+ return () => {};
314
+ },
315
+ );
316
+ const next = { unstable_on: nextSubscribe };
317
+ let current = previous;
318
+ const subject = new EventSubscriptionSubject({
319
+ event: "test",
320
+ binding: {
321
+ path: null,
322
+ getState: () => current,
323
+ subscribe: (callback) => {
324
+ outerUpdate = callback;
325
+ return () => {};
326
+ },
327
+ },
328
+ });
329
+ const listener = vi.fn();
330
+ subject.subscribe(listener);
331
+
332
+ current = next;
333
+ expect(() => outerUpdate()).toThrow(cleanupError);
334
+ expect(nextSubscribe).toHaveBeenCalledOnce();
335
+
336
+ nextEvent("payload");
337
+ expect(listener).toHaveBeenCalledWith("payload");
338
+ });
339
+ });
@@ -58,6 +58,10 @@ export const notifySubscribers = <TArgs extends unknown[]>(
58
58
  }
59
59
  };
60
60
 
61
+ export const runCleanups = (cleanups: Iterable<Unsubscribe>): void => {
62
+ notifySubscribers(cleanups);
63
+ };
64
+
61
65
  const shallowEqualOrUndefined = <T extends object>(
62
66
  a: T | undefined,
63
67
  b: T | undefined,
@@ -136,8 +140,9 @@ export abstract class BaseSubject {
136
140
  if (this._connection) return;
137
141
  this._connection = this._connect();
138
142
  } else {
139
- this._connection?.();
143
+ const connection = this._connection;
140
144
  this._connection = undefined;
145
+ connection?.();
141
146
  }
142
147
  }
143
148
 
@@ -284,17 +289,19 @@ export class NestedSubscriptionSubject<
284
289
  if (newState === lastState) return;
285
290
  lastState = newState;
286
291
 
287
- innerUnsubscribe?.();
288
- innerUnsubscribe = newState?.subscribe(callback);
289
-
290
- callback();
292
+ const previousInner = innerUnsubscribe;
293
+ innerUnsubscribe = undefined;
294
+ try {
295
+ previousInner?.();
296
+ } finally {
297
+ innerUnsubscribe = newState?.subscribe(callback);
298
+ callback();
299
+ }
291
300
  };
292
301
 
293
302
  const outerUnsubscribe = this.outerSubscribe(onRuntimeUpdate);
294
- return () => {
295
- outerUnsubscribe?.();
296
- innerUnsubscribe?.();
297
- };
303
+ return () =>
304
+ runCleanups([() => outerUnsubscribe?.(), () => innerUnsubscribe?.()]);
298
305
  }
299
306
  }
300
307
 
@@ -329,14 +336,17 @@ export class EventSubscriptionSubject<
329
336
  if (newState === lastState) return;
330
337
  lastState = newState;
331
338
 
332
- innerUnsubscribe?.();
333
- innerUnsubscribe = newState?.unstable_on(this.config.event, callback);
339
+ const previousInner = innerUnsubscribe;
340
+ innerUnsubscribe = undefined;
341
+ try {
342
+ previousInner?.();
343
+ } finally {
344
+ innerUnsubscribe = newState?.unstable_on(this.config.event, callback);
345
+ }
334
346
  };
335
347
 
336
348
  const outerUnsubscribe = this.outerSubscribe(onRuntimeUpdate);
337
- return () => {
338
- outerUnsubscribe?.();
339
- innerUnsubscribe?.();
340
- };
349
+ return () =>
350
+ runCleanups([() => outerUnsubscribe?.(), () => innerUnsubscribe?.()]);
341
351
  }
342
352
  }
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
2
  import {
3
3
  MessageRepository,
4
4
  ExportedMessageRepository,
@@ -59,10 +59,6 @@ describe("MessageRepository", () => {
59
59
  mockGenerateId.mockImplementation(() => `mock-id-${nextMockId++}`);
60
60
  });
61
61
 
62
- afterEach(() => {
63
- vi.clearAllMocks();
64
- });
65
-
66
62
  // Core functionality tests - these test the public contract
67
63
  describe("Basic CRUD operations", () => {
68
64
  it("should add a new message to the repository", () => {
@@ -240,6 +236,80 @@ describe("MessageRepository", () => {
240
236
  expect(messages2.map((m) => m.id)).toEqual(["parent-id", "branch2-id"]);
241
237
  });
242
238
 
239
+ it("keeps a nested branch selected when revisiting its ancestor", () => {
240
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "r" }));
241
+ repository.addOrUpdateMessage("r", createTestMessage({ id: "a" }));
242
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "x" }));
243
+ repository.addOrUpdateMessage("r", createTestMessage({ id: "b" }));
244
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "y" }));
245
+
246
+ repository.switchToBranch("y");
247
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
248
+ "r",
249
+ "b",
250
+ "y",
251
+ ]);
252
+
253
+ repository.switchToBranch("r");
254
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
255
+ "r",
256
+ "b",
257
+ "y",
258
+ ]);
259
+
260
+ repository.switchToBranch("x");
261
+ repository.switchToBranch("r");
262
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
263
+ "r",
264
+ "a",
265
+ "x",
266
+ ]);
267
+ });
268
+
269
+ it("keeps the selected root branch when deletion selects from the root", () => {
270
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "a" }));
271
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "x" }));
272
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "b" }));
273
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "y" }));
274
+
275
+ repository.switchToBranch("y");
276
+ repository.deleteMessage("y", null);
277
+
278
+ expect(repository.getMessages().map((m) => m.id)).toEqual(["b"]);
279
+ });
280
+
281
+ it("keeps the selected root branch after relinking a message", () => {
282
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "a" }));
283
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "m" }));
284
+ repository.addOrUpdateMessage("m", createTestMessage({ id: "x" }));
285
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "b" }));
286
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "m" }));
287
+
288
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
289
+ "b",
290
+ "m",
291
+ "x",
292
+ ]);
293
+
294
+ repository.deleteMessage("x", null);
295
+
296
+ expect(repository.getMessages().map((m) => m.id)).toEqual(["b", "m"]);
297
+ });
298
+
299
+ it("keeps the selected branch when relinking an off-branch message", () => {
300
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "a" }));
301
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "m" }));
302
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "n" }));
303
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "b" }));
304
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "n" }));
305
+
306
+ expect(repository.getMessages().map((m) => m.id)).toEqual(["a", "m"]);
307
+
308
+ repository.deleteMessage("m", null);
309
+
310
+ expect(repository.getMessages().map((m) => m.id)).toEqual(["a"]);
311
+ });
312
+
243
313
  it("should operate on a long message history without overflowing the stack", () => {
244
314
  const messageCount = 30_000;
245
315
  const messages = createLongBranchMessages(messageCount);
@@ -486,6 +556,31 @@ describe("MessageRepository", () => {
486
556
  ).toBe("parent-id");
487
557
  });
488
558
 
559
+ it("round-trips after reparenting under a later-added ancestor", () => {
560
+ const messageA = createTestMessage({ id: "A" });
561
+ repository.addOrUpdateMessage(null, messageA);
562
+ repository.addOrUpdateMessage("A", createTestMessage({ id: "B" }));
563
+ repository.addOrUpdateMessage("A", createTestMessage({ id: "C" }));
564
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "X" }));
565
+ repository.addOrUpdateMessage("X", messageA);
566
+
567
+ const exported = repository.export();
568
+ expect(exported.messages.map((m) => m.message.id)).toEqual([
569
+ "X",
570
+ "A",
571
+ "B",
572
+ "C",
573
+ ]);
574
+
575
+ const restored = new MessageRepository();
576
+ restored.import(exported);
577
+
578
+ expect(restored.headId).toBe("B");
579
+ expect(restored.getMessages().map((m) => m.id)).toEqual(["X", "A", "B"]);
580
+ restored.switchToBranch("C");
581
+ expect(restored.getMessages().map((m) => m.id)).toEqual(["X", "A", "C"]);
582
+ });
583
+
489
584
  it("should import repository state", () => {
490
585
  const parent = createTestMessage({ id: "parent-id" });
491
586
  const child = createTestMessage({ id: "child-id" });
@@ -941,5 +1036,22 @@ describe("MessageRepository", () => {
941
1036
 
942
1037
  expect(repository.headId).toBe("C");
943
1038
  });
1039
+
1040
+ it("should advance the head when re-parenting onto the current head", () => {
1041
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "a" }));
1042
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "h" }));
1043
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "b" }));
1044
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "c" }));
1045
+
1046
+ repository.addOrUpdateMessage("h", createTestMessage({ id: "c" }));
1047
+
1048
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
1049
+ "a",
1050
+ "h",
1051
+ "c",
1052
+ ]);
1053
+ expect(repository.headId).toBe("c");
1054
+ expect(repository.getMessage("c").parentId).toBe("h");
1055
+ });
944
1056
  });
945
1057
  });
@@ -22,6 +22,7 @@ import { deferred } from "./remote-thread-list-test-helpers";
22
22
 
23
23
  const EMPTY_STATE: RemoteThreadState = {
24
24
  isLoading: false,
25
+ loadError: undefined,
25
26
  isLoadingMore: false,
26
27
  cursor: undefined,
27
28
  newThreadId: undefined,