@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,329 @@
1
+ export type ThreadTitleClaim = {
2
+ readonly title: string;
3
+ readonly order: number;
4
+ readonly settled: Promise<boolean>;
5
+ readonly settle: (renamed: boolean) => void;
6
+ };
7
+
8
+ type ThreadTitleGeneration = {
9
+ readonly automatic: boolean;
10
+ readonly order: number;
11
+ claim: ThreadTitleClaim | null;
12
+ beforeGenerationClaims: readonly ThreadTitleClaim[];
13
+ superseded: boolean;
14
+ readonly persisted: Promise<string | undefined>;
15
+ readonly settlePersisted: (title: string | undefined) => void;
16
+ };
17
+
18
+ export type ThreadTitleState = {
19
+ generations: Set<ThreadTitleGeneration>;
20
+ pendingClaim: ThreadTitleClaim | null;
21
+ inFlightClaims: Set<ThreadTitleClaim>;
22
+ manualTitle: string | undefined;
23
+ latestExplicit: ThreadTitleGeneration | null;
24
+ nextOrder: number;
25
+ };
26
+
27
+ export type ThreadTitleGenerationRun = {
28
+ states: Map<string, ThreadTitleState>;
29
+ threadId: string;
30
+ automatic: boolean;
31
+ generate: (
32
+ onTitle: (title: string | undefined) => Promise<void>,
33
+ ) => Promise<void>;
34
+ rename: (title: string) => Promise<void>;
35
+ applyTitle: (title: string | undefined) => Promise<void>;
36
+ };
37
+
38
+ function getThreadTitleState(
39
+ states: Map<string, ThreadTitleState>,
40
+ threadId: string,
41
+ ): ThreadTitleState {
42
+ let state = states.get(threadId);
43
+ if (state === undefined) {
44
+ state = {
45
+ generations: new Set(),
46
+ pendingClaim: null,
47
+ inFlightClaims: new Set(),
48
+ manualTitle: undefined,
49
+ latestExplicit: null,
50
+ nextOrder: 0,
51
+ };
52
+ states.set(threadId, state);
53
+ }
54
+ return state;
55
+ }
56
+
57
+ function pruneThreadTitleState(
58
+ states: Map<string, ThreadTitleState>,
59
+ threadId: string,
60
+ state: ThreadTitleState,
61
+ ): void {
62
+ if (
63
+ state.generations.size === 0 &&
64
+ state.pendingClaim === null &&
65
+ state.inFlightClaims.size === 0 &&
66
+ state.manualTitle === undefined &&
67
+ states.get(threadId) === state
68
+ ) {
69
+ states.delete(threadId);
70
+ }
71
+ }
72
+
73
+ function isCurrentGeneration(
74
+ state: ThreadTitleState,
75
+ generation: ThreadTitleGeneration,
76
+ ): boolean {
77
+ return (
78
+ !generation.superseded &&
79
+ (state.latestExplicit?.order ?? 0) <= generation.order
80
+ );
81
+ }
82
+
83
+ function isCurrentClaim(
84
+ state: ThreadTitleState,
85
+ claim: ThreadTitleClaim,
86
+ ): boolean {
87
+ return (state.latestExplicit?.order ?? 0) < claim.order;
88
+ }
89
+
90
+ export function startThreadTitleRename(
91
+ states: Map<string, ThreadTitleState>,
92
+ threadId: string,
93
+ title: string,
94
+ ): ThreadTitleClaim {
95
+ const state = getThreadTitleState(states, threadId);
96
+ let settle!: (renamed: boolean) => void;
97
+ const settled = new Promise<boolean>((resolve) => {
98
+ settle = resolve;
99
+ });
100
+ const claim: ThreadTitleClaim = {
101
+ title,
102
+ order: ++state.nextOrder,
103
+ settled,
104
+ settle,
105
+ };
106
+ state.pendingClaim = claim;
107
+ state.inFlightClaims.add(claim);
108
+ for (const generation of state.generations) {
109
+ if (generation.order < claim.order) generation.claim = claim;
110
+ }
111
+ return claim;
112
+ }
113
+
114
+ export function finishThreadTitleRename(
115
+ states: Map<string, ThreadTitleState>,
116
+ threadId: string,
117
+ claim: ThreadTitleClaim,
118
+ renamed: boolean,
119
+ ): void {
120
+ claim.settle(renamed);
121
+ const state = states.get(threadId);
122
+ if (state === undefined) return;
123
+ state.inFlightClaims.delete(claim);
124
+ if (state.pendingClaim === claim) {
125
+ state.pendingClaim = null;
126
+ if (renamed) {
127
+ state.manualTitle = claim.title;
128
+ }
129
+ }
130
+ pruneThreadTitleState(states, threadId, state);
131
+ }
132
+
133
+ function takeManualTitle(
134
+ states: Map<string, ThreadTitleState>,
135
+ threadId: string,
136
+ state: ThreadTitleState,
137
+ ): string | undefined {
138
+ const retained = state.manualTitle;
139
+ if (retained === undefined) return undefined;
140
+ state.manualTitle = undefined;
141
+ pruneThreadTitleState(states, threadId, state);
142
+ return retained;
143
+ }
144
+
145
+ export function clearThreadTitleState(
146
+ states: Map<string, ThreadTitleState>,
147
+ threadId: string,
148
+ ): void {
149
+ states.delete(threadId);
150
+ }
151
+
152
+ function startThreadTitleGeneration(
153
+ states: Map<string, ThreadTitleState>,
154
+ threadId: string,
155
+ automatic: boolean,
156
+ ): ThreadTitleGeneration | null {
157
+ const state = getThreadTitleState(states, threadId);
158
+ if (
159
+ automatic &&
160
+ state.pendingClaim === null &&
161
+ takeManualTitle(states, threadId, state) !== undefined
162
+ ) {
163
+ return null;
164
+ }
165
+
166
+ let settlePersisted!: (title: string | undefined) => void;
167
+ const persisted = new Promise<string | undefined>((resolve) => {
168
+ settlePersisted = resolve;
169
+ });
170
+ const generation: ThreadTitleGeneration = {
171
+ automatic,
172
+ order: ++state.nextOrder,
173
+ claim: automatic ? state.pendingClaim : null,
174
+ beforeGenerationClaims: automatic ? [] : [...state.inFlightClaims],
175
+ superseded: false,
176
+ persisted,
177
+ settlePersisted,
178
+ };
179
+ if (!automatic) {
180
+ state.pendingClaim = null;
181
+ state.manualTitle = undefined;
182
+ state.latestExplicit = generation;
183
+ for (const active of state.generations) {
184
+ if (active.automatic) active.superseded = true;
185
+ }
186
+ }
187
+ state.generations.add(generation);
188
+ return generation;
189
+ }
190
+
191
+ /**
192
+ * Runs one title generation under the per-thread rename claims and generation
193
+ * ordering, so the title that wins locally is also the title left on the
194
+ * server.
195
+ *
196
+ * A generated run persists the title itself, and the adapter exposes no
197
+ * compare-and-set, so the run that loses reasserts the winner through `rename`
198
+ * only after `generate` has resolved. Reasserting mid-stream would race the
199
+ * run's own write and lose the winning title on the server.
200
+ */
201
+ export async function runThreadTitleGeneration({
202
+ states,
203
+ threadId,
204
+ automatic,
205
+ generate,
206
+ rename,
207
+ applyTitle,
208
+ }: ThreadTitleGenerationRun): Promise<void> {
209
+ const state = getThreadTitleState(states, threadId);
210
+ const generation = startThreadTitleGeneration(states, threadId, automatic);
211
+ if (generation === null) return;
212
+
213
+ let persistedTitle: string | undefined;
214
+ let persistedOrder = generation.order;
215
+
216
+ const settleClaim = async (claim: ThreadTitleClaim) => {
217
+ const renamed = await claim.settled;
218
+ if (generation.claim !== claim) return undefined;
219
+ if (!renamed) {
220
+ generation.claim = null;
221
+ if (state.pendingClaim === claim) state.pendingClaim = null;
222
+ }
223
+ return renamed;
224
+ };
225
+
226
+ const retainManualTitle = () =>
227
+ automatic && takeManualTitle(states, threadId, state) !== undefined;
228
+
229
+ // `generate` resolves only once the run has persisted its title, so a run
230
+ // that lost the ordering race while it streamed has already overwritten the
231
+ // winner. It waits for the winning generation to persist its own title and
232
+ // writes that title back.
233
+ const repairLostRace = async () => {
234
+ if (persistedTitle === undefined) return;
235
+ while (true) {
236
+ const claim = generation.claim;
237
+ if (
238
+ claim !== null &&
239
+ claim.order > persistedOrder &&
240
+ isCurrentClaim(state, claim)
241
+ ) {
242
+ const renamed = await settleClaim(claim);
243
+ if (renamed === true) {
244
+ persistedTitle = claim.title;
245
+ persistedOrder = claim.order;
246
+ await rename(claim.title);
247
+ }
248
+ continue;
249
+ }
250
+ const winner = state.latestExplicit;
251
+ if (winner === null || winner.order <= persistedOrder) return;
252
+ const title = await winner.persisted;
253
+ if (state.latestExplicit !== winner) continue;
254
+ persistedOrder = winner.order;
255
+ if (title === undefined || title === persistedTitle) return;
256
+ persistedTitle = title;
257
+ await rename(title);
258
+ }
259
+ };
260
+
261
+ const runGeneration = async () => {
262
+ // An explicit generation waits for every earlier rename because any of
263
+ // those server writes may settle last.
264
+ if (generation.beforeGenerationClaims.length > 0) {
265
+ await Promise.all(
266
+ generation.beforeGenerationClaims.map((claim) => claim.settled),
267
+ );
268
+ }
269
+ if (generation.claim !== null) {
270
+ const renamed = await settleClaim(generation.claim);
271
+ if (renamed === true) {
272
+ generation.superseded = true;
273
+ return;
274
+ }
275
+ if (renamed === false && retainManualTitle()) return;
276
+ }
277
+ if (!isCurrentGeneration(state, generation)) return;
278
+
279
+ let sawTitle = false;
280
+ let lastTitle: string | undefined;
281
+ await generate(async (title) => {
282
+ sawTitle = true;
283
+ lastTitle = title;
284
+ if (title !== undefined) persistedTitle = title;
285
+ const claim = generation.claim;
286
+ if (claim !== null) {
287
+ const renamed = await settleClaim(claim);
288
+ if (renamed !== false) return;
289
+ if (retainManualTitle()) {
290
+ generation.superseded = true;
291
+ return;
292
+ }
293
+ }
294
+ if (isCurrentGeneration(state, generation)) await applyTitle(lastTitle);
295
+ });
296
+
297
+ while (true) {
298
+ const claim = generation.claim;
299
+ if (claim === null) return;
300
+ const renamed = await settleClaim(claim);
301
+ if (renamed === undefined) continue;
302
+ if (renamed === false) {
303
+ if (retainManualTitle()) return;
304
+ if (sawTitle && isCurrentGeneration(state, generation)) {
305
+ await applyTitle(lastTitle);
306
+ }
307
+ return;
308
+ }
309
+ if (!isCurrentClaim(state, claim)) return;
310
+ await rename(claim.title);
311
+ persistedTitle = claim.title;
312
+ persistedOrder = claim.order;
313
+ if (generation.claim !== claim) continue;
314
+ if (!isCurrentGeneration(state, generation)) return;
315
+ await applyTitle(claim.title);
316
+ generation.superseded = true;
317
+ return;
318
+ }
319
+ };
320
+
321
+ try {
322
+ await runGeneration();
323
+ await repairLostRace();
324
+ } finally {
325
+ generation.settlePersisted(persistedTitle);
326
+ state.generations.delete(generation);
327
+ pruneThreadTitleState(states, threadId, state);
328
+ }
329
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyTitleStream } from "./title";
3
+
4
+ const createTextStream = (text: string) =>
5
+ new ReadableStream({
6
+ start(controller) {
7
+ controller.enqueue({
8
+ type: "part-start",
9
+ path: [0],
10
+ part: { type: "text" },
11
+ });
12
+ controller.enqueue({
13
+ type: "text-delta",
14
+ path: [0],
15
+ textDelta: text,
16
+ });
17
+ controller.enqueue({ type: "part-finish", path: [0] });
18
+ controller.close();
19
+ },
20
+ }) as never;
21
+
22
+ describe("applyTitleStream", () => {
23
+ it("ignores an empty stream", async () => {
24
+ const titles: (string | undefined)[] = [];
25
+
26
+ await applyTitleStream(
27
+ new ReadableStream({
28
+ start(controller) {
29
+ controller.close();
30
+ },
31
+ }) as never,
32
+ async (title) => {
33
+ titles.push(title);
34
+ },
35
+ );
36
+
37
+ expect(titles).toEqual([]);
38
+ });
39
+
40
+ it("ignores whitespace-only titles", async () => {
41
+ const titles: (string | undefined)[] = [];
42
+
43
+ await applyTitleStream(createTextStream(" "), async (title) => {
44
+ titles.push(title);
45
+ });
46
+
47
+ expect(titles).toEqual([]);
48
+ });
49
+
50
+ it("forwards titles containing text unchanged", async () => {
51
+ const titles: (string | undefined)[] = [];
52
+
53
+ await applyTitleStream(
54
+ createTextStream(" Generated title "),
55
+ async (title) => {
56
+ titles.push(title);
57
+ },
58
+ );
59
+
60
+ expect(titles).toContain(" Generated title ");
61
+ });
62
+ });
@@ -1,3 +1,16 @@
1
+ import { AssistantMessageStream } from "assistant-stream";
2
+
1
3
  export const isTitleSourceMessage = (message: {
2
4
  status?: { type: string } | undefined;
3
5
  }) => message.status?.type !== "running";
