@assistant-ui/core 0.3.15 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/dist/internal.d.ts +7 -2
  2. package/dist/internal.js +8 -3
  3. package/dist/model-context/frame/host.d.ts.map +1 -1
  4. package/dist/model-context/frame/host.js +3 -2
  5. package/dist/model-context/frame/host.js.map +1 -1
  6. package/dist/model-context/frame/provider.d.ts +2 -0
  7. package/dist/model-context/frame/provider.d.ts.map +1 -1
  8. package/dist/model-context/frame/provider.js +88 -25
  9. package/dist/model-context/frame/provider.js.map +1 -1
  10. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  11. package/dist/model-context/interactable-composer-metadata.js +1 -1
  12. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  13. package/dist/model-context/registry.d.ts +2 -2
  14. package/dist/model-context/registry.d.ts.map +1 -1
  15. package/dist/model-context/registry.js +9 -28
  16. package/dist/model-context/registry.js.map +1 -1
  17. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  18. package/dist/react/client/InMemoryThreadList.js +24 -20
  19. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts.map +1 -1
  21. package/dist/react/client/Interactables.js +59 -177
  22. package/dist/react/client/Interactables.js.map +1 -1
  23. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  24. package/dist/react/client/RemoteThreadList.js +23 -56
  25. package/dist/react/client/RemoteThreadList.js.map +1 -1
  26. package/dist/react/index.d.ts +2 -1
  27. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  28. package/dist/react/interactables-legacy/Interactables.js +56 -174
  29. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  30. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  31. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  32. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  33. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  34. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  35. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  36. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  37. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  38. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +1 -1
  39. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +1 -1
  40. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  41. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +23 -55
  42. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  43. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  44. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  45. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  46. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  47. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  48. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  49. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  50. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  51. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  52. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  53. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  54. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  55. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  56. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  57. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  58. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  59. package/dist/react/runtimes/external-message-converter.js +33 -233
  60. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  61. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  62. package/dist/runtime/api/composer-runtime.js.map +1 -1
  63. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  64. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  65. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  66. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  67. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  68. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  69. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  70. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  71. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  72. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  73. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  74. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  75. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  76. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  77. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  78. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  79. package/dist/runtime/internal.d.ts +2 -1
  80. package/dist/runtime/internal.js +2 -1
  81. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  82. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  83. package/dist/runtime/queue/message-queue.js +1 -1
  84. package/dist/runtime/queue/message-queue.js.map +1 -1
  85. package/dist/runtime/queue/queue-item.d.ts +12 -0
  86. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  87. package/dist/runtime/queue/queue-item.js +6 -0
  88. package/dist/runtime/queue/queue-item.js.map +1 -0
  89. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  90. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  91. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  92. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  93. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  94. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  95. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  96. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  97. package/dist/runtime/utils/auto-status.d.ts +6 -1
  98. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  99. package/dist/runtime/utils/auto-status.js +7 -1
  100. package/dist/runtime/utils/auto-status.js.map +1 -1
  101. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  102. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  103. package/dist/runtime/utils/external-message-conversion.js +221 -0
  104. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  105. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  106. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  107. package/dist/runtime/utils/external-store-message.js +2 -1
  108. package/dist/runtime/utils/external-store-message.js.map +1 -1
  109. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  110. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  111. package/dist/runtime/utils/message-repository-session.js +130 -0
  112. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  113. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  114. package/dist/runtime/utils/message-repository.js +3 -4
  115. package/dist/runtime/utils/message-repository.js.map +1 -1
  116. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  117. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  118. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  119. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  120. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  121. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  122. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  123. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  124. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  125. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  126. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  127. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  128. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  129. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +122 -20
  130. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  131. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  132. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  133. package/dist/runtimes/local/local-thread-runtime-core.js +20 -2
  134. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  135. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  136. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  137. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  138. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  139. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  140. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  141. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  142. package/dist/store/clients/external-thread.d.ts.map +1 -1
  143. package/dist/store/clients/external-thread.js +74 -99
  144. package/dist/store/clients/external-thread.js.map +1 -1
  145. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  146. package/dist/store/clients/single-thread-list.js +1 -1
  147. package/dist/store/clients/single-thread-list.js.map +1 -1
  148. package/dist/store/clients/suggestions.d.ts.map +1 -1
  149. package/dist/store/clients/suggestions.js +16 -7
  150. package/dist/store/clients/suggestions.js.map +1 -1
  151. package/dist/store/index.d.ts +4 -2
  152. package/dist/store/scope-registration.d.ts +1 -1
  153. package/dist/store/scopes/composer.d.ts +2 -1
  154. package/dist/store/scopes/composer.d.ts.map +1 -1
  155. package/dist/store/scopes/message.d.ts +1 -1
  156. package/dist/store/scopes/queue-item.d.ts +2 -9
  157. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  158. package/dist/store/scopes/queue-item.js +0 -6
  159. package/dist/types/message.d.ts +7 -3
  160. package/dist/types/message.d.ts.map +1 -1
  161. package/dist/types/message.js.map +1 -1
  162. package/dist/utils/data-url.d.ts +14 -1
  163. package/dist/utils/data-url.d.ts.map +1 -1
  164. package/dist/utils/data-url.js +13 -1
  165. package/dist/utils/data-url.js.map +1 -1
  166. package/dist/utils/invoke-user-callback.d.ts +5 -0
  167. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  168. package/dist/utils/invoke-user-callback.js +19 -0
  169. package/dist/utils/invoke-user-callback.js.map +1 -0
  170. package/dist/utils/normalizePartStatus.js.map +1 -1
  171. package/package.json +10 -10
  172. package/src/internal.ts +12 -0
  173. package/src/model-context/frame/host.test.ts +91 -7
  174. package/src/model-context/frame/host.ts +7 -2
  175. package/src/model-context/frame/provider.test.ts +267 -12
  176. package/src/model-context/frame/provider.ts +113 -31
  177. package/src/model-context/interactable-composer-metadata.ts +1 -1
  178. package/src/model-context/registry.test.ts +44 -0
  179. package/src/model-context/registry.ts +13 -35
  180. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  181. package/src/react/client/InMemoryThreadList.ts +7 -1
  182. package/src/react/client/Interactables.ts +19 -178
  183. package/src/react/client/RemoteThreadList.test.ts +96 -0
  184. package/src/react/client/RemoteThreadList.ts +27 -59
  185. package/src/react/interactables-legacy/Interactables.ts +20 -177
  186. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  187. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  188. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +26 -56
  189. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  190. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  191. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  192. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  193. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  194. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  195. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  196. package/src/react/runtimes/external-message-converter.ts +58 -481
  197. package/src/runtime/api/composer-runtime.ts +1 -1
  198. package/src/runtime/api/message-runtime.test.ts +28 -1
  199. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  200. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  201. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  202. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  203. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  204. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  205. package/src/runtime/internal.ts +5 -0
  206. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  207. package/src/runtime/queue/message-queue.ts +1 -4
  208. package/src/runtime/queue/queue-item.ts +10 -0
  209. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  210. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  211. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  212. package/src/runtime/utils/auto-status.ts +33 -0
  213. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  214. package/src/runtime/utils/external-message-conversion.ts +494 -0
  215. package/src/runtime/utils/external-store-message.ts +2 -0
  216. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  217. package/src/runtime/utils/message-repository-session.ts +176 -0
  218. package/src/runtime/utils/message-repository.ts +7 -4
  219. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  220. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  221. package/src/runtime/utils/thread-message-like.ts +2 -0
  222. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  223. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +175 -28
  224. package/src/runtimes/local/local-thread-runtime-core.test.ts +172 -1
  225. package/src/runtimes/local/local-thread-runtime-core.ts +48 -3
  226. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  227. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  228. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  229. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  230. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  231. package/src/store/clients/external-thread.ts +16 -61
  232. package/src/store/clients/single-thread-list.ts +4 -1
  233. package/src/store/clients/suggestions.test.ts +31 -0
  234. package/src/store/clients/suggestions.ts +18 -19
  235. package/src/store/scopes/message.ts +1 -1
  236. package/src/store/scopes/queue-item.ts +2 -9
  237. package/src/tests/MessageRepository.test.ts +77 -0
  238. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  239. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  240. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +164 -0
  241. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  242. package/src/tests/external-thread-attachments.test.tsx +173 -0
  243. package/src/tests/single-thread-list-item.test.tsx +47 -0
  244. package/src/tests/thread-message-like.test.ts +39 -1
  245. package/src/types/message.ts +8 -3
  246. package/src/utils/data-url.test.ts +52 -1
  247. package/src/utils/data-url.ts +21 -0
  248. package/src/utils/invoke-user-callback.test.ts +82 -0
  249. package/src/utils/invoke-user-callback.ts +31 -0
  250. package/src/utils/normalizePartStatus.ts +1 -1
  251. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -2,12 +2,12 @@ import type { Tool } from "assistant-stream";
