@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
@@ -0,0 +1,602 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ clearThreadTitleState,
4
+ finishThreadTitleRename,
5
+ runThreadTitleGeneration,
6
+ startThreadTitleRename,
7
+ type ThreadTitleState,
8
+ } from "./title-generation";
9
+
10
+ const deferred = <T>() => {
11
+ let resolve!: (value: T) => void;
12
+ const promise = new Promise<T>((r) => {
13
+ resolve = r;
14
+ });
15
+ return { promise, resolve };
16
+ };
17
+
18
+ const noop = async () => {};
19
+
20
+ const flushMicrotasks = async () => {
21
+ for (let i = 0; i < 10; i += 1) await Promise.resolve();
22
+ };
23
+
24
+ describe("runThreadTitleGeneration", () => {
25
+ it("reasserts a rename that lands while the generated stream is open", async () => {
26
+ const states = new Map<string, ThreadTitleState>();
27
+ const applied: (string | undefined)[] = [];
28
+ const rename = vi.fn(noop);
29
+ const streamOpen = deferred<void>();
30
+
31
+ const generation = runThreadTitleGeneration({
32
+ states,
33
+ threadId: "t1",
34
+ automatic: true,
35
+ generate: async (onTitle) => {
36
+ await streamOpen.promise;
37
+ await onTitle("Generated");
38
+ },
39
+ rename,
40
+ applyTitle: async (title) => {
41
+ applied.push(title);
42
+ },
43
+ });
44
+
45
+ const claim = startThreadTitleRename(states, "t1", "Manual");
46
+ finishThreadTitleRename(states, "t1", claim, true);
47
+ streamOpen.resolve();
48
+ await generation;
49
+
50
+ expect(applied).toEqual(["Manual"]);
51
+ expect(rename).toHaveBeenCalledTimes(1);
52
+ expect(rename).toHaveBeenCalledWith("Manual");
53
+ });
54
+
55
+ it("applies the generated title when the rename fails", async () => {
56
+ const states = new Map<string, ThreadTitleState>();
57
+ const applied: (string | undefined)[] = [];
58
+ const rename = vi.fn(noop);
59
+ const streamOpen = deferred<void>();
60
+
61
+ const generation = runThreadTitleGeneration({
62
+ states,
63
+ threadId: "t1",
64
+ automatic: true,
65
+ generate: async (onTitle) => {
66
+ await streamOpen.promise;
67
+ await onTitle("Generated");
68
+ },
69
+ rename,
70
+ applyTitle: async (title) => {
71
+ applied.push(title);
72
+ },
73
+ });
74
+
75
+ const claim = startThreadTitleRename(states, "t1", "Manual");
76
+ finishThreadTitleRename(states, "t1", claim, false);
77
+ streamOpen.resolve();
78
+ await generation;
79
+
80
+ expect(applied).toEqual(["Generated"]);
81
+ expect(rename).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it("skips the next automatic generation after a completed rename, once", async () => {
85
+ const states = new Map<string, ThreadTitleState>();
86
+ const generate = vi.fn(async (onTitle: (t: string) => Promise<void>) => {
87
+ await onTitle("Generated");
88
+ });
89
+ const run = () =>
90
+ runThreadTitleGeneration({
91
+ states,
92
+ threadId: "t1",
93
+ automatic: true,
94
+ generate,
95
+ rename: noop,
96
+ applyTitle: noop,
97
+ });
98
+
99
+ const claim = startThreadTitleRename(states, "t1", "Manual");
100
+ finishThreadTitleRename(states, "t1", claim, true);
101
+
102
+ await run();
103
+ expect(generate).not.toHaveBeenCalled();
104
+
105
+ await run();
106
+ expect(generate).toHaveBeenCalledTimes(1);
107
+ });
108
+
109
+ it("reasserts only after generate resolves, never mid-stream", async () => {
110
+ const states = new Map<string, ThreadTitleState>();
111
+ const order: string[] = [];
112
+ const streamOpen = deferred<void>();
113
+
114
+ const generation = runThreadTitleGeneration({
115
+ states,
116
+ threadId: "t1",
117
+ automatic: true,
118
+ generate: async (onTitle) => {
119
+ await streamOpen.promise;
120
+ await onTitle("Generated");
121
+ order.push("generate-resolved");
122
+ },
123
+ rename: async () => {
124
+ order.push("rename");
125
+ },
126
+ applyTitle: async () => {
127
+ order.push("applyTitle");
128
+ },
129
+ });
130
+
131
+ const claim = startThreadTitleRename(states, "t1", "Manual");
132
+ finishThreadTitleRename(states, "t1", claim, true);
133
+ streamOpen.resolve();
134
+ await generation;
135
+
136
+ expect(order).toEqual(["generate-resolved", "rename", "applyTitle"]);
137
+ });
138
+
139
+ it("reasserts the newer claim when a rename starts during the reassert", async () => {
140
+ const states = new Map<string, ThreadTitleState>();
141
+ const applied: (string | undefined)[] = [];
142
+ const renamed: string[] = [];
143
+ const streamOpen = deferred<void>();
144
+
145
+ const generation = runThreadTitleGeneration({
146
+ states,
147
+ threadId: "t1",
148
+ automatic: true,
149
+ generate: async (onTitle) => {
150
+ await streamOpen.promise;
151
+ await onTitle("Generated");
152
+ },
153
+ rename: async (title) => {
154
+ renamed.push(title);
155
+ if (renamed.length === 1) {
156
+ const second = startThreadTitleRename(states, "t1", "Second");
157
+ finishThreadTitleRename(states, "t1", second, true);
158
+ }
159
+ },
160
+ applyTitle: async (title) => {
161
+ applied.push(title);
162
+ },
163
+ });
164
+
165
+ const first = startThreadTitleRename(states, "t1", "First");
166
+ finishThreadTitleRename(states, "t1", first, true);
167
+ streamOpen.resolve();
168
+ await generation;
169
+
170
+ expect(renamed).toEqual(["First", "Second"]);
171
+ expect(applied).toEqual(["Second"]);
172
+ });
173
+
174
+ it("keeps an earlier manual title when a newer rename fails", async () => {
175
+ const states = new Map<string, ThreadTitleState>();
176
+ const applied: (string | undefined)[] = [];
177
+ const generate = vi.fn(async (onTitle: (t: string) => Promise<void>) => {
178
+ await onTitle("Generated");
179
+ });
180
+
181
+ const first = startThreadTitleRename(states, "t1", "First manual title");
182
+ finishThreadTitleRename(states, "t1", first, true);
183
+ const second = startThreadTitleRename(states, "t1", "Second manual title");
184
+
185
+ const generation = runThreadTitleGeneration({
186
+ states,
187
+ threadId: "t1",
188
+ automatic: true,
189
+ generate,
190
+ rename: noop,
191
+ applyTitle: async (title) => {
192
+ applied.push(title);
193
+ },
194
+ });
195
+
196
+ finishThreadTitleRename(states, "t1", second, false);
197
+ await generation;
198
+
199
+ expect(generate).not.toHaveBeenCalled();
200
+ expect(applied).toEqual([]);
201
+ });
202
+
203
+ it("drops per-thread state when the thread is deleted", async () => {
204
+ const states = new Map<string, ThreadTitleState>();
205
+ const claim = startThreadTitleRename(states, "t1", "Manual");
206
+ finishThreadTitleRename(states, "t1", claim, true);
207
+ expect(states.size).toBe(1);
208
+
209
+ clearThreadTitleState(states, "t1");
210
+ expect(states.size).toBe(0);
211
+ });
212
+
213
+ it("lets an explicit generation supersede an in-flight automatic one", async () => {
214
+ const states = new Map<string, ThreadTitleState>();
215
+ const applied: (string | undefined)[] = [];
216
+ const streamOpen = deferred<void>();
217
+ const applyTitle = async (title: string | undefined) => {
218
+ applied.push(title);
219
+ };
220
+
221
+ const automatic = runThreadTitleGeneration({
222
+ states,
223
+ threadId: "t1",
224
+ automatic: true,
225
+ generate: async (onTitle) => {
226
+ await streamOpen.promise;
227
+ await onTitle("Automatic");
228
+ },
229
+ rename: noop,
230
+ applyTitle,
231
+ });
232
+
233
+ await runThreadTitleGeneration({
234
+ states,
235
+ threadId: "t1",
236
+ automatic: false,
237
+ generate: async (onTitle) => {
238
+ await onTitle("Explicit");
239
+ },
240
+ rename: noop,
241
+ applyTitle,
242
+ });
243
+
244
+ streamOpen.resolve();
245
+ await automatic;
246
+
247
+ expect(applied).toEqual(["Explicit"]);
248
+ });
249
+
250
+ it("holds every explicit generation until a pending rename settles", async () => {
251
+ const states = new Map<string, ThreadTitleState>();
252
+ const renameOpen = deferred<void>();
253
+ const generated: string[] = [];
254
+ let server: string | undefined;
255
+
256
+ const claim = startThreadTitleRename(states, "t1", "Manual");
257
+ const rename = (async () => {
258
+ await renameOpen.promise;
259
+ server = claim.title;
260
+ finishThreadTitleRename(states, "t1", claim, true);
261
+ })();
262
+ const runOf = (title: string) =>
263
+ runThreadTitleGeneration({
264
+ states,
265
+ threadId: "t1",
266
+ automatic: false,
267
+ generate: async (onTitle) => {
268
+ generated.push(title);
269
+ server = title;
270
+ await onTitle(title);
271
+ },
272
+ rename: async (next) => {
273
+ server = next;
274
+ },
275
+ applyTitle: noop,
276
+ });
277
+
278
+ const first = runOf("First");
279
+ const second = runOf("Second");
280
+ await flushMicrotasks();
281
+
282
+ expect(generated).toEqual([]);
283
+
284
+ renameOpen.resolve();
285
+ await Promise.all([rename, first, second]);
286
+
287
+ expect(generated).toEqual(["Second"]);
288
+ expect(server).toBe("Second");
289
+ });
290
+
291
+ it("waits for every overlapping rename before explicit generation", async () => {
292
+ const states = new Map<string, ThreadTitleState>();
293
+ const firstOpen = deferred<void>();
294
+ const secondOpen = deferred<void>();
295
+ const generated: string[] = [];
296
+ let server: string | undefined;
297
+
298
+ const firstClaim = startThreadTitleRename(states, "t1", "First");
299
+ const firstRename = (async () => {
300
+ await firstOpen.promise;
301
+ server = firstClaim.title;
302
+ finishThreadTitleRename(states, "t1", firstClaim, true);
303
+ })();
304
+ const secondClaim = startThreadTitleRename(states, "t1", "Second");
305
+ const secondRename = (async () => {
306
+ await secondOpen.promise;
307
+ server = secondClaim.title;
308
+ finishThreadTitleRename(states, "t1", secondClaim, true);
309
+ })();
310
+ const generation = runThreadTitleGeneration({
311
+ states,
312
+ threadId: "t1",
313
+ automatic: false,
314
+ generate: async (onTitle) => {
315
+ generated.push("Explicit");
316
+ server = "Explicit";
317
+ await onTitle("Explicit");
318
+ },
319
+ rename: async (title) => {
320
+ server = title;
321
+ },
322
+ applyTitle: noop,
323
+ });
324
+
325
+ secondOpen.resolve();
326
+ await secondRename;
327
+ await flushMicrotasks();
328
+ expect(generated).toEqual([]);
329
+
330
+ firstOpen.resolve();
331
+ await Promise.all([firstRename, generation]);
332
+
333
+ expect(generated).toEqual(["Explicit"]);
334
+ expect(server).toBe("Explicit");
335
+ });
336
+
337
+ it("reasserts the explicit title when the superseded run persists last", async () => {
338
+ const states = new Map<string, ThreadTitleState>();
339
+ const streamOpen = deferred<void>();
340
+ let server: string | undefined;
341
+ const runOf = (automatic: boolean, title: string, open?: Promise<void>) =>
342
+ runThreadTitleGeneration({
343
+ states,
344
+ threadId: "t1",
345
+ automatic,
346
+ generate: async (onTitle) => {
347
+ if (open) await open;
348
+ await onTitle(title);
349
+ server = title;
350
+ },
351
+ rename: async (next) => {
352
+ server = next;
353
+ },
354
+ applyTitle: noop,
355
+ });
356
+
357
+ const automatic = runOf(true, "Automatic", streamOpen.promise);
358
+ await runOf(false, "Explicit");
359
+ expect(server).toBe("Explicit");
360
+
361
+ streamOpen.resolve();
362
+ await automatic;
363
+
364
+ expect(server).toBe("Explicit");
365
+ });
366
+
367
+ it("leaves the explicit title alone when the superseded run persists first", async () => {
368
+ const states = new Map<string, ThreadTitleState>();
369
+ const automaticStream = deferred<void>();
370
+ const explicitStream = deferred<void>();
371
+ let server: string | undefined;
372
+ const runOf = (automatic: boolean, title: string, open: Promise<void>) =>
373
+ runThreadTitleGeneration({
374
+ states,
375
+ threadId: "t1",
376
+ automatic,
377
+ generate: async (onTitle) => {
378
+ await open;
379
+ await onTitle(title);
380
+ server = title;
381
+ },
382
+ rename: async (next) => {
383
+ server = next;
384
+ },
385
+ applyTitle: noop,
386
+ });
387
+
388
+ const automatic = runOf(true, "Automatic", automaticStream.promise);
389
+ const explicit = runOf(false, "Explicit", explicitStream.promise);
390
+
391
+ automaticStream.resolve();
392
+ explicitStream.resolve();
393
+ await Promise.all([automatic, explicit]);
394
+
395
+ expect(server).toBe("Explicit");
396
+ });
397
+
398
+ it("keeps a rename that lands after the superseding explicit generation", async () => {
399
+ const states = new Map<string, ThreadTitleState>();
400
+ const streamOpen = deferred<void>();
401
+ let server: string | undefined;
402
+ const runOf = (automatic: boolean, title: string, open?: Promise<void>) =>
403
+ runThreadTitleGeneration({
404
+ states,
405
+ threadId: "t1",
406
+ automatic,
407
+ generate: async (onTitle) => {
408
+ if (open) await open;
409
+ await onTitle(title);
410
+ server = title;
411
+ },
412
+ rename: async (next) => {
413
+ server = next;
414
+ },
415
+ applyTitle: noop,
416
+ });
417
+
418
+ const automatic = runOf(true, "Automatic", streamOpen.promise);
419
+ await runOf(false, "Explicit");
420
+
421
+ const claim = startThreadTitleRename(states, "t1", "Manual");
422
+ server = "Manual";
423
+ finishThreadTitleRename(states, "t1", claim, true);
424
+
425
+ streamOpen.resolve();
426
+ await automatic;
427
+
428
+ expect(server).toBe("Manual");
429
+ });
430
+
431
+ it("keeps a rename that lands while the superseded run waits to reassert", async () => {
432
+ const states = new Map<string, ThreadTitleState>();
433
+ const automaticStream = deferred<void>();
434
+ const explicitStream = deferred<void>();
435
+ let server: string | undefined;
436
+ const runOf = (automatic: boolean, title: string, open: Promise<void>) =>
437
+ runThreadTitleGeneration({
438
+ states,
439
+ threadId: "t1",
440
+ automatic,
441
+ generate: async (onTitle) => {
442
+ await open;
443
+ await onTitle(title);
444
+ server = title;
445
+ },
446
+ rename: async (next) => {
447
+ server = next;
448
+ },
449
+ applyTitle: noop,
450
+ });
451
+
452
+ const automatic = runOf(true, "Automatic", automaticStream.promise);
453
+ const explicit = runOf(false, "Explicit", explicitStream.promise);
454
+
455
+ automaticStream.resolve();
456
+ await flushMicrotasks();
457
+
458
+ const claim = startThreadTitleRename(states, "t1", "Manual");
459
+ server = "Manual";
460
+ finishThreadTitleRename(states, "t1", claim, true);
461
+
462
+ explicitStream.resolve();
463
+ await Promise.all([automatic, explicit]);
464
+
465
+ expect(server).toBe("Manual");
466
+ });
467
+
468
+ it("keeps a rename that lands while the superseded run reasserts", async () => {
469
+ const states = new Map<string, ThreadTitleState>();
470
+ const streamOpen = deferred<void>();
471
+ const renameOpen = deferred<void>();
472
+ let server: string | undefined;
473
+ let blocked = false;
474
+ const rename = async (next: string) => {
475
+ if (!blocked) {
476
+ blocked = true;
477
+ await renameOpen.promise;
478
+ }
479
+ server = next;
480
+ };
481
+
482
+ const automatic = runThreadTitleGeneration({
483
+ states,
484
+ threadId: "t1",
485
+ automatic: true,
486
+ generate: async (onTitle) => {
487
+ await streamOpen.promise;
488
+ await onTitle("Automatic");
489
+ server = "Automatic";
490
+ },
491
+ rename,
492
+ applyTitle: noop,
493
+ });
494
+
495
+ await runThreadTitleGeneration({
496
+ states,
497
+ threadId: "t1",
498
+ automatic: false,
499
+ generate: async (onTitle) => {
500
+ await onTitle("Explicit");
501
+ server = "Explicit";
502
+ },
503
+ rename,
504
+ applyTitle: noop,
505
+ });
506
+
507
+ streamOpen.resolve();
508
+ await flushMicrotasks();
509
+
510
+ const claim = startThreadTitleRename(states, "t1", "Manual");
511
+ server = "Manual";
512
+ finishThreadTitleRename(states, "t1", claim, true);
513
+
514
+ renameOpen.resolve();
515
+ await automatic;
516
+
517
+ expect(server).toBe("Manual");
518
+ });
519
+
520
+ it("skips a claim a newer explicit generation has outranked", async () => {
521
+ const states = new Map<string, ThreadTitleState>();
522
+ const streamOpen = deferred<void>();
523
+ const renamed: string[] = [];
524
+ const runOf = (automatic: boolean, title: string, open?: Promise<void>) =>
525
+ runThreadTitleGeneration({
526
+ states,
527
+ threadId: "t1",
528
+ automatic,
529
+ generate: async (onTitle) => {
530
+ if (open) await open;
531
+ await onTitle(title);
532
+ },
533
+ rename: async (next) => {
534
+ renamed.push(next);
535
+ },
536
+ applyTitle: noop,
537
+ });
538
+
539
+ const automatic = runOf(true, "Automatic", streamOpen.promise);
540
+ await runOf(false, "Explicit");
541
+
542
+ const claim = startThreadTitleRename(states, "t1", "Manual");
543
+ finishThreadTitleRename(states, "t1", claim, true);
544
+
545
+ await runOf(false, "Newer");
546
+
547
+ streamOpen.resolve();
548
+ await automatic;
549
+
550
+ expect(renamed).toEqual(["Newer"]);
551
+ });
552
+
553
+ it("does not reassert when the superseded run persisted nothing", async () => {
554
+ const states = new Map<string, ThreadTitleState>();
555
+ const streamOpen = deferred<void>();
556
+ const rename = vi.fn(noop);
557
+
558
+ const automatic = runThreadTitleGeneration({
559
+ states,
560
+ threadId: "t1",
561
+ automatic: true,
562
+ generate: async (onTitle) => {
563
+ await streamOpen.promise;
564
+ await onTitle(undefined);
565
+ },
566
+ rename,
567
+ applyTitle: noop,
568
+ });
569
+
570
+ await runThreadTitleGeneration({
571
+ states,
572
+ threadId: "t1",
573
+ automatic: false,
574
+ generate: async (onTitle) => {
575
+ await onTitle("Explicit");
576
+ },
577
+ rename,
578
+ applyTitle: noop,
579
+ });
580
+
581
+ streamOpen.resolve();
582
+ await automatic;
583
+
584
+ expect(rename).not.toHaveBeenCalled();
585
+ });
586
+
587
+ it("drops per-thread state once nothing is in flight", async () => {
588
+ const states = new Map<string, ThreadTitleState>();
589
+ await runThreadTitleGeneration({
590
+ states,
591
+ threadId: "t1",
592
+ automatic: true,
593
+ generate: async (onTitle) => {
594
+ await onTitle("Generated");
595
+ },
596
+ rename: noop,
597
+ applyTitle: noop,
598
+ });
599
+
600
+ expect(states.size).toBe(0);
601
+ });
602
+ });