@assistant-ui/react 0.15.17 → 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 +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- package/dist/mcp-apps/McpAppRenderer.d.ts +30 -8
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +91 -47
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +41 -22
- 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 +2 -2
- 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/trigger/TriggerPopoverAction.js +46 -15
- package/dist/primitives/composer/trigger/TriggerPopoverAction.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/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/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/messagePart/MessagePartText.d.ts +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +3 -3
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +14 -5
- package/dist/sandbox-host/SandboxHost.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 +2 -8
- 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 +4 -4
- package/package.json +12 -12
- package/src/index.ts +13 -0
- package/src/mcp-apps/McpAppRenderer.test.tsx +253 -5
- package/src/mcp-apps/McpAppRenderer.tsx +147 -52
- package/src/mcp-apps/app-frame.test.tsx +248 -1
- package/src/mcp-apps/app-frame.tsx +51 -5
- package/src/mcp-apps/bridge.ts +7 -2
- package/src/mcp-apps/index.ts +5 -1
- package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +3 -10
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
- package/src/sandbox-host/SandboxHost.test.tsx +120 -2
- package/src/sandbox-host/SandboxHost.tsx +26 -6
- package/src/unstable/useComposerInputHistory.test.tsx +2 -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.ts +2 -9
- 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/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 -19
- 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/utils/invokeCallbackSafely.ts +0 -11
- package/src/utils/useToolArgsFieldStatus.ts +0 -18
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/unstable/webmcp/webmcp-host.ts
|
|
2
|
+
const isThenable = (value) => typeof value?.then === "function";
|
|
3
|
+
const resolveModelContext = () => {
|
|
4
|
+
const context = globalThis.document?.modelContext ?? globalThis.navigator?.modelContext;
|
|
5
|
+
if (!context) return void 0;
|
|
6
|
+
if (typeof context.registerTool === "function") return context;
|
|
7
|
+
console.warn("[assistant-ui] Ignoring a modelContext with no callable registerTool; WebMCP reports unsupported.");
|
|
8
|
+
};
|
|
9
|
+
const createHost = (context) => ({
|
|
10
|
+
available: true,
|
|
11
|
+
registerTool: (def, onError) => {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const handle = context.registerTool(def, { signal: controller.signal });
|
|
14
|
+
if (isThenable(handle)) handle.then(() => {}, (error) => onError?.(error));
|
|
15
|
+
return () => controller.abort();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const getDefaultWebMcpHost = () => {
|
|
19
|
+
const context = resolveModelContext();
|
|
20
|
+
if (!context) return {
|
|
21
|
+
available: false,
|
|
22
|
+
registerTool: () => () => {}
|
|
23
|
+
};
|
|
24
|
+
return createHost(context);
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { getDefaultWebMcpHost };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=webmcp-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webmcp-host.js","names":["WebMcpContent","type","text","data","mimeType","WebMcpCallToolResult","content","isError","WebMcpToolDescriptor","name","description","inputSchema","execute","args","context","signal","AbortSignal","Promise","WebMcpModelContext","registerTool","tool","options","WebMcpHost","available","def","onError","error","ModelContextHost","modelContext","isThenable","value","then","resolveModelContext","globalThis","document","navigator","undefined","console","warn","createHost","controller","AbortController","handle","abort","getDefaultWebMcpHost"],"sources":["../../../src/unstable/webmcp/webmcp-host.ts"],"sourcesContent":["export type WebMcpContent =\n | { type: \"text\"; text: string }\n | { type: \"image\"; data: string; mimeType: string };\n\nexport type WebMcpCallToolResult = {\n content: WebMcpContent[];\n isError?: boolean;\n};\n\nexport type WebMcpToolDescriptor = {\n name: string;\n description: string;\n inputSchema: unknown;\n execute: (\n args: unknown,\n context?: { signal?: AbortSignal },\n ) => Promise<WebMcpCallToolResult>;\n};\n\nexport type WebMcpModelContext = {\n /**\n * Aborting `options.signal` is what unregisters the tool. The explainer\n * defines no other removal method, and a name-keyed one could not be used\n * safely: a later registration may already hold the name.\n */\n registerTool(\n tool: WebMcpToolDescriptor,\n options?: { signal?: AbortSignal },\n ): Promise<void> | void;\n};\n\nexport type WebMcpHost = {\n available: boolean;\n registerTool(\n def: WebMcpToolDescriptor,\n onError?: (error: unknown) => void,\n ): () => void;\n};\n\ntype ModelContextHost = { modelContext?: WebMcpModelContext } | undefined;\n\nconst isThenable = (value: unknown): value is Promise<unknown> =>\n typeof (value as { then?: unknown } | null | undefined)?.then === \"function\";\n\n// The explainer puts modelContext on document; navigator is tolerated for\n// older drafts and extensions that still inject it there.\nconst resolveModelContext = (): WebMcpModelContext | undefined => {\n const context =\n (globalThis.document as ModelContextHost)?.modelContext ??\n (globalThis.navigator as ModelContextHost)?.modelContext;\n if (!context) return undefined;\n if (typeof context.registerTool === \"function\") return context;\n console.warn(\n \"[assistant-ui] Ignoring a modelContext with no callable registerTool; WebMCP reports unsupported.\",\n );\n return undefined;\n};\n\nconst createHost = (context: WebMcpModelContext): WebMcpHost => ({\n available: true,\n registerTool: (def, onError) => {\n const controller = new AbortController();\n const handle = context.registerTool(def, { signal: controller.signal });\n if (isThenable(handle)) {\n handle.then(\n () => {},\n (error) => onError?.(error),\n );\n }\n return () => controller.abort();\n },\n});\n\nexport const getDefaultWebMcpHost = (): WebMcpHost => {\n const context = resolveModelContext();\n if (!context) {\n return {\n available: false,\n registerTool: () => () => {},\n };\n }\n return createHost(context);\n};\n"],"mappings":";AAyCA,MAAM6B,cAAcC,UAClB,OAAQA,OAAiDC,SAAS;AAIpE,MAAMC,4BAA4D;CAChE,MAAMlB,UACHmB,WAAWC,UAA+BN,gBAC1CK,WAAWE,WAAgCP;CAC9C,IAAI,CAACd,SAAS,OAAOsB,KAAAA;CACrB,IAAI,OAAOtB,QAAQK,iBAAiB,YAAY,OAAOL;CACvDuB,QAAQC,KACN,mGACF;AAEF;AAEA,MAAMC,cAAczB,aAA6C;CAC/DS,WAAW;CACXJ,eAAeK,KAAKC,YAAY;EAC9B,MAAMe,aAAa,IAAIC,gBAAgB;EACvC,MAAMC,SAAS5B,QAAQK,aAAaK,KAAK,EAAET,QAAQyB,WAAWzB,OAAO,CAAC;EACtE,IAAIc,WAAWa,MAAM,GACnBA,OAAOX,WACC,CAAC,IACNL,UAAUD,UAAUC,KAAK,CAC5B;EAEF,aAAac,WAAWG,MAAM;CAChC;AACF;AAEA,MAAaC,6BAAyC;CACpD,MAAM9B,UAAUkB,oBAAoB;CACpC,IAAI,CAAClB,SACH,OAAO;EACLS,WAAW;EACXJ,0BAA0B,CAAC;CAC7B;CAEF,OAAOoB,WAAWzB,OAAO;AAC3B"}
|
|
@@ -93,16 +93,16 @@ declare const Primitive: {
|
|
|
93
93
|
}, "ref"> & {
|
|
94
94
|
render?: ReactElement | undefined;
|
|
95
95
|
} & RefAttributes<HTMLSpanElement>>;
|
|
96
|
-
|
|
96
|
+
ul: ForwardRefExoticComponent<Omit<import("react").ClassAttributes<HTMLUListElement> & import("react").HTMLAttributes<HTMLUListElement> & {
|
|
97
97
|
asChild?: boolean;
|
|
98
98
|
}, "ref"> & {
|
|
99
99
|
render?: ReactElement | undefined;
|
|
100
|
-
} & RefAttributes<
|
|
101
|
-
|
|
100
|
+
} & RefAttributes<HTMLUListElement>>;
|
|
101
|
+
svg: ForwardRefExoticComponent<Omit<import("react").SVGProps<SVGSVGElement> & {
|
|
102
102
|
asChild?: boolean;
|
|
103
103
|
}, "ref"> & {
|
|
104
104
|
render?: ReactElement | undefined;
|
|
105
|
-
} & RefAttributes<
|
|
105
|
+
} & RefAttributes<SVGSVGElement>>;
|
|
106
106
|
};
|
|
107
107
|
//#endregion
|
|
108
108
|
export { Primitive, type PrimitiveProps, type WithRenderPropProps, withRenderProp };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.18",
|
|
4
4
|
"description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -55,15 +55,15 @@
|
|
|
55
55
|
],
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@assistant-ui/core": "^0.3.
|
|
59
|
-
"@assistant-ui/store": "^0.3.
|
|
60
|
-
"@assistant-ui/tap": "^0.9.
|
|
61
|
-
"assistant-cloud": "^0.1.
|
|
62
|
-
"assistant-stream": "^0.3.
|
|
58
|
+
"@assistant-ui/core": "^0.3.17",
|
|
59
|
+
"@assistant-ui/store": "^0.3.12",
|
|
60
|
+
"@assistant-ui/tap": "^0.9.16",
|
|
61
|
+
"assistant-cloud": "^0.1.43",
|
|
62
|
+
"assistant-stream": "^0.3.41",
|
|
63
63
|
"radix-ui": "^1.6.7",
|
|
64
64
|
"react-textarea-autosize": "^8.5.9",
|
|
65
|
-
"safe-content-frame": "^0.0.
|
|
66
|
-
"zod": "^4.4
|
|
65
|
+
"safe-content-frame": "^0.0.29",
|
|
66
|
+
"zod": "^4.5.4",
|
|
67
67
|
"zustand": "^5.0.15"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
@@ -81,11 +81,11 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@assistant-ui/vite": "0.0.
|
|
85
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
86
|
-
"@testing-library/react": "^16.3.
|
|
84
|
+
"@assistant-ui/vite": "0.0.16",
|
|
85
|
+
"@assistant-ui/x-buildutils": "0.0.25",
|
|
86
|
+
"@testing-library/react": "^16.3.3",
|
|
87
87
|
"@types/json-schema": "^7.0.15",
|
|
88
|
-
"@types/node": "^26.
|
|
88
|
+
"@types/node": "^26.4.0",
|
|
89
89
|
"@types/react": "^19.2.18",
|
|
90
90
|
"@types/react-dom": "^19.2.5",
|
|
91
91
|
"jsdom": "^30.0.1",
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type {
|
|
|
43
43
|
ThreadListItemState,
|
|
44
44
|
} from "@assistant-ui/core";
|
|
45
45
|
|
|
46
|
+
export { toolApprovalAcceptsText } from "@assistant-ui/core";
|
|
47
|
+
|
|
46
48
|
export { useCloudThreadListRuntime } from "@assistant-ui/core/react";
|
|
47
49
|
export { AssistantCloud } from "assistant-cloud";
|
|
48
50
|
|
|
@@ -360,6 +362,7 @@ export type {
|
|
|
360
362
|
GenerativeUISpec,
|
|
361
363
|
Unstable_AudioMessagePart,
|
|
362
364
|
RespondToToolApprovalOptions,
|
|
365
|
+
ToolApprovalDisplay,
|
|
363
366
|
ToolApprovalOption,
|
|
364
367
|
ToolApprovalOptionKind,
|
|
365
368
|
ToolApprovalResponse,
|
|
@@ -515,6 +518,7 @@ export {
|
|
|
515
518
|
getMcpAppFromToolPart,
|
|
516
519
|
} from "./mcp-apps";
|
|
517
520
|
export type {
|
|
521
|
+
McpAppPartOptions,
|
|
518
522
|
McpAppRendererOptions,
|
|
519
523
|
McpAppMetadata,
|
|
520
524
|
McpAppResource,
|
|
@@ -531,3 +535,12 @@ export type {
|
|
|
531
535
|
ToolCallMessagePartMcpMetadata,
|
|
532
536
|
} from "./mcp-apps";
|
|
533
537
|
export type { McpAppResourceOutput } from "@assistant-ui/core/react";
|
|
538
|
+
export type { ShimLoadError, ShimLoadErrorCode } from "safe-content-frame";
|
|
539
|
+
|
|
540
|
+
// Unstable - WebMCP provider (exposes frontend tools to a WebMCP-capable browser)
|
|
541
|
+
export {
|
|
542
|
+
unstable_useWebMcpProvider,
|
|
543
|
+
type Unstable_WebMcpProviderOptions,
|
|
544
|
+
type Unstable_WebMcpProviderResult,
|
|
545
|
+
} from "./unstable/webmcp/useWebMcpProvider";
|
|
546
|
+
export { defaultWebMcpFilter as unstable_defaultWebMcpFilter } from "./unstable/webmcp/convertTools";
|
|
@@ -9,16 +9,21 @@ import type {
|
|
|
9
9
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
10
|
import type {
|
|
11
11
|
McpAppBridgeHandlers,
|
|
12
|
+
McpAppFrameProps,
|
|
13
|
+
McpAppHostContext,
|
|
12
14
|
McpAppsHost,
|
|
13
15
|
McpAppsRemoteHostOptions,
|
|
14
16
|
} from "./types";
|
|
15
17
|
|
|
16
|
-
const { framePropsMock } = vi.hoisted(() => ({
|
|
18
|
+
const { appendMock, framePropsMock } = vi.hoisted(() => ({
|
|
19
|
+
appendMock: vi.fn(),
|
|
20
|
+
framePropsMock: vi.fn(),
|
|
21
|
+
}));
|
|
17
22
|
|
|
18
23
|
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
19
24
|
...(await importOriginal()),
|
|
20
25
|
useAui: () => ({
|
|
21
|
-
thread:
|
|
26
|
+
thread: { append: appendMock },
|
|
22
27
|
}),
|
|
23
28
|
}));
|
|
24
29
|
|
|
@@ -43,9 +48,12 @@ const createDeferred = <T,>() => {
|
|
|
43
48
|
return { promise, resolve };
|
|
44
49
|
};
|
|
45
50
|
|
|
46
|
-
const createPart = (
|
|
51
|
+
const createPart = (
|
|
52
|
+
serverId?: string,
|
|
53
|
+
toolCallId = "call-1",
|
|
54
|
+
): ToolCallMessagePartProps => ({
|
|
47
55
|
type: "tool-call",
|
|
48
|
-
toolCallId
|
|
56
|
+
toolCallId,
|
|
49
57
|
toolName: "search",
|
|
50
58
|
args: {},
|
|
51
59
|
argsText: "{}",
|
|
@@ -105,6 +113,60 @@ function MemoizedHarness({ host }: { host: McpAppsHost }) {
|
|
|
105
113
|
return <MemoizedPart Renderer={renderer.render} />;
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
const MemoizedPartById = memo(function MemoizedPartById({
|
|
117
|
+
Renderer,
|
|
118
|
+
toolCallId,
|
|
119
|
+
}: {
|
|
120
|
+
Renderer: ToolCallMessagePartComponent;
|
|
121
|
+
toolCallId: string;
|
|
122
|
+
}) {
|
|
123
|
+
return <Renderer {...createPart(undefined, toolCallId)} />;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function OptionsHarness({
|
|
127
|
+
host,
|
|
128
|
+
hostContext,
|
|
129
|
+
handlers,
|
|
130
|
+
forPart,
|
|
131
|
+
toolCallIds = ["call-1"],
|
|
132
|
+
}: {
|
|
133
|
+
host: McpAppsHost;
|
|
134
|
+
hostContext?: McpAppHostContext;
|
|
135
|
+
handlers?: McpAppRendererOptions["handlers"];
|
|
136
|
+
forPart?: McpAppRendererOptions["forPart"];
|
|
137
|
+
toolCallIds?: string[];
|
|
138
|
+
}) {
|
|
139
|
+
const renderer = useResource(
|
|
140
|
+
McpAppRenderer({
|
|
141
|
+
host: Host({ host }),
|
|
142
|
+
...(hostContext === undefined ? {} : { hostContext }),
|
|
143
|
+
...(handlers === undefined ? {} : { handlers }),
|
|
144
|
+
...(forPart === undefined ? {} : { forPart }),
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
return toolCallIds.map((toolCallId) => (
|
|
148
|
+
<MemoizedPartById
|
|
149
|
+
key={toolCallId}
|
|
150
|
+
Renderer={renderer.render}
|
|
151
|
+
toolCallId={toolCallId}
|
|
152
|
+
/>
|
|
153
|
+
));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const framePropsCalls = () =>
|
|
157
|
+
framePropsMock.mock.calls.map(([props]) => props as McpAppFrameProps);
|
|
158
|
+
|
|
159
|
+
const loadingHost = (): McpAppsHost => ({
|
|
160
|
+
loadResource: vi.fn(async ({ uri }) => ({
|
|
161
|
+
uri,
|
|
162
|
+
mimeType: "text/html;profile=mcp-app" as const,
|
|
163
|
+
html: "",
|
|
164
|
+
})),
|
|
165
|
+
callTool: vi.fn(),
|
|
166
|
+
readResource: vi.fn(),
|
|
167
|
+
listResources: vi.fn(),
|
|
168
|
+
});
|
|
169
|
+
|
|
108
170
|
function RemoteHarness({
|
|
109
171
|
url,
|
|
110
172
|
headers,
|
|
@@ -127,9 +189,146 @@ function RemoteHarness({
|
|
|
127
189
|
|
|
128
190
|
describe("McpAppRenderer", () => {
|
|
129
191
|
beforeEach(() => {
|
|
192
|
+
appendMock.mockReset();
|
|
130
193
|
framePropsMock.mockReset();
|
|
131
194
|
});
|
|
132
195
|
|
|
196
|
+
it("loads newly mounted parts through the committed host", async () => {
|
|
197
|
+
const hostA = loadingHost();
|
|
198
|
+
const hostB = loadingHost();
|
|
199
|
+
const view = render(
|
|
200
|
+
<OptionsHarness host={hostA} toolCallIds={["call-1"]} />,
|
|
201
|
+
);
|
|
202
|
+
await waitFor(() => expect(hostA.loadResource).toHaveBeenCalledOnce());
|
|
203
|
+
vi.mocked(hostA.loadResource).mockClear();
|
|
204
|
+
|
|
205
|
+
view.rerender(
|
|
206
|
+
<OptionsHarness host={hostB} toolCallIds={["call-1", "call-2"]} />,
|
|
207
|
+
);
|
|
208
|
+
await waitFor(() => expect(hostB.loadResource).toHaveBeenCalled());
|
|
209
|
+
|
|
210
|
+
expect(hostA.loadResource).not.toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("merges forPart handlers over the thread-wide handlers", async () => {
|
|
214
|
+
const onInitialized = vi.fn();
|
|
215
|
+
render(
|
|
216
|
+
<OptionsHarness
|
|
217
|
+
host={loadingHost()}
|
|
218
|
+
handlers={{ onInitialized }}
|
|
219
|
+
forPart={(part) => ({
|
|
220
|
+
handlers: {
|
|
221
|
+
requestDisplayMode: ({ mode }) => {
|
|
222
|
+
void part;
|
|
223
|
+
return { mode };
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
})}
|
|
227
|
+
/>,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(0));
|
|
231
|
+
const handlers = framePropsCalls().at(-1)?.handlers;
|
|
232
|
+
expect(handlers?.requestDisplayMode).toBeDefined();
|
|
233
|
+
handlers?.onInitialized?.();
|
|
234
|
+
expect(onInitialized).toHaveBeenCalledOnce();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("leaves mounted parts alone when renderer options are unchanged", async () => {
|
|
238
|
+
const host = loadingHost();
|
|
239
|
+
const hostContext: McpAppHostContext = { displayMode: "inline" };
|
|
240
|
+
const view = () => <OptionsHarness host={host} hostContext={hostContext} />;
|
|
241
|
+
|
|
242
|
+
const rendered = render(view());
|
|
243
|
+
await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(0));
|
|
244
|
+
const rendersBefore = framePropsCalls().length;
|
|
245
|
+
|
|
246
|
+
rendered.rerender(view());
|
|
247
|
+
expect(framePropsCalls().length).toBe(rendersBefore);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("delivers a changed renderer host context to mounted parts", async () => {
|
|
251
|
+
const host = loadingHost();
|
|
252
|
+
const view = render(
|
|
253
|
+
<OptionsHarness host={host} hostContext={{ displayMode: "inline" }} />,
|
|
254
|
+
);
|
|
255
|
+
await waitFor(() =>
|
|
256
|
+
expect(framePropsCalls().at(-1)?.hostContext).toEqual({
|
|
257
|
+
displayMode: "inline",
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
view.rerender(
|
|
262
|
+
<OptionsHarness
|
|
263
|
+
host={host}
|
|
264
|
+
hostContext={{ displayMode: "fullscreen" }}
|
|
265
|
+
/>,
|
|
266
|
+
);
|
|
267
|
+
await waitFor(() =>
|
|
268
|
+
expect(framePropsCalls().at(-1)?.hostContext).toEqual({
|
|
269
|
+
displayMode: "fullscreen",
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("resolves host context per part through forPart", async () => {
|
|
275
|
+
const modes: Record<string, string> = {
|
|
276
|
+
"call-1": "inline",
|
|
277
|
+
"call-2": "fullscreen",
|
|
278
|
+
};
|
|
279
|
+
render(
|
|
280
|
+
<OptionsHarness
|
|
281
|
+
host={loadingHost()}
|
|
282
|
+
hostContext={{ theme: "light" }}
|
|
283
|
+
toolCallIds={["call-1", "call-2"]}
|
|
284
|
+
forPart={(part) => ({
|
|
285
|
+
hostContext: {
|
|
286
|
+
theme: "light",
|
|
287
|
+
displayMode: modes[part.toolCallId] as "inline" | "fullscreen",
|
|
288
|
+
},
|
|
289
|
+
})}
|
|
290
|
+
/>,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(1));
|
|
294
|
+
const delivered = framePropsCalls().map((props) => props.hostContext);
|
|
295
|
+
expect(delivered).toContainEqual({
|
|
296
|
+
theme: "light",
|
|
297
|
+
displayMode: "inline",
|
|
298
|
+
});
|
|
299
|
+
expect(delivered).toContainEqual({
|
|
300
|
+
theme: "light",
|
|
301
|
+
displayMode: "fullscreen",
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("gives forPart handlers the part that owns the frame", async () => {
|
|
306
|
+
const promoted: string[] = [];
|
|
307
|
+
render(
|
|
308
|
+
<OptionsHarness
|
|
309
|
+
host={loadingHost()}
|
|
310
|
+
toolCallIds={["call-1", "call-2"]}
|
|
311
|
+
forPart={(part) => ({
|
|
312
|
+
hostContext: { toolCallId: part.toolCallId },
|
|
313
|
+
handlers: {
|
|
314
|
+
requestDisplayMode: ({ mode }) => {
|
|
315
|
+
promoted.push(`${part.toolCallId}:${mode}`);
|
|
316
|
+
return { mode };
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
})}
|
|
320
|
+
/>,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(1));
|
|
324
|
+
const second = framePropsCalls().find(
|
|
325
|
+
(props) => props.hostContext?.["toolCallId"] === "call-2",
|
|
326
|
+
);
|
|
327
|
+
await second?.handlers?.requestDisplayMode?.({ mode: "fullscreen" });
|
|
328
|
+
|
|
329
|
+
expect(promoted).toEqual(["call-2:fullscreen"]);
|
|
330
|
+
});
|
|
331
|
+
|
|
133
332
|
it("injects serverId into loadResource and reloads when it changes", async () => {
|
|
134
333
|
const loadResource = vi.fn(async ({ uri }: { uri: string }) => ({
|
|
135
334
|
uri,
|
|
@@ -250,7 +449,7 @@ describe("McpAppRenderer", () => {
|
|
|
250
449
|
framePropsMock.mockClear();
|
|
251
450
|
view.rerender(<MemoizedHarness host={nextHost} />);
|
|
252
451
|
|
|
253
|
-
expect(nextHost.loadResource).toHaveBeenCalledTimes(1);
|
|
452
|
+
await waitFor(() => expect(nextHost.loadResource).toHaveBeenCalledTimes(1));
|
|
254
453
|
expect(framePropsMock).not.toHaveBeenCalled();
|
|
255
454
|
|
|
256
455
|
nextResource.resolve({
|
|
@@ -473,6 +672,55 @@ describe("McpAppRenderer", () => {
|
|
|
473
672
|
expect(listResources).toHaveBeenCalledWith(params);
|
|
474
673
|
});
|
|
475
674
|
|
|
675
|
+
it("appends MCP Apps message content in the default sendMessage handler", async () => {
|
|
676
|
+
render(<Harness host={loadingHost()} />);
|
|
677
|
+
await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
|
|
678
|
+
const handlers = framePropsMock.mock.lastCall?.[0]
|
|
679
|
+
.handlers as McpAppBridgeHandlers;
|
|
680
|
+
|
|
681
|
+
expect(
|
|
682
|
+
handlers.sendMessage?.({
|
|
683
|
+
role: "user",
|
|
684
|
+
content: [
|
|
685
|
+
{ type: "text", text: "Open Grade 5 Math" },
|
|
686
|
+
{ type: "image", data: "image", mimeType: "image/png" },
|
|
687
|
+
{ type: "text", text: "Period 4" },
|
|
688
|
+
],
|
|
689
|
+
}),
|
|
690
|
+
).toEqual({ ok: true });
|
|
691
|
+
expect(appendMock).toHaveBeenCalledWith({
|
|
692
|
+
content: [
|
|
693
|
+
{ type: "text", text: "Open Grade 5 Math" },
|
|
694
|
+
{ type: "text", text: "Period 4" },
|
|
695
|
+
],
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
expect(handlers.sendMessage?.({ text: "typed instead" })).toEqual({
|
|
699
|
+
ok: true,
|
|
700
|
+
});
|
|
701
|
+
expect(appendMock).toHaveBeenLastCalledWith({
|
|
702
|
+
content: [{ type: "text", text: "typed instead" }],
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
expect(
|
|
706
|
+
handlers.sendMessage?.({
|
|
707
|
+
role: "user",
|
|
708
|
+
content: [{ type: "image", data: "image", mimeType: "image/png" }],
|
|
709
|
+
}),
|
|
710
|
+
).toEqual({
|
|
711
|
+
isError: true,
|
|
712
|
+
ok: false,
|
|
713
|
+
reason: "no text content to append",
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
expect(handlers.sendMessage?.({ role: "user" })).toEqual({
|
|
717
|
+
isError: true,
|
|
718
|
+
ok: false,
|
|
719
|
+
reason: "unrecognised params shape",
|
|
720
|
+
});
|
|
721
|
+
expect(appendMock).toHaveBeenCalledTimes(2);
|
|
722
|
+
});
|
|
723
|
+
|
|
476
724
|
it("uses caller UI handlers and keeps data-plane handlers on the host", async () => {
|
|
477
725
|
const host: McpAppsHost = {
|
|
478
726
|
loadResource: vi.fn(async ({ uri }) => ({
|