@assistant-ui/react 0.15.4 → 0.15.6

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 (64) hide show
  1. package/dist/client/ExternalThread.d.ts.map +1 -1
  2. package/dist/client/ExternalThread.js +384 -368
  3. package/dist/client/ExternalThread.js.map +1 -1
  4. package/dist/context/providers/MessageProvider.js +15 -34
  5. package/dist/context/providers/MessageProvider.js.map +1 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +2 -2
  8. package/dist/legacy-runtime/AssistantRuntimeProvider.d.ts +6 -1
  9. package/dist/legacy-runtime/AssistantRuntimeProvider.d.ts.map +1 -1
  10. package/dist/legacy-runtime/AssistantRuntimeProvider.js +10 -8
  11. package/dist/legacy-runtime/AssistantRuntimeProvider.js.map +1 -1
  12. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +1 -1
  13. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +8 -1
  14. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
  15. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +5 -1
  16. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +1 -1
  17. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  18. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +44 -3
  19. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  20. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -1
  21. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  22. package/dist/primitives/attachment/AttachmentThumb.d.ts.map +1 -1
  23. package/dist/primitives/attachment/AttachmentThumb.js +17 -14
  24. package/dist/primitives/attachment/AttachmentThumb.js.map +1 -1
  25. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  26. package/dist/primitives/composer/ComposerAttachmentDropzone.js +64 -50
  27. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  28. package/dist/primitives/composer/ComposerInput.js +1 -0
  29. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  30. package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -1
  31. package/dist/primitives/queueItem/QueueItemText.js +10 -4
  32. package/dist/primitives/queueItem/QueueItemText.js.map +1 -1
  33. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
  34. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -4
  35. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  36. package/dist/utils/createActionButton.js +1 -1
  37. package/dist/utils/createActionButton.js.map +1 -1
  38. package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
  39. package/package.json +16 -14
  40. package/src/client/ExternalThread.ts +30 -17
  41. package/src/context/providers/MessageProvider.tsx +8 -6
  42. package/src/index.ts +5 -1
  43. package/src/legacy-runtime/AssistantRuntimeProvider.tsx +13 -3
  44. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +14 -1
  45. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +38 -0
  46. package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +5 -1
  47. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +7 -0
  48. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +290 -0
  49. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +62 -1
  50. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +1 -1
  51. package/src/primitives/attachment/AttachmentThumb.test.tsx +70 -0
  52. package/src/primitives/attachment/AttachmentThumb.tsx +8 -4
  53. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +103 -3
  54. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +18 -3
  55. package/src/primitives/composer/ComposerInput.test.tsx +53 -2
  56. package/src/primitives/composer/ComposerInput.tsx +3 -0
  57. package/src/primitives/queueItem/QueueItemText.tsx +8 -2
  58. package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +74 -0
  59. package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +3 -3
  60. package/src/tests/external-thread-parity.test.tsx +52 -4
  61. package/src/tests/local-runtime-queue.test.tsx +199 -6
  62. package/src/utils/createActionButton.test.tsx +18 -0
  63. package/src/utils/createActionButton.tsx +1 -1
  64. package/LICENSE +0 -21
@@ -34,7 +34,14 @@ const userTexts = (aui: ReturnType<typeof useAui>) =>
34
34
  m.content.map((p) => (p.type === "text" ? p.text : "")).join(""),
35
35
  );
36
36
 
