@assistant-ui/react 0.15.14 → 0.15.16

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 (210) hide show
  1. package/dist/assistant-transport.d.ts +32 -0
  2. package/dist/assistant-transport.d.ts.map +1 -0
  3. package/dist/assistant-transport.js +27 -0
  4. package/dist/assistant-transport.js.map +1 -0
  5. package/dist/context/providers/MessageProvider.js +1 -1
  6. package/dist/devtools/DevToolsHooks.d.ts +0 -2
  7. package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
  8. package/dist/devtools/DevToolsHooks.js +10 -12
  9. package/dist/devtools/DevToolsHooks.js.map +1 -1
  10. package/dist/hooks/useMessageQuote.js +1 -1
  11. package/dist/hooks/useToolCallElapsed.js +1 -1
  12. package/dist/index.d.ts +4 -7
  13. package/dist/index.js +6 -8
  14. package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
  15. package/dist/mcp-apps/McpAppRenderer.js +1 -1
  16. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  17. package/dist/mcp-apps/McpAppsRemoteHost.js +18 -2
  18. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  19. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  20. package/dist/mcp-apps/bridge.js +33 -24
  21. package/dist/mcp-apps/bridge.js.map +1 -1
  22. package/dist/model-context/makeAssistantVisible.js +1 -1
  23. package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
  24. package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
  25. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +5 -2
  26. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  27. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
  28. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
  29. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +24 -37
  31. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  32. package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -1
  33. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
  34. package/dist/primitives/attachment/AttachmentName.js +1 -1
  35. package/dist/primitives/attachment/AttachmentRemove.js +1 -1
  36. package/dist/primitives/attachment/AttachmentThumb.js +1 -1
  37. package/dist/primitives/branchPicker/BranchPickerCount.js +1 -1
  38. package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -1
  39. package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
  40. package/dist/primitives/branchPicker/BranchPickerRoot.js +20 -15
  41. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  42. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
  43. package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
  44. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  45. package/dist/primitives/composer/ComposerDictationTranscript.d.ts +2 -2
  46. package/dist/primitives/composer/ComposerDictationTranscript.js +3 -3
  47. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  48. package/dist/primitives/composer/ComposerInput.js +2 -2
  49. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  50. package/dist/primitives/composer/ComposerQuote.js +1 -1
  51. package/dist/primitives/composer/ComposerRoot.js +1 -1
  52. package/dist/primitives/composer/ComposerStopDictation.js +1 -1
  53. package/dist/primitives/composer/trigger/TriggerPopover.js +1 -1
  54. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  55. package/dist/primitives/composer/trigger/triggerSelectionResource.js +24 -20
  56. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  57. package/dist/primitives/composer/useComposerInputState.js +1 -1
  58. package/dist/primitives/message/MessageIf.js +1 -1
  59. package/dist/primitives/message/MessagePartsGrouped.js +1 -1
  60. package/dist/primitives/message/MessageRoot.js +1 -1
  61. package/dist/primitives/message.js +1 -1
  62. package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
  63. package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
  64. package/dist/primitives/queueItem/QueueItemText.js +1 -1
  65. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
  66. package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
  67. package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
  68. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  69. package/dist/primitives/thread/ThreadIf.js +1 -1
  70. package/dist/primitives/thread/ThreadRoot.d.ts +3 -0
  71. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  72. package/dist/primitives/thread/ThreadRoot.js +44 -9
  73. package/dist/primitives/thread/ThreadRoot.js.map +1 -1
  74. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  75. package/dist/primitives/thread/ThreadViewport.js +2 -13
  76. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  77. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  78. package/dist/primitives/thread/useThreadViewportAutoScroll.js +23 -8
  79. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  80. package/dist/primitives/threadList/ThreadListNew.js +1 -1
  81. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  82. package/dist/sandbox-host/SandboxHost.js +6 -1
  83. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  84. package/dist/unstable/useComposerInput.d.ts +2 -2
  85. package/dist/unstable/useComposerInput.js +1 -1
  86. package/dist/unstable/useComposerInput.js.map +1 -1
  87. package/dist/unstable/useComposerInputHistory.js +2 -2
  88. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  89. package/dist/unstable/useLiveCompletionAdapter.d.ts +5 -0
  90. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  91. package/dist/unstable/useLiveCompletionAdapter.js +16 -3
  92. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  93. package/dist/unstable/useMentionAdapter.d.ts +1 -1
  94. package/dist/utils/invokeCallbackSafely.d.ts +5 -0
  95. package/dist/utils/invokeCallbackSafely.d.ts.map +1 -0
  96. package/dist/utils/invokeCallbackSafely.js +15 -0
  97. package/dist/utils/invokeCallbackSafely.js.map +1 -0
  98. package/dist/utils/smooth/SmoothContext.d.ts +6 -6
  99. package/dist/utils/smooth/SmoothContext.js +1 -1
  100. package/dist/utils/smooth/useSmooth.d.ts +3 -0
  101. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  102. package/dist/utils/smooth/useSmooth.js +16 -1
  103. package/dist/utils/smooth/useSmooth.js.map +1 -1
  104. package/dist/utils/useToolArgsFieldStatus.js +1 -1
  105. package/package.json +10 -10
  106. package/src/assistant-transport.ts +94 -0
  107. package/src/devtools/DevToolsHooks.test.ts +40 -0
  108. package/src/devtools/DevToolsHooks.ts +10 -15
  109. package/src/index.ts +8 -4
  110. package/src/mcp-apps/McpAppsRemoteHost.test.ts +29 -3
  111. package/src/mcp-apps/McpAppsRemoteHost.ts +42 -7
  112. package/src/mcp-apps/bridge.test.ts +153 -0
  113. package/src/mcp-apps/bridge.ts +42 -30
  114. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
  115. package/src/primitives/actionBar/ActionBarExportMarkdown.test.tsx +67 -0
  116. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +3 -1
  117. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +0 -9
  118. package/src/primitives/branchPicker/BranchPickerRoot.tsx +5 -3
  119. package/src/primitives/composer/ComposerDictationTranscript.tsx +2 -2
  120. package/src/primitives/composer/ComposerInput.tsx +1 -1
  121. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +135 -0
  122. package/src/primitives/composer/trigger/triggerSelectionResource.ts +2 -0
  123. package/src/primitives/thread/ThreadRoot.test.tsx +251 -0
  124. package/src/primitives/thread/ThreadRoot.tsx +33 -0
  125. package/src/primitives/thread/ThreadViewport.tsx +1 -8
  126. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +152 -0
  127. package/src/primitives/thread/useThreadViewportAutoScroll.ts +26 -19
  128. package/src/sandbox-host/SandboxHost.test.tsx +90 -0
  129. package/src/sandbox-host/SandboxHost.tsx +8 -2
  130. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +24 -3
  131. package/src/tests/RemoteThreadListRuntime.reloadMainThread.test.tsx +1 -2
  132. package/src/tests/augmentations.test.ts +49 -0
  133. package/src/tests/in-memory-thread-list.test.tsx +1 -1
  134. package/src/tests/threadMessageById.test.tsx +61 -1
  135. package/src/unstable/useComposerInput.ts +2 -2
  136. package/src/unstable/useComposerInputHistory.test.tsx +4 -4
  137. package/src/unstable/useComposerInputHistory.ts +1 -1
  138. package/src/unstable/useLiveCompletionAdapter.test.tsx +157 -0
  139. package/src/unstable/useLiveCompletionAdapter.ts +41 -17
  140. package/src/utils/invokeCallbackSafely.ts +11 -0
  141. package/src/utils/smooth/useSmooth.test.tsx +110 -0
  142. package/src/utils/smooth/useSmooth.ts +20 -6
  143. package/dist/client/InMemoryThreadList.d.ts +0 -12
  144. package/dist/client/InMemoryThreadList.d.ts.map +0 -1
  145. package/dist/client/InMemoryThreadList.js +0 -374
  146. package/dist/client/InMemoryThreadList.js.map +0 -1
  147. package/dist/context/react/utils/ensureBinding.d.ts +0 -5
  148. package/dist/context/react/utils/ensureBinding.d.ts.map +0 -1
  149. package/dist/context/react/utils/ensureBinding.js +0 -24
  150. package/dist/context/react/utils/ensureBinding.js.map +0 -1
  151. package/dist/context/react/utils/useRuntimeState.d.ts +0 -13
  152. package/dist/context/react/utils/useRuntimeState.d.ts.map +0 -1
  153. package/dist/context/react/utils/useRuntimeState.js +0 -33
  154. package/dist/context/react/utils/useRuntimeState.js.map +0 -1
  155. package/dist/legacy-runtime/cloud/auiV0.d.ts +0 -119
  156. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +0 -1
  157. package/dist/legacy-runtime/cloud/auiV0.js +0 -137
  158. package/dist/legacy-runtime/cloud/auiV0.js.map +0 -1
  159. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts +0 -16
  160. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts.map +0 -1
  161. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js +0 -36
  162. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js.map +0 -1
  163. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +0 -17
  164. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts.map +0 -1
  165. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +0 -55
  166. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +0 -1
  167. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +0 -11
  168. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +0 -1
  169. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +0 -165
  170. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +0 -1
  171. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts +0 -15
  172. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +0 -1
  173. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +0 -81
  174. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +0 -1
  175. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +0 -126
  176. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +0 -1
  177. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.js +0 -0
  178. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts +0 -14
  179. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +0 -1
  180. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +0 -290
  181. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +0 -1
  182. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts +0 -7
  183. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts.map +0 -1
  184. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +0 -25
  185. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +0 -1
  186. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts +0 -7
  187. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts.map +0 -1
  188. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +0 -27
  189. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js.map +0 -1
  190. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +0 -2
  191. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js +0 -2
  192. package/src/client/InMemoryThreadList.ts +0 -247
  193. package/src/context/react/utils/ensureBinding.ts +0 -46
  194. package/src/context/react/utils/useRuntimeState.ts +0 -46
  195. package/src/legacy-runtime/cloud/auiV0.ts +0 -331
  196. package/src/legacy-runtime/cloud/useCloudThreadListRuntime.ts +0 -31
  197. package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +0 -68
  198. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +0 -455
  199. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +0 -172
  200. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +0 -113
  201. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +0 -79
  202. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +0 -97
  203. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +0 -307
  204. package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +0 -170
  205. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +0 -141
  206. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +0 -585
  207. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +0 -476
  208. package/src/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.ts +0 -20
  209. package/src/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.ts +0 -9
  210. package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +0 -1
