@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
@@ -2,6 +2,7 @@ import type {
2
2
  RemoteThreadInitializeResponse,
3
3
  RemoteThreadMetadata,
4
4
  } from "./types";
5
+ import { generateId } from "../../utils/id";
5
6
 
6
7
  export type RemoteThreadData =
7
8
  | {
@@ -11,6 +12,7 @@ export type RemoteThreadData =
11
12
  readonly status: "new";
12
13
  readonly title: undefined;
13
14
  readonly custom: undefined;
15
+ readonly localOrigin?: true;
14
16
  }
15
17
  | {
16
18
  readonly id: string;
@@ -20,6 +22,7 @@ export type RemoteThreadData =
20
22
  readonly status: "regular" | "archived";
21
23
  readonly title?: string | undefined;
22
24
  readonly custom: undefined;
25
+ readonly localOrigin?: true;
23
26
  }
24
27
  | {
25
28
  readonly id: string;
@@ -30,6 +33,7 @@ export type RemoteThreadData =
30
33
  readonly title?: string | undefined;
31
34
  readonly lastMessageAt?: Date | undefined;
32
35
  readonly custom?: Record<string, unknown> | undefined;
36
+ readonly localOrigin?: true;
33
37
  };
34
38
 
35
39
  export type THREAD_MAPPING_ID = string & { __brand: "THREAD_MAPPING_ID" };
@@ -38,6 +42,8 @@ export function createThreadMappingId(id: string): THREAD_MAPPING_ID {
38
42
  return id as THREAD_MAPPING_ID;
39
43
  }
40
44
 
45
+ export const LOCAL_THREAD_ID_PREFIX = "__LOCALID_";
46
+
41
47
  export const normalizeCursor = (c: string | undefined): string | undefined =>
42
48
  c || undefined;
43
49
 
@@ -98,6 +104,123 @@ export type RemoteThreadState = {
98
104
  readonly threadData: Readonly<Record<THREAD_MAPPING_ID, RemoteThreadData>>;
99
105
  };
100
106
 
