@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
@@ -0,0 +1,216 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, render } from "@testing-library/react";
4
+ import { startTransition, Suspense, type ReactNode } from "react";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
7
+ import type { AppendMessage } from "../../types/message";
8
+ import { makeAdapter } from "../../tests/remote-thread-list-test-helpers";
9
+ import { AssistantRuntimeProvider } from "../AssistantRuntimeProvider";
10
+ import { useExternalStoreRuntime } from "./useExternalStoreRuntime";
11
+ import { useRemoteThreadListRuntime } from "./useRemoteThreadListRuntime";
12
+
13
+ const EMPTY_MESSAGES: readonly never[] = [];
14
+
15
+ const userMessage = (text: string): AppendMessage => ({
16
+ parentId: null,
17
+ sourceId: null,
18
+ runConfig: {},
19
+ role: "user",
20
+ content: [{ type: "text", text }],
21
+ attachments: [],
22
+ metadata: { custom: {} },
23
+ createdAt: new Date(),
24
+ startRun: true,
25
+ });
26
+
27
+ const getThreadCore = (runtime: AssistantRuntime) =>
28
+ (
29
+ runtime.thread as unknown as {
30
+ __internal_threadBinding: {
31
+ getState(): { append(message: AppendMessage): Promise<void> };
32
+ };
33
+ }
34
+ ).__internal_threadBinding.getState();
35
+
36
+ const createHarness = ({ stableHook = false } = {}) => {
37
+ const adapter = makeAdapter();
38
+ const onNewA = vi.fn(async () => {});
39
+ const onNewB = vi.fn(async () => {});
40
+ const renderB = vi.fn();
41
+ const renderThreadRuntime = vi.fn();
42
+ const runtimeRef: { current: AssistantRuntime | null } = { current: null };
43
+ const pending = new Promise<never>(() => {});
44
+ let suspend = false;
45
+
46
+ const Blocker = () => {
47
+ if (suspend) throw pending;
48
+ return null;
49
+ };
50
+ // Every shipped adapter passes a fresh function expression, so a hoisted
51
+ // variant is the only way to exercise an unchanged published hook.
52
+ const useHoistedThreadRuntime = () => {
53
+ renderThreadRuntime();
54
+ return useExternalStoreRuntime({
55
+ messages: EMPTY_MESSAGES,
56
+ onNew: onNewA,
57
+ });
58
+ };
59
+ const App = ({
60
+ onNew,
61
+ children,
62
+ }: {
63
+ onNew: typeof onNewA;
64
+ children?: ReactNode;
65
+ }) => {
66
+ if (onNew === onNewB) renderB();
67
+ const useThreadRuntime = stableHook
68
+ ? useHoistedThreadRuntime
69
+ : () => {
70
+ renderThreadRuntime();
71
+ return useExternalStoreRuntime({ messages: EMPTY_MESSAGES, onNew });
72
+ };
73
+ const runtime = useRemoteThreadListRuntime({
74
+ adapter,
75
+ runtimeHook: useThreadRuntime,
76
+ });
77
+ runtimeRef.current = runtime;
78
+ return (
79
+ <AssistantRuntimeProvider runtime={runtime}>
80
+ {children ?? <Blocker />}
81
+ </AssistantRuntimeProvider>
82
+ );
83
+ };
84
+
85
+ return {
86
+ App,
87
+ onNewA,
88
+ onNewB,
89
+ renderB,
90
+ renderThreadRuntime,
91
+ runtimeRef,
92
+ suspend: () => {
93
+ suspend = true;
94
+ },
95
+ };
96
+ };
97
+
98
+ describe("useRemoteThreadListRuntime concurrent options", () => {
99
+ it("keeps new threads on the committed runtime hook", async () => {
100
+ const { App, onNewA, onNewB, renderB, runtimeRef, suspend } =
101
+ createHarness();
102
+ const view = render(
103
+ <Suspense fallback={null}>
104
+ <App onNew={onNewA} />
105
+ </Suspense>,
106
+ );
107
+
108
+ await act(async () => {
109
+ await getThreadCore(runtimeRef.current!).append(userMessage("first"));
110
+ });
111
+
112
+ act(() => {
113
+ suspend();
114
+ startTransition(() =>
115
+ view.rerender(
116
+ <Suspense fallback={null}>
117
+ <App onNew={onNewB} />
118
+ </Suspense>,
119
+ ),
120
+ );
121
+ });
122
+ expect(renderB).toHaveBeenCalled();
123
+
124
+ await act(async () => {
125
+ await runtimeRef.current!.threads.switchToNewThread();
126
+ await getThreadCore(runtimeRef.current!).append(userMessage("second"));
127
+ });
128
+
129
+ expect(onNewA).toHaveBeenCalledTimes(2);
130
+ expect(onNewB).not.toHaveBeenCalled();
131
+ });
132
+
133
+ it("publishes runtime hook changes after a committed render", async () => {
134
+ const { App, onNewA, onNewB, runtimeRef } = createHarness();
135
+ const view = render(<App onNew={onNewA} />);
136
+
137
+ await act(async () => {
138
+ await getThreadCore(runtimeRef.current!).append(userMessage("first"));
139
+ });
140
+
141
+ view.rerender(<App onNew={onNewB} />);
142
+ await act(async () => {
143
+ await getThreadCore(runtimeRef.current!).append(userMessage("second"));
144
+ });
145
+
146
+ expect(onNewA).toHaveBeenCalledTimes(1);
147
+ expect(onNewB).toHaveBeenCalledTimes(1);
148
+ });
149
+
150
+ it("costs two passes per provider commit in the documented shape", async () => {
151
+ // Fresh children on every render, as the AssistantRuntimeProvider JSDoc
152
+ // example does, so its memo does not block: the host renders once against
153
+ // the previously committed hook, then the publish adds the corrective pass.
154
+ const { App, onNewA, renderThreadRuntime } = createHarness();
155
+ const view = render(<App onNew={onNewA} />);
156
+
157
+ await act(async () => {});
158
+ renderThreadRuntime.mockClear();
159
+
160
+ await act(async () => {
161
+ view.rerender(<App onNew={onNewA} />);
162
+ });
163
+
164
+ expect(renderThreadRuntime).toHaveBeenCalledTimes(2);
165
+ });
166
+
167
+ it("costs one pass when the provider memo blocks the host render", async () => {
168
+ const { App, onNewA, renderThreadRuntime } = createHarness();
169
+ const children = <div />;
170
+ const view = render(<App onNew={onNewA}>{children}</App>);
171
+
172
+ await act(async () => {});
173
+ renderThreadRuntime.mockClear();
174
+
175
+ await act(async () => {
176
+ view.rerender(<App onNew={onNewA}>{children}</App>);
177
+ });
178
+
179
+ expect(renderThreadRuntime).toHaveBeenCalledTimes(1);
180
+ });
181
+
182
+ it("does not re-run thread runtimes when the published hook is unchanged", async () => {
183
+ const { App, onNewA, renderThreadRuntime } = createHarness({
184
+ stableHook: true,
185
+ });
186
+ const children = <div />;
187
+ const view = render(<App onNew={onNewA}>{children}</App>);
188
+
189
+ await act(async () => {});
190
+ renderThreadRuntime.mockClear();
191
+
192
+ await act(async () => {
193
+ view.rerender(<App onNew={onNewA}>{children}</App>);
194
+ });
195
+
196
+ expect(renderThreadRuntime).not.toHaveBeenCalled();
197
+ });
198
+
199
+ it("gives a thread created after an options change the committed hook", async () => {
200
+ const { App, onNewA, onNewB, runtimeRef } = createHarness();
201
+ const view = render(<App onNew={onNewA} />);
202
+ await act(async () => {});
203
+
204
+ await act(async () => {
205
+ view.rerender(<App onNew={onNewB} />);
206
+ });
207
+
208
+ await act(async () => {
209
+ await runtimeRef.current!.threads.switchToNewThread();
210
+ await getThreadCore(runtimeRef.current!).append(userMessage("first"));
211
+ });
212
+
213
+ expect(onNewB).toHaveBeenCalledTimes(1);
214
+ expect(onNewA).not.toHaveBeenCalled();
215
+ });
216
+ });
@@ -12,6 +12,8 @@ import type { RemoteThreadListOptions } from "../../runtimes/remote-thread-list/
12
12
  import type { AssistantRuntimeCore } from "../../runtime/interfaces/assistant-runtime-core";