@@ -228,4 +228,94 @@ describe("SandboxHost", () => {
228
228
  expect(onError).toHaveBeenCalledTimes(1);
229
229
  expect(onError.mock.calls[0]![0].message).toBe("boom");
230
230
  });
231
+
232
+ it("does not report render failures after unmount", async () => {
233
+ let rejectRender!: (error: Error) => void;
234
+ renderHtmlMock.mockReturnValue(
235
+ new Promise((_, reject) => {
236
+ rejectRender = reject;
237
+ }),
238
+ );
239
+ const onError = vi.fn();
240
+
241
+ await act(async () => {
242
+ root.render(
243
+ <SandboxHost
244
+ content={{ html: "" }}
245
+ contentKey="k"
246
+ createBridge={() => ({ onMessage: vi.fn(), dispose: vi.fn() })}
247
+ onError={onError}
248
+ />,
249
+ );
250
+ });
251
+
252
+ await act(async () => {
253
+ root.unmount();
254
+ });
255
+ rejectRender(new Error("late failure"));
256
+ await flush();
257
+
258
+ expect(onError).not.toHaveBeenCalled();
259
+ });
260
+
261
+ it("contains failures thrown by onError", async () => {
262
+ const renderError = new Error("render failed");
263
+ const callbackError = new Error("error callback failed");
264
+ renderHtmlMock.mockRejectedValue(renderError);
265
+ const consoleError = vi
266
+ .spyOn(console, "error")
267
+ .mockImplementation(() => {});
268
+
269
+ try {
270
+ await act(async () => {
271
+ root.render(
272
+ <SandboxHost
273
+ content={{ html: "" }}
274
+ contentKey="k"
275
+ createBridge={() => ({ onMessage: vi.fn(), dispose: vi.fn() })}
276
+ onError={() => {
277
+ throw callbackError;
278
+ }}
279
+ />,
280
+ );
281
+ });
282
+ await flush();
283
+
284
+ expect(consoleError).toHaveBeenCalledWith(
285
+ "[assistant-ui] SandboxHost onError callback threw an error",
286
+ callbackError,
287
+ );
288
+ } finally {
289
+ consoleError.mockRestore();
290
+ }
291
+ });
292
+
293
+ it("disposes the rendered frame when bridge creation fails", async () => {
294
+ const rendered = fakeRendered();
295
+ renderHtmlMock.mockResolvedValue(rendered);
296
+ const error = new Error("bridge failed");
297
+ const onError = vi.fn();
298
+
299
+ await act(async () => {
300
+ root.render(
301
+ <SandboxHost
302
+ content={{ html: "" }}
303
+ contentKey="k"
304
+ createBridge={() => {
305
+ throw error;
306
+ }}
307
+ onError={onError}
308
+ />,
309
+ );
310
+ });
311
+ await flush();
312
+
313
+ expect(onError).toHaveBeenCalledWith(error);
314
+ expect(rendered.dispose).toHaveBeenCalledTimes(1);
315
+
316
+ await act(async () => {
317
+ root.unmount();
318
+ });
319
+ expect(rendered.dispose).toHaveBeenCalledTimes(1);
320
+ });
231
321
  });