107
+ export const createEmptyRemoteThreadState = (): RemoteThreadState => ({
108
+ isLoading: true,
109
+ isLoadingMore: false,
110
+ cursor: undefined,
111
+ newThreadId: undefined,
112
+ threadIds: [],
113
+ archivedThreadIds: [],
114
+ threadIdMap: {},
115
+ threadData: {},
116
+ });
117
+
118
+ export const seedNewThread = (
119
+ state: RemoteThreadState,
120
+ ): { id: string; state: RemoteThreadState } => {
121
+ let id: string;
122
+ do {
123
+ id = `${LOCAL_THREAD_ID_PREFIX}${generateId()}`;
124
+ } while (state.threadIdMap[id]);
125
+ const mappingId = createThreadMappingId(id);
126
+ return {
127
+ id,
128
+ state: {
129
+ ...state,
130
+ newThreadId: id,
131
+ threadIdMap: {
132
+ ...state.threadIdMap,
133
+ [id]: mappingId,
134
+ },
135
+ threadData: {
136
+ ...state.threadData,
137
+ [mappingId]: {
138
+ status: "new",
139
+ id,
140
+ remoteId: undefined,
141
+ externalId: undefined,
142
+ title: undefined,
143
+ custom: undefined,
144
+ localOrigin: true,
145
+ } satisfies RemoteThreadData,
146
+ },
147
+ },
148
+ };
149
+ };
150
+
151
+ // A list() response predating a mid-flight local transition (a thread
152
+ // initialized while the request was running) omits that thread, and the
153
+ // completed-optimistic replay cannot re-add it because the merged threadData
154
+ // already carries the final status. Threads whose status moved from new (or
155
+ // nonexistent) at request time to regular/archived are re-inserted at the
156
+ // top, in the order the pre-response lists carried them: updateStatusReducer
157
+ // prepends as transitions happen, so the pre-response lists hold newest
158
+ // first, which threadData insertion order need not match. Threads the server already knew stay governed by the response, so
159
+ // a server-side deletion is not resurrected — and a thread unknown to the
160
+ // snapshot is only rescued when it carries the localOrigin marker stamped at
161
+ // draft creation, so a deep-linked thread the fetch path deliberately
162
+ // appended at the tail is not hoisted, whatever its remote id looks like.
163
+ export const preserveMidLoadTransitions = (
164
+ state: RemoteThreadState,
165
+ priorOrder: Pick<RemoteThreadState, "threadIds" | "archivedThreadIds">,
166
+ statusAtRequest: ReadonlyMap<string, RemoteThreadData["status"]>,
167
+ ): RemoteThreadState => {
168
+ const regular = new Set(state.threadIds);
169
+ const archived = new Set(state.archivedThreadIds);
170
+ const rescuedRegular: RemoteThreadData[] = [];
171
+ const rescuedArchived: RemoteThreadData[] = [];
172
+
173
+ const contains = (ids: ReadonlySet<string>, data: RemoteThreadData) =>
174
+ ids.has(data.id) || (data.remoteId !== undefined && ids.has(data.remoteId));
175
+
176
+ for (const data of Object.values(state.threadData)) {
177
+ const before = statusAtRequest.get(data.id);
178
+ if (before !== undefined && before !== "new") continue;
179
+ if (before === undefined && data.localOrigin !== true) continue;
180
+
181
+ if (data.status === "regular" && !contains(regular, data)) {
182
+ rescuedRegular.push(data);
183
+ regular.add(data.id);
184
+ } else if (data.status === "archived" && !contains(archived, data)) {
185
+ rescuedArchived.push(data);
186
+ archived.add(data.id);
187
+ }
188
+ }
189
+
190
+ if (rescuedRegular.length === 0 && rescuedArchived.length === 0) return state;
191
+
192
+ const position = (ids: readonly string[], data: RemoteThreadData) => {
193
+ const byId = ids.indexOf(data.id);
194
+ if (byId !== -1) return byId;
195
+ const byRemoteId =
196
+ data.remoteId !== undefined ? ids.indexOf(data.remoteId) : -1;
197
+ return byRemoteId !== -1 ? byRemoteId : Number.MAX_SAFE_INTEGER;
198
+ };
199
+ rescuedRegular.sort(
200
+ (a, b) =>
201
+ position(priorOrder.threadIds, a) - position(priorOrder.threadIds, b),
202
+ );
203
+ rescuedArchived.sort(
204
+ (a, b) =>
205
+ position(priorOrder.archivedThreadIds, a) -
206
+ position(priorOrder.archivedThreadIds, b),
207
+ );
208
+
209
+ return {
210
+ ...state,
211
+ threadIds: [...rescuedRegular.map((d) => d.id), ...state.threadIds],
212
+ archivedThreadIds: [
213
+ ...rescuedArchived.map((d) => d.id),
214
+ ...state.archivedThreadIds,
215
+ ],
216
+ };
217
+ };
218
+
219
+ export const statusSnapshot = (
220
+ state: RemoteThreadState,
221
+ ): ReadonlyMap<string, RemoteThreadData["status"]> =>
222
+ new Map(Object.values(state.threadData).map((d) => [d.id, d.status]));
223
+
101
224
  export const getThreadData = (
102
225
  state: RemoteThreadState,
103
226
  threadIdOrRemoteId: string,
@@ -96,6 +96,10 @@ export type RemoteThreadListAdapter = {
96
96
 
97
97
  export type RemoteThreadListOptions = {
98
98
  runtimeHook: () => AssistantRuntime;
99
+
100
+ /**
101
+ * The adapter reference should remain stable across renders. Replacing it reloads the list and drops cached threads that are not in the replacement page. In-flight mutations from the previous adapter are cancelled.
102
+ */
99
103
  adapter: RemoteThreadListAdapter;
100
104
 
101
105
  /**
@@ -78,6 +78,23 @@ sees only the first result.
78
78
  ### A.7. Previously-resolved tool loses its `result` (back to undefined)
79
79
  Silently ignored. The entry stays in the resolved phase internally.
80
80
 
81
+ ### A.8. Tool call carries a provider `approval`
82
+ The entry is marked `skipExecute`, exactly like a call observed with
83
+ a `result`, whether the gate is present when the call is first observed
84
+ or lands on an already-active entry. A gate belongs to the provider:
85
+ `approved === true` means the provider is producing the result, `false`
86
+ means it records the denial, so the frontend `execute` never fires for
87
+ that `toolCallId` once the gate is present. `streamCall` still fires
88
+ once and the backend result flows through A.5 when it lands.
89
+
90
+ If the gate lands after the args stream has closed but before an
91
+ in-flight `execute` resolves, the execute runs to completion (its side
92
+ effects happen) but its result chunk is dropped: `onResult` never fires,
93
+ and the `executing` status, plus any `human()` interrupt the execution
94
+ parked, stays up until the promise settles. Only a gate that lands after
95
+ the result chunk has already been emitted is fully too late; the adapter
96
+ must project the gate before that (#6285).
97
+
81
98
  ## B. Tool call disappears from snapshot
82
99
 
83
100
  ### B.1. Tool call removed entirely (rollback, branch switch)
@@ -47,6 +47,7 @@ const createAssistantMessage = (
47
47
  toolCallId?: string;
48
48
  toolName?: string;
49
49
  nestedMessages?: ThreadAssistantMessage[];
50
+ approval?: { id: string; approved?: boolean };
50
51
  },
51
52
  ): ThreadAssistantMessage => ({
52
53
  id: "m-1",
@@ -70,6 +71,7 @@ const createAssistantMessage = (
70
71
  ...(options?.result !== undefined && { result: options.result }),
71
72
  ...(options?.isError !== undefined && { isError: options.isError }),
72
73
  ...(options?.nestedMessages && { messages: options.nestedMessages }),
74
+ ...(options?.approval && { approval: options.approval }),
73
75
  },
74
76
  ],
75
77
  });
@@ -370,6 +372,144 @@ describe("ToolInvocationTracker", () => {
370
372
  });
371
373
  });
372
374
 
375
+ it("never executes a tool call that carries a provider approval", async () => {
376
+ const execute = vi.fn(async () => ({ deleted: true }));
377
+ const getTools = () => ({
378
+ deleteFile: {
379
+ parameters: { type: "object", properties: {} },
380
+ execute,
381
+ } satisfies Tool,
382
+ });
383
+ const onResult = vi.fn();
384
+ const tracker = new ToolInvocationTracker(getTools, {
385
+ onResult,
386
+ onStatusesChange: () => {},
387
+ });
388
+ tracker.setState(createState([]));
389
+
390
+ const gated = (approval: { id: string; approved?: boolean }) =>
391
+ createAssistantMessage(
392
+ '{"path":"/tmp/a"}',
393
+ { path: "/tmp/a" },
394
+ { toolName: "deleteFile", approval },
395
+ );
396
+
397
+ tracker.setState(createState([gated({ id: "approval-1" })], false));
398
+ await new Promise((r) => setTimeout(r, 0));
399
+ expect(execute).not.toHaveBeenCalled();
400
+
401
+ tracker.setState(
402
+ createState([gated({ id: "approval-1", approved: true })], true),
403
+ );
404
+ await new Promise((r) => setTimeout(r, 0));
405
+
406
+ expect(execute).not.toHaveBeenCalled();
407
+ expect(onResult).not.toHaveBeenCalled();
408
+ });
409
+
410
+ it("stops executing a live tool call once a provider approval lands", async () => {
411
+ const execute = vi.fn(async () => ({ deleted: true }));
412
+ const getTools = () => ({
413
+ deleteFile: {
414
+ parameters: { type: "object", properties: {} },
415
+ execute,
416
+ } satisfies Tool,
417
+ });
418
+ const onResult = vi.fn();
419
+ const tracker = new ToolInvocationTracker(getTools, {
420
+ onResult,
421
+ onStatusesChange: () => {},
422
+ });
423
+ tracker.setState(createState([]));
424
+
425
+ tracker.setState(
426
+ createState(
427
+ [
428
+ createAssistantMessage(
429
+ '{"path":"/tmp',
430
+ {},
431
+ { toolName: "deleteFile" },
432
+ ),
433
+ ],
434
+ true,
435
+ ),
436
+ );
437
+ tracker.setState(
438
+ createState(
439
+ [
440
+ createAssistantMessage(
441
+ '{"path":"/tmp/a"}',
442
+ { path: "/tmp/a" },
443
+ { toolName: "deleteFile", approval: { id: "approval-1" } },
444
+ ),
445
+ ],
446
+ true,
447
+ ),
448
+ );
449
+ await new Promise((r) => setTimeout(r, 0));
450
+
451
+ expect(execute).not.toHaveBeenCalled();
452
+ expect(onResult).not.toHaveBeenCalled();
453
+ });
454
+
455
+ it("drops the result of an in-flight execute once a provider approval lands", async () => {
456
+ let resolveExecute!: (value: { deleted: boolean }) => void;
457
+ const execute = vi.fn(
458
+ () =>
459
+ new Promise<{ deleted: boolean }>((r) => {
460
+ resolveExecute = r;
461
+ }),
462
+ );
463
+ const getTools = () => ({
464
+ deleteFile: {
465
+ parameters: { type: "object", properties: {} },
466
+ execute,
467
+ } satisfies Tool,
468
+ });
469
+ const onResult = vi.fn();
470
+ const tracker = new ToolInvocationTracker(getTools, {
471
+ onResult,
472
+ onStatusesChange: () => {},
473
+ });
474
+ tracker.setState(createState([]));
475
+
476
+ tracker.setState(
477
+ createState(
478
+ [
479
+ createAssistantMessage(
480
+ '{"path":"/tmp/a"}',
481
+ { path: "/tmp/a" },
482
+ {
483
+ toolName: "deleteFile",
484
+ },
485
+ ),
486
+ ],
487
+ true,
488
+ ),
489
+ );
490
+ await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
491
+
492
+ tracker.setState(
493
+ createState(
494
+ [
495
+ createAssistantMessage(
496
+ '{"path":"/tmp/a"}',
497
+ { path: "/tmp/a" },
498
+ {
499
+ toolName: "deleteFile",
500
+ approval: { id: "approval-1" },
501
+ },
502
+ ),
503
+ ],
504
+ true,
505
+ ),
506
+ );
507
+ resolveExecute({ deleted: true });
508
+ await new Promise((r) => setTimeout(r, 0));
509
+
510
+ expect(onResult).not.toHaveBeenCalled();
511
+ });
512
+
373
513
  it("does not re-execute asynchronously loaded resolved tool calls after reset", async () => {
374
514
  const execute = vi.fn(async () => ({ forecast: "ok" }));
375
515
  const getTools = () => ({
@@ -519,6 +519,7 @@ export class ToolInvocationTracker {
519
519
  // The host already set the result (via the live snapshot's
520
520
  // `setResponse` path). Suppress the executor's redundant emit.
521
521
  if (entry?.hasResult) return;
522
+ if (entry.skipExecute) return;
522
523
 
523
524
  this._invokeOnResult({
524
525
  type: "add-tool-result",
@@ -794,6 +795,8 @@ export class ToolInvocationTracker {
794
795
  );
795
796
  }
796
797
 
798
+ if (content.approval !== undefined) entry.skipExecute = true;
799
+
797
800
  this._processArgsText(entry, content);
798
801
 
799
802
  if (content.result !== undefined && !entry.hasResult) {
@@ -44,6 +44,10 @@ import type { ComposerSendOptions } from "../scopes/composer";
44
44
  import { fileMatchesAccept } from "../../adapters/attachment";
45
45
  import { getThreadMessageText } from "../../utils/text";
46
46
  import { resolveToolApprovalResponse } from "../../runtime/utils/resolveToolApprovalResponse";
47
+ import {
48
+ AttachmentAddOperations,
49
+ drainAttachmentAdd,
50
+ } from "../../runtime/utils/attachment-add-operations";
47
51
  import { toMessagePartStatus } from "../../utils/normalizePartStatus";
48
52
  import { ModelContext } from "./model-context-client";
49
53
  import { ThreadSuggestions } from "./suggestions";
@@ -420,53 +424,6 @@ type ComposerClientResourceProps = {
420
424
  attachmentAdapter?: AttachmentAdapter | undefined;
421
425
  };
422
426
 
423
- type AttachmentAddOperation = {
424
- cancelled: boolean;
425
- attachmentIds: Set<string>;
426
- };
427
-
428
- class AttachmentAddOperations {
429
- private readonly operations = new Set<AttachmentAddOperation>();
430
-
431
- start() {
432
- const operation: AttachmentAddOperation = {
433
- cancelled: false,
434
- attachmentIds: new Set(),
435
- };
436
- this.operations.add(operation);
437
- return operation;
438
- }
439
-
440
- accept(operation: AttachmentAddOperation, attachmentId: string) {
441
- if (operation.cancelled) return false;
442
- operation.attachmentIds.add(attachmentId);
443
- return true;
444
- }
445
-
446
- finish(operation: AttachmentAddOperation) {
447
- this.operations.delete(operation);
448
- }
449
-
450
- isCancelled(operation: AttachmentAddOperation) {
451
- return operation.cancelled;
452
- }
453
-
454
- cancel(attachmentId: string) {
455
- for (const operation of [...this.operations]) {
456
- if (!operation.attachmentIds.has(attachmentId)) continue;
457
- operation.cancelled = true;
458
- this.operations.delete(operation);
459
- }
460
- }
461
-
462
- cancelAll() {
463
- for (const operation of this.operations) {
464
- operation.cancelled = true;
465
- }
466
- this.operations.clear();
467
- }
468
- }
469
-
470
427
  const useQueueItemClient = ({
471
428
  item,
472
429
  onMove,
@@ -486,19 +443,6 @@ const useQueueItemClient = ({
486
443
 
487
444
  const QueueItemClient = resource(useQueueItemClient);
488
445
 
489
- const drainAdapterAdd = async (
490
- result: ReturnType<AttachmentAdapter["add"]>,
491
- upsert: (attachment: Attachment) => boolean,
492
- ) => {
493
- if (Symbol.asyncIterator in result) {
494
- for await (const attachment of result) {
495
- if (!upsert(attachment)) break;
496
- }
497
- } else {
498
- upsert(await result);
499
- }
500
- };
501
-
502
446
  // State whose setter tracks the latest value in a ref, so imperative
503
447
  // call sequences (setText immediately followed by send) observe the write
504
448
  // before React re-renders — legacy composer parity.
@@ -672,7 +616,7 @@ const useComposerClientResource = ({
672
616
  if (!isCreateAttachment(fileOrAttachment) && attachmentAdapter) {
673
617
  const operation = attachmentAddOperations.start();
674
618
  try {
675
- await drainAdapterAdd(
619
+ await drainAttachmentAdd(
676
620
  attachmentAdapter.add({ file: fileOrAttachment }),
677
621
  (attachment) => {
678
622
  if (!attachmentAddOperations.accept(operation, attachment.id))
@@ -795,6 +739,17 @@ const useComposerClientResource = ({
795
739
  }
796
740
  },
797
741
  cancel: () => {
742
+ // An edit session ends here, so its in-flight adapter adds must not
743
+ // land in a later session; the thread composer's cancel stops the run
744
+ // and leaves the draft (and its pending adds) alone.
745
+ if (type === "edit") {
746
+ attachmentAddOperations.cancelAll();
747
+ const removed = attachmentsRef.current;
748
+ setAttachments([]);
749
+ removePendingAttachments(removed).catch((error) => {
750
+ console.error("Failed to remove cancelled edit attachments", error);
751
+ });
752
+ }
798
753
  onCancel?.();
799
754
  if (type === "edit") setIsEditing(false);
800
755
  },
@@ -1085,7 +1040,7 @@ const useExternalThread = ({
1085
1040
  }));
1086
1041
 
1087
1042
  return {
1088
- isEmpty: messages.length === 0,
1043
+ isEmpty: messages.length === 0 && !isLoading,
1089
1044
  isDisabled: false,
1090
1045
  isLoading,
1091
1046
  isRunning,
@@ -93,7 +93,10 @@ const useSingleThreadList = ({
93
93
  !(
94
94
  typeof selector === "object" &&
95
95
  "index" in selector &&
96
- selector.index === 0
96
+ selector.index === 0 &&
97
+ // Index selectors address their archived/regular subset, and the
98
+ // archived subset here is always empty.
99
+ !selector.archived
97
100
  )
98
101
  ) {
99
102
  throw new Error(
@@ -0,0 +1,31 @@
1
+ import { useState } from "react";
2
+ import { createTapRoot, flushTapSync, useResource } from "@assistant-ui/tap";
3
+ import { describe, expect, it } from "vitest";
4
+ import { Suggestions, type SuggestionConfig } from "./suggestions";
5
+
6
+ describe("Suggestions", () => {
7
+ it("updates when the configured suggestions change", () => {
8
+ let setSuggestions!: (suggestions: SuggestionConfig[]) => void;
9
+ const root = createTapRoot(function SuggestionsRoot() {
10
+ const [suggestions, setValue] = useState<SuggestionConfig[]>([
11
+ "account-a",
12
+ ]);
13
+ setSuggestions = setValue;
14
+ return useResource(Suggestions(suggestions));
15
+ });
16
+
17
+ try {
18
+ expect(root.getValue().getState().suggestions[0]?.prompt).toBe(
19
+ "account-a",
20
+ );
21
+
22
+ flushTapSync(() => setSuggestions(["account-b"]));
23
+
24
+ expect(root.getValue().getState().suggestions[0]?.prompt).toBe(
25
+ "account-b",
26
+ );
27
+ } finally {
28
+ root.unmount();
29
+ }
30
+ });
31
+ });
@@ -1,4 +1,4 @@
1
- import { useMemo, useState } from "react";
1
+ import { useMemo } from "react";
2
2
  import { resource, withKey } from "@assistant-ui/tap";
3
3
  import type { ClientOutput } from "@assistant-ui/store";
4
4
  import { useClientLookup } from "@assistant-ui/store/client";
@@ -40,26 +40,25 @@ const useSuggestionsClient = (
40
40
  const useStaticSuggestions = (
41
41
  suggestions?: SuggestionConfig[],
42
42
  ): ClientOutput<"suggestions"> => {
43
- const [state] = useState<SuggestionsState>(() => {
44
- const normalizedSuggestions = (suggestions ?? []).map((s) => {
45
- if (typeof s === "string") {
43
+ const state = useMemo<SuggestionsState>(
44
+ () => ({
45
+ suggestions: (suggestions ?? []).map((s) => {
46
+ if (typeof s === "string") {
47
+ return {
48
+ title: s,
49
+ label: "",
50
+ prompt: s,
51
+ };
52
+ }
46
53
  return {
47
- title: s,
48
- label: "",
49
- prompt: s,
54
+ title: s.title,
55
+ label: s.label,
56
+ prompt: s.prompt,
50
57
  };
51
- }
52
- return {
53
- title: s.title,
54
- label: s.label,
55
- prompt: s.prompt,
56
- };
57
- });
58
-
59
- return {
60
- suggestions: normalizedSuggestions,
61
- };
62
- });
58
+ }),
59
+ }),
60
+ [suggestions],
61
+ );
63
62
 
64
63
  return useSuggestionsClient(state);
65
64
  };
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { ThreadListAdapterChangedError } from "../../runtimes/remote-thread-list/adapter-changed";
3
+ import { handleRuntimeAction } from "./handle-runtime-action";
4
+
5
+ describe("handleRuntimeAction", () => {
6
+ it("does not log a silent adapter-change cancellation", async () => {
7
+ const error = new ThreadListAdapterChangedError();
8
+ const consoleError = vi
9
+ .spyOn(console, "error")
10
+ .mockImplementation(() => {});
11
+
12
+ try {
13
+ await expect(
14
+ handleRuntimeAction("thread list archive", () => Promise.reject(error)),
15
+ ).rejects.toBe(error);
16
+ expect(consoleError).not.toHaveBeenCalled();
17
+ } finally {
18
+ consoleError.mockRestore();
19
+ }
20
+ });
21
+
22
+ it("logs an ordinary rejection", async () => {
23
+ const error = new Error("archive failed");
24
+ const consoleError = vi
25
+ .spyOn(console, "error")
26
+ .mockImplementation(() => {});
27
+
28
+ try {
29
+ await expect(
30
+ handleRuntimeAction("thread list archive", () => Promise.reject(error)),
31
+ ).rejects.toBe(error);
32
+ expect(consoleError).toHaveBeenCalledExactlyOnceWith(
33
+ "[assistant-ui] thread list archive failed:",
34
+ error,
35
+ );
36
+ } finally {
37
+ consoleError.mockRestore();
38
+ }
39
+ });
40
+ });
@@ -1,3 +1,5 @@
1
+ import { isSilentRuntimeAction } from "../../utils/silent-runtime-action";
2
+
1
3
  export const handleRuntimeAction = (
2
4
  label: string,
3
5
  execute: () => Promise<void>,
@@ -5,6 +7,7 @@ export const handleRuntimeAction = (
5
7
  const task = execute();
6
8
 
7
9
  void task.catch((error: unknown) => {
10
+ if (isSilentRuntimeAction(error)) return;
8
11
  console.error(`[assistant-ui] ${label} failed:`, error);
9
12
  });
10
13
  return task;
@@ -19,7 +19,7 @@ export type MessageState = ThreadMessage & {
19
19
  * @example
20
20
  * ```ts
21
21
  * import { WebSpeechSynthesisAdapter } from "@assistant-ui/react";
22
- * import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
22
+ * import { useChatRuntime } from "@assistant-ui/ai-sdk";
23
23
  *
24
24
  * const runtime = useChatRuntime({
25
25
  * adapters: {
@@ -1,14 +1,7 @@
1
- import type { FileMessagePart, TextMessagePart } from "../../types/message";
2
1
  import type { QueuePlacement } from "../../runtime/queue/external-thread-queue-adapter";
2
+ import type { QueueItemState } from "../../runtime/queue/queue-item";
3
3
 
4
- export type QueueItemState = {
5
- readonly id: string;
6
- /** @deprecated Derive from the text parts of `parts` instead. Removal after 2026-11-05. */
7
- readonly prompt: string;
8
- readonly parts: readonly (FileMessagePart | TextMessagePart)[];
9
- };
10
-
11
- export const EMPTY_QUEUE_ITEMS: readonly QueueItemState[] = Object.freeze([]);
4
+ export type { QueueItemState };
12
5
 
13
6
  export type QueueItemMethods = {
14
7
  getState(): QueueItemState;