@assistant-ui/react 0.14.13 → 0.14.15

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 (214) hide show
  1. package/README.md +5 -1
  2. package/dist/client/ExternalThread.d.ts +2 -12
  3. package/dist/client/ExternalThread.d.ts.map +1 -1
  4. package/dist/client/ExternalThread.js +30 -29
  5. package/dist/client/ExternalThread.js.map +1 -1
  6. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  7. package/dist/client/InMemoryThreadList.js +24 -13
  8. package/dist/client/InMemoryThreadList.js.map +1 -1
  9. package/dist/client/SingleThreadList.d.ts.map +1 -1
  10. package/dist/client/SingleThreadList.js +12 -8
  11. package/dist/client/SingleThreadList.js.map +1 -1
  12. package/dist/context/providers/ThreadViewportProvider.js +1 -1
  13. package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
  14. package/dist/context/react/ThreadViewportContext.js +1 -1
  15. package/dist/context/react/utils/createContextHook.js +1 -1
  16. package/dist/context/react/utils/ensureBinding.js.map +1 -1
  17. package/dist/context/react/utils/useRuntimeState.js +1 -1
  18. package/dist/context/stores/ThreadViewport.js.map +1 -1
  19. package/dist/devtools/DevToolsHooks.js.map +1 -1
  20. package/dist/index.d.ts +4 -4
  21. package/dist/index.js +3 -3
  22. package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
  23. package/dist/legacy-runtime/cloud/auiV0.js +1 -1
  24. package/dist/legacy-runtime/hooks/AssistantContext.js.map +1 -1
  25. package/dist/legacy-runtime/hooks/AttachmentContext.js.map +1 -1
  26. package/dist/legacy-runtime/hooks/ComposerContext.js.map +1 -1
  27. package/dist/legacy-runtime/hooks/MessageContext.js.map +1 -1
  28. package/dist/legacy-runtime/hooks/MessagePartContext.js.map +1 -1
  29. package/dist/legacy-runtime/hooks/ThreadContext.js +1 -1
  30. package/dist/legacy-runtime/hooks/ThreadContext.js.map +1 -1
  31. package/dist/legacy-runtime/hooks/ThreadListItemContext.js.map +1 -1
  32. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +1 -1
  33. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +14 -0
  34. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +1 -0
  35. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +101 -0
  36. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +1 -0
  37. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +1 -1
  38. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  39. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +13 -2
  40. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  41. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +1 -1
  42. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +1 -1
  43. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  44. package/dist/mcp-apps/McpAppRenderer.js +7 -7
  45. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  46. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  47. package/dist/mcp-apps/McpAppsRemoteHost.js +5 -4
  48. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  49. package/dist/mcp-apps/app-frame.d.ts +1 -1
  50. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  51. package/dist/mcp-apps/app-frame.js +82 -104
  52. package/dist/mcp-apps/app-frame.js.map +1 -1
  53. package/dist/mcp-apps/bridge.d.ts +3 -3
  54. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  55. package/dist/mcp-apps/bridge.js +35 -10
  56. package/dist/mcp-apps/bridge.js.map +1 -1
  57. package/dist/mcp-apps/types.d.ts +2 -12
  58. package/dist/mcp-apps/types.d.ts.map +1 -1
  59. package/dist/mcp-apps/types.js.map +1 -1
  60. package/dist/model-context/frame/useAssistantFrameHost.js +1 -1
  61. package/dist/model-context/makeAssistantVisible.js +1 -1
  62. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  63. package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
  64. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -1
  65. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  66. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
  67. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
  68. package/dist/primitives/actionBar/ActionBarInteractionContext.js +1 -1
  69. package/dist/primitives/actionBar/ActionBarRoot.js +1 -1
  70. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +1 -1
  71. package/dist/primitives/actionBarMore/ActionBarMoreContent.js +1 -1
  72. package/dist/primitives/actionBarMore/ActionBarMoreItem.js +1 -1
  73. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +1 -1
  74. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js +1 -1
  75. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js +1 -1
  76. package/dist/primitives/assistantModal/AssistantModalAnchor.js +1 -1
  77. package/dist/primitives/assistantModal/AssistantModalContent.js +1 -1
  78. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
  79. package/dist/primitives/assistantModal/AssistantModalTrigger.js +1 -1
  80. package/dist/primitives/attachment/AttachmentRemove.js +1 -1
  81. package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
  82. package/dist/primitives/attachment/AttachmentRoot.js +1 -1
  83. package/dist/primitives/attachment/AttachmentThumb.js +1 -1
  84. package/dist/primitives/branchPicker/BranchPickerRoot.js +1 -1
  85. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
  86. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
  87. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.js +1 -1
  88. package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
  89. package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
  90. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  91. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  92. package/dist/primitives/composer/ComposerDictationTranscript.js +1 -1
  93. package/dist/primitives/composer/ComposerInput.js +1 -1
  94. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  95. package/dist/primitives/composer/ComposerInputPluginContext.js +1 -1
  96. package/dist/primitives/composer/ComposerQuote.js +1 -1
  97. package/dist/primitives/composer/ComposerQuote.js.map +1 -1
  98. package/dist/primitives/composer/ComposerRoot.js +1 -1
  99. package/dist/primitives/composer/ComposerSend.js +1 -1
  100. package/dist/primitives/composer/ComposerStopDictation.js +1 -1
  101. package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
  102. package/dist/primitives/composer/trigger/TriggerPopover.js +2 -2
  103. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  104. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +1 -1
  105. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +1 -1
  106. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +1 -1
  107. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +1 -1
  108. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +1 -1
  109. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  110. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +8 -7
  111. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  112. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +1 -1
  113. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  114. package/dist/primitives/composer/trigger/triggerDetectionResource.js +5 -4
  115. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  116. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  117. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +8 -7
  118. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  119. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  120. package/dist/primitives/composer/trigger/triggerNavigationResource.js +13 -12
  121. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  122. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  123. package/dist/primitives/composer/trigger/triggerSelectionResource.js +7 -6
  124. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  125. package/dist/primitives/error/ErrorMessage.js +1 -1
  126. package/dist/primitives/error/ErrorRoot.js +1 -1
  127. package/dist/primitives/message/MessagePartsGrouped.js +1 -1
  128. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  129. package/dist/primitives/message/MessageRoot.js +1 -1
  130. package/dist/primitives/message/MessageRoot.js.map +1 -1
  131. package/dist/primitives/messagePart/MessagePartImage.js +1 -1
  132. package/dist/primitives/messagePart/MessagePartText.js +1 -1
  133. package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
  134. package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
  135. package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
  136. package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
  137. package/dist/primitives/queueItem/QueueItemText.js +1 -1
  138. package/dist/primitives/reasoning/useScrollLock.js +1 -1
  139. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  140. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
  141. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
  142. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +1 -1
  143. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  144. package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
  145. package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
  146. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  147. package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
  148. package/dist/primitives/thread/ThreadRoot.js +1 -1
  149. package/dist/primitives/thread/ThreadScrollToBottom.js +1 -1
  150. package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
  151. package/dist/primitives/thread/ThreadViewport.js +1 -1
  152. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  153. package/dist/primitives/thread/ThreadViewportFooter.js +1 -1
  154. package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
  155. package/dist/primitives/thread/topAnchor/topAnchorTurn.js.map +1 -1
  156. package/dist/primitives/thread/topAnchor/topAnchorUtils.js.map +1 -1
  157. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +1 -1
  158. package/dist/primitives/thread/useThreadViewportAutoScroll.js +1 -1
  159. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  160. package/dist/primitives/threadList/ThreadListNew.js +1 -1
  161. package/dist/primitives/threadList/ThreadListRoot.js +1 -1
  162. package/dist/primitives/threadListItem/ThreadListItemRoot.js +1 -1
  163. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -1
  164. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js +1 -1
  165. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js +1 -1
  166. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -1
  167. package/dist/sandbox-host/SandboxHost.d.ts +50 -0
  168. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -0
  169. package/dist/sandbox-host/SandboxHost.js +85 -0
  170. package/dist/sandbox-host/SandboxHost.js.map +1 -0
  171. package/dist/unstable/useMentionAdapter.d.ts +2 -2
  172. package/dist/unstable/useMentionAdapter.js +2 -2
  173. package/dist/unstable/useMentionAdapter.js.map +1 -1
  174. package/dist/unstable/useSlashCommandAdapter.js +1 -1
  175. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  176. package/dist/utils/Primitive.js +1 -1
  177. package/dist/utils/createActionButton.js +1 -1
  178. package/dist/utils/createActionButton.js.map +1 -1
  179. package/dist/utils/hooks/useManagedRef.js +1 -1
  180. package/dist/utils/hooks/useMediaQuery.js +1 -1
  181. package/dist/utils/hooks/useMediaQuery.js.map +1 -1
  182. package/dist/utils/hooks/useOnResizeContent.js +1 -1
  183. package/dist/utils/hooks/useOnScrollToBottom.js +1 -1
  184. package/dist/utils/hooks/useSizeHandle.js +1 -1
  185. package/dist/utils/json/is-json.js.map +1 -1
  186. package/dist/utils/smooth/SmoothContext.js +1 -1
  187. package/dist/utils/smooth/SmoothContext.js.map +1 -1
  188. package/dist/utils/smooth/useSmooth.js +1 -1
  189. package/dist/utils/smooth/useSmooth.js.map +1 -1
  190. package/package.json +21 -20
  191. package/src/client/ExternalThread.ts +484 -515
  192. package/src/client/InMemoryThreadList.ts +154 -142
  193. package/src/client/SingleThreadList.ts +88 -81
  194. package/src/context/providers/ThreadViewportProvider.tsx +2 -2
  195. package/src/index.ts +18 -3
  196. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +426 -0
  197. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +146 -0
  198. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +16 -1
  199. package/src/mcp-apps/McpAppRenderer.tsx +28 -35
  200. package/src/mcp-apps/McpAppsRemoteHost.ts +25 -24
  201. package/src/mcp-apps/app-frame.tsx +100 -141
  202. package/src/mcp-apps/bridge.test.ts +100 -60
  203. package/src/mcp-apps/bridge.ts +43 -21
  204. package/src/mcp-apps/types.ts +2 -12
  205. package/src/primitives/composer/trigger/TriggerPopover.tsx +1 -1
  206. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +75 -76
  207. package/src/primitives/composer/trigger/triggerDetectionResource.ts +6 -5
  208. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +9 -13
  209. package/src/primitives/composer/trigger/triggerNavigationResource.ts +14 -19
  210. package/src/primitives/composer/trigger/triggerSelectionResource.ts +8 -7
  211. package/src/sandbox-host/SandboxHost.test.tsx +231 -0
  212. package/src/sandbox-host/SandboxHost.tsx +185 -0
  213. package/src/tests/local-runtime-queue.test.tsx +305 -0
  214. package/src/unstable/useMentionAdapter.ts +2 -2
