@assistant-ui/react 0.14.28 → 0.14.29

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 (91) hide show
  1. package/dist/client/ExternalThread.d.ts +11 -1
  2. package/dist/client/ExternalThread.d.ts.map +1 -1
  3. package/dist/client/ExternalThread.js +758 -513
  4. package/dist/client/ExternalThread.js.map +1 -1
  5. package/dist/legacy-runtime/cloud/auiV0.d.ts +12 -1
  6. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
  7. package/dist/legacy-runtime/cloud/auiV0.js +2 -1
  8. package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
  9. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +1 -1
  10. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +7 -6
  11. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.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 +30 -15
  14. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
  15. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  16. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +8 -4
  17. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  18. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +1 -0
  19. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +1 -1
  20. package/dist/primitives/actionBar/ActionBarRoot.js +3 -3
  21. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  22. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts +7 -6
  23. package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
  24. package/dist/primitives/actionBar/useActionBarFloatStatus.js +10 -11
  25. package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
  26. package/dist/primitives/attachment/AttachmentRemove.js +1 -0
  27. package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
  28. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -0
  29. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
  30. package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
  31. package/dist/primitives/composer/ComposerAddAttachment.js +8 -2
  32. package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
  33. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  34. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -3
  35. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  36. package/dist/primitives/composer/ComposerInput.js +6 -4
  37. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  38. package/dist/primitives/composer/ComposerSend.js +1 -0
  39. package/dist/primitives/composer/ComposerSend.js.map +1 -1
  40. package/dist/primitives/composer/ComposerStopDictation.js +1 -0
  41. package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
  42. package/dist/primitives/queueItem/QueueItemRemove.js +1 -0
  43. package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
  44. package/dist/primitives/queueItem/QueueItemSteer.js +1 -0
  45. package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
  46. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -0
  47. package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
  48. package/dist/primitives/thread/ThreadScrollToBottom.js +1 -0
  49. package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
  50. package/dist/utils/hooks/useOnResizeContent.js +1 -0
  51. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  52. package/dist/utils/hooks/useSizeHandle.js +1 -0
  53. package/dist/utils/hooks/useSizeHandle.js.map +1 -1
  54. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  55. package/dist/utils/smooth/useSmooth.js +2 -2
  56. package/dist/utils/smooth/useSmooth.js.map +1 -1
  57. package/package.json +9 -9
  58. package/src/client/ExternalThread.ts +340 -53
  59. package/src/legacy-runtime/cloud/auiV0.ts +14 -0
  60. package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +4 -1
  61. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +37 -10
  62. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +79 -0
  63. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +97 -0
  64. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +113 -2
  65. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +10 -2
  66. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +189 -0
  67. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +20 -4
  68. package/src/primitives/actionBar/useActionBarFloatStatus.ts +7 -5
  69. package/src/primitives/composer/ComposerAddAttachment.test.tsx +108 -0
  70. package/src/primitives/composer/ComposerAddAttachment.ts +10 -4
  71. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +3 -2
  72. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +3 -2
  73. package/src/primitives/composer/ComposerInput.test.tsx +98 -1
  74. package/src/primitives/composer/ComposerInput.tsx +10 -8
  75. package/src/tests/DataRenderers.test.tsx +212 -0
  76. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +1 -11
  77. package/src/tests/external-thread-attachments.test.tsx +292 -1
  78. package/src/tests/external-thread-parity.test.tsx +232 -0
  79. package/src/tests/generative-ui.test.tsx +1 -46
  80. package/src/tests/messagePartSwitchRace.test.tsx +25 -11
  81. package/src/tests/messagePartTypeMismatch.test.tsx +2 -2
  82. package/src/tests/threadMessageById.test.tsx +0 -7
  83. package/src/utils/smooth/useSmooth.ts +7 -4
  84. package/src/tests/BaseComposerRuntimeCore.test.ts +0 -621
  85. package/src/tests/BaseSubscribable.test.ts +0 -54
  86. package/src/tests/DataRenderers.test.ts +0 -355
  87. package/src/tests/ExternalStoreThreadRuntimeCore.test.ts +0 -685
  88. package/src/tests/LocalThreadRuntimeCore.test.ts +0 -324
  89. package/src/tests/auiV0Encode.test.ts +0 -188
  90. package/src/tests/external-message-converter.test.ts +0 -346
  91. package/src/tests/shouldContinue.test.ts +0 -122
