@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
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
8
|
-
type MutableRefObject,
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from "react";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
McpAppMetadata,
|
|
13
|
+
TextMessagePart,
|
|
14
|
+
ToolCallMessagePart,
|
|
15
|
+
} from "@assistant-ui/core";
|
|
12
16
|
import type {
|
|
13
17
|
ToolCallMessagePartComponent,
|
|
14
18
|
ToolCallMessagePartProps,
|
|
@@ -29,13 +33,11 @@ import type {
|
|
|
29
33
|
import { getMcpAppFromToolPart } from "./utils";
|
|
30
34
|
import { isRecord } from "@assistant-ui/core/internal";
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*/
|
|
38
|
-
host: ResourceElement<McpAppsHost>;
|
|
36
|
+
/**
|
|
37
|
+
* Options that apply to a single MCP app. `McpAppRenderer` takes them as
|
|
38
|
+
* thread-wide defaults, and its `forPart` resolver overrides them for one part.
|
|
39
|
+
*/
|
|
40
|
+
export type McpAppPartOptions = {
|
|
39
41
|
/** Sandbox + container styling. Passes through to SafeContentFrame. */
|
|
40
42
|
sandbox?: McpAppSandboxConfig;
|
|
41
43
|
/**
|
|
@@ -47,6 +49,18 @@ export type McpAppRendererOptions = {
|
|
|
47
49
|
hostInfo?: McpAppHostInfo;
|
|
48
50
|
/** Delivered to the widget on initialize and pushed via `notifications/host_context/changed` on change. */
|
|
49
51
|
hostContext?: McpAppHostContext;
|
|
52
|
+
/**
|
|
53
|
+
* Optional widget interaction and lifecycle handlers. Data-plane handlers
|
|
54
|
+
* (`callTool`, `readResource`, `listResources`) always use `host`.
|
|
55
|
+
*
|
|
56
|
+
* Which handlers a widget may call is captured when its frame mounts, so a
|
|
57
|
+
* `forPart` resolver returns the same handler keys for a part on every
|
|
58
|
+
* render even when the implementations change.
|
|
59
|
+
*/
|
|
60
|
+
handlers?: Omit<
|
|
61
|
+
McpAppBridgeHandlers,
|
|
62
|
+
"callTool" | "readResource" | "listResources"
|
|
63
|
+
>;
|
|
50
64
|
/** Rendered when no MCP app is on the part, or while load is in flight / failed (unless overridden). */
|
|
51
65
|
fallback?: ReactNode;
|
|
52
66
|
/** Rendered while the resource is loading. Defaults to `fallback`. */
|
|
@@ -55,6 +69,26 @@ export type McpAppRendererOptions = {
|
|
|
55
69
|
errorFallback?: ReactNode | ((error: Error) => ReactNode);
|
|
56
70
|
};
|
|
57
71
|
|
|
72
|
+
export type McpAppRendererOptions = McpAppPartOptions & {
|
|
73
|
+
/**
|
|
74
|
+
* Provides the data-plane operations the widget can request
|
|
75
|
+
* (`loadResource`, `callTool`, `readResource`, `listResources`). Use
|
|
76
|
+
* `McpAppsRemoteHost({ url })` for the default HTTP-route convention.
|
|
77
|
+
*/
|
|
78
|
+
host: ResourceElement<McpAppsHost>;
|
|
79
|
+
/**
|
|
80
|
+
* Resolves the options for one part. Each returned value replaces the
|
|
81
|
+
* thread-wide option of the same name, so a host can give every MCP app its
|
|
82
|
+
* own display mode and answer `requestDisplayMode` for the part that asked.
|
|
83
|
+
*
|
|
84
|
+
* `handlers` is the exception and merges per key. Its keys are capabilities
|
|
85
|
+
* negotiated with the widget rather than data, so replacing the whole bag to
|
|
86
|
+
* add one handler would withdraw the others from that part's widget with
|
|
87
|
+
* nothing to observe it by.
|
|
88
|
+
*/
|
|
89
|
+
forPart?: (part: ToolCallMessagePart) => McpAppPartOptions;
|
|
90
|
+
};
|
|
91
|
+
|
|
58
92
|
type LoadedResourceState = {
|
|
59
93
|
host: McpAppsHost;
|
|
60
94
|
resourceUri: string;
|
|
@@ -63,7 +97,31 @@ type LoadedResourceState = {
|
|
|
63
97
|
error?: Error;
|
|
64
98
|
};
|
|
65
99
|
|
|
66
|
-
type
|
|
100
|
+
type RendererState = {
|
|
101
|
+
host: McpAppsHost;
|
|
102
|
+
options: McpAppRendererOptions;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type UseRendererStore = UseBoundStore<StoreApi<RendererState>>;
|
|
106
|
+
|
|
107
|
+
// Callers pass options as an object literal, so the resource hands this a fresh
|
|
108
|
+
// identity on every run; comparing the values keeps an unchanged run from
|
|
109
|
+
// re-rendering every mounted part. `host` is excluded because it is an element
|
|
110
|
+
// rebuilt on every run, and the store carries the resolved host separately.
|
|
111
|
+
const isSameOptions = (
|
|
112
|
+
a: McpAppRendererOptions,
|
|
113
|
+
b: McpAppRendererOptions,
|
|
114
|
+
): boolean => {
|
|
115
|
+
const aKeys = Object.keys(a).filter((key) => key !== "host");
|
|
116
|
+
const bKeys = Object.keys(b).filter((key) => key !== "host");
|
|
117
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
118
|
+
const aValues = a as Record<string, unknown>;
|
|
119
|
+
const bValues = b as Record<string, unknown>;
|
|
120
|
+
return aKeys.every(
|
|
121
|
+
(key) =>
|
|
122
|
+
Object.hasOwn(bValues, key) && Object.is(aValues[key], bValues[key]),
|
|
123
|
+
);
|
|
124
|
+
};
|
|
67
125
|
|
|
68
126
|
function getInput(part: {
|
|
69
127
|
status: { type: string };
|
|
@@ -83,45 +141,80 @@ const defaultOpenLink = ({ url }: { url: string }) => {
|
|
|
83
141
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
84
142
|
};
|
|
85
143
|
|
|
86
|
-
function
|
|
87
|
-
if (typeof params === "string") return params;
|
|
144
|
+
function extractSendMessageTexts(params: unknown): string[] | undefined {
|
|
145
|
+
if (typeof params === "string") return [params];
|
|
88
146
|
if (!params || typeof params !== "object") return undefined;
|
|
89
147
|
const obj = params as Record<string, unknown>;
|
|
90
|
-
if (typeof obj["prompt"] === "string") return obj["prompt"];
|
|
91
|
-
if (typeof obj["text"] === "string") return obj["text"];
|
|
92
|
-
if (typeof obj["message"] === "string") return obj["message"];
|
|
148
|
+
if (typeof obj["prompt"] === "string") return [obj["prompt"]];
|
|
149
|
+
if (typeof obj["text"] === "string") return [obj["text"]];
|
|
150
|
+
if (typeof obj["message"] === "string") return [obj["message"]];
|
|
151
|
+
if (Array.isArray(obj["content"])) {
|
|
152
|
+
return obj["content"]
|
|
153
|
+
.filter(
|
|
154
|
+
(block): block is { type: "text"; text: string } =>
|
|
155
|
+
isRecord(block) &&
|
|
156
|
+
block["type"] === "text" &&
|
|
157
|
+
typeof block["text"] === "string",
|
|
158
|
+
)
|
|
159
|
+
.map((block) => block.text);
|
|
160
|
+
}
|
|
93
161
|
return undefined;
|
|
94
162
|
}
|
|
95
163
|
|
|
164
|
+
function extractSendMessageContent(
|
|
165
|
+
params: unknown,
|
|
166
|
+
): { content: TextMessagePart[] } | { reason: string } {
|
|
167
|
+
const texts = extractSendMessageTexts(params);
|
|
168
|
+
if (!texts) return { reason: "unrecognised params shape" };
|
|
169
|
+
const content = texts
|
|
170
|
+
.filter((text) => text !== "")
|
|
171
|
+
.map((text): TextMessagePart => ({ type: "text", text }));
|
|
172
|
+
if (content.length === 0) {
|
|
173
|
+
return { reason: "no text content to append" };
|
|
174
|
+
}
|
|
175
|
+
return { content };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolvePartOptions(
|
|
179
|
+
options: McpAppRendererOptions,
|
|
180
|
+
part: ToolCallMessagePartProps,
|
|
181
|
+
): McpAppRendererOptions {
|
|
182
|
+
const overrides = options.forPart?.(part);
|
|
183
|
+
if (!overrides) return options;
|
|
184
|
+
const handlers =
|
|
185
|
+
options.handlers && overrides.handlers
|
|
186
|
+
? { ...options.handlers, ...overrides.handlers }
|
|
187
|
+
: (overrides.handlers ?? options.handlers);
|
|
188
|
+
return {
|
|
189
|
+
...options,
|
|
190
|
+
...overrides,
|
|
191
|
+
...(handlers === undefined ? {} : { handlers }),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
96
195
|
function InlineRenderer({
|
|
97
196
|
part,
|
|
98
|
-
|
|
99
|
-
optionsRef,
|
|
197
|
+
useRendererStore,
|
|
100
198
|
}: {
|
|
101
199
|
part: ToolCallMessagePartProps;
|
|
102
|
-
|
|
103
|
-
optionsRef: MutableRefObject<McpAppRendererOptions>;
|
|
200
|
+
useRendererStore: UseRendererStore;
|
|
104
201
|
}) {
|
|
105
|
-
const opts = optionsRef.current;
|
|
106
202
|
const aui = useAui();
|
|
203
|
+
const rendererOptions = useRendererStore((state) => state.options);
|
|
204
|
+
const opts = resolvePartOptions(rendererOptions, part);
|
|
107
205
|
const app = getMcpAppFromToolPart(part);
|
|
108
206
|
const cachedAppRef = useRef<McpAppMetadata | undefined>(undefined);
|
|
109
|
-
|
|
110
|
-
app != null
|
|
111
|
-
|
|
112
|
-
cachedAppRef.current?.serverId !== app.serverId)
|
|
113
|
-
) {
|
|
114
|
-
cachedAppRef.current = app;
|
|
115
|
-
}
|
|
207
|
+
useLayoutEffect(() => {
|
|
208
|
+
if (app != null) cachedAppRef.current = app;
|
|
209
|
+
}, [app]);
|
|
116
210
|
const appForRender = app ?? cachedAppRef.current;
|
|
117
211
|
|
|
118
212
|
const [loadedResource, setLoadedResource] = useState<LoadedResourceState>();
|
|
119
213
|
|
|
120
|
-
const host =
|
|
214
|
+
const host = useRendererStore((state) => state.host);
|
|
121
215
|
const resourceUri = appForRender?.resourceUri;
|
|
122
216
|
const serverId = appForRender?.serverId;
|
|
123
|
-
const
|
|
124
|
-
serverIdRef.current = serverId;
|
|
217
|
+
const callerHandlers = opts.handlers;
|
|
125
218
|
useEffect(() => {
|
|
126
219
|
if (resourceUri == null) return;
|
|
127
220
|
let cancelled = false;
|
|
@@ -129,12 +222,16 @@ function InlineRenderer({
|
|
|
129
222
|
const targetUri = resourceUri;
|
|
130
223
|
const targetServerId = serverId;
|
|
131
224
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
225
|
+
// Host changes are published later in this passive flush. Defer until that
|
|
226
|
+
// publication lands, then verify this effect still owns the current host.
|
|
227
|
+
const loadResource = async () => {
|
|
228
|
+
await Promise.resolve();
|
|
229
|
+
if (cancelled || useRendererStore.getState().host !== targetHost) return;
|
|
230
|
+
try {
|
|
231
|
+
const res = await targetHost.loadResource({
|
|
232
|
+
uri: targetUri,
|
|
233
|
+
...(targetServerId ? { serverId: targetServerId } : {}),
|
|
234
|
+
});
|
|
138
235
|
if (!cancelled)
|
|
139
236
|
setLoadedResource({
|
|
140
237
|
host: targetHost,
|
|
@@ -144,8 +241,7 @@ function InlineRenderer({
|
|
|
144
241
|
: {}),
|
|
145
242
|
resource: res,
|
|
146
243
|
});
|
|
147
|
-
})
|
|
148
|
-
.catch((error: unknown) => {
|
|
244
|
+
} catch (error: unknown) {
|
|
149
245
|
if (!cancelled) {
|
|
150
246
|
setLoadedResource({
|
|
151
247
|
host: targetHost,
|
|
@@ -156,43 +252,50 @@ function InlineRenderer({
|
|
|
156
252
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
157
253
|
});
|
|
158
254
|
}
|
|
159
|
-
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
void loadResource();
|
|
160
258
|
|
|
161
259
|
return () => {
|
|
162
260
|
cancelled = true;
|
|
163
261
|
};
|
|
164
|
-
}, [host, resourceUri, serverId]);
|
|
262
|
+
}, [host, resourceUri, serverId, useRendererStore]);
|
|
165
263
|
|
|
166
264
|
const bridgeHandlers = useMemo<McpAppBridgeHandlers>(
|
|
167
265
|
() => ({
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
266
|
+
...callerHandlers,
|
|
267
|
+
openLink: callerHandlers?.openLink ?? defaultOpenLink,
|
|
268
|
+
sendMessage:
|
|
269
|
+
callerHandlers?.sendMessage ??
|
|
270
|
+
((params) => {
|
|
271
|
+
const result = extractSendMessageContent(params);
|
|
272
|
+
if ("reason" in result) {
|
|
273
|
+
return { isError: true, ok: false, reason: result.reason };
|
|
274
|
+
}
|
|
275
|
+
aui.thread.append({ content: result.content });
|
|
276
|
+
return { ok: true };
|
|
277
|
+
}),
|
|
175
278
|
callTool: (params) =>
|
|
176
|
-
|
|
279
|
+
useRendererStore.getState().host.callTool({
|
|
177
280
|
...params,
|
|
178
|
-
...(
|
|
281
|
+
...(serverId ? { serverId } : {}),
|
|
179
282
|
}),
|
|
180
283
|
readResource: (params) =>
|
|
181
|
-
|
|
284
|
+
useRendererStore.getState().host.readResource({
|
|
182
285
|
...params,
|
|
183
|
-
...(
|
|
286
|
+
...(serverId ? { serverId } : {}),
|
|
184
287
|
}),
|
|
185
288
|
listResources: (params) => {
|
|
186
|
-
if (!
|
|
187
|
-
return
|
|
289
|
+
if (!serverId) {
|
|
290
|
+
return useRendererStore.getState().host.listResources(params);
|
|
188
291
|
}
|
|
189
|
-
return
|
|
292
|
+
return useRendererStore.getState().host.listResources({
|
|
190
293
|
...(isRecord(params) ? params : {}),
|
|
191
|
-
serverId
|
|
294
|
+
serverId,
|
|
192
295
|
});
|
|
193
296
|
},
|
|
194
297
|
}),
|
|
195
|
-
[aui,
|
|
298
|
+
[aui, callerHandlers, serverId, useRendererStore],
|
|
196
299
|
);
|
|
197
300
|
|
|
198
301
|
const loadedResourceForApp =
|
|
@@ -246,27 +349,26 @@ const useMcpAppRenderer = (
|
|
|
246
349
|
): { readonly render: ToolCallMessagePartComponent } => {
|
|
247
350
|
const host = useResource(options.host);
|
|
248
351
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
create<{ host: McpAppsHost }>(() => ({ host })),
|
|
352
|
+
// The rendered component identity has to stay stable, or every part remounts
|
|
353
|
+
// its frame; options reach the parts through the store instead of props.
|
|
354
|
+
const [useRendererStore] = useState(() =>
|
|
355
|
+
create<RendererState>(() => ({ host, options })),
|
|
254
356
|
);
|
|
255
357
|
useEffect(() => {
|
|
256
|
-
|
|
257
|
-
|
|
358
|
+
useRendererStore.setState((prev) =>
|
|
359
|
+
prev.host === host && isSameOptions(prev.options, options)
|
|
360
|
+
? prev
|
|
361
|
+
: { host, options },
|
|
362
|
+
);
|
|
363
|
+
}, [host, options, useRendererStore]);
|
|
258
364
|
|
|
259
365
|
const render = useMemo((): ToolCallMessagePartComponent => {
|
|
260
366
|
const Render: ToolCallMessagePartComponent = (props) => (
|
|
261
|
-
<InlineRenderer
|
|
262
|
-
part={props}
|
|
263
|
-
useHostStore={useHostStore}
|
|
264
|
-
optionsRef={optionsRef}
|
|
265
|
-
/>
|
|
367
|
+
<InlineRenderer part={props} useRendererStore={useRendererStore} />
|
|
266
368
|
);
|
|
267
369
|
Render.displayName = "McpAppRenderer";
|
|
268
370
|
return Render;
|
|
269
|
-
}, [
|
|
371
|
+
}, [useRendererStore]);
|
|
270
372
|
|
|
271
373
|
return { render };
|
|
272
374
|
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, render, waitFor } from "@testing-library/react";
|
|
4
|
+
import { useResource } from "@assistant-ui/tap";
|
|
5
|
+
import {
|
|
6
|
+
startTransition,
|
|
7
|
+
Suspense,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { describe, expect, it, vi } from "vitest";
|
|
12
|
+
import type { McpAppsHost } from "./types";
|
|
13
|
+
import { McpAppsRemoteHost } from "./McpAppsRemoteHost";
|
|
14
|
+
|
|
15
|
+
describe("McpAppsRemoteHost concurrent rendering", () => {
|
|
16
|
+
it("keeps committed authorization options during an interrupted render", async () => {
|
|
17
|
+
const pending = new Promise<never>(() => {});
|
|
18
|
+
const fetch = vi.fn(async () => Response.json({ content: [] }));
|
|
19
|
+
let committedHost: McpAppsHost | undefined;
|
|
20
|
+
|
|
21
|
+
const Probe = ({
|
|
22
|
+
authorization,
|
|
23
|
+
suspend,
|
|
24
|
+
children,
|
|
25
|
+
}: {
|
|
26
|
+
authorization: string;
|
|
27
|
+
suspend: boolean;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}) => {
|
|
30
|
+
const host = useResource(
|
|
31
|
+
McpAppsRemoteHost({
|
|
32
|
+
url: "/api/mcp-apps",
|
|
33
|
+
fetch,
|
|
34
|
+
headers: { authorization },
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
useLayoutEffect(() => {
|
|
38
|
+
committedHost = host;
|
|
39
|
+
}, [host]);
|
|
40
|
+
if (suspend) throw pending;
|
|
41
|
+
return children;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const view = render(
|
|
45
|
+
<Suspense fallback={null}>
|
|
46
|
+
<Probe authorization="Bearer workspace-a" suspend={false}>
|
|
47
|
+
workspace-a
|
|
48
|
+
</Probe>
|
|
49
|
+
</Suspense>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
startTransition(() => {
|
|
54
|
+
view.rerender(
|
|
55
|
+
<Suspense fallback={null}>
|
|
56
|
+
<Probe authorization="Bearer workspace-b" suspend>
|
|
57
|
+
workspace-b
|
|
58
|
+
</Probe>
|
|
59
|
+
</Suspense>,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(view.container.textContent).toBe("workspace-a");
|
|
65
|
+
await committedHost?.callTool({ name: "search" });
|
|
66
|
+
|
|
67
|
+
expect(fetch).toHaveBeenCalledWith("/api/mcp-apps", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"content-type": "application/json",
|
|
71
|
+
authorization: "Bearer workspace-a",
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
method: "tools/call",
|
|
75
|
+
params: { name: "search" },
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
view.unmount();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("keeps the URL and authorization from one committed options snapshot", async () => {
|
|
83
|
+
const fetch = vi.fn(async () => Response.json({ content: [] }));
|
|
84
|
+
let currentHost: McpAppsHost | undefined;
|
|
85
|
+
|
|
86
|
+
const Probe = ({
|
|
87
|
+
url,
|
|
88
|
+
authorization,
|
|
89
|
+
}: {
|
|
90
|
+
url: string;
|
|
91
|
+
authorization: string;
|
|
92
|
+
}) => {
|
|
93
|
+
const host = useResource(
|
|
94
|
+
McpAppsRemoteHost({
|
|
95
|
+
url,
|
|
96
|
+
fetch,
|
|
97
|
+
headers: { authorization },
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
useLayoutEffect(() => {
|
|
101
|
+
currentHost = host;
|
|
102
|
+
void host.callTool({ name: "layout-request" });
|
|
103
|
+
}, [host]);
|
|
104
|
+
return null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const view = render(
|
|
108
|
+
<Probe url="/workspace-a/mcp" authorization="Bearer workspace-a" />,
|
|
109
|
+
);
|
|
110
|
+
fetch.mockClear();
|
|
111
|
+
|
|
112
|
+
view.rerender(
|
|
113
|
+
<Probe url="/workspace-b/mcp" authorization="Bearer workspace-b" />,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(fetch).toHaveBeenNthCalledWith(1, "/workspace-b/mcp", {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"content-type": "application/json",
|
|
120
|
+
authorization: "Bearer workspace-b",
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
method: "tools/call",
|
|
124
|
+
params: { name: "layout-request" },
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await currentHost?.callTool({ name: "committed-request" });
|
|
129
|
+
|
|
130
|
+
expect(fetch).toHaveBeenNthCalledWith(2, "/workspace-b/mcp", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
"content-type": "application/json",
|
|
134
|
+
authorization: "Bearer workspace-b",
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
method: "tools/call",
|
|
138
|
+
params: { name: "committed-request" },
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
view.unmount();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("refreshes pending options when a URL switch render retries", async () => {
|
|
146
|
+
let authorization = "Bearer workspace-b-old";
|
|
147
|
+
let resumed = false;
|
|
148
|
+
let resume: (() => void) | undefined;
|
|
149
|
+
const pending = new Promise<void>((resolve) => {
|
|
150
|
+
resume = () => {
|
|
151
|
+
resumed = true;
|
|
152
|
+
resolve();
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
const fetch = vi.fn(async () => Response.json({ content: [] }));
|
|
156
|
+
|
|
157
|
+
const Probe = ({ url }: { url: string }) => {
|
|
158
|
+
const host = useResource(
|
|
159
|
+
McpAppsRemoteHost({
|
|
160
|
+
url,
|
|
161
|
+
fetch,
|
|
162
|
+
headers: {
|
|
163
|
+
authorization:
|
|
164
|
+
url === "/workspace-a/mcp" ? "Bearer workspace-a" : authorization,
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
useLayoutEffect(() => {
|
|
169
|
+
void host.callTool({ name: "layout-request" });
|
|
170
|
+
}, [host]);
|
|
171
|
+
if (url === "/workspace-b/mcp" && !resumed) throw pending;
|
|
172
|
+
return <span>{url}</span>;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const view = render(
|
|
176
|
+
<Suspense fallback={null}>
|
|
177
|
+
<Probe url="/workspace-a/mcp" />
|
|
178
|
+
</Suspense>,
|
|
179
|
+
);
|
|
180
|
+
try {
|
|
181
|
+
fetch.mockClear();
|
|
182
|
+
|
|
183
|
+
act(() => {
|
|
184
|
+
startTransition(() => {
|
|
185
|
+
view.rerender(
|
|
186
|
+
<Suspense fallback={null}>
|
|
187
|
+
<Probe url="/workspace-b/mcp" />
|
|
188
|
+
</Suspense>,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
expect(view.container.textContent).toBe("/workspace-a/mcp");
|
|
193
|
+
|
|
194
|
+
authorization = "Bearer workspace-b-new";
|
|
195
|
+
await act(async () => resume?.());
|
|
196
|
+
await waitFor(() =>
|
|
197
|
+
expect(view.container.textContent).toBe("/workspace-b/mcp"),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(fetch).toHaveBeenCalledWith("/workspace-b/mcp", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
"content-type": "application/json",
|
|
204
|
+
authorization: "Bearer workspace-b-new",
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
method: "tools/call",
|
|
208
|
+
params: { name: "layout-request" },
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
} finally {
|
|
212
|
+
view.unmount();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useRef } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { resource } from "@assistant-ui/tap";
|
|
3
3
|
import type {
|
|
4
4
|
McpAppResource,
|
|
@@ -111,20 +111,17 @@ const useMcpAppsRemoteHost = (
|
|
|
111
111
|
options: McpAppsRemoteHostOptions,
|
|
112
112
|
): McpAppsHost => {
|
|
113
113
|
const optionsRef = useRef(options);
|
|
114
|
-
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
optionsRef.current = options;
|
|
116
|
+
});
|
|
115
117
|
|
|
116
118
|
const url = options.url;
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
...(current.fetch !== undefined ? { fetch: current.fetch } : {}),
|
|
124
|
-
...(current.headers !== undefined ? { headers: current.headers } : {}),
|
|
125
|
-
};
|
|
126
|
-
};
|
|
127
|
-
return {
|
|
120
|
+
const hostState = useMemo(() => {
|
|
121
|
+
let pendingOptions = options;
|
|
122
|
+
const getCurrentOptions = () =>
|
|
123
|
+
optionsRef.current.url === url ? optionsRef.current : pendingOptions;
|
|
124
|
+
const host: McpAppsHost = {
|
|
128
125
|
loadResource: async (params) => {
|
|
129
126
|
const options = getCurrentOptions();
|
|
130
127
|
return parseMcpAppResource(
|
|
@@ -139,7 +136,26 @@ const useMcpAppsRemoteHost = (
|
|
|
139
136
|
listResources: (params) =>
|
|
140
137
|
postToHost(getCurrentOptions(), "resources/list", params),
|
|
141
138
|
};
|
|
139
|
+
return {
|
|
140
|
+
host,
|
|
141
|
+
updatePendingOptions: (next: McpAppsRemoteHostOptions) => {
|
|
142
|
+
pendingOptions = next;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
// oxlint-disable-next-line react/exhaustive-deps -- URL changes replace the host identity; pending and same-URL options are refreshed outside the memo
|
|
142
146
|
}, [url]);
|
|
147
|
+
|
|
148
|
+
// The pending snapshot is read only while the committed ref still lags this
|
|
149
|
+
// host's URL, and every write to it carries that same render's options, so
|
|
150
|
+
// no render can pair one URL with another's credentials. A committed host can
|
|
151
|
+
// be written here (a child layout effect re-rendering synchronously runs
|
|
152
|
+
// before passive effects publish the ref), which stays coherent for the same
|
|
153
|
+
// reason.
|
|
154
|
+
if (optionsRef.current.url !== url) {
|
|
155
|
+
hostState.updatePendingOptions(options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return hostState.host;
|
|
143
159
|
};
|
|
144
160
|
|
|
145
161
|
export const McpAppsRemoteHost = resource(useMcpAppsRemoteHost);
|