@assistant-ui/core 0.3.2 → 0.3.4
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.
- package/dist/index.d.ts +4 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +9 -7
- package/dist/internal.js +8 -6
- package/dist/react/primitive-hooks/useActionBarCopy.js +51 -24
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +29 -25
- package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +2 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +2 -2
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts +6 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.d.ts.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.js +1 -0
- package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +67 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +76 -15
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +4 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +21 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +26 -2
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +29 -8
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/types/MessagePartComponentTypes.d.ts +8 -0
- package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
- package/dist/runtime/api/bindings.d.ts +5 -0
- package/dist/runtime/api/bindings.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +5 -1
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +14 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +6 -1
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +7 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +11 -3
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +3 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +54 -12
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +10 -0
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/internal.d.ts +4 -4
- package/dist/runtime/internal.js +4 -4
- package/dist/runtimes/external-store/external-store-adapter.d.ts +9 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +4 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +6 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -0
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.js +29 -23
- package/dist/store/clients/chain-of-thought-client.js.map +1 -1
- package/dist/store/clients/thread-message-client.d.ts.map +1 -1
- package/dist/store/clients/thread-message-client.js +101 -89
- package/dist/store/clients/thread-message-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +66 -45
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +91 -75
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +7 -0
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/threads.d.ts +1 -0
- package/dist/store/scopes/threads.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/message.d.ts +22 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/data-url.d.ts +14 -1
- package/dist/utils/data-url.d.ts.map +1 -1
- package/dist/utils/data-url.js +23 -1
- package/dist/utils/data-url.js.map +1 -1
- package/dist/utils/getGroupStatus.d.ts +9 -0
- package/dist/utils/getGroupStatus.d.ts.map +1 -0
- package/dist/utils/getGroupStatus.js +15 -0
- package/dist/utils/getGroupStatus.js.map +1 -0
- package/dist/utils/image-media-type.d.ts +13 -0
- package/dist/utils/image-media-type.d.ts.map +1 -0
- package/dist/utils/image-media-type.js +141 -0
- package/dist/utils/image-media-type.js.map +1 -0
- package/dist/utils/normalizePartStatus.d.ts +8 -0
- package/dist/utils/normalizePartStatus.d.ts.map +1 -0
- package/dist/utils/normalizePartStatus.js +39 -0
- package/dist/utils/normalizePartStatus.js.map +1 -0
- package/dist/utils/wire-media.d.ts +35 -0
- package/dist/utils/wire-media.d.ts.map +1 -0
- package/dist/utils/wire-media.js +55 -0
- package/dist/utils/wire-media.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/internal.ts +13 -1
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +109 -14
- package/src/react/primitive-hooks/useActionBarCopy.ts +27 -2
- package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +148 -0
- package/src/react/primitive-hooks/useSuggestionTrigger.ts +10 -4
- package/src/react/primitives/message/MessageGroupedParts.tsx +4 -4
- package/src/react/primitives/message/MessageParts.tsx +6 -1
- package/src/react/providers/ReadonlyThreadProvider.tsx +1 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +157 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +160 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +135 -32
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +37 -0
- package/src/react/runtimes/cloud/auiV0.ts +59 -14
- package/src/react/types/MessagePartComponentTypes.ts +8 -0
- package/src/runtime/api/bindings.ts +5 -0
- package/src/runtime/api/message-runtime.test.ts +140 -0
- package/src/runtime/api/message-runtime.ts +10 -5
- package/src/runtime/api/thread-list-runtime.ts +22 -1
- package/src/runtime/api/thread-runtime.ts +15 -6
- package/src/runtime/base/base-composer-runtime-core.ts +72 -19
- package/src/runtime/interfaces/thread-list-runtime-core.ts +11 -0
- package/src/runtime/interfaces/thread-runtime-core.ts +15 -0
- package/src/runtimes/external-store/external-store-adapter.ts +9 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +9 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +8 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -0
- package/src/store/clients/chain-of-thought-client.ts +2 -7
- package/src/store/clients/thread-message-client.test.ts +84 -0
- package/src/store/clients/thread-message-client.ts +14 -4
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -2
- package/src/store/runtime-clients/thread-list-runtime-client.ts +13 -1
- package/src/store/scopes/thread-list-item.ts +7 -0
- package/src/store/scopes/threads.ts +1 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-reloadMainThread.test.ts +257 -0
- package/src/tests/auiV0Encode.test.ts +218 -1
- package/src/tests/base-composer-runtime-core-addAttachment.test.ts +296 -0
- package/src/tests/event-subscription-listener-errors.test.ts +1 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +39 -1
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +30 -0
- package/src/utils/data-url.ts +24 -0
- package/src/utils/getGroupStatus.test.ts +36 -0
- package/src/utils/getGroupStatus.ts +31 -0
- package/src/utils/image-media-type.test.ts +96 -0
- package/src/utils/image-media-type.ts +118 -0
- package/src/utils/normalizePartStatus.test.ts +59 -0
- package/src/utils/normalizePartStatus.ts +55 -0
- package/src/utils/wire-media.test.ts +96 -0
- package/src/utils/wire-media.ts +70 -0
|
@@ -38,6 +38,22 @@ const makeComposer = (adapter?: AttachmentAdapter) => {
|
|
|
38
38
|
return new DefaultThreadComposerRuntimeCore(runtime);
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
const makeUploadingAttachment = (
|
|
42
|
+
file: File,
|
|
43
|
+
id: string,
|
|
44
|
+
): PendingAttachment => ({
|
|
45
|
+
id,
|
|
46
|
+
type: "image",
|
|
47
|
+
name: file.name,
|
|
48
|
+
contentType: file.type,
|
|
49
|
+
file,
|
|
50
|
+
status: {
|
|
51
|
+
type: "running",
|
|
52
|
+
reason: "uploading",
|
|
53
|
+
progress: 0,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
41
57
|
describe("BaseComposerRuntimeCore.addAttachment error events", () => {
|
|
42
58
|
it("emits attachmentAddError when no adapter is configured", async () => {
|
|
43
59
|
const composer = makeComposer();
|
|
@@ -128,6 +144,286 @@ describe("BaseComposerRuntimeCore.addAttachment error events", () => {
|
|
|
128
144
|
expect(onError).not.toHaveBeenCalled();
|
|
129
145
|
});
|
|
130
146
|
|
|
147
|
+
it("does not restore an attachment removed while add is still running", async () => {
|
|
148
|
+
let finishUpload!: () => void;
|
|
149
|
+
const uploadPending = new Promise<void>((resolve) => {
|
|
150
|
+
finishUpload = resolve;
|
|
151
|
+
});
|
|
152
|
+
let finishRemoval!: () => void;
|
|
153
|
+
const removalPending = new Promise<void>((resolve) => {
|
|
154
|
+
finishRemoval = resolve;
|
|
155
|
+
});
|
|
156
|
+
const remove = vi.fn(() => removalPending);
|
|
157
|
+
const continueUpload = vi.fn();
|
|
158
|
+
const cleanUpUpload = vi.fn();
|
|
159
|
+
const composer = makeComposer(
|
|
160
|
+
makeAdapter({
|
|
161
|
+
remove,
|
|
162
|
+
async *add({ file }: { file: File }) {
|
|
163
|
+
const attachment = makeUploadingAttachment(file, "att-1");
|
|
164
|
+
try {
|
|
165
|
+
yield attachment;
|
|
166
|
+
await uploadPending;
|
|
167
|
+
yield {
|
|
168
|
+
...attachment,
|
|
169
|
+
status: { type: "requires-action", reason: "composer-send" },
|
|
170
|
+
};
|
|
171
|
+
continueUpload();
|
|
172
|
+
yield attachment;
|
|
173
|
+
} finally {
|
|
174
|
+
cleanUpUpload();
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
const onAdd = vi.fn();
|
|
180
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
181
|
+
|
|
182
|
+
const addTask = composer.addAttachment(
|
|
183
|
+
new File(["x"], "f.png", { type: "image/png" }),
|
|
184
|
+
);
|
|
185
|
+
await vi.waitFor(() => {
|
|
186
|
+
expect(composer.attachments).toHaveLength(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const removeTask = composer.removeAttachment("att-1");
|
|
190
|
+
finishUpload();
|
|
191
|
+
await addTask;
|
|
192
|
+
|
|
193
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
194
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
195
|
+
expect(continueUpload).not.toHaveBeenCalled();
|
|
196
|
+
expect(cleanUpUpload).toHaveBeenCalledTimes(1);
|
|
197
|
+
|
|
198
|
+
finishRemoval();
|
|
199
|
+
await removeTask;
|
|
200
|
+
expect(composer.attachments).toHaveLength(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("settles a pending attachment when adapter removal fails", async () => {
|
|
204
|
+
let finishUpload!: () => void;
|
|
205
|
+
const uploadPending = new Promise<void>((resolve) => {
|
|
206
|
+
finishUpload = resolve;
|
|
207
|
+
});
|
|
208
|
+
const cleanUpUpload = vi.fn();
|
|
209
|
+
const composer = makeComposer(
|
|
210
|
+
makeAdapter({
|
|
211
|
+
remove: vi.fn().mockRejectedValue(new Error("remove failed")),
|
|
212
|
+
async *add({ file }: { file: File }) {
|
|
213
|
+
const attachment = makeUploadingAttachment(file, "att-1");
|
|
214
|
+
try {
|
|
215
|
+
yield attachment;
|
|
216
|
+
await uploadPending;
|
|
217
|
+
yield {
|
|
218
|
+
...attachment,
|
|
219
|
+
status: { type: "requires-action", reason: "composer-send" },
|
|
220
|
+
};
|
|
221
|
+
} finally {
|
|
222
|
+
cleanUpUpload();
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
const onAdd = vi.fn();
|
|
228
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
229
|
+
|
|
230
|
+
const addTask = composer.addAttachment(
|
|
231
|
+
new File(["x"], "f.png", { type: "image/png" }),
|
|
232
|
+
);
|
|
233
|
+
await vi.waitFor(() => {
|
|
234
|
+
expect(composer.attachments).toHaveLength(1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await expect(composer.removeAttachment("att-1")).rejects.toThrow(
|
|
238
|
+
"remove failed",
|
|
239
|
+
);
|
|
240
|
+
expect(composer.attachments[0]?.status).toEqual({
|
|
241
|
+
type: "incomplete",
|
|
242
|
+
reason: "error",
|
|
243
|
+
message: "remove failed",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
finishUpload();
|
|
247
|
+
await addTask;
|
|
248
|
+
|
|
249
|
+
expect(composer.attachments[0]?.status.type).toBe("incomplete");
|
|
250
|
+
expect(cleanUpUpload).toHaveBeenCalledTimes(1);
|
|
251
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("keeps a concurrent add registered before its first yield", async () => {
|
|
255
|
+
let startSlowUpload!: () => void;
|
|
256
|
+
const slowUploadStarted = new Promise<void>((resolve) => {
|
|
257
|
+
startSlowUpload = resolve;
|
|
258
|
+
});
|
|
259
|
+
let finishSlowUpload!: () => void;
|
|
260
|
+
const slowUploadPending = new Promise<void>((resolve) => {
|
|
261
|
+
finishSlowUpload = resolve;
|
|
262
|
+
});
|
|
263
|
+
const composer = makeComposer(
|
|
264
|
+
makeAdapter({
|
|
265
|
+
async *add({ file }: { file: File }) {
|
|
266
|
+
const attachment = makeUploadingAttachment(file, `att-${file.name}`);
|
|
267
|
+
if (file.name === "slow.png") await slowUploadStarted;
|
|
268
|
+
yield attachment;
|
|
269
|
+
if (file.name === "slow.png") await slowUploadPending;
|
|
270
|
+
yield {
|
|
271
|
+
...attachment,
|
|
272
|
+
status: { type: "requires-action", reason: "composer-send" },
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
const onAdd = vi.fn();
|
|
278
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
279
|
+
|
|
280
|
+
const slowAddTask = composer.addAttachment(
|
|
281
|
+
new File(["slow"], "slow.png", { type: "image/png" }),
|
|
282
|
+
);
|
|
283
|
+
await composer.addAttachment(
|
|
284
|
+
new File(["fast"], "fast.png", { type: "image/png" }),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
startSlowUpload();
|
|
288
|
+
await vi.waitFor(() => {
|
|
289
|
+
expect(
|
|
290
|
+
composer.attachments.some(
|
|
291
|
+
(attachment) => attachment.id === "att-slow.png",
|
|
292
|
+
),
|
|
293
|
+
).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
await composer.removeAttachment("att-slow.png");
|
|
296
|
+
finishSlowUpload();
|
|
297
|
+
await slowAddTask;
|
|
298
|
+
|
|
299
|
+
expect(
|
|
300
|
+
composer.attachments.some(
|
|
301
|
+
(attachment) => attachment.id === "att-slow.png",
|
|
302
|
+
),
|
|
303
|
+
).toBe(false);
|
|
304
|
+
expect(onAdd).toHaveBeenCalledTimes(1);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("cancels an add cleared before its first yield", async () => {
|
|
308
|
+
let startUpload!: () => void;
|
|
309
|
+
const uploadPending = new Promise<void>((resolve) => {
|
|
310
|
+
startUpload = resolve;
|
|
311
|
+
});
|
|
312
|
+
const composer = makeComposer(
|
|
313
|
+
makeAdapter({
|
|
314
|
+
async *add({ file }: { file: File }) {
|
|
315
|
+
await uploadPending;
|
|
316
|
+
yield makeUploadingAttachment(file, "att-1");
|
|
317
|
+
},
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
const onAdd = vi.fn();
|
|
321
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
322
|
+
|
|
323
|
+
const addTask = composer.addAttachment(
|
|
324
|
+
new File(["x"], "f.png", { type: "image/png" }),
|
|
325
|
+
);
|
|
326
|
+
await composer.clearAttachments();
|
|
327
|
+
startUpload();
|
|
328
|
+
await addTask;
|
|
329
|
+
|
|
330
|
+
expect(composer.attachments).toHaveLength(0);
|
|
331
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("cancels a promise add cleared before it resolves", async () => {
|
|
335
|
+
let finishUpload!: () => void;
|
|
336
|
+
const uploadPending = new Promise<void>((resolve) => {
|
|
337
|
+
finishUpload = resolve;
|
|
338
|
+
});
|
|
339
|
+
const composer = makeComposer(
|
|
340
|
+
makeAdapter({
|
|
341
|
+
add: async ({ file }: { file: File }) => {
|
|
342
|
+
await uploadPending;
|
|
343
|
+
return makeUploadingAttachment(file, "att-1");
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
347
|
+
const onAdd = vi.fn();
|
|
348
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
349
|
+
|
|
350
|
+
const addTask = composer.addAttachment(
|
|
351
|
+
new File(["x"], "f.png", { type: "image/png" }),
|
|
352
|
+
);
|
|
353
|
+
await composer.clearAttachments();
|
|
354
|
+
finishUpload();
|
|
355
|
+
await addTask;
|
|
356
|
+
|
|
357
|
+
expect(composer.attachments).toHaveLength(0);
|
|
358
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("cancels an add reset before its first yield", async () => {
|
|
362
|
+
let startUpload!: () => void;
|
|
363
|
+
const uploadPending = new Promise<void>((resolve) => {
|
|
364
|
+
startUpload = resolve;
|
|
365
|
+
});
|
|
366
|
+
const composer = makeComposer(
|
|
367
|
+
makeAdapter({
|
|
368
|
+
async *add({ file }: { file: File }) {
|
|
369
|
+
await uploadPending;
|
|
370
|
+
yield makeUploadingAttachment(file, "att-1");
|
|
371
|
+
},
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
const onAdd = vi.fn();
|
|
375
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
376
|
+
|
|
377
|
+
const addTask = composer.addAttachment(
|
|
378
|
+
new File(["x"], "f.png", { type: "image/png" }),
|
|
379
|
+
);
|
|
380
|
+
await composer.reset();
|
|
381
|
+
startUpload();
|
|
382
|
+
await addTask;
|
|
383
|
+
|
|
384
|
+
expect(composer.attachments).toHaveLength(0);
|
|
385
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("cancels every concurrent add sharing an attachment id", async () => {
|
|
389
|
+
let finishUploads!: () => void;
|
|
390
|
+
const uploadsPending = new Promise<void>((resolve) => {
|
|
391
|
+
finishUploads = resolve;
|
|
392
|
+
});
|
|
393
|
+
const firstYieldsConsumed = vi.fn();
|
|
394
|
+
const composer = makeComposer(
|
|
395
|
+
makeAdapter({
|
|
396
|
+
async *add({ file }: { file: File }) {
|
|
397
|
+
const attachment = makeUploadingAttachment(file, "shared");
|
|
398
|
+
yield attachment;
|
|
399
|
+
firstYieldsConsumed();
|
|
400
|
+
await uploadsPending;
|
|
401
|
+
yield {
|
|
402
|
+
...attachment,
|
|
403
|
+
status: { type: "requires-action", reason: "composer-send" },
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
const onAdd = vi.fn();
|
|
409
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
410
|
+
|
|
411
|
+
const addTasks = [
|
|
412
|
+
composer.addAttachment(new File(["a"], "a.png", { type: "image/png" })),
|
|
413
|
+
composer.addAttachment(new File(["b"], "b.png", { type: "image/png" })),
|
|
414
|
+
];
|
|
415
|
+
await vi.waitFor(() => {
|
|
416
|
+
expect(firstYieldsConsumed).toHaveBeenCalledTimes(2);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await composer.removeAttachment("shared");
|
|
420
|
+
finishUploads();
|
|
421
|
+
await Promise.all(addTasks);
|
|
422
|
+
|
|
423
|
+
expect(composer.attachments).toHaveLength(0);
|
|
424
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
425
|
+
});
|
|
426
|
+
|
|
131
427
|
it("adds prepared attachments when File is unavailable", async () => {
|
|
132
428
|
const composer = makeComposer();
|
|
133
429
|
const fileConstructor = globalThis.File;
|
|
@@ -4,11 +4,13 @@ import type { ExternalStoreThreadRuntimeCore } from "../runtimes/external-store/
|
|
|
4
4
|
import type { ExternalStoreThreadListAdapter } from "../runtimes/external-store/external-store-adapter";
|
|
5
5
|
import { ThreadListRuntimeImpl } from "../runtime/api/thread-list-runtime";
|
|
6
6
|
|
|
7
|
-
const makeFactory = () =>
|
|
7
|
+
const makeFactory = (overrides: Record<string, unknown> = {}) =>
|
|
8
8
|
vi.fn(
|
|
9
9
|
() =>
|
|
10
10
|
({
|
|
11
11
|
subscribe: () => () => {},
|
|
12
|
+
capabilities: { cancel: false },
|
|
13
|
+
...overrides,
|
|
12
14
|
}) as unknown as ExternalStoreThreadRuntimeCore,
|
|
13
15
|
);
|
|
14
16
|
|
|
@@ -244,3 +246,39 @@ describe("ExternalStoreThreadListRuntimeCore - switchToThread", () => {
|
|
|
244
246
|
expect(onSwitchToThread).not.toHaveBeenCalled();
|
|
245
247
|
});
|
|
246
248
|
});
|
|
249
|
+
|
|
250
|
+
describe("ExternalStoreThreadListRuntimeCore.reloadMainThread", () => {
|
|
251
|
+
it("dispatches to the main thread's refetch capability", async () => {
|
|
252
|
+
const refetch = vi.fn(async () => {});
|
|
253
|
+
const core = new ExternalStoreThreadListRuntimeCore(
|
|
254
|
+
makeAdapter(),
|
|
255
|
+
makeFactory({ unstable_refetchThread: refetch }),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
await core.reloadMainThread();
|
|
259
|
+
|
|
260
|
+
expect(refetch).toHaveBeenCalledOnce();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("resolves quietly when the adapter declares no refetch capability", async () => {
|
|
264
|
+
const core = new ExternalStoreThreadListRuntimeCore(
|
|
265
|
+
makeAdapter(),
|
|
266
|
+
makeFactory(),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await expect(core.reloadMainThread()).resolves.toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("propagates a refetch failure", async () => {
|
|
273
|
+
const core = new ExternalStoreThreadListRuntimeCore(
|
|
274
|
+
makeAdapter(),
|
|
275
|
+
makeFactory({
|
|
276
|
+
unstable_refetchThread: async () => {
|
|
277
|
+
throw new Error("refetch failed");
|
|
278
|
+
},
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await expect(core.reloadMainThread()).rejects.toThrow("refetch failed");
|
|
283
|
+
});
|
|
284
|
+
});
|
package/src/types/index.ts
CHANGED
package/src/types/message.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type PartProviderMetadata = {
|
|
|
14
14
|
export type TextMessagePart = {
|
|
15
15
|
readonly type: "text";
|
|
16
16
|
readonly text: string;
|
|
17
|
+
readonly status?: MessagePartStreamStatus;
|
|
17
18
|
readonly providerMetadata?: PartProviderMetadata;
|
|
18
19
|
readonly parentId?: string;
|
|
19
20
|
};
|
|
@@ -21,6 +22,7 @@ export type TextMessagePart = {
|
|
|
21
22
|
export type ReasoningMessagePart = {
|
|
22
23
|
readonly type: "reasoning";
|
|
23
24
|
readonly text: string;
|
|
25
|
+
readonly status?: MessagePartStreamStatus;
|
|
24
26
|
readonly providerMetadata?: PartProviderMetadata;
|
|
25
27
|
readonly parentId?: string;
|
|
26
28
|
};
|
|
@@ -60,9 +62,20 @@ export type FileMessagePart = {
|
|
|
60
62
|
readonly filename?: string;
|
|
61
63
|
readonly data: string;
|
|
62
64
|
readonly mimeType: string;
|
|
65
|
+
/** How `data` goes on the wire: a url or id reference; omitted = inferred (http(s) → url, else base64). "url" is honored by the LangChain-family, A2A, AG-UI, and Google ADK runtimes; "id" by the LangChain family only. */
|
|
66
|
+
readonly sourceType?: "url" | "id";
|
|
63
67
|
readonly parentId?: string;
|
|
64
68
|
};
|
|
65
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @deprecated Use {@link FileMessagePart} with an `audio/*` mime type. `file`
|
|
72
|
+
* is the carrier for every non-image binary modality: it is a member of both
|
|
73
|
+
* the user and assistant unions, carries a filename, and can declare how its
|
|
74
|
+
* payload goes on the wire, none of which this shape can express. The payload
|
|
75
|
+
* form a `file` part needs is still adapter specific; see the Part Types
|
|
76
|
+
* section of the message primitive docs. Honored everywhere it is accepted; it
|
|
77
|
+
* will not gain fields.
|
|
78
|
+
*/
|
|
66
79
|
export type Unstable_AudioMessagePart = {
|
|
67
80
|
readonly type: "audio";
|
|
68
81
|
readonly audio: {
|
|
@@ -266,6 +279,23 @@ export type MessagePartStatus =
|
|
|
266
279
|
readonly error?: unknown;
|
|
267
280
|
};
|
|
268
281
|
|
|
282
|
+
export type MessagePartStreamStatus =
|
|
283
|
+
| {
|
|
284
|
+
readonly type: "running";
|
|
285
|
+
}
|
|
286
|
+
| {
|
|
287
|
+
readonly type: "complete";
|
|
288
|
+
}
|
|
289
|
+
| {
|
|
290
|
+
readonly type: "incomplete";
|
|
291
|
+
readonly reason:
|
|
292
|
+
| "cancelled"
|
|
293
|
+
| "length"
|
|
294
|
+
| "content-filter"
|
|
295
|
+
| "other"
|
|
296
|
+
| "error";
|
|
297
|
+
};
|
|
298
|
+
|
|
269
299
|
export type ToolCallMessagePartStatus =
|
|
270
300
|
| {
|
|
271
301
|
/** The tool call is waiting for UI or human input before continuing. */
|
package/src/utils/data-url.ts
CHANGED
|
@@ -7,3 +7,27 @@ export function parseDataUrl(
|
|
|
7
7
|
if (!match) return null;
|
|
8
8
|
return { mimeType: match[1]!.toLowerCase(), data: match[2]! };
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a payload is something `new URL()` accepts. Adapters that place a
|
|
13
|
+
* `FileMessagePart` payload into a url-typed wire field use this to decide
|
|
14
|
+
* whether it has to be wrapped in a data URL envelope first; base64 cannot
|
|
15
|
+
* contain a colon, so a real payload is never misread as a url.
|
|
16
|
+
*/
|
|
17
|
+
export function isParsableUrl(value: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
new URL(value);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The media type a data URL declares, whether or not its payload is base64.
|
|
28
|
+
* `parseDataUrl` only matches base64 payloads because it also returns the
|
|
29
|
+
* bytes; this reads the declaration alone.
|
|
30
|
+
*/
|
|
31
|
+
export function dataUrlMediaType(value: string): string | undefined {
|
|
32
|
+
return /^data:([^;,]+)(?:[;,])/i.exec(value)?.[1]?.toLowerCase();
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getGroupStatus } from "./getGroupStatus";
|
|
3
|
+
|
|
4
|
+
describe("getGroupStatus", () => {
|
|
5
|
+
it("reports running when the first member is running and the last is complete", () => {
|
|
6
|
+
const parts = [
|
|
7
|
+
{ status: { type: "running" as const } },
|
|
8
|
+
{ status: { type: "complete" as const } },
|
|
9
|
+
];
|
|
10
|
+
const status = getGroupStatus(parts);
|
|
11
|
+
|
|
12
|
+
expect(status).toEqual({ type: "running" });
|
|
13
|
+
expect(getGroupStatus(parts, [0, 1])).toBe(status);
|
|
14
|
+
expect(status).toBe(getGroupStatus([{ status: { type: "running" } }]));
|
|
15
|
+
expect(Object.isFrozen(status)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("reports the last status when every member is complete", () => {
|
|
19
|
+
const status = { type: "complete" } as const;
|
|
20
|
+
|
|
21
|
+
expect(getGroupStatus([{ status }, { status }])).toBe(status);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("reports complete for an empty group", () => {
|
|
25
|
+
expect(getGroupStatus([])).toEqual({ type: "complete" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("matches the positional outcome for statusless adapters", () => {
|
|
29
|
+
expect(
|
|
30
|
+
getGroupStatus(
|
|
31
|
+
[{ status: { type: "complete" } }, { status: { type: "running" } }],
|
|
32
|
+
[0, 1],
|
|
33
|
+
),
|
|
34
|
+
).toEqual({ type: "running" });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MessagePartStatus,
|
|
3
|
+
ToolCallMessagePartStatus,
|
|
4
|
+
} from "../types/message";
|
|
5
|
+
import { COMPLETE_STATUS, RUNNING_STATUS } from "./normalizePartStatus";
|
|
6
|
+
|
|
7
|
+
type PartWithStatus = {
|
|
8
|
+
readonly status: MessagePartStatus | ToolCallMessagePartStatus;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getGroupStatus = (
|
|
12
|
+
parts: readonly (PartWithStatus | undefined)[],
|
|
13
|
+
indices?: readonly number[],
|
|
14
|
+
): MessagePartStatus | ToolCallMessagePartStatus => {
|
|
15
|
+
if (indices) {
|
|
16
|
+
for (const index of indices) {
|
|
17
|
+
if (parts[index]?.status.type === "running") return RUNNING_STATUS;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lastIndex = indices.at(-1);
|
|
21
|
+
return lastIndex === undefined
|
|
22
|
+
? COMPLETE_STATUS
|
|
23
|
+
: (parts[lastIndex]?.status ?? COMPLETE_STATUS);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
if (part?.status.type === "running") return RUNNING_STATUS;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return parts.at(-1)?.status ?? COMPLETE_STATUS;
|
|
31
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { detectImageMediaType } from "./image-media-type";
|
|
3
|
+
|
|
4
|
+
const toBase64 = (bytes: number[]) =>
|
|
5
|
+
Buffer.from(Uint8Array.from(bytes)).toString("base64");
|
|
6
|
+
|
|
7
|
+
describe("detectImageMediaType", () => {
|
|
8
|
+
it.each([
|
|
9
|
+
{
|
|
10
|
+
label: "gif",
|
|
11
|
+
bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61],
|
|
12
|
+
type: "image/gif",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
label: "png",
|
|
16
|
+
bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
|
|
17
|
+
type: "image/png",
|
|
18
|
+
},
|
|
19
|
+
{ label: "jpeg", bytes: [0xff, 0xd8, 0xff, 0xe0], type: "image/jpeg" },
|
|
20
|
+
{ label: "bmp", bytes: [0x42, 0x4d, 0x00, 0x00], type: "image/bmp" },
|
|
21
|
+
{
|
|
22
|
+
label: "tiff little endian",
|
|
23
|
+
bytes: [0x49, 0x49, 0x2a, 0x00],
|
|
24
|
+
type: "image/tiff",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "tiff big endian",
|
|
28
|
+
bytes: [0x4d, 0x4d, 0x00, 0x2a],
|
|
29
|
+
type: "image/tiff",
|
|
30
|
+
},
|
|
31
|
+
])("detects $label", ({ bytes, type }) => {
|
|
32
|
+
expect(detectImageMediaType(toBase64(bytes))).toBe(type);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("detects webp across its variable size bytes", () => {
|
|
36
|
+
const bytes = [
|
|
37
|
+
0x52, 0x49, 0x46, 0x46, 0xde, 0xad, 0xbe, 0xef, 0x57, 0x45, 0x42, 0x50,
|
|
38
|
+
];
|
|
39
|
+
expect(detectImageMediaType(toBase64(bytes))).toBe("image/webp");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each([
|
|
43
|
+
{ label: "avif", eighth: [0x61, 0x76, 0x69, 0x66], type: "image/avif" },
|
|
44
|
+
{ label: "heic", eighth: [0x68, 0x65, 0x69, 0x63], type: "image/heic" },
|
|
45
|
+
])("detects $label whatever its ftyp box size", ({ eighth, type }) => {
|
|
46
|
+
for (const size of [0x20, 0x18, 0x1c]) {
|
|
47
|
+
const bytes = [0x00, 0x00, 0x00, size, 0x66, 0x74, 0x79, 0x70, ...eighth];
|
|
48
|
+
expect(detectImageMediaType(toBase64(bytes))).toBe(type);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("without Buffer, as in a browser", () => {
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.unstubAllGlobals();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("decodes through atob", () => {
|
|
58
|
+
// encoded before the stub, since the helper itself uses Buffer
|
|
59
|
+
const jpeg = toBase64([0xff, 0xd8, 0xff, 0xe0]);
|
|
60
|
+
const webp = toBase64([
|
|
61
|
+
0x52, 0x49, 0x46, 0x46, 0xde, 0xad, 0xbe, 0xef, 0x57, 0x45, 0x42, 0x50,
|
|
62
|
+
]);
|
|
63
|
+
vi.stubGlobal("Buffer", undefined);
|
|
64
|
+
|
|
65
|
+
expect(detectImageMediaType(jpeg)).toBe("image/jpeg");
|
|
66
|
+
expect(detectImageMediaType(webp)).toBe("image/webp");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns undefined when atob rejects the input", () => {
|
|
70
|
+
vi.stubGlobal("Buffer", undefined);
|
|
71
|
+
|
|
72
|
+
expect(detectImageMediaType("$$$$")).toBe(undefined);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns undefined for bytes matching no signature", () => {
|
|
77
|
+
expect(detectImageMediaType(toBase64([0x01, 0x02, 0x03, 0x04]))).toBe(
|
|
78
|
+
undefined,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("does not throw on input that is not base64", () => {
|
|
83
|
+
for (const value of [
|
|
84
|
+
"not base64 at all!!",
|
|
85
|
+
"",
|
|
86
|
+
"$$$$",
|
|
87
|
+
"https://x.example",
|
|
88
|
+
]) {
|
|
89
|
+
expect(() => detectImageMediaType(value)).not.toThrow();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns undefined for a payload shorter than any signature", () => {
|
|
94
|
+
expect(detectImageMediaType("/w==")).toBe(undefined);
|
|
95
|
+
});
|
|
96
|
+
});
|