@assistant-ui/react 0.15.16 → 0.15.17

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 (223) hide show
  1. package/README.md +3 -3
  2. package/dist/index.d.ts +4 -3
  3. package/dist/index.js +2 -2
  4. package/dist/mcp-apps/McpAppRenderer.d.ts +6 -1
  5. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  6. package/dist/mcp-apps/McpAppRenderer.js +19 -12
  7. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  8. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  9. package/dist/mcp-apps/McpAppsRemoteHost.js +20 -17
  10. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  11. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  12. package/dist/mcp-apps/app-frame.js +189 -94
  13. package/dist/mcp-apps/app-frame.js.map +1 -1
  14. package/dist/mcp-apps/index.d.ts +2 -2
  15. package/dist/model-context/frame/useAssistantFrameHost.d.ts +1 -1
  16. package/dist/model-context/frame/useAssistantFrameHost.js +10 -11
  17. package/dist/model-context/frame/useAssistantFrameHost.js.map +1 -1
  18. package/dist/model-context/makeAssistantVisible.js +1 -1
  19. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  20. package/dist/primitives/actionBar/ActionBarCopy.js +3 -3
  21. package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
  22. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +3 -3
  23. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  24. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +3 -3
  25. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
  26. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +3 -3
  27. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
  28. package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
  29. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +3 -3
  30. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  31. package/dist/primitives/actionBarMore/ActionBarMoreContent.js.map +1 -1
  32. package/dist/primitives/actionBarMore/ActionBarMoreItem.js.map +1 -1
  33. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
  34. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js.map +1 -1
  35. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js.map +1 -1
  36. package/dist/primitives/actionBarMore/scope.d.ts +2 -2
  37. package/dist/primitives/actionBarMore/scope.d.ts.map +1 -1
  38. package/dist/primitives/actionBarMore/scope.js.map +1 -1
  39. package/dist/primitives/assistantModal/AssistantModalAnchor.js.map +1 -1
  40. package/dist/primitives/assistantModal/AssistantModalContent.js +1 -1
  41. package/dist/primitives/assistantModal/AssistantModalContent.js.map +1 -1
  42. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  43. package/dist/primitives/assistantModal/AssistantModalTrigger.js.map +1 -1
  44. package/dist/primitives/assistantModal/scope.d.ts +2 -2
  45. package/dist/primitives/assistantModal/scope.d.ts.map +1 -1
  46. package/dist/primitives/assistantModal/scope.js.map +1 -1
  47. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  48. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  49. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  50. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  51. package/dist/primitives/composer/ComposerInput.js +1 -3
  52. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  53. package/dist/primitives/composer/ComposerQuote.js +5 -5
  54. package/dist/primitives/composer/ComposerQuote.js.map +1 -1
  55. package/dist/primitives/composer/ComposerRoot.js +3 -3
  56. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  57. package/dist/primitives/composer/trigger/TriggerPopover.d.ts +5 -0
  58. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  59. package/dist/primitives/composer/trigger/TriggerPopover.js +157 -130
  60. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  61. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +3 -3
  62. package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -1
  63. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +4 -4
  64. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  65. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +4 -4
  66. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  67. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -0
  68. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  69. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +63 -61
  70. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  71. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +2 -0
  72. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts.map +1 -1
  73. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  74. package/dist/primitives/composer/trigger/detectTrigger.d.ts +21 -1
  75. package/dist/primitives/composer/trigger/detectTrigger.d.ts.map +1 -0
  76. package/dist/primitives/composer/trigger/detectTrigger.js +17 -3
  77. package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -1
  78. package/dist/primitives/composer/trigger/index.d.ts +3 -1
  79. package/dist/primitives/composer/trigger/index.d.ts.map +1 -1
  80. package/dist/primitives/composer/trigger/index.js.map +1 -1
  81. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +3 -4
  82. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  83. package/dist/primitives/composer/trigger/triggerDetectionResource.js +15 -14
  84. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  85. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  86. package/dist/primitives/composer/trigger/triggerSelectionResource.js +18 -19
  87. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  88. package/dist/primitives/composer.d.ts +3 -2
  89. package/dist/primitives/composer.js.map +1 -1
  90. package/dist/primitives/error/ErrorMessage.js.map +1 -1
  91. package/dist/primitives/message/MessageIf.js.map +1 -1
  92. package/dist/primitives/message/MessageParts.js.map +1 -1
  93. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  94. package/dist/primitives/message/MessageRoot.js +4 -4
  95. package/dist/primitives/message/MessageRoot.js.map +1 -1
  96. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  97. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +3 -3
  98. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
  99. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +3 -3
  100. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  101. package/dist/primitives/thread/ThreadIf.js.map +1 -1
  102. package/dist/primitives/thread/ThreadViewport.js +3 -3
  103. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  104. package/dist/primitives/thread/ThreadViewportFooter.js +3 -3
  105. package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
  106. package/dist/primitives/thread/useThreadViewportAutoScroll.js +1 -1
  107. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  108. package/dist/primitives/threadList/ThreadListNew.js +3 -3
  109. package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
  110. package/dist/primitives/threadListFocusGroup.d.ts +2 -1
  111. package/dist/primitives/threadListFocusGroup.d.ts.map +1 -1
  112. package/dist/primitives/threadListFocusGroup.js +2 -2
  113. package/dist/primitives/threadListFocusGroup.js.map +1 -1
  114. package/dist/primitives/threadListItem/ThreadListItemRoot.js +3 -3
  115. package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
  116. package/dist/primitives/threadListItem/ThreadListItemTrigger.js +1 -1
  117. package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
  118. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -1
  119. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
  120. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js.map +1 -1
  121. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +1 -1
  122. package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
  123. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js.map +1 -1
  124. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -2
  125. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js.map +1 -1
  126. package/dist/primitives/threadListItemMore/scope.d.ts +2 -2
  127. package/dist/primitives/threadListItemMore/scope.d.ts.map +1 -1
  128. package/dist/primitives/threadListItemMore/scope.js.map +1 -1
  129. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  130. package/dist/sandbox-host/SandboxHost.js +137 -70
  131. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  132. package/dist/unstable/useLiveCompletionAdapter.js +3 -1
  133. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  134. package/dist/unstable/useSlashCommandAdapter.js +41 -27
  135. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  136. package/dist/utils/Primitive.d.ts +2 -2
  137. package/dist/utils/Primitive.js +2 -2
  138. package/dist/utils/Primitive.js.map +1 -1
  139. package/dist/utils/createActionButton.js +3 -3
  140. package/dist/utils/createActionButton.js.map +1 -1
  141. package/dist/utils/hooks/useOnResizeContent.js +1 -1
  142. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  143. package/dist/utils/hooks/useOnScrollToBottom.js +1 -1
  144. package/dist/utils/hooks/useOnScrollToBottom.js.map +1 -1
  145. package/dist/utils/smooth/SmoothContext.d.ts +24 -0
  146. package/dist/utils/smooth/SmoothContext.d.ts.map +1 -1
  147. package/dist/utils/smooth/useSmooth.js +1 -1
  148. package/dist/utils/smooth/useSmooth.js.map +1 -1
  149. package/dist/utils/useToolArgsFieldStatus.d.ts +5 -1
  150. package/package.json +11 -19
  151. package/src/index.ts +9 -1
  152. package/src/mcp-apps/McpAppRenderer.test.tsx +133 -4
  153. package/src/mcp-apps/McpAppRenderer.tsx +28 -21
  154. package/src/mcp-apps/McpAppsRemoteHost.concurrent.test.tsx +215 -0
  155. package/src/mcp-apps/McpAppsRemoteHost.ts +28 -12
  156. package/src/mcp-apps/app-frame.test.tsx +102 -0
  157. package/src/mcp-apps/app-frame.tsx +17 -4
  158. package/src/mcp-apps/index.ts +1 -0
  159. package/src/model-context/frame/AssistantFrame.test.ts +3 -3
  160. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
  161. package/src/model-context/frame/useAssistantFrameHost.ts +2 -2
  162. package/src/model-context/makeAssistantVisible.tsx +1 -1
  163. package/src/primitives/actionBar/ActionBarCopy.tsx +1 -1
  164. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +1 -1
  165. package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +1 -1
  166. package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +1 -1
  167. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +1 -1
  168. package/src/primitives/actionBarMore/scope.ts +2 -2
  169. package/src/primitives/assistantModal/AssistantModalContent.tsx +1 -1
  170. package/src/primitives/assistantModal/scope.ts +2 -2
  171. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +1 -1
  172. package/src/primitives/composer/ComposerInput.test.tsx +2 -1
  173. package/src/primitives/composer/ComposerInput.tsx +3 -3
  174. package/src/primitives/composer/ComposerQuote.tsx +1 -1
  175. package/src/primitives/composer/ComposerRoot.tsx +1 -1
  176. package/src/primitives/composer/trigger/TriggerPopover.tsx +16 -1
  177. package/src/primitives/composer/trigger/TriggerPopoverBack.tsx +1 -1
  178. package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +1 -1
  179. package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +1 -1
  180. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +4 -1
  181. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +2 -0
  182. package/src/primitives/composer/trigger/detectTrigger.test.ts +88 -5
  183. package/src/primitives/composer/trigger/detectTrigger.ts +50 -9
  184. package/src/primitives/composer/trigger/index.ts +1 -0
  185. package/src/primitives/composer/trigger/triggerDetectionResource.ts +10 -7
  186. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +15 -3
  187. package/src/primitives/composer/trigger/triggerSelectionResource.ts +1 -4
  188. package/src/primitives/composer.ts +1 -0
  189. package/src/primitives/message/MessageRoot.tsx +1 -1
  190. package/src/primitives/selectionToolbar/SelectionToolbarQuote.tsx +1 -1
  191. package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +1 -1
  192. package/src/primitives/thread/ThreadViewport.tsx +1 -1
  193. package/src/primitives/thread/ThreadViewportFooter.tsx +1 -1
  194. package/src/primitives/thread/useThreadViewportAutoScroll.ts +1 -1
  195. package/src/primitives/threadList/ThreadListNew.tsx +1 -1
  196. package/src/primitives/threadListFocusGroup.ts +2 -2
  197. package/src/primitives/threadListItem/ThreadListItemRoot.tsx +1 -1
  198. package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +1 -1
  199. package/src/primitives/threadListItemMore/ThreadListItemMoreContent.tsx +1 -1
  200. package/src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx +1 -1
  201. package/src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx +2 -2
  202. package/src/primitives/threadListItemMore/scope.ts +2 -2
  203. package/src/sandbox-host/SandboxHost.test.tsx +45 -1
  204. package/src/sandbox-host/SandboxHost.tsx +16 -3
  205. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +31 -1
  206. package/src/unstable/useLiveCompletionAdapter.test.tsx +60 -1
  207. package/src/unstable/useLiveCompletionAdapter.ts +4 -1
  208. package/src/unstable/useSlashCommandAdapter.test.tsx +141 -0
  209. package/src/unstable/useSlashCommandAdapter.ts +52 -17
  210. package/src/utils/Primitive.tsx +2 -2
  211. package/src/utils/createActionButton.tsx +1 -1
  212. package/src/utils/hooks/useOnResizeContent.ts +1 -1
  213. package/src/utils/hooks/useOnScrollToBottom.ts +1 -1
  214. package/src/utils/smooth/useSmooth.ts +1 -1
  215. package/dist/legacy-runtime/runtime/MessagePartRuntime.d.ts +0 -3
  216. package/dist/legacy-runtime/runtime/MessagePartRuntime.js +0 -2
  217. package/dist/legacy-runtime/runtime/MessageRuntime.d.ts +0 -3
  218. package/dist/legacy-runtime/runtime/MessageRuntime.js +0 -2
  219. package/dist/legacy-runtime/runtime/ThreadListRuntime.d.ts +0 -3
  220. package/dist/legacy-runtime/runtime/ThreadListRuntime.js +0 -2
  221. package/src/legacy-runtime/runtime/MessagePartRuntime.ts +0 -2
  222. package/src/legacy-runtime/runtime/MessageRuntime.ts +0 -3
  223. package/src/legacy-runtime/runtime/ThreadListRuntime.ts +0 -4
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { forwardRef } from "react";
4
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
4
+ import { useComposedRefs } from "radix-ui/internal";
5
5
  import {
6
6
  type ActionButtonElement,
7
7
  type ActionButtonProps,
@@ -6,7 +6,7 @@ import {
6
6
  forwardRef,
7
7
  } from "react";
8
8
  import { Direction, DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
9
- import { composeEventHandlers } from "@radix-ui/primitive";
9
+ import { composeEventHandlers } from "radix-ui/internal";
10
10
  import type { WithRenderPropProps } from "../../utils/Primitive";
11
11
  import { DropdownMenuRenderContent } from "../dropdownMenuRenderPrimitives";
12
12
  import { useThreadListItemFocus } from "../threadListFocusGroup";
@@ -8,7 +8,7 @@ import {
8
8
  useContext,
9
9
  } from "react";
10
10
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
11
- import { useControllableState } from "@radix-ui/react-use-controllable-state";
11
+ import { useControllableState } from "radix-ui/internal";
12
12
  import { type ScopedProps, useDropdownMenuScope } from "./scope";
13
13
 
14
14
  const ThreadListItemMoreSetOpenContext = createContext<
@@ -5,8 +5,8 @@ import {
5
5
  Direction,
6
6
  type DropdownMenu as DropdownMenuPrimitive,
7
7
  } from "radix-ui";
8
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
9
- import { composeEventHandlers } from "@radix-ui/primitive";
8
+ import { useComposedRefs } from "radix-ui/internal";
9
+ import { composeEventHandlers } from "radix-ui/internal";
10
10
  import type { WithRenderPropProps } from "../../utils/Primitive";
11
11
  import { DropdownMenuRenderTrigger } from "../dropdownMenuRenderPrimitives";
12
12
  import { useThreadListItemFocus } from "../threadListFocusGroup";
@@ -1,8 +1,8 @@
1
1
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
2
- import type { Scope } from "@radix-ui/react-context";
2
+ import type { Context } from "radix-ui/internal";
3
3
 
4
4
  export const useDropdownMenuScope: ReturnType<
5
5
  typeof DropdownMenuPrimitive.createDropdownMenuScope
6
6
  > = DropdownMenuPrimitive.createDropdownMenuScope();
7
7
 
8
- export type ScopedProps<P> = P & { __scopeThreadListItemMore?: Scope };
8
+ export type ScopedProps<P> = P & { __scopeThreadListItemMore?: Context.Scope };
@@ -1,5 +1,5 @@
1
1
  // @vitest-environment jsdom
2
- import { act } from "react";
2
+ import { act, startTransition, Suspense } from "react";
3
3
  import { createRoot, type Root } from "react-dom/client";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
 
@@ -318,4 +318,48 @@ describe("SandboxHost", () => {
318
318
  });
319
319
  expect(rendered.dispose).toHaveBeenCalledTimes(1);
320
320
  });