13
13
  import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
14
14
  import { RemoteThreadListThreadListRuntimeCore } from "./RemoteThreadListThreadListRuntimeCore";
15
+ import { WritableSubscribable } from "../../subscribable/subscribable";
16
+ import { useSubscribable } from "../../store/runtime-clients/useSubscribable";
15
17
  import { useAui } from "@assistant-ui/store";
16
18
 
17
19
  class RemoteThreadListRuntimeCore
@@ -48,14 +50,30 @@ const useRemoteThreadListRuntimeImpl = (
48
50
  export const useRemoteThreadListRuntime = (
49
51
  options: RemoteThreadListOptions,
50
52
  ): AssistantRuntime => {
51
- const runtimeHookRef = useRef(options.runtimeHook);
52
- runtimeHookRef.current = options.runtimeHook;
53
+ const [runtimeHookStore] = useState(
54
+ () => new WritableSubscribable(options.runtimeHook),
55
+ );
56
+ useEffect(() => {
57
+ runtimeHookStore.setState(options.runtimeHook);
58
+ }, [runtimeHookStore, options.runtimeHook]);
53
59
 
54
60
  const initialThreadIdRef = useRef(options.initialThreadId);
55
61
 
56
- const stableRuntimeHook = useCallback(() => {
57
- return runtimeHookRef.current();
58
- }, []);
62
+ // Thread resources subscribe to the store rather than reading a ref, so a
63
+ // hook published at commit reaches exactly the resources that use it and an
64
+ // abandoned render publishes nothing. The store pins its server snapshot to
65
+ // the constructor value for hydration, which tap reads on any never-mounted
66
+ // fiber, so the live state serves as the server snapshot here.
67
+ const stableRuntimeHook = useCallback(
68
+ function useCommittedRuntimeHook() {
69
+ return useSubscribable({
70
+ subscribe: runtimeHookStore.subscribe,
71
+ getState: runtimeHookStore.getState,
72
+ getServerSnapshot: runtimeHookStore.getState,
73
+ })();
74
+ },
75
+ [runtimeHookStore],
76
+ );
59
77
 
60
78
  const onThreadIdChange = useEffectEvent((threadId: string | undefined) => {
61
79
  options.onThreadIdChange?.(threadId);
@@ -91,7 +109,7 @@ export const useRemoteThreadListRuntime = (
91
109
 
92
110
  // If allowNesting is true and already inside a thread list context,
93
111
  // just call the runtimeHook directly (no-op behavior)
94
- return stableRuntimeHook();
112
+ return options.runtimeHook();
95
113
  }
96
114
 
97
115
  const runtime = useRemoteThreadListRuntimeImpl(stableOptions);
@@ -83,10 +83,14 @@ export type ToolCallMessagePartProps<
83
83
  /**
84
84
  * Responds to a server-side tool approval gate. Only valid while
85
85
  * `approval` is set on the part, `approval.approved === undefined`, and
86
- * no `approval.resolution` is recorded. Accepts a boolean decision or the
87
- * id of one of `approval.options`; option kinds resolve to the boolean.
86
+ * no `approval.resolution` is recorded. Accepts a boolean decision, the
87
+ * id of one of `approval.options` (option kinds resolve to the boolean),
88
+ * or a free-form answer when the request accepts one.
89
+ *
90
+ * Resolves once the runtime has accepted the response, and rejects when
91
+ * it could not be recorded, so the controls can stay retryable.
88
92
  */
89
- respondToApproval: (response: ToolApprovalResponse) => void;
93
+ respondToApproval: (response: ToolApprovalResponse) => Promise<void>;
90
94
  };
91
95
 
92
96
  /** Component used to render a tool-call message part. */
@@ -1,5 +1,6 @@
1
1
  import type { ToolCallMessagePartComponent } from "../MessagePartComponentTypes";
2
2
  import type { Unsubscribe } from "../../..";
3
+ import type { ToolCallText } from "../../../model-context/tool-call-text";
3
4
 
4
5
  export type McpAppResourceOutput = {
5
6
  readonly render: ToolCallMessagePartComponent;
@@ -12,6 +13,7 @@ export type McpAppResourceOutput = {
12
13
  */
13
14
  type ToolRegistration = {
14
15
  readonly render: ToolCallMessagePartComponent;
16
+ readonly renderText?: ToolCallText<any, any, unknown> | undefined;
15
17
  /** Whether this UI renders standalone, outside the chain-of-thought trace. */
16
18
  readonly standalone: boolean;
17
19
  };
@@ -28,7 +28,7 @@ type MessagePartSnapshotBinding = SubscribableWithState<
28
28
  export type MessagePartRuntime = {
29
29
  addToolResult(result: any | ToolResponse<any>): void;
30
30
  resumeToolCall(payload: unknown): void;
31
- respondToToolApproval(response: ToolApprovalResponse): void;
31
+ respondToToolApproval(response: ToolApprovalResponse): Promise<void>;
32
32
 
33
33
  readonly path: MessagePartRuntimePath;
34
34
  getState(): MessagePartState;
@@ -113,7 +113,7 @@ export class MessagePartRuntimeImpl implements MessagePartRuntime {
113
113
  });
114
114
  }
115
115
 
116
- public respondToToolApproval(response: ToolApprovalResponse) {
116
+ public respondToToolApproval(response: ToolApprovalResponse): Promise<void> {
117
117
  const state = this.contentBinding.getState();
118
118
  if (!state) throw new Error("Message part is not available");
119
119
 
@@ -131,7 +131,7 @@ export class MessagePartRuntimeImpl implements MessagePartRuntime {
131
131
 
132
132
  if (!this.threadApi) throw new Error("Thread API is not available");
133
133
 
134
- this.threadApi
134
+ return this.threadApi
135
135
  .getState()
136
136
  .respondToToolApproval(
137
137
  resolveToolApprovalResponse(state.approval, response),
@@ -7,7 +7,10 @@ import {
7
7
  SKIP_UPDATE,
8
8
  ShallowMemoizeSubject,
9
9
  } from "../../subscribable/subscribable";
10
- import type { ThreadListRuntimeCore } from "../interfaces/thread-list-runtime-core";
10
+ import type {
11
+ ThreadListRuntimeCore,
12
+ ThreadListRuntimeEvent,
13
+ } from "../interfaces/thread-list-runtime-core";
11
14
  import {
12
15
  type ThreadListItemRuntime,
13
16
  ThreadListItemRuntimeImpl,
@@ -21,6 +24,7 @@ import {
21
24
  } from "./thread-runtime";
22
25
 
23
26
  const RESOLVED_PROMISE = Promise.resolve();
27
+ const NOOP_UNSUBSCRIBE = () => {};
24
28
 
25
29
  export type ThreadListState = {
26
30
  readonly mainThreadId: string;
@@ -57,6 +61,16 @@ export type ThreadListRuntime = {
57
61
  ): Promise<void>;
58
62
  switchToNewThread(): Promise<void>;
59
63
 
64
+ /**
65
+ * Observes lifecycle events on every thread this list keeps alive, so an
66
+ * event that fires while its thread is not selected stays observable. Thread
67
+ * lists that mount only the main thread never emit; their main thread's
68
+ * runtime is observed directly.
69
+ */
70
+ unstable_subscribeThreadEvents(
71
+ callback: (event: ThreadListRuntimeEvent) => void,
72
+ ): Unsubscribe;
73
+
60
74
  getLoadThreadsPromise(): Promise<void>;
61
75
  reload(): Promise<void>;
62
76
  /**
@@ -171,6 +185,8 @@ export class ThreadListRuntimeImpl implements ThreadListRuntime {
171
185
  protected __internal_bindMethods() {
172
186
  this.switchToThread = this.switchToThread.bind(this);
173
187
  this.switchToNewThread = this.switchToNewThread.bind(this);
188
+ this.unstable_subscribeThreadEvents =
189
+ this.unstable_subscribeThreadEvents.bind(this);
174
190
  this.getLoadThreadsPromise = this.getLoadThreadsPromise.bind(this);
175
191
  this.reload = this.reload.bind(this);
176
192
  this.reloadMainThread = this.reloadMainThread.bind(this);
@@ -194,6 +210,14 @@ export class ThreadListRuntimeImpl implements ThreadListRuntime {
194
210
  return this._core.switchToNewThread();
195
211
  }
196
212
 
213
+ public unstable_subscribeThreadEvents(
214
+ callback: (event: ThreadListRuntimeEvent) => void,
215
+ ): Unsubscribe {
216
+ return (
217
+ this._core.unstable_subscribeThreadEvents?.(callback) ?? NOOP_UNSUBSCRIBE
218
+ );
219
+ }
220
+
197
221
  public getLoadThreadsPromise(): Promise<void> {
198
222
  return this._core.getLoadThreadsPromise();
199
223
  }
@@ -1,6 +1,8 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import type { RealtimeVoiceAdapter } from "../../adapters/voice";
3
+ import type { ModelContextProvider } from "../../model-context/types";
3
4
  import type { AppendMessage } from "../../types/message";
5
+ import { CompositeContextProvider } from "../../utils/composite-context-provider";
4
6
  import type {
5
7
  AddToolResultOptions,
6
8
  ResumeRunConfig,
@@ -45,8 +47,11 @@ const createVoiceAdapter = () => {
45
47
  class TestRuntime extends BaseThreadRuntimeCore {
46
48
  private readonly voiceAdapter: ReturnType<typeof createVoiceAdapter>;
47
49
 
48
- constructor(voiceAdapter: ReturnType<typeof createVoiceAdapter>) {
49
- super({ getModelContext: () => ({}) });
50
+ constructor(
51
+ voiceAdapter: ReturnType<typeof createVoiceAdapter>,
52
+ contextProvider: ModelContextProvider = { getModelContext: () => ({}) },
53
+ ) {
54
+ super(contextProvider);
50
55
  this.voiceAdapter = voiceAdapter;
51
56
  }
52
57
 
@@ -99,7 +104,7 @@ class TestRuntime extends BaseThreadRuntimeCore {
99
104
  resumeRun(_config: ResumeRunConfig) {}
100
105
  addToolResult(_options: AddToolResultOptions) {}
101
106
  resumeToolCall(_options: ResumeToolCallOptions) {}
102
- respondToToolApproval(_options: RespondToToolApprovalOptions) {}
107
+ async respondToToolApproval(_options: RespondToToolApprovalOptions) {}
103
108
  cancelRun() {}
104
109
  exportExternalState() {
105
110
  return {};
@@ -126,6 +131,92 @@ describe("BaseThreadRuntimeCore subscriptions", () => {
126
131
  expect(() => runtime.reset()).toThrow(error);
127
132
  expect(laterSubscriber).toHaveBeenCalledOnce();
128
133
  });
134
+
135
+ it("isolates initialize listener errors during late replay", async () => {
136
+ const consoleError = vi
137
+ .spyOn(console, "error")
138
+ .mockImplementation(() => {});
139
+ const runtime = new TestRuntime(createVoiceAdapter());
140
+ runtime.reset();
141
+
142
+ const syncError = new Error("sync listener failed");
143
+ const asyncError = new Error("async listener failed");
144
+ runtime.unstable_on("initialize", () => {
145
+ throw syncError;
146
+ });
147
+ runtime.unstable_on("initialize", async () => {
148
+ throw asyncError;
149
+ });
150
+
151
+ await vi.waitFor(() => {
152
+ expect(consoleError).toHaveBeenCalledTimes(2);
153
+ expect(consoleError).toHaveBeenCalledWith(
154
+ '[assistant-ui] Thread runtime "initialize" listener threw an error',
155
+ syncError,
156
+ );
157
+ expect(consoleError).toHaveBeenCalledWith(
158
+ '[assistant-ui] Thread runtime "initialize" listener threw an error',
159
+ asyncError,
160
+ );
161
+ });
162
+ });
163
+
164
+ it("isolates modelContextUpdate listener errors during context updates", () => {
165
+ const consoleError = vi
166
+ .spyOn(console, "error")
167
+ .mockImplementation(() => {});
168
+ const provider = new CompositeContextProvider();
169
+ const runtime = new TestRuntime(createVoiceAdapter(), provider);
170
+ const listenerError = new Error("model context listener failed");
171
+ const laterListener = vi.fn();
172
+
173
+ runtime.unstable_on("modelContextUpdate", () => {
174
+ throw listenerError;
175
+ });
176
+ runtime.unstable_on("modelContextUpdate", laterListener);
177
+
178
+ const unregister = provider.registerModelContextProvider({
179
+ getModelContext: () => ({}),
180
+ });
181
+
182
+ expect(laterListener).toHaveBeenCalledOnce();
183
+ expect(consoleError).toHaveBeenNthCalledWith(
184
+ 1,
185
+ '[assistant-ui] Thread runtime "modelContextUpdate" listener threw an error',
186
+ listenerError,
187
+ );
188
+ expect(() => unregister()).not.toThrow();
189
+ expect(laterListener).toHaveBeenCalledTimes(2);
190
+ expect(consoleError).toHaveBeenNthCalledWith(
191
+ 2,
192
+ '[assistant-ui] Thread runtime "modelContextUpdate" listener threw an error',
193
+ listenerError,
194
+ );
195
+ });
196
+
197
+ it("observes rejected modelContextUpdate listener promises", async () => {
198
+ const consoleError = vi
199
+ .spyOn(console, "error")
200
+ .mockImplementation(() => {});
201
+ const provider = new CompositeContextProvider();
202
+ const runtime = new TestRuntime(createVoiceAdapter(), provider);
203
+ const listenerError = new Error("async model context listener failed");
204
+
205
+ runtime.unstable_on("modelContextUpdate", async () => {
206
+ throw listenerError;
207
+ });
208
+
209
+ expect(() =>
210
+ provider.registerModelContextProvider({ getModelContext: () => ({}) }),
211
+ ).not.toThrow();
212
+
213
+ await vi.waitFor(() => {
214
+ expect(consoleError).toHaveBeenCalledWith(
215
+ '[assistant-ui] Thread runtime "modelContextUpdate" listener threw an error',
216
+ listenerError,
217
+ );
218
+ });
219
+ });
129
220
  });
130
221
 
131
222
  describe("BaseThreadRuntimeCore voice volume subscriptions", () => {
@@ -51,7 +51,7 @@ export abstract class BaseThreadRuntimeCore
51
51
  {
52
52
  private _isInitialized = false;
53
53
 
54
- protected readonly repository = new MessageRepository();
54
+ protected repository = new MessageRepository();
55
55
  public abstract get adapters(): BaseThreadAdapters | undefined;
56
56
  public abstract get isDisabled(): boolean;
57
57
  public abstract get isSendDisabled(): boolean;
@@ -68,7 +68,7 @@ export abstract class BaseThreadRuntimeCore
68
68
  public abstract resumeToolCall(options: ResumeToolCallOptions): void;
69
69
  public abstract respondToToolApproval(
70
70
  options: RespondToToolApprovalOptions,
71
- ): void;
71
+ ): Promise<void>;
72
72
  public abstract cancelRun(): void;
73
73
  public abstract exportExternalState(): any;
74
74
  public abstract importExternalState(state: any): void;
@@ -518,7 +518,11 @@ export abstract class BaseThreadRuntimeCore
518
518
  const wrapped = callback as (payload?: unknown) => void;
519
519
  if (event === "modelContextUpdate") {
520
520
  // provider.subscribe is `() => void`; pump the typed empty payload to the user callback.
521
- return this._contextProvider.subscribe?.(() => wrapped({})) ?? (() => {});
521
+ return (
522
+ this._contextProvider.subscribe?.(() =>
523
+ notifyEventListeners([wrapped], {}, `Thread runtime "${event}"`),
524
+ ) ?? (() => {})
525
+ );
522
526
  }
523
527
 
524
528
  let subscribers = this._eventSubscribers.get(event);
@@ -532,7 +536,9 @@ export abstract class BaseThreadRuntimeCore
532
536
  // after the thread already initialized, mirroring a BehaviorSubject.
533
537
  if (event === "initialize" && this._isInitialized) {
534
538
  queueMicrotask(() => {
535
- if (subscribers.has(wrapped)) wrapped({});
539
+ if (subscribers.has(wrapped)) {
540
+ notifyEventListeners([wrapped], {}, `Thread runtime "${event}"`);
541
+ }
536
542
  });
537
543
  }
538
544
 
@@ -1,5 +1,8 @@
1
1
  import type { Unsubscribe } from "../../types/unsubscribe";
2
- import type { ThreadRuntimeCore } from "./thread-runtime-core";
2
+ import type {
3
+ ThreadRuntimeCore,
4
+ ThreadRuntimeEventType,
5
+ } from "./thread-runtime-core";
3
6
 
4
7
  export type ThreadListItemStatus = "archived" | "regular" | "new" | "deleted";
5
8
 
@@ -16,6 +19,11 @@ export type ThreadListItemCoreState = {
16
19
  readonly runtime?: ThreadRuntimeCore | undefined;
17
20
  };
18
21
 
22
+ export type ThreadListRuntimeEvent = {
23
+ readonly threadId: string;
24
+ readonly type: ThreadRuntimeEventType;
25
+ };
26
+
19
27
  export type ThreadListRuntimeCore = {
20
28
  readonly isLoading: boolean;
21
29
  readonly isLoadingMore?: boolean;
@@ -41,6 +49,17 @@ export type ThreadListRuntimeCore = {
41
49
  */
42
50
  unstable_isThreadRunning?(threadId: string): boolean;
43
51
 
52
+ /**
53
+ * Lifecycle events from every thread this list keeps alive, including
54
+ * threads that are not the main one. Implemented by thread lists that keep
55
+ * runtimes alive for non-main threads. A thread list that mounts only the
56
+ * main thread leaves this undefined: its other threads have no runtime and so
57
+ * emit nothing, and the main thread's runtime is observed directly.
58
+ */
59
+ unstable_subscribeThreadEvents?(
60
+ callback: (event: ThreadListRuntimeEvent) => void,
61
+ ): Unsubscribe;
62
+
44
63
  getItemById(threadId: string): ThreadListItemCoreState | undefined;
45
64
 
46
65
  switchToThread(
@@ -63,6 +63,8 @@ export type RespondToToolApprovalOptions = {
63
63
  approved: boolean;
64
64
  /** The approval option that produced this decision, when the request carried options. */
65
65
  optionId?: string;
66
+ /** The free-form answer, when the request asked for one. */
67
+ text?: string;
66
68
  reason?: string;
67
69
  };
68
70
 
@@ -165,7 +167,21 @@ export type ThreadRuntimeCore = Readonly<{
165
167
 
166
168
  addToolResult: (options: AddToolResultOptions) => void;
167
169
  resumeToolCall: (options: ResumeToolCallOptions) => void;
168
- respondToToolApproval: (options: RespondToToolApprovalOptions) => void;
170
+ /**
171
+ * Records a decision on a tool approval gate. Resolves once the runtime has
172
+ * accepted the response and rejects when it could not be recorded, so a
173
+ * caller can leave the gate retryable rather than spending it. A capability
174
+ * or state precondition still throws synchronously; a failure to record
175
+ * arrives as a rejection, including one an adapter raises synchronously.
176
+ *
177
+ * Acceptance is as far as the runtime can see the response: one that records
178
+ * the decision locally settles on the record, while one that answers by
179
+ * resuming a run settles on the resume. A failure of the work the decision
180
+ * unblocks is reported through the runtime's own error channel, not here.
181
+ */
182
+ respondToToolApproval: (
183
+ options: RespondToToolApprovalOptions,
184
+ ) => Promise<void>;
169
185
 
170
186
  speak: (messageId: string) => void;
171
187
  stopSpeaking: () => void;
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getAutoStatus } from "./auto-status";
3
+
4
+ describe("getAutoStatus", () => {
5
+ it("reports a cancelled message as incomplete", () => {
6
+ expect(
7
+ getAutoStatus(true, false, false, false, undefined, true),
8
+ ).toMatchObject({ type: "incomplete", reason: "cancelled" });
9
+ });
10
+
11
+ it("keeps a cancelled message incomplete once a later message arrives", () => {
12
+ expect(
13
+ getAutoStatus(false, false, false, false, undefined, true),
14
+ ).toMatchObject({ type: "incomplete", reason: "cancelled" });
15
+ });
16
+
17
+ it("reports an uncancelled message as complete", () => {
18
+ expect(
19
+ getAutoStatus(true, false, false, false, undefined, false),
20
+ ).toMatchObject({ type: "complete", reason: "unknown" });
21
+ });
22
+
23
+ it.each([
24
+ ["running", { type: "running" }, true, false, false, undefined],
25
+ [
26
+ "an interrupted tool call",
27
+ { type: "requires-action", reason: "interrupt" },
28
+ false,
29
+ true,
30
+ true,
31
+ undefined,
32
+ ],
33
+ [
34
+ "a pending tool call",
35
+ { type: "requires-action", reason: "tool-calls" },
36
+ false,
37
+ false,
38
+ true,
39
+ undefined,
40
+ ],
41
+ [
42
+ "an error",
43
+ { type: "incomplete", reason: "error", error: "boom" },
44
+ false,
45
+ false,
46
+ false,
47
+ "boom",
48
+ ],
49
+ ])(
50
+ "keeps %s ahead of cancellation",
51
+ (_label, expected, isRunning, interrupted, pending, error) => {
52
+ expect(
53
+ getAutoStatus(true, isRunning, interrupted, pending, error, true),
54
+ ).toMatchObject(expected);
55
+ },
56
+ );
57
+ });