@assistant-ui/react 0.15.16 → 0.15.18
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 +4 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -2
- package/dist/mcp-apps/McpAppRenderer.d.ts +36 -9
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +108 -57
- 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 +20 -17
- package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +229 -115
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.js +2 -3
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/index.d.ts +3 -3
- package/dist/model-context/frame/useAssistantFrameHost.d.ts +1 -1
- package/dist/model-context/frame/useAssistantFrameHost.js +10 -11
- package/dist/model-context/frame/useAssistantFrameHost.js.map +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 +3 -3
- package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +3 -3
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +3 -3
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +3 -3
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js +3 -3
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreContent.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreItem.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js.map +1 -1
- package/dist/primitives/actionBarMore/scope.d.ts +2 -2
- package/dist/primitives/actionBarMore/scope.d.ts.map +1 -1
- package/dist/primitives/actionBarMore/scope.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalAnchor.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalContent.js +1 -1
- package/dist/primitives/assistantModal/AssistantModalContent.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalTrigger.js.map +1 -1
- package/dist/primitives/assistantModal/scope.d.ts +2 -2
- package/dist/primitives/assistantModal/scope.d.ts.map +1 -1
- package/dist/primitives/assistantModal/scope.js.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerRoot.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.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +1 -3
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
- package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
- package/dist/primitives/composer/ComposerQuote.js +5 -5
- package/dist/primitives/composer/ComposerQuote.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +3 -3
- package/dist/primitives/composer/ComposerRoot.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts +5 -0
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js +157 -130
- package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js +3 -3
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +4 -4
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js +4 -4
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -0
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js +63 -61
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +2 -0
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
- package/dist/primitives/composer/trigger/detectTrigger.d.ts +21 -1
- package/dist/primitives/composer/trigger/detectTrigger.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/detectTrigger.js +17 -3
- package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -1
- package/dist/primitives/composer/trigger/index.d.ts +3 -1
- package/dist/primitives/composer/trigger/index.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/index.js.map +1 -1
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +3 -4
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.js +15 -14
- package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -5
- 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 +18 -19
- package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
- package/dist/primitives/composer.d.ts +3 -2
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/error/ErrorMessage.js.map +1 -1
- package/dist/primitives/message/MessageIf.js.map +1 -1
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/message/MessageRoot.js +4 -4
- package/dist/primitives/message/MessageRoot.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts +1 -1
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +3 -3
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +3 -3
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadIf.js.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +3 -3
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/ThreadViewportFooter.js +3 -3
- package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +4 -4
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/primitives/threadList/ThreadListNew.js +3 -3
- package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
- package/dist/primitives/threadListFocusGroup.d.ts +2 -1
- package/dist/primitives/threadListFocusGroup.d.ts.map +1 -1
- package/dist/primitives/threadListFocusGroup.js +2 -2
- package/dist/primitives/threadListFocusGroup.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.js +3 -3
- package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js.map +1 -1
- package/dist/primitives/threadListItemMore/scope.d.ts +2 -2
- package/dist/primitives/threadListItemMore/scope.d.ts.map +1 -1
- package/dist/primitives/threadListItemMore/scope.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +148 -72
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +3 -1
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +9 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +50 -43
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
- package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
- package/dist/unstable/useModelContextSnapshot.js +61 -0
- package/dist/unstable/useModelContextSnapshot.js.map +1 -0
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +37 -29
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
- package/dist/unstable/webmcp/convertTools.d.ts +21 -0
- package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
- package/dist/unstable/webmcp/convertTools.js +110 -0
- package/dist/unstable/webmcp/convertTools.js.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js +105 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.js +29 -0
- package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
- package/dist/utils/Primitive.d.ts +6 -6
- package/dist/utils/Primitive.js +2 -2
- package/dist/utils/Primitive.js.map +1 -1
- package/dist/utils/createActionButton.js +3 -3
- package/dist/utils/createActionButton.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +1 -1
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/hooks/useOnScrollToBottom.js +1 -1
- package/dist/utils/hooks/useOnScrollToBottom.js.map +1 -1
- package/dist/utils/smooth/SmoothContext.d.ts +24 -0
- package/dist/utils/smooth/SmoothContext.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +1 -1
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +14 -22
- package/src/index.ts +22 -1
- package/src/mcp-apps/McpAppRenderer.test.tsx +386 -9
- package/src/mcp-apps/McpAppRenderer.tsx +171 -69
- package/src/mcp-apps/McpAppsRemoteHost.concurrent.test.tsx +215 -0
- package/src/mcp-apps/McpAppsRemoteHost.ts +28 -12
- package/src/mcp-apps/app-frame.test.tsx +349 -0
- package/src/mcp-apps/app-frame.tsx +68 -9
- package/src/mcp-apps/bridge.ts +7 -2
- package/src/mcp-apps/index.ts +6 -1
- package/src/model-context/frame/AssistantFrame.test.ts +3 -3
- package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
- package/src/model-context/frame/useAssistantFrameHost.ts +2 -2
- package/src/model-context/makeAssistantVisible.tsx +1 -1
- package/src/primitives/actionBar/ActionBarCopy.tsx +1 -1
- package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +1 -1
- package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +1 -1
- package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +1 -1
- package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +1 -1
- package/src/primitives/actionBarMore/scope.ts +2 -2
- package/src/primitives/assistantModal/AssistantModalContent.tsx +1 -1
- package/src/primitives/assistantModal/scope.ts +2 -2
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +1 -1
- package/src/primitives/composer/ComposerInput.test.tsx +2 -1
- package/src/primitives/composer/ComposerInput.tsx +3 -3
- package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
- package/src/primitives/composer/ComposerQuote.tsx +1 -1
- package/src/primitives/composer/ComposerRoot.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopover.tsx +16 -1
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverBack.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
- package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopoverResource.ts +4 -1
- package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +2 -0
- package/src/primitives/composer/trigger/detectTrigger.test.ts +88 -5
- package/src/primitives/composer/trigger/detectTrigger.ts +50 -9
- package/src/primitives/composer/trigger/index.ts +1 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
- package/src/primitives/composer/trigger/triggerDetectionResource.ts +10 -7
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +3 -10
- package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +15 -3
- package/src/primitives/composer/trigger/triggerSelectionResource.ts +1 -4
- package/src/primitives/composer.ts +1 -0
- package/src/primitives/message/MessageRoot.tsx +1 -1
- package/src/primitives/selectionToolbar/SelectionToolbarQuote.tsx +1 -1
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +1 -1
- package/src/primitives/thread/ThreadViewport.tsx +1 -1
- package/src/primitives/thread/ThreadViewportFooter.tsx +1 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -7
- package/src/primitives/threadList/ThreadListNew.tsx +1 -1
- package/src/primitives/threadListFocusGroup.ts +2 -2
- package/src/primitives/threadListItem/ThreadListItemRoot.tsx +1 -1
- package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +1 -1
- package/src/primitives/threadListItemMore/ThreadListItemMoreContent.tsx +1 -1
- package/src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx +1 -1
- package/src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx +2 -2
- package/src/primitives/threadListItemMore/scope.ts +2 -2
- package/src/sandbox-host/SandboxHost.test.tsx +165 -3
- package/src/sandbox-host/SandboxHost.tsx +42 -9
- package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +31 -1
- package/src/unstable/useComposerInputHistory.test.tsx +2 -1
- package/src/unstable/useLiveCompletionAdapter.test.tsx +60 -1
- package/src/unstable/useLiveCompletionAdapter.ts +4 -1
- package/src/unstable/useMentionAdapter.test.tsx +337 -0
- package/src/unstable/useMentionAdapter.ts +94 -57
- package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
- package/src/unstable/useModelContextSnapshot.ts +67 -0
- package/src/unstable/useSlashCommandAdapter.test.tsx +141 -0
- package/src/unstable/useSlashCommandAdapter.ts +47 -19
- package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
- package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
- package/src/unstable/webmcp/convertTools.test.ts +393 -0
- package/src/unstable/webmcp/convertTools.ts +180 -0
- package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
- package/src/unstable/webmcp/useWebMcpProvider.ts +150 -0
- package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
- package/src/unstable/webmcp/webmcp-host.ts +83 -0
- package/src/utils/Primitive.tsx +2 -2
- package/src/utils/createActionButton.tsx +1 -1
- package/src/utils/hooks/useOnResizeContent.ts +1 -1
- package/src/utils/hooks/useOnScrollToBottom.ts +1 -1
- package/src/utils/smooth/useSmooth.ts +1 -1
- package/dist/legacy-runtime/runtime/MessagePartRuntime.d.ts +0 -3
- package/dist/legacy-runtime/runtime/MessagePartRuntime.js +0 -2
- package/dist/legacy-runtime/runtime/MessageRuntime.d.ts +0 -3
- package/dist/legacy-runtime/runtime/MessageRuntime.js +0 -2
- package/dist/legacy-runtime/runtime/ThreadListRuntime.d.ts +0 -3
- package/dist/legacy-runtime/runtime/ThreadListRuntime.js +0 -2
- package/dist/utils/invokeCallbackSafely.d.ts +0 -5
- package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
- package/dist/utils/invokeCallbackSafely.js +0 -15
- package/dist/utils/invokeCallbackSafely.js.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.d.ts +0 -15
- package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.js +0 -23
- package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
- package/src/legacy-runtime/runtime/MessagePartRuntime.ts +0 -2
- package/src/legacy-runtime/runtime/MessageRuntime.ts +0 -3
- package/src/legacy-runtime/runtime/ThreadListRuntime.ts +0 -4
- package/src/utils/invokeCallbackSafely.ts +0 -11
- package/src/utils/useToolArgsFieldStatus.ts +0 -18
|
@@ -81,7 +81,8 @@ vi.mock("./trigger/TriggerPopoverRootContext", () => ({
|
|
|
81
81
|
|
|
82
82
|
let escapeKeydownHandler: ((event: KeyboardEvent) => void) | null = null;
|
|
83
83
|
|
|
84
|
-
vi.mock("
|
|
84
|
+
vi.mock("radix-ui/internal", async (importOriginal) => ({
|
|
85
|
+
...(await importOriginal<typeof import("radix-ui/internal")>()),
|
|
85
86
|
useEscapeKeydown: (handler: (event: KeyboardEvent) => void) => {
|
|
86
87
|
escapeKeydownHandler = handler;
|
|
87
88
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { composeEventHandlers } from "
|
|
4
|
-
import { useComposedRefs } from "
|
|
3
|
+
import { composeEventHandlers } from "radix-ui/internal";
|
|
4
|
+
import { useComposedRefs } from "radix-ui/internal";
|
|
5
5
|
import { Slot } from "radix-ui";
|
|
6
6
|
import {
|
|
7
7
|
type ClipboardEvent,
|
|
@@ -19,7 +19,7 @@ import TextareaAutosize, {
|
|
|
19
19
|
type TextareaAutosizeProps,
|
|
20
20
|
type TextareaHeightChangeMeta,
|
|
21
21
|
} from "react-textarea-autosize";
|
|
22
|
-
import { useEscapeKeydown } from "
|
|
22
|
+
import { useEscapeKeydown } from "radix-ui/internal";
|
|
23
23
|
import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
|
|
24
24
|
import { useMediaQuery } from "../../utils/hooks/useMediaQuery";
|
|
25
25
|
import { useAui } from "@assistant-ui/store";
|
|
@@ -51,16 +51,6 @@ export type ComposerInputPluginRegistry = {
|
|
|
51
51
|
const ComposerInputPluginRegistryContext =
|
|
52
52
|
createContext<ComposerInputPluginRegistry | null>(null);
|
|
53
53
|
|
|
54
|
-
export const useComposerInputPluginRegistry =
|
|
55
|
-
(): ComposerInputPluginRegistry => {
|
|
56
|
-
const ctx = useContext(ComposerInputPluginRegistryContext);
|
|
57
|
-
if (!ctx)
|
|
58
|
-
throw new Error(
|
|
59
|
-
"useComposerInputPluginRegistry must be used within a ComposerInputPluginProvider",
|
|
60
|
-
);
|
|
61
|
-
return ctx;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
54
|
export const useComposerInputPluginRegistryOptional =
|
|
65
55
|
(): ComposerInputPluginRegistry | null => {
|
|
66
56
|
return useContext(ComposerInputPluginRegistryContext);
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
useTriggerPopoverAriaPublish,
|
|
29
29
|
useTriggerPopoverRootContext,
|
|
30
30
|
} from "./TriggerPopoverRootContext";
|
|
31
|
+
import type { TriggerMatcher } from "./detectTrigger";
|
|
31
32
|
|
|
32
33
|
const TriggerPopoverScopeContext =
|
|
33
34
|
createContext<TriggerPopoverResourceOutput | null>(null);
|
|
@@ -70,6 +71,8 @@ export namespace ComposerPrimitiveTriggerPopover {
|
|
|
70
71
|
> & {
|
|
71
72
|
/** The character(s) that activate this trigger (e.g. `"@"`, `"/"`). Also serves as the trigger identity within the root. */
|
|
72
73
|
readonly char: string;
|
|
74
|
+
/** Overrides trigger detection for both textarea and Lexical inputs. `endOffset` is the exclusive replace bound. */
|
|
75
|
+
readonly matcher?: TriggerMatcher | undefined;
|
|
73
76
|
/** Adapter providing categories and items. */
|
|
74
77
|
readonly adapter?: Unstable_TriggerAdapter | undefined;
|
|
75
78
|
/** Whether the adapter is resolving items, surfaced to the popover scope for async sources. @default false */
|
|
@@ -111,6 +114,7 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
|
|
|
111
114
|
(
|
|
112
115
|
{
|
|
113
116
|
char,
|
|
117
|
+
matcher,
|
|
114
118
|
adapter,
|
|
115
119
|
isLoading = false,
|
|
116
120
|
"aria-label": ariaLabel,
|
|
@@ -122,6 +126,15 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
|
|
|
122
126
|
const aui = useAui();
|
|
123
127
|
const text = useAuiState((s) => s.composer.text);
|
|
124
128
|
const popoverId = useId();
|
|
129
|
+
const matcherRef = useRef(matcher);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
matcherRef.current = matcher;
|
|
132
|
+
});
|
|
133
|
+
const [registeredMatcher] = useState(
|
|
134
|
+
() => (nextText: string, triggerChar: string, cursorPosition: number) =>
|
|
135
|
+
matcherRef.current!(nextText, triggerChar, cursorPosition),
|
|
136
|
+
);
|
|
137
|
+
const hasMatcher = matcher !== undefined;
|
|
125
138
|
|
|
126
139
|
// Track in state (for resource reactivity) + ref (dev warning on duplicate registrations).
|
|
127
140
|
const behaviorRef = useRef<TriggerBehavior | null>(null);
|
|
@@ -165,6 +178,7 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
|
|
|
165
178
|
adapter,
|
|
166
179
|
text,
|
|
167
180
|
triggerChar: char,
|
|
181
|
+
matcher,
|
|
168
182
|
behavior: behavior ?? undefined,
|
|
169
183
|
aui,
|
|
170
184
|
popoverId,
|
|
@@ -178,10 +192,11 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
|
|
|
178
192
|
useEffect(() => {
|
|
179
193
|
return root.register({
|
|
180
194
|
char,
|
|
195
|
+
...(hasMatcher ? { matcher: registeredMatcher } : {}),
|
|
181
196
|
...(behavior ? { behavior } : {}),
|
|
182
197
|
resource: getResource(),
|
|
183
198
|
});
|
|
184
|
-
}, [root, char, behavior]);
|
|
199
|
+
}, [root, char, hasMatcher, behavior]);
|
|
185
200
|
|
|
186
201
|
const pluginRegistry = useComposerInputPluginRegistryOptional();
|
|
187
202
|
useEffect(() => {
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
Unstable_TriggerItem,
|
|
6
6
|
} from "@assistant-ui/core";
|
|
7
7
|
import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
|
|
8
|
-
import { useEffect, useRef, type FC } from "react";
|
|
8
|
+
import { useEffect, useInsertionEffect, useRef, type FC } from "react";
|
|
9
9
|
import { useTriggerBehaviorRegistration } from "./TriggerPopover";
|
|
10
10
|
import type { TriggerBehavior } from "./triggerSelectionResource";
|
|
11
11
|
|
|
@@ -42,7 +42,9 @@ export const ComposerPrimitiveTriggerPopoverAction: FC<
|
|
|
42
42
|
> = ({ formatter, onExecute, removeOnExecute }) => {
|
|
43
43
|
const { register } = useTriggerBehaviorRegistration();
|
|
44
44
|
const onExecuteRef = useRef(onExecute);
|
|
45
|
-
|
|
45
|
+
useInsertionEffect(() => {
|
|
46
|
+
onExecuteRef.current = onExecute;
|
|
47
|
+
}, [onExecute]);
|
|
46
48
|
|
|
47
49
|
useEffect(() => {
|
|
48
50
|
const behavior: TriggerBehavior = {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ComponentPropsWithoutRef,
|
|
7
7
|
forwardRef,
|
|
8
8
|
} from "react";
|
|
9
|
-
import { composeEventHandlers } from "
|
|
9
|
+
import { composeEventHandlers } from "radix-ui/internal";
|
|
10
10
|
import { useTriggerPopoverScopeContext } from "./TriggerPopover";
|
|
11
11
|
|
|
12
12
|
export namespace ComposerPrimitiveTriggerPopoverBack {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { Suspense, type ReactNode } from "react";
|
|
4
|
+
import { render, waitFor } from "@testing-library/react";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { Unstable_TriggerItem } from "@assistant-ui/core";
|
|
7
|
+
import type { TriggerBehavior } from "./triggerSelectionResource";
|
|
8
|
+
|
|
9
|
+
const mocks = vi.hoisted(() => ({ register: vi.fn() }));
|
|
10
|
+
|
|
11
|
+
vi.mock("./TriggerPopover", async (importOriginal) => ({
|
|
12
|
+
...(await importOriginal<typeof import("./TriggerPopover")>()),
|
|
13
|
+
useTriggerBehaviorRegistration: () => ({ register: mocks.register }),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { ComposerPrimitiveTriggerPopoverAction } from "./TriggerPopoverAction";
|
|
17
|
+
import { ComposerPrimitiveTriggerPopoverDirective } from "./TriggerPopoverDirective";
|
|
18
|
+
|
|
19
|
+
const item: Unstable_TriggerItem = {
|
|
20
|
+
id: "item-1",
|
|
21
|
+
type: "test",
|
|
22
|
+
label: "Item",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const verifyCommittedCallback = async (
|
|
26
|
+
renderBehavior: (callback: (item: Unstable_TriggerItem) => void) => ReactNode,
|
|
27
|
+
invoke: (behavior: TriggerBehavior) => void,
|
|
28
|
+
) => {
|
|
29
|
+
let behavior: TriggerBehavior | undefined;
|
|
30
|
+
mocks.register.mockImplementation((next: TriggerBehavior) => {
|
|
31
|
+
behavior = next;
|
|
32
|
+
return () => {};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let suspend = false;
|
|
36
|
+
const suspended = new Promise<never>(() => {});
|
|
37
|
+
const renderProbe = vi.fn();
|
|
38
|
+
const Suspender = () => {
|
|
39
|
+
if (suspend) throw suspended;
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
const Probe = () => {
|
|
43
|
+
renderProbe();
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
const View = ({
|
|
47
|
+
callback,
|
|
48
|
+
}: {
|
|
49
|
+
callback: (item: Unstable_TriggerItem) => void;
|
|
50
|
+
}) => (
|
|
51
|
+
<Suspense fallback={null}>
|
|
52
|
+
{renderBehavior(callback)}
|
|
53
|
+
<Probe />
|
|
54
|
+
<Suspender />
|
|
55
|
+
</Suspense>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const committed = vi.fn();
|
|
59
|
+
const abandoned = vi.fn();
|
|
60
|
+
const view = render(<View callback={committed} />);
|
|
61
|
+
|
|
62
|
+
await waitFor(() => expect(behavior).toBeDefined());
|
|
63
|
+
renderProbe.mockClear();
|
|
64
|
+
|
|
65
|
+
suspend = true;
|
|
66
|
+
view.rerender(<View callback={abandoned} />);
|
|
67
|
+
await waitFor(() => expect(renderProbe).toHaveBeenCalled());
|
|
68
|
+
|
|
69
|
+
invoke(behavior!);
|
|
70
|
+
expect(committed).toHaveBeenCalledExactlyOnceWith(item);
|
|
71
|
+
expect(abandoned).not.toHaveBeenCalled();
|
|
72
|
+
|
|
73
|
+
suspend = false;
|
|
74
|
+
view.rerender(<View callback={abandoned} />);
|
|
75
|
+
invoke(behavior!);
|
|
76
|
+
expect(abandoned).toHaveBeenCalledExactlyOnceWith(item);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe("TriggerPopover behavior callbacks", () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
mocks.register.mockReset();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("keeps the committed action callback during abandoned renders", async () => {
|
|
85
|
+
await verifyCommittedCallback(
|
|
86
|
+
(onExecute) => (
|
|
87
|
+
<ComposerPrimitiveTriggerPopoverAction onExecute={onExecute} />
|
|
88
|
+
),
|
|
89
|
+
(behavior) => {
|
|
90
|
+
if (behavior.kind !== "action") throw new Error("Expected action");
|
|
91
|
+
behavior.onExecute(item);
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("keeps the committed directive callback during abandoned renders", async () => {
|
|
97
|
+
await verifyCommittedCallback(
|
|
98
|
+
(onInserted) => (
|
|
99
|
+
<ComposerPrimitiveTriggerPopoverDirective onInserted={onInserted} />
|
|
100
|
+
),
|
|
101
|
+
(behavior) => {
|
|
102
|
+
if (behavior.kind !== "directive") {
|
|
103
|
+
throw new Error("Expected directive");
|
|
104
|
+
}
|
|
105
|
+
behavior.onInserted?.(item);
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
forwardRef,
|
|
9
9
|
useCallback,
|
|
10
10
|
} from "react";
|
|
11
|
-
import { composeEventHandlers } from "
|
|
11
|
+
import { composeEventHandlers } from "radix-ui/internal";
|
|
12
12
|
import { useTriggerPopoverScopeContext } from "./TriggerPopover";
|
|
13
13
|
import type { Unstable_TriggerCategory } from "@assistant-ui/core";
|
|
14
14
|
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
Unstable_TriggerItem,
|
|
6
6
|
} from "@assistant-ui/core";
|
|
7
7
|
import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
|
|
8
|
-
import { useEffect, useRef, type FC } from "react";
|
|
8
|
+
import { useEffect, useInsertionEffect, useRef, type FC } from "react";
|
|
9
9
|
import { useTriggerBehaviorRegistration } from "./TriggerPopover";
|
|
10
10
|
import type { TriggerBehavior } from "./triggerSelectionResource";
|
|
11
11
|
|
|
@@ -39,7 +39,9 @@ export const ComposerPrimitiveTriggerPopoverDirective: FC<
|
|
|
39
39
|
> = ({ formatter, onInserted }) => {
|
|
40
40
|
const { register } = useTriggerBehaviorRegistration();
|
|
41
41
|
const onInsertedRef = useRef(onInserted);
|
|
42
|
-
|
|
42
|
+
useInsertionEffect(() => {
|
|
43
|
+
onInsertedRef.current = onInserted;
|
|
44
|
+
}, [onInserted]);
|
|
43
45
|
|
|
44
46
|
useEffect(() => {
|
|
45
47
|
const behavior: TriggerBehavior = {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
forwardRef,
|
|
9
9
|
useCallback,
|
|
10
10
|
} from "react";
|
|
11
|
-
import { composeEventHandlers } from "
|
|
11
|
+
import { composeEventHandlers } from "radix-ui/internal";
|
|
12
12
|
import { useTriggerPopoverScopeContext } from "./TriggerPopover";
|
|
13
13
|
import type { Unstable_TriggerItem } from "@assistant-ui/core";
|
|
14
14
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type SelectItemOverride,
|
|
15
15
|
type TriggerBehavior,
|
|
16
16
|
} from "./triggerSelectionResource";
|
|
17
|
+
import type { TriggerMatcher } from "./detectTrigger";
|
|
17
18
|
|
|
18
19
|
export type { SelectItemOverride, TriggerBehavior };
|
|
19
20
|
export type { TriggerPopoverKeyEvent } from "./triggerKeyboardResource";
|
|
@@ -57,6 +58,7 @@ const useTriggerPopoverResource = ({
|
|
|
57
58
|
adapter,
|
|
58
59
|
text,
|
|
59
60
|
triggerChar,
|
|
61
|
+
matcher,
|
|
60
62
|
behavior,
|
|
61
63
|
aui,
|
|
62
64
|
popoverId,
|
|
@@ -65,6 +67,7 @@ const useTriggerPopoverResource = ({
|
|
|
65
67
|
adapter: Unstable_TriggerAdapter | undefined;
|
|
66
68
|
text: string;
|
|
67
69
|
triggerChar: string;
|
|
70
|
+
matcher: TriggerMatcher | undefined;
|
|
68
71
|
behavior: TriggerBehavior | undefined;
|
|
69
72
|
aui: AssistantClient;
|
|
70
73
|
/** Stable ID for accessible element IDs (pass React's useId() from component layer). */
|
|
@@ -72,7 +75,7 @@ const useTriggerPopoverResource = ({
|
|
|
72
75
|
isLoading: boolean;
|
|
73
76
|
}): TriggerPopoverResourceOutput => {
|
|
74
77
|
const detection = useResource(
|
|
75
|
-
TriggerDetectionResource({ text, triggerChar }),
|
|
78
|
+
TriggerDetectionResource({ text, triggerChar, matcher }),
|
|
76
79
|
);
|
|
77
80
|
|
|
78
81
|
const open =
|
|
@@ -16,9 +16,11 @@ import {
|
|
|
16
16
|
} from "../ComposerInputPluginContext";
|
|
17
17
|
import type { TriggerPopoverResourceOutput } from "./TriggerPopoverResource";
|
|
18
18
|
import type { TriggerBehavior } from "./triggerSelectionResource";
|
|
19
|
+
import type { TriggerMatcher } from "./detectTrigger";
|
|
19
20
|
|
|
20
21
|
export type RegisteredTrigger = {
|
|
21
22
|
readonly char: string;
|
|
23
|
+
readonly matcher?: TriggerMatcher | undefined;
|
|
22
24
|
/** Behavior contributed by a child `TriggerPopover.Directive` / `.Action`. */
|
|
23
25
|
readonly behavior?: TriggerBehavior;
|
|
24
26
|
readonly resource: TriggerPopoverResourceOutput;
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { detectTrigger } from "./detectTrigger";
|
|
2
|
+
import { detectTrigger, type TriggerMatcher } from "./detectTrigger";
|
|
3
|
+
|
|
4
|
+
const matchFromLastTrigger: TriggerMatcher = (
|
|
5
|
+
text,
|
|
6
|
+
triggerChar,
|
|
7
|
+
cursorPosition,
|
|
8
|
+
) => {
|
|
9
|
+
const textUpToCursor = text.slice(0, cursorPosition);
|
|
10
|
+
const offset = textUpToCursor.lastIndexOf(triggerChar);
|
|
11
|
+
if (offset === -1) return null;
|
|
12
|
+
return {
|
|
13
|
+
query: textUpToCursor.slice(offset + triggerChar.length),
|
|
14
|
+
offset,
|
|
15
|
+
endOffset: cursorPosition,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
3
18
|
|
|
4
19
|
describe("detectTrigger", () => {
|
|
5
20
|
it("detects @query at cursor position", () => {
|
|
6
21
|
expect(detectTrigger("hello @wea", "@", 10)).toEqual({
|
|
7
22
|
query: "wea",
|
|
8
23
|
offset: 6,
|
|
24
|
+
endOffset: 10,
|
|
9
25
|
});
|
|
10
26
|
});
|
|
11
27
|
|
|
@@ -25,14 +41,76 @@ describe("detectTrigger", () => {
|
|
|
25
41
|
expect(detectTrigger("@foo", "@", 4)).toEqual({
|
|
26
42
|
query: "foo",
|
|
27
43
|
offset: 0,
|
|
44
|
+
endOffset: 4,
|
|
28
45
|
});
|
|
29
46
|
});
|
|
30
47
|
|
|
31
48
|
it("stops at whitespace in query", () => {
|
|
32
|
-
// "@foo bar" — space terminates the mention
|
|
33
49
|
expect(detectTrigger("@foo bar", "@", 8)).toBeNull();
|
|
34
50
|
});
|
|
35
51
|
|
|
52
|
+
it("uses a custom matcher for multi-word queries", () => {
|
|
53
|
+
expect(
|
|
54
|
+
detectTrigger("hello @Example UK", "@", 17, matchFromLastTrigger),
|
|
55
|
+
).toEqual({
|
|
56
|
+
query: "Example UK",
|
|
57
|
+
offset: 6,
|
|
58
|
+
endOffset: 17,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("keeps a matcher endOffset that is not the query length", () => {
|
|
63
|
+
const matchNormalized: TriggerMatcher = (
|
|
64
|
+
text,
|
|
65
|
+
triggerChar,
|
|
66
|
+
cursorPosition,
|
|
67
|
+
) => {
|
|
68
|
+
const textUpToCursor = text.slice(0, cursorPosition);
|
|
69
|
+
const offset = textUpToCursor.lastIndexOf(triggerChar);
|
|
70
|
+
if (offset === -1) return null;
|
|
71
|
+
return {
|
|
72
|
+
query: "example",
|
|
73
|
+
offset,
|
|
74
|
+
endOffset: cursorPosition,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
detectTrigger("hello @Example UK", "@", 17, matchNormalized),
|
|
80
|
+
).toEqual({
|
|
81
|
+
query: "example",
|
|
82
|
+
offset: 6,
|
|
83
|
+
endOffset: 17,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("rejects a matcher whose offset is not the trigger", () => {
|
|
88
|
+
const match: TriggerMatcher = () => ({
|
|
89
|
+
query: "x",
|
|
90
|
+
offset: 0,
|
|
91
|
+
endOffset: 2,
|
|
92
|
+
});
|
|
93
|
+
expect(detectTrigger("hello @x", "@", 8, match)).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("rejects a matcher that matches at the caret", () => {
|
|
97
|
+
const match: TriggerMatcher = () => ({
|
|
98
|
+
query: "",
|
|
99
|
+
offset: 6,
|
|
100
|
+
endOffset: 7,
|
|
101
|
+
});
|
|
102
|
+
expect(detectTrigger("hello @foo", "@", 6, match)).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("rejects a matcher whose endOffset is inside the trigger", () => {
|
|
106
|
+
const match: TriggerMatcher = () => ({
|
|
107
|
+
query: "",
|
|
108
|
+
offset: 6,
|
|
109
|
+
endOffset: 6,
|
|
110
|
+
});
|
|
111
|
+
expect(detectTrigger("hello @foo", "@", 10, match)).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
36
114
|
it("stops at newline", () => {
|
|
37
115
|
expect(detectTrigger("@foo\nbar", "@", 8)).toBeNull();
|
|
38
116
|
});
|
|
@@ -45,20 +123,19 @@ describe("detectTrigger", () => {
|
|
|
45
123
|
expect(detectTrigger("hello\t@foo", "@", 10)).toEqual({
|
|
46
124
|
query: "foo",
|
|
47
125
|
offset: 6,
|
|
126
|
+
endOffset: 10,
|
|
48
127
|
});
|
|
49
128
|
});
|
|
50
129
|
|
|
51
130
|
it("finds trigger closest to cursor, not earlier ones", () => {
|
|
52
|
-
// Text has two @: "hello @old text @new"
|
|
53
|
-
// Cursor at end → should find @new
|
|
54
131
|
expect(detectTrigger("hello @old text @new", "@", 20)).toEqual({
|
|
55
132
|
query: "new",
|
|
56
133
|
offset: 16,
|
|
134
|
+
endOffset: 20,
|
|
57
135
|
});
|
|
58
136
|
});
|
|
59
137
|
|
|
60
138
|
it("ignores trigger after cursor", () => {
|
|
61
|
-
// Cursor at position 5, trigger at position 10
|
|
62
139
|
expect(detectTrigger("hello text @foo", "@", 5)).toBeNull();
|
|
63
140
|
});
|
|
64
141
|
|
|
@@ -66,6 +143,7 @@ describe("detectTrigger", () => {
|
|
|
66
143
|
expect(detectTrigger("hello @@foo", "@@", 11)).toEqual({
|
|
67
144
|
query: "foo",
|
|
68
145
|
offset: 6,
|
|
146
|
+
endOffset: 11,
|
|
69
147
|
});
|
|
70
148
|
});
|
|
71
149
|
|
|
@@ -73,6 +151,7 @@ describe("detectTrigger", () => {
|
|
|
73
151
|
expect(detectTrigger("hello @", "@", 7)).toEqual({
|
|
74
152
|
query: "",
|
|
75
153
|
offset: 6,
|
|
154
|
+
endOffset: 7,
|
|
76
155
|
});
|
|
77
156
|
});
|
|
78
157
|
|
|
@@ -80,6 +159,7 @@ describe("detectTrigger", () => {
|
|
|
80
159
|
expect(detectTrigger("全角\u3000@foo", "@", 7)).toEqual({
|
|
81
160
|
query: "foo",
|
|
82
161
|
offset: 3,
|
|
162
|
+
endOffset: 7,
|
|
83
163
|
});
|
|
84
164
|
});
|
|
85
165
|
|
|
@@ -87,6 +167,7 @@ describe("detectTrigger", () => {
|
|
|
87
167
|
expect(detectTrigger("x\u00A0@foo", "@", 6)).toEqual({
|
|
88
168
|
query: "foo",
|
|
89
169
|
offset: 2,
|
|
170
|
+
endOffset: 6,
|
|
90
171
|
});
|
|
91
172
|
});
|
|
92
173
|
|
|
@@ -98,6 +179,7 @@ describe("detectTrigger", () => {
|
|
|
98
179
|
expect(detectTrigger("hello\u3000@foo", "@", 10)).toEqual({
|
|
99
180
|
query: "foo",
|
|
100
181
|
offset: 6,
|
|
182
|
+
endOffset: 10,
|
|
101
183
|
});
|
|
102
184
|
});
|
|
103
185
|
|
|
@@ -105,6 +187,7 @@ describe("detectTrigger", () => {
|
|
|
105
187
|
expect(detectTrigger("hello\u00a0@foo", "@", 10)).toEqual({
|
|
106
188
|
query: "foo",
|
|
107
189
|
offset: 6,
|
|
190
|
+
endOffset: 10,
|
|
108
191
|
});
|
|
109
192
|
});
|
|
110
193
|
|
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
const WHITESPACE_RE = /\s/u;
|
|
2
2
|
|
|
3
|
+
export type TriggerMatch = {
|
|
4
|
+
readonly query: string;
|
|
5
|
+
readonly offset: number;
|
|
6
|
+
readonly endOffset: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Locates a trigger query in `text` relative to `cursorPosition`.
|
|
11
|
+
*
|
|
12
|
+
* `offset` is the index of `triggerChar`. `endOffset` is the exclusive end of
|
|
13
|
+
* the span both the textarea and Lexical replace on select. `query` is the
|
|
14
|
+
* search string and may differ from that span.
|
|
15
|
+
*
|
|
16
|
+
* Return null when the caret is at or before the trigger. Escape moves the
|
|
17
|
+
* caret to `offset` so a match there would reopen the popover.
|
|
18
|
+
*/
|
|
19
|
+
export type TriggerMatcher = (
|
|
20
|
+
text: string,
|
|
21
|
+
triggerChar: string,
|
|
22
|
+
cursorPosition: number,
|
|
23
|
+
) => TriggerMatch | null;
|
|
24
|
+
|
|
25
|
+
export function resolveTriggerMatch(
|
|
26
|
+
text: string,
|
|
27
|
+
triggerChar: string,
|
|
28
|
+
cursorPosition: number,
|
|
29
|
+
match: TriggerMatch | null,
|
|
30
|
+
): TriggerMatch | null {
|
|
31
|
+
if (match === null) return null;
|
|
32
|
+
const { query, offset, endOffset } = match;
|
|
33
|
+
if (offset < 0 || offset + triggerChar.length > cursorPosition) return null;
|
|
34
|
+
if (!text.startsWith(triggerChar, offset)) return null;
|
|
35
|
+
if (endOffset < offset + triggerChar.length || endOffset > text.length) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return { query, offset, endOffset };
|
|
39
|
+
}
|
|
40
|
+
|
|
3
41
|
/**
|
|
4
42
|
* Detect a trigger character in text relative to the cursor position.
|
|
5
43
|
*
|
|
@@ -9,27 +47,30 @@ export function detectTrigger(
|
|
|
9
47
|
text: string,
|
|
10
48
|
triggerChar: string,
|
|
11
49
|
cursorPosition: number,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
50
|
+
matcher?: TriggerMatcher | undefined,
|
|
51
|
+
): TriggerMatch | null {
|
|
52
|
+
if (matcher) {
|
|
53
|
+
return resolveTriggerMatch(
|
|
54
|
+
text,
|
|
55
|
+
triggerChar,
|
|
56
|
+
cursorPosition,
|
|
57
|
+
matcher(text, triggerChar, cursorPosition),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
17
61
|
const textUpToCursor = text.slice(0, cursorPosition);
|
|
18
62
|
|
|
19
|
-
// Search backwards from cursor for the trigger character.
|
|
20
|
-
// Stop at any whitespace during scan — trigger must be contiguous with cursor.
|
|
21
63
|
for (let i = textUpToCursor.length - 1; i >= 0; i--) {
|
|
22
64
|
const char = textUpToCursor[i]!;
|
|
23
65
|
|
|
24
66
|
if (WHITESPACE_RE.test(char)) return null;
|
|
25
67
|
|
|
26
68
|
if (textUpToCursor.startsWith(triggerChar, i)) {
|
|
27
|
-
// Trigger must be preceded by whitespace or be at start of text
|
|
28
69
|
if (i > 0 && !WHITESPACE_RE.test(textUpToCursor[i - 1]!)) continue;
|
|
29
70
|
|
|
30
71
|
const query = textUpToCursor.slice(i + triggerChar.length);
|
|
31
72
|
|
|
32
|
-
return { query, offset: i };
|
|
73
|
+
return { query, offset: i, endOffset: cursorPosition };
|
|
33
74
|
}
|
|
34
75
|
}
|
|
35
76
|
|
|
@@ -21,6 +21,7 @@ export {
|
|
|
21
21
|
} from "./TriggerPopoverItems";
|
|
22
22
|
export { ComposerPrimitiveTriggerPopoverBack } from "./TriggerPopoverBack";
|
|
23
23
|
export type { TriggerBehavior } from "./triggerSelectionResource";
|
|
24
|
+
export type { TriggerMatch, TriggerMatcher } from "./detectTrigger";
|
|
24
25
|
|
|
25
26
|
import { ComposerPrimitiveTriggerPopover as Base } from "./TriggerPopover";
|
|
26
27
|
import { ComposerPrimitiveTriggerPopoverAction } from "./TriggerPopoverAction";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { matchesTriggerItemQuery } from "./matchesTriggerItemQuery";
|
|
3
|
+
import type { Unstable_TriggerItem } from "@assistant-ui/core";
|
|
4
|
+
|
|
5
|
+
const item = (
|
|
6
|
+
overrides: Partial<Unstable_TriggerItem>,
|
|
7
|
+
): Unstable_TriggerItem => ({
|
|
8
|
+
id: "summarize",
|
|
9
|
+
type: "command",
|
|
10
|
+
label: "Summarize",
|
|
11
|
+
...overrides,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("matchesTriggerItemQuery", () => {
|
|
15
|
+
it("matches every item on an empty query", () => {
|
|
16
|
+
expect(matchesTriggerItemQuery(item({}), "")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("matches case-insensitively against the id", () => {
|
|
20
|
+
expect(
|
|
21
|
+
matchesTriggerItemQuery(item({ id: "SummArize", label: "y" }), "marize"),
|
|
22
|
+
).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("matches case-insensitively against the label", () => {
|
|
26
|
+
expect(
|
|
27
|
+
matchesTriggerItemQuery(
|
|
28
|
+
item({ id: "x", label: "Translate Text" }),
|
|
29
|
+
"late",
|
|
30
|
+
),
|
|
31
|
+
).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("matches against the description when present", () => {
|
|
35
|
+
expect(
|
|
36
|
+
matchesTriggerItemQuery(
|
|
37
|
+
item({ id: "x", label: "y", description: "Condense the thread" }),
|
|
38
|
+
"condense",
|
|
39
|
+
),
|
|
40
|
+
).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not match when no field contains the query", () => {
|
|
44
|
+
expect(
|
|
45
|
+
matchesTriggerItemQuery(item({ description: "nothing here" }), "zzz"),
|
|
46
|
+
).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not throw on items without a description", () => {
|
|
50
|
+
expect(matchesTriggerItemQuery(item({}), "zzz")).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|