6
+
7
+ export const applyTitleStream = async (
8
+ stream: Parameters<typeof AssistantMessageStream.fromAssistantStream>[0],
9
+ onTitle: (title: string | undefined) => Promise<void>,
10
+ ) => {
11
+ const messageStream = AssistantMessageStream.fromAssistantStream(stream);
12
+ for await (const result of messageStream) {
13
+ const title = result.parts.filter((part) => part.type === "text")[0]?.text;
14
+ if (title?.trim()) await onTitle(title);
15
+ }
16
+ };
@@ -3,6 +3,7 @@ import type { AssistantRuntime } from "../../runtime/api/assistant-runtime";
3
3
  import type { AssistantStream } from "assistant-stream";
4
4
  import type { ThreadHistoryAdapter } from "../../adapters/thread-history";
5
5
  import type { AttachmentAdapter } from "../../adapters/attachment";
6
+ import type { FeedbackAdapter } from "../../adapters/feedback";
6
7
  import type { ModelContextProvider } from "../../model-context/types";
7
8
 
8
9
  /* oxlint-disable typescript/no-explicit-any -- structural stand-in for ComponentType without depending on react types */
@@ -40,6 +41,7 @@ export type RuntimeAdapters = {
40
41
  modelContext?: ModelContextProvider | undefined;
41
42
  history?: ThreadHistoryAdapter | undefined;
42
43
  attachments?: AttachmentAdapter | undefined;
44
+ feedback?: FeedbackAdapter | undefined;
43
45
  };
