@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.
- package/README.md +5 -1
- package/dist/client/ExternalThread.d.ts +2 -12
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +30 -29
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/client/InMemoryThreadList.d.ts.map +1 -1
- package/dist/client/InMemoryThreadList.js +24 -13
- package/dist/client/InMemoryThreadList.js.map +1 -1
- package/dist/client/SingleThreadList.d.ts.map +1 -1
- package/dist/client/SingleThreadList.js +12 -8
- package/dist/client/SingleThreadList.js.map +1 -1
- package/dist/context/providers/ThreadViewportProvider.js +1 -1
- package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
- package/dist/context/react/ThreadViewportContext.js +1 -1
- package/dist/context/react/utils/createContextHook.js +1 -1
- package/dist/context/react/utils/ensureBinding.js.map +1 -1
- package/dist/context/react/utils/useRuntimeState.js +1 -1
- package/dist/context/stores/ThreadViewport.js.map +1 -1
- package/dist/devtools/DevToolsHooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
- package/dist/legacy-runtime/cloud/auiV0.js +1 -1
- package/dist/legacy-runtime/hooks/AssistantContext.js.map +1 -1
- package/dist/legacy-runtime/hooks/AttachmentContext.js.map +1 -1
- package/dist/legacy-runtime/hooks/ComposerContext.js.map +1 -1
- package/dist/legacy-runtime/hooks/MessageContext.js.map +1 -1
- package/dist/legacy-runtime/hooks/MessagePartContext.js.map +1 -1
- package/dist/legacy-runtime/hooks/ThreadContext.js +1 -1
- package/dist/legacy-runtime/hooks/ThreadContext.js.map +1 -1
- package/dist/legacy-runtime/hooks/ThreadListItemContext.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +14 -0
- package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +1 -0
- package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +101 -0
- package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +1 -0
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +13 -2
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +1 -1
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +7 -7
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.js +5 -4
- package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +82 -104
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.d.ts +3 -3
- package/dist/mcp-apps/bridge.d.ts.map +1 -1
- package/dist/mcp-apps/bridge.js +35 -10
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/types.d.ts +2 -12
- package/dist/mcp-apps/types.d.ts.map +1 -1
- package/dist/mcp-apps/types.js.map +1 -1
- package/dist/model-context/frame/useAssistantFrameHost.js +1 -1
- package/dist/model-context/makeAssistantVisible.js +1 -1
- package/dist/model-context/makeAssistantVisible.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
- package/dist/primitives/actionBar/ActionBarInteractionContext.js +1 -1
- package/dist/primitives/actionBar/ActionBarRoot.js +1 -1
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreContent.js +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreItem.js +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js +1 -1
- package/dist/primitives/assistantModal/AssistantModalAnchor.js +1 -1
- package/dist/primitives/assistantModal/AssistantModalContent.js +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
- package/dist/primitives/assistantModal/AssistantModalTrigger.js +1 -1
- package/dist/primitives/attachment/AttachmentRemove.js +1 -1
- package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
- package/dist/primitives/attachment/AttachmentRoot.js +1 -1
- package/dist/primitives/attachment/AttachmentThumb.js +1 -1
- package/dist/primitives/branchPicker/BranchPickerRoot.js +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtRoot.js +1 -1
- package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
- package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerDictationTranscript.js +1 -1
- package/dist/primitives/composer/ComposerInput.js +1 -1
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.js +1 -1
- package/dist/primitives/composer/ComposerQuote.js +1 -1
- package/dist/primitives/composer/ComposerQuote.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +1 -1
- package/dist/primitives/composer/ComposerSend.js +1 -1
- package/dist/primitives/composer/ComposerStopDictation.js +1 -1
- package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js +2 -2
- package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js +8 -7
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.js +5 -4
- package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +8 -7
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +13 -12
- package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +7 -6
- package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
- package/dist/primitives/error/ErrorMessage.js +1 -1
- package/dist/primitives/error/ErrorRoot.js +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/message/MessageRoot.js +1 -1
- package/dist/primitives/message/MessageRoot.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartImage.js +1 -1
- package/dist/primitives/messagePart/MessagePartText.js +1 -1
- package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
- package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
- package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemText.js +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +1 -1
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
- package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
- package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
- package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
- package/dist/primitives/thread/ThreadRoot.js +1 -1
- package/dist/primitives/thread/ThreadScrollToBottom.js +1 -1
- package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +1 -1
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/ThreadViewportFooter.js +1 -1
- package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
- package/dist/primitives/thread/topAnchor/topAnchorTurn.js.map +1 -1
- package/dist/primitives/thread/topAnchor/topAnchorUtils.js.map +1 -1
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/primitives/threadList/ThreadListNew.js +1 -1
- package/dist/primitives/threadList/ThreadListRoot.js +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.js +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts +50 -0
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -0
- package/dist/sandbox-host/SandboxHost.js +85 -0
- package/dist/sandbox-host/SandboxHost.js.map +1 -0
- package/dist/unstable/useMentionAdapter.d.ts +2 -2
- package/dist/unstable/useMentionAdapter.js +2 -2
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +1 -1
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/utils/Primitive.js +1 -1
- package/dist/utils/createActionButton.js +1 -1
- package/dist/utils/createActionButton.js.map +1 -1
- package/dist/utils/hooks/useManagedRef.js +1 -1
- package/dist/utils/hooks/useMediaQuery.js +1 -1
- package/dist/utils/hooks/useMediaQuery.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +1 -1
- package/dist/utils/hooks/useOnScrollToBottom.js +1 -1
- package/dist/utils/hooks/useSizeHandle.js +1 -1
- package/dist/utils/json/is-json.js.map +1 -1
- package/dist/utils/smooth/SmoothContext.js +1 -1
- package/dist/utils/smooth/SmoothContext.js.map +1 -1
- package/dist/utils/smooth/useSmooth.js +1 -1
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +21 -20
- package/src/client/ExternalThread.ts +484 -515
- package/src/client/InMemoryThreadList.ts +154 -142
- package/src/client/SingleThreadList.ts +88 -81
- package/src/context/providers/ThreadViewportProvider.tsx +2 -2
- package/src/index.ts +18 -3
- package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +426 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +146 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +16 -1
- package/src/mcp-apps/McpAppRenderer.tsx +28 -35
- package/src/mcp-apps/McpAppsRemoteHost.ts +25 -24
- package/src/mcp-apps/app-frame.tsx +100 -141
- package/src/mcp-apps/bridge.test.ts +100 -60
- package/src/mcp-apps/bridge.ts +43 -21
- package/src/mcp-apps/types.ts +2 -12
- package/src/primitives/composer/trigger/TriggerPopover.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopoverResource.ts +75 -76
- package/src/primitives/composer/trigger/triggerDetectionResource.ts +6 -5
- package/src/primitives/composer/trigger/triggerKeyboardResource.ts +9 -13
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +14 -19
- package/src/primitives/composer/trigger/triggerSelectionResource.ts +8 -7
- package/src/sandbox-host/SandboxHost.test.tsx +231 -0
- package/src/sandbox-host/SandboxHost.tsx +185 -0
- package/src/tests/local-runtime-queue.test.tsx +305 -0
- package/src/unstable/useMentionAdapter.ts +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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] =
|
|
28
|
+
}): TriggerDetectionResourceOutput {
|
|
29
|
+
const [cursorPosition, setCursorPosition] = useState(text.length);
|
|
29
30
|
|
|
30
|
-
const trigger =
|
|
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
|
-
|
|
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] =
|
|
60
|
+
}): TriggerKeyboardResourceOutput {
|
|
61
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
useEffect(() => {
|
|
68
64
|
setHighlightedIndex(0);
|
|
69
65
|
}, [navigableList]);
|
|
70
66
|
|
|
71
|
-
|
|
67
|
+
useEffect(() => {
|
|
72
68
|
setHighlightedIndex(0);
|
|
73
69
|
}, [isSearchMode, activeCategoryId]);
|
|
74
70
|
|
|
75
|
-
const highlightIndex =
|
|
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 =
|
|
77
|
+
const handleKeyDown = useEffectEvent(
|
|
82
78
|
(e: TriggerPopoverKeyEvent): boolean => {
|
|
83
79
|
if (!open) return false;
|
|
84
80
|
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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] =
|
|
50
|
+
}): TriggerNavigationResourceOutput {
|
|
51
|
+
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(
|
|
57
52
|
null,
|
|
58
53
|
);
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
useEffect(() => {
|
|
61
56
|
if (!open) setActiveCategoryId(null);
|
|
62
57
|
}, [open]);
|
|
63
58
|
|
|
64
|
-
const categories =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
122
|
+
const selectCategory = useEffectEvent((categoryId: string) => {
|
|
128
123
|
setActiveCategoryId(categoryId);
|
|
129
124
|
});
|
|
130
125
|
|
|
131
|
-
const goBack =
|
|
126
|
+
const goBack = useEffectEvent(() => {
|
|
132
127
|
setActiveCategoryId(null);
|
|
133
128
|
});
|
|
134
129
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
55
|
+
const selectItemOverrideRef = useRef<SelectItemOverride | null>(null);
|
|
55
56
|
|
|
56
|
-
const registerSelectItemOverride =
|
|
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 =
|
|
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 =
|
|
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
|
+
}
|