37
- const renderWithRuntime = (adapter: ChatModelAdapter, enableQueue: boolean) => {
37
+ const renderWithRuntime = (
38
+ adapter: ChatModelAdapter,
39
+ enableQueue: boolean,
40
+ options?: {
41
+ unstable_queueClearOnRewind?: boolean;
42
+ unstable_queueClearOnCancel?: boolean;
43
+ },
44
+ ) => {
38
45
  const captured: { aui?: ReturnType<typeof useAui> } = {};
39
46
  const Capture: FC = () => {
40
47
  captured.aui = useAui();
@@ -43,6 +50,7 @@ const renderWithRuntime = (adapter: ChatModelAdapter, enableQueue: boolean) => {
43
50
  const App: FC = () => {
44
51
  const runtime = useLocalRuntime(adapter, {
45
52
  unstable_enableMessageQueue: enableQueue,
53
+ ...options,
46
54
  });
47
55
  return (
48
56
  <AssistantRuntimeProvider runtime={runtime}>
@@ -114,6 +122,43 @@ describe("local runtime message queue", () => {
114
122
  ).toEqual(["b"]);
115
123
  });
116
124
 
125
+ it("defaults a mid-run send to steer, ahead of an explicitly queued item", async () => {
126
+ const { adapter, releases, getRunCount } = createCountingAdapter();
127
+ const aui = renderWithRuntime(adapter, true);
128
+
129
+ await send(aui, "first");
130
+ expect(getRunCount()).toBe(1);
131
+
132
+ await act(async () => {
133
+ aui.thread.composer().setText("behind");
134
+ aui.thread.composer().send({ steer: false });
135
+ await flush();
136
+ });
137
+ await send(aui, "next");
138
+ expect(
139
+ aui.thread
140
+ .composer()
141
+ .getState()
142
+ .queue.map((q) => q.prompt),
143
+ ).toEqual(["next", "behind"]);
144
+
145
+ await act(async () => {
146
+ releases[0]!();
147
+ await flush();
148
+ await flush();
149
+ });
150
+ expect(getRunCount()).toBe(2);
151
+ expect(userTexts(aui)).toEqual(["first", "next"]);
152
+
153
+ await act(async () => {
154
+ releases[1]!();
155
+ await flush();
156
+ await flush();
157
+ });
158
+ expect(getRunCount()).toBe(3);
159
+ expect(userTexts(aui)).toEqual(["first", "next", "behind"]);
160
+ });
161
+
117
162
  it("queueItem(index).remove() drops a queued message", async () => {
118
163
  const { adapter } = createCountingAdapter();
119
164
  const aui = renderWithRuntime(adapter, true);
@@ -132,7 +177,7 @@ describe("local runtime message queue", () => {
132
177
  expect(queue[0]!.prompt).toBe("b");
133
178
  });
134
179
 
135
- it("clears the queue when the run is cancelled, without flushing", async () => {
180
+ it("clears queued items when the user cancels the run", async () => {
136
181
  const { adapter, getRunCount } = createCountingAdapter();
137
182
  const aui = renderWithRuntime(adapter, true);
138
183
 
@@ -147,13 +192,50 @@ describe("local runtime message queue", () => {
147
192
  await flush();
148
193
  });
149
194
 
195
+ // Stop means stop: nothing pending, nothing dispatched
196
+ expect(getRunCount()).toBe(1);
150
197
  expect(aui.thread.composer().getState().queue).toEqual([]);
151
- // cancelling must not start the next queued message
198
+ });
199
+
200
+ it("keeps queued items on cancel when unstable_queueClearOnCancel is false", async () => {
201
+ const { adapter, getRunCount } = createCountingAdapter();
202
+ const aui = renderWithRuntime(adapter, true, {
203
+ unstable_queueClearOnCancel: false,
204
+ });
205
+
206
+ await send(aui, "first");
207
+ await send(aui, "a");
208
+ await send(aui, "b");
209
+ expect(aui.thread.composer().getState().queue).toHaveLength(2);
210
+
211
+ await act(async () => {
212
+ aui.thread.cancelRun();
213
+ await flush();
214
+ await flush();
215
+ });
216
+
217
+ // cancel pauses the queue: items survive, nothing auto-dispatches
152
218
  expect(getRunCount()).toBe(1);
219
+ expect(
220
+ aui.thread
221
+ .composer()
222
+ .getState()
223
+ .queue.map((q) => q.prompt),
224
+ ).toEqual(["a", "b"]);
225
+
226
+ // the next explicit send drains the head
227
+ await send(aui, "c");
228
+ expect(getRunCount()).toBe(2);
229
+ expect(
230
+ aui.thread
231
+ .composer()
232
+ .getState()
233
+ .queue.map((q) => q.prompt),
234
+ ).toEqual(["b", "c"]);
153
235
  });
154
236
 
155
- it("applies an edit instead of queuing it, dropping pending items", async () => {
156
- const { adapter, releases } = createCountingAdapter();
237
+ it("applies an edit instead of queuing it and clears the pending items", async () => {
238
+ const { adapter, releases, getRunCount } = createCountingAdapter();
157
239
  const aui = renderWithRuntime(adapter, true);
158
240
 
159
241
  await send(aui, "first");
@@ -174,10 +256,52 @@ describe("local runtime message queue", () => {
174
256
  message.composer().setText("edited");
175
257
  message.composer().send();
176
258
  await flush();
259
+ await flush();
260
+ });
261
+
262
+ // the edit is applied (branches the thread); the rewind clears the queue,
263
+ // so nothing pending dispatches against the new branch
264
+ expect(aui.thread.composer().getState().queue).toEqual([]);
265
+ expect(getRunCount()).toBe(3); // first, second, edit — not "queued"
266
+ });
267
+
268
+ it("keeps the queue across an edit when unstable_queueClearOnRewind is false", async () => {
269
+ const { adapter, releases, getRunCount } = createCountingAdapter();
270
+ const aui = renderWithRuntime(adapter, true, {
271
+ unstable_queueClearOnRewind: false,
272
+ });
273
+
274
+ await send(aui, "first");
275
+ await act(async () => {
276
+ releases[0]!();
277
+ await flush();
278
+ });
279
+
280
+ await send(aui, "second");
281
+ await send(aui, "queued");
282
+ expect(aui.thread.composer().getState().queue).toHaveLength(1);
283
+
284
+ await act(async () => {
285
+ const message = aui.thread.message({ index: 0 });
286
+ message.composer().beginEdit();
287
+ message.composer().setText("edited");
288
+ message.composer().send();
289
+ await flush();
290
+ });
291
+
292
+ // the edit's rerun survives; the queued item waits for it to settle
293
+ expect(getRunCount()).toBe(3);
294
+ expect(aui.thread.composer().getState().queue).toHaveLength(1);
295
+
296
+ await act(async () => {
297
+ releases[2]!();
298
+ await flush();
177
299
  });
178
300
 
179
- // the edit is applied (branches the thread) and the stale queue is cleared
301
+ // the surviving queue drains after the edit's run completes
302
+ expect(getRunCount()).toBe(4);
180
303
  expect(aui.thread.composer().getState().queue).toEqual([]);
304
+ expect(userTexts(aui)).toEqual(["edited", "queued"]);
181
305
  });
182
306
 
183
307
  it("buffers a send during a regenerate instead of interrupting it", async () => {
@@ -214,6 +338,75 @@ describe("local runtime message queue", () => {
214
338
  ).toEqual(["Y"]);
215
339
  });
216
340
 
341
+ it("drains the pending head after a regenerate started inside the cancellation window", async () => {
342
+ const { adapter, releases, getRunCount } = createCountingAdapter();
343
+ const aui = renderWithRuntime(adapter, true, {
344
+ unstable_queueClearOnCancel: false,
345
+ });
346
+
347
+ await send(aui, "first");
348
+ await act(async () => {
349
+ releases[0]!();
350
+ await flush();
351
+ });
352
+
353
+ // regenerate, buffer a send, then cancel and regenerate again before
354
+ // the cancelled settle lands
355
+ await act(async () => {
356
+ aui.thread.message({ index: 1 }).reload();
357
+ await flush();
358
+ });
359
+ await send(aui, "pending");
360
+ await act(async () => {
361
+ aui.thread.cancelRun();
362
+ aui.thread.message({ index: 1 }).reload();
363
+ await flush();
364
+ });
365
+ expect(getRunCount()).toBe(3);
366
+ expect(aui.thread.composer().getState().queue).toHaveLength(1);
367
+
368
+ // the cancelled settle must not eat the replacement's: once the
369
+ // replacement settles, the pending head drains exactly once
370
+ await act(async () => {
371
+ releases[2]!();
372
+ await flush();
373
+ });
374
+ expect(getRunCount()).toBe(4);
375
+ expect(aui.thread.composer().getState().queue).toEqual([]);
376
+ });
377
+
378
+ it("keeps a second regenerate alive when the first one's settle arrives", async () => {
379
+ const { adapter, releases, getRunCount } = createCountingAdapter();
380
+ const aui = renderWithRuntime(adapter, true);
381
+
382
+ await send(aui, "first");
383
+ await act(async () => {
384
+ releases[0]!();
385
+ await flush();
386
+ });
387
+
388
+ await act(async () => {
389
+ aui.thread.message({ index: 1 }).reload();
390
+ await flush();
391
+ });
392
+ await send(aui, "Y");
393
+
394
+ // the second regenerate aborts the first; its settle must not dispatch
395
+ await act(async () => {
396
+ aui.thread.message({ index: 1 }).reload();
397
+ await flush();
398
+ });
399
+ expect(getRunCount()).toBe(3);
400
+ expect(aui.thread.composer().getState().queue).toHaveLength(1);
401
+
402
+ await act(async () => {
403
+ releases[2]!();
404
+ await flush();
405
+ });
406
+ expect(getRunCount()).toBe(4);
407
+ expect(aui.thread.composer().getState().queue).toEqual([]);
408
+ });
409
+
217
410
  it("advances exactly once after a failed run, without deadlocking", async () => {
218
411
  const releases: Array<() => void> = [];
219
412
  let runCount = 0;
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { createActionButton } from "./createActionButton";
4
+
5
+ const TestButton = createActionButton("TestButton", () => () => {});
6
+
7
+ describe("createActionButton", () => {
8
+ it("defaults to type=button", () => {
9
+ const html = renderToStaticMarkup(<TestButton />);
10
+ expect(html).toContain('type="button"');
11
+ });
12
+
13
+ it("allows the caller to override type", () => {
14
+ const html = renderToStaticMarkup(<TestButton type="submit" />);
15
+ expect(html).toContain('type="submit"');
16
+ expect(html).not.toContain('type="button"');
17
+ });
18
+ });
@@ -41,8 +41,8 @@ export const createActionButton = <TProps,>(
41
41
  const callback = useActionButton(forwardedProps as TProps) ?? undefined;
42
42
  return (
43
43
  <Primitive.button
44
- {...primitiveProps}
45
44
  type="button"
45
+ {...primitiveProps}
46
46
  ref={forwardedRef}
47
47
  disabled={primitiveProps.disabled || !callback}
48
48
  onClick={composeEventHandlers(primitiveProps.onClick, callback)}
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 AgentbaseAI Inc.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.