44
46
 
45
47
  export type RemoteThreadListAdapter = {
@@ -54,6 +56,14 @@ export type RemoteThreadListAdapter = {
54
56
  unarchive(remoteId: string): Promise<void>;
55
57
  delete(remoteId: string): Promise<void>;
56
58
  initialize(threadId: string): Promise<RemoteThreadInitializeResponse>;
59
+ /**
60
+ * Generates a title for the thread and streams it back.
61
+ *
62
+ * When the generation persists the title itself, the returned stream must
63
+ * not complete before that write has landed. Concurrent generations are
64
+ * ordered by stream completion, and a run whose write outlives its stream
65
+ * can overwrite a newer title.
66
+ */
57
67
  generateTitle(
58
68
  remoteId: string,
59
69
  unstable_messages: readonly ThreadMessage[],
@@ -21,7 +21,10 @@ import type {
21
21
  CreateAttachment,
22
22
  PendingAttachment,
23
23
  } from "../../types/attachment";
24
- import { isCreateAttachment } from "../../types/attachment";
24
+ import {
25
+ isAttachmentComplete,
26
+ isCreateAttachment,
27
+ } from "../../types/attachment";
25
28
  import type {
26
29
  AddToolResultOptions,
27
30
  RespondToToolApprovalOptions,
@@ -463,6 +466,35 @@ const useLiveState = <T>(initial: T) => {
463
466
  return [state, set, ref] as const;
464
467
  };
465
468
 
469
+ // A try/catch inside the resource makes the React Compiler bail out of the
470
+ // whole hook, so the adapter call and its failure status live at module scope.
471
+ const removeAttachmentThroughAdapter = async (
472
+ attachment: Attachment,
473
+ attachmentAdapter: AttachmentAdapter | undefined,
474
+ setAttachments: (
475
+ next:
476
+ | readonly Attachment[]
477
+ | ((prev: readonly Attachment[]) => readonly Attachment[]),
478
+ ) => void,
479
+ ) => {
480
+ try {
481
+ await attachmentAdapter?.remove(attachment);
482
+ } catch (error) {
483
+ const message = error instanceof Error ? error.message : String(error);
484
+ setAttachments((prev) =>
485
+ prev.map((candidate) =>
486
+ candidate.id === attachment.id && !isAttachmentComplete(candidate)
487
+ ? {
488
+ ...candidate,
489
+ status: { type: "incomplete", reason: "error", message },
490
+ }
491
+ : candidate,
492
+ ),
493
+ );
494
+ throw error;
495
+ }
496
+ };
497
+
466
498
  // Composer Client - minimal implementation
467
499
  const useComposerClientResource = ({
468
500
  type,
@@ -516,7 +548,13 @@ const useComposerClientResource = ({
516
548
  attachment,
517
549
  onRemove: async () => {
518
550
  attachmentAddOperations.cancel(attachment.id);
519
- await attachmentAdapter?.remove(attachment);
551
+ if (!isAttachmentComplete(attachment)) {
552
+ await removeAttachmentThroughAdapter(
553
+ attachment,
554
+ attachmentAdapter,
555
+ setAttachments,
556
+ );
557
+ }
520
558
  setAttachments((prev) =>
521
559
  prev.filter((a) => a.id !== attachment.id),
522
560
  );
@@ -905,7 +943,11 @@ const useExternalThread = ({
905
943
  : undefined;
906
944
  };
907
945
 
946
+ // `messages` changes identity on every streamed token, and this body fans out
947
+ // to one client per message, so pruning during render would re-run the whole
948
+ // fan-out per token. The effect bails out on an unchanged map instead.
908
949
  useEffect(() => {
950
+ // eslint-disable-next-line react-hooks/set-state-in-effect
909
951
  setSubmittedFeedback((prev) => {
910
952
  const live = Object.entries(prev).filter(([id, entry]) => {
911
953
  const msg = messages.find((m) => m.id === id);
@@ -1113,7 +1155,10 @@ const useExternalThread = ({
1113
1155
  }
1114
1156
  : {
1115
1157
  createdAt: message.createdAt ?? new Date(),
1116
- parentId: message.parentId ?? messages.at(-1)?.id ?? null,
1158
+ parentId:
1159
+ message.parentId === undefined
1160
+ ? (messages.at(-1)?.id ?? null)
1161
+ : message.parentId,
1117
1162
  sourceId: message.sourceId ?? null,
1118
1163
  role: message.role ?? "user",
1119
1164
  content: message.content,
@@ -3,6 +3,7 @@ import { resource } from "@assistant-ui/tap";
3
3
  import type { ClientOutput } from "@assistant-ui/store";
4
4
  import { CompositeContextProvider } from "../../utils/composite-context-provider";
5
5
  import type { ModelContextState } from "../scopes/model-context";
6
+ import { shallowEqual } from "@assistant-ui/store/internal";
6
7
 
7
8
  const EMPTY_TOOL_NAMES: readonly string[] = [];
8
9
 
@@ -12,7 +13,7 @@ const INITIAL_STATE: ModelContextState = {
12
13
  };
13
14
 
14
15
  const toolNamesEqual = (a: readonly string[], b: readonly string[]): boolean =>
15
- a === b || (a.length === b.length && a.every((v, i) => v === b[i]));
16
+ a === b || shallowEqual(a, b);
16
17
 
17
18
  const deriveState = (
18
19
  composite: CompositeContextProvider,
@@ -36,6 +37,9 @@ const useModelContext = (): ClientOutput<"modelContext"> => {
36
37
  );
37
38
 
38
39
  useEffect(() => {
40
+ // The composite is an external store; this catches the snapshot up for
41
+ // providers registered between render and subscribe.
42
+ // eslint-disable-next-line react-hooks/set-state-in-effect
39
43
  setState((prev) => deriveState(composite, prev));
40
44
  return composite.subscribe(() => {
41
45
  setState((prev) => deriveState(composite, prev));
@@ -59,6 +59,7 @@ const useSingleThreadList = ({
59
59
  mainThreadId: THREAD_ID,
60
60
  newThreadId: null,
61
61
  isLoading: false,
62
+ loadError: undefined,
62
63
  isLoadingMore: false,
63
64
  hasMore: false,
64
65
  threadIds: [THREAD_ID],
@@ -151,6 +151,46 @@ describe("ThreadSuggestions", () => {
151
151
  }
152
152
  });
153
153
 
154
+ it("drops sparse holes before the per-suggestion lookup", () => {
155
+ let setSuggestions!: (suggestions: ThreadSuggestion[]) => void;
156
+ const root = createTapRoot(function ThreadSuggestionsRoot() {
157
+ const [suggestions, setValue] = useState<ThreadSuggestion[]>([
158
+ { prompt: "Prompt A" },
159
+ { prompt: "Prompt B" },
160
+ ]);
161
+ setSuggestions = setValue;
162
+ return useResource(ThreadSuggestions(suggestions));
163
+ });
164
+
165
+ try {
166
+ const sparse: ThreadSuggestion[] = new Array(2);
167
+ sparse[1] = { prompt: "Prompt B" };
168
+ flushTapSync(() => setSuggestions(sparse));
169
+
170
+ const state = root.getValue().getState();
171
+ const expected = { title: "Prompt B", label: "", prompt: "Prompt B" };
172
+ expect(state.suggestions).toEqual([expected]);
173
+ expect(Object.keys(state.suggestions)).toEqual(["0"]);
174
+ const firstSuggestion = root
175
+ .getValue()
176
+ .suggestion({ index: 0 })
177
+ .getState();
178
+ expect(firstSuggestion).toEqual(expected);
179
+
180
+ // An equivalent sparse update keeps both identities stable.
181
+ const sparseAgain: ThreadSuggestion[] = new Array(2);
182
+ sparseAgain[1] = { prompt: "Prompt B" };
183
+ flushTapSync(() => setSuggestions(sparseAgain));
184
+
185
+ expect(root.getValue().getState()).toBe(state);
186
+ expect(root.getValue().suggestion({ index: 0 }).getState()).toBe(
187
+ firstSuggestion,
188
+ );
189
+ } finally {
190
+ root.unmount();
191
+ }
192
+ });
193
+
154
194
  it("reuses unchanged suggestion state when one entry changes", () => {
155
195
  let setSuggestions!: (suggestions: ThreadSuggestion[]) => void;
156
196
  const root = createTapRoot(function ThreadSuggestionsRoot() {
@@ -17,18 +17,21 @@ const useStableSuggestionsState = (
17
17
  const cell = useMemo(() => ({}) as { state?: SuggestionsState }, []);
18
18
  const previous = cell.state;
19
19
 
20
- const suggestions = next.suggestions.map((suggestion, index) => {
21
- const previousSuggestion = previous?.suggestions[index];
22
- return previousSuggestion && shallowEqual(previousSuggestion, suggestion)
23
- ? previousSuggestion
24
- : suggestion;
20
+ // forEach skips array holes, so a sparse caller array normalizes to a dense
21
+ // list here, before the per-suggestion resource lookup indexes every slot.
22
+ const suggestions: SuggestionState[] = [];
23
+ next.suggestions.forEach((suggestion) => {
24
+ // Probed at the compacted destination index, not the sparse source index,
25
+ // so an equivalent hole-containing update keeps reusing prior identities.
26
+ const previousSuggestion = previous?.suggestions[suggestions.length];
27
+ suggestions.push(
28
+ previousSuggestion && shallowEqual(previousSuggestion, suggestion)
29
+ ? previousSuggestion
30
+ : suggestion,
31
+ );
25
32
  });
26
33
  const state =
27
- previous &&
28
- previous.suggestions.length === suggestions.length &&
29
- suggestions.every(
30
- (suggestion, index) => suggestion === previous.suggestions[index],
31
- )
34
+ previous && shallowEqual(suggestions, previous.suggestions)
32
35
  ? previous
33
36
  : { suggestions };
34
37