@@ -6,6 +6,7 @@ import {
6
6
  SafeContentFrame,
7
7
  type SandboxOption,
8
8
  } from "safe-content-frame";
9
+ import { invokeCallbackSafely } from "../utils/invokeCallbackSafely";
9
10
 
10
11
  const DEFAULT_PRODUCT = "assistant-ui-sandbox";
11
12
  const DEFAULT_MAX_HEIGHT = 800;
@@ -147,8 +148,13 @@ export function SandboxHost({
147
148
  window.addEventListener("message", onMessage);
148
149
  })
149
150
  .catch((err) => {
150
- liveRef.current.onError?.(
151
- err instanceof Error ? err : new Error(String(err)),
151
+ if (cancelled) return;
152
+ frame?.dispose();
153
+ frame = null;
154
+ const error = err instanceof Error ? err : new Error(String(err));
155
+ invokeCallbackSafely(
156
+ () => liveRef.current.onError?.(error),
157
+ "SandboxHost onError",
152
158
  );
153
159
  });
154
160
 
@@ -26,9 +26,14 @@ const dummyHistory: ThreadHistoryAdapter = {
26
26
  append: async () => {},
27
27
  };
28
28
 
29
- const makeRuntimeHook = (capture: { adapters: CapturedAdapters }) =>
29
+ const makeRuntimeHook = (capture: {
30
+ adapters: CapturedAdapters;
31
+ calls?: CapturedAdapters[];
32
+ }) =>
30
33
  function useTestRuntimeHook() {
31
- capture.adapters = useRuntimeAdapters();
34
+ const adapters = useRuntimeAdapters();
35
+ capture.calls?.push(adapters);
36
+ capture.adapters = adapters;
32
37
  return useLocalRuntime(noOpAdapter);
33
38
  };
34
39
 
@@ -68,12 +73,16 @@ const wrapInRuntimeAdapterProvider = (
68
73
 
69
74
  describe("RemoteThreadListAdapter.unstable_Provider", () => {
70
75
  it("makes Provider context visible to the runtime hook while preserving outer modelContext", async () => {
71
- const capture: { adapters: CapturedAdapters } = { adapters: null };
76
+ const capture: { adapters: CapturedAdapters; calls: CapturedAdapters[] } = {
77
+ adapters: null,
78
+ calls: [],
79
+ };
72
80
  const adapter = makeAdapter({
73
81
  unstable_Provider: wrapInRuntimeAdapterProvider(dummyHistory),
74
82
  });
75
83
  await renderAndWaitForBinder(adapter, capture);
76
84
 
85
+ expect(capture.calls[0]?.history).toBe(dummyHistory);
77
86
  expect(capture.adapters?.history).toBe(dummyHistory);
78
87
  expect(capture.adapters?.modelContext).toBeDefined();
79
88
  });
@@ -86,6 +95,18 @@ describe("RemoteThreadListAdapter.unstable_Provider", () => {
86
95
  expect(capture.adapters?.history).toBeUndefined();
87
96
  });
88
97
 
98
+ it("makes useAdapters context visible when no Provider is supplied", async () => {
99
+ const capture: { adapters: CapturedAdapters } = { adapters: null };
100
+ const adapter = makeAdapter({
101
+ unstable_useAdapters: function useTestAdapters() {
102
+ return { history: dummyHistory };
103
+ },
104
+ });
105
+ await renderAndWaitForBinder(adapter, capture);
106
+
107
+ expect(capture.adapters?.history).toBe(dummyHistory);
108
+ });
109
+
89
110
  it("picks up a swapped Provider on re-render", async () => {
90
111
  const capture: { adapters: CapturedAdapters } = { adapters: null };
91
112
  const firstHistory: ThreadHistoryAdapter = {
@@ -86,8 +86,7 @@ describe("threads.reloadMainThread", () => {
86
86
  await runtime.threads.reloadMainThread();
87
87
  });
88
88
 
89
- // the hook ran again, which is what re-runs the adapter's load()
90
- await waitFor(() => expect(mounts.count).toBe(beforeReload + 1));
89
+ await waitFor(() => expect(mounts.count).toBeGreaterThan(beforeReload));
91
90
  });
92
91
 
93
92
  it("keeps a thread runtime readable across the remount", async () => {
@@ -1,5 +1,11 @@
1
1
  import { describe, expectTypeOf, it } from "vitest";
2
2
  import type { UserCommands, UserExternalState } from "../augmentations";
3
+ import type {
4
+ AssistantTransportCommand,
5
+ useAssistantTransportRuntime,
6
+ useAssistantTransportSendCommand,
7
+ useAssistantTransportState,
8
+ } from "../index";
3
9
 
4
10
  declare module "../augmentations" {
5
11
  namespace Assistant {
@@ -36,4 +42,47 @@ describe("Assistant augmentations", () => {
36
42
  expectTypeOf<{ type: "core-command" }>().toExtend<UserCommands>();
37
43
  expectTypeOf<{ fromCore: boolean }>().toExtend<UserExternalState>();
38
44
  });
45
+
46
+ it("react-side contributions reach the public transport types", () => {
47
+ expectTypeOf<{
48
+ type: "react-command";
49
+ data: number;
50
+ }>().toExtend<AssistantTransportCommand>();
51
+ expectTypeOf<{ fromReact: boolean }>().toExtend<
52
+ ReturnType<typeof useAssistantTransportState>
53
+ >();
54
+ expectTypeOf<{
55
+ type: "react-command";
56
+ data: number;
57
+ }>().toExtend<
58
+ Parameters<ReturnType<typeof useAssistantTransportSendCommand>>[0]
59
+ >();
60
+ type RuntimeOptions = Parameters<typeof useAssistantTransportRuntime>[0];
61
+ expectTypeOf<{
62
+ type: "react-command";
63
+ data: number;
64
+ }>().toExtend<
65
+ Parameters<RuntimeOptions["converter"]>[1]["pendingCommands"][number]
66
+ >();
67
+ expectTypeOf<{
68
+ type: "react-command";
69
+ data: number;
70
+ }>().toExtend<
71
+ Parameters<NonNullable<RuntimeOptions["onError"]>>[1]["commands"][number]
72
+ >();
73
+ expectTypeOf<{
74
+ type: "react-command";
75
+ data: number;
76
+ }>().toExtend<
77
+ Parameters<NonNullable<RuntimeOptions["onCancel"]>>[0]["commands"][number]
78
+ >();
79
+ expectTypeOf<{
80
+ type: "react-command";
81
+ data: number;
82
+ }>().toExtend<
83
+ Parameters<
84
+ NonNullable<RuntimeOptions["prepareSendCommandsRequest"]>
85
+ >[0]["commands"][number]
86
+ >();
87
+ });
39
88
  });
@@ -4,7 +4,7 @@ import { render, waitFor } from "@testing-library/react";
4
4
  import type { FC } from "react";
5
5
  import { describe, it, expect, vi } from "vitest";
6
6
  import { useAui, AuiProvider } from "@assistant-ui/store";
7
- import { InMemoryThreadList } from "../client/InMemoryThreadList";
7
+ import { InMemoryThreadList } from "@assistant-ui/core/store";
8
8
  import { ExternalThread } from "../index";
9
9
 
10
10
  const renderThreads = () => {
@@ -3,6 +3,7 @@
3
3
  import { act, render, screen } from "@testing-library/react";
4
4
  import { useState, type FC, type PropsWithChildren } from "react";
5
5
  import { describe, expect, it } from "vitest";
6
+ import { useAui } from "@assistant-ui/store";
6
7
  import {
7
8
  AssistantRuntimeProvider,
8
9
  useExternalStoreRuntime,
@@ -13,7 +14,11 @@ import * as ThreadPrimitive from "../primitives/thread";
13
14
  import * as MessagePrimitive from "../primitives/message";
14
15
  import * as MessagePartPrimitive from "../primitives/messagePart";
15
16
 
16
- type Msg = { id: string; role: "user" | "assistant"; text: string };
17
+ type Msg = {
18
+ id: string;
19
+ role: "user" | "assistant" | "system";
20
+ text: string;
21
+ };
17
22
 
18
23
  const convertMessage = (m: Msg): ThreadMessageLike => ({
19
24
  id: m.id,
@@ -39,6 +44,7 @@ const Provider: FC<PropsWithChildren<{ initial: Msg[] }>> = ({
39
44
  messages,
40
45
  convertMessage,
41
46
  onNew: async () => {},
47
+ onEdit: async () => {},
42
48
  });
43
49
  return (
44
50
  <AssistantRuntimeProvider runtime={runtime}>
@@ -47,6 +53,19 @@ const Provider: FC<PropsWithChildren<{ initial: Msg[] }>> = ({
47
53
  );
48
54
  };
49
55
 
56
+ let beginEdit: (() => void) | undefined;
57
+
58
+ const CaptureBeginEdit: FC = () => {
59
+ const aui = useAui();
60
+ beginEdit = () => aui.thread.message({ index: 0 }).composer().beginEdit();
61
+ return null;
62
+ };
63
+
64
+ const SYSTEM_COMPONENTS = {
65
+ UserMessage: () => <span>user</span>,
66
+ AssistantMessage: () => <span>assistant</span>,
67
+ };
68
+
50
69
  let lastIds: readonly string[] | undefined;
51
70
  const idIdentities: (readonly string[])[] = [];
52
71
 
@@ -215,3 +234,44 @@ describe("ThreadPrimitive.Unstable_MessageById", () => {
215
234
  expect(screen.getByTestId("by-id").textContent).toBe("first");
216
235
  });
217
236
  });
237
+
238
+ describe("system message editing fallback", () => {
239
+ it.each([
240
+ [
241
+ "Messages",
242
+ () => <ThreadPrimitive.Messages components={SYSTEM_COMPONENTS} />,
243
+ ],
244
+ [
245
+ "MessageByIndex",
246
+ () => (
247
+ <ThreadPrimitive.MessageByIndex
248
+ index={0}
249
+ components={SYSTEM_COMPONENTS}
250
+ />
251
+ ),
252
+ ],
253
+ [
254
+ "Unstable_MessageById",
255
+ () => (
256
+ <ThreadPrimitive.Unstable_MessageById
257
+ messageId="s1"
258
+ components={SYSTEM_COMPONENTS}
259
+ />
260
+ ),
261
+ ],
262
+ ])(
263
+ "renders nothing for a system message in edit mode through %s",
264
+ async (_, renderMessage) => {
265
+ const view = render(
266
+ <Provider initial={[{ id: "s1", role: "system", text: "sys" }]}>
267
+ <CaptureBeginEdit />
268
+ {renderMessage()}
269
+ </Provider>,
270
+ );
271
+
272
+ await act(async () => beginEdit!());
273
+
274
+ expect(view.container.textContent).toBe("");
275
+ },
276
+ );
277
+ });
@@ -26,8 +26,8 @@ export type Unstable_ComposerInput = {
26
26
  value: string;
27
27
  /**
28
28
  * Writes `text` into the composer, mirroring `ComposerPrimitive.Input`:
29
- * a no-op unless the composer is editing, committed via `flushTapSync` so the
30
- * controlled value stays in sync within the same tick.
29
+ * a no-op unless the composer is editing; the controlled value stays in
30
+ * sync within the same tick.
31
31
  */
32
32
  setText(text: string): void;
33
33
  /**
@@ -6,7 +6,7 @@ const fixture = {
6
6
  messages: [] as { role: string; content: { type: string; text: string }[] }[],
7
7
  composerType: "thread",
8
8
  activeAria: null as object | null,
9
- switchedToHandlers: [] as (() => void)[],
9
+ selectionChangedHandlers: [] as (() => void)[],
10
10
  };
11
11
  const setText = vi.fn();
12
12
 
@@ -18,7 +18,7 @@ vi.mock("@assistant-ui/store", () => ({
18
18
  },
19
19
  thread: { getState: () => ({ messages: fixture.messages }) },
20
20
  on: (_event: string, cb: () => void) => {
21
- fixture.switchedToHandlers.push(cb);
21
+ fixture.selectionChangedHandlers.push(cb);
22
22
  return () => {};
23
23
  },
24
24
  }),
@@ -71,7 +71,7 @@ beforeEach(() => {
71
71
  fixture.messages = [user("first"), assistant("reply"), user("second")];
72
72
  fixture.composerType = "thread";
73
73
  fixture.activeAria = null;
74
- fixture.switchedToHandlers = [];
74
+ fixture.selectionChangedHandlers = [];
75
75
  setText.mockClear();
76
76
  });
77
77
 
@@ -192,7 +192,7 @@ describe("unstable_useComposerInputHistory", () => {
192
192
  it("resets browsing when the thread switches", () => {
193
193
  const { textarea } = setup("");
194
194
  arrow(textarea, "ArrowUp");
195
- fixture.switchedToHandlers.forEach((cb) => cb());
195
+ fixture.selectionChangedHandlers.forEach((cb) => cb());
196
196
  textarea.value = "second";
197
197
  const notPrevented = arrow(textarea, "ArrowDown");
198
198
  expect(notPrevented).toBe(true);
@@ -72,7 +72,7 @@ export function unstable_useComposerInputHistory(): Unstable_ComposerInputHistor
72
72
  useEffect(() => {
73
73
  if (aui.composer.getState().type !== "thread") return undefined;
74
74
 
75
- return aui.on("threadListItem.switchedTo", () => {
75
+ return aui.on("threads.selectionChanged", () => {
76
76
  browseRef.current = null;
77
77
  });
78
78
  }, [aui]);
@@ -129,6 +129,137 @@ describe("unstable_useLiveCompletionAdapter", () => {
129
129
  expect(result.current.adapter.search!("ab")).toEqual([item("ab")]);
130
130
  });
131
131
 
132
+ it("refreshes cached results when the fetcher cache key changes", async () => {
133
+ const fetcherA = vi.fn(async () => [item("workspace-a")]);
134
+ const fetcherB = vi.fn(async () => [item("workspace-b")]);
135
+ const { result, rerender } = renderHook(
136
+ ({ fetcher, cacheKey }) =>
137
+ unstable_useLiveCompletionAdapter({
138
+ fetcher,
139
+ cacheKey,
140
+ debounceMs: 0,
141
+ }),
142
+ { initialProps: { fetcher: fetcherA, cacheKey: "workspace-a" } },
143
+ );
144
+
145
+ await act(async () => {
146
+ result.current.adapter.search!("alice");
147
+ await vi.advanceTimersByTimeAsync(0);
148
+ });
149
+ expect(result.current.adapter.search!("alice")).toEqual([
150
+ item("workspace-a"),
151
+ ]);
152
+
153
+ await act(async () => {
154
+ rerender({ fetcher: fetcherB, cacheKey: "workspace-b" });
155
+ });
156
+ expect(result.current.adapter.search!("alice")).toEqual([]);
157
+
158
+ await act(async () => {
159
+ await vi.advanceTimersByTimeAsync(0);
160
+ });
161
+ expect(fetcherB).toHaveBeenCalledWith("alice");
162
+ expect(result.current.adapter.search!("alice")).toEqual([
163
+ item("workspace-b"),
164
+ ]);
165
+ });
166
+
167
+ it("drops pending results after the fetcher cache key changes", async () => {
168
+ let resolveA!: (items: readonly Unstable_TriggerItem[]) => void;
169
+ const fetcherA = vi.fn(
170
+ () =>
171
+ new Promise<readonly Unstable_TriggerItem[]>((resolve) => {
172
+ resolveA = resolve;
173
+ }),
174
+ );
175
+ const fetcherB = vi.fn(async () => [item("workspace-b")]);
176
+ const { result, rerender } = renderHook(
177
+ ({ fetcher, cacheKey }) =>
178
+ unstable_useLiveCompletionAdapter({
179
+ fetcher,
180
+ cacheKey,
181
+ debounceMs: 0,
182
+ }),
183
+ { initialProps: { fetcher: fetcherA, cacheKey: "workspace-a" } },
184
+ );
185
+
186
+ await act(async () => {
187
+ result.current.adapter.search!("alice");
188
+ await vi.advanceTimersByTimeAsync(0);
189
+ });
190
+ await act(async () => {
191
+ rerender({ fetcher: fetcherB, cacheKey: "workspace-b" });
192
+ });
193
+ await act(async () => {
194
+ result.current.adapter.search!("alice");
195
+ await vi.advanceTimersByTimeAsync(0);
196
+ });
197
+ await act(async () => {
198
+ resolveA([item("workspace-a")]);
199
+ });
200
+
201
+ expect(result.current.adapter.search!("alice")).toEqual([
202
+ item("workspace-b"),
203
+ ]);
204
+ });
205
+
206
+ it("keeps cached results when only the fetcher identity changes", async () => {
207
+ const first = vi.fn(async () => [item("alice")]);
208
+ const second = vi.fn(async () => [item("bob")]);
209
+ const { result, rerender } = renderHook(
210
+ ({ fetcher }) =>
211
+ unstable_useLiveCompletionAdapter({
212
+ fetcher,
213
+ cacheKey: "workspace-a",
214
+ debounceMs: 0,
215
+ }),
216
+ { initialProps: { fetcher: first } },
217
+ );
218
+
219
+ await act(async () => {
220
+ result.current.adapter.search!("alice");
221
+ await vi.advanceTimersByTimeAsync(0);
222
+ });
223
+ expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
224
+
225
+ await act(async () => {
226
+ rerender({ fetcher: second });
227
+ await vi.advanceTimersByTimeAsync(0);
228
+ });
229
+ expect(second).not.toHaveBeenCalled();
230
+ expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
231
+ });
232
+
233
+ it("drops a pending result when only the cache key changes", async () => {
234
+ let resolveFirst!: (items: readonly Unstable_TriggerItem[]) => void;
235
+ const first = new Promise<readonly Unstable_TriggerItem[]>((resolve) => {
236
+ resolveFirst = resolve;
237
+ });
238
+ const fetcher = vi.fn(() => first);
239
+ const { result, rerender } = renderHook(
240
+ ({ cacheKey }) =>
241
+ unstable_useLiveCompletionAdapter({ fetcher, cacheKey, debounceMs: 0 }),
242
+ { initialProps: { cacheKey: "workspace-a" } },
243
+ );
244
+
245
+ await act(async () => {
246
+ result.current.adapter.search!("alice");
247
+ await vi.advanceTimersByTimeAsync(0);
248
+ });
249
+ expect(fetcher).toHaveBeenCalledTimes(1);
250
+
251
+ await act(async () => {
252
+ rerender({ cacheKey: "workspace-b" });
253
+ });
254
+ await act(async () => {
255
+ resolveFirst([item("workspace-a")]);
256
+ });
257
+
258
+ expect(result.current.isLoading).toBe(false);
259
+ expect(fetcher).toHaveBeenCalledTimes(1);
260
+ expect(result.current.adapter.search!("alice")).toEqual([]);
261
+ });
262
+
132
263
  it("allows a failed query to be retried", async () => {
133
264
  const fetcher = vi
134
265
  .fn<(query: string) => Promise<readonly Unstable_TriggerItem[]>>()
@@ -156,6 +287,32 @@ describe("unstable_useLiveCompletionAdapter", () => {
156
287
  expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
157
288
  });
158
289
 
290
+ it("allows a synchronously failed query to be retried", async () => {
291
+ const fetcher = vi
292
+ .fn<(query: string) => Promise<readonly Unstable_TriggerItem[]>>()
293
+ .mockImplementationOnce(() => {
294
+ throw new Error("invalid request configuration");
295
+ })
296
+ .mockResolvedValueOnce([item("alice")]);
297
+ const { result } = renderHook(() =>
298
+ unstable_useLiveCompletionAdapter({ fetcher, debounceMs: 0 }),
299
+ );
300
+
301
+ await act(async () => {
302
+ result.current.adapter.search!("alice");
303
+ await vi.advanceTimersByTimeAsync(0);
304
+ });
305
+ expect(fetcher).toHaveBeenCalledTimes(1);
306
+ expect(result.current.isLoading).toBe(false);
307
+
308
+ await act(async () => {
309
+ result.current.adapter.search!("alice");
310
+ await vi.advanceTimersByTimeAsync(0);
311
+ });
312
+ expect(fetcher).toHaveBeenCalledTimes(2);
313
+ expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
314
+ });
315
+
159
316
  it("does not automatically retry when search runs during every render", async () => {
160
317
  const fetcher = vi
161
318
  .fn<(query: string) => Promise<readonly Unstable_TriggerItem[]>>()
@@ -1,6 +1,13 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
4
11
  import type {
5
12
  Unstable_TriggerAdapter,
6
13
  Unstable_TriggerItem,
@@ -13,6 +20,11 @@ export type Unstable_UseLiveCompletionAdapterOptions = {
13
20
  * next render.
14
21
  */
15
22
  readonly fetcher: (query: string) => Promise<readonly Unstable_TriggerItem[]>;
23
+ /**
24
+ * Identifies the fetcher's data source. Change this when switching accounts,
25
+ * workspaces, or another boundary that should invalidate cached results.
26
+ */
27
+ readonly cacheKey?: string | number | undefined;
16
28
  /** Debounce applied before a fetch fires, in milliseconds. @default 60 */
17
29
  readonly debounceMs?: number | undefined;
18
30
  /** When `false`, no fetch is scheduled and the adapter stays empty. @default true */
@@ -52,7 +64,7 @@ const NO_QUERY = "\u0000";
52
64
  export function unstable_useLiveCompletionAdapter(
53
65
  options: Unstable_UseLiveCompletionAdapterOptions,
54
66
  ): { adapter: Unstable_TriggerAdapter; isLoading: boolean } {
55
- const { fetcher, debounceMs = 60, enabled = true } = options;
67
+ const { fetcher, cacheKey, debounceMs = 60, enabled = true } = options;
56
68
 
57
69
  const [state, setState] = useState<{
58
70
  query: string;
@@ -99,21 +111,23 @@ export function unstable_useLiveCompletionAdapter(
99
111
  setIsLoading(true);
100
112
  timerRef.current = setTimeout(() => {
101
113
  timerRef.current = null;
102
- fetcherRef.current(query).then(
103
- (items) => {
104
- if (token !== tokenRef.current) return;
105
- pendingRetryQueryRef.current = null;
106
- setState({ query, items, failed: false });
107
- setIsLoading(false);
108
- },
109
- () => {
110
- if (token !== tokenRef.current) return;
111
- pendingQueryRef.current = null;
112
- pendingRetryQueryRef.current = null;
113
- setState({ query, items: [], failed: true });
114
- setIsLoading(false);
115
- },
116
- );
114
+ Promise.resolve()
115
+ .then(() => fetcherRef.current(query))
116
+ .then(
117
+ (items) => {
118
+ if (token !== tokenRef.current) return;
119
+ pendingRetryQueryRef.current = null;
120
+ setState({ query, items, failed: false });
121
+ setIsLoading(false);
122
+ },
123
+ () => {
124
+ if (token !== tokenRef.current) return;
125
+ pendingQueryRef.current = null;
126
+ pendingRetryQueryRef.current = null;
127
+ setState({ query, items: [], failed: true });
128
+ setIsLoading(false);
129
+ },
130
+ );
117
131
  }, debounceMs);
118
132
  },
119
133
  [enabled, debounceMs, cancelTimer, rearmPendingRetry],
@@ -127,6 +141,16 @@ export function unstable_useLiveCompletionAdapter(
127
141
  setIsLoading(false);
128
142
  }, [cancelTimer, rearmPendingRetry]);
129
143
 
144
+ const cacheKeyRef = useRef(cacheKey);
145
+ useLayoutEffect(() => {
146
+ if (cacheKeyRef.current === cacheKey) return;
147
+ cacheKeyRef.current = cacheKey;
148
+ invalidatePending();
149
+ retryableQueryRef.current = null;
150
+ pendingRetryQueryRef.current = null;
151
+ setState({ query: NO_QUERY, items: [], failed: false });
152
+ }, [cacheKey, invalidatePending]);
153
+
130
154
  useEffect(() => {
131
155
  if (enabled) return;
132
156
  invalidatePending();
@@ -0,0 +1,11 @@
1
+ export const invokeCallbackSafely = (invoke: () => unknown, name: string) => {
2
+ const reportFailure = (error: unknown) => {
3
+ console.error(`[assistant-ui] ${name} callback threw an error`, error);
4
+ };
5
+
6
+ try {
7
+ void Promise.resolve(invoke()).catch(reportFailure);
8
+ } catch (error) {
9
+ reportFailure(error);
10
+ }
11
+ };