321
+
322
+ it("keeps bridge options scoped to committed renders", async () => {
323
+ let resolveRender!: (frame: ReturnType<typeof fakeRendered>) => void;
324
+ renderHtmlMock.mockReturnValue(
325
+ new Promise((resolve) => {
326
+ resolveRender = resolve;
327
+ }),
328
+ );
329
+ const rendered = fakeRendered();
330
+ const bridge = { onMessage: vi.fn(), dispose: vi.fn() };
331
+ const createBridgeA = vi.fn(() => bridge);
332
+ const createBridgeB = vi.fn(() => bridge);
333
+ const interruptedRender = vi.fn();
334
+ const pending = new Promise<never>(() => {});
335
+ const Block = () => {
336
+ interruptedRender();
337
+ throw pending;
338
+ };
339
+ const view = (createBridge: typeof createBridgeA, blocked: boolean) => (
340
+ <Suspense fallback={null}>
341
+ <SandboxHost
342
+ content={{ html: "" }}
343
+ contentKey="k"
344
+ createBridge={createBridge}
345
+ />
346
+ {blocked ? <Block /> : null}
347
+ </Suspense>
348
+ );
349
+
350
+ await act(async () => {
351
+ root.render(view(createBridgeA, false));
352
+ });
353
+ act(() => {
354
+ startTransition(() => root.render(view(createBridgeB, true)));
355
+ });
356
+ await vi.waitFor(() => expect(interruptedRender).toHaveBeenCalled());
357
+ await act(async () => {
358
+ resolveRender(rendered);
359
+ await new Promise((resolve) => setTimeout(resolve, 0));
360
+ });
361
+
362
+ expect(createBridgeA).toHaveBeenCalledTimes(1);
363
+ expect(createBridgeB).not.toHaveBeenCalled();
364
+ });
321
365
  });
