@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
@@ -113,6 +113,52 @@ describe("RemoteThreadListThreadListRuntimeCore load race", () => {
113
113
  expect(core.threadIds).toEqual([]);
114
114
  });
115
115
 
116
+ it("keeps the initialized runtime when its listed duplicate is selected", async () => {
117
+ const listDeferred = deferred<ListResult>();
118
+ const initializeDeferred = deferred<{
119
+ remoteId: string;
120
+ externalId: undefined;
121
+ }>();
122
+ const adapter = makeAdapter({
123
+ list: vi.fn(() => listDeferred.promise),
124
+ initialize: vi.fn(() => initializeDeferred.promise),
125
+ });
126
+ const core = createCore(adapter);
127
+ const hookManager = (
128
+ core as unknown as {
129
+ _hookManager: { stopThreadRuntime: (threadId: string) => void };
130
+ }
131
+ )._hookManager;
132
+ const stopThreadRuntime = vi.spyOn(hookManager, "stopThreadRuntime");
133
+
134
+ const loadPromise = core.getLoadThreadsPromise();
135
+ await core.switchToNewThread();
136
+ const localId = core.newThreadId!;
137
+ const initializePromise = core.initialize(localId);
138
+
139
+ listDeferred.resolve({
140
+ threads: [
141
+ { status: "regular", remoteId: "remote-1", externalId: "remote-1" },
142
+ ],
143
+ });
144
+ await loadPromise;
145
+ await core.switchToThread("remote-1");
146
+
147
+ initializeDeferred.resolve({
148
+ remoteId: "remote-1",
149
+ externalId: undefined,
150
+ });
151
+ await initializePromise;
152
+
153
+ expect(core.mainThreadId).toBe(localId);
154
+ expect(core.threadIds).toEqual([localId]);
155
+ expect(Object.keys(core.threadItems)).toEqual([localId]);
156
+ expect(core.getItemById("remote-1")?.id).toBe(localId);
157
+ expect(core.getItemById("remote-1")?.externalId).toBe("remote-1");
158
+ expect(stopThreadRuntime).toHaveBeenCalledWith("remote-1");
159
+ expect(stopThreadRuntime).not.toHaveBeenCalledWith(localId);
160
+ });
161
+
116
162
  it("does not leave the collapsed slot in both lists when the race reported it archived", async () => {
117
163
  const listDeferred = deferred<ListResult>();
118
164
  const initializeDeferred = deferred<{
@@ -160,6 +206,7 @@ describe("preserveMidLoadTransitions", () => {
160
206
  lists: { threadIds?: string[]; archivedThreadIds?: string[] } = {},
161
207
  ): RemoteThreadState => ({
162
208
  isLoading: false,
209
+ loadError: undefined,
163
210
  isLoadingMore: false,
164
211
  cursor: undefined,
165
212
  newThreadId: undefined,
@@ -6,6 +6,95 @@ import {
6
6
  } from "./remote-thread-list-test-helpers";
7
7
 
8
8
  describe("RemoteThreadListThreadListRuntimeCore title generation", () => {
9
+ it("preserves an existing title when generation returns no title", async () => {
10
+ const adapter = makeAdapter({
11
+ list: vi.fn(async () => ({
12
+ threads: [
13
+ {
14
+ status: "regular" as const,
15
+ remoteId: "thread-1",
16
+ externalId: "thread-1",
17
+ title: "Existing title",
18
+ },
19
+ ],
20
+ })),
21
+ });
22
+ const core = createCore(adapter);
23
+ await core.getLoadThreadsPromise();
24
+
25
+ const internals = core as unknown as {
26
+ _hookManager: { getThreadRuntimeCore: () => { messages: never[] } };
27
+ };
28
+ internals._hookManager.getThreadRuntimeCore = () => ({ messages: [] });
29
+
30
+ await core.generateTitle("thread-1");
31
+
32
+ expect(adapter.generateTitle).toHaveBeenCalledOnce();
33
+ expect(core.getItemById("thread-1")?.title).toBe("Existing title");
34
+ });
35
+
36
+ it("keeps a manual rename made during automatic title generation", async () => {
37
+ const generatedTitle = deferred<ReadableStream>();
38
+ const adapter = makeAdapter({
39
+ list: vi.fn(async () => ({
40
+ threads: [
41
+ {
42
+ status: "regular" as const,
43
+ remoteId: "thread-1",
44
+ externalId: "thread-1",
45
+ title: "New chat",
46
+ },
47
+ ],
48
+ })),
49
+ generateTitle: vi.fn(async () => generatedTitle.promise as never),
50
+ });
51
+ const core = createCore(adapter);
52
+ await core.getLoadThreadsPromise();
53
+
54
+ const internals = core as unknown as {
55
+ _hookManager: { getThreadRuntimeCore: () => { messages: never[] } };
56
+ };
57
+ internals._hookManager.getThreadRuntimeCore = () => ({ messages: [] });
58
+
59
+ const generation = core.generateTitle("thread-1", { automatic: true });
60
+ await vi.waitFor(() => {
61
+ expect(adapter.generateTitle).toHaveBeenCalledOnce();
62
+ });
63
+ await core.rename("thread-1", "Manual title");
64
+
65
+ generatedTitle.resolve(
66
+ new ReadableStream({
67
+ start(controller) {
68
+ controller.enqueue({
69
+ type: "part-start",
70
+ path: [0],
71
+ part: { type: "text" },
72
+ });
73
+ controller.enqueue({
74
+ type: "text-delta",
75
+ path: [0],
76
+ textDelta: "Generated title",
77
+ });
78
+ controller.enqueue({ type: "part-finish", path: [0] });
79
+ controller.close();
80
+ },
81
+ }),
82
+ );
83
+ await generation;
84
+
85
+ expect(core.getItemById("thread-1")?.title).toBe("Manual title");
86
+ expect(adapter.rename).toHaveBeenNthCalledWith(
87
+ 1,
88
+ "thread-1",
89
+ "Manual title",
90
+ );
91
+ expect(adapter.rename).toHaveBeenNthCalledWith(
92
+ 2,
93
+ "thread-1",
94
+ "Manual title",
95
+ );
96
+ });
97
+
9
98
  it("keeps a generated title when an earlier rename completes during another update", async () => {
10
99
  const reloadRequest = deferred<{
11
100
  threads: {
@@ -8,6 +8,20 @@ import {
8
8
  } from "../adapters/attachment";
9
9
 
10
10
  describe("fileMatchesAccept", () => {
11
+ it.each([
12
+ ["BACKUP.TAR.GZ", ".tar.gz"],
13
+ [".env", ".env"],
14
+ ])("accepts %s for %s", (name, accept) => {
15
+ expect(fileMatchesAccept({ name, type: "" }, accept)).toBe(true);
16
+ });
17
+
18
+ it.each([
19
+ ["png", ".png"],
20
+ ["foo.mjs", ".js"],
21
+ ])("rejects %s for %s", (name, accept) => {
22
+ expect(fileMatchesAccept({ name, type: "" }, accept)).toBe(false);
23
+ });
24
+
11
25
  it("matches MIME types with parameters", () => {
12
26
  expect(
13
27
  fileMatchesAccept(
@@ -103,6 +103,45 @@ describe("BaseComposerRuntimeCore", () => {
103
103
  expect(listener).not.toHaveBeenCalled();
104
104
  });
105
105
 
106
+ it("reset keeps discarded text out of later dictation results", async () => {
107
+ let emitSpeech!: (result: DictationAdapter.Result) => void;
108
+ composer.setDictationAdapter({
109
+ listen: () => ({
110
+ status: { type: "running" },
111
+ stop: async () => {},
112
+ cancel: () => {},
113
+ onSpeechStart: () => () => {},
114
+ onSpeechEnd: () => () => {},
115
+ onSpeech: (callback) => {
116
+ emitSpeech = callback;
117
+ return () => {};
118
+ },
119
+ }),
120
+ });
121
+
122
+ composer.setText("old");
123
+ composer.startDictation();
124
+ try {
125
+ await composer.reset();
126
+ expect(composer.text).toBe("");
127
+ emitSpeech({ transcript: "new", isFinal: true });
128
+ expect(composer.text).toBe("new");
129
+
130
+ emitSpeech({ transcript: "pending", isFinal: false });
131
+ expect(composer.text).toBe("new pending");
132
+ await composer.reset();
133
+ expect(composer.text).toBe("");
134
+ expect(composer.dictation?.transcript).toBeUndefined();
135
+
136
+ emitSpeech({ transcript: "next", isFinal: false });
137
+ expect(composer.text).toBe("next");
138
+ emitSpeech({ transcript: "next final", isFinal: true });
139
+ expect(composer.text).toBe("next final");
140
+ } finally {
141
+ composer.stopDictation();
142
+ await Promise.resolve();
143
+ }
144
+ });
106
145
  it("send includes quote in metadata and clears it", async () => {
107
146
  const quote = { text: "quoted", messageId: "m1" };
108
147
  composer.setText("reply");
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
2
2
  import { ExternalStoreThreadListRuntimeCore } from "../runtimes/external-store/external-store-thread-list-runtime-core";
3
3
  import type { ExternalStoreThreadRuntimeCore } from "../runtimes/external-store/external-store-thread-runtime-core";
4
4
  import { ExternalStoreThreadRuntimeCore as ExternalStoreThreadRuntimeCoreImpl } from "../runtimes/external-store/external-store-thread-runtime-core";
5
+ import { ExternalStoreRuntimeCore } from "../runtimes/external-store/external-store-runtime-core";
5
6
  import type { ExternalStoreThreadListAdapter } from "../runtimes/external-store/external-store-adapter";
6
7
  import type { ExternalStoreAdapter } from "../runtimes/external-store/external-store-adapter";
7
8
  import type { ModelContextProvider } from "../model-context/types";
@@ -109,10 +110,56 @@ describe("ExternalStoreThreadListRuntimeCore - construction", () => {
109
110
  // Two empty-adapter constructions should share the frozen DEFAULT_THREAD_DATA
110
111
  // singleton rather than each getting a fresh `{ ... }` clone.
111
112
  expect(a.threadItems).toBe(b.threadItems);
113
+ expect(Object.getPrototypeOf(a.threadItems)).toBeNull();
112
114
  });
115
+
116
+ it.each(["__proto__", "constructor", "toString"])(
117
+ "returns undefined for an absent prototype-named thread id %s",
118
+ (threadId) => {
119
+ const core = new ExternalStoreThreadListRuntimeCore(
120
+ makeAdapter(),
121
+ makeFactory(),
122
+ );
123
+
124
+ expect(core.getItemById(threadId)).toBeUndefined();
125
+ expect(Object.hasOwn(core.threadItems, threadId)).toBe(false);
126
+ },
127
+ );
113
128
  });
114
129
 
115
130
  describe("ExternalStoreThreadListRuntimeCore - __internal_setAdapter", () => {
131
+ it("updates subscribed loading state when thread ids and arrays stay unchanged", () => {
132
+ const threadList = makeAdapter({
133
+ threadId: "thread-alpha",
134
+ threads: [{ id: "thread-alpha", status: "regular" }],
135
+ archivedThreads: [],
136
+ isLoading: true,
137
+ });
138
+ const adapter: ExternalStoreAdapter = {
139
+ messages: [],
140
+ onNew: async () => {},
141
+ adapters: { threadList },
142
+ };
143
+ const core = new ExternalStoreRuntimeCore(adapter);
144
+ const runtime = new ThreadListRuntimeImpl(core.threads);
145
+ const seen: boolean[] = [];
146
+ const unsubscribe = runtime.subscribe(() => {
147
+ seen.push(runtime.getState().isLoading);
148
+ });
149
+ expect(runtime.getState().isLoading).toBe(true);
150
+
151
+ for (const isLoading of [false, true, undefined, false]) {
152
+ core.setAdapter({
153
+ ...adapter,
154
+ adapters: { threadList: { ...threadList, isLoading } },
155
+ });
156
+ }
157
+
158
+ expect(seen).toEqual([false, true, false]);
159
+ expect(runtime.getState().isLoading).toBe(false);
160
+ unsubscribe();
161
+ });
162
+
116
163
  it("updates mainThreadId when adapter.threadId changes", () => {
117
164
  const core = new ExternalStoreThreadListRuntimeCore(
118
165
  makeAdapter({ threadId: "thread-alpha" }),
@@ -198,6 +245,42 @@ describe("ExternalStoreThreadListRuntimeCore - __internal_setAdapter", () => {
198
245
  expect(item?.id).toBe("thread-beta");
199
246
  });
200
247
 
248
+ it.each(["__proto__", "constructor", "toString"])(
249
+ "synthesizes a main entry for a prototype-named thread id %s",
250
+ (threadId) => {
251
+ const core = new ExternalStoreThreadListRuntimeCore(
252
+ makeAdapter({ threadId }),
253
+ makeFactory(),
254
+ );
255
+
256
+ expect(core.getItemById(threadId)).toEqual(
257
+ expect.objectContaining({ id: threadId, status: "regular" }),
258
+ );
259
+ expect(Object.hasOwn(core.threadItems, threadId)).toBe(true);
260
+ expect(Object.getPrototypeOf(core.threadItems)).toBeNull();
261
+ },
262
+ );
263
+
264
+ it.each(["regular", "archived"] as const)(
265
+ "preserves a prototype-named %s adapter entry",
266
+ (status) => {
267
+ const adapter: ExternalStoreThreadListAdapter =
268
+ status === "regular"
269
+ ? { threads: [{ id: "__proto__", status }] }
270
+ : { archivedThreads: [{ id: "__proto__", status }] };
271
+ const core = new ExternalStoreThreadListRuntimeCore(
272
+ makeAdapter(adapter),
273
+ makeFactory(),
274
+ );
275
+
276
+ expect(core.getItemById("__proto__")).toEqual(
277
+ expect.objectContaining({ id: "__proto__", status }),
278
+ );
279
+ expect(Object.hasOwn(core.threadItems, "__proto__")).toBe(true);
280
+ expect(Object.getPrototypeOf(core.threadItems)).toBeNull();
281
+ },
282
+ );
283
+
201
284
  it("does not retain stale synthesized entries across mainThreadId switches", () => {
202
285
  const core = new ExternalStoreThreadListRuntimeCore(
203
286
  makeAdapter({ threadId: "thread-alpha" }),
@@ -160,26 +160,35 @@ describe("ExternalStoreThreadRuntimeCore adapter contract", () => {
160
160
  expect(onNew).not.toHaveBeenCalled();
161
161
  });
162
162
 
163
- it("throws when adapter has no onEdit and parentId differs from head", async () => {
164
- const messages = [createUserMessage("u1"), createAssistantMessage("a1")];
165
- const adapter = createBaseAdapter({ messages });
166
- const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
163
+ it.each(["u1", null])(
164
+ "rejects parent %s without onEdit",
165
+ async (parentId) => {
166
+ const messages = [
167
+ createUserMessage("u1"),
168
+ createAssistantMessage("a1"),
169
+ ];
170
+ const adapter = createBaseAdapter({ messages });
171
+ const core = new ExternalStoreThreadRuntimeCore(
172
+ contextProvider,
173
+ adapter,
174
+ );
167
175
 
168
- const appendMessage: AppendMessage = {
169
- role: "user",
170
- content: [{ type: "text", text: "Edit" }],
171
- attachments: [],
172
- createdAt: new Date(),
173
- parentId: "u1",
174
- sourceId: null,
175
- runConfig: undefined,
176
- metadata: { custom: {} },
177
- } as AppendMessage;
176
+ const appendMessage: AppendMessage = {
177
+ role: "user",
178
+ content: [{ type: "text", text: "Edit" }],
179
+ attachments: [],
180
+ createdAt: new Date(),
181
+ parentId,
182
+ sourceId: null,
183
+ runConfig: undefined,
184
+ metadata: { custom: {} },
185
+ } as AppendMessage;
178
186
 
179
- await expect(core.append(appendMessage)).rejects.toThrow(
180
- "Runtime does not support editing messages.",
181
- );
182
- });
187
+ await expect(core.append(appendMessage)).rejects.toThrow(
188
+ "Runtime does not support editing messages.",
189
+ );
190
+ },
191
+ );
183
192
  });
184
193
 
185
194
  describe("startRun", () => {
@@ -4,6 +4,7 @@ import { act, render, waitFor } from "@testing-library/react";
4
4
  import type { FC } from "react";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { AuiProvider, useAui } from "@assistant-ui/store";
7
+ import { CompositeAttachmentAdapter } from "../adapters/attachment";
7
8
  import type {
8
9
  ExternalThreadMessage,
9
10
  ExternalThreadProps,
@@ -404,6 +405,53 @@ describe("ExternalThread attachments", () => {
404
405
  },
405
406
  );
406
407
 
408
+ it("preserves pending attachments when adapter removal fails", async () => {
409
+ const error = new Error("remove failed");
410
+ const remove = vi.fn(async () => {
411
+ throw error;
412
+ });
413
+ const file = new File(["data"], "notes.txt", { type: "text/plain" });
414
+ const adapter = {
415
+ accept: "*",
416
+ add: async () => ({
417
+ id: "att-1",
418
+ type: "file" as const,
419
+ name: file.name,
420
+ contentType: file.type,
421
+ file,
422
+ status: {
423
+ type: "requires-action" as const,
424
+ reason: "composer-send" as const,
425
+ },
426
+ }),
427
+ send: async () => ({}) as never,
428
+ remove,
429
+ };
430
+ const aui = renderThreadWithProps({ attachmentAdapter: adapter });
431
+ const composer = () => aui().thread.composer();
432
+
433
+ await act(() => composer().addAttachment(file));
434
+ await waitFor(() =>
435
+ expect(composer().getState().attachments).toHaveLength(1),
436
+ );
437
+
438
+ await expect(
439
+ act(() => composer().attachment({ id: "att-1" }).remove()),
440
+ ).rejects.toBe(error);
441
+
442
+ expect(remove).toHaveBeenCalledWith(
443
+ expect.objectContaining({ id: "att-1" }),
444
+ );
445
+ expect(composer().getState().attachments).toHaveLength(1);
446
+ await waitFor(() =>
447
+ expect(composer().getState().attachments[0]?.status).toEqual({
448
+ type: "incomplete",
449
+ reason: "error",
450
+ message: "remove failed",
451
+ }),
452
+ );
453
+ });
454
+
407
455
  it.each([
408
456
  [
409
457
  "clearAttachments",
@@ -610,6 +658,97 @@ describe("ExternalThread attachments", () => {
610
658
  });
611
659
  });
612
660
 
661
+ it("removes complete edit attachments without adapter cleanup", async () => {
662
+ const remove = vi.fn(async () => {});
663
+ const completeAttachment = {
664
+ id: "image-1",
665
+ type: "image",
666
+ name: "image",
667
+ content: [{ type: "image", image: "data:image/png;base64,data" }],
668
+ status: { type: "complete" },
669
+ } as unknown as CompleteAttachment;
670
+ const aui = renderThreadWithProps({
671
+ messages: [
672
+ {
673
+ id: "u1",
674
+ role: "user",
675
+ content: [{ type: "text", text: "hi" }],
676
+ createdAt: new Date(0),
677
+ attachments: [completeAttachment],
678
+ metadata: { custom: {} },
679
+ } as unknown as ExternalThreadMessage,
680
+ ],
681
+ onEdit: () => {},
682
+ attachmentAdapter: new CompositeAttachmentAdapter([
683
+ {
684
+ accept: "image/*",
685
+ add: async () => {
686
+ throw new Error("not used");
687
+ },
688
+ send: async () => {
689
+ throw new Error("not used");
690
+ },
691
+ remove,
692
+ },
693
+ ]),
694
+ });
695
+ const composer = () => aui().thread.message({ id: "u1" }).composer();
696
+
697
+ await act(async () => composer().beginEdit());
698
+ await waitFor(() =>
699
+ expect(composer().getState().attachments).toHaveLength(1),
700
+ );
701
+ await act(() => composer().attachment({ id: "image-1" }).remove());
702
+
703
+ expect(remove).not.toHaveBeenCalled();
704
+ expect(composer().getState().attachments).toEqual([]);
705
+ });
706
+
707
+ it("calls adapter.remove for pending edit attachments", async () => {
708
+ const remove = vi.fn(async () => {});
709
+ const file = new File(["data"], "notes.txt", { type: "text/plain" });
710
+ const aui = renderThreadWithProps({
711
+ messages: [
712
+ {
713
+ id: "u1",
714
+ role: "user",
715
+ content: [{ type: "text", text: "hi" }],
716
+ createdAt: new Date(0),
717
+ attachments: [],
718
+ metadata: { custom: {} },
719
+ } as unknown as ExternalThreadMessage,
720
+ ],
721
+ onEdit: () => {},
722
+ attachmentAdapter: {
723
+ accept: "*",
724
+ add: async () => ({
725
+ id: "pending-1",
726
+ type: "document" as const,
727
+ name: file.name,
728
+ contentType: file.type,
729
+ file,
730
+ status: {
731
+ type: "requires-action" as const,
732
+ reason: "composer-send" as const,
733
+ },
734
+ }),
735
+ send: async () => ({}) as never,
736
+ remove,
737
+ },
738
+ });
739
+ const composer = () => aui().thread.message({ id: "u1" }).composer();
740
+
741
+ await act(async () => composer().beginEdit());
742
+ await act(() => composer().addAttachment(file));
743
+ await act(() => composer().attachment({ id: "pending-1" }).remove());
744
+
745
+ expect(remove).toHaveBeenCalledTimes(1);
746
+ expect(remove).toHaveBeenCalledWith(
747
+ expect.objectContaining({ id: "pending-1" }),
748
+ );
749
+ expect(composer().getState().attachments).toEqual([]);
750
+ });
751
+
613
752
  it("merges the failed send into a draft the user already modified", async () => {
614
753
  vi.spyOn(console, "error").mockImplementation(() => {});
615
754
  let rejectSend!: (reason: unknown) => void;
@@ -229,6 +229,29 @@ describe("ExternalThread unset optional callbacks", () => {
229
229
  });
230
230
  });
231
231
 
232
+ describe("ExternalThread append parent selection", () => {
233
+ it("passes an explicit null parent to the host", () => {
234
+ const onNew = vi.fn();
235
+ const { aui } = renderThread({
236
+ messages: [assistantMessage({ type: "complete", reason: "stop" })],
237
+ onNew,
238
+ });
239
+
240
+ aui().thread.append({
241
+ parentId: null,
242
+ content: [{ type: "text", text: "new root" }],
243
+ startRun: false,
244
+ });
245
+
246
+ expect(onNew).toHaveBeenCalledExactlyOnceWith(
247
+ expect.objectContaining({
248
+ parentId: null,
249
+ content: [{ type: "text", text: "new root" }],
250
+ }),
251
+ );
252
+ });
253
+ });
254
+
232
255
  describe("ExternalThread composer", () => {
233
256
  it("dispatches a synchronous setText + send sequence", async () => {
234
257
  const onNew = vi.fn();
@@ -26,6 +26,7 @@ type ListResult = {
26
26
 
27
27
  const INITIAL_STATE: RemoteThreadState = {
28
28
  isLoading: true,
29
+ loadError: undefined,
29
30
  isLoadingMore: false,
30
31
  cursor: undefined,
31
32
  newThreadId: undefined,
@@ -9,6 +9,7 @@ const createMockCore = (
9
9
  loadPromise: Promise<void> = Promise.resolve(),
10
10
  ): ThreadListRuntimeCore => ({
11
11
  isLoading: false,
12
+ loadError: undefined,
12
13
  mainThreadId: MAIN_THREAD_ID,
13
14
  newThreadId: undefined,
14
15
  threadIds: [MAIN_THREAD_ID],
@@ -352,6 +352,84 @@ const switchTo = async (runtime: AssistantRuntime, remoteId: string) => {
352
352
  });
353
353
  };
354
354
 
355
+ describe("ThreadListRuntime.getById", () => {
356
+ it("keeps background messages and metadata bound to the requested thread", async () => {
357
+ const harness = renderRemoteList(listAdapter(), async () => ({
358
+ content: [],
359
+ }));
360
+ const runtime = harness.getRuntime();
361
+ await waitFor(() => {
362
+ expect(runtime.threads.mainItem.getState().remoteId).toBe("thread-a");
363
+ });
364
+ await act(async () => {
365
+ runtime.thread.reset([
366
+ {
367
+ id: "message-a",
368
+ role: "user",
369
+ content: [{ type: "text", text: "A" }],
370
+ },
371
+ ]);
372
+ });
373
+ await switchTo(runtime, "thread-b");
374
+ await act(async () => {
375
+ runtime.thread.reset([
376
+ {
377
+ id: "message-b",
378
+ role: "user",
379
+ content: [{ type: "text", text: "B" }],
380
+ },
381
+ ]);
382
+ });
383
+ const threadBId = runtime.threads.mainItem.getState().id;
384
+ await switchTo(runtime, "thread-a");
385
+
386
+ const threadB = runtime.threads.getById(threadBId);
387
+ expect(threadB.getState()).toMatchObject({
388
+ threadId: threadBId,
389
+ messages: [{ id: "message-b" }],
390
+ metadata: {
391
+ id: threadBId,
392
+ remoteId: "thread-b",
393
+ title: "B",
394
+ isMain: false,
395
+ },
396
+ });
397
+ expect(runtime.thread.getState()).toMatchObject({
398
+ messages: [{ id: "message-a" }],
399
+ metadata: { remoteId: "thread-a", isMain: true },
400
+ });
401
+
402
+ let observed = threadB.getState();
403
+ const unsubscribe = threadB.subscribe(() => {
404
+ observed = threadB.getState();
405
+ });
406
+ try {
407
+ await act(async () => {
408
+ await runtime.threads.getItemById(threadBId).rename("Renamed B");
409
+ });
410
+ expect(observed).toMatchObject({
411
+ threadId: threadBId,
412
+ messages: [{ id: "message-b" }],
413
+ metadata: { title: "Renamed B", isMain: false },
414
+ });
415
+
416
+ await switchTo(runtime, "thread-b");
417
+ expect(observed.metadata).toMatchObject({
418
+ title: "Renamed B",
419
+ isMain: true,
420
+ });
421
+ await switchTo(runtime, "thread-a");
422
+ expect(observed).toMatchObject({
423
+ threadId: threadBId,
424
+ messages: [{ id: "message-b" }],
425
+ metadata: { title: "Renamed B", isMain: false },
426
+ });
427
+ } finally {
428
+ unsubscribe();
429
+ }
430
+ });
431
+ });
432
+
355
433
  describe("background thread run events", () => {
356
434
  it("delivers runEnd globally after the running thread is switched away", async () => {
357
435
  const runA = deferred<{ content: [] }>();