@assistant-ui/core 0.2.21 → 0.2.22

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 (209) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/speech.d.ts.map +1 -1
  3. package/dist/adapters/speech.js +9 -2
  4. package/dist/adapters/speech.js.map +1 -1
  5. package/dist/adapters/voice.d.ts.map +1 -1
  6. package/dist/adapters/voice.js +13 -5
  7. package/dist/adapters/voice.js.map +1 -1
  8. package/dist/internal.d.ts +3 -1
  9. package/dist/internal.js +3 -1
  10. package/dist/model-context/frame/host.d.ts +1 -0
  11. package/dist/model-context/frame/host.d.ts.map +1 -1
  12. package/dist/model-context/frame/host.js +5 -0
  13. package/dist/model-context/frame/host.js.map +1 -1
  14. package/dist/model-context/frame/provider.d.ts.map +1 -1
  15. package/dist/model-context/frame/provider.js +1 -0
  16. package/dist/model-context/frame/provider.js.map +1 -1
  17. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  18. package/dist/react/AssistantProvider.js.map +1 -1
  19. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  20. package/dist/react/adapters/LocalStorageThreadListAdapter.js +77 -50
  21. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  22. package/dist/react/client/Interactables.js.map +1 -1
  23. package/dist/react/client/Tools.d.ts.map +1 -1
  24. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  25. package/dist/react/model-context/useInteractable.js.map +1 -1
  26. package/dist/react/primitive-hooks/useActionBarCopy.js +13 -13
  27. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  28. package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts.map +1 -1
  29. package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
  30. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.d.ts.map +1 -1
  31. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  32. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
  33. package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
  34. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  35. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
  36. package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
  37. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  38. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  39. package/dist/react/primitives/message/MessageParts.d.ts +2 -1
  40. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  41. package/dist/react/primitives/message/MessageParts.js +46 -36
  42. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  43. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  44. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  45. package/dist/react/primitives/thread/ThreadSuggestions.js.map +1 -1
  46. package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
  47. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
  48. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +18 -30
  49. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
  50. package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
  51. package/dist/react/providers/PartByIndexProvider.js +18 -30
  52. package/dist/react/providers/PartByIndexProvider.js.map +1 -1
  53. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  54. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -0
  55. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  56. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +9 -2
  57. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  58. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +49 -8
  59. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  60. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
  61. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  62. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  63. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +2 -1
  64. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  65. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  66. package/dist/react/runtimes/external-message-converter.js +1 -1
  67. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  68. package/dist/react/runtimes/useLocalRuntime.d.ts +1 -0
  69. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  70. package/dist/react/runtimes/useLocalRuntime.js +9 -1
  71. package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
  72. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  73. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  74. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  75. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  76. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  77. package/dist/runtime/api/message-runtime.js +4 -4
  78. package/dist/runtime/api/message-runtime.js.map +1 -1
  79. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  80. package/dist/runtime/api/thread-list-item-runtime.js +2 -1
  81. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  82. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  83. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  84. package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -1
  85. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  86. package/dist/runtime/base/base-composer-runtime-core.js +41 -14
  87. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  88. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  89. package/dist/runtime/base/base-thread-runtime-core.js +2 -1
  90. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  91. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  92. package/dist/runtime/base/default-edit-composer-runtime-core.js +4 -2
  93. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  94. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  95. package/dist/runtime/base/default-thread-composer-runtime-core.js +2 -2
  96. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  97. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  98. package/dist/runtime/utils/thread-message-like.js +4 -1
  99. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  100. package/dist/runtimes/external-store/external-store-runtime-core.d.ts.map +1 -1
  101. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  102. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  103. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  104. package/dist/runtimes/local/local-runtime-core.d.ts.map +1 -1
  105. package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
  106. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  107. package/dist/runtimes/local/local-thread-runtime-core.js +5 -2
  108. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  109. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  110. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +4 -4
  111. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  112. package/dist/runtimes/remote-thread-list/optimistic-state.js +22 -9
  113. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  114. package/dist/runtimes/remote-thread-list/types.d.ts +1 -1
  115. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  116. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +1 -2
  117. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  118. package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
  119. package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
  120. package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
  121. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  122. package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
  123. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  124. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  125. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  126. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  127. package/dist/subscribable/subscribable.d.ts +1 -1
  128. package/dist/subscribable/subscribable.d.ts.map +1 -1
  129. package/dist/subscribable/subscribable.js +11 -3
  130. package/dist/subscribable/subscribable.js.map +1 -1
  131. package/dist/tests/remote-thread-list-test-helpers.d.ts +2 -1
  132. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  133. package/dist/tests/remote-thread-list-test-helpers.js +5 -2
  134. package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
  135. package/dist/types/attachment.d.ts +2 -1
  136. package/dist/types/attachment.d.ts.map +1 -1
  137. package/dist/types/attachment.js +6 -0
  138. package/dist/types/attachment.js.map +1 -0
  139. package/dist/types/message.d.ts +1 -0
  140. package/dist/types/message.d.ts.map +1 -1
  141. package/dist/types/message.js.map +1 -1
  142. package/dist/utils/data-url.d.ts +9 -0
  143. package/dist/utils/data-url.d.ts.map +1 -0
  144. package/dist/utils/data-url.js +14 -0
  145. package/dist/utils/data-url.js.map +1 -0
  146. package/dist/utils/notify-event-listeners.d.ts +6 -0
  147. package/dist/utils/notify-event-listeners.d.ts.map +1 -0
  148. package/dist/utils/notify-event-listeners.js +16 -0
  149. package/dist/utils/notify-event-listeners.js.map +1 -0
  150. package/package.json +10 -9
  151. package/src/adapters/speech.test.ts +88 -0
  152. package/src/adapters/speech.ts +17 -2
  153. package/src/adapters/voice.test.ts +130 -0
  154. package/src/adapters/voice.ts +26 -6
  155. package/src/internal.ts +5 -0
  156. package/src/model-context/frame/host.test.ts +100 -0
  157. package/src/model-context/frame/host.ts +10 -0
  158. package/src/model-context/frame/provider.test.ts +83 -0
  159. package/src/model-context/frame/provider.ts +1 -0
  160. package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +82 -0
  161. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +94 -54
  162. package/src/react/primitive-hooks/useActionBarCopy.test.ts +79 -0
  163. package/src/react/primitive-hooks/useActionBarCopy.ts +4 -3
  164. package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -3
  165. package/src/react/primitives/message/MessageParts.tsx +48 -35
  166. package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +19 -2
  167. package/src/react/providers/PartByIndexProvider.tsx +16 -2
  168. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +18 -0
  169. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +1 -0
  170. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +48 -7
  171. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +69 -0
  172. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +1 -1
  173. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +16 -0
  174. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +2 -1
  175. package/src/react/runtimes/external-message-converter.test.tsx +21 -0
  176. package/src/react/runtimes/external-message-converter.ts +1 -1
  177. package/src/react/runtimes/useLocalRuntime.test.tsx +56 -0
  178. package/src/react/runtimes/useLocalRuntime.ts +13 -1
  179. package/src/runtime/api/message-runtime.test.ts +81 -0
  180. package/src/runtime/api/message-runtime.ts +4 -6
  181. package/src/runtime/api/thread-list-item-runtime.ts +6 -1
  182. package/src/runtime/base/base-composer-runtime-core.ts +77 -31
  183. package/src/runtime/base/base-thread-runtime-core.ts +2 -1
  184. package/src/runtime/base/default-edit-composer-runtime-core.ts +4 -2
  185. package/src/runtime/base/default-thread-composer-runtime-core.ts +2 -2
  186. package/src/runtime/utils/thread-message-like.ts +1 -0
  187. package/src/runtimes/local/local-thread-runtime-core.test.ts +150 -1
  188. package/src/runtimes/local/local-thread-runtime-core.ts +4 -2
  189. package/src/runtimes/remote-thread-list/optimistic-state.test.ts +63 -0
  190. package/src/runtimes/remote-thread-list/optimistic-state.ts +49 -17
  191. package/src/runtimes/remote-thread-list/types.ts +1 -1
  192. package/src/runtimes/tool-invocations/EDGE_CASES.md +13 -19
  193. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +127 -6
  194. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +4 -2
  195. package/src/subscribable/subscribable.test.ts +58 -0
  196. package/src/subscribable/subscribable.ts +13 -3
  197. package/src/tests/OptimisticState-list-race.test.ts +33 -0
  198. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-order.test.ts +223 -0
  199. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +44 -0
  200. package/src/tests/base-composer-runtime-core-send.test.ts +433 -5
  201. package/src/tests/default-edit-composer-runtime-core.test.ts +43 -5
  202. package/src/tests/event-subscription-listener-errors.test.ts +205 -0
  203. package/src/tests/remote-thread-list-test-helpers.ts +14 -7
  204. package/src/tests/thread-message-like.test.ts +57 -0
  205. package/src/types/attachment.ts +5 -0
  206. package/src/types/message.ts +1 -0
  207. package/src/utils/data-url.test.ts +50 -0
  208. package/src/utils/data-url.ts +9 -0
  209. package/src/utils/notify-event-listeners.ts +30 -0