@@ -1,4 +1,5 @@
1
- import { resource, tapMemo, tapState } from "@assistant-ui/tap";
1
+ import { useMemo, useState } from "react";
2
+ import { resource } from "@assistant-ui/tap";
2
3
  import { detectTrigger } from "./detectTrigger";
3
4
 
4
5
  /** Detected trigger position within the composer text. */
@@ -18,16 +19,16 @@ export type TriggerDetectionResourceOutput = {
18
19
 
19
20
  /** Tracks cursor position and derives the active trigger + query from composer text. */
20
21
  export const TriggerDetectionResource = resource(
21
- ({
22
+ function TriggerDetectionResource({
22
23
  text,
23
24
  triggerChar,
24
25
  }: {
25
26
  text: string;
26
27
  triggerChar: string;
27
- }): TriggerDetectionResourceOutput => {
28
- const [cursorPosition, setCursorPosition] = tapState(text.length);
28
+ }): TriggerDetectionResourceOutput {
29
+ const [cursorPosition, setCursorPosition] = useState(text.length);
29
30
 
30
- const trigger = tapMemo(() => {
31
+ const trigger = useMemo(() => {
31
32
  const pos = Math.min(cursorPosition, text.length);
32
33
  return detectTrigger(text, triggerChar, pos);
33
34
  }, [cursorPosition, text, triggerChar]);
@@ -1,9 +1,5 @@
1
- import {
2
- resource,
3
- tapEffect,
4
- tapEffectEvent,
5
- tapState,
6
- } from "@assistant-ui/tap";
1
+ import { useEffect, useEffectEvent, useState } from "react";
2
+ import { resource } from "@assistant-ui/tap";
7
3
  import type {
8
4
  Unstable_TriggerCategory,
9
5
  Unstable_TriggerItem,
@@ -39,7 +35,7 @@ export type TriggerKeyboardResourceOutput = {
39
35
  * category drill-in, back, and close to the callbacks supplied by the parent.
40
36
  */
41
37
  export const TriggerKeyboardResource = resource(
42
- ({
38
+ function TriggerKeyboardResource({
43
39
  navigableList,
44
40
  isSearchMode,
45
41
  activeCategoryId,
@@ -61,24 +57,24 @@ export const TriggerKeyboardResource = resource(
61
57
  selectCategory: (categoryId: string) => void;
62
58
  goBack: () => void;
63
59
  close: () => void;
64
- }): TriggerKeyboardResourceOutput => {
65
- const [highlightedIndex, setHighlightedIndex] = tapState(0);
60
+ }): TriggerKeyboardResourceOutput {
61
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
66
62
 
67
- tapEffect(() => {
63
+ useEffect(() => {
68
64
  setHighlightedIndex(0);
69
65
  }, [navigableList]);
70
66
 
71
- tapEffect(() => {
67
+ useEffect(() => {
72
68
  setHighlightedIndex(0);
73
69
  }, [isSearchMode, activeCategoryId]);
74
70
 
75
- const highlightIndex = tapEffectEvent((index: number) => {
71
+ const highlightIndex = useEffectEvent((index: number) => {
76
72
  if (index < 0 || index >= navigableList.length) return;
77
73
  if (index === highlightedIndex) return;
78
74
  setHighlightedIndex(index);
79
75
  });
80
76
 
81
- const handleKeyDown = tapEffectEvent(
77
+ const handleKeyDown = useEffectEvent(
82
78
  (e: TriggerPopoverKeyEvent): boolean => {
83
79
  if (!open) return false;
84
80
 
@@ -1,10 +1,5 @@
1
- import {
2
- resource,
3
- tapEffect,
4
- tapEffectEvent,
5
- tapMemo,
6
- tapState,
7
- } from "@assistant-ui/tap";
1
+ import { useEffect, useEffectEvent, useMemo, useState } from "react";
2
+ import { resource } from "@assistant-ui/tap";
8
3
  import type {
9
4
  Unstable_TriggerAdapter,
10
5
  Unstable_TriggerCategory,
@@ -44,7 +39,7 @@ export type TriggerNavigationResourceOutput = {
44
39
  * adapter + current query. Pure derivation — no side effects on the composer.
45
40
  */
46
41
  export const TriggerNavigationResource = resource(
47
- ({
42
+ function TriggerNavigationResource({
48
43
  adapter,
49
44
  query,
50
45
  open,
@@ -52,28 +47,28 @@ export const TriggerNavigationResource = resource(
52
47
  adapter: Unstable_TriggerAdapter | undefined;
53
48
  query: string;
54
49
  open: boolean;
55
- }): TriggerNavigationResourceOutput => {
56
- const [activeCategoryId, setActiveCategoryId] = tapState<string | null>(
50
+ }): TriggerNavigationResourceOutput {
51
+ const [activeCategoryId, setActiveCategoryId] = useState<string | null>(
57
52
  null,
58
53
  );
59
54
 
60
- tapEffect(() => {
55
+ useEffect(() => {
61
56
  if (!open) setActiveCategoryId(null);
62
57
  }, [open]);
63
58
 
64
- const categories = tapMemo<readonly Unstable_TriggerCategory[]>(() => {
59
+ const categories = useMemo<readonly Unstable_TriggerCategory[]>(() => {
65
60
  if (!open || !adapter) return [];
66
61
  return adapter.categories();
67
62
  }, [open, adapter]);
68
63
 
69
64
  const effectiveActiveCategoryId = open ? activeCategoryId : null;
70
65
 
71
- const allItems = tapMemo<readonly Unstable_TriggerItem[]>(() => {
66
+ const allItems = useMemo<readonly Unstable_TriggerItem[]>(() => {
72
67
  if (!effectiveActiveCategoryId || !adapter) return [];
73
68
  return adapter.categoryItems(effectiveActiveCategoryId);
74
69
  }, [effectiveActiveCategoryId, adapter]);
75
70
 
76
- const searchResults = tapMemo<
71
+ const searchResults = useMemo<
77
72
  readonly Unstable_TriggerItem[] | null
78
73
  >(() => {
79
74
  if (!open || !adapter || effectiveActiveCategoryId) return null;
@@ -96,7 +91,7 @@ export const TriggerNavigationResource = resource(
96
91
 
97
92
  const isSearchMode = searchResults !== null;
98
93
 
99
- const filteredCategories = tapMemo(() => {
94
+ const filteredCategories = useMemo(() => {
100
95
  if (isSearchMode) return [];
101
96
  if (!query) return categories;
102
97
  const lower = query.toLowerCase();
@@ -105,14 +100,14 @@ export const TriggerNavigationResource = resource(
105
100
  );
106
101
  }, [categories, query, isSearchMode]);
107
102
 
108
- const filteredItems = tapMemo(() => {
103
+ const filteredItems = useMemo(() => {
109
104
  if (isSearchMode) return searchResults ?? [];
110
105
  if (!query) return allItems;
111
106
  const lower = query.toLowerCase();
112
107
  return allItems.filter((item) => matchesQuery(item, lower));
113
108
  }, [allItems, query, isSearchMode, searchResults]);
114
109
 
115
- const navigableList = tapMemo(() => {
110
+ const navigableList = useMemo(() => {
116
111
  if (isSearchMode) return searchResults ?? [];
117
112
  if (effectiveActiveCategoryId) return filteredItems;
118
113
  return filteredCategories;
@@ -124,11 +119,11 @@ export const TriggerNavigationResource = resource(
124
119
  filteredCategories,
125
120
  ]);
126
121
 
127
- const selectCategory = tapEffectEvent((categoryId: string) => {
122
+ const selectCategory = useEffectEvent((categoryId: string) => {
128
123
  setActiveCategoryId(categoryId);
129
124
  });
130
125
 
131
- const goBack = tapEffectEvent(() => {
126
+ const goBack = useEffectEvent(() => {
132
127
  setActiveCategoryId(null);
133
128
  });
134
129
 
@@ -1,4 +1,5 @@
1
- import { resource, tapEffectEvent, tapRef } from "@assistant-ui/tap";
1
+ import { useEffectEvent, useRef } from "react";
2
+ import { resource } from "@assistant-ui/tap";
2
3
  import type {
3
4
  Unstable_DirectiveFormatter,
4
5
  Unstable_TriggerItem,
@@ -33,7 +34,7 @@ export type TriggerSelectionResourceOutput = {
33
34
 
34
35
  /** Owns composer text mutation + behavior dispatch on item selection. */
35
36
  export const TriggerSelectionResource = resource(
36
- ({
37
+ function TriggerSelectionResource({
37
38
  behavior,
38
39
  trigger,
39
40
  aui,
@@ -48,12 +49,12 @@ export const TriggerSelectionResource = resource(
48
49
  setCursorPosition: (pos: number) => void;
49
50
  /** Called after a successful selection so the parent can reset nav state. */
50
51
  onSelected: () => void;
51
- }): TriggerSelectionResourceOutput => {
52
+ }): TriggerSelectionResourceOutput {
52
53
  // Select-item override: lets Lexical's DirectivePlugin intercept selection
53
54
  // and drive its own node insertion.
54
- const selectItemOverrideRef = tapRef<SelectItemOverride | null>(null);
55
+ const selectItemOverrideRef = useRef<SelectItemOverride | null>(null);
55
56
 
56
- const registerSelectItemOverride = tapEffectEvent(
57
+ const registerSelectItemOverride = useEffectEvent(
57
58
  (fn: SelectItemOverride) => {
58
59
  selectItemOverrideRef.current = fn;
59
60
  return () => {
@@ -64,7 +65,7 @@ export const TriggerSelectionResource = resource(
64
65
  },
65
66
  );
66
67
 
67
- const selectItem = tapEffectEvent((item: Unstable_TriggerItem) => {
68
+ const selectItem = useEffectEvent((item: Unstable_TriggerItem) => {
68
69
  if (!trigger || !behavior) return;
69
70
 
70
71
  if (selectItemOverrideRef.current?.(item)) {
@@ -105,7 +106,7 @@ export const TriggerSelectionResource = resource(
105
106
  onSelected();
106
107
  });
107
108
 
108
- const close = tapEffectEvent(() => {
109
+ const close = useEffectEvent(() => {
109
110
  onSelected();
110
111
  // Move cursor before the trigger so trigger detection deactivates
111
112
  if (trigger) {
@@ -0,0 +1,231 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const { renderHtmlMock } = vi.hoisted(() => ({ renderHtmlMock: vi.fn() }));
7
+
8
+ vi.mock("safe-content-frame", () => ({
9
+ SafeContentFrame: class {
10
+ renderHtml = renderHtmlMock;
11
+ },
12
+ }));
13
+
14
+ import {
15
+ SandboxHost,
16
+ isSandboxFrameMessage,
17
+ type SandboxBridge,
18
+ type SandboxHostApi,
19
+ } from "./SandboxHost";
20
+
21
+ const validData = { jsonrpc: "2.0", method: "x" };
22
+
23
+ function makeFrame() {
24
+ const iframe = document.createElement("iframe");
25
+ document.body.appendChild(iframe);
26
+ return { iframe, origin: "https://app.example" };
27
+ }
28
+
29
+ function fakeRendered() {
30
+ const iframe = document.createElement("iframe");
31
+ document.body.appendChild(iframe);
32
+ return {
33
+ iframe,
34
+ origin: "https://fake.scf.test",
35
+ sendMessage: vi.fn(),
36
+ dispose: vi.fn(),
37
+ fullyLoadedPromiseWithTimeout: vi.fn(),
38
+ };
39
+ }
40
+
41
+ const flush = () =>
42
+ act(async () => {
43
+ await new Promise((r) => setTimeout(r, 0));
44
+ });
45
+
46
+ describe("isSandboxFrameMessage", () => {
47
+ it("accepts a message from the frame's contentWindow at its origin", () => {
48
+ const frame = makeFrame();
49
+ const event = new MessageEvent("message", {
50
+ data: validData,
51
+ origin: frame.origin,
52
+ source: frame.iframe.contentWindow,
53
+ });
54
+ expect(isSandboxFrameMessage(event, frame)).toBe(true);
55
+ });
56
+
57
+ it("rejects a message from a different origin", () => {
58
+ const frame = makeFrame();
59
+ const event = new MessageEvent("message", {
60
+ data: validData,
61
+ origin: "https://attacker.example",
62
+ source: frame.iframe.contentWindow,
63
+ });
64
+ expect(isSandboxFrameMessage(event, frame)).toBe(false);
65
+ });
66
+
67
+ it("rejects a message from a different source window", () => {
68
+ const frame = makeFrame();
69
+ const event = new MessageEvent("message", {
70
+ data: validData,
71
+ origin: frame.origin,
72
+ source: window,
73
+ });
74
+ expect(isSandboxFrameMessage(event, frame)).toBe(false);
75
+ });
76
+ });
77
+
78
+ describe("SandboxHost", () => {
79
+ let container: HTMLDivElement;
80
+ let root: Root;
81
+
82
+ beforeEach(() => {
83
+ container = document.createElement("div");
84
+ document.body.appendChild(container);
85
+ root = createRoot(container);
86
+ renderHtmlMock.mockReset();
87
+ });
88
+
89
+ afterEach(() => {
90
+ act(() => {
91
+ try {
92
+ root.unmount();
93
+ } catch {
94
+ // already unmounted by the test
95
+ }
96
+ });
97
+ container.remove();
98
+ });
99
+
100
+ it("delivers only frame-validated messages to the bridge", async () => {
101
+ const rendered = fakeRendered();
102
+ renderHtmlMock.mockResolvedValue(rendered);
103
+ const onMessage = vi.fn();
104
+ const bridge: SandboxBridge = { onMessage, dispose: vi.fn() };
105
+
106
+ await act(async () => {
107
+ root.render(
108
+ <SandboxHost
109
+ content={{ html: "" }}
110
+ contentKey="k"
111
+ createBridge={() => bridge}
112
+ />,
113
+ );
114
+ });
115
+ await flush();
116
+
117
+ window.dispatchEvent(
118
+ new MessageEvent("message", {
119
+ data: validData,
120
+ origin: rendered.origin,
121
+ source: rendered.iframe.contentWindow,
122
+ }),
123
+ );
124
+ expect(onMessage).toHaveBeenCalledTimes(1);
125
+
126
+ window.dispatchEvent(
127
+ new MessageEvent("message", {
128
+ data: validData,
129
+ origin: "https://attacker.example",
130
+ source: rendered.iframe.contentWindow,
131
+ }),
132
+ );
133
+ window.dispatchEvent(
134
+ new MessageEvent("message", {
135
+ data: validData,
136
+ origin: rendered.origin,
137
+ source: window,
138
+ }),
139
+ );
140
+ expect(onMessage).toHaveBeenCalledTimes(1);
141
+ });
142
+
143
+ it("clamps the bridge-reported height to maxHeight and ignores invalid values", async () => {
144
+ const rendered = fakeRendered();
145
+ renderHtmlMock.mockResolvedValue(rendered);
146
+ let host!: SandboxHostApi;
147
+ const bridge: SandboxBridge = { onMessage: vi.fn(), dispose: vi.fn() };
148
+
149
+ await act(async () => {
150
+ root.render(
151
+ <SandboxHost
152
+ content={{ html: "" }}
153
+ contentKey="k"
154
+ maxHeight={800}
155
+ createBridge={(_frame, h) => {
156
+ host = h;
157
+ return bridge;
158
+ }}
159
+ />,
160
+ );
161
+ });
162
+ await flush();
163
+
164
+ const div = container.firstElementChild as HTMLDivElement;
165
+ await act(async () => host.setHeight(200));
166
+ expect(div.style.height).toBe("200px");
167
+ await act(async () => host.setHeight(5000));
168
+ expect(div.style.height).toBe("800px");
169
+ await act(async () => host.setHeight(0));
170
+ expect(div.style.height).toBe("800px");
171
+ });
172
+
173
+ it("disposes the bridge before the frame and detaches the listener on unmount", async () => {
174
+ const rendered = fakeRendered();
175
+ renderHtmlMock.mockResolvedValue(rendered);
176
+ const order: string[] = [];
177
+ const onMessage = vi.fn();
178
+ const bridge: SandboxBridge = {
179
+ onMessage,
180
+ dispose: vi.fn(() => order.push("bridge")),
181
+ };
182
+ rendered.dispose = vi.fn(() => order.push("frame"));
183
+
184
+ await act(async () => {
185
+ root.render(
186
+ <SandboxHost
187
+ content={{ html: "" }}
188
+ contentKey="k"
189
+ createBridge={() => bridge}
190
+ />,
191
+ );
192
+ });
193
+ await flush();
194
+
195
+ await act(async () => {
196
+ root.unmount();
197
+ });
198
+
199
+ expect(order).toEqual(["bridge", "frame"]);
200
+
201
+ onMessage.mockClear();
202
+ window.dispatchEvent(
203
+ new MessageEvent("message", {
204
+ data: validData,
205
+ origin: rendered.origin,
206
+ source: rendered.iframe.contentWindow,
207
+ }),
208
+ );
209
+ expect(onMessage).not.toHaveBeenCalled();
210
+ });
211
+
212
+ it("calls onError when rendering rejects", async () => {
213
+ renderHtmlMock.mockRejectedValue(new Error("boom"));
214
+ const onError = vi.fn();
215
+
216
+ await act(async () => {
217
+ root.render(
218
+ <SandboxHost
219
+ content={{ html: "" }}
220
+ contentKey="k"
221
+ createBridge={() => ({ onMessage: vi.fn(), dispose: vi.fn() })}
222
+ onError={onError}
223
+ />,
224
+ );
225
+ });
226
+ await flush();
227
+
228
+ expect(onError).toHaveBeenCalledTimes(1);
229
+ expect(onError.mock.calls[0]![0].message).toBe("boom");
230
+ });
231
+ });
@@ -0,0 +1,185 @@
1
+ "use client";
2
+
3
+ import { type CSSProperties, useEffect, useRef, useState } from "react";
4
+ import {
5
+ type RenderedFrame,
6
+ SafeContentFrame,
7
+ type SandboxOption,
8
+ } from "safe-content-frame";
9
+
10
+ const DEFAULT_PRODUCT = "assistant-ui-sandbox";
11
+ const DEFAULT_MAX_HEIGHT = 800;
12
+
13
+ export type SandboxHostConfig = {
14
+ sandbox?: SandboxOption[];
15
+ useShadowDom?: boolean;
16
+ enableBrowserCaching?: boolean;
17
+ salt?: string;
18
+ product?: string;
19
+ className?: string;
20
+ style?: CSSProperties;
21
+ unsafeDocumentWrite?: boolean;
22
+ };
23
+
24
+ export type SandboxHostFrame = Pick<
25
+ RenderedFrame,
26
+ "iframe" | "origin" | "sendMessage"
27
+ >;
28
+
29
+ export type SandboxHostApi = {
30
+ setHeight: (height: number) => void;
31
+ };
32
+
33
+ export type SandboxBridge = {
34
+ onMessage: (event: MessageEvent) => void;
35
+ dispose: () => void;
36
+ };
37
+
38
+ export type SandboxContent = { html: string };
39
+
40
+ export type SandboxHostProps = {
41
+ content: SandboxContent;
42
+ contentKey: string;
43
+ sandbox?: SandboxHostConfig | undefined;
44
+ maxHeight?: number | undefined;
45
+ createBridge: (
46
+ frame: SandboxHostFrame,
47
+ host: SandboxHostApi,
48
+ ) => SandboxBridge;
49
+ onError?: ((error: Error) => void) | undefined;
50
+ containerProps?: Record<string, string | undefined> | undefined;
51
+ };
52
+
53
+ export function isSandboxFrameMessage(
54
+ event: MessageEvent,
55
+ frame: { iframe: HTMLIFrameElement; origin: string },
56
+ ): boolean {
57
+ return (
58
+ event.source === frame.iframe.contentWindow && event.origin === frame.origin
59
+ );
60
+ }
61
+
62
+ type LiveSnapshot = {
63
+ content: SandboxContent;
64
+ sandbox: SandboxHostConfig | undefined;
65
+ createBridge: SandboxHostProps["createBridge"];
66
+ onError: SandboxHostProps["onError"];
67
+ };
68
+
69
+ export function SandboxHost({
70
+ content,
71
+ contentKey,
72
+ sandbox,
73
+ maxHeight = DEFAULT_MAX_HEIGHT,
74
+ createBridge,
75
+ onError,
76
+ containerProps,
77
+ }: SandboxHostProps) {
78
+ const containerRef = useRef<HTMLDivElement>(null);
79
+ const [contentHeight, setContentHeight] = useState<number | undefined>(
80
+ undefined,
81
+ );
82
+
83
+ const liveRef = useRef<LiveSnapshot>(null!);
84
+ liveRef.current = { content, sandbox, createBridge, onError };
85
+
86
+ useEffect(() => {
87
+ const container = containerRef.current;
88
+ if (!container) return;
89
+
90
+ let cancelled = false;
91
+ let frame: RenderedFrame | null = null;
92
+ let bridge: SandboxBridge | null = null;
93
+ let onMessage: ((event: MessageEvent) => void) | null = null;
94
+
95
+ const { content: liveContent, sandbox: sb } = liveRef.current;
96
+
97
+ const scf = new SafeContentFrame(sb?.product ?? DEFAULT_PRODUCT, {
98
+ ...(sb?.sandbox !== undefined && { sandbox: sb.sandbox }),
99
+ ...(sb?.useShadowDom !== undefined && { useShadowDom: sb.useShadowDom }),
100
+ ...(sb?.enableBrowserCaching !== undefined && {
101
+ enableBrowserCaching: sb.enableBrowserCaching,
102
+ }),
103
+ ...(sb?.salt !== undefined && { salt: sb.salt }),
104
+ });
105
+
106
+ const renderOpts =
107
+ sb?.unsafeDocumentWrite !== undefined
108
+ ? { unsafeDocumentWrite: sb.unsafeDocumentWrite }
109
+ : undefined;
110
+
111
+ scf
112
+ .renderHtml(liveContent.html, container, renderOpts)
113
+ .then((rendered) => {
114
+ if (cancelled) {
115
+ rendered.dispose();
116
+ return;
117
+ }
118
+ frame = rendered;
119
+
120
+ const hostApi: SandboxHostApi = {
121
+ setHeight: (height) => {
122
+ if (
123
+ typeof height === "number" &&
124
+ Number.isFinite(height) &&
125
+ height > 0
126
+ ) {
127
+ setContentHeight(height);
128
+ }
129
+ },
130
+ };
131
+
132
+ bridge = liveRef.current.createBridge(
133
+ {
134
+ iframe: rendered.iframe,
135
+ origin: rendered.origin,
136
+ sendMessage: rendered.sendMessage,
137
+ },
138
+ hostApi,
139
+ );
140
+
141
+ // Single owner of the window listener; the cross-origin guard runs
142
+ // here so the bridge only sees frame-validated messages.
143
+ onMessage = (event) => {
144
+ if (!isSandboxFrameMessage(event, rendered)) return;
145
+ bridge?.onMessage(event);
146
+ };
147
+ window.addEventListener("message", onMessage);
148
+ })
149
+ .catch((err) => {
150
+ liveRef.current.onError?.(
151
+ err instanceof Error ? err : new Error(String(err)),
152
+ );
153
+ });
154
+
155
+ return () => {
156
+ cancelled = true;
157
+ if (onMessage) {
158
+ window.removeEventListener("message", onMessage);
159
+ onMessage = null;
160
+ }
161
+ bridge?.dispose();
162
+ bridge = null;
163
+ frame?.dispose();
164
+ frame = null;
165
+ setContentHeight(undefined);
166
+ };
167
+ // oxlint-disable-next-line react/exhaustive-deps -- re-init only on contentKey change; live values flow through liveRef
168
+ }, [contentKey]);
169
+
170
+ const resolvedHeight =
171
+ contentHeight != null ? Math.min(contentHeight, maxHeight) : undefined;
172
+ const mergedStyle =
173
+ resolvedHeight != null
174
+ ? { ...sandbox?.style, height: resolvedHeight }
175
+ : sandbox?.style;
176
+
177
+ return (
178
+ <div
179
+ {...containerProps}
180
+ ref={containerRef}
181
+ className={sandbox?.className}
182
+ style={mergedStyle}
183
+ />
184
+ );
185
+ }