@@ -0,0 +1,232 @@
1
+ // @vitest-environment jsdom
2
+
3
+ // Legacy-runtime parity: part statuses derive from the message status, and
4
+ // imperative composer call sequences observe writes before React re-renders.
5
+
6
+ import { render, waitFor } from "@testing-library/react";
7
+ import type { FC } from "react";
8
+ import { describe, it, expect, vi } from "vitest";
9
+ import { useAui, AuiProvider } from "@assistant-ui/store";
10
+ import type { ThreadMessage } from "@assistant-ui/core";
11
+ import {
12
+ ExternalThread,
13
+ type ExternalThreadProps,
14
+ type ExternalThreadMessage,
15
+ } from "../client/ExternalThread";
16
+
17
+ const renderThread = (props: ExternalThreadProps) => {
18
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
19
+ const Capture: FC = () => {
20
+ captured.aui = useAui();
21
+ return null;
22
+ };
23
+ const App: FC<{ threadProps: ExternalThreadProps }> = ({ threadProps }) => {
24
+ const aui = useAui({ thread: ExternalThread(threadProps) });
25
+ return (
26
+ <AuiProvider value={aui}>
27
+ <Capture />
28
+ </AuiProvider>
29
+ );
30
+ };
31
+ const utils = render(<App threadProps={props} />);
32
+ return {
33
+ aui: () => captured.aui!,
34
+ rerender: (next: ExternalThreadProps) =>
35
+ utils.rerender(<App threadProps={next} />),
36
+ };
37
+ };
38
+
39
+ const assistantMessage = (
40
+ status: ThreadMessage["status"],
41
+ result?: string,
42
+ ): ExternalThreadMessage =>
43
+ ({
44
+ id: "a1",
45
+ role: "assistant",
46
+ content: [
47
+ { type: "text", text: "let me check" },
48
+ {
49
+ type: "tool-call",
50
+ toolCallId: "tc1",
51
+ toolName: "probe_tool",
52
+ args: {},
53
+ argsText: "{}",
54
+ ...(result !== undefined && { result }),
55
+ },
56
+ ],
57
+ createdAt: new Date(0),
58
+ status,
59
+ metadata: { custom: {} },
60
+ }) as unknown as ExternalThreadMessage;
61
+
62
+ describe("ExternalThread part status", () => {
63
+ it("gives an unresolved tool call its message's status", () => {
64
+ const { aui } = renderThread({
65
+ messages: [assistantMessage({ type: "running" })],
66
+ isRunning: true,
67
+ });
68
+ const part = (toolCallId: string) =>
69
+ aui().thread().message({ id: "a1" }).part({ toolCallId }).getState();
70
+ expect(part("tc1").status).toEqual({ type: "running" });
71
+ });
72
+
73
+ it("marks a resolved tool call complete and streams only the last part", () => {
74
+ const { aui } = renderThread({
75
+ messages: [assistantMessage({ type: "running" }, "ok")],
76
+ isRunning: true,
77
+ });
78
+ const state = aui().thread().message({ id: "a1" }).getState();
79
+ expect(state.parts[0]!.status).toEqual({ type: "complete" });
80
+ expect(state.parts[1]!.status).toEqual({ type: "complete" });
81
+ });
82
+
83
+ it("keeps parts complete on requires-action and user messages", () => {
84
+ const { aui } = renderThread({
85
+ messages: [
86
+ {
87
+ id: "u1",
88
+ role: "user",
89
+ content: [{ type: "text", text: "hi" }],
90
+ createdAt: new Date(0),
91
+ attachments: [],
92
+ metadata: { custom: {} },
93
+ } as unknown as ExternalThreadMessage,
94
+ assistantMessage({ type: "requires-action", reason: "tool-calls" }),
95
+ ],
96
+ isRunning: false,
97
+ });
98
+ expect(
99
+ aui().thread().message({ id: "u1" }).part({ index: 0 }).getState().status,
100
+ ).toEqual({ type: "complete" });
101
+ expect(
102
+ aui()
103
+ .thread()
104
+ .message({ id: "a1" })
105
+ .part({ toolCallId: "tc1" })
106
+ .getState().status,
107
+ ).toEqual({ type: "requires-action", reason: "tool-calls" });
108
+ expect(
109
+ aui().thread().message({ id: "a1" }).part({ index: 0 }).getState().status,
110
+ ).toEqual({ type: "complete" });
111
+ });
112
+ });
113
+
114
+ describe("ExternalThread unset optional callbacks", () => {
115
+ it("throws a capability error when the callback prop is not set", () => {
116
+ const { aui } = renderThread({
117
+ messages: [
118
+ assistantMessage({ type: "requires-action", reason: "tool-calls" }),
119
+ ],
120
+ isRunning: false,
121
+ });
122
+ const part = () =>
123
+ aui().thread().message({ id: "a1" }).part({ toolCallId: "tc1" });
124
+
125
+ expect(() => part().addToolResult("ok")).toThrow(
126
+ "Runtime does not support tool results (onAddToolResult is not set).",
127
+ );
128
+ expect(() => part().resumeToolCall(undefined)).toThrow(
129
+ "Runtime does not support resuming tool calls (onResumeToolCall is not set).",
130
+ );
131
+ expect(() => aui().thread().resumeRun()).toThrow(
132
+ "Runtime does not support resuming runs (onResume is not set).",
133
+ );
134
+ expect(() => aui().thread().importExternalState({})).toThrow(
135
+ "Runtime does not support importing external states (onLoadExternalState is not set).",
136
+ );
137
+ });
138
+ });
139
+
140
+ describe("ExternalThread composer", () => {
141
+ it("dispatches a synchronous setText + send sequence", async () => {
142
+ const onNew = vi.fn();
143
+ const { aui } = renderThread({ messages: [], isRunning: false, onNew });
144
+
145
+ aui().thread().composer().setText("hello");
146
+ aui().thread().composer().send();
147
+
148
+ await waitFor(() => expect(onNew).toHaveBeenCalledTimes(1));
149
+ expect(onNew.mock.calls[0]![0].content).toEqual([
150
+ { type: "text", text: "hello" },
151
+ ]);
152
+ await waitFor(() =>
153
+ expect(aui().thread().getState().composer.text).toBe(""),
154
+ );
155
+ });
156
+
157
+ it("stamps the thread head as parentId on queue-adapter sends", async () => {
158
+ const enqueue = vi.fn();
159
+ const { aui } = renderThread({
160
+ messages: [
161
+ {
162
+ id: "u1",
163
+ role: "user",
164
+ content: [{ type: "text", text: "hi" }],
165
+ createdAt: new Date(0),
166
+ attachments: [],
167
+ metadata: { custom: {} },
168
+ } as unknown as ExternalThreadMessage,
169
+ ],
170
+ isRunning: true,
171
+ queue: {
172
+ items: [],
173
+ enqueue,
174
+ steer: vi.fn(),
175
+ remove: vi.fn(),
176
+ clear: vi.fn(),
177
+ },
178
+ });
179
+
180
+ aui().thread().composer().setText("queued");
181
+ aui().thread().composer().send();
182
+
183
+ await waitFor(() => expect(enqueue).toHaveBeenCalledTimes(1));
184
+ expect(enqueue.mock.calls[0]![0].parentId).toBe("u1");
185
+ });
186
+
187
+ it("still refuses to send an empty composer synchronously after a send", async () => {
188
+ const onNew = vi.fn();
189
+ const { aui } = renderThread({ messages: [], isRunning: false, onNew });
190
+
191
+ aui().thread().composer().send();
192
+ aui().thread().composer().setText("first");
193
+ aui().thread().composer().send();
194
+ aui().thread().composer().send();
195
+
196
+ await new Promise((resolve) => setTimeout(resolve, 0));
197
+ expect(onNew).toHaveBeenCalledTimes(1);
198
+ });
199
+ });
200
+
201
+ describe("ExternalThread duplicate message ids", () => {
202
+ const userMessage = (id: string, text: string): ExternalThreadMessage =>
203
+ ({
204
+ id,
205
+ role: "user",
206
+ content: [{ type: "text", text }],
207
+ createdAt: new Date(0),
208
+ metadata: { custom: {} },
209
+ }) as unknown as ExternalThreadMessage;
210
+
211
+ it("warns and keeps the last occurrence instead of throwing on a duplicate id", () => {
212
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
213
+ try {
214
+ const { aui } = renderThread({
215
+ messages: [
216
+ userMessage("u1", "hi"),
217
+ userMessage("dup", "stale"),
218
+ userMessage("dup", "fresh"),
219
+ ],
220
+ });
221
+
222
+ const state = aui().thread().getState();
223
+ expect(state.messages.map((m) => m.id)).toEqual(["u1", "dup"]);
224
+
225
+ const dup = aui().thread().message({ id: "dup" }).getState();
226
+ expect(dup.parts[0]).toMatchObject({ type: "text", text: "fresh" });
227
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('"dup"'));
228
+ } finally {
229
+ warn.mockRestore();
230
+ }
231
+ });
232
+ });
@@ -66,37 +66,6 @@ describe("MessagePrimitive.GenerativeUI (same-realm renderer)", () => {
66
66
  expect(out).toContain("hello world");
67
67
  });
