@assistant-ui/core 0.3.11 → 0.3.13

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 (132) hide show
  1. package/dist/index.d.ts +6 -5
  2. package/dist/index.js +2 -2
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal.d.ts +6 -5
  5. package/dist/internal.js +5 -4
  6. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  7. package/dist/react/client/interactable-model-context.js +5 -1
  8. package/dist/react/client/interactable-model-context.js.map +1 -1
  9. package/dist/runtime/api/message-runtime.d.ts +2 -3
  10. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  11. package/dist/runtime/api/message-runtime.js +2 -16
  12. package/dist/runtime/api/message-runtime.js.map +1 -1
  13. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  14. package/dist/runtime/api/thread-runtime.js +5 -1
  15. package/dist/runtime/api/thread-runtime.js.map +1 -1
  16. package/dist/runtime/base/base-composer-runtime-core.d.ts +25 -0
  17. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  18. package/dist/runtime/base/base-composer-runtime-core.js +64 -11
  19. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  20. package/dist/runtime/base/base-thread-runtime-core.d.ts +14 -0
  21. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  22. package/dist/runtime/base/base-thread-runtime-core.js +31 -0
  23. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  24. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  25. package/dist/runtime/base/default-edit-composer-runtime-core.js +1 -6
  26. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  27. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  28. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -4
  29. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  30. package/dist/runtime/internal.d.ts +4 -4
  31. package/dist/runtime/internal.js +4 -4
  32. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +15 -0
  33. package/dist/runtime/queue/external-thread-queue-adapter.d.ts.map +1 -1
  34. package/dist/runtime/queue/message-queue.js +23 -10
  35. package/dist/runtime/queue/message-queue.js.map +1 -1
  36. package/dist/runtimes/external-store/external-store-adapter.d.ts +7 -0
  37. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  38. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +3 -1
  39. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  40. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +49 -13
  41. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  42. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  43. package/dist/runtimes/local/local-thread-runtime-core.js +2 -1
  44. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  45. package/dist/store/clients/external-thread.d.ts +61 -0
  46. package/dist/store/clients/external-thread.d.ts.map +1 -0
  47. package/dist/store/clients/external-thread.js +1445 -0
  48. package/dist/store/clients/external-thread.js.map +1 -0
  49. package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
  50. package/dist/store/clients/runtime-adapter.js +5 -2
  51. package/dist/store/clients/runtime-adapter.js.map +1 -1
  52. package/dist/store/clients/single-thread-list.d.ts +9 -0
  53. package/dist/store/clients/single-thread-list.d.ts.map +1 -0
  54. package/dist/store/clients/single-thread-list.js +175 -0
  55. package/dist/store/clients/single-thread-list.js.map +1 -0
  56. package/dist/store/clients/suggestions.d.ts +3 -1
  57. package/dist/store/clients/suggestions.d.ts.map +1 -1
  58. package/dist/store/clients/suggestions.js +65 -37
  59. package/dist/store/clients/suggestions.js.map +1 -1
  60. package/dist/store/index.d.ts +5 -3
  61. package/dist/store/index.js +4 -2
  62. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  63. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +64 -50
  64. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  65. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  66. package/dist/store/runtime-clients/thread-runtime-client.js +98 -82
  67. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  68. package/dist/store/scope-registration.d.ts +2 -2
  69. package/dist/store/scopes/suggestions.d.ts +6 -1
  70. package/dist/store/scopes/suggestions.d.ts.map +1 -1
  71. package/dist/store/scopes/thread.d.ts +17 -0
  72. package/dist/store/scopes/thread.d.ts.map +1 -1
  73. package/dist/types/augmentations.d.ts +33 -0
  74. package/dist/types/augmentations.d.ts.map +1 -0
  75. package/dist/types/augmentations.js +0 -0
  76. package/dist/types/error.d.ts +15 -1
  77. package/dist/types/error.d.ts.map +1 -1
  78. package/dist/types/error.js +18 -1
  79. package/dist/types/error.js.map +1 -1
  80. package/dist/types/message.d.ts +2 -0
  81. package/dist/types/message.d.ts.map +1 -1
  82. package/dist/types/message.js.map +1 -1
  83. package/dist/utils/normalizePartStatus.d.ts +3 -2
  84. package/dist/utils/normalizePartStatus.d.ts.map +1 -1
  85. package/dist/utils/normalizePartStatus.js +15 -1
  86. package/dist/utils/normalizePartStatus.js.map +1 -1
  87. package/package.json +5 -5
  88. package/src/index.ts +7 -0
  89. package/src/internal.ts +1 -1
  90. package/src/react/client/interactable-model-context.ts +6 -1
  91. package/src/react/runtimes/useExternalStoreRuntime.suggestions.test.tsx +114 -0
  92. package/src/runtime/api/message-runtime.test.ts +1 -1
  93. package/src/runtime/api/message-runtime.ts +2 -35
  94. package/src/runtime/api/thread-runtime.ts +8 -1
  95. package/src/runtime/base/base-composer-runtime-core.ts +100 -8
  96. package/src/runtime/base/base-thread-runtime-core.ts +36 -0
  97. package/src/runtime/base/default-edit-composer-runtime-core.ts +1 -15
  98. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -11
  99. package/src/runtime/queue/external-thread-queue-adapter.ts +17 -0
  100. package/src/runtime/queue/message-queue.ts +26 -9
  101. package/src/runtimes/external-store/external-store-adapter.ts +7 -0
  102. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +100 -26
  103. package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
  104. package/src/store/clients/external-thread.ts +1255 -0
  105. package/src/store/clients/runtime-adapter.ts +5 -2
  106. package/src/store/clients/single-thread-list.ts +116 -0
  107. package/src/store/clients/suggestions.ts +39 -13
  108. package/src/store/index.ts +6 -0
  109. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -28
  110. package/src/store/runtime-clients/thread-runtime-client.ts +5 -0
  111. package/src/store/scopes/suggestions.ts +6 -0
  112. package/src/store/scopes/thread.ts +17 -0
  113. package/src/tests/append-interactable-snapshots.test.ts +350 -0
  114. package/src/tests/base-composer-runtime-core-send.test.ts +126 -0
  115. package/src/tests/base-composer-runtime-core.test.ts +108 -1
  116. package/src/tests/default-edit-composer-runtime-core.test.ts +0 -68
  117. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +300 -0
  118. package/src/tests/external-thread-attachments.test.tsx +584 -0
  119. package/src/tests/external-thread-branches.test.tsx +157 -0
  120. package/src/tests/external-thread-feedback.test.tsx +204 -0
  121. package/src/tests/external-thread-parity.test.tsx +548 -0
  122. package/src/tests/external-thread-refetch.test.tsx +117 -0
  123. package/src/tests/external-thread-speech.test.tsx +328 -0
  124. package/src/tests/external-thread-suggestions.test.tsx +59 -0
  125. package/src/tests/message-queue.test.ts +26 -0
  126. package/src/tests/thread-message-like.test.ts +39 -0
  127. package/src/tests/thread-switch-events.test.tsx +136 -0
  128. package/src/types/augmentations.ts +33 -0
  129. package/src/types/error.ts +24 -0
  130. package/src/types/message.ts +2 -0
  131. package/src/utils/normalizePartStatus.ts +27 -0
  132. package/src/tests/default-thread-composer-runtime-core.test.ts +0 -82
