@assistant-ui/react 0.15.22 → 0.15.23
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/LICENSE +1 -1
- package/dist/primitives/message/MessageIf.d.ts.map +1 -1
- package/dist/primitives/message/MessageIf.js +6 -3
- package/dist/primitives/message/MessageIf.js.map +1 -1
- package/dist/primitives/thread/ThreadRoot.d.ts +3 -2
- package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadRoot.js +47 -27
- package/dist/primitives/thread/ThreadRoot.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.js +1 -0
- package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.d.ts.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -0
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.d.ts.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -0
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js.map +1 -1
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +1 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -1
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js +27 -39
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.js +22 -13
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
- package/package.json +10 -7
- package/src/primitives/message/MessageIf.test.tsx +72 -0
- package/src/primitives/message/MessageIf.ts +8 -3
- package/src/primitives/thread/ThreadRoot.test.tsx +130 -0
- package/src/primitives/thread/ThreadRoot.tsx +30 -3
- package/src/primitives/threadListItem/ThreadListItemRoot.tsx +2 -0
- package/src/primitives/threadListItem/ThreadListKeyboardNav.test.tsx +65 -0
- package/src/primitives/threadListItemMore/ThreadListItemMoreContent.tsx +7 -0
- package/src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx +2 -0
- package/src/tests/RemoteThreadListRuntime.reloadMainThread.test.tsx +8 -13
- package/src/tests/remote-thread-list-test-helpers.ts +15 -0
- package/src/tests/threadListItemIsRunning.test.tsx +10 -7
- package/src/unstable/webmcp/WebMcpRegistrationResource.ts +9 -8
- package/src/unstable/webmcp/useWebMcpProvider.test.tsx +36 -2
- package/src/unstable/webmcp/useWebMcpProvider.ts +40 -13
|
@@ -10,6 +10,7 @@ export type WebMcpRegistrationProps = {
|
|
|
10
10
|
/** Re-registers when it changes; an execute-only edit reads through instead. */
|
|
11
11
|
signature: string;
|
|
12
12
|
tool: Tool<any, any>;
|
|
13
|
+
getCurrentTool: (name: string) => Tool<any, any> | undefined;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
// The explainer specifies NotAllowedError for a page whose tools permission is
|
|
@@ -26,16 +27,12 @@ const useWebMcpRegistration = ({
|
|
|
26
27
|
name,
|
|
27
28
|
signature,
|
|
28
29
|
tool,
|
|
30
|
+
getCurrentTool,
|
|
29
31
|
}: WebMcpRegistrationProps): string | null => {
|
|
30
32
|
// A refused name is remembered for as long as the tool stays in the model
|
|
31
33
|
// context, so a permanent collision warns once rather than on every sync.
|
|
32
34
|
const [refused, setRefused] = useState(false);
|
|
33
|
-
const
|
|
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]);
|
|
35
|
+
const fallbackToolRef = useRef(tool);
|
|
39
36
|
|
|
40
37
|
useEffect(() => {
|
|
41
38
|
if (refused) return undefined;
|
|
@@ -53,7 +50,11 @@ const useWebMcpRegistration = ({
|
|
|
53
50
|
|
|
54
51
|
try {
|
|
55
52
|
dispose = host.registerTool(
|
|
56
|
-
toWebMcpTool(
|
|
53
|
+
toWebMcpTool(
|
|
54
|
+
name,
|
|
55
|
+
() => getCurrentTool(name) ?? fallbackToolRef.current,
|
|
56
|
+
lifecycle.signal,
|
|
57
|
+
),
|
|
57
58
|
(error) =>
|
|
58
59
|
refuse(
|
|
59
60
|
notPermitted(error)
|
|
@@ -84,7 +85,7 @@ const useWebMcpRegistration = ({
|
|
|
84
85
|
);
|
|
85
86
|
}
|
|
86
87
|
};
|
|
87
|
-
}, [host, name, signature, refused]);
|
|
88
|
+
}, [getCurrentTool, host, name, signature, refused]);
|
|
88
89
|
|
|
89
90
|
return refused ? null : name;
|
|
90
91
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { StrictMode, type ReactNode } from "react";
|
|
4
|
-
import { cleanup } from "@testing-library/react";
|
|
3
|
+
import { StrictMode, type ReactNode, useLayoutEffect } from "react";
|
|
4
|
+
import { act, cleanup } from "@testing-library/react";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import type { Tool } from "assistant-stream";
|
|
7
7
|
import type {
|
|
@@ -195,6 +195,40 @@ describe("unstable_useWebMcpProvider", () => {
|
|
|
195
195
|
expect(host.unregisterCalls).toEqual([]);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
it("calls through to the committed implementation from a descendant layout effect", async () => {
|
|
199
|
+
const host = useHost(createFakeWebMcpHost());
|
|
200
|
+
const provider = createProvider({
|
|
201
|
+
search: frontendTool({ execute: async () => "first" }),
|
|
202
|
+
});
|
|
203
|
+
let invoke = false;
|
|
204
|
+
let execution: ReturnType<WebMcpToolDescriptor["execute"]> | undefined;
|
|
205
|
+
|
|
206
|
+
const InvokeOnCommit = ({ children }: { children: ReactNode }) => {
|
|
207
|
+
useLayoutEffect(() => {
|
|
208
|
+
if (invoke) execution = host.registry.get("search")?.execute({});
|
|
209
|
+
});
|
|
210
|
+
return children;
|
|
211
|
+
};
|
|
212
|
+
const { rerender } = mountProvider(provider, {}, (children) => (
|
|
213
|
+
<InvokeOnCommit>{children}</InvokeOnCommit>
|
|
214
|
+
));
|
|
215
|
+
await waitForNames(["search"]);
|
|
216
|
+
|
|
217
|
+
act(() => {
|
|
218
|
+
invoke = true;
|
|
219
|
+
provider.setTools({
|
|
220
|
+
search: frontendTool({ execute: async () => "second" }),
|
|
221
|
+
});
|
|
222
|
+
rerender({});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await expect(execution).resolves.toEqual({
|
|
226
|
+
content: [{ type: "text", text: "second" }],
|
|
227
|
+
});
|
|
228
|
+
expect(host.registerCalls).toEqual(["search"]);
|
|
229
|
+
expect(host.unregisterCalls).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
|
|
198
232
|
it("re-registers when the description or the schema changes", async () => {
|
|
199
233
|
const host = useHost(createFakeWebMcpHost());
|
|
200
234
|
const provider = createProvider({ search: frontendTool() });
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useInsertionEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore,
|
|
10
|
+
} from "react";
|
|
4
11
|
import { useResources, useTapRoot, withKey } from "@assistant-ui/tap";
|
|
5
|
-
import { useAui
|
|
12
|
+
import { useAui } from "@assistant-ui/store";
|
|
6
13
|
import type { Tool } from "assistant-stream";
|
|
7
14
|
import { getDefaultWebMcpHost, type WebMcpHost } from "./webmcp-host";
|
|
8
15
|
import { defaultWebMcpFilter, toWebMcpInputSchema } from "./convertTools";
|
|
@@ -73,20 +80,16 @@ const useStableNames = (names: readonly (string | null)[]) => {
|
|
|
73
80
|
};
|
|
74
81
|
|
|
75
82
|
const useWebMcpRegistry = ({
|
|
76
|
-
aui,
|
|
77
83
|
host,
|
|
78
84
|
filter,
|
|
85
|
+
tools,
|
|
86
|
+
getCurrentTool,
|
|
79
87
|
}: {
|
|
80
|
-
aui: AssistantClient;
|
|
81
88
|
host: WebMcpHost;
|
|
82
89
|
filter: (name: string, tool: Tool<any, any>) => boolean;
|
|
90
|
+
tools: Record<string, Tool<any, any>>;
|
|
91
|
+
getCurrentTool: (name: string) => Tool<any, any> | undefined;
|
|
83
92
|
}) => {
|
|
84
|
-
const tools = useModelContextSnapshot(
|
|
85
|
-
aui,
|
|
86
|
-
host.available,
|
|
87
|
-
modelContextToolSource,
|
|
88
|
-
);
|
|
89
|
-
|
|
90
93
|
const elements = [];
|
|
91
94
|
for (const [name, tool] of Object.entries(tools)) {
|
|
92
95
|
try {
|
|
@@ -95,8 +98,14 @@ const useWebMcpRegistry = ({
|
|
|
95
98
|
elements.push(
|
|
96
99
|
withKey(
|
|
97
100
|
name,
|
|
98
|
-
WebMcpRegistrationResource({
|
|
99
|
-
|
|
101
|
+
WebMcpRegistrationResource({
|
|
102
|
+
host,
|
|
103
|
+
name,
|
|
104
|
+
signature,
|
|
105
|
+
tool,
|
|
106
|
+
getCurrentTool,
|
|
107
|
+
}),
|
|
108
|
+
[host, name, signature, tool, getCurrentTool],
|
|
100
109
|
),
|
|
101
110
|
);
|
|
102
111
|
} catch (error) {
|
|
@@ -126,9 +135,27 @@ export const unstable_useWebMcpProvider = (
|
|
|
126
135
|
const aui = useAui();
|
|
127
136
|
const [host] = useState(getDefaultWebMcpHost);
|
|
128
137
|
const filter = options.filter ?? defaultWebMcpFilter;
|
|
138
|
+
const tools = useModelContextSnapshot(
|
|
139
|
+
aui,
|
|
140
|
+
host.available,
|
|
141
|
+
modelContextToolSource,
|
|
142
|
+
);
|
|
143
|
+
const currentToolsRef = useRef(tools);
|
|
144
|
+
useInsertionEffect(() => {
|
|
145
|
+
currentToolsRef.current = tools;
|
|
146
|
+
}, [tools]);
|
|
147
|
+
const getCurrentTool = useCallback(
|
|
148
|
+
(name: string) => currentToolsRef.current[name],
|
|
149
|
+
[],
|
|
150
|
+
);
|
|
129
151
|
|
|
130
152
|
const root = useTapRoot(function WebMcpProviderRoot() {
|
|
131
|
-
return useWebMcpRegistry({
|
|
153
|
+
return useWebMcpRegistry({
|
|
154
|
+
host,
|
|
155
|
+
filter,
|
|
156
|
+
tools,
|
|
157
|
+
getCurrentTool,
|
|
158
|
+
});
|
|
132
159
|
});
|
|
133
160
|
const registeredToolNames = useSyncExternalStore(
|
|
134
161
|
root.subscribe,
|