@assistant-ui/react 0.15.15 → 0.15.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/devtools/DevToolsHooks.d.ts +0 -2
- package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
- package/dist/devtools/DevToolsHooks.js +10 -12
- package/dist/devtools/DevToolsHooks.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/mcp-apps/McpAppRenderer.d.ts +6 -1
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +20 -13
- 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 +189 -94
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.d.ts.map +1 -1
- package/dist/mcp-apps/bridge.js +2 -8
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/index.d.ts +2 -2
- 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.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +7 -4
- 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.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerRoot.js +20 -15
- 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.d.ts +2 -2
- package/dist/primitives/composer/ComposerDictationTranscript.js +2 -2
- 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/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/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/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/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/triggerSelectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +10 -7
- 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/message.js +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/ThreadRoot.d.ts +3 -0
- package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadRoot.js +3 -0
- package/dist/primitives/thread/ThreadRoot.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.js +1 -1
- 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 +138 -68
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.js +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +3 -1
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +41 -27
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/utils/Primitive.d.ts +2 -2
- 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/invokeCallbackSafely.d.ts +5 -0
- package/dist/utils/invokeCallbackSafely.d.ts.map +1 -0
- package/dist/utils/invokeCallbackSafely.js +15 -0
- package/dist/utils/invokeCallbackSafely.js.map +1 -0
- package/dist/utils/smooth/SmoothContext.d.ts +30 -6
- package/dist/utils/smooth/SmoothContext.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.d.ts +3 -0
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +17 -2
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/dist/utils/useToolArgsFieldStatus.d.ts +5 -1
- package/package.json +12 -20
- package/src/devtools/DevToolsHooks.test.ts +40 -0
- package/src/devtools/DevToolsHooks.ts +10 -15
- package/src/index.ts +9 -1
- package/src/mcp-apps/McpAppRenderer.test.tsx +133 -4
- package/src/mcp-apps/McpAppRenderer.tsx +28 -21
- 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 +102 -0
- package/src/mcp-apps/app-frame.tsx +17 -4
- package/src/mcp-apps/bridge.test.ts +21 -0
- package/src/mcp-apps/bridge.ts +2 -14
- package/src/mcp-apps/index.ts +1 -0
- 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.test.tsx +67 -0
- package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +4 -2
- 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/branchPicker/BranchPickerRoot.tsx +5 -3
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +1 -1
- package/src/primitives/composer/ComposerDictationTranscript.tsx +2 -2
- package/src/primitives/composer/ComposerInput.test.tsx +2 -1
- package/src/primitives/composer/ComposerInput.tsx +3 -3
- 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/TriggerPopoverBack.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +1 -1
- 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/triggerDetectionResource.ts +10 -7
- package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +147 -0
- package/src/primitives/composer/trigger/triggerSelectionResource.ts +3 -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/ThreadRoot.tsx +3 -0
- package/src/primitives/thread/ThreadViewport.tsx +1 -1
- package/src/primitives/thread/ThreadViewportFooter.tsx +1 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +1 -1
- 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 +106 -1
- package/src/sandbox-host/SandboxHost.tsx +22 -5
- package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +31 -1
- package/src/unstable/useLiveCompletionAdapter.test.tsx +117 -1
- package/src/unstable/useLiveCompletionAdapter.ts +4 -1
- package/src/unstable/useSlashCommandAdapter.test.tsx +141 -0
- package/src/unstable/useSlashCommandAdapter.ts +52 -17
- 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/invokeCallbackSafely.ts +11 -0
- package/src/utils/smooth/useSmooth.test.tsx +110 -0
- package/src/utils/smooth/useSmooth.ts +21 -7
- package/dist/context/react/utils/ensureBinding.d.ts +0 -5
- package/dist/context/react/utils/ensureBinding.d.ts.map +0 -1
- package/dist/context/react/utils/ensureBinding.js +0 -24
- package/dist/context/react/utils/ensureBinding.js.map +0 -1
- package/dist/context/react/utils/useRuntimeState.d.ts +0 -13
- package/dist/context/react/utils/useRuntimeState.d.ts.map +0 -1
- package/dist/context/react/utils/useRuntimeState.js +0 -33
- package/dist/context/react/utils/useRuntimeState.js.map +0 -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/src/context/react/utils/ensureBinding.ts +0 -46
- package/src/context/react/utils/useRuntimeState.ts +0 -46
- 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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DevToolsHooks, DevToolsProviderApi } from "./DevToolsHooks";
|
|
3
|
+
|
|
4
|
+
describe("DevToolsHooks", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("notifies every listener when one throws", () => {
|
|
10
|
+
const error = new Error("listener failed");
|
|
11
|
+
const consoleError = vi
|
|
12
|
+
.spyOn(console, "error")
|
|
13
|
+
.mockImplementation(() => {});
|
|
14
|
+
const laterListener = vi.fn();
|
|
15
|
+
const unsubscribeThrowing = DevToolsHooks.subscribe(() => {
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
const unsubscribeLater = DevToolsHooks.subscribe(laterListener);
|
|
19
|
+
let unregister = () => {};
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
unregister = DevToolsProviderApi.register({});
|
|
23
|
+
const apiId = [...DevToolsHooks.getApis().keys()].at(-1);
|
|
24
|
+
expect(apiId).toBeDefined();
|
|
25
|
+
|
|
26
|
+
DevToolsHooks.clearEventLogs(apiId!);
|
|
27
|
+
|
|
28
|
+
expect(laterListener).toHaveBeenCalledTimes(2);
|
|
29
|
+
expect(consoleError).toHaveBeenCalledTimes(2);
|
|
30
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
31
|
+
"[assistant-ui] DevTools listener threw an error",
|
|
32
|
+
error,
|
|
33
|
+
);
|
|
34
|
+
} finally {
|
|
35
|
+
unsubscribeThrowing();
|
|
36
|
+
unsubscribeLater();
|
|
37
|
+
unregister();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Unsubscribe } from "@assistant-ui/core";
|
|
2
|
+
import { notifyEventListeners } from "@assistant-ui/core/internal";
|
|
2
3
|
import type { AssistantClient } from "@assistant-ui/store";
|
|
3
4
|
|
|
4
5
|
export interface EventLog {
|
|
@@ -54,6 +55,10 @@ const getHook = (): DevToolsHook => {
|
|
|
54
55
|
return newHook;
|
|
55
56
|
};
|
|
56
57
|
|
|
58
|
+
const notifyListeners = (apiId: number): void => {
|
|
59
|
+
notifyEventListeners(getHook().listeners, apiId, "DevTools");
|
|
60
|
+
};
|
|
61
|
+
|
|
57
62
|
export class DevToolsHooks {
|
|
58
63
|
static subscribe(listener: () => void): Unsubscribe {
|
|
59
64
|
const hook = getHook();
|
|
@@ -69,17 +74,12 @@ export class DevToolsHooks {
|
|
|
69
74
|
if (!entry) return;
|
|
70
75
|
|
|
71
76
|
entry.logs = [];
|
|
72
|
-
|
|
77
|
+
notifyListeners(apiId);
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
static getApis(): Map<number, DevToolsApiEntry> {
|
|
76
81
|
return getHook().apis;
|
|
77
82
|
}
|
|
78
|
-
|
|
79
|
-
private static notifyListeners(apiId: number): void {
|
|
80
|
-
const hook = getHook();
|
|
81
|
-
hook.listeners.forEach((listener) => listener(apiId));
|
|
82
|
-
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export class DevToolsProviderApi {
|
|
@@ -116,15 +116,15 @@ export class DevToolsProviderApi {
|
|
|
116
116
|
);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
notifyListeners(apiId);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
const stateUnsubscribe = aui.subscribe?.(() => {
|
|
123
|
-
|
|
123
|
+
notifyListeners(apiId);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
hook.apis.set(apiId, entry);
|
|
127
|
-
|
|
127
|
+
notifyListeners(apiId);
|
|
128
128
|
|
|
129
129
|
return () => {
|
|
130
130
|
const hook = getHook();
|
|
@@ -136,12 +136,7 @@ export class DevToolsProviderApi {
|
|
|
136
136
|
|
|
137
137
|
hook.apis.delete(apiId);
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
notifyListeners(apiId);
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
private static notifyListeners(apiId: number): void {
|
|
144
|
-
const hook = getHook();
|
|
145
|
-
hook.listeners.forEach((listener) => listener(apiId));
|
|
146
|
-
}
|
|
147
142
|
}
|
package/src/index.ts
CHANGED
|
@@ -83,7 +83,12 @@ export {
|
|
|
83
83
|
} from "@assistant-ui/core";
|
|
84
84
|
|
|
85
85
|
// --- adapters/suggestion ---
|
|
86
|
-
export type {
|
|
86
|
+
export type {
|
|
87
|
+
SuggestionAdapter,
|
|
88
|
+
SuggestionAdapterGenerateOptions,
|
|
89
|
+
CreateSuggestionAdapterOptions,
|
|
90
|
+
} from "@assistant-ui/core";
|
|
91
|
+
export { createSuggestionAdapter } from "@assistant-ui/core";
|
|
87
92
|
|
|
88
93
|
// --- adapters/RuntimeAdapterProvider ---
|
|
89
94
|
export {
|
|
@@ -475,6 +480,8 @@ export {
|
|
|
475
480
|
useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers,
|
|
476
481
|
useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional,
|
|
477
482
|
type RegisteredTrigger as Unstable_RegisteredTrigger,
|
|
483
|
+
type TriggerMatch as Unstable_TriggerMatch,
|
|
484
|
+
type TriggerMatcher as Unstable_TriggerMatcher,
|
|
478
485
|
type TriggerBehavior as Unstable_TriggerBehavior,
|
|
479
486
|
} from "./primitives/composer/trigger";
|
|
480
487
|
export type {
|
|
@@ -520,6 +527,7 @@ export type {
|
|
|
520
527
|
McpAppsHost,
|
|
521
528
|
McpAppsRemoteHostOptions,
|
|
522
529
|
McpAppToolCallParams,
|
|
530
|
+
McpAppBridgeHandlers,
|
|
523
531
|
ToolCallMessagePartMcpMetadata,
|
|
524
532
|
} from "./mcp-apps";
|
|
525
533
|
export type { McpAppResourceOutput } from "@assistant-ui/core/react";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { render, waitFor } from "@testing-library/react";
|
|
3
3
|
import { resource, useResource, withKey } from "@assistant-ui/tap";
|
|
4
|
-
import { memo } from "react";
|
|
4
|
+
import { act, memo, startTransition, Suspense } from "react";
|
|
5
5
|
import type {
|
|
6
6
|
ToolCallMessagePartComponent,
|
|
7
7
|
ToolCallMessagePartProps,
|
|
@@ -29,7 +29,7 @@ vi.mock("./app-frame", () => ({
|
|
|
29
29
|
},
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
-
import { McpAppRenderer } from "./McpAppRenderer";
|
|
32
|
+
import { McpAppRenderer, type McpAppRendererOptions } from "./McpAppRenderer";
|
|
33
33
|
import { McpAppsRemoteHost } from "./McpAppsRemoteHost";
|
|
34
34
|
|
|
35
35
|
const useHost = ({ host }: { host: McpAppsHost }) => host;
|
|
@@ -61,14 +61,31 @@ const createPart = (serverId?: string): ToolCallMessagePartProps => ({
|
|
|
61
61
|
respondToApproval: vi.fn(),
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
const createPartWithoutApp = (): ToolCallMessagePartProps => {
|
|
65
|
+
const part = createPart();
|
|
66
|
+
delete part.mcp;
|
|
67
|
+
return part;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function Harness({
|
|
71
|
+
host,
|
|
72
|
+
serverId,
|
|
73
|
+
handlers,
|
|
74
|
+
part,
|
|
75
|
+
}: {
|
|
76
|
+
host: McpAppsHost;
|
|
77
|
+
serverId?: string;
|
|
78
|
+
handlers?: McpAppRendererOptions["handlers"];
|
|
79
|
+
part?: ToolCallMessagePartProps;
|
|
80
|
+
}) {
|
|
65
81
|
const renderer = useResource(
|
|
66
82
|
McpAppRenderer({
|
|
67
83
|
host: Host({ host }),
|
|
84
|
+
...(handlers === undefined ? {} : { handlers }),
|
|
68
85
|
}),
|
|
69
86
|
);
|
|
70
87
|
const Renderer = renderer.render;
|
|
71
|
-
return <Renderer {...createPart(serverId)} />;
|
|
88
|
+
return <Renderer {...(part ?? createPart(serverId))} />;
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
const MemoizedPart = memo(function MemoizedPart({
|
|
@@ -142,6 +159,64 @@ describe("McpAppRenderer", () => {
|
|
|
142
159
|
});
|
|
143
160
|
});
|
|
144
161
|
|
|
162
|
+
it("keeps bridge server ids scoped to committed renders", async () => {
|
|
163
|
+
const host: McpAppsHost = {
|
|
164
|
+
loadResource: vi.fn(async ({ uri }) => ({
|
|
165
|
+
uri,
|
|
166
|
+
mimeType: "text/html;profile=mcp-app" as const,
|
|
167
|
+
html: "",
|
|
168
|
+
})),
|
|
169
|
+
callTool: vi.fn(),
|
|
170
|
+
readResource: vi.fn(),
|
|
171
|
+
listResources: vi.fn(),
|
|
172
|
+
};
|
|
173
|
+
const interruptedRender = vi.fn();
|
|
174
|
+
const pending = new Promise<never>(() => {});
|
|
175
|
+
const Blocker = ({ blocked }: { blocked: boolean }) => {
|
|
176
|
+
if (blocked) {
|
|
177
|
+
interruptedRender();
|
|
178
|
+
throw pending;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
};
|
|
182
|
+
const view = (part: ToolCallMessagePartProps, blocked: boolean) => (
|
|
183
|
+
<Suspense fallback={null}>
|
|
184
|
+
<Harness host={host} part={part} />
|
|
185
|
+
<Blocker blocked={blocked} />
|
|
186
|
+
</Suspense>
|
|
187
|
+
);
|
|
188
|
+
const rendered = render(view(createPart("server-a"), false));
|
|
189
|
+
await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
|
|
190
|
+
const handlers = framePropsMock.mock.lastCall?.[0]
|
|
191
|
+
.handlers as McpAppBridgeHandlers;
|
|
192
|
+
|
|
193
|
+
act(() => {
|
|
194
|
+
startTransition(() =>
|
|
195
|
+
rendered.rerender(view(createPart("server-b"), true)),
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
expect(interruptedRender).toHaveBeenCalled();
|
|
199
|
+
await handlers.callTool?.({ name: "search" });
|
|
200
|
+
|
|
201
|
+
expect(host.callTool).toHaveBeenCalledWith({
|
|
202
|
+
name: "search",
|
|
203
|
+
serverId: "server-a",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
framePropsMock.mockClear();
|
|
207
|
+
vi.mocked(host.callTool).mockClear();
|
|
208
|
+
rendered.rerender(view(createPartWithoutApp(), false));
|
|
209
|
+
await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
|
|
210
|
+
expect(framePropsMock.mock.lastCall?.[0].app.serverId).toBe("server-a");
|
|
211
|
+
const fallbackHandlers = framePropsMock.mock.lastCall?.[0]
|
|
212
|
+
.handlers as McpAppBridgeHandlers;
|
|
213
|
+
await fallbackHandlers.callTool?.({ name: "search" });
|
|
214
|
+
expect(host.callTool).toHaveBeenCalledWith({
|
|
215
|
+
name: "search",
|
|
216
|
+
serverId: "server-a",
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
145
220
|
it("reloads the resource and hides stale HTML when the host changes", async () => {
|
|
146
221
|
const nextResource = createDeferred<{
|
|
147
222
|
uri: string;
|
|
@@ -397,4 +472,58 @@ describe("McpAppRenderer", () => {
|
|
|
397
472
|
await handlers.listResources?.(params);
|
|
398
473
|
expect(listResources).toHaveBeenCalledWith(params);
|
|
399
474
|
});
|
|
475
|
+
|
|
476
|
+
it("uses caller UI handlers and keeps data-plane handlers on the host", async () => {
|
|
477
|
+
const host: McpAppsHost = {
|
|
478
|
+
loadResource: vi.fn(async ({ uri }) => ({
|
|
479
|
+
uri,
|
|
480
|
+
mimeType: "text/html;profile=mcp-app" as const,
|
|
481
|
+
html: "",
|
|
482
|
+
})),
|
|
483
|
+
callTool: vi.fn(),
|
|
484
|
+
readResource: vi.fn(),
|
|
485
|
+
listResources: vi.fn(),
|
|
486
|
+
};
|
|
487
|
+
const requestDisplayMode = vi.fn(({ mode }) => ({ mode }));
|
|
488
|
+
const updateModelContext = vi.fn();
|
|
489
|
+
const openLink = vi.fn();
|
|
490
|
+
const sendMessage = vi.fn();
|
|
491
|
+
const onInitialized = vi.fn();
|
|
492
|
+
|
|
493
|
+
render(
|
|
494
|
+
<Harness
|
|
495
|
+
host={host}
|
|
496
|
+
handlers={{
|
|
497
|
+
requestDisplayMode,
|
|
498
|
+
updateModelContext,
|
|
499
|
+
openLink,
|
|
500
|
+
sendMessage,
|
|
501
|
+
onInitialized,
|
|
502
|
+
}}
|
|
503
|
+
/>,
|
|
504
|
+
);
|
|
505
|
+
await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
|
|
506
|
+
const handlers = framePropsMock.mock.lastCall?.[0]
|
|
507
|
+
.handlers as McpAppBridgeHandlers;
|
|
508
|
+
|
|
509
|
+
expect(await handlers.requestDisplayMode?.({ mode: "fullscreen" })).toEqual(
|
|
510
|
+
{ mode: "fullscreen" },
|
|
511
|
+
);
|
|
512
|
+
await handlers.updateModelContext?.({ text: "context" });
|
|
513
|
+
await handlers.openLink?.({ url: "https://example.com" });
|
|
514
|
+
await handlers.sendMessage?.({ text: "hello" });
|
|
515
|
+
handlers.onInitialized?.();
|
|
516
|
+
await handlers.callTool?.({ name: "search" });
|
|
517
|
+
await handlers.readResource?.({ uri: "ui://resource" });
|
|
518
|
+
await handlers.listResources?.();
|
|
519
|
+
|
|
520
|
+
expect(requestDisplayMode).toHaveBeenCalledWith({ mode: "fullscreen" });
|
|
521
|
+
expect(updateModelContext).toHaveBeenCalledWith({ text: "context" });
|
|
522
|
+
expect(openLink).toHaveBeenCalledWith({ url: "https://example.com" });
|
|
523
|
+
expect(sendMessage).toHaveBeenCalledWith({ text: "hello" });
|
|
524
|
+
expect(onInitialized).toHaveBeenCalledOnce();
|
|
525
|
+
expect(host.callTool).toHaveBeenCalledWith({ name: "search" });
|
|
526
|
+
expect(host.readResource).toHaveBeenCalledWith({ uri: "ui://resource" });
|
|
527
|
+
expect(host.listResources).toHaveBeenCalledWith(undefined);
|
|
528
|
+
});
|
|
400
529
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
@@ -47,6 +48,14 @@ export type McpAppRendererOptions = {
|
|
|
47
48
|
hostInfo?: McpAppHostInfo;
|
|
48
49
|
/** Delivered to the widget on initialize and pushed via `notifications/host_context/changed` on change. */
|
|
49
50
|
hostContext?: McpAppHostContext;
|
|
51
|
+
/**
|
|
52
|
+
* Optional widget interaction and lifecycle handlers. Data-plane handlers
|
|
53
|
+
* (`callTool`, `readResource`, `listResources`) always use `host`.
|
|
54
|
+
*/
|
|
55
|
+
handlers?: Omit<
|
|
56
|
+
McpAppBridgeHandlers,
|
|
57
|
+
"callTool" | "readResource" | "listResources"
|
|
58
|
+
>;
|
|
50
59
|
/** Rendered when no MCP app is on the part, or while load is in flight / failed (unless overridden). */
|
|
51
60
|
fallback?: ReactNode;
|
|
52
61
|
/** Rendered while the resource is loading. Defaults to `fallback`. */
|
|
@@ -106,13 +115,9 @@ function InlineRenderer({
|
|
|
106
115
|
const aui = useAui();
|
|
107
116
|
const app = getMcpAppFromToolPart(part);
|
|
108
117
|
const cachedAppRef = useRef<McpAppMetadata | undefined>(undefined);
|
|
109
|
-
|
|
110
|
-
app != null
|
|
111
|
-
|
|
112
|
-
cachedAppRef.current?.serverId !== app.serverId)
|
|
113
|
-
) {
|
|
114
|
-
cachedAppRef.current = app;
|
|
115
|
-
}
|
|
118
|
+
useLayoutEffect(() => {
|
|
119
|
+
if (app != null) cachedAppRef.current = app;
|
|
120
|
+
}, [app]);
|
|
116
121
|
const appForRender = app ?? cachedAppRef.current;
|
|
117
122
|
|
|
118
123
|
const [loadedResource, setLoadedResource] = useState<LoadedResourceState>();
|
|
@@ -120,8 +125,7 @@ function InlineRenderer({
|
|
|
120
125
|
const host = useHostStore((state) => state.host);
|
|
121
126
|
const resourceUri = appForRender?.resourceUri;
|
|
122
127
|
const serverId = appForRender?.serverId;
|
|
123
|
-
const
|
|
124
|
-
serverIdRef.current = serverId;
|
|
128
|
+
const callerHandlers = opts.handlers;
|
|
125
129
|
useEffect(() => {
|
|
126
130
|
if (resourceUri == null) return;
|
|
127
131
|
let cancelled = false;
|
|
@@ -165,34 +169,37 @@ function InlineRenderer({
|
|
|
165
169
|
|
|
166
170
|
const bridgeHandlers = useMemo<McpAppBridgeHandlers>(
|
|
167
171
|
() => ({
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
...callerHandlers,
|
|
173
|
+
openLink: callerHandlers?.openLink ?? defaultOpenLink,
|
|
174
|
+
sendMessage:
|
|
175
|
+
callerHandlers?.sendMessage ??
|
|
176
|
+
((params) => {
|
|
177
|
+
const text = extractSendMessageText(params);
|
|
178
|
+
if (!text) return { ok: false, reason: "unrecognised params shape" };
|
|
179
|
+
aui.thread.append({ content: [{ type: "text", text }] });
|
|
180
|
+
return { ok: true };
|
|
181
|
+
}),
|
|
175
182
|
callTool: (params) =>
|
|
176
183
|
useHostStore.getState().host.callTool({
|
|
177
184
|
...params,
|
|
178
|
-
...(
|
|
185
|
+
...(serverId ? { serverId } : {}),
|
|
179
186
|
}),
|
|
180
187
|
readResource: (params) =>
|
|
181
188
|
useHostStore.getState().host.readResource({
|
|
182
189
|
...params,
|
|
183
|
-
...(
|
|
190
|
+
...(serverId ? { serverId } : {}),
|
|
184
191
|
}),
|
|
185
192
|
listResources: (params) => {
|
|
186
|
-
if (!
|
|
193
|
+
if (!serverId) {
|
|
187
194
|
return useHostStore.getState().host.listResources(params);
|
|
188
195
|
}
|
|
189
196
|
return useHostStore.getState().host.listResources({
|
|
190
197
|
...(isRecord(params) ? params : {}),
|
|
191
|
-
serverId
|
|
198
|
+
serverId,
|
|
192
199
|
});
|
|
193
200
|
},
|
|
194
201
|
}),
|
|
195
|
-
[aui, useHostStore],
|
|
202
|
+
[aui, callerHandlers, serverId, useHostStore],
|
|
196
203
|
);
|
|
197
204
|
|
|
198
205
|
const loadedResourceForApp =
|
|
@@ -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);
|