@assistant-ui/core 0.3.17 → 0.3.18

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 (284) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/attachment.js +5 -4
  3. package/dist/adapters/attachment.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal.d.ts +3 -2
  7. package/dist/internal.js +3 -2
  8. package/dist/model-context/frame/host.d.ts.map +1 -1
  9. package/dist/model-context/frame/host.js +10 -4
  10. package/dist/model-context/frame/host.js.map +1 -1
  11. package/dist/model-context/frame/provider.d.ts +2 -0
  12. package/dist/model-context/frame/provider.d.ts.map +1 -1
  13. package/dist/model-context/frame/provider.js +30 -7
  14. package/dist/model-context/frame/provider.js.map +1 -1
  15. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
  16. package/dist/model-context/interactable-composer-metadata.js +16 -9
  17. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  18. package/dist/model-context/registry.d.ts.map +1 -1
  19. package/dist/model-context/registry.js +3 -5
  20. package/dist/model-context/registry.js.map +1 -1
  21. package/dist/model-context/types.d.ts.map +1 -1
  22. package/dist/model-context/types.js +5 -4
  23. package/dist/model-context/types.js.map +1 -1
  24. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  25. package/dist/react/client/InMemoryThreadList.js +62 -56
  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 +24 -27
  29. package/dist/react/client/Interactables.js.map +1 -1
  30. package/dist/react/client/RemoteThreadList.d.ts +1 -0
  31. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  32. package/dist/react/client/RemoteThreadList.js +151 -141
  33. package/dist/react/client/RemoteThreadList.js.map +1 -1
  34. package/dist/react/client/Tools.d.ts.map +1 -1
  35. package/dist/react/client/Tools.js +18 -16
  36. package/dist/react/client/Tools.js.map +1 -1
  37. package/dist/react/interactables-legacy/Interactables.js +242 -121
  38. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  39. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
  40. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +11 -9
  41. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -1
  42. package/dist/react/model-context/makeAssistantTool.d.ts +2 -2
  43. package/dist/react/model-context/makeAssistantTool.js +1 -1
  44. package/dist/react/model-context/makeAssistantTool.js.map +1 -1
  45. package/dist/react/model-context/useAssistantTool.d.ts +2 -2
  46. package/dist/react/model-context/useAssistantTool.js +1 -1
  47. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  48. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
  49. package/dist/react/model-context/useToolArgsStatus.js +2 -1
  50. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  51. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  52. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +16 -8
  53. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  54. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -1
  55. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  56. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +103 -103
  57. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  58. package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -0
  59. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  60. package/dist/react/runtimes/RemoteThreadResource.js +13 -11
  61. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  62. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +4 -1
  63. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  64. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +401 -34
  65. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  66. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
  67. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +2 -1
  68. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
  69. package/dist/react/runtimes/useExternalStoreRuntime.js +75 -46
  70. package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -1
  71. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  72. package/dist/react/runtimes/useRemoteThreadListRuntime.js +63 -23
  73. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  74. package/dist/runtime/api/thread-list-item-runtime.d.ts +7 -3
  75. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  76. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  77. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  78. package/dist/runtime/api/thread-list-runtime.d.ts +3 -0
  79. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  80. package/dist/runtime/api/thread-list-runtime.js +18 -14
  81. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  82. package/dist/runtime/api/thread-runtime.d.ts +4 -0
  83. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  84. package/dist/runtime/api/thread-runtime.js +1 -1
  85. package/dist/runtime/api/thread-runtime.js.map +1 -1
  86. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  87. package/dist/runtime/base/base-composer-runtime-core.js +16 -12
  88. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  89. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  90. package/dist/runtime/base/base-thread-runtime-core.js +6 -5
  91. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  92. package/dist/runtime/interfaces/composer-runtime-core.d.ts +12 -5
  93. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  94. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +4 -1
  95. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  96. package/dist/runtime/utils/external-message-conversion.js +1 -1
  97. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  98. package/dist/runtime/utils/message-repository.d.ts +1 -0
  99. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  100. package/dist/runtime/utils/message-repository.js +15 -6
  101. package/dist/runtime/utils/message-repository.js.map +1 -1
  102. package/dist/runtime/utils/thread-message-like.js +1 -1
  103. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  104. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  105. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +21 -27
  106. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  107. package/dist/runtimes/internal.d.ts +2 -2
  108. package/dist/runtimes/internal.js +2 -2
  109. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +18 -1
  110. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  111. package/dist/runtimes/remote-thread-list/remote-thread-state.js +130 -42
  112. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  113. package/dist/runtimes/remote-thread-list/title-generation.d.ts +49 -0
  114. package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -0
  115. package/dist/runtimes/remote-thread-list/title-generation.js +197 -0
  116. package/dist/runtimes/remote-thread-list/title-generation.js.map +1 -0
  117. package/dist/runtimes/remote-thread-list/title.d.ts +3 -1
  118. package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -1
  119. package/dist/runtimes/remote-thread-list/title.js +9 -1
  120. package/dist/runtimes/remote-thread-list/title.js.map +1 -1
  121. package/dist/runtimes/remote-thread-list/types.d.ts +10 -0
  122. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  123. package/dist/store/clients/external-thread.d.ts.map +1 -1
  124. package/dist/store/clients/external-thread.js +19 -3
  125. package/dist/store/clients/external-thread.js.map +1 -1
  126. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  127. package/dist/store/clients/model-context-client.js +2 -1
  128. package/dist/store/clients/model-context-client.js.map +1 -1
  129. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  130. package/dist/store/clients/single-thread-list.js +1 -0
  131. package/dist/store/clients/single-thread-list.js.map +1 -1
  132. package/dist/store/clients/suggestions.d.ts.map +1 -1
  133. package/dist/store/clients/suggestions.js +5 -4
  134. package/dist/store/clients/suggestions.js.map +1 -1
  135. package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -0
  136. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  137. package/dist/store/runtime-clients/composer-runtime-client.js +97 -61
  138. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  139. package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -0
  140. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  141. package/dist/store/runtime-clients/message-runtime-client.js +221 -144
  142. package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
  143. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  144. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  145. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  146. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  147. package/dist/store/runtime-clients/thread-list-runtime-client.js +54 -52
  148. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  149. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  150. package/dist/store/runtime-clients/thread-runtime-client.js +193 -121
  151. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  152. package/dist/store/scopes/composer.d.ts +10 -2
  153. package/dist/store/scopes/composer.d.ts.map +1 -1
  154. package/dist/store/scopes/message.d.ts +30 -1
  155. package/dist/store/scopes/message.d.ts.map +1 -1
  156. package/dist/store/scopes/thread-list-item.d.ts +2 -2
  157. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  158. package/dist/store/scopes/thread.d.ts +8 -0
  159. package/dist/store/scopes/thread.d.ts.map +1 -1
  160. package/dist/store/scopes/threads.d.ts +1 -0
  161. package/dist/store/scopes/threads.d.ts.map +1 -1
  162. package/dist/types/attachment.d.ts +2 -1
  163. package/dist/types/attachment.d.ts.map +1 -1
  164. package/dist/types/attachment.js +2 -1
  165. package/dist/types/attachment.js.map +1 -1
  166. package/dist/utils/abortable-iterable.d.ts +19 -0
  167. package/dist/utils/abortable-iterable.d.ts.map +1 -0
  168. package/dist/utils/abortable-iterable.js +72 -0
  169. package/dist/utils/abortable-iterable.js.map +1 -0
  170. package/dist/utils/composite-context-provider.d.ts.map +1 -1
  171. package/dist/utils/composite-context-provider.js +20 -2
  172. package/dist/utils/composite-context-provider.js.map +1 -1
  173. package/dist/utils/record.d.ts +11 -0
  174. package/dist/utils/record.d.ts.map +1 -0
  175. package/dist/utils/record.js +12 -0
  176. package/dist/utils/record.js.map +1 -0
  177. package/package.json +10 -10
  178. package/src/adapters/attachment.test.ts +50 -1
  179. package/src/adapters/attachment.ts +7 -4
  180. package/src/index.ts +1 -0
  181. package/src/internal.ts +4 -0
  182. package/src/model-context/frame/host.test.ts +24 -0
  183. package/src/model-context/frame/host.ts +10 -4
  184. package/src/model-context/frame/provider.test.ts +71 -0
  185. package/src/model-context/frame/provider.ts +37 -7
  186. package/src/model-context/interactable-composer-metadata.test.ts +28 -0
  187. package/src/model-context/interactable-composer-metadata.ts +9 -3
  188. package/src/model-context/registry.test.ts +12 -0
  189. package/src/model-context/registry.ts +3 -2
  190. package/src/model-context/types.test.ts +25 -0
  191. package/src/model-context/types.ts +9 -4
  192. package/src/react/RuntimeAdapter.test.ts +34 -0
  193. package/src/react/client/InMemoryThreadList.ts +1 -0
  194. package/src/react/client/Interactables.test.ts +28 -0
  195. package/src/react/client/Interactables.ts +15 -13
  196. package/src/react/client/RemoteThreadList.background.test.ts +148 -6
  197. package/src/react/client/RemoteThreadList.concurrent.test.tsx +42 -1
  198. package/src/react/client/RemoteThreadList.test.ts +345 -3
  199. package/src/react/client/RemoteThreadList.ts +166 -110
  200. package/src/react/client/Tools.ts +22 -11
  201. package/src/react/interactables-legacy/Interactables.ts +16 -16
  202. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +9 -10
  203. package/src/react/model-context/makeAssistantTool.ts +2 -2
  204. package/src/react/model-context/useAssistantTool.ts +2 -2
  205. package/src/react/model-context/useToolArgsStatus.test.tsx +30 -0
  206. package/src/react/model-context/useToolArgsStatus.ts +2 -1
  207. package/src/react/runtimes/RemoteThreadListHookInstanceManager.destroy.test.tsx +172 -0
  208. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +33 -0
  209. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +7 -1
  210. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +28 -5
  211. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.load-error.test.ts +132 -0
  212. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +105 -0
  213. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +136 -111
  214. package/src/react/runtimes/RemoteThreadResource.test.ts +9 -0
  215. package/src/react/runtimes/RemoteThreadResource.ts +19 -12
  216. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +483 -6
  217. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +504 -68
  218. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +15 -0
  219. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +1 -0
  220. package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +41 -0
  221. package/src/react/runtimes/useExternalStoreRuntime.ts +10 -4
  222. package/src/react/runtimes/useLocalRuntime.test.tsx +20 -0
  223. package/src/react/runtimes/useRemoteThreadListRuntime.load-error.test.tsx +108 -0
  224. package/src/react/runtimes/useRemoteThreadListRuntime.ts +33 -0
  225. package/src/runtime/api/message-runtime.test.ts +75 -0
  226. package/src/runtime/api/thread-list-item-runtime.ts +10 -3
  227. package/src/runtime/api/thread-list-runtime.ts +21 -11
  228. package/src/runtime/api/thread-runtime.test.ts +78 -0
  229. package/src/runtime/api/thread-runtime.ts +8 -1
  230. package/src/runtime/base/base-composer-runtime-core.ts +36 -8
  231. package/src/runtime/base/base-thread-runtime-core.test.ts +55 -1
  232. package/src/runtime/base/base-thread-runtime-core.ts +6 -4
  233. package/src/runtime/interfaces/composer-runtime-core.ts +10 -5
  234. package/src/runtime/interfaces/thread-list-runtime-core.ts +5 -1
  235. package/src/runtime/utils/external-message-conversion.test.ts +68 -0
  236. package/src/runtime/utils/external-message-conversion.ts +1 -1
  237. package/src/runtime/utils/message-repository.ts +29 -17
  238. package/src/runtime/utils/thread-message-like.ts +1 -1
  239. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +18 -13
  240. package/src/runtimes/internal.ts +1 -0
  241. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +128 -2
  242. package/src/runtimes/remote-thread-list/remote-thread-state.ts +182 -42
  243. package/src/runtimes/remote-thread-list/title-generation.test.ts +602 -0
  244. package/src/runtimes/remote-thread-list/title-generation.ts +329 -0
  245. package/src/runtimes/remote-thread-list/title.test.ts +62 -0
  246. package/src/runtimes/remote-thread-list/title.ts +13 -0
  247. package/src/runtimes/remote-thread-list/types.ts +10 -0
  248. package/src/store/clients/external-thread.ts +48 -3
  249. package/src/store/clients/model-context-client.ts +5 -1
  250. package/src/store/clients/single-thread-list.ts +1 -0
  251. package/src/store/clients/suggestions.test.ts +40 -0
  252. package/src/store/clients/suggestions.ts +13 -10
  253. package/src/store/runtime-clients/composer-runtime-client.ts +42 -9
  254. package/src/store/runtime-clients/message-runtime-client.ts +54 -6
  255. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +4 -2
  256. package/src/store/runtime-clients/thread-list-runtime-client.ts +1 -0
  257. package/src/store/runtime-clients/thread-runtime-client.ts +58 -10
  258. package/src/store/scopes/composer.ts +16 -4
  259. package/src/store/scopes/message.ts +14 -0
  260. package/src/store/scopes/thread-list-item.ts +5 -2
  261. package/src/store/scopes/thread.ts +4 -0
  262. package/src/store/scopes/threads.ts +1 -0
  263. package/src/tests/MessageRepository.test.ts +116 -0
  264. package/src/tests/OptimisticState-list-race.test.ts +1 -0
  265. package/src/tests/RemoteThreadListThreadListRuntimeCore-initialize-adapter-change.test.ts +38 -0
  266. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +47 -0
  267. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +89 -0
  268. package/src/tests/attachment-adapters.test.ts +14 -0
  269. package/src/tests/base-composer-runtime-core.test.ts +39 -0
  270. package/src/tests/external-store-thread-list-runtime-core.test.ts +83 -0
  271. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +27 -18
  272. package/src/tests/external-thread-attachments.test.tsx +139 -0
  273. package/src/tests/external-thread-parity.test.tsx +23 -0
  274. package/src/tests/remote-thread-list-isLoading.test.ts +1 -0
  275. package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
  276. package/src/tests/thread-switch-events.test.tsx +78 -0
  277. package/src/tests/tools-scope-migration.test.tsx +28 -0
  278. package/src/types/attachment.ts +4 -0
  279. package/src/utils/abortable-iterable.test.ts +213 -0
  280. package/src/utils/abortable-iterable.ts +93 -0
  281. package/src/utils/composite-context-provider.test.ts +25 -0
  282. package/src/utils/composite-context-provider.ts +23 -2
  283. package/src/utils/record.test.ts +48 -0
  284. package/src/utils/record.ts +11 -0