@@ -1,6 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { type CSSProperties, useEffect, useRef, useState } from "react";
3
+ import {
4
+ type CSSProperties,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
4
10
  import {
5
11
  type RenderedFrame,
6
12
  SafeContentFrame,
@@ -81,8 +87,15 @@ export function SandboxHost({
81
87
  undefined,
82
88
  );
83
89
 
84
- const liveRef = useRef<LiveSnapshot>(null!);
85
- liveRef.current = { content, sandbox, createBridge, onError };
90
+ const liveRef = useRef<LiveSnapshot>({
91
+ content,
92
+ sandbox,
93
+ createBridge,
94
+ onError,
95
+ });
96
+ useLayoutEffect(() => {
97
+ liveRef.current = { content, sandbox, createBridge, onError };
98
+ }, [content, sandbox, createBridge, onError]);
86
99
 
87
100
  useEffect(() => {
88
101
  const container = containerRef.current;
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
3
  import { act, render, waitFor } from "@testing-library/react";
4
- import type { FC, PropsWithChildren } from "react";
4
+ import { useState, type FC, type PropsWithChildren } from "react";
5
5
  import { describe, expect, it } from "vitest";
6
6
  import {
7
7
  RuntimeAdapterProvider,
@@ -107,6 +107,36 @@ describe("RemoteThreadListAdapter.unstable_Provider", () => {
107
107
  expect(capture.adapters?.history).toBe(dummyHistory);
108
108
  });
109
109
 
110
+ it("picks up a post-mount useAdapters bag", async () => {
111
+ const capture: { adapters: CapturedAdapters } = { adapters: null };
112
+ const firstHistory: ThreadHistoryAdapter = {
113
+ load: async () => ({ messages: [] }),
114
+ append: async () => {},
115
+ };
116
+ const secondHistory: ThreadHistoryAdapter = {
117
+ load: async () => ({ messages: [] }),
118
+ append: async () => {},
119
+ };
120
+
121
+ let bump: (() => void) | undefined;
122
+ const adapter = makeAdapter({
123
+ unstable_useAdapters: function useTestAdapters() {
124
+ const [swapped, setSwapped] = useState(false);
125
+ bump = () => setSwapped(true);
126
+ return { history: swapped ? secondHistory : firstHistory };
127
+ },
128
+ });
129
+
130
+ await renderAndWaitForBinder(adapter, capture);
131
+ expect(capture.adapters?.history).toBe(firstHistory);
132
+
133
+ await act(async () => {
134
+ bump!();
135
+ });
136
+
137
+ expect(capture.adapters?.history).toBe(secondHistory);
138
+ });
139
+
110
140
  it("picks up a swapped Provider on re-render", async () => {
111
141
  const capture: { adapters: CapturedAdapters } = { adapters: null };
112
142
  const firstHistory: ThreadHistoryAdapter = {
@@ -1,5 +1,6 @@
1
1
  /** @vitest-environment jsdom */
2
- import { act, renderHook } from "@testing-library/react";
2
+ import { startTransition, Suspense } from "react";
3
+ import { act, render, renderHook } from "@testing-library/react";
3
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
5
  import type { Unstable_TriggerItem } from "@assistant-ui/core";
5
6
  import { unstable_useLiveCompletionAdapter } from "./useLiveCompletionAdapter";
@@ -230,6 +231,64 @@ describe("unstable_useLiveCompletionAdapter", () => {
230
231
  expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
231
232
  });
232
233
 
234
+ it("keeps pending queries scoped to the committed fetcher", async () => {
235
+ const fetcherA = vi.fn(async () => [item("workspace-a")]);
236
+ const fetcherB = vi.fn(async () => [item("workspace-b")]);
237
+ const interruptedRender = vi.fn();
238
+ const pending = new Promise<never>(() => {});
239
+ let adapter!: ReturnType<
240
+ typeof unstable_useLiveCompletionAdapter
241
+ >["adapter"];
242
+ const Harness = ({
243
+ fetcher,
244
+ cacheKey,
245
+ blocked,
246
+ }: {
247
+ fetcher: typeof fetcherA;
248
+ cacheKey: string;
249
+ blocked: boolean;
250
+ }) => {
251
+ const result = unstable_useLiveCompletionAdapter({
252
+ fetcher,
253
+ cacheKey,
254
+ debounceMs: 50,
255
+ });
256
+ if (blocked) {
257
+ interruptedRender();
258
+ throw pending;
259
+ }
260
+ adapter = result.adapter;
261
+ return null;
262
+ };
263
+ const view = (
264
+ fetcher: typeof fetcherA,
265
+ cacheKey: string,
266
+ blocked: boolean,
267
+ ) => (
268
+ <Suspense fallback={null}>
269
+ <Harness fetcher={fetcher} cacheKey={cacheKey} blocked={blocked} />
270
+ </Suspense>
271
+ );
272
+ const rendered = render(view(fetcherA, "workspace-a", false));
273
+
274
+ await act(async () => {
275
+ adapter.search!("alice");
276
+ await vi.advanceTimersByTimeAsync(0);
277
+ });
278
+ act(() => {
279
+ startTransition(() =>
280
+ rendered.rerender(view(fetcherB, "workspace-b", true)),
281
+ );
282
+ });
283
+ expect(interruptedRender).toHaveBeenCalled();
284
+ await act(async () => {
285
+ await vi.advanceTimersByTimeAsync(50);
286
+ });
287
+
288
+ expect(fetcherA).toHaveBeenCalledOnce();
289
+ expect(fetcherB).not.toHaveBeenCalled();
290
+ });
291
+
233
292
  it("drops a pending result when only the cache key changes", async () => {
234
293
  let resolveFirst!: (items: readonly Unstable_TriggerItem[]) => void;
235
294
  const first = new Promise<readonly Unstable_TriggerItem[]>((resolve) => {
@@ -74,7 +74,10 @@ export function unstable_useLiveCompletionAdapter(
74
74
  const [isLoading, setIsLoading] = useState(false);
75
75
 
76
76
  const fetcherRef = useRef(fetcher);
77
- fetcherRef.current = fetcher;
77
+ // The debounce timer must only observe fetchers from committed renders.
78
+ useLayoutEffect(() => {
79
+ fetcherRef.current = fetcher;
80
+ }, [fetcher]);
78
81
 
79
82
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
80
83
  const tokenRef = useRef(0);
@@ -0,0 +1,141 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { startTransition, Suspense, type PropsWithChildren } from "react";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import {
7
+ unstable_useSlashCommandAdapter,
8
+ type Unstable_SlashCommand,
9
+ } from "./useSlashCommandAdapter";
10
+
11
+ const command = (
12
+ label: string,
13
+ execute: () => void,
14
+ ): Unstable_SlashCommand => ({
15
+ id: "run",
16
+ label,
17
+ execute,
18
+ });
19
+
20
+ describe("unstable_useSlashCommandAdapter", () => {
21
+ it("keeps commands scoped to committed renders", () => {
22
+ const executeA = vi.fn();
23
+ const executeB = vi.fn();
24
+ const commandsA = [command("Workspace A", executeA)];
25
+ const commandsB = [command("Workspace B", executeB)];
26
+ const renderedLabel = vi.fn();
27
+ const interruptedRender = vi.fn();
28
+ const pending = new Promise<never>(() => {});
29
+ let blocked = false;
30
+
31
+ const Blocker = () => {
32
+ if (blocked) {
33
+ interruptedRender();
34
+ throw pending;
35
+ }
36
+ return null;
37
+ };
38
+ const Wrapper = ({ children }: PropsWithChildren) => (
39
+ <Suspense fallback={null}>
40
+ {children}
41
+ <Blocker />
42
+ </Suspense>
43
+ );
44
+
45
+ const { result, rerender } = renderHook(
46
+ ({ commands }) => {
47
+ renderedLabel(commands[0]?.label);
48
+ return unstable_useSlashCommandAdapter({ commands });
49
+ },
50
+ {
51
+ initialProps: { commands: commandsA },
52
+ wrapper: Wrapper,
53
+ },
54
+ );
55
+
56
+ act(() => {
57
+ blocked = true;
58
+ startTransition(() => rerender({ commands: commandsB }));
59
+ });
60
+
61
+ expect(interruptedRender).toHaveBeenCalled();
62
+ expect(renderedLabel).toHaveBeenCalledWith("Workspace B");
63
+ expect(result.current.adapter.search?.("")).toEqual([
64
+ { id: "run", type: "command", label: "Workspace A" },
65
+ ]);
66
+
67
+ result.current.action.onExecute({
68
+ id: "run",
69
+ type: "command",
70
+ label: "Workspace A",
71
+ });
72
+ expect(executeA).toHaveBeenCalledOnce();
73
+ expect(executeB).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it("updates search while keeping actions stable after commands commit", () => {
77
+ const executeA = vi.fn();
78
+ const executeB = vi.fn();
79
+ const commandsA = [command("Workspace A", executeA)];
80
+ const commandsB = [command("Workspace B", executeB)];
81
+ const { result, rerender } = renderHook(
82
+ ({ commands }) => unstable_useSlashCommandAdapter({ commands }),
83
+ { initialProps: { commands: commandsA } },
84
+ );
85
+ const initial = result.current;
86
+
87
+ rerender({ commands: commandsB });
88
+
89
+ expect(result.current.adapter).not.toBe(initial.adapter);
90
+ expect(result.current.action).toBe(initial.action);
91
+ expect(result.current.adapter.search?.("")).toEqual([
92
+ { id: "run", type: "command", label: "Workspace B" },
93
+ ]);
94
+
95
+ result.current.action.onExecute({
96
+ id: "run",
97
+ type: "command",
98
+ label: "Workspace B",
99
+ });
100
+ expect(executeA).not.toHaveBeenCalled();
101
+ expect(executeB).toHaveBeenCalledOnce();
102
+ });
103
+
104
+ it("matches the displayed label for a command with no explicit label", () => {
105
+ const { result } = renderHook(() =>
106
+ unstable_useSlashCommandAdapter({
107
+ commands: [{ id: "summarize", execute: vi.fn() }],
108
+ }),
109
+ );
110
+
111
+ expect(result.current.adapter.search?.("/")).toEqual([
112
+ { id: "summarize", type: "command", label: "/summarize" },
113
+ ]);
114
+ expect(result.current.adapter.search?.("sum")).toEqual([
115
+ { id: "summarize", type: "command", label: "/summarize" },
116
+ ]);
117
+ expect(result.current.adapter.search?.("nope")).toEqual([]);
118
+ });
119
+
120
+ it("keeps the adapter stable for equivalent inline commands", () => {
121
+ const executeA = vi.fn();
122
+ const executeB = vi.fn();
123
+ const { result, rerender } = renderHook(
124
+ ({ execute }) =>
125
+ unstable_useSlashCommandAdapter({
126
+ commands: [{ id: "run", label: "Run", execute }],
127
+ }),
128
+ { initialProps: { execute: executeA } },
129
+ );
130
+ const initialAdapter = result.current.adapter;
131
+
132
+ rerender({ execute: executeB });
133
+
134
+ expect(result.current.adapter).toBe(initialAdapter);
135
+ act(() =>
136
+ result.current.action.onExecute(result.current.adapter.search!("")[0]!),
137
+ );
138
+ expect(executeA).not.toHaveBeenCalled();
139
+ expect(executeB).toHaveBeenCalledOnce();
140
+ });
141
+ });
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useMemo, useRef } from "react";
3
+ import { useLayoutEffect, useMemo, useRef } from "react";
4
4
  import type {
5
5
  Unstable_TriggerAdapter,
6
6
  Unstable_TriggerItem,
@@ -61,34 +61,51 @@ export function unstable_useSlashCommandAdapter(
61
61
  const { commands, removeOnExecute } = options;
62
62
 
63
63
  const commandsRef = useRef(commands);
64
- commandsRef.current = commands;
64
+ const committedItemsRef = useRef<readonly Unstable_TriggerItem[]>(undefined);
65
+ const nextItems = commands.map(toItem);
66
+ // A referential cache, never state: `items` is always structurally equal to
67
+ // `nextItems`, so the render-time read cannot change what the adapter shows.
68
+ const items = areTriggerItemsEqual(committedItemsRef.current, nextItems)
69
+ ? committedItemsRef.current
70
+ : nextItems;
65
71
 
66
- return useMemo(() => {
67
- const adapter: Unstable_TriggerAdapter = {
72
+ // The adapter's callbacks must only observe commands from committed renders.
73
+ useLayoutEffect(() => {
74
+ commandsRef.current = commands;
75
+ committedItemsRef.current = items;
76
+ }, [commands, items]);
77
+
78
+ const adapter = useMemo<Unstable_TriggerAdapter>(
79
+ () => ({
68
80
  categories: () => [],
69
81
  categoryItems: () => [],
70
82
  search: (query: string) => {
71
83
  const lower = query.toLowerCase();
72
- return commandsRef.current
73
- .filter((c) => matchesQuery(c, lower))
74
- .map(toItem);
84
+ return items.filter((item) => matchesQuery(item, lower));
75
85
  },
76
- };
86
+ }),
87
+ [items],
88
+ );
77
89
 
78
- const action: Unstable_SlashCommandAction = {
90
+ const action = useMemo<Unstable_SlashCommandAction>(
91
+ () => ({
79
92
  onExecute: (item) => {
80
93
  commandsRef.current.find((c) => c.id === item.id)?.execute();
81
94
  },
82
95
  ...(removeOnExecute !== undefined ? { removeOnExecute } : {}),
83
- };
96
+ }),
97
+ [removeOnExecute],
98
+ );
84
99
 
85
- return {
100
+ return useMemo(
101
+ () => ({
86
102
  adapter,
87
103
  action,
88
104
  ...(options.iconMap ? { iconMap: options.iconMap } : {}),
89
105
  ...(options.fallbackIcon ? { fallbackIcon: options.fallbackIcon } : {}),
90
- };
91
- }, [removeOnExecute, options.iconMap, options.fallbackIcon]);
106
+ }),
107
+ [adapter, action, options.iconMap, options.fallbackIcon],
108
+ );
92
109
  }
93
110
 
94
111
  function toItem(cmd: Unstable_SlashCommand): Unstable_TriggerItem {
@@ -101,10 +118,28 @@ function toItem(cmd: Unstable_SlashCommand): Unstable_TriggerItem {
101
118
  };
102
119
  }
103
120
 
104
- function matchesQuery(cmd: Unstable_SlashCommand, lower: string): boolean {
121
+ function matchesQuery(item: Unstable_TriggerItem, lower: string): boolean {
105
122
  if (!lower) return true;
106
- if (cmd.id.toLowerCase().includes(lower)) return true;
107
- if (cmd.label?.toLowerCase().includes(lower)) return true;
108
- if (cmd.description?.toLowerCase().includes(lower)) return true;
123
+ if (item.id.toLowerCase().includes(lower)) return true;
124
+ if (item.label.toLowerCase().includes(lower)) return true;
125
+ if (item.description?.toLowerCase().includes(lower)) return true;
109
126
  return false;
110
127
  }
128
+
129
+ function areTriggerItemsEqual(
130
+ previous: readonly Unstable_TriggerItem[] | undefined,
131
+ next: readonly Unstable_TriggerItem[],
132
+ ): previous is readonly Unstable_TriggerItem[] {
133
+ if (!previous || previous.length !== next.length) return false;
134
+
135
+ return previous.every((item, index) => {
136
+ const nextItem = next[index];
137
+ return (
138
+ nextItem !== undefined &&
139
+ item.id === nextItem.id &&
140
+ item.label === nextItem.label &&
141
+ item.description === nextItem.description &&
142
+ item.metadata?.icon === nextItem.metadata?.icon
143
+ );
144
+ });
145
+ }
@@ -11,10 +11,10 @@ import {
11
11
  forwardRef,
12
12
  isValidElement,
13
13
  } from "react";
14
- import { Primitive as RadixPrimitive } from "@radix-ui/react-primitive";
14
+ import { Primitive as RadixPrimitive } from "radix-ui/internal";
15
15
 
16
16
  /**
17
- * Thin wrapper around `@radix-ui/react-primitive` that adds `render` prop support.
17
+ * Thin wrapper around Radix `Primitive` that adds `render` prop support.
18
18
  *
19
19
  * When `render` is provided, it is converted to the equivalent `asChild` pattern:
20
20
  * render={<Comp props />} + children → asChild + <Comp props>{children}</Comp>
@@ -5,7 +5,7 @@ import {
5
5
  type MouseEventHandler,
6
6
  } from "react";
7
7
  import { Primitive } from "./Primitive";
8
- import { composeEventHandlers } from "@radix-ui/primitive";
8
+ import { composeEventHandlers } from "radix-ui/internal";
9
9
 
10
10
  type ActionButtonCallback<TProps> = (
11
11
  props: TProps,
@@ -1,4 +1,4 @@
1
- import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
1
+ import { useCallbackRef } from "radix-ui/internal";
2
2
  import { useCallback } from "react";
3
3
  import { useManagedRef } from "./useManagedRef";
4
4
 
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
3
+ import { useCallbackRef } from "radix-ui/internal";
4
4
  import { useEffect } from "react";
5
5
  import { useThreadViewport } from "../../context/react/ThreadViewportContext";
6
6
 
@@ -8,7 +8,7 @@ import type {
8
8
  TextMessagePart,
9
9
  MessagePartState,
10
10
  } from "@assistant-ui/core";
11
- import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
11
+ import { useCallbackRef } from "radix-ui/internal";
12
12
  import { useMediaQuery } from "../hooks/useMediaQuery";
13
13
  import { useSmoothStatusStore } from "./SmoothContext";
14
14
  import { writableStore } from "../../context/ReadonlyStore";
@@ -1,3 +0,0 @@
1
- import { MessagePartRuntime, MessagePartState } from "@assistant-ui/core";
2
- import { MessagePartRuntimeImpl } from "@assistant-ui/core/internal";
3
- export { type MessagePartRuntime, MessagePartRuntimeImpl, type MessagePartState };
@@ -1,2 +0,0 @@
1
- import { MessagePartRuntimeImpl } from "@assistant-ui/core/internal";
2
- export { MessagePartRuntimeImpl };
@@ -1,3 +0,0 @@
1
- import { MessageRuntime, MessageState } from "@assistant-ui/core";
2
- import { MessageRuntimeImpl, MessageStateBinding } from "@assistant-ui/core/internal";
3
- export { type MessageRuntime, MessageRuntimeImpl, type MessageState, type MessageStateBinding };
@@ -1,2 +0,0 @@
1
- import { MessageRuntimeImpl } from "@assistant-ui/core/internal";
2
- export { MessageRuntimeImpl };
@@ -1,3 +0,0 @@
1
- import { ThreadListRuntime, ThreadListState } from "@assistant-ui/core";
2
- import { ThreadListRuntimeCoreBinding, ThreadListRuntimeImpl } from "@assistant-ui/core/internal";
3
- export { type ThreadListRuntime, type ThreadListRuntimeCoreBinding, ThreadListRuntimeImpl, type ThreadListState };
@@ -1,2 +0,0 @@
1
- import { ThreadListRuntimeImpl } from "@assistant-ui/core/internal";
2
- export { ThreadListRuntimeImpl };
@@ -1,2 +0,0 @@
1
- export type { MessagePartState, MessagePartRuntime } from "@assistant-ui/core";
2
- export { MessagePartRuntimeImpl } from "@assistant-ui/core/internal";
@@ -1,3 +0,0 @@
1
- export type { MessageState, MessageRuntime } from "@assistant-ui/core";
2
- export type { MessageStateBinding } from "@assistant-ui/core/internal";
3
- export { MessageRuntimeImpl } from "@assistant-ui/core/internal";
@@ -1,4 +0,0 @@
1
- export type { ThreadListState, ThreadListRuntime } from "@assistant-ui/core";
2
-
3
- export type { ThreadListRuntimeCoreBinding } from "@assistant-ui/core/internal";
4
- export { ThreadListRuntimeImpl } from "@assistant-ui/core/internal";