68
68
 
69
- it("throws GenerativeUIRenderError for unknown components", () => {
70
- const spec: GenerativeUISpec = {
71
- root: { component: "NotAllowed", props: {} },
72
- };
73
-
74
- expect(() =>
75
- renderToStaticMarkup(
76
- <GenerativeUIRender spec={spec} components={{ Card }} />,
77
- ),
78
- ).toThrow(GenerativeUIRenderError);
79
- });
80
-
81
- it("uses Fallback when provided for unknown components", () => {
82
- const spec: GenerativeUISpec = {
83
- root: { component: "NotAllowed", props: { foo: 1 } },
84
- };
85
- const Fallback = ({ component }: any) => (
86
- <span data-fallback={component}>missing</span>
87
- );
88
-
89
- const out = renderToStaticMarkup(
90
- <GenerativeUIRender
91
- spec={spec}
92
- components={{ Card }}
93
- Fallback={Fallback}
94
- />,
95
- );
96
- expect(out).toContain('data-fallback="NotAllowed"');
97
- expect(out).toContain("missing");
98
- });
99
-
100
69
  it("handles empty / partial specs gracefully (stream-friendly)", () => {
101
70
  const out1 = renderToStaticMarkup(
102
71
  <GenerativeUIRender spec={{ root: [] }} components={{ Card }} />,
@@ -133,21 +102,7 @@ describe("MessagePrimitive.GenerativeUI (same-realm renderer)", () => {
133
102
  }
134
103
  });
135
104
 
136
- it("respects user-provided keys", () => {
137
- const spec: GenerativeUISpec = {
138
- root: [
139
- { component: "Card", props: { title: "x" }, key: "stable-1" },
140
- { component: "Card", props: { title: "y" }, key: "stable-2" },
141
- ],
142
- };
143
- expect(() =>
144
- renderToStaticMarkup(
145
- <GenerativeUIRender spec={spec} components={{ Card }} />,
146
- ),
147
- ).not.toThrow();
148
- });
149
-
150
- it("is a typed error subclass with the offending component name", () => {
105
+ it("throws a typed error naming the offending unknown component", () => {
151
106
  try {
152
107
  renderToStaticMarkup(
153
108
  <GenerativeUIRender
@@ -3,7 +3,7 @@
3
3
  import { act, cleanup, render, waitFor } from "@testing-library/react";
4
4
  import { flushSync } from "react-dom";
5
5
  import { useEffect, useState, type FC } from "react";
6
- import { afterEach, describe, expect, it } from "vitest";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
7
  import { useAui, useAuiState } from "@assistant-ui/store";
8
8
  import { AssistantRuntimeProvider, PartByIndexProvider } from "../context";
9
9
  import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
@@ -83,8 +83,12 @@ const ChildrenMessage: FC = () => (
83
83
  );
84
84
 
85
85
  const InvalidPartReader: FC = () => {
86
- useAuiState((s) => s.part.type);
87
- return null;
86
+ const part = useAuiState((s) => s.part);
87
+ return (
88
+ <p data-testid="clamped-part">
89
+ {part.type === "text" ? part.text : part.type}
90
+ </p>
91
+ );
88
92
  };
89
93
 
90
94
  const InvalidPartMessage: FC = () => (
@@ -111,8 +115,8 @@ afterEach(() => {
111
115
  harness.runtime = null;
112
116
  });
113
117
 
114
- describe("part hooks under a thread-switch race (#5118)", () => {
115
- it("an out-of-band flushSync re-render during the swap window does not crash", async () => {
118
+ describe("part hooks under a thread-switch race", () => {
119
+ it("swaps every part when the incoming message has different part types", async () => {
116
120
  let view!: ReturnType<typeof render>;
117
121
  await act(async () => {
118
122
  view = render(<App />);
@@ -148,11 +152,21 @@ describe("part hooks under a thread-switch race (#5118)", () => {
148
152
  },
149
153
  );
150
154
 
151
- it("still rejects an index that was never valid", async () => {
152
- await expect(
153
- act(async () => {
154
- render(<App MessageComponent={InvalidPartMessage} />);
155
- }),
156
- ).rejects.toThrow("useClientLookup: Index 2 out of bounds (length: 2)");
155
+ it("warns and clamps an index that was never valid", async () => {
156
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
157
+ try {
158
+ let view!: ReturnType<typeof render>;
159
+ await act(async () => {
160
+ view = render(<App MessageComponent={InvalidPartMessage} />);
161
+ });
162
+ expect(warn).toHaveBeenCalledWith(
163
+ expect.stringContaining(
164
+ "useClientLookup: Clamped stale index 2 to 1 (length: 2)",
165
+ ),
166
+ );
167
+ expect(view.getByTestId("clamped-part").textContent).toBe("world");
168
+ } finally {
169
+ warn.mockRestore();
170
+ }
157
171
  });
158
172
  });
@@ -69,7 +69,7 @@ const renderToolCallMessage = (renderPart: () => ReactNode) => {
69
69
  );
70
70
  };
71
71
 
72
- describe("part hooks tolerate a transient part-type mismatch (#5118)", () => {
72
+ describe("part hooks tolerate a transient part-type mismatch", () => {
73
73
  it("MessagePartPrimitive.Text renders empty when the part is a tool-call", async () => {
74
74
  renderToolCallMessage(() => (
75
75
  <MessagePartPrimitive.Text data-testid="text" />
@@ -121,7 +121,7 @@ describe("part hooks tolerate a transient part-type mismatch (#5118)", () => {
121
121
  expect(file.textContent).toBe("");
122
122
  });
123
123
 
124
- it("a real text part still renders its text (no regression in the happy path)", async () => {
124
+ it("a real text part still renders its text", async () => {
125
125
  const textMessage: ThreadMessageLike[] = [
126
126
  {
127
127
  role: "assistant",
@@ -215,10 +215,3 @@ describe("ThreadPrimitive.Unstable_MessageById", () => {
215
215
  expect(screen.getByTestId("by-id").textContent).toBe("first");
216
216
  });
217
217
  });
218
-
219
- describe("exports", () => {
220
- it("exposes the unstable id-keyed surface", () => {
221
- expect(typeof unstable_useThreadMessageIds).toBe("function");
222
- expect(ThreadPrimitive.Unstable_MessageById).toBeDefined();
223
- });
224
- });
@@ -58,10 +58,13 @@ class TextStreamAnimator {
58
58
  public maxCharsPerFrame: number = Infinity;
59
59
  public minCommitMs: number = 0;
60
60
 
61
- constructor(
62
- public currentText: string,
63
- private setText: (newText: string) => void,
64
- ) {}
61
+ public currentText: string;
62
+ private setText: (newText: string) => void;
63
+
64
+ constructor(currentText: string, setText: (newText: string) => void) {
65
+ this.currentText = currentText;
66
+ this.setText = setText;
67
+ }
65
68
 
66
69
  start() {
67
70
  if (this.animationFrameId !== null) return;