@@ -5,6 +5,21 @@ import { renderHook } from "@testing-library/react";
5
5
  import type { AssistantCloud } from "assistant-cloud";
6
6
  import { createCloudThreadListAdapter } from "./createCloudThreadListAdapter";
7
7
 
8
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
9
+ ...(await importOriginal<typeof import("@assistant-ui/store")>()),
10
+ useAui: () => ({
11
+ threads: {
12
+ getState: () => ({ mainThreadId: "local-1", threadItems: [] }),
13
+ },
14
+ thread: { getState: () => ({ isEmpty: true, suggestions: [] }) },
15
+ threadListItem: {
16
+ getState: () => ({ id: "local-1", remoteId: "remote-1" }),
17
+ },
18
+ on: () => () => {},
19
+ subscribe: () => () => {},
20
+ }),
21
+ }));
22
+
8
23
  const makeCloud = () =>
9
24
  ({
10
25
  threads: {
@@ -43,6 +43,7 @@ export const useCloudRuntimeAdapters = (
43
43
  () => ({
44
44
  history,
45
45
  attachments,
46
+ feedback: history.feedback,
46
47
  }),
47
48
  [history, attachments],
48
49
  );
@@ -5,8 +5,49 @@ import { act, render } from "@testing-library/react";
5
5
  import { describe, expect, it, vi } from "vitest";
6
6
  import { useExternalStoreRuntime } from "./useExternalStoreRuntime";
7
7
  import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
8
+ import type { ThreadMessage } from "../../types/message";
9
+ import { RuntimeAdapterProvider } from "./RuntimeAdapterProvider";
10
+
11
+ const userMessage: ThreadMessage = {
12
+ id: "user-1",
13
+ role: "user",
14
+ content: [{ type: "text", text: "hello" }],
15
+ attachments: [],
16
+ createdAt: new Date(0),
17
+ metadata: { custom: {} },
18
+ };
8
19
 
9
20
  describe("useExternalStoreRuntime lifecycle", () => {
21
+ it("uses feedback supplied by the per-thread adapter context", () => {
22
+ const submit = vi.fn();
23
+ const capture: { runtime: AssistantRuntime | null } = { runtime: null };
24
+ const App = () => {
25
+ const runtime = useExternalStoreRuntime({
26
+ messages: [userMessage],
27
+ onNew: async () => {},
28
+ });
29
+ capture.runtime = runtime;
30
+ return null;
31
+ };
32
+
33
+ render(
34
+ <RuntimeAdapterProvider adapters={{ feedback: { submit } }}>
35
+ <App />
36
+ </RuntimeAdapterProvider>,
37
+ );
38
+
39
+ expect(capture.runtime!.thread.getState().capabilities.feedback).toBe(true);
40
+ act(() => {
41
+ capture
42
+ .runtime!.thread.getMessageById("user-1")
43
+ .submitFeedback({ type: "positive" });
44
+ });
45
+ expect(submit).toHaveBeenCalledWith({
46
+ message: expect.objectContaining({ id: "user-1" }),
47
+ type: "positive",
48
+ });
49
+ });
50
+
10
51
  it("keeps dispatching appends after StrictMode's simulated remount", async () => {
11
52
  const onNew = vi.fn(async () => {});
12
53
  const capture: { runtime: AssistantRuntime | null } = { runtime: null };
@@ -11,7 +11,15 @@ import { useRuntimeAdapters } from "./RuntimeAdapterProvider";
11
11
  export const useExternalStoreRuntime = <T>(
12
12
  store: ExternalStoreAdapter<T>,
13
13
  ): AssistantRuntime => {
14
- const [runtime] = useState(() => new ExternalStoreRuntimeCore(store));
14
+ const { modelContext, feedback } = useRuntimeAdapters() ?? {};
15
+ const adaptedStore = useMemo(() => {
16
+ if (!feedback || store.adapters?.feedback) return store;
17
+ return {
18
+ ...store,
19
+ adapters: { ...store.adapters, feedback },
20
+ };
21
+ }, [feedback, store]);
22
+ const [runtime] = useState(() => new ExternalStoreRuntimeCore(adaptedStore));
15
23
 
16
24
  useEffect(() => {
17
25
  return () => {
@@ -20,11 +28,9 @@ export const useExternalStoreRuntime = <T>(
20
28
  }, [runtime]);
21
29
 
22
30
  useEffect(() => {
23
- runtime.setAdapter(store);
31
+ runtime.setAdapter(adaptedStore);
24
32
  });
25
33
 
26
- const { modelContext } = useRuntimeAdapters() ?? {};
27
-
28
34
  useEffect(() => {
29
35
  if (!modelContext) return undefined;
30
36
  return runtime.registerModelContextProvider(modelContext);
@@ -32,6 +32,26 @@ afterEach(() => {
32
32
  });
33
33
 
34
34
  describe("useLocalRuntime", () => {
35
+ it("enables feedback for Cloud threads", async () => {
36
+ const cloud = makeCloud();
37
+ let runtime: ReturnType<typeof useLocalRuntime> | null = null;
38
+ const App = () => {
39
+ runtime = useLocalRuntime(chatModel, { cloud });
40
+ return (
41
+ <AssistantRuntimeProvider runtime={runtime}>
42
+ <div />
43
+ </AssistantRuntimeProvider>
44
+ );
45
+ };
46
+
47
+ render(<App />);
48
+
49
+ await waitFor(() => {
50
+ expect(cloud.threads.list).toHaveBeenCalledTimes(2);
51
+ expect(runtime!.thread.getState().capabilities.feedback).toBe(true);
52
+ });
53
+ });
54
+
35
55
  it("surfaces the live thread after mount without user input", async () => {
36
56
  const auiRef: { current: ReturnType<typeof useAui> | null } = {
37
57
  current: null,
@@ -0,0 +1,108 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, cleanup, render, waitFor } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
6
+ import { makeAdapter } from "../../tests/remote-thread-list-test-helpers";
7
+ import { AssistantRuntimeProvider } from "../AssistantRuntimeProvider";
8
+ import { useExternalStoreRuntime } from "./useExternalStoreRuntime";
9
+ import { useRemoteThreadListRuntime } from "./useRemoteThreadListRuntime";
10
+
11
+ const EMPTY_MESSAGES: readonly never[] = [];
12
+
13
+ afterEach(() => {
14
+ cleanup();
15
+ vi.restoreAllMocks();
16
+ });
17
+
18
+ describe("useRemoteThreadListRuntime load recovery", () => {
19
+ it("reloads once when the browser comes online after a failed load", async () => {
20
+ const error = new Error("offline");
21
+ vi.spyOn(console, "error").mockImplementation(() => {});
22
+ const list = vi
23
+ .fn()
24
+ .mockRejectedValueOnce(error)
25
+ .mockResolvedValueOnce({ threads: [] });
26
+ const adapter = makeAdapter({ list });
27
+ const runtimeRef: { current: AssistantRuntime | null } = { current: null };
28
+ const useThreadRuntime = () =>
29
+ useExternalStoreRuntime({
30
+ messages: EMPTY_MESSAGES,
31
+ onNew: async () => {},
32
+ } as never);
33
+
34
+ const App = () => {
35
+ const runtime = useRemoteThreadListRuntime({
36
+ adapter,
37
+ runtimeHook: useThreadRuntime,
38
+ });
39
+ runtimeRef.current = runtime;
40
+ return (
41
+ <AssistantRuntimeProvider runtime={runtime}>
42
+ {null}
43
+ </AssistantRuntimeProvider>
44
+ );
45
+ };
46
+
47
+ render(<App />);
48
+ await waitFor(() => expect(list).toHaveBeenCalledTimes(1));
49
+ await waitFor(() =>
50
+ expect(runtimeRef.current!.threads.getState().loadError).toBe(error),
51
+ );
52
+
53
+ act(() => window.dispatchEvent(new Event("online")));
54
+
55
+ await waitFor(() => expect(list).toHaveBeenCalledTimes(2));
56
+ await act(async () => {});
57
+ expect(list).toHaveBeenCalledTimes(2);
58
+ expect(runtimeRef.current!.threads.getState().loadError).toBeUndefined();
59
+ });
60
+
61
+ it("reloads once when the page becomes visible after a failed load", async () => {
62
+ const error = new Error("offline");
63
+ vi.spyOn(console, "error").mockImplementation(() => {});
64
+ vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible");
65
+ const list = vi
66
+ .fn()
67
+ .mockRejectedValueOnce(error)
68
+ .mockResolvedValueOnce({ threads: [] });
69
+ const adapter = makeAdapter({ list });
70
+ const runtimeRef: { current: AssistantRuntime | null } = { current: null };
71
+ const useThreadRuntime = () =>
72
+ useExternalStoreRuntime({
73
+ messages: EMPTY_MESSAGES,
74
+ onNew: async () => {},
75
+ } as never);
76
+
77
+ const App = () => {
78
+ const runtime = useRemoteThreadListRuntime({
79
+ adapter,
80
+ runtimeHook: useThreadRuntime,
81
+ });
82
+ runtimeRef.current = runtime;
83
+ return (
84
+ <AssistantRuntimeProvider runtime={runtime}>
85
+ {null}
86
+ </AssistantRuntimeProvider>
87
+ );
88
+ };
89
+
90
+ render(<App />);
91
+ await waitFor(() => expect(list).toHaveBeenCalledTimes(1));
92
+ await waitFor(() =>
93
+ expect(runtimeRef.current!.threads.getState().loadError).toBe(error),
94
+ );
95
+
96
+ act(() => document.dispatchEvent(new Event("visibilitychange")));
97
+
98
+ await waitFor(() => expect(list).toHaveBeenCalledTimes(2));
99
+ await waitFor(() =>
100
+ expect(runtimeRef.current!.threads.getState().loadError).toBeUndefined(),
101
+ );
102
+
103
+ act(() => document.dispatchEvent(new Event("visibilitychange")));
104
+ await act(async () => {});
105
+
106
+ expect(list).toHaveBeenCalledTimes(2);
107
+ });
108
+ });
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  useState,
3
3
  useEffect,
4
+ useInsertionEffect,
4
5
  useMemo,
5
6
  useRef,
6
7
  useCallback,
@@ -39,11 +40,43 @@ const useRemoteThreadListRuntimeImpl = (
39
40
  options: RemoteThreadListOptions,
40
41
  ): AssistantRuntime => {
41
42
  const [runtime] = useState(() => new RemoteThreadListRuntimeCore(options));
43
+
44
+ // Insertion-effect cleanup runs only when React deletes the fiber, so a
45
+ // hidden <Activity> or a re-suspended boundary keeps the threads alive; the
46
+ // disposal is deferred to a microtask because it notifies subscribers and
47
+ // React forbids scheduling updates from an insertion effect.
48
+ useInsertionEffect(
49
+ () => () => queueMicrotask(() => runtime.threads.__internal_dispose()),
50
+ [runtime],
51
+ );
52
+
42
53
  useEffect(() => {
43
54
  runtime.threads.__internal_setOptions(options);
44
55
  runtime.threads.__internal_load();
45
56
  }, [runtime, options]);
46
57
 
58
+ useEffect(() => {
59
+ if (typeof window === "undefined" || typeof document === "undefined") {
60
+ return;
61
+ }
62
+
63
+ const reloadAfterError = () => {
64
+ if (runtime.threads.loadError !== undefined) {
65
+ void runtime.threads.reload();
66
+ }
67
+ };
68
+ const reloadAfterVisible = () => {
69
+ if (document.visibilityState === "visible") reloadAfterError();
70
+ };
71
+
72
+ window.addEventListener("online", reloadAfterError);
73
+ document.addEventListener("visibilitychange", reloadAfterVisible);
74
+ return () => {
75
+ window.removeEventListener("online", reloadAfterError);
76
+ document.removeEventListener("visibilitychange", reloadAfterVisible);
77
+ };
78
+ }, [runtime]);
79
+
47
80
  return useMemo(() => new AssistantRuntimeImpl(runtime), [runtime]);
48
81
  };
49
82
 
@@ -11,6 +11,7 @@ import {
11
11
  type MessageStateBinding,
12
12
  } from "./message-runtime";
13
13
  import { toMessagePartStatus } from "../../utils/normalizePartStatus";
14
+ import { convertExternalMessageChunk } from "../utils/external-message-conversion";
14
15
 
15
16
  const messagePath = {
16
17
  ref: "threads.main.messages[0]",
@@ -256,6 +257,80 @@ describe("toMessagePartStatus", () => {
256
257
  });
257
258
 
258
259
  describe("MessageRuntimeImpl paths", () => {
260
+ it.each([undefined, ""])(
261
+ "looks up separate tool calls with ID %s without breaking provider result matching",
262
+ (toolCallId) => {
263
+ const id = toolCallId === undefined ? {} : { toolCallId };
264
+ const converted = convertExternalMessageChunk(
265
+ {
266
+ inputs: [{}],
267
+ outputs: [
268
+ {
269
+ role: "assistant",
270
+ content: [
271
+ { type: "tool-call", ...id, toolName: "weather", args: {} },
272
+ { type: "tool-call", ...id, toolName: "search", args: {} },
273
+ {
274
+ type: "tool-call",
275
+ toolCallId: "provider-call",
276
+ toolName: "calendar",
277
+ args: { day: "Monday" },
278
+ },
279
+ ],
280
+ },
281
+ {
282
+ role: "assistant",
283
+ content: [
284
+ {
285
+ type: "tool-call",
286
+ toolCallId: "provider-call",
287
+ toolName: "calendar",
288
+ args: { day: "Tuesday" },
289
+ },
290
+ ],
291
+ },
292
+ {
293
+ role: "tool",
294
+ toolCallId: "provider-call",
295
+ toolName: "calendar",
296
+ result: "Meeting at noon",
297
+ },
298
+ ],
299
+ },
300
+ 0,
301
+ 1,
302
+ false,
303
+ undefined,
304
+ );
305
+ const runtime = new MessageRuntimeImpl(
306
+ {
307
+ ...messageBinding,
308
+ getState: () => ({ ...message, ...converted }),
309
+ },
310
+ threadBinding,
311
+ );
312
+ const calls = converted.content.filter(
313
+ (part) => part.type === "tool-call",
314
+ );
315
+
316
+ expect(
317
+ calls.map((call) =>
318
+ runtime.getMessagePartByToolCallId(call.toolCallId).getState(),
319
+ ),
320
+ ).toMatchObject([
321
+ { type: "tool-call", toolName: "weather" },
322
+ { type: "tool-call", toolName: "search" },
323
+ {
324
+ type: "tool-call",
325
+ toolCallId: "provider-call",
326
+ toolName: "calendar",
327
+ args: { day: "Tuesday" },
328
+ result: "Meeting at noon",
329
+ },
330
+ ]);
331
+ },
332
+ );
333
+
259
334
  it("appends nested selectors to the message path", () => {
260
335
  const runtime = new MessageRuntimeImpl(messageBinding, threadBinding);
261
336
 
@@ -30,12 +30,17 @@ import type { ThreadListItemStatus } from "../interfaces/thread-list-runtime-cor
30
30
 
31
31
  export type { ThreadListItemState, ThreadListItemStatus };
32
32
 
33
+ export type ThreadListItemGenerateTitleOptions = {
34
+ /** Marks a generation started by the automatic title trigger. */
35
+ automatic?: boolean;
36
+ };
37
+
33
38
  export type ThreadListItemRuntime = {
34
39
  readonly path: ThreadListItemRuntimePath;
35
40
  getState(): ThreadListItemState;
36
41
 
37
42
  initialize(): Promise<{ remoteId: string; externalId: string | undefined }>;
38
- generateTitle(): Promise<void>;
43
+ generateTitle(options?: ThreadListItemGenerateTitleOptions): Promise<void>;
39
44
 
40
45
  switchTo(options?: { unarchive?: boolean }): Promise<void>;
41
46
  rename(newTitle: string): Promise<void>;
@@ -147,9 +152,11 @@ export class ThreadListItemRuntimeImpl implements ThreadListItemRuntime {
147
152
  return this._threadListBinding.initialize(state.id);
148
153
  }
149
154
 
150
- public generateTitle(): Promise<void> {
155
+ public generateTitle(
156
+ options?: ThreadListItemGenerateTitleOptions,
157
+ ): Promise<void> {
151
158
  const state = this._core.getState();
152
- return this._threadListBinding.generateTitle(state.id);
159
+ return this._threadListBinding.generateTitle(state.id, options);
153
160
  }
154
161
 
155
162
  public unstable_on<E extends ThreadListItemEventType>(
@@ -15,6 +15,7 @@ import {
15
15
  type ThreadListItemRuntime,
16
16
  ThreadListItemRuntimeImpl,
17
17
  type ThreadListItemState,
18
+ type ThreadListItemStateBinding,
18
19
  } from "./thread-list-item-runtime";
19
20
  import {
20
21
  type ThreadListItemRuntimeBinding,
@@ -32,6 +33,8 @@ export type ThreadListState = {
32
33
  readonly threadIds: readonly string[];
33
34
  readonly archivedThreadIds: readonly string[];
34
35
  readonly isLoading: boolean;
36
+ /** The error thrown by the most recent thread list load that failed, cleared when a later load starts. */
37
+ readonly loadError: unknown;
35
38
  readonly isLoadingMore: boolean;
36
39
  readonly hasMore: boolean;
37
40
  readonly threadItems: Readonly<
@@ -97,6 +100,7 @@ const getThreadListState = (
97
100
  threadIds: threadList.threadIds,
98
101
  archivedThreadIds: threadList.archivedThreadIds,
99
102
  isLoading: threadList.isLoading,
103
+ loadError: threadList.loadError,
100
104
  isLoadingMore: threadList.isLoadingMore ?? false,
101
105
  hasMore: threadList.hasMore ?? false,
102
106
  threadItems: threadList.threadItems,
@@ -250,6 +254,21 @@ export class ThreadListRuntimeImpl implements ThreadListRuntime {
250
254
  return this._mainThreadListItemRuntime;
251
255
  }
252
256
 
257
+ private _createItemStateBinding(
258
+ threadId: string,
259
+ ): ThreadListItemStateBinding {
260
+ return new ShallowMemoizeSubject({
261
+ path: {
262
+ ref: `threadItems[threadId=${threadId}]`,
263
+ threadSelector: { type: "threadId", threadId },
264
+ },
265
+ getState: () => {
266
+ return getThreadListItemState(this._core, threadId);
267
+ },
268
+ subscribe: (callback) => this._core.subscribe(callback),
269
+ });
270
+ }
271
+
253
272
  public getById(threadId: string) {
254
273
  return new this._runtimeFactory(
255
274
  new NestedSubscriptionSubject({
@@ -260,7 +279,7 @@ export class ThreadListRuntimeImpl implements ThreadListRuntime {
260
279
  getState: () => this._core.getThreadRuntimeCore(threadId),
261
280
  subscribe: (callback) => this._core.subscribe(callback),
262
281
  }),
263
- this.mainItem,
282
+ this._createItemStateBinding(threadId),
264
283
  );
265
284
  }
266
285
 
@@ -301,16 +320,7 @@ export class ThreadListRuntimeImpl implements ThreadListRuntime {
301
320
 
302
321
  public getItemById(threadId: string) {
303
322
  return new ThreadListItemRuntimeImpl(
304
- new ShallowMemoizeSubject({
305
- path: {
306
- ref: `threadItems[threadId=${threadId}]`,
307
- threadSelector: { type: "threadId", threadId },
308
- },
309
- getState: () => {
310
- return getThreadListItemState(this._core, threadId);
311
- },
312
- subscribe: (callback) => this._core.subscribe(callback),
313
- }),
323
+ this._createItemStateBinding(threadId),
314
324
  this._core,
315
325
  );
316
326
  }
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { LocalRuntimeCore } from "../../runtimes/local/local-runtime-core";
3
+ import { ExternalStoreRuntimeCore } from "../../runtimes/external-store/external-store-runtime-core";
4
+ import { AssistantRuntimeImpl } from "./assistant-runtime";
5
+
6
+ describe("ThreadRuntime.append", () => {
7
+ it.each([
8
+ { parent: { parentId: null }, expectedParent: null, visible: ["new"] },
9
+ {
10
+ parent: {},
11
+ expectedParent: "tail",
12
+ visible: ["root", "tail", "new"],
13
+ },
14
+ {
15
+ parent: { parentId: "root" },
16
+ expectedParent: "root",
17
+ visible: ["root", "new"],
18
+ },
19
+ ])(
20
+ "selects the branch under $expectedParent",
21
+ ({ parent, expectedParent, visible }) => {
22
+ const core = new LocalRuntimeCore(
23
+ { adapters: { chatModel: { run: async () => ({ content: [] }) } } },
24
+ [
25
+ { id: "root", role: "user", content: "root" },
26
+ { id: "tail", role: "assistant", content: "tail" },
27
+ ],
28
+ );
29
+ const thread = new AssistantRuntimeImpl(core).thread;
30
+
31
+ thread.append({
32
+ ...parent,
33
+ content: [{ type: "text", text: "new" }],
34
+ startRun: false,
35
+ });
36
+
37
+ expect(
38
+ thread.getState().messages.map((message) => message.content),
39
+ ).toEqual(visible.map((text) => [{ type: "text", text }]));
40
+ expect(thread.export().messages.at(-1)?.parentId).toBe(expectedParent);
41
+ },
42
+ );
43
+ });
44
+
45
+ describe("ThreadRuntime.append with an external store", () => {
46
+ it("routes an explicit root parent to onEdit instead of onNew", async () => {
47
+ const onNew = vi.fn(async () => {});
48
+ const onEdit = vi.fn(async () => {});
49
+ const core = new ExternalStoreRuntimeCore({
50
+ messages: [
51
+ {
52
+ id: "old",
53
+ role: "user",
54
+ content: [{ type: "text", text: "old" }],
55
+ createdAt: new Date(0),
56
+ attachments: [],
57
+ metadata: { custom: {} },
58
+ },
59
+ ],
60
+ onNew,
61
+ onEdit,
62
+ });
63
+ const thread = new AssistantRuntimeImpl(core).thread;
64
+
65
+ thread.append({
66
+ parentId: null,
67
+ content: [{ type: "text", text: "new root" }],
68
+ startRun: false,
69
+ });
70
+
71
+ await vi.waitFor(() =>
72
+ expect(onEdit).toHaveBeenCalledExactlyOnceWith(
73
+ expect.objectContaining({ parentId: null }),
74
+ ),
75
+ );
76
+ expect(onNew).not.toHaveBeenCalled();
77
+ });
78
+ });
@@ -77,6 +77,10 @@ const toStartRunConfig = (message: CreateStartRunConfig): StartRunConfig => {
77
77
  export type CreateAppendMessage =
78
78
  | string
79
79
  | {
80
+ /**
81
+ * An omitted value or `undefined` selects the current tail.
82
+ * `null` selects a root branch.
83
+ */
80
84
  parentId?: string | null | undefined;
81
85
  sourceId?: string | null | undefined;
82
86
  role?: AppendMessage["role"] | undefined;
@@ -107,7 +111,10 @@ const toAppendMessage = (
107
111
 
108
112
  return {
109
113
  createdAt: message.createdAt ?? new Date(),
110
- parentId: message.parentId ?? messages.at(-1)?.id ?? null,
114
+ parentId:
115
+ message.parentId === undefined
116
+ ? (messages.at(-1)?.id ?? null)
117
+ : message.parentId,
111
118
  sourceId: message.sourceId ?? null,
112
119
  role: message.role ?? "user",
113
120
  content: message.content,