@@ -6,6 +6,8 @@ import {
6
6
  import type { ExternalStoreAdapter } from "../runtimes/external-store/external-store-adapter";
7
7
  import type { ModelContextProvider } from "../model-context/types";
8
8
  import type { AppendMessage, ThreadMessage } from "../types/message";
9
+ import { createMessageQueue } from "../runtime/queue/message-queue";
10
+ import { getThreadMessageText } from "../utils/text";
9
11
 
10
12
  const createContextProvider = (): ModelContextProvider => ({
11
13
  getModelContext: () => ({}),
@@ -235,6 +237,175 @@ describe("ExternalStoreThreadRuntimeCore adapter contract", () => {
235
237
  expect(lastCall.map((m) => m.id)).toContain("server-msg");
236
238
  });
237
239
 
240
+ it("does not revert a store update that lands before the resync flushes", async () => {
241
+ const optimisticAssistant = {
242
+ ...createAssistantMessage("server-msg", "partial answer"),
243
+ status: { type: "running" as const },
244
+ metadata: {
245
+ unstable_state: null,
246
+ unstable_annotations: [],
247
+ unstable_data: [],
248
+ steps: [],
249
+ custom: {},
250
+ isOptimistic: true,
251
+ },
252
+ } as ThreadMessage;
253
+ const setMessages = vi.fn();
254
+ const adapter = createBaseAdapter({
255
+ messages: [createUserMessage("u1"), optimisticAssistant],
256
+ isRunning: true,
257
+ onCancel: vi.fn(),
258
+ setMessages,
259
+ });
260
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
261
+
262
+ core.cancelRun();
263
+
264
+ // The store settles the cancelled turn and re-supplies it in the same
265
+ // tick as the cancel.
266
+ core.__internal_setAdapter(
267
+ createBaseAdapter({
268
+ messages: [
269
+ createUserMessage("u1"),
270
+ createAssistantMessage("server-msg", "partial answer (stopped)"),
271
+ ],
272
+ isRunning: false,
273
+ onCancel: vi.fn(),
274
+ setMessages,
275
+ }),
276
+ );
277
+
278
+ await new Promise((resolve) => setTimeout(resolve, 0));
279
+ expect(setMessages).toHaveBeenCalled();
280
+ const lastCall = setMessages.mock.lastCall?.[0] as ThreadMessage[];
281
+ const texts = lastCall.map(getThreadMessageText);
282
+ expect(texts).toContain("partial answer (stopped)");
283
+ expect(texts).not.toContain("partial answer");
284
+ });
285
+
286
+ it("re-applies the user leaf rollback when the store updates before the flush", async () => {
287
+ const messages = [createUserMessage("u1", "cancel me")];
288
+ const setMessages = vi.fn();
289
+ const adapter = createBaseAdapter({
290
+ messages,
291
+ isRunning: true,
292
+ onCancel: vi.fn(),
293
+ setMessages,
294
+ });
295
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
296
+
297
+ core.cancelRun();
298
+
299
+ // The host flips isRunning after onCancel; the store still holds the
300
+ // rolled-back message because only the deferred resync removes it.
301
+ core.__internal_setAdapter(
302
+ createBaseAdapter({
303
+ messages,
304
+ isRunning: false,
305
+ onCancel: vi.fn(),
306
+ setMessages,
307
+ }),
308
+ );
309
+
310
+ await new Promise((resolve) => setTimeout(resolve, 0));
311
+ expect(core.composer.text).toBe("cancel me");
312
+ expect(setMessages).toHaveBeenCalled();
313
+ const lastCall = setMessages.mock.lastCall?.[0] as ThreadMessage[];
314
+ expect(lastCall.map((m) => m.id)).not.toContain("u1");
315
+ expect(core.export().messages.map((m) => m.message.id)).not.toContain(
316
+ "u1",
317
+ );
318
+ });
319
+
320
+ it("keeps a rolled-back user leaf the store answered in the gap and takes the draft back", async () => {
321
+ const setMessages = vi.fn();
322
+ const adapter = createBaseAdapter({
323
+ messages: [createUserMessage("u1", "cancel me")],
324
+ isRunning: true,
325
+ onCancel: vi.fn(),
326
+ setMessages,
327
+ });
328
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
329
+
330
+ core.cancelRun();
331
+ expect(core.composer.text).toBe("cancel me");
332
+
333
+ core.__internal_setAdapter(
334
+ createBaseAdapter({
335
+ messages: [
336
+ createUserMessage("u1", "cancel me"),
337
+ createAssistantMessage("a1", "answered anyway"),
338
+ ],
339
+ isRunning: false,
340
+ onCancel: vi.fn(),
341
+ setMessages,
342
+ }),
343
+ );
344
+
345
+ await new Promise((resolve) => setTimeout(resolve, 0));
346
+ expect(core.composer.text).toBe("");
347
+ const lastCall = setMessages.mock.lastCall?.[0] as ThreadMessage[];
348
+ expect(lastCall.map((m) => m.id)).toEqual(["u1", "a1"]);
349
+ });
350
+
351
+ it("leaves an edited draft alone when the store answered in the gap", async () => {
352
+ const setMessages = vi.fn();
353
+ const adapter = createBaseAdapter({
354
+ messages: [createUserMessage("u1", "cancel me")],
355
+ isRunning: true,
356
+ onCancel: vi.fn(),
357
+ setMessages,
358
+ });
359
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
360
+
361
+ core.cancelRun();
362
+ core.composer.setText("edited");
363
+
364
+ core.__internal_setAdapter(
365
+ createBaseAdapter({
366
+ messages: [
367
+ createUserMessage("u1", "cancel me"),
368
+ createAssistantMessage("a1", "answered anyway"),
369
+ ],
370
+ isRunning: false,
371
+ onCancel: vi.fn(),
372
+ setMessages,
373
+ }),
374
+ );
375
+
376
+ await new Promise((resolve) => setTimeout(resolve, 0));
377
+ expect(core.composer.text).toBe("edited");
378
+ const lastCall = setMessages.mock.lastCall?.[0] as ThreadMessage[];
379
+ expect(lastCall.map((m) => m.id)).toEqual(["u1", "a1"]);
380
+ });
381
+
382
+ it("drops a placeholder regenerated between cancel and flush", async () => {
383
+ const setMessages = vi.fn();
384
+ const adapter = createBaseAdapter({
385
+ messages: [createUserMessage("u1", "cancel me")],
386
+ isRunning: true,
387
+ onCancel: vi.fn(),
388
+ setMessages,
389
+ });
390
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
391
+
392
+ core.cancelRun();
393
+
394
+ core.__internal_setAdapter(
395
+ createBaseAdapter({
396
+ messages: [createUserMessage("u1", "cancel me")],
397
+ isRunning: true,
398
+ onCancel: vi.fn(),
399
+ setMessages,
400
+ }),
401
+ );
402
+
403
+ await new Promise((resolve) => setTimeout(resolve, 0));
404
+ expect(core.composer.text).toBe("cancel me");
405
+ const lastCall = setMessages.mock.lastCall?.[0] as ThreadMessage[];
406
+ expect(lastCall).toEqual([]);
407
+ });
408
+
238
409
  it("evicts an empty optimistic head on cancel", async () => {
239
410
  const optimisticAssistant = {
240
411
  ...createAssistantMessage("server-msg", ""),
@@ -264,6 +435,135 @@ describe("ExternalStoreThreadRuntimeCore adapter contract", () => {
264
435
  const lastCall = setMessages.mock.lastCall?.[0] as ThreadMessage[];
265
436
  expect(lastCall.map((m) => m.id)).not.toContain("server-msg");
266
437
  });
438
+
439
+ it("pauses a queue instead of dispatching the next message", async () => {
440
+ const dispatched: string[] = [];
441
+ const queue = createMessageQueue({
442
+ run: (message) => {
443
+ dispatched.push(getThreadMessageText(message));
444
+ },
445
+ });
446
+ const adapter = createBaseAdapter({
447
+ isRunning: true,
448
+ onCancel: vi.fn(),
449
+ queue: queue.adapter,
450
+ });
451
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
452
+ const appendAtTail = (text: string) =>
453
+ core.append({
454
+ role: "user",
455
+ content: [{ type: "text", text }],
456
+ attachments: [],
457
+ createdAt: new Date(),
458
+ parentId: core.messages.at(-1)?.id ?? null,
459
+ sourceId: null,
460
+ runConfig: undefined,
461
+ metadata: { custom: {} },
462
+ } as AppendMessage);
463
+
464
+ await appendAtTail("first");
465
+ await appendAtTail("queued");
466
+
467
+ core.cancelRun();
468
+ // the aborted run settles; a queue host reports that the same way it
469
+ // reports any other run ending
470
+ queue.notifyIdle();
471
+
472
+ expect(dispatched).toEqual(["first"]);
473
+ expect(queue.adapter.steerItems.map((item) => item.prompt)).toEqual([
474
+ "queued",
475
+ ]);
476
+ });
477
+
478
+ it("keeps a trailing user message out of the composer without setMessages", async () => {
479
+ const userMessage = createUserMessage("u1", "Do not duplicate me");
480
+ const adapter = createBaseAdapter({
481
+ messages: [userMessage],
482
+ isRunning: true,
483
+ onCancel: vi.fn(),
484
+ });
485
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
486
+
487
+ core.cancelRun();
488
+
489
+ await new Promise((resolve) => setTimeout(resolve, 0));
490
+ expect(core.composer.text).toBe("");
491
+ expect(core.export().messages.map((m) => m.message.id)).toContain("u1");
492
+ });
493
+
494
+ it("keeps the message in the thread when the composer is busy", async () => {
495
+ const adapter = createBaseAdapter({
496
+ messages: [createUserMessage("u1", "cancel me")],
497
+ isRunning: true,
498
+ onCancel: vi.fn(),
499
+ setMessages: vi.fn(),
500
+ });
501
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
502
+ core.composer.setText("something else");
503
+
504
+ core.cancelRun();
505
+
506
+ await new Promise((resolve) => setTimeout(resolve, 0));
507
+ expect(core.composer.text).toBe("something else");
508
+ expect(core.export().messages.map((m) => m.message.id)).toContain("u1");
509
+ });
510
+
511
+ it("keeps a message with non-text content in the thread", async () => {
512
+ const userMessage = {
513
+ ...createUserMessage("u1", "look at this"),
514
+ content: [
515
+ { type: "text" as const, text: "look at this" },
516
+ { type: "image" as const, image: "https://example.com/cat.png" },
517
+ ],
518
+ } as ThreadMessage;
519
+ const adapter = createBaseAdapter({
520
+ messages: [userMessage],
521
+ isRunning: true,
522
+ onCancel: vi.fn(),
523
+ setMessages: vi.fn(),
524
+ });
525
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
526
+
527
+ core.cancelRun();
528
+
529
+ await new Promise((resolve) => setTimeout(resolve, 0));
530
+ expect(core.composer.text).toBe("");
531
+ expect(core.export().messages.map((m) => m.message.id)).toContain("u1");
532
+ });
533
+
534
+ it("restores the message whole when the composer is free", async () => {
535
+ const attachment = {
536
+ id: "a1",
537
+ type: "file" as const,
538
+ name: "spec.pdf",
539
+ contentType: "application/pdf",
540
+ status: { type: "complete" as const },
541
+ content: [],
542
+ };
543
+ const quote = { text: "quoted", messageId: "m-1" };
544
+ const userMessage = {
545
+ ...createUserMessage("u1", "cancel me"),
546
+ attachments: [attachment],
547
+ metadata: { custom: { quote } },
548
+ } as ThreadMessage;
549
+ const adapter = createBaseAdapter({
550
+ messages: [userMessage],
551
+ isRunning: true,
552
+ onCancel: vi.fn(),
553
+ setMessages: vi.fn(),
554
+ });
555
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
556
+
557
+ core.cancelRun();
558
+
559
+ await new Promise((resolve) => setTimeout(resolve, 0));
560
+ expect(core.composer.text).toBe("cancel me");
561
+ expect(core.composer.attachments).toEqual([attachment]);
562
+ expect(core.composer.quote).toEqual(quote);
563
+ expect(core.export().messages.map((m) => m.message.id)).not.toContain(
564
+ "u1",
565
+ );
566
+ });
267
567
  });
268
568
 
269
569
  describe("optimistic assistant message", () => {