2
2
  import {
3
3
  type ModelContext,
4
4
  type ModelContextProvider,
5
- mergeModelContexts,
6
5
  type AssistantToolProps,
7
6
  type AssistantInstructionsConfig,
8
7
  } from "./types";
9
- import type { Unsubscribe } from "../types/unsubscribe";
10
8
  import { notifySubscribers as notifyStateSubscribers } from "../subscribable/subscribable";
9
+ import { CompositeContextProvider } from "../utils/composite-context-provider";
10
+ import type { Unsubscribe } from "../types/unsubscribe";
11
11
  import type {
12
12
  ModelContextRegistryToolHandle,
13
13
  ModelContextRegistryInstructionHandle,
@@ -17,9 +17,14 @@ import type {
17
17
  export class ModelContextRegistry implements ModelContextProvider {
18
18
  private _tools = new Map<symbol, AssistantToolProps<any, any>>();
19
19
  private _instructions = new Map<symbol, string>();
20
- private _providers = new Map<symbol, ModelContextProvider>();
20
+ private _contextProviders = new CompositeContextProvider();
21
21
  private _subscribers = new Set<() => void>();
22
- private _providerUnsubscribes = new Map<symbol, Unsubscribe | undefined>();
22
+
23
+ constructor() {
24
+ this._contextProviders.subscribe(() => {
25
+ this.notifySubscribers();
26
+ });
27
+ }
23
28
 
24
29
  getModelContext(): ModelContext {
25
30
  const instructions = Array.from(this._instructions.values()).filter(
@@ -35,9 +40,7 @@ export class ModelContextRegistry implements ModelContextProvider {
35
40
  tools[toolName] = tool;
36
41
  }
37
42
 
38
- const providerContexts = mergeModelContexts(
39
- new Set(this._providers.values()),
40
- );
43
+ const providerContexts = this._contextProviders.getModelContext();
41
44
 
42
45
  const context: ModelContext = {
43
46
  system,
@@ -139,36 +142,11 @@ export class ModelContextRegistry implements ModelContextProvider {
139
142
  addProvider(
140
143
  provider: ModelContextProvider,
141
144
  ): ModelContextRegistryProviderHandle {
142
- const id = Symbol();
143
-
144
- this._providers.set(id, provider);
145
-
146
- let unsubscribe: Unsubscribe | undefined;
147
- try {
148
- unsubscribe = provider.subscribe?.(() => {
149
- this.notifySubscribers();
150
- });
151
- } catch (error) {
152
- this._providers.delete(id);
153
- try {
154
- this.notifySubscribers();
155
- } catch (notifyError) {
156
- console.error(notifyError);
157
- }
158
- throw error;
159
- }
160
- this._providerUnsubscribes.set(id, unsubscribe);
161
-
162
- this.notifySubscribers();
145
+ const unregister =
146
+ this._contextProviders.registerModelContextProvider(provider);
163
147
 
164
148
  return {
165
- remove: () => {
166
- this._providers.delete(id);
167
- const unsubscribe = this._providerUnsubscribes.get(id);
168
- unsubscribe?.();
169
- this._providerUnsubscribes.delete(id);
170
- this.notifySubscribers();
171
- },
149
+ remove: unregister,
172
150
  };
173
151
  }
174
152
  }
@@ -146,3 +146,52 @@ describe("InMemoryThreadList delete", () => {
146
146
  expect(state.mainThreadId).not.toBe("main");
147
147
  });
148
148
  });
149
+
150
+ describe("InMemoryThreadList item index selectors", () => {
151
+ it("resolves index selectors within the archived and regular subsets", async () => {
152
+ const { getAui } = setup();
153
+ await act(async () => {});
154
+
155
+ await act(async () => {
156
+ getAui().threads.switchToNewThread();
157
+ });
158
+ await act(async () => {});
159
+ const b = getAui().threads.getState().mainThreadId;
160
+
161
+ await act(async () => {
162
+ getAui().threads.switchToNewThread();
163
+ });
164
+ await act(async () => {});
165
+
166
+ await act(async () => {
167
+ getAui().threads.item({ id: b }).archive();
168
+ });
169
+ await act(async () => {});
170
+
171
+ const state = getAui().threads.getState();
172
+ expect(state.archivedThreadIds).toEqual([b]);
173
+ expect(state.threadIds).toHaveLength(2);
174
+
175
+ for (const [index, id] of state.archivedThreadIds.entries()) {
176
+ expect(
177
+ getAui().threads.item({ index, archived: true }).getState().id,
178
+ ).toBe(id);
179
+ }
180
+ for (const [index, id] of state.threadIds.entries()) {
181
+ expect(getAui().threads.item({ index }).getState().id).toBe(id);
182
+ expect(
183
+ getAui().threads.item({ index, archived: false }).getState().id,
184
+ ).toBe(id);
185
+ }
186
+
187
+ expect(() =>
188
+ getAui().threads.item({ index: state.threadIds.length }),
189
+ ).toThrow("out of bounds");
190
+ expect(() =>
191
+ getAui().threads.item({
192
+ index: state.archivedThreadIds.length,
193
+ archived: true,
194
+ }),
195
+ ).toThrow("out of bounds");
196
+ });
197
+ });
@@ -236,7 +236,13 @@ const useInMemoryThreadList = (
236
236
  const index = threads.findIndex((t) => t.id === selector.id);
237
237
  return threadListItems.get({ index });
238
238
  }
239
- return threadListItems.get(selector);
239
+ // The lookup is keyed over the combined thread array, while index
240
+ // selectors address the regular/archived subset the primitives render.
241
+ const ids = selector.archived ? state.archivedThreadIds : state.threadIds;
242
+ const id = ids[selector.index];
243
+ if (id === undefined) return threadListItems.get({ index: -1 });
244
+ const index = threads.findIndex((t) => t.id === id);
245
+ return threadListItems.get({ index });
240
246
  },
241
247
  thread: () => mainThreadClient.methods,
242
248
  };
@@ -24,8 +24,7 @@ import {
24
24
  interactableToolName,
25
25
  } from "../../model-context/interactable-composer-metadata";
26
26
  import { notifySubscribers as notifyStateSubscribers } from "../../subscribable/subscribable";
27
-
28
- const PERSISTENCE_DEBOUNCE_MS = 500;
27
+ import { useInteractablePersistenceQueue } from "../interactables-shared/useInteractablePersistenceQueue";
29
28
 
30
29
  type RestorePersistedStateOptions = {
31
30
  stash: Map<string, unknown>;
@@ -109,26 +108,6 @@ const useInteractablesResource = ({
109
108
  const adapterRef = useRef<
110
109
  Unstable_InteractablePersistenceAdapter | undefined
111
110
  >(undefined);
112
- const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
113
- undefined,
114
- );
115
- const syncSeqRef = useRef(0);
116
- const latestSyncSeqByIdRef = useRef(new Map<string, number>());
117
- const inFlightPersistenceRef = useRef(0);
118
- const flushResolversRef = useRef<Array<() => void>>([]);
119
- const dirtyIdsRef = useRef(new Set<string>());
120
-
121
- type PersistenceBatch = {
122
- adapter: Unstable_InteractablePersistenceAdapter;
123
- payload: Unstable_InteractablePersistedState;
124
- dirtyIds: Set<string>;
125
- seq: number;
126
- };
127
-
128
- const outgoingQueueRef = useRef<PersistenceBatch[]>([]);
129
- const runPersistenceRef = useRef<(batch?: PersistenceBatch) => void>(
130
- () => {},
131
- );
132
111
 
133
112
  const setStateAndRef = useCallback(
134
113
  (
@@ -152,149 +131,28 @@ const useInteractablesResource = ({
152
131
  return result;
153
132
  }, []);
154
133
 
155
- const takeDirtyBatch = useCallback(
134
+ const updatePersistenceStatus = useCallback(
156
135
  (
157
- adapter: Unstable_InteractablePersistenceAdapter,
158
- ): PersistenceBatch | undefined => {
159
- if (dirtyIdsRef.current.size === 0) return;
160
- const dirtyIds = new Set(dirtyIdsRef.current);
161
- dirtyIdsRef.current.clear();
162
- const seq = ++syncSeqRef.current;
163
- for (const id of dirtyIds) latestSyncSeqByIdRef.current.set(id, seq);
164
- return { adapter, payload: exportState(), dirtyIds, seq };
165
- },
166
- [exportState],
167
- );
168
-
169
- const enqueuePersistence = useCallback(
170
- (adapter: Unstable_InteractablePersistenceAdapter) => {
171
- const batch = takeDirtyBatch(adapter);
172
- if (!batch) return;
173
- if (inFlightPersistenceRef.current === 0) {
174
- runPersistenceRef.current(batch);
175
- } else {
176
- outgoingQueueRef.current.push(batch);
177
- }
178
- },
179
- [takeDirtyBatch],
180
- );
181
-
182
- const runPersistence = useCallback(
183
- async (batch?: PersistenceBatch) => {
184
- const resolved =
185
- batch ??
186
- (adapterRef.current ? takeDirtyBatch(adapterRef.current) : undefined);
187
- if (!resolved) {
188
- if (inFlightPersistenceRef.current === 0) {
189
- for (const resolve of flushResolversRef.current) resolve();
190
- flushResolversRef.current = [];
191
- }
192
- return;
193
- }
194
-
195
- const { adapter, payload, dirtyIds, seq } = resolved;
196
- inFlightPersistenceRef.current += 1;
197
-
198
- setStateAndRef((prev) => ({
199
- ...prev,
200
- persistence: {
201
- ...prev.persistence,
202
- ...Object.fromEntries(
203
- [...dirtyIds].map((id) => [
204
- id,
205
- { isPending: true, error: undefined },
206
- ]),
207
- ),
208
- },
209
- }));
210
-
211
- try {
212
- await adapter.save(payload);
213
- setStateAndRef((prev) => {
214
- let changed = false;
215
- const persistence = { ...prev.persistence };
216
- for (const id of dirtyIds) {
217
- if (
218
- latestSyncSeqByIdRef.current.get(id) !== seq ||
219
- dirtyIdsRef.current.has(id)
220
- )
221
- continue;
222
- latestSyncSeqByIdRef.current.delete(id);
223
- delete persistence[id];
224
- changed = true;
225
- }
226
- return changed ? { ...prev, persistence } : prev;
227
- });
228
- } catch (e) {
229
- setStateAndRef((prev) => {
230
- let changed = false;
231
- const persistence = { ...prev.persistence };
232
- for (const id of dirtyIds) {
233
- if (
234
- latestSyncSeqByIdRef.current.get(id) !== seq ||
235
- dirtyIdsRef.current.has(id)
236
- )
237
- continue;
238
- latestSyncSeqByIdRef.current.delete(id);
239
- persistence[id] = { isPending: false, error: e };
240
- changed = true;
241
- }
242
- return changed ? { ...prev, persistence } : prev;
243
- });
244
- } finally {
245
- inFlightPersistenceRef.current -= 1;
246
- const next =
247
- outgoingQueueRef.current.shift() ??
248
- (adapterRef.current && dirtyIdsRef.current.size > 0
249
- ? takeDirtyBatch(adapterRef.current)
250
- : undefined);
251
- if (next) {
252
- if (debounceTimerRef.current !== undefined) {
253
- clearTimeout(debounceTimerRef.current);
254
- debounceTimerRef.current = undefined;
255
- }
256
- runPersistenceRef.current(next);
257
- } else if (inFlightPersistenceRef.current === 0) {
258
- for (const resolve of flushResolversRef.current) resolve();
259
- flushResolversRef.current = [];
260
- }
261
- }
136
+ updater: (
137
+ prev: Unstable_InteractablesState["persistence"],
138
+ ) => Unstable_InteractablesState["persistence"],
139
+ ) => {
140
+ setStateAndRef((prev) => {
141
+ const persistence = updater(prev.persistence);
142
+ return persistence === prev.persistence
143
+ ? prev
144
+ : { ...prev, persistence };
145
+ });
262
146
  },
263
- [setStateAndRef, takeDirtyBatch],
147
+ [setStateAndRef],
264
148
  );
265
- runPersistenceRef.current = (nextBatch) => {
266
- void runPersistence(nextBatch);
267
- };
268
149
 
269
- const flushIfPending = useCallback(() => {
270
- if (debounceTimerRef.current !== undefined) {
271
- clearTimeout(debounceTimerRef.current);
272
- debounceTimerRef.current = undefined;
273
- }
274
- if (adapterRef.current) enqueuePersistence(adapterRef.current);
275
- }, [enqueuePersistence]);
276
-
277
- const schedulePersistence = useCallback(
278
- (id: string) => {
279
- if (!adapterRef.current) return;
280
- dirtyIdsRef.current.add(id);
281
- if (debounceTimerRef.current !== undefined) {
282
- clearTimeout(debounceTimerRef.current);
283
- }
284
- debounceTimerRef.current = setTimeout(() => {
285
- debounceTimerRef.current = undefined;
286
- if (inFlightPersistenceRef.current === 0 && adapterRef.current) {
287
- enqueuePersistence(adapterRef.current);
288
- } else {
289
- debounceTimerRef.current = setTimeout(() => {
290
- debounceTimerRef.current = undefined;
291
- if (adapterRef.current) enqueuePersistence(adapterRef.current);
292
- }, PERSISTENCE_DEBOUNCE_MS);
293
- }
294
- }, PERSISTENCE_DEBOUNCE_MS);
295
- },
296
- [enqueuePersistence],
297
- );
150
+ const { flushIfPending, schedulePersistence, flush } =
151
+ useInteractablePersistenceQueue({
152
+ adapterRef,
153
+ snapshot: exportState,
154
+ updatePersistenceStatus,
155
+ });
298
156
 
299
157
  const restorePersistedState = useCallback(
300
158
  (
@@ -394,23 +252,6 @@ const useInteractablesResource = ({
394
252
  };
395
253
  }, [persistence, setPersistenceAdapter]);
396
254
 
397
- const flush = useCallback(async () => {
398
- if (debounceTimerRef.current !== undefined) {
399
- clearTimeout(debounceTimerRef.current);
400
- debounceTimerRef.current = undefined;
401
- }
402
- const hasWork =
403
- inFlightPersistenceRef.current > 0 ||
404
- dirtyIdsRef.current.size > 0 ||
405
- outgoingQueueRef.current.length > 0;
406
- if (!hasWork) return;
407
- const p = new Promise<void>((resolve) => {
408
- flushResolversRef.current.push(resolve);
409
- });
410
- if (adapterRef.current) enqueuePersistence(adapterRef.current);
411
- return p;
412
- }, [enqueuePersistence]);
413
-
414
255
  const setDefState = useCallback(
415
256
  (id: string, updater: (prev: unknown) => unknown) => {
416
257
  touchedIdsRef.current.add(id);
@@ -234,6 +234,37 @@ describe("RemoteThreadList", () => {
234
234
  handle.destroy();
235
235
  });
236
236
 
237
+ it("keeps a thread initialized during the list() flight after switching away", async () => {
238
+ const listDeferred =
239
+ deferred<Awaited<ReturnType<RemoteThreadListAdapter["list"]>>>();
240
+ const adapter = makeAdapter({
241
+ list: vi.fn(() => listDeferred.promise),
242
+ });
243
+ const { handle } = mountList(adapter);
244
+ const aui = handle.getClient();
245
+
246
+ const loadPromise = aui.threads.getLoadThreadsPromise();
247
+ const initializedId = aui.threads.getState().mainThreadId;
248
+ await aui.threads.item("main").initialize();
249
+ await vi.waitFor(() => {
250
+ expect(aui.threads.getState().threadIds).toContain(initializedId);
251
+ });
252
+
253
+ listDeferred.resolve({
254
+ threads: [{ status: "regular" as const, remoteId: "t1", title: "One" }],
255
+ });
256
+ await loadPromise;
257
+
258
+ flushTapSync(() => aui.threads.switchToNewThread());
259
+ await vi.waitFor(() => {
260
+ expect(aui.threads.getState().mainThreadId).not.toBe(initializedId);
261
+ });
262
+
263
+ expect(aui.threads.getState().threadIds[0]).toBe(initializedId);
264
+ expect(aui.threads.getState().threadIds).toContain("t1");
265
+ handle.destroy();
266
+ });
267
+
237
268
  it("switches to a listed thread and back to a new thread", async () => {
238
269
  const adapter = makeAdapter({
239
270
  list: vi.fn(async () => ({
@@ -331,6 +362,71 @@ describe("RemoteThreadList", () => {
331
362
  handle.destroy();
332
363
  });
333
364
 
365
+ it("preserves generated titles across an overlapping reload", async () => {
366
+ const reload = deferred<{
367
+ threads: {
368
+ status: "regular";
369
+ remoteId: string;
370
+ title: string;
371
+ }[];
372
+ }>();
373
+ const adapter = makeAdapter({
374
+ list: vi
375
+ .fn()
376
+ .mockResolvedValueOnce({
377
+ threads: [
378
+ { status: "regular" as const, remoteId: "t1", title: "One" },
379
+ ],
380
+ })
381
+ .mockImplementationOnce(() => reload.promise),
382
+ generateTitle: vi.fn(
383
+ async () =>
384
+ new ReadableStream({
385
+ start(controller) {
386
+ controller.enqueue({
387
+ type: "part-start",
388
+ path: [],
389
+ part: { type: "text" },
390
+ });
391
+ controller.enqueue({
392
+ type: "text-delta",
393
+ path: [0],
394
+ textDelta: "Generated",
395
+ });
396
+ controller.enqueue({ type: "part-finish", path: [0] });
397
+ controller.close();
398
+ },
399
+ }) as never,
400
+ ),
401
+ });
402
+ const { handle } = mountList(adapter);
403
+ const aui = handle.getClient();
404
+ await aui.threads.getLoadThreadsPromise();
405
+ flushTapSync(() => aui.threads.switchToThread("t1"));
406
+ await vi.waitFor(() => {
407
+ expect(aui.threads.getState().mainThreadId).toBe("t1");
408
+ });
409
+
410
+ const reloadPromise = aui.threads.reload();
411
+ await vi.waitFor(() => {
412
+ expect(adapter.list).toHaveBeenCalledTimes(2);
413
+ });
414
+ await aui.threads.item({ id: "t1" }).rename("Renamed");
415
+ await aui.threads.item({ id: "t1" }).generateTitle();
416
+ await vi.waitFor(() => {
417
+ expect(aui.threads.item({ id: "t1" }).getState().title).toBe("Generated");
418
+ });
419
+
420
+ reload.resolve({
421
+ threads: [{ status: "regular", remoteId: "t1", title: "One" }],
422
+ });
423
+ await reloadPromise;
424
+ await vi.waitFor(() => {
425
+ expect(aui.threads.item({ id: "t1" }).getState().title).toBe("Generated");
426
+ });
427
+ handle.destroy();
428
+ });
429
+
334
430
  it("opens a controlled threadId without echoing it back", async () => {
335
431
  const adapter = makeAdapter({
336
432
  list: vi.fn(async () => ({
@@ -13,16 +13,19 @@ import {
13
13
  useClientResource,
14
14
  } from "@assistant-ui/store/client";
15
15
  import { isDevelopment, useThreadSelectionEvents } from "../../store/internal";
16
- import { generateId } from "../../utils/id";
17
16
  import { OptimisticState } from "../../runtimes/remote-thread-list/optimistic-state";
18
17
  import {
19
18
  classifyThreads,
19
+ createEmptyRemoteThreadState,
20
20
  createThreadMappingId,
21
21
  getThreadData,
22
22
  normalizeCursor,
23
23
  updateStatusReducer,
24
24
  type RemoteThreadData,
25
25
  type RemoteThreadState,
26
+ preserveMidLoadTransitions,
27
+ seedNewThread,
28
+ statusSnapshot,
26
29
  } from "../../runtimes/remote-thread-list/remote-thread-state";
27
30
  import type {
28
31
  RemoteThreadInitializeResponse,
@@ -40,16 +43,7 @@ import { AdaptedRemoteThread } from "./AdaptedRemoteThread";
40
43
 
41
44
  const RESOLVED_PROMISE = Promise.resolve();
42
45
 
43
- const EMPTY_LIST: RemoteThreadState = {
44
- isLoading: true,
45
- isLoadingMore: false,
46
- cursor: undefined,
47
- newThreadId: undefined,
48
- threadIds: [],
49
- archivedThreadIds: [],
50
- threadIdMap: {},
51
- threadData: {},
52
- };
46
+ const EMPTY_LIST = createEmptyRemoteThreadState();
53
47
 
54
48
  export type RemoteThreadListProps = {
55
49
  /**
@@ -91,46 +85,14 @@ const toInitializeResult = (
91
85
 
92
86
  const applyTitleStream = async (
93
87
  stream: Parameters<typeof AssistantMessageStream.fromAssistantStream>[0],
94
- onTitle: (title: string | undefined) => void,
88
+ onTitle: (title: string | undefined) => Promise<void>,
95
89
  ) => {
96
90
  const messageStream = AssistantMessageStream.fromAssistantStream(stream);
97
91
  for await (const result of messageStream) {
98
- onTitle(result.parts.filter((part) => part.type === "text")[0]?.text);
92
+ await onTitle(result.parts.filter((part) => part.type === "text")[0]?.text);
99
93
  }
100
94
  };
101
95
 
102
- const seedNewThread = (
103
- state: RemoteThreadState,
104
- ): { id: string; state: RemoteThreadState } => {
105
- let id: string;
106
- do {
107
- id = `__LOCALID_${generateId()}`;
108
- } while (state.threadIdMap[id]);
109
- const mappingId = createThreadMappingId(id);
110
- return {
111
- id,
112
- state: {
113
- ...state,
114
- newThreadId: id,
115
- threadIdMap: {
116
- ...state.threadIdMap,
117
- [id]: mappingId,
118
- },
119
- threadData: {
120
- ...state.threadData,
121
- [mappingId]: {
122
- status: "new",
123
- id,
124
- remoteId: undefined,
125
- externalId: undefined,
126
- title: undefined,
127
- custom: undefined,
128
- },
129
- },
130
- },
131
- };
132
- };
133
-
134
96
  const useThreadListItemClient = (props: {
135
97
  data: RemoteThreadData;
136
98
  isRunning: boolean;
@@ -424,6 +386,7 @@ const useRemoteThreadList = (
424
386
  if (session.loadPromise) return session.loadPromise;
425
387
  const generation = session.loadGeneration;
426
388
  const adapter = session.adapter;
389
+ const statusAtRequest = statusSnapshot(store.baseValue);
427
390
  session.loadPromise = store
428
391
  .optimisticUpdate({
429
392
  execute: () => adapter.list(),
@@ -437,7 +400,7 @@ const useRemoteThreadList = (
437
400
  threadIdMap: {},
438
401
  threadData: {},
439
402
  });
440
- return {
403
+ const merged = {
441
404
  ...state,
442
405
  isLoading: false,
443
406
  cursor: normalizeCursor(page.nextCursor),
@@ -452,6 +415,7 @@ const useRemoteThreadList = (
452
415
  ...fresh.threadData,
453
416
  },
454
417
  };
418
+ return preserveMidLoadTransitions(merged, state, statusAtRequest);
455
419
  },
456
420
  })
457
421
  .catch((error: unknown) => {
@@ -985,19 +949,23 @@ const useRemoteThreadList = (
985
949
  if (!messages) return;
986
950
  const stream = await currentAdapter.generateTitle(remoteId, messages);
987
951
  requireAdapterGeneration(adapterGeneration);
988
- await applyTitleStream(stream, (newTitle) => {
989
- if (adapterGeneration !== session.adapterGeneration) return;
990
- const state = store.baseValue;
991
- const current = getThreadData(state, data.id);
992
- if (!current) return;
993
- store.update({
994
- ...state,
995
- threadData: {
996
- ...state.threadData,
997
- [current.id]: {
998
- ...current,
999
- title: newTitle,
1000
- },
952
+ await applyTitleStream(stream, async (newTitle) => {
953
+ await store.optimisticUpdate({
954
+ execute: async () => {},
955
+ optimistic: (state) => {
956
+ if (adapterGeneration !== session.adapterGeneration) return state;
957
+ const current = getThreadData(state, data.id);
958
+ if (!current) return state;
959
+ return {
960
+ ...state,
961
+ threadData: {
962
+ ...state.threadData,
963
+ [current.id]: {
964
+ ...current,
965
+ title: newTitle,
966
+ },
967
+ },
968
+ };
1001
969
  },
1002
970
  });
1003
971
  });