@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,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import type { AssistantClient } from "@assistant-ui/store";
|
|
5
|
+
import type { Unsubscribe } from "@assistant-ui/core";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where a snapshot comes from and what counts as a change to it. Declared once
|
|
9
|
+
* per consumer at module scope: the subscription effect keys on this object, so
|
|
10
|
+
* an inline source without an `isEqual` re-reads itself into a render loop.
|
|
11
|
+
*/
|
|
12
|
+
export type ModelContextSnapshotSource<T> = {
|
|
13
|
+
/** Seeds the snapshot when it starts disabled. */
|
|
14
|
+
readonly empty: T;
|
|
15
|
+
/** Projects the fields the consumer observes out of the live model context. */
|
|
16
|
+
read(aui: AssistantClient): T;
|
|
17
|
+
subscribe(aui: AssistantClient, onChange: () => void): Unsubscribe;
|
|
18
|
+
/**
|
|
19
|
+
* An equal pair keeps the previous reference. Omitted by a consumer that
|
|
20
|
+
* re-derives on every notification, where a caller republishing an unchanged
|
|
21
|
+
* tool set is how it asks to be re-run.
|
|
22
|
+
*/
|
|
23
|
+
isEqual?(previous: T, next: T): boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `getModelContext()` rebuilds its result on every call, so a projection of the
|
|
28
|
+
* fields a consumer observes is held in state and refreshed on notification
|
|
29
|
+
* rather than read during render. Keeping the previous reference for an equal
|
|
30
|
+
* projection is what stops a notification carrying no observable change from
|
|
31
|
+
* invalidating everything derived from it.
|
|
32
|
+
*/
|
|
33
|
+
export const useModelContextSnapshot = <T>(
|
|
34
|
+
aui: AssistantClient,
|
|
35
|
+
enabled: boolean,
|
|
36
|
+
source: ModelContextSnapshotSource<T>,
|
|
37
|
+
): T => {
|
|
38
|
+
const [snapshot, setSnapshot] = useState(() =>
|
|
39
|
+
enabled ? source.read(aui) : source.empty,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!enabled) return undefined;
|
|
44
|
+
const read = () => {
|
|
45
|
+
const next = source.read(aui);
|
|
46
|
+
setSnapshot((previous) =>
|
|
47
|
+
source.isEqual?.(previous, next) ? previous : next,
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
read();
|
|
51
|
+
return source.subscribe(aui, read);
|
|
52
|
+
}, [aui, enabled, source]);
|
|
53
|
+
|
|
54
|
+
return snapshot;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Shallow own-key comparison, for a projection whose values are records. */
|
|
58
|
+
export const shallowEqualRecords = (
|
|
59
|
+
previous: Readonly<Record<string, unknown>>,
|
|
60
|
+
next: Readonly<Record<string, unknown>>,
|
|
61
|
+
): boolean => {
|
|
62
|
+
const keys = Object.keys(previous);
|
|
63
|
+
if (keys.length !== Object.keys(next).length) return false;
|
|
64
|
+
return keys.every(
|
|
65
|
+
(key) => Object.hasOwn(next, key) && previous[key] === next[key],
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
Unstable_TriggerItem,
|
|
7
7
|
} from "@assistant-ui/core";
|
|
8
8
|
import type { Unstable_IconComponent } from "./useMentionAdapter";
|
|
9
|
+
import { matchesTriggerItemQuery } from "../primitives/composer/trigger/matchesTriggerItemQuery";
|
|
9
10
|
|
|
10
11
|
export type Unstable_SlashCommand = {
|
|
11
12
|
readonly id: string;
|
|
@@ -81,7 +82,7 @@ export function unstable_useSlashCommandAdapter(
|
|
|
81
82
|
categoryItems: () => [],
|
|
82
83
|
search: (query: string) => {
|
|
83
84
|
const lower = query.toLowerCase();
|
|
84
|
-
return items.filter((item) =>
|
|
85
|
+
return items.filter((item) => matchesTriggerItemQuery(item, lower));
|
|
85
86
|
},
|
|
86
87
|
}),
|
|
87
88
|
[items],
|
|
@@ -118,14 +119,6 @@ function toItem(cmd: Unstable_SlashCommand): Unstable_TriggerItem {
|
|
|
118
119
|
};
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
function matchesQuery(item: Unstable_TriggerItem, lower: string): boolean {
|
|
122
|
-
if (!lower) return true;
|
|
123
|
-
if (item.id.toLowerCase().includes(lower)) return true;
|
|
124
|
-
if (item.label.toLowerCase().includes(lower)) return true;
|
|
125
|
-
if (item.description?.toLowerCase().includes(lower)) return true;
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
122
|
function areTriggerItemsEqual(
|
|
130
123
|
previous: readonly Unstable_TriggerItem[] | undefined,
|
|
131
124
|
next: readonly Unstable_TriggerItem[],
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { resource } from "@assistant-ui/tap";
|
|
3
|
+
import type { Tool } from "assistant-stream";
|
|
4
|
+
import { toWebMcpTool } from "./convertTools";
|
|
5
|
+
import type { WebMcpHost } from "./webmcp-host";
|
|
6
|
+
|
|
7
|
+
export type WebMcpRegistrationProps = {
|
|
8
|
+
host: WebMcpHost;
|
|
9
|
+
name: string;
|
|
10
|
+
/** Re-registers when it changes; an execute-only edit reads through instead. */
|
|
11
|
+
signature: string;
|
|
12
|
+
tool: Tool<any, any>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// The explainer specifies NotAllowedError for a page whose tools permission is
|
|
16
|
+
// off, which a cross-origin iframe without allow="tools" reaches routinely. It
|
|
17
|
+
// documents no rejection for a name already taken, so that stays a hedge.
|
|
18
|
+
const notPermitted = (error: unknown) =>
|
|
19
|
+
(error as { name?: unknown } | null | undefined)?.name === "NotAllowedError";
|
|
20
|
+
|
|
21
|
+
const notPermittedMessage = (name: string) =>
|
|
22
|
+
`[assistant-ui] WebMCP registration for tool "${name}" was not permitted; the page's tools permission is disabled.`;
|
|
23
|
+
|
|
24
|
+
const useWebMcpRegistration = ({
|
|
25
|
+
host,
|
|
26
|
+
name,
|
|
27
|
+
signature,
|
|
28
|
+
tool,
|
|
29
|
+
}: WebMcpRegistrationProps): string | null => {
|
|
30
|
+
// A refused name is remembered for as long as the tool stays in the model
|
|
31
|
+
// context, so a permanent collision warns once rather than on every sync.
|
|
32
|
+
const [refused, setRefused] = useState(false);
|
|
33
|
+
const toolRef = useRef(tool);
|
|
34
|
+
// Commit phase, so an abandoned render cannot hand the host a tool the tree
|
|
35
|
+
// never committed.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
toolRef.current = tool;
|
|
38
|
+
}, [tool]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (refused) return undefined;
|
|
42
|
+
|
|
43
|
+
const lifecycle = new AbortController();
|
|
44
|
+
let live = true;
|
|
45
|
+
let dispose: (() => void) | undefined;
|
|
46
|
+
|
|
47
|
+
const refuse = (message: string, error: unknown) => {
|
|
48
|
+
if (!live) return;
|
|
49
|
+
live = false;
|
|
50
|
+
setRefused(true);
|
|
51
|
+
console.warn(message, error);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
dispose = host.registerTool(
|
|
56
|
+
toWebMcpTool(name, () => toolRef.current, lifecycle.signal),
|
|
57
|
+
(error) =>
|
|
58
|
+
refuse(
|
|
59
|
+
notPermitted(error)
|
|
60
|
+
? notPermittedMessage(name)
|
|
61
|
+
: `[assistant-ui] WebMCP registration for tool "${name}" failed (name may already be registered).`,
|
|
62
|
+
error,
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
refuse(
|
|
67
|
+
notPermitted(error)
|
|
68
|
+
? notPermittedMessage(name)
|
|
69
|
+
: `[assistant-ui] Skipping WebMCP registration for tool "${name}": registerTool failed (name may already be registered).`,
|
|
70
|
+
error,
|
|
71
|
+
);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
live = false;
|
|
77
|
+
lifecycle.abort();
|
|
78
|
+
try {
|
|
79
|
+
dispose();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.warn(
|
|
82
|
+
`[assistant-ui] Unregistering WebMCP tool "${name}" failed.`,
|
|
83
|
+
error,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}, [host, name, signature, refused]);
|
|
88
|
+
|
|
89
|
+
return refused ? null : name;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const WebMcpRegistrationResource = resource(useWebMcpRegistration);
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { expect, vi } from "vitest";
|
|
4
|
+
import { AuiConfig, AuiProvider, useAui } from "@assistant-ui/store";
|
|
5
|
+
import { ModelContext } from "@assistant-ui/core/store";
|
|
6
|
+
import type { ModelContextProvider } from "@assistant-ui/core";
|
|
7
|
+
import type { Tool } from "assistant-stream";
|
|
8
|
+
import type {
|
|
9
|
+
WebMcpHost,
|
|
10
|
+
WebMcpModelContext,
|
|
11
|
+
WebMcpToolDescriptor,
|
|
12
|
+
} from "../webmcp-host";
|
|
13
|
+
import {
|
|
14
|
+
unstable_useWebMcpProvider,
|
|
15
|
+
type Unstable_WebMcpProviderOptions,
|
|
16
|
+
type Unstable_WebMcpProviderResult,
|
|
17
|
+
} from "../useWebMcpProvider";
|
|
18
|
+
|
|
19
|
+
export type FakeWebMcpHost = WebMcpHost & {
|
|
20
|
+
registry: Map<string, WebMcpToolDescriptor>;
|
|
21
|
+
registerCalls: string[];
|
|
22
|
+
unregisterCalls: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const createFakeWebMcpHost = (): FakeWebMcpHost => {
|
|
26
|
+
const registry = new Map<string, WebMcpToolDescriptor>();
|
|
27
|
+
const registerCalls: string[] = [];
|
|
28
|
+
const unregisterCalls: string[] = [];
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
available: true,
|
|
32
|
+
registry,
|
|
33
|
+
registerCalls,
|
|
34
|
+
unregisterCalls,
|
|
35
|
+
registerTool: (def) => {
|
|
36
|
+
if (registry.has(def.name)) {
|
|
37
|
+
throw new Error(`Tool "${def.name}" is already registered`);
|
|
38
|
+
}
|
|
39
|
+
registry.set(def.name, def);
|
|
40
|
+
registerCalls.push(def.name);
|
|
41
|
+
let disposed = false;
|
|
42
|
+
return () => {
|
|
43
|
+
if (disposed) return;
|
|
44
|
+
disposed = true;
|
|
45
|
+
if (registry.get(def.name) === def) registry.delete(def.name);
|
|
46
|
+
unregisterCalls.push(def.name);
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Mirrors the spec's registerTool: the tool enters the map synchronously while
|
|
53
|
+
// the promise resolves on a later task, aborting removes it and rejects, and a
|
|
54
|
+
// name already in the map is refused.
|
|
55
|
+
export const createAsyncModelContext = (): Map<
|
|
56
|
+
string,
|
|
57
|
+
WebMcpToolDescriptor
|
|
58
|
+
> => {
|
|
59
|
+
const registry = new Map<string, WebMcpToolDescriptor>();
|
|
60
|
+
const context: WebMcpModelContext = {
|
|
61
|
+
registerTool: (tool, options) => {
|
|
62
|
+
if (registry.has(tool.name)) {
|
|
63
|
+
return Promise.reject(
|
|
64
|
+
new DOMException(
|
|
65
|
+
`Tool "${tool.name}" is already registered`,
|
|
66
|
+
"InvalidStateError",
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
registry.set(tool.name, tool);
|
|
71
|
+
return new Promise<void>((resolve, reject) => {
|
|
72
|
+
options?.signal?.addEventListener("abort", () => {
|
|
73
|
+
registry.delete(tool.name);
|
|
74
|
+
reject(options.signal?.reason);
|
|
75
|
+
});
|
|
76
|
+
setTimeout(() => resolve(), 0);
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
(document as { modelContext?: WebMcpModelContext }).modelContext = context;
|
|
81
|
+
return registry;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const frontendTool = (
|
|
85
|
+
overrides: Partial<Tool<any, any>> = {},
|
|
86
|
+
): Tool<any, any> =>
|
|
87
|
+
({
|
|
88
|
+
type: "frontend",
|
|
89
|
+
description: "search things",
|
|
90
|
+
parameters: { type: "object", properties: {} },
|
|
91
|
+
execute: async () => "found",
|
|
92
|
+
...overrides,
|
|
93
|
+
}) as Tool<any, any>;
|
|
94
|
+
|
|
95
|
+
export type FakeProvider = ModelContextProvider & {
|
|
96
|
+
setTools: (next: Record<string, Tool<any, any>>) => void;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const createProvider = (
|
|
100
|
+
initialTools: Record<string, Tool<any, any>>,
|
|
101
|
+
): FakeProvider => {
|
|
102
|
+
let tools = initialTools;
|
|
103
|
+
const listeners = new Set<() => void>();
|
|
104
|
+
return {
|
|
105
|
+
getModelContext: () => ({ tools }),
|
|
106
|
+
subscribe: (callback: () => void) => {
|
|
107
|
+
listeners.add(callback);
|
|
108
|
+
return () => {
|
|
109
|
+
listeners.delete(callback);
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
setTools: (next) => {
|
|
113
|
+
tools = next;
|
|
114
|
+
listeners.forEach((callback) => callback());
|
|
115
|
+
},
|
|
116
|
+
} as FakeProvider;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let latest: Unstable_WebMcpProviderResult;
|
|
120
|
+
|
|
121
|
+
export const providerResult = (): Unstable_WebMcpProviderResult => latest;
|
|
122
|
+
|
|
123
|
+
const Probe = ({ options }: { options: Unstable_WebMcpProviderOptions }) => {
|
|
124
|
+
latest = unstable_useWebMcpProvider(options);
|
|
125
|
+
return null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const Registrar = ({ provider }: { provider: ModelContextProvider }) => {
|
|
129
|
+
const aui = useAui();
|
|
130
|
+
useEffect(() => aui.modelContext.register(provider), [aui, provider]);
|
|
131
|
+
return null;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const Harness = ({
|
|
135
|
+
provider,
|
|
136
|
+
options,
|
|
137
|
+
}: {
|
|
138
|
+
provider: ModelContextProvider;
|
|
139
|
+
options: Unstable_WebMcpProviderOptions;
|
|
140
|
+
}) => (
|
|
141
|
+
<AuiProvider config={AuiConfig({ modelContext: ModelContext() } as never)}>
|
|
142
|
+
<Registrar provider={provider} />
|
|
143
|
+
<Probe options={options} />
|
|
144
|
+
</AuiProvider>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export const mountProvider = (
|
|
148
|
+
provider: ModelContextProvider,
|
|
149
|
+
options: Unstable_WebMcpProviderOptions = {},
|
|
150
|
+
wrap: (children: ReactNode) => ReactNode = (children) => children,
|
|
151
|
+
) => {
|
|
152
|
+
const view = render(
|
|
153
|
+
<>{wrap(<Harness provider={provider} options={options} />)}</>,
|
|
154
|
+
);
|
|
155
|
+
return {
|
|
156
|
+
view,
|
|
157
|
+
rerender: (next: Unstable_WebMcpProviderOptions) =>
|
|
158
|
+
view.rerender(
|
|
159
|
+
<>{wrap(<Harness provider={provider} options={next} />)}</>,
|
|
160
|
+
),
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const waitForNames = (names: string[]) =>
|
|
165
|
+
vi.waitFor(() => expect(providerResult().registeredToolNames).toEqual(names));
|
|
166
|
+
|
|
167
|
+
export const silenceWarnings = () =>
|
|
168
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|