@assistant-ui/core 0.3.14 → 0.3.16

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 (341) hide show
  1. package/dist/adapters/voice.js +11 -0
  2. package/dist/adapters/voice.js.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/internal.d.ts +8 -2
  5. package/dist/internal.js +9 -3
  6. package/dist/model-context/frame/host.d.ts +1 -1
  7. package/dist/model-context/frame/host.d.ts.map +1 -1
  8. package/dist/model-context/frame/host.js +20 -5
  9. package/dist/model-context/frame/host.js.map +1 -1
  10. package/dist/model-context/frame/provider.d.ts +6 -0
  11. package/dist/model-context/frame/provider.d.ts.map +1 -1
  12. package/dist/model-context/frame/provider.js +126 -16
  13. package/dist/model-context/frame/provider.js.map +1 -1
  14. package/dist/model-context/frame/types.d.ts +4 -1
  15. package/dist/model-context/frame/types.d.ts.map +1 -1
  16. package/dist/model-context/frame/types.js.map +1 -1
  17. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  18. package/dist/model-context/interactable-composer-metadata.js +1 -1
  19. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  20. package/dist/model-context/registry.d.ts +2 -2
  21. package/dist/model-context/registry.d.ts.map +1 -1
  22. package/dist/model-context/registry.js +9 -28
  23. package/dist/model-context/registry.js.map +1 -1
  24. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  25. package/dist/react/client/InMemoryThreadList.js +24 -20
  26. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  27. package/dist/react/client/Interactables.d.ts.map +1 -1
  28. package/dist/react/client/Interactables.js +59 -177
  29. package/dist/react/client/Interactables.js.map +1 -1
  30. package/dist/react/client/RemoteThreadList.d.ts +1 -1
  31. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  32. package/dist/react/client/RemoteThreadList.js +117 -78
  33. package/dist/react/client/RemoteThreadList.js.map +1 -1
  34. package/dist/react/index.d.ts +2 -1
  35. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  36. package/dist/react/interactables-legacy/Interactables.js +56 -174
  37. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  38. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  39. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  40. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  41. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  42. package/dist/react/primitive-hooks/useActionBarCopy.js +7 -1
  43. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  44. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +3 -0
  45. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
  46. package/dist/react/primitive-hooks/useThreadIsEmpty.js +3 -0
  47. package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -1
  48. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +3 -0
  49. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
  50. package/dist/react/primitive-hooks/useThreadIsRunning.js +3 -0
  51. package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -1
  52. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  53. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  54. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  55. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  56. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +3 -1
  57. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  58. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +6 -0
  59. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  60. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +7 -1
  61. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  62. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +174 -64
  63. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  64. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  65. package/dist/react/runtimes/RemoteThreadResource.js +2 -0
  66. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  67. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  68. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  69. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  70. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  71. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js +1 -1
  72. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  73. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  74. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  75. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  76. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  77. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  78. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  79. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  80. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  81. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  82. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +38 -5
  83. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  84. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  85. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  86. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  87. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  88. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  89. package/dist/react/runtimes/external-message-converter.js +33 -233
  90. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  91. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  92. package/dist/runtime/api/composer-runtime.js.map +1 -1
  93. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  94. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  95. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  96. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  97. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  98. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  99. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  100. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  101. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  102. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  103. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  104. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  105. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  106. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  107. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  108. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  109. package/dist/runtime/internal.d.ts +2 -1
  110. package/dist/runtime/internal.js +2 -1
  111. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  112. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  113. package/dist/runtime/queue/message-queue.js +1 -1
  114. package/dist/runtime/queue/message-queue.js.map +1 -1
  115. package/dist/runtime/queue/queue-item.d.ts +12 -0
  116. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  117. package/dist/runtime/queue/queue-item.js +6 -0
  118. package/dist/runtime/queue/queue-item.js.map +1 -0
  119. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  120. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  121. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  122. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  123. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  124. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  125. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  126. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  127. package/dist/runtime/utils/auto-status.d.ts +6 -1
  128. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  129. package/dist/runtime/utils/auto-status.js +7 -1
  130. package/dist/runtime/utils/auto-status.js.map +1 -1
  131. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  132. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  133. package/dist/runtime/utils/external-message-conversion.js +221 -0
  134. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  135. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  136. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  137. package/dist/runtime/utils/external-store-message.js +2 -1
  138. package/dist/runtime/utils/external-store-message.js.map +1 -1
  139. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  140. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  141. package/dist/runtime/utils/message-repository-session.js +130 -0
  142. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  143. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  144. package/dist/runtime/utils/message-repository.js +30 -22
  145. package/dist/runtime/utils/message-repository.js.map +1 -1
  146. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  147. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  148. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  149. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  150. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  151. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  152. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  153. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  154. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  155. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  156. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  157. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  158. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  159. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +124 -20
  160. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  161. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +102 -0
  162. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -0
  163. package/dist/runtimes/inert/InertThreadRuntimeCore.js +156 -0
  164. package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -0
  165. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  166. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  167. package/dist/runtimes/local/local-thread-runtime-core.js +45 -8
  168. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  169. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +12 -60
  170. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  171. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +10 -145
  172. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  173. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +9 -0
  174. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -0
  175. package/dist/runtimes/remote-thread-list/adapter-changed.js +13 -0
  176. package/dist/runtimes/remote-thread-list/adapter-changed.js.map +1 -0
  177. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  178. package/dist/runtimes/remote-thread-list/empty-thread-core.js +16 -155
  179. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  180. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +2 -0
  181. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  182. package/dist/runtimes/remote-thread-list/optimistic-state.js +11 -0
  183. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  184. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  185. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  186. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  187. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  188. package/dist/runtimes/remote-thread-list/types.d.ts +3 -0
  189. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  190. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  191. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  192. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  193. package/dist/store/clients/external-thread.d.ts.map +1 -1
  194. package/dist/store/clients/external-thread.js +75 -100
  195. package/dist/store/clients/external-thread.js.map +1 -1
  196. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  197. package/dist/store/clients/single-thread-list.js +1 -1
  198. package/dist/store/clients/single-thread-list.js.map +1 -1
  199. package/dist/store/clients/suggestions.d.ts.map +1 -1
  200. package/dist/store/clients/suggestions.js +16 -7
  201. package/dist/store/clients/suggestions.js.map +1 -1
  202. package/dist/store/index.d.ts +4 -2
  203. package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
  204. package/dist/store/runtime-clients/handle-runtime-action.js +2 -0
  205. package/dist/store/runtime-clients/handle-runtime-action.js.map +1 -1
  206. package/dist/store/scope-registration.d.ts +1 -1
  207. package/dist/store/scopes/composer.d.ts +2 -1
  208. package/dist/store/scopes/composer.d.ts.map +1 -1
  209. package/dist/store/scopes/message.d.ts +1 -1
  210. package/dist/store/scopes/queue-item.d.ts +2 -9
  211. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  212. package/dist/store/scopes/queue-item.js +0 -6
  213. package/dist/types/message.d.ts +8 -4
  214. package/dist/types/message.d.ts.map +1 -1
  215. package/dist/types/message.js.map +1 -1
  216. package/dist/utils/data-url.d.ts +14 -1
  217. package/dist/utils/data-url.d.ts.map +1 -1
  218. package/dist/utils/data-url.js +13 -1
  219. package/dist/utils/data-url.js.map +1 -1
  220. package/dist/utils/invoke-user-callback.d.ts +5 -0
  221. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  222. package/dist/utils/invoke-user-callback.js +19 -0
  223. package/dist/utils/invoke-user-callback.js.map +1 -0
  224. package/dist/utils/normalizePartStatus.js.map +1 -1
  225. package/dist/utils/silent-runtime-action.d.ts +6 -0
  226. package/dist/utils/silent-runtime-action.d.ts.map +1 -0
  227. package/dist/utils/silent-runtime-action.js +7 -0
  228. package/dist/utils/silent-runtime-action.js.map +1 -0
  229. package/package.json +10 -10
  230. package/src/adapters/voice.test.ts +66 -1
  231. package/src/adapters/voice.ts +8 -0
  232. package/src/internal.ts +13 -0
  233. package/src/model-context/frame/host.test.ts +152 -9
  234. package/src/model-context/frame/host.ts +23 -5
  235. package/src/model-context/frame/provider.test.ts +601 -19
  236. package/src/model-context/frame/provider.ts +158 -20
  237. package/src/model-context/frame/types.ts +5 -0
  238. package/src/model-context/interactable-composer-metadata.ts +1 -1
  239. package/src/model-context/registry.test.ts +44 -0
  240. package/src/model-context/registry.ts +13 -35
  241. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  242. package/src/react/client/InMemoryThreadList.ts +7 -1
  243. package/src/react/client/Interactables.ts +19 -178
  244. package/src/react/client/RemoteThreadList.test.ts +292 -0
  245. package/src/react/client/RemoteThreadList.ts +119 -82
  246. package/src/react/interactables-legacy/Interactables.ts +20 -177
  247. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  248. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  249. package/src/react/primitive-hooks/useActionBarCopy.test.ts +15 -0
  250. package/src/react/primitive-hooks/useActionBarCopy.ts +13 -3
  251. package/src/react/primitive-hooks/useThreadIsEmpty.ts +3 -0
  252. package/src/react/primitive-hooks/useThreadIsRunning.ts +3 -0
  253. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +12 -0
  254. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.republish.test.ts +87 -0
  255. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +251 -76
  256. package/src/react/runtimes/RemoteThreadResource.ts +2 -0
  257. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  258. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +58 -0
  259. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.ts +3 -1
  260. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  261. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  262. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  263. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  264. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +98 -3
  265. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +68 -6
  266. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  267. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  268. package/src/react/runtimes/external-message-converter.ts +58 -481
  269. package/src/react/runtimes/useLocalRuntime.test.tsx +5 -5
  270. package/src/runtime/api/composer-runtime.ts +1 -1
  271. package/src/runtime/api/message-runtime.test.ts +28 -1
  272. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  273. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  274. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  275. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  276. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  277. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  278. package/src/runtime/internal.ts +5 -0
  279. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  280. package/src/runtime/queue/message-queue.ts +1 -4
  281. package/src/runtime/queue/queue-item.ts +10 -0
  282. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  283. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  284. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  285. package/src/runtime/utils/auto-status.ts +33 -0
  286. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  287. package/src/runtime/utils/external-message-conversion.ts +494 -0
  288. package/src/runtime/utils/external-store-message.ts +2 -0
  289. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  290. package/src/runtime/utils/message-repository-session.ts +176 -0
  291. package/src/runtime/utils/message-repository.ts +33 -22
  292. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  293. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  294. package/src/runtime/utils/thread-message-like.ts +2 -0
  295. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  296. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +179 -28
  297. package/src/runtimes/inert/InertThreadRuntimeCore.test.ts +253 -0
  298. package/src/runtimes/inert/InertThreadRuntimeCore.ts +232 -0
  299. package/src/runtimes/local/local-thread-runtime-core.test.ts +491 -1
  300. package/src/runtimes/local/local-thread-runtime-core.ts +86 -11
  301. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +17 -209
  302. package/src/runtimes/remote-thread-list/adapter-changed.ts +10 -0
  303. package/src/runtimes/remote-thread-list/empty-thread-core.ts +28 -221
  304. package/src/runtimes/remote-thread-list/optimistic-state.test.ts +18 -0
  305. package/src/runtimes/remote-thread-list/optimistic-state.ts +13 -0
  306. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  307. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  308. package/src/runtimes/remote-thread-list/types.ts +4 -0
  309. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  310. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  311. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  312. package/src/store/clients/external-thread.ts +17 -62
  313. package/src/store/clients/single-thread-list.ts +4 -1
  314. package/src/store/clients/suggestions.test.ts +31 -0
  315. package/src/store/clients/suggestions.ts +18 -19
  316. package/src/store/runtime-clients/handle-runtime-action.test.ts +40 -0
  317. package/src/store/runtime-clients/handle-runtime-action.ts +3 -0
  318. package/src/store/scopes/message.ts +1 -1
  319. package/src/store/scopes/queue-item.ts +2 -9
  320. package/src/tests/MessageRepository.test.ts +125 -0
  321. package/src/tests/RemoteThreadListThreadListRuntimeCore-errors.test.ts +2 -2
  322. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  323. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +8 -6
  324. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  325. package/src/tests/empty-thread-core.test.ts +6 -0
  326. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +184 -0
  327. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  328. package/src/tests/external-thread-attachments.test.tsx +173 -0
  329. package/src/tests/external-thread-isEmpty.test.tsx +100 -0
  330. package/src/tests/remote-thread-list-adapter-switch.test.ts +383 -0
  331. package/src/tests/single-thread-list-item.test.tsx +47 -0
  332. package/src/tests/thread-message-like.test.ts +39 -1
  333. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +67 -0
  334. package/src/types/message.ts +9 -4
  335. package/src/utils/data-url.test.ts +52 -1
  336. package/src/utils/data-url.ts +21 -0
  337. package/src/utils/invoke-user-callback.test.ts +82 -0
  338. package/src/utils/invoke-user-callback.ts +31 -0
  339. package/src/utils/normalizePartStatus.ts +1 -1
  340. package/src/utils/silent-runtime-action.ts +6 -0
  341. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -16,9 +16,13 @@ import type {
16
16
  import {
17
17
  getExternalStoreMessages,
18
18
  bindExternalStoreMessage,
19
+ FALLBACK_ID_PREFIX,
19
20
  } from "../../runtime/utils/external-store-message";
20
21
  import { ThreadMessageConverter } from "./thread-message-converter";
21
- import { getAutoStatus, isAutoStatus } from "../../runtime/utils/auto-status";
22
+ import {
23
+ getContentAutoStatus,
24
+ isAutoStatus,
25
+ } from "../../runtime/utils/auto-status";
22
26
  import {
23
27
  fromThreadMessageLike,
24
28
  type ThreadMessageLike,
@@ -39,8 +43,11 @@ import {
39
43
  MessageRepository,
40
44
  } from "../../runtime/utils/message-repository";
41
45
  import { generateId } from "../../utils/id";
42
- import { ToolInvocationTracker } from "../tool-invocations/ToolInvocationTracker";
43
- import { EMPTY_QUEUE_ITEMS } from "../../store/scopes/queue-item";
46
+ import {
47
+ ToolInvocationTracker,
48
+ type ToolExecutionStatus,
49
+ } from "../tool-invocations/ToolInvocationTracker";
50
+ import { EMPTY_QUEUE_ITEMS } from "../../runtime/queue/queue-item";
44
51
  import type { QuoteInfo } from "../../types/quote";
45
52
  import {
46
53
  captureThreadRuntimeGeneration,
@@ -110,6 +117,7 @@ export class ExternalStoreThreadRuntimeCore
110
117
  }
111
118
  // Unlike `isLoading`: pass `undefined` through to preserve the `getThreadState` fallback.
112
119
  public get isRunning(): boolean | undefined {
120
+ if (this._hasExecutingTools(this._store)) return true;
113
121
  return this._store.isRunning;
114
122
  }
115
123
 
@@ -136,6 +144,16 @@ export class ExternalStoreThreadRuntimeCore
136
144
 
137
145
  private _converter = new ThreadMessageConverter();
138
146
 
147
+ // Ids the host was asked to delete via onDelete. The snapshot pass evicts
148
+ // them from the repository once the host's array no longer carries them;
149
+ // an id the host kept is dropped from the set without eviction.
150
+ // Branch-changing mutations (edit, branch switch, reload) invalidate the
151
+ // set, because after them the incoming array omits off-branch ids for
152
+ // reasons unrelated to deletion. Plain tail sends do not clear: a tail
153
+ // append cannot make a visible id absent, so id-absence stays unambiguous
154
+ // and a delete whose confirmation races a send keeps its eviction.
155
+ private _pendingDeleteEvictions = new Set<string>();
156
+
139
157
  private _store!: ExternalStoreAdapter<any>;
140
158
 
141
159
  private _getInitializePromise?: () => Promise<unknown> | undefined;
@@ -153,6 +171,51 @@ export class ExternalStoreThreadRuntimeCore
153
171
  * snapshot — only when `adapter.unstable_enableToolInvocations === true`.
154
172
  */
155
173
  private _toolInvocations: ToolInvocationTracker | null = null;
174
+ private _toolStatuses: ReadonlyMap<string, ToolExecutionStatus> = new Map();
175
+ private _effectiveIsRunning = false;
176
+ private _inTrackerUpdate = false;
177
+ private _pendingRunningRefresh = false;
178
+
179
+ /**
180
+ * Tracker mutations initiated by this class (setState, reset) can publish
181
+ * status changes synchronously. Re-entering the snapshot pipeline from
182
+ * inside them would feed the tracker a stale snapshot and consume the
183
+ * restore arming a reset just installed, so the running refresh is
184
+ * deferred until the mutation returns and stays off the tracker.
185
+ */
186
+ private _runTrackerUpdate(fn: () => void): void {
187
+ this._inTrackerUpdate = true;
188
+ try {
189
+ fn();
190
+ } finally {
191
+ this._inTrackerUpdate = false;
192
+ }
193
+ if (this._pendingRunningRefresh) {
194
+ this._pendingRunningRefresh = false;
195
+ this._refreshEffectiveIsRunning();
196
+ }
197
+ }
198
+
199
+ private _refreshEffectiveIsRunning(): void {
200
+ const isRunning = this._getEffectiveIsRunning(this._store);
201
+ if (this._effectiveIsRunning === isRunning) return;
202
+ this._effectiveIsRunning = isRunning;
203
+ this._notifyEventSubscribers(isRunning ? "runStart" : "runEnd", {});
204
+ this._notifySubscribers();
205
+ }
206
+
207
+ private _hasExecutingTools(store: ExternalStoreAdapter<any>): boolean {
208
+ if (store.unstable_enableToolInvocations !== true) return false;
209
+ if (this._toolInvocations === null) return false;
210
+ for (const status of this._toolStatuses.values()) {
211
+ if (status.type === "executing") return true;
212
+ }
213
+ return false;
214
+ }
215
+
216
+ private _getEffectiveIsRunning(store: ExternalStoreAdapter<any>): boolean {
217
+ return (store.isRunning ?? false) || this._hasExecutingTools(store);
218
+ }
156
219
 
157
220
  public override beginEdit(messageId: string) {
158
221
  if (!this._store.onEdit)
@@ -172,12 +235,17 @@ export class ExternalStoreThreadRuntimeCore
172
235
  public __internal_setAdapter(store: ExternalStoreAdapter<any>) {
173
236
  if (this._store === store) return;
174
237
 
175
- const isRunning = store.isRunning ?? false;
238
+ this._updateStoreSnapshot(store);
239
+ }
240
+
241
+ private _updateStoreSnapshot(store: ExternalStoreAdapter<any>) {
242
+ const previousIsRunning = this._effectiveIsRunning;
176
243
  this.isDisabled = store.isDisabled ?? false;
177
244
  this.isSendDisabled = store.isSendDisabled ?? false;
178
245
 
179
246
  const oldStore = this._store as ExternalStoreAdapter<any> | undefined;
180
247
  this._store = store;
248
+ const isRunning = this._getEffectiveIsRunning(store);
181
249
  if (oldStore?.queue !== store.queue) {
182
250
  this._transformedQueue = undefined;
183
251
  store.queue?.__internal_setDispatchTransform?.((message) => {
@@ -229,7 +297,8 @@ export class ExternalStoreThreadRuntimeCore
229
297
  if (
230
298
  oldStore &&
231
299
  oldStore.isRunning === store.isRunning &&
232
- oldStore.messageRepository === store.messageRepository
300
+ oldStore.messageRepository === store.messageRepository &&
301
+ previousIsRunning === isRunning
233
302
  ) {
234
303
  this._notifySubscribers();
235
304
  return;
@@ -252,6 +321,7 @@ export class ExternalStoreThreadRuntimeCore
252
321
  this.repository.deleteMessage(message.id);
253
322
  }
254
323
  }
324
+ this._pendingDeleteEvictions.clear();
255
325
  this.repository.resetHead(headId);
256
326
  messages = this.repository.getMessages();
257
327
  }
@@ -264,7 +334,8 @@ export class ExternalStoreThreadRuntimeCore
264
334
  this._converter = new ThreadMessageConverter();
265
335
  } else if (
266
336
  oldStore.isRunning === store.isRunning &&
267
- oldStore.messages === store.messages
337
+ oldStore.messages === store.messages &&
338
+ previousIsRunning === isRunning
268
339
  ) {
269
340
  this._notifySubscribers();
270
341
  // no conversion update
@@ -278,27 +349,31 @@ export class ExternalStoreThreadRuntimeCore
278
349
  if (!store.convertMessage) return m;
279
350
 
280
351
  const isLast = idx === (store.messages?.length ?? 0) - 1;
281
- const autoStatus = getAutoStatus(
282
- isLast,
283
- isRunning,
284
- false,
285
- false,
286
- undefined,
287
- );
352
+ const fallbackId = `${FALLBACK_ID_PREFIX}${idx}`;
288
353
 
289
354
  if (
290
355
  cache &&
291
356
  (cache.role !== "assistant" ||
292
357
  !isAutoStatus(cache.status) ||
293
- cache.status === autoStatus)
294
- )
358
+ cache.status ===
359
+ getContentAutoStatus(cache.content, isLast, isRunning))
360
+ ) {
361
+ if (
362
+ cache.id.startsWith(FALLBACK_ID_PREFIX) &&
363
+ cache.id !== fallbackId
364
+ ) {
365
+ const updated = { ...cache, id: fallbackId };
366
+ bindExternalStoreMessage(updated, m);
367
+ return updated;
368
+ }
295
369
  return cache;
370
+ }
296
371
 
297
372
  const messageLike = store.convertMessage(m, idx);
298
373
  const newMessage = fromThreadMessageLike(
299
374
  messageLike,
300
- idx.toString(),
301
- autoStatus,
375
+ fallbackId,
376
+ getContentAutoStatus(messageLike.content, isLast, isRunning),
302
377
  );
303
378
  bindExternalStoreMessage(newMessage, m);
304
379
  return newMessage;
@@ -324,6 +399,20 @@ export class ExternalStoreThreadRuntimeCore
324
399
  const parent = messages[i - 1];
325
400
  this.repository.addOrUpdateMessage(parent?.id ?? null, message);
326
401
  }
402
+
403
+ if (this._pendingDeleteEvictions.size > 0) {
404
+ const incomingIds = new Set(messages.map((m) => m.id));
405
+ for (const id of this._pendingDeleteEvictions) {
406
+ this._pendingDeleteEvictions.delete(id);
407
+ if (incomingIds.has(id)) continue;
408
+ try {
409
+ this.repository.getMessage(id);
410
+ } catch {
411
+ continue;
412
+ }
413
+ this.repository.deleteMessage(id);
414
+ }
415
+ }
327
416
  } else {
328
417
  throw new Error(
329
418
  "ExternalStoreAdapter must provide either 'messages' or 'messageRepository'",
@@ -333,8 +422,9 @@ export class ExternalStoreThreadRuntimeCore
333
422
  // Common logic for both paths
334
423
  if (messages.length > 0) this.ensureInitialized();
335
424
 
336
- if ((oldStore?.isRunning ?? false) !== (store.isRunning ?? false)) {
337
- if (store.isRunning) {
425
+ this._effectiveIsRunning = isRunning;
426
+ if (previousIsRunning !== isRunning) {
427
+ if (isRunning) {
338
428
  this._notifyEventSubscribers("runStart", {});
339
429
  } else {
340
430
  this._notifyEventSubscribers("runEnd", {});
@@ -361,7 +451,7 @@ export class ExternalStoreThreadRuntimeCore
361
451
 
362
452
  this._messages = this.repository.getMessages();
363
453
 
364
- this._driveToolInvocations();
454
+ this._runTrackerUpdate(() => this._driveToolInvocations());
365
455
 
366
456
  this._notifySubscribers();
367
457
  }
@@ -380,6 +470,7 @@ export class ExternalStoreThreadRuntimeCore
380
470
  if (this._toolInvocations) {
381
471
  this._toolInvocations.reset();
382
472
  this._toolInvocations = null;
473
+ this._toolStatuses = new Map();
383
474
  this._store.setToolStatuses?.({});
384
475
  }
385
476
  return;
@@ -423,7 +514,19 @@ export class ExternalStoreThreadRuntimeCore
423
514
  }
424
515
  },
425
516
  onStatusesChange: (statuses) => {
426
- this._store.setToolStatuses?.(Object.fromEntries(statuses));
517
+ const hadExecutingTools = this._hasExecutingTools(this._store);
518
+ this._toolStatuses = statuses;
519
+ try {
520
+ this._store.setToolStatuses?.(Object.fromEntries(statuses));
521
+ } finally {
522
+ if (hadExecutingTools !== this._hasExecutingTools(this._store)) {
523
+ if (this._inTrackerUpdate) {
524
+ this._pendingRunningRefresh = true;
525
+ } else {
526
+ this._updateStoreSnapshot(this._store);
527
+ }
528
+ }
529
+ }
427
530
  },
428
531
  },
429
532
  );
@@ -431,7 +534,7 @@ export class ExternalStoreThreadRuntimeCore
431
534
 
432
535
  this._toolInvocations.setState({
433
536
  messages: this._messages,
434
- isRunning: this._store.isRunning ?? false,
537
+ isRunning: this._getEffectiveIsRunning(this._store),
435
538
  ...(this._store.isLoading !== undefined && {
436
539
  isLoading: this._store.isLoading,
437
540
  }),
@@ -475,7 +578,7 @@ export class ExternalStoreThreadRuntimeCore
475
578
  throw new Error("Runtime does not support switching branches.");
476
579
 
477
580
  // Silently ignore branch switches while running
478
- if (this._store.isRunning) {
581
+ if (this._getEffectiveIsRunning(this._store)) {
479
582
  return;
480
583
  }
481
584
 
@@ -485,6 +588,7 @@ export class ExternalStoreThreadRuntimeCore
485
588
  : null;
486
589
 
487
590
  this.repository.switchToBranch(branchId);
591
+ this._pendingDeleteEvictions.clear();
488
592
  this.updateMessages(this.repository.getMessages());
489
593
  if (onBranchChange) {
490
594
  this._notifyBranchChange(previousHeadId, onBranchChange);
@@ -547,7 +651,7 @@ export class ExternalStoreThreadRuntimeCore
547
651
  // Buffering does not start a run, so the tool-abort below must wait
548
652
  // until the queue flushes. By then the prior run (and its tools) has
549
653
  // settled.
550
- if (message.steer ?? this._store.isRunning ?? false)
654
+ if (message.steer ?? this._getEffectiveIsRunning(this._store))
551
655
  this._store.queue.steer(message);
552
656
  else this._store.queue.enqueue(message);
553
657
  return;
@@ -574,6 +678,7 @@ export class ExternalStoreThreadRuntimeCore
574
678
  if (isEdit) {
575
679
  if (!this._store.onEdit)
576
680
  throw new Error("Runtime does not support editing messages.");
681
+ this._pendingDeleteEvictions.clear();
577
682
  await this._store.onEdit(message);
578
683
  } else {
579
684
  await this._store.onNew(message);
@@ -582,14 +687,29 @@ export class ExternalStoreThreadRuntimeCore
582
687
 
583
688
  public async deleteMessage(messageId: string): Promise<void> {
584
689
  if (this._store.onDelete) {
585
- await this._store.onDelete(messageId);
690
+ // The host owns deletion here, and it may decline (fail a server call,
691
+ // cancel a confirm dialog, ignore an off-branch id). The eviction is
692
+ // therefore deferred to the snapshot pass, which evicts only once the
693
+ // host's own array no longer carries the id. Registered before the
694
+ // callback because an optimistic host publishes that snapshot while
695
+ // the callback is still awaited.
696
+ const wasVisible = this.repository
697
+ .getMessages()
698
+ .some((m) => m.id === messageId);
699
+ if (wasVisible) this._pendingDeleteEvictions.add(messageId);
700
+ try {
701
+ await this._store.onDelete(messageId);
702
+ } catch (error) {
703
+ this._pendingDeleteEvictions.delete(messageId);
704
+ throw error;
705
+ }
586
706
  return;
587
707
  }
588
708
 
589
709
  if (!this._store.setMessages)
590
710
  throw new Error("Runtime does not support deleting messages.");
591
711
 
592
- if (this._store.isRunning) {
712
+ if (this._getEffectiveIsRunning(this._store)) {
593
713
  await this._toolInvocations?.abort();
594
714
  }
595
715
 
@@ -597,7 +717,32 @@ export class ExternalStoreThreadRuntimeCore
597
717
  const messageIndex = messages.findIndex((m) => m.id === messageId);
598
718
  if (messageIndex === -1) throw new Error("Message not found.");
599
719
 
720
+ this._pendingDeleteEvictions.clear();
600
721
  this.updateMessages(messages.filter((message) => message.id !== messageId));
722
+ this._evictDeletedMessage(messageId);
723
+ }
724
+
725
+ // The snapshot pass only relinks incoming messages; it never evicts, so
726
+ // without this the deleted message survives as a sibling branch that the
727
+ // branch picker can resurrect into the host store. `_messages` is refreshed
728
+ // before notifying so `messages` and the branch graph agree at notify time,
729
+ // mirroring the end of the snapshot pass.
730
+ private _evictDeletedMessage(messageId: string) {
731
+ // Positional fallback ids are remapped in the snapshot pass; evicting
732
+ // them first leaves the pre-renumber node as a sibling of the new head.
733
+ if (messageId.startsWith(FALLBACK_ID_PREFIX)) return;
734
+ // A synchronous host update (e.g. a setMessages that re-entered the
735
+ // snapshot pass) may have evicted the message already; only that case is
736
+ // skipped, so genuine repository errors still propagate.
737
+ try {
738
+ this.repository.getMessage(messageId);
739
+ } catch {
740
+ return;
741
+ }
742
+
743
+ this.repository.deleteMessage(messageId);
744
+ this._messages = this.repository.getMessages();
745
+ this._notifySubscribers();
601
746
  }
602
747
 
603
748
  public getQueueItems() {
@@ -622,6 +767,8 @@ export class ExternalStoreThreadRuntimeCore
622
767
  if (!this._store.onReload)
623
768
  throw new Error("Runtime does not support reloading messages.");
624
769
 
770
+ this._pendingDeleteEvictions.clear();
771
+
625
772
  // Auto-abort in-flight client-side tool executions when a run reloads;
626
773
  // any results that land afterward would target a turn that no longer
627
774
  // exists. See `append` above for full rationale.
@@ -655,7 +802,7 @@ export class ExternalStoreThreadRuntimeCore
655
802
  // back here via __internal_setAdapter. The tracker publishes the
656
803
  // cleared status map itself, so adapter-side statuses reset only when
657
804
  // the tracker is the source of truth.
658
- this._toolInvocations?.reset();
805
+ this._runTrackerUpdate(() => this._toolInvocations?.reset());
659
806
 
660
807
  this._store.onLoadExternalState(state);
661
808
  }
@@ -666,7 +813,7 @@ export class ExternalStoreThreadRuntimeCore
666
813
  * without run-cancel semantics (`onCancel`, composer draft restoration).
667
814
  */
668
815
  public unstable_notifySessionReset(): void {
669
- this._toolInvocations?.reset();
816
+ this._runTrackerUpdate(() => this._toolInvocations?.reset());
670
817
  this._store.queue?.__internal_notifyCancelled?.();
671
818
  }
672
819
 
@@ -674,6 +821,8 @@ export class ExternalStoreThreadRuntimeCore
674
821
  if (!this._store.onCancel)
675
822
  throw new Error("Runtime does not support cancelling runs.");
676
823
 
824
+ const generation = captureThreadRuntimeGeneration(this);
825
+
677
826
  // Abort any in-flight client-side tool executions. Fire-and-forget —
678
827
  // the abort resolves once executions settle, but we don't gate the
679
828
  // cancel on it.
@@ -731,6 +880,8 @@ export class ExternalStoreThreadRuntimeCore
731
880
  // tick. Read the repository at flush time and re-apply the rollbacks to
732
881
  // it, instead of stamping a snapshot captured above over the newer state.
733
882
  setTimeout(() => {
883
+ if (!isThreadRuntimeGenerationCurrent(this, generation)) return;
884
+
734
885
  this.dropEmptyOptimisticHead();
735
886
  if (movedLeaf) {
736
887
  const current = this.repository.getMessages();
@@ -0,0 +1,253 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BaseSubscribable } from "../../subscribable/subscribable";
3
+ import { ReadonlyThreadRuntimeCore } from "../readonly/ReadonlyThreadRuntimeCore";
4
+ import { EMPTY_THREAD_CORE } from "../remote-thread-list/empty-thread-core";
5
+
6
+ const READONLY_ERROR = /readonly thread/;
7
+ const EMPTY_ERROR = /empty thread/;
8
+
9
+ const SHARED_THROWING_METHODS = [
10
+ "switchToBranch",
11
+ "append",
12
+ "deleteMessage",
13
+ "startRun",
14
+ "resumeRun",
15
+ "unstable_notifySessionReset",
16
+ "addToolResult",
17
+ "resumeToolCall",
18
+ "respondToToolApproval",
19
+ "speak",
20
+ "connectVoice",
21
+ "muteVoice",
22
+ "unmuteVoice",
23
+ "submitFeedback",
24
+ "exportExternalState",
25
+ "importExternalState",
26
+ "beginEdit",
27
+ "import",
28
+ "reset",
29
+ ] as const;
30
+
31
+ const SHARED_THROWING_COMPOSER_METHODS = [
32
+ "setText",
33
+ "setRole",
34
+ "setRunConfig",
35
+ "send",
36
+ "startDictation",
37
+ "setQuote",
38
+ ] as const;
39
+
40
+ const SHARED_NOOP_COMPOSER_METHODS = [
41
+ "cancel",
42
+ "stopDictation",
43
+ "moveQueueItem",
44
+ "removeQueueItem",
45
+ ] as const;
46
+
47
+ type AnyCore = Record<string, any>;
48
+
49
+ const cores = [
50
+ [
51
+ "ReadonlyThreadRuntimeCore",
52
+ () => new ReadonlyThreadRuntimeCore() as unknown as AnyCore,
53
+ READONLY_ERROR,
54
+ ],
55
+ [
56
+ "EMPTY_THREAD_CORE",
57
+ () => EMPTY_THREAD_CORE as unknown as AnyCore,
58
+ EMPTY_ERROR,
59
+ ],
60
+ ] as const;
61
+
62
+ describe.each(cores)("%s shared inert surface", (_name, makeCore, error) => {
63
+ it.each(SHARED_THROWING_METHODS)("%s throws the core's error", (method) => {
64
+ expect(() => makeCore()[method]!()).toThrow(error);
65
+ });
66
+
67
+ it.each(SHARED_THROWING_COMPOSER_METHODS)(
68
+ "composer.%s throws the core's error",
69
+ (method) => {
70
+ expect(() => makeCore().composer[method]!()).toThrow(error);
71
+ },
72
+ );
73
+
74
+ it.each(["addAttachment", "removeAttachment"] as const)(
75
+ "composer.%s rejects with the core's error",
76
+ async (method) => {
77
+ await expect(makeCore().composer[method]!()).rejects.toThrow(error);
78
+ },
79
+ );
80
+
81
+ it.each(SHARED_NOOP_COMPOSER_METHODS)("composer.%s is a no-op", (method) => {
82
+ expect(() => makeCore().composer[method]!()).not.toThrow();
83
+ });
84
+
85
+ it("composer.reset and composer.clearAttachments resolve without throwing", async () => {
86
+ const core = makeCore();
87
+ await expect(core.composer.reset()).resolves.toBeUndefined();
88
+ await expect(core.composer.clearAttachments()).resolves.toBeUndefined();
89
+ });
90
+
91
+ it("reports every capability as disabled", () => {
92
+ expect(makeCore().capabilities).toEqual({
93
+ switchToBranch: false,
94
+ switchBranchDuringRun: false,
95
+ edit: false,
96
+ delete: false,
97
+ reload: false,
98
+ refetchThread: false,
99
+ cancel: false,
100
+ unstable_copy: false,
101
+ speech: false,
102
+ dictation: false,
103
+ voice: false,
104
+ attachments: false,
105
+ feedback: false,
106
+ queue: false,
107
+ });
108
+ });
109
+
110
+ it("exposes inert read-only state", () => {
111
+ const core = makeCore();
112
+ expect(core.getModelContext()).toEqual({});
113
+ expect(core.getEditComposer()).toBeUndefined();
114
+ expect(core.getVoiceVolume()).toBe(0);
115
+ expect(core.speech).toBeUndefined();
116
+ expect(core.voice).toBeUndefined();
117
+ expect(core.state).toBeNull();
118
+ expect(core.suggestions).toEqual([]);
119
+ expect(core.extras).toBeUndefined();
120
+ expect(core.isDisabled).toBe(false);
121
+ expect(core.isSendDisabled).toBe(false);
122
+ });
123
+
124
+ it("exposes an empty composer that accepts everything and holds nothing", () => {
125
+ const composer = makeCore().composer;
126
+ expect(composer.attachmentAccept).toBe("*");
127
+ expect(composer.attachments).toEqual([]);
128
+ expect(composer.text).toBe("");
129
+ expect(composer.role).toBe("user");
130
+ expect(composer.runConfig).toEqual({});
131
+ expect(composer.queue).toEqual([]);
132
+ expect(composer.dictation).toBeUndefined();
133
+ expect(composer.quote).toBeUndefined();
134
+ expect(composer.isEmpty).toBe(true);
135
+ expect(composer.canSend).toBe(false);
136
+ expect(composer.canCancel).toBe(false);
137
+ });
138
+
139
+ it("throws one stable error object from every mutator", () => {
140
+ const core = makeCore();
141
+ const thrown = SHARED_THROWING_METHODS.map((method) => {
142
+ try {
143
+ core[method]!();
144
+ return undefined;
145
+ } catch (e) {
146
+ return e;
147
+ }
148
+ });
149
+
150
+ expect(thrown[0]).toBeInstanceOf(Error);
151
+ expect(thrown.every((e) => e === thrown[0])).toBe(true);
152
+ });
153
+
154
+ it("throws a receiver TypeError when a mutator is detached", () => {
155
+ const detached = makeCore().append!;
156
+ expect(() => detached()).toThrow(TypeError);
157
+ });
158
+
159
+ it("subscribeVoiceVolume and unstable_on hand back no-op unsubscribers", () => {
160
+ const core = makeCore();
161
+ expect(() => core.subscribeVoiceVolume(() => {})()).not.toThrow();
162
+ expect(() => core.unstable_on("runStart", () => {})()).not.toThrow();
163
+ });
164
+ });
165
+
166
+ describe("no-op versus throw divergences", () => {
167
+ it.each(["cancelRun", "stopSpeaking", "disconnectVoice"] as const)(
168
+ "%s is a no-op on the readonly core but throws on the empty core",
169
+ (method) => {
170
+ const readonly = new ReadonlyThreadRuntimeCore() as unknown as AnyCore;
171
+ expect(() => readonly[method]!()).not.toThrow();
172
+ expect(() =>
173
+ (EMPTY_THREAD_CORE as unknown as AnyCore)[method]!(),
174
+ ).toThrow(EMPTY_ERROR);
175
+ },
176
+ );
177
+
178
+ it("readonly is not loading; empty is loading so it is not read as an empty conversation", () => {
179
+ expect(new ReadonlyThreadRuntimeCore().isLoading).toBe(false);
180
+ expect(EMPTY_THREAD_CORE.isLoading).toBe(true);
181
+ });
182
+
183
+ it("readonly composer is not editing; empty composer is", () => {
184
+ expect(new ReadonlyThreadRuntimeCore().composer.isEditing).toBe(false);
185
+ expect(EMPTY_THREAD_CORE.composer.isEditing).toBe(true);
186
+ });
187
+
188
+ it("the two cores carry distinct error objects and messages", () => {
189
+ const grab = (fn: () => void) => {
190
+ try {
191
+ fn();
192
+ return undefined;
193
+ } catch (e) {
194
+ return e as Error;
195
+ }
196
+ };
197
+
198
+ const readonlyError = grab(() => new ReadonlyThreadRuntimeCore().append());
199
+ const emptyError = grab(() =>
200
+ (EMPTY_THREAD_CORE as unknown as AnyCore).append!(),
201
+ );
202
+
203
+ expect(readonlyError).not.toBe(emptyError);
204
+ expect(readonlyError?.message).toBe(
205
+ "This is a readonly thread. You cannot perform mutations on readonly threads.",
206
+ );
207
+ expect(emptyError?.message).toMatch(/placeholder for the main thread/);
208
+ });
209
+ });
210
+
211
+ describe("readonly notifications", () => {
212
+ it("notifies subscribers and settles waitForUpdate when messages change", async () => {
213
+ const core = new ReadonlyThreadRuntimeCore();
214
+ const seen: number[] = [];
215
+ const unsubscribe = core.subscribe(() => {
216
+ seen.push(core.messages.length);
217
+ });
218
+ const update = core.waitForUpdate();
219
+
220
+ core.setMessages([
221
+ {
222
+ id: "m1",
223
+ role: "user",
224
+ content: [{ type: "text", text: "hi" }],
225
+ createdAt: new Date(0),
226
+ status: { type: "complete", reason: "stop" },
227
+ metadata: { custom: {} },
228
+ } as any,
229
+ ]);
230
+
231
+ await expect(update).resolves.toBeUndefined();
232
+ expect(seen).toEqual([1]);
233
+ unsubscribe();
234
+ });
235
+ });
236
+
237
+ describe("empty singleton", () => {
238
+ it("does not retain subscribe callbacks", () => {
239
+ const subscribers = (
240
+ EMPTY_THREAD_CORE as unknown as Record<string, unknown>
241
+ )._subscribers as Set<unknown>;
242
+ const before = subscribers.size;
243
+ const unsubscribe = EMPTY_THREAD_CORE.subscribe(() => undefined);
244
+ expect(subscribers.size).toBe(before);
245
+ expect(() => unsubscribe()).not.toThrow();
246
+ });
247
+
248
+ it("rejects waitForUpdate instead of hanging", async () => {
249
+ await expect(
250
+ (EMPTY_THREAD_CORE as unknown as BaseSubscribable).waitForUpdate(),
251
+ ).rejects.toThrow(EMPTY_ERROR);
252
+ });
253
+ });