@@ -1,4 +1,5 @@
1
1
  import type { Unsubscribe } from "../types/unsubscribe";
2
+ import { notifyEventListeners } from "../utils/notify-event-listeners";
2
3
 
3
4
  export const SKIP_UPDATE = Symbol("skip-update");
4
5
  export type SKIP_UPDATE = typeof SKIP_UPDATE;
@@ -39,7 +40,10 @@ function shallowEqual<T extends object>(
39
40
  if (objA === undefined) return false;
40
41
  if (objB === undefined) return false;
41
42
 
42
- for (const key of Object.keys(objA)) {
43
+ const keysA = Object.keys(objA);
44
+ if (keysA.length !== Object.keys(objB).length) return false;
45
+
46
+ for (const key of keysA) {
43
47
  const valueA = objA[key as keyof T];
44
48
  const valueB = objB[key as keyof T];
45
49
  if (!Object.is(valueA, valueB)) return false;
@@ -99,7 +103,12 @@ export abstract class BaseSubject {
99
103
 
100
104
  protected abstract _connect(): Unsubscribe;
101
105
 
102
- protected notifySubscribers(payload?: unknown) {
106
+ protected notifySubscribers(payload?: unknown, errorContext?: string) {
107
+ if (errorContext) {
108
+ notifyEventListeners(this._subscriptions, payload, errorContext);
109
+ return;
110
+ }
111
+
103
112
  for (const callback of this._subscriptions) callback(payload);
104
113
  }
105
114
 
@@ -273,8 +282,9 @@ export class EventSubscriptionSubject<
273
282
  }
274
283
 
275
284
  protected _connect(): Unsubscribe {
285
+ const errorContext = `Runtime event "${this.config.event}"`;
276
286
  const callback = (payload?: unknown) => {
277
- this.notifySubscribers(payload);
287
+ this.notifySubscribers(payload, errorContext);
278
288
  };
279
289
 
280
290
  let lastState = this.config.binding.getState();
@@ -79,6 +79,39 @@ const applyListResult = (
79
79
  };
80
80
 
81
81
  describe("list + delete race condition", () => {
82
+ it("stale list() does not re-add a thread deleted before list started", async () => {
83
+ const state = new OptimisticState<RemoteThreadState>(
84
+ applyListResult(EMPTY_STATE, {
85
+ threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
86
+ }),
87
+ );
88
+ const deleteDeferred = deferred<void>();
89
+ const listDeferred = deferred<ListResult>();
90
+
91
+ const deletePromise = state.optimisticUpdate({
92
+ execute: () => deleteDeferred.promise,
93
+ optimistic: (s) => updateStatusReducer(s, "thread-A", "deleted"),
94
+ });
95
+ const listPromise = state.optimisticUpdate({
96
+ execute: () => listDeferred.promise,
97
+ loading: (s) => ({ ...s, isLoading: true }),
98
+ then: applyListResult,
99
+ });
100
+
101
+ deleteDeferred.resolve();
102
+ await deletePromise;
103
+
104
+ listDeferred.resolve({
105
+ threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
106
+ });
107
+ await listPromise;
108
+
109
+ expect(state.value.threadIds).not.toContain("thread-A");
110
+ expect(
111
+ state.value.threadData[createThreadMappingId("thread-A")],
112
+ ).toBeUndefined();
113
+ });
114
+
82
115
  it("stale list() does not re-add a thread deleted while list was in flight", async () => {
83
116
  const state = new OptimisticState<RemoteThreadState>(EMPTY_STATE);
84
117
 
@@ -0,0 +1,223 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { RemoteThreadMetadata } from "../runtimes/remote-thread-list/types";
3
+ import {
4
+ createCore,
5
+ deferred,
6
+ makeAdapter,
7
+ setStartThreadRuntime,
8
+ } from "./remote-thread-list-test-helpers";
9
+
10
+ const thread = (id: string): RemoteThreadMetadata => ({
11
+ status: "regular",
12
+ remoteId: id,
13
+ externalId: id,
14
+ title: id,
15
+ });
16
+
17
+ describe("RemoteThreadListThreadListRuntimeCore.switchToThread order", () => {
18
+ it("preserves the latest switch when an earlier fetch resolves last", async () => {
19
+ const fetchB = deferred<RemoteThreadMetadata>();
20
+ const fetchC = deferred<RemoteThreadMetadata>();
21
+ const adapter = makeAdapter({
22
+ fetch: vi.fn((id: string) =>
23
+ id === "thread-b" ? fetchB.promise : fetchC.promise,
24
+ ),
25
+ });
26
+ const core = createCore(adapter);
27
+
28
+ const switchToB = core.switchToThread("thread-b");
29
+ const switchToC = core.switchToThread("thread-c");
30
+
31
+ fetchC.resolve(thread("thread-c"));
32
+ await switchToC;
33
+ fetchB.resolve(thread("thread-b"));
34
+ await switchToB;
35
+
36
+ expect(core.mainThreadId).toBe(core.getItemById("thread-c")?.id);
37
+ });
38
+
39
+ it("preserves the latest switch when an earlier runtime starts last", async () => {
40
+ const runtimeB = deferred<unknown>();
41
+ const runtimeC = deferred<unknown>();
42
+ const adapter = makeAdapter({
43
+ list: vi.fn(async () => ({
44
+ threads: [thread("thread-b"), thread("thread-c")],
45
+ })),
46
+ });
47
+ const core = createCore(adapter);
48
+ await core.getLoadThreadsPromise();
49
+
50
+ setStartThreadRuntime(core, (id) =>
51
+ id === core.getItemById("thread-b")?.id
52
+ ? runtimeB.promise
53
+ : runtimeC.promise,
54
+ );
55
+
56
+ const switchToB = core.switchToThread("thread-b");
57
+ const switchToC = core.switchToThread("thread-c");
58
+
59
+ runtimeC.resolve({});
60
+ await switchToC;
61
+ runtimeB.resolve({});
62
+ await switchToB;
63
+
64
+ expect(core.mainThreadId).toBe(core.getItemById("thread-c")?.id);
65
+ });
66
+
67
+ it("invalidates an earlier switch while a new thread request is pending", async () => {
68
+ const initializeThread = deferred<{
69
+ remoteId: string;
70
+ externalId: string;
71
+ }>();
72
+ const runtimeB = deferred<unknown>();
73
+ const adapter = makeAdapter({
74
+ initialize: vi.fn(() => initializeThread.promise),
75
+ list: vi.fn(async () => ({ threads: [thread("thread-b")] })),
76
+ });
77
+ const core = createCore(adapter);
78
+ await core.getLoadThreadsPromise();
79
+
80
+ const initialNewThreadId = core.newThreadId;
81
+ if (!initialNewThreadId) throw new Error("Expected an initial new thread");
82
+
83
+ const threadBId = core.getItemById("thread-b")?.id;
84
+ setStartThreadRuntime(core, (id) =>
85
+ id === threadBId ? runtimeB.promise : Promise.resolve({}),
86
+ );
87
+
88
+ const initialize = core.initialize(initialNewThreadId);
89
+ const switchToB = core.switchToThread("thread-b");
90
+ const switchToNewThread = core.switchToNewThread();
91
+
92
+ runtimeB.resolve({});
93
+ await switchToB;
94
+
95
+ expect(core.mainThreadId).not.toBe(threadBId);
96
+
97
+ initializeThread.resolve({
98
+ remoteId: "initialized-thread",
99
+ externalId: "initialized-thread",
100
+ });
101
+ await initialize;
102
+ await switchToNewThread;
103
+
104
+ expect(core.mainThreadId).toBe(core.getItemById(core.newThreadId!)?.id);
105
+ });
106
+
107
+ it("does not let a pending new thread request override a later switch", async () => {
108
+ const initializeThread = deferred<{
109
+ remoteId: string;
110
+ externalId: string;
111
+ }>();
112
+ const adapter = makeAdapter({
113
+ initialize: vi.fn(() => initializeThread.promise),
114
+ list: vi.fn(async () => ({ threads: [thread("thread-c")] })),
115
+ });
116
+ const core = createCore(adapter);
117
+ await core.getLoadThreadsPromise();
118
+
119
+ const initialNewThreadId = core.newThreadId;
120
+ if (!initialNewThreadId) throw new Error("Expected an initial new thread");
121
+
122
+ const initialize = core.initialize(initialNewThreadId);
123
+ const switchToNewThread = core.switchToNewThread();
124
+ await core.switchToThread("thread-c");
125
+
126
+ initializeThread.resolve({
127
+ remoteId: "initialized-thread",
128
+ externalId: "initialized-thread",
129
+ });
130
+ await initialize;
131
+ await switchToNewThread;
132
+
133
+ expect(core.mainThreadId).toBe(core.getItemById("thread-c")?.id);
134
+ });
135
+
136
+ it("keeps mutations waiting when a newer switch fails", async () => {
137
+ const fetchB = deferred<RemoteThreadMetadata>();
138
+ const newThreadRuntime = deferred<unknown>();
139
+ const adapter = makeAdapter({
140
+ fetch: vi.fn(() => fetchB.promise),
141
+ list: vi.fn(async () => ({ threads: [thread("thread-a")] })),
142
+ });
143
+ const core = createCore(adapter);
144
+ await core.getLoadThreadsPromise();
145
+ await core.switchToThread("thread-a");
146
+
147
+ const threadAId = core.getItemById("thread-a")?.id;
148
+ const newThreadId = core.newThreadId;
149
+ if (!newThreadId) throw new Error("Expected an initial new thread");
150
+
151
+ setStartThreadRuntime(core, (id) =>
152
+ id === core.getItemById(newThreadId)?.id
153
+ ? newThreadRuntime.promise
154
+ : Promise.resolve({}),
155
+ );
156
+
157
+ const deleteA = core.delete("thread-a");
158
+ const switchToB = core.switchToThread("thread-b");
159
+ const switchToBResult = expect(switchToB).rejects.toThrow("fetch failed");
160
+
161
+ newThreadRuntime.resolve({});
162
+ fetchB.reject(new Error("fetch failed"));
163
+
164
+ await switchToBResult;
165
+ await deleteA;
166
+
167
+ expect(core.mainThreadId).not.toBe(threadAId);
168
+ expect(core.mainThreadId).toBe(core.getItemById(core.newThreadId!)?.id);
169
+ });
170
+
171
+ it("lets a newer successful switch move the mutation target away", async () => {
172
+ const fetchB = deferred<RemoteThreadMetadata>();
173
+ const newThreadRuntime = deferred<unknown>();
174
+ const adapter = makeAdapter({
175
+ fetch: vi.fn(() => fetchB.promise),
176
+ list: vi.fn(async () => ({ threads: [thread("thread-a")] })),
177
+ });
178
+ const core = createCore(adapter);
179
+ await core.getLoadThreadsPromise();
180
+ await core.switchToThread("thread-a");
181
+
182
+ const newThreadId = core.newThreadId;
183
+ if (!newThreadId) throw new Error("Expected an initial new thread");
184
+
185
+ setStartThreadRuntime(core, (id) =>
186
+ id === core.getItemById(newThreadId)?.id
187
+ ? newThreadRuntime.promise
188
+ : Promise.resolve({}),
189
+ );
190
+
191
+ const deleteA = core.delete("thread-a");
192
+ const switchToB = core.switchToThread("thread-b");
193
+
194
+ newThreadRuntime.resolve({});
195
+ fetchB.resolve(thread("thread-b"));
196
+
197
+ await switchToB;
198
+ await deleteA;
199
+
200
+ expect(core.mainThreadId).toBe(core.getItemById("thread-b")?.id);
201
+ });
202
+
203
+ it("does not let a mutation cancel an existing pending switch", async () => {
204
+ const fetchB = deferred<RemoteThreadMetadata>();
205
+ const adapter = makeAdapter({
206
+ fetch: vi.fn(() => fetchB.promise),
207
+ list: vi.fn(async () => ({ threads: [thread("thread-a")] })),
208
+ });
209
+ const core = createCore(adapter);
210
+ await core.getLoadThreadsPromise();
211
+ await core.switchToThread("thread-a");
212
+
213
+ const switchToB = core.switchToThread("thread-b");
214
+ const deleteA = core.delete("thread-a");
215
+
216
+ fetchB.resolve(thread("thread-b"));
217
+
218
+ await switchToB;
219
+ await deleteA;
220
+
221
+ expect(core.mainThreadId).toBe(core.getItemById("thread-b")?.id);
222
+ });
223
+ });
@@ -128,6 +128,50 @@ describe("BaseComposerRuntimeCore.addAttachment error events", () => {
128
128
  expect(onError).not.toHaveBeenCalled();
129
129
  });
130
130
 
131
+ it("adds prepared attachments when File is unavailable", async () => {
132
+ const composer = makeComposer();
133
+ const fileConstructor = globalThis.File;
134
+ vi.stubGlobal("File", undefined);
135
+
136
+ try {
137
+ await composer.addAttachment({
138
+ name: "notes.txt",
139
+ contentType: "text/plain",
140
+ content: [{ type: "text", text: "hello" }],
141
+ });
142
+ } finally {
143
+ vi.stubGlobal("File", fileConstructor);
144
+ }
145
+
146
+ expect(composer.attachments[0]).toMatchObject({
147
+ type: "document",
148
+ name: "notes.txt",
149
+ contentType: "text/plain",
150
+ content: [{ type: "text", text: "hello" }],
151
+ status: { type: "complete" },
152
+ });
153
+ });
154
+
155
+ it("uses the adapter for foreign files that expose content", async () => {
156
+ const add = vi.fn(makeAdapter().add);
157
+ const composer = makeComposer(makeAdapter({ add }));
158
+ const foreignFile = {
159
+ name: "photo.png",
160
+ type: "image/png",
161
+ lastModified: 0,
162
+ content: [{ type: "text", text: "implementation detail" }],
163
+ } as File;
164
+
165
+ await composer.addAttachment(foreignFile);
166
+
167
+ expect(add).toHaveBeenCalledWith({ file: foreignFile });
168
+ expect(composer.attachments[0]).toMatchObject({
169
+ type: "image",
170
+ name: "photo.png",
171
+ contentType: "image/png",
172
+ });
173
+ });
174
+
131
175
  it("accepts JSON files with the application/json media type", async () => {
132
176
  const composer = makeComposer(new SimpleTextAttachmentAdapter());
133
177