@agent-native/core 0.72.2 → 0.72.4
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +16 -0
- package/corpus/core/package.json +6 -6
- package/corpus/core/src/a2a/client.ts +7 -3
- package/corpus/core/src/client/AgentPanel.tsx +12 -1
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +263 -13
- package/corpus/core/src/client/api-path.ts +28 -0
- package/corpus/core/src/client/embed-auth.ts +26 -0
- package/corpus/core/src/client/mcp-app-host.ts +6 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
- package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +16 -4
- package/corpus/core/src/client/use-chat-threads.ts +86 -5
- package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
- package/corpus/core/src/embedding/agent.ts +6 -1
- package/corpus/core/src/embedding/bridge.ts +34 -1
- package/corpus/core/src/mcp/build-server.ts +38 -10
- package/corpus/core/src/mcp/embed-app.ts +120 -45
- package/corpus/core/src/mcp/index.ts +5 -0
- package/corpus/core/src/mcp/org-directory.ts +70 -11
- package/corpus/core/src/mcp/server.ts +14 -0
- package/corpus/core/src/mcp-client/config.ts +12 -0
- package/corpus/core/src/mcp-client/index.ts +2 -0
- package/corpus/core/src/mcp-client/manager.ts +268 -9
- package/corpus/core/src/mcp-client/remote-store.ts +154 -0
- package/corpus/core/src/mcp-client/routes.ts +2 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
- package/corpus/core/src/server/index.ts +1 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
- package/corpus/core/src/vite/client.ts +53 -0
- package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
- package/corpus/templates/assets/AGENTS.md +7 -5
- package/corpus/templates/assets/actions/generate-asset.ts +161 -0
- package/corpus/templates/assets/actions/list-assets.ts +4 -2
- package/corpus/templates/assets/actions/navigate.ts +2 -1
- package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -2
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +27 -4
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +35 -5
- package/corpus/templates/assets/app/routes/_index.tsx +12 -0
- package/corpus/templates/assets/app/routes/chat.$threadId.tsx +1 -0
- package/corpus/templates/assets/app/routes/library.tsx +243 -40
- package/corpus/templates/chat/actions/navigate.ts +3 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +7 -2
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +27 -4
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +38 -6
- package/corpus/templates/chat/app/routes/_index.tsx +13 -0
- package/corpus/templates/chat/app/routes/chat.$threadId.tsx +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
- package/corpus/templates/design/AGENTS.md +10 -5
- package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
- package/corpus/templates/design/actions/insert-asset.ts +219 -0
- package/corpus/templates/design/actions/view-screen.ts +3 -1
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
- package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
- package/corpus/templates/dispatch/app/routes/chat.$threadId.tsx +26 -0
- package/dist/a2a/client.d.ts +1 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +5 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +6 -2
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +4 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts +22 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +215 -13
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/api-path.d.ts.map +1 -1
- package/dist/client/api-path.js +27 -0
- package/dist/client/api-path.js.map +1 -1
- package/dist/client/embed-auth.d.ts.map +1 -1
- package/dist/client/embed-auth.js +27 -0
- package/dist/client/embed-auth.js.map +1 -1
- package/dist/client/mcp-app-host.d.ts.map +1 -1
- package/dist/client/mcp-app-host.js +5 -0
- package/dist/client/mcp-app-host.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
- package/dist/client/resources/use-mcp-servers.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +3 -1
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +7 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +9 -5
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts +6 -0
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +79 -5
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/demo/fetch-interceptor.js +1 -1
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/embedding/agent.d.ts +1 -1
- package/dist/embedding/agent.d.ts.map +1 -1
- package/dist/embedding/agent.js +1 -1
- package/dist/embedding/agent.js.map +1 -1
- package/dist/embedding/bridge.d.ts.map +1 -1
- package/dist/embedding/bridge.js +27 -1
- package/dist/embedding/bridge.js.map +1 -1
- package/dist/extensions/schema.d.ts +2 -2
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +32 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +120 -45
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/org-directory.d.ts +1 -0
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +59 -11
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +9 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp-client/config.d.ts +12 -0
- package/dist/mcp-client/config.d.ts.map +1 -1
- package/dist/mcp-client/config.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/manager.d.ts +2 -0
- package/dist/mcp-client/manager.d.ts.map +1 -1
- package/dist/mcp-client/manager.js +201 -9
- package/dist/mcp-client/manager.js.map +1 -1
- package/dist/mcp-client/remote-store.d.ts +27 -0
- package/dist/mcp-client/remote-store.d.ts.map +1 -1
- package/dist/mcp-client/remote-store.js +105 -0
- package/dist/mcp-client/remote-store.js.map +1 -1
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +1 -0
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
- package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
- package/dist/provider-api/corpus-jobs.d.ts +8 -8
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +8 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +3 -1
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +40 -3
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
- package/dist/sharing/actions/share-resource.d.ts +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +44 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
2
9
|
import {
|
|
3
10
|
AppBridge,
|
|
4
11
|
PostMessageTransport,
|
|
@@ -8,8 +15,14 @@ import {
|
|
|
8
15
|
} from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
9
16
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
10
17
|
import { IconAlertTriangle, IconLoader2 } from "@tabler/icons-react";
|
|
18
|
+
import {
|
|
19
|
+
AGENT_NATIVE_EMBED_MESSAGE_TYPES,
|
|
20
|
+
AGENT_NATIVE_EMBED_PROTOCOL,
|
|
21
|
+
AGENT_NATIVE_EMBED_VERSION,
|
|
22
|
+
} from "../../embedding/protocol.js";
|
|
11
23
|
import type { AgentMcpAppPayload } from "../../mcp-client/app-result.js";
|
|
12
24
|
import { agentNativePath } from "../api-path.js";
|
|
25
|
+
import { sendToAgentChat, type AgentChatRequestMode } from "../agent-chat.js";
|
|
13
26
|
import { cn } from "../utils.js";
|
|
14
27
|
|
|
15
28
|
export const DEFAULT_MCP_APP_IFRAME_HEIGHT = 650;
|
|
@@ -17,6 +30,8 @@ export const MCP_APP_INITIALIZE_TIMEOUT_MS = 8000;
|
|
|
17
30
|
const MIN_IFRAME_HEIGHT = 220;
|
|
18
31
|
const VIEWPORT_MARGIN = 16;
|
|
19
32
|
const SANDBOX_FLAGS = "allow-scripts allow-forms allow-popups";
|
|
33
|
+
const useBrowserLayoutEffect =
|
|
34
|
+
typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
20
35
|
|
|
21
36
|
export interface McpAppRendererProps {
|
|
22
37
|
app: AgentMcpAppPayload;
|
|
@@ -29,10 +44,24 @@ type ResourceUiMeta = {
|
|
|
29
44
|
prefersBorder?: boolean;
|
|
30
45
|
};
|
|
31
46
|
|
|
47
|
+
type McpContentPart = {
|
|
48
|
+
type?: unknown;
|
|
49
|
+
text?: unknown;
|
|
50
|
+
data?: unknown;
|
|
51
|
+
mimeType?: unknown;
|
|
52
|
+
url?: unknown;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type McpAppModelContext = {
|
|
56
|
+
content?: unknown;
|
|
57
|
+
structuredContent?: unknown;
|
|
58
|
+
};
|
|
59
|
+
|
|
32
60
|
export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
33
61
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
34
62
|
const desiredHeightRef = useRef(DEFAULT_MCP_APP_IFRAME_HEIGHT);
|
|
35
|
-
const
|
|
63
|
+
const modelContextRef = useRef<McpAppModelContext | null>(null);
|
|
64
|
+
const readyRef = useRef(false);
|
|
36
65
|
const [height, setHeight] = useState(DEFAULT_MCP_APP_IFRAME_HEIGHT);
|
|
37
66
|
const [error, setError] = useState<string | null>(null);
|
|
38
67
|
const [ready, setReady] = useState(false);
|
|
@@ -49,6 +78,23 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
49
78
|
);
|
|
50
79
|
const externalOpenUrl = useMemo(() => openUrlFromMcpApp(app), [app]);
|
|
51
80
|
|
|
81
|
+
// Keep the latest payload/permissions/csp reachable from the bridge effect
|
|
82
|
+
// without making them effect dependencies. The embedded resource identity is
|
|
83
|
+
// fully captured by `srcDoc`. The bridge effect must NOT re-run when a benign
|
|
84
|
+
// parent re-render hands us a new `app` object reference with identical
|
|
85
|
+
// content (common during chat streaming/polling): re-running tears down a
|
|
86
|
+
// live, already-initialized MCP App (teardownResource) and re-arms the
|
|
87
|
+
// initialize watchdog against a fresh host AppBridge that the embed shell
|
|
88
|
+
// will never re-handshake (its connect promise is memoized), surfacing a
|
|
89
|
+
// false "MCP App did not finish initializing." error after the app is
|
|
90
|
+
// visibly working.
|
|
91
|
+
const appRef = useRef(app);
|
|
92
|
+
const supportedPermissionsRef = useRef(supportedPermissions);
|
|
93
|
+
const uiCspRef = useRef(uiMeta.csp);
|
|
94
|
+
appRef.current = app;
|
|
95
|
+
supportedPermissionsRef.current = supportedPermissions;
|
|
96
|
+
uiCspRef.current = uiMeta.csp;
|
|
97
|
+
|
|
52
98
|
useEffect(() => {
|
|
53
99
|
desiredHeightRef.current = DEFAULT_MCP_APP_IFRAME_HEIGHT;
|
|
54
100
|
setHeight(
|
|
@@ -57,11 +103,18 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
57
103
|
availableMcpAppHeight(iframeRef.current),
|
|
58
104
|
),
|
|
59
105
|
);
|
|
60
|
-
|
|
106
|
+
readyRef.current = false;
|
|
61
107
|
setReady(false);
|
|
62
108
|
setError(null);
|
|
109
|
+
modelContextRef.current = null;
|
|
63
110
|
}, [srcDoc]);
|
|
64
111
|
|
|
112
|
+
const markReady = useCallback(() => {
|
|
113
|
+
readyRef.current = true;
|
|
114
|
+
setReady(true);
|
|
115
|
+
setError(null);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
65
118
|
const applyHeight = useCallback((desiredHeight?: number) => {
|
|
66
119
|
if (
|
|
67
120
|
typeof desiredHeight === "number" &&
|
|
@@ -114,11 +167,26 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
114
167
|
|
|
115
168
|
useEffect(() => {
|
|
116
169
|
const iframe = iframeRef.current;
|
|
117
|
-
if (!iframe?.contentWindow || !srcDoc
|
|
170
|
+
if (!iframe?.contentWindow || !srcDoc) return;
|
|
171
|
+
const frameWindow = iframe.contentWindow;
|
|
172
|
+
const listener = (event: MessageEvent) => {
|
|
173
|
+
if (event.source !== frameWindow) return;
|
|
174
|
+
if (isMcpAppReadyMessage(event.data)) {
|
|
175
|
+
markReady();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
window.addEventListener("message", listener);
|
|
179
|
+
return () => window.removeEventListener("message", listener);
|
|
180
|
+
}, [markReady, srcDoc]);
|
|
181
|
+
|
|
182
|
+
useBrowserLayoutEffect(() => {
|
|
183
|
+
const iframe = iframeRef.current;
|
|
184
|
+
if (!iframe?.contentWindow || !srcDoc) return;
|
|
118
185
|
|
|
186
|
+
const currentApp = appRef.current;
|
|
119
187
|
let closed = false;
|
|
120
188
|
const initializeTimer = window.setTimeout(() => {
|
|
121
|
-
if (closed) return;
|
|
189
|
+
if (closed || readyRef.current) return;
|
|
122
190
|
setReady(false);
|
|
123
191
|
setError("MCP App did not finish initializing.");
|
|
124
192
|
}, MCP_APP_INITIALIZE_TIMEOUT_MS);
|
|
@@ -131,13 +199,13 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
131
199
|
serverResources: {},
|
|
132
200
|
logging: {},
|
|
133
201
|
sandbox: {
|
|
134
|
-
permissions:
|
|
135
|
-
csp:
|
|
202
|
+
permissions: supportedPermissionsRef.current,
|
|
203
|
+
csp: uiCspRef.current ?? {},
|
|
136
204
|
},
|
|
137
205
|
},
|
|
138
206
|
{
|
|
139
207
|
hostContext: buildHostContext(
|
|
140
|
-
|
|
208
|
+
currentApp,
|
|
141
209
|
availableMcpAppHeight(iframe),
|
|
142
210
|
) as any,
|
|
143
211
|
},
|
|
@@ -152,12 +220,9 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
152
220
|
bridge.addEventListener("initialized", () => {
|
|
153
221
|
if (closed) return;
|
|
154
222
|
clearTimeout(initializeTimer);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
setError(null);
|
|
159
|
-
void bridge.sendToolInput({ arguments: app.toolInput });
|
|
160
|
-
void bridge.sendToolResult(app.toolResult as CallToolResult);
|
|
223
|
+
markReady();
|
|
224
|
+
void bridge.sendToolInput({ arguments: appRef.current.toolInput });
|
|
225
|
+
void bridge.sendToolResult(appRef.current.toolResult as CallToolResult);
|
|
161
226
|
});
|
|
162
227
|
bridge.addEventListener("loggingmessage", ({ level, data }) => {
|
|
163
228
|
if (level === "error" || level === "critical" || level === "alert") {
|
|
@@ -170,13 +235,16 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
170
235
|
return {};
|
|
171
236
|
};
|
|
172
237
|
bridge.oncalltool = async ({ name, arguments: toolArguments }) => {
|
|
173
|
-
const toolName = normalizeSameServerToolName(
|
|
238
|
+
const toolName = normalizeSameServerToolName(
|
|
239
|
+
appRef.current.serverId,
|
|
240
|
+
name,
|
|
241
|
+
);
|
|
174
242
|
if (!toolName) {
|
|
175
243
|
return errorToolResult("Cross-server MCP App tool calls are blocked.");
|
|
176
244
|
}
|
|
177
245
|
try {
|
|
178
246
|
return await postMcpAppEndpoint<CallToolResult>("call-tool", {
|
|
179
|
-
serverId:
|
|
247
|
+
serverId: appRef.current.serverId,
|
|
180
248
|
toolName,
|
|
181
249
|
arguments:
|
|
182
250
|
toolArguments && typeof toolArguments === "object"
|
|
@@ -188,22 +256,40 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
188
256
|
}
|
|
189
257
|
};
|
|
190
258
|
(bridge as any).onlisttools = async () =>
|
|
191
|
-
postMcpAppEndpoint("list-tools", { serverId:
|
|
259
|
+
postMcpAppEndpoint("list-tools", { serverId: appRef.current.serverId });
|
|
192
260
|
bridge.onreadresource = async ({ uri }) =>
|
|
193
|
-
postMcpAppEndpoint("read-resource", {
|
|
261
|
+
postMcpAppEndpoint("read-resource", {
|
|
262
|
+
serverId: appRef.current.serverId,
|
|
263
|
+
uri,
|
|
264
|
+
});
|
|
194
265
|
bridge.onlistresources = async () => ({ resources: [] });
|
|
195
266
|
bridge.onlistresourcetemplates = async () => ({ resourceTemplates: [] });
|
|
196
267
|
bridge.ondownloadfile = async () => ({ isError: true });
|
|
197
|
-
bridge.onmessage = async () =>
|
|
198
|
-
|
|
268
|
+
bridge.onmessage = async (params) => {
|
|
269
|
+
const message = messageTextFromMcpUiMessage(params);
|
|
270
|
+
if (!message.trim()) return { isError: true };
|
|
271
|
+
const mode = requestModeFromMcpUiMessage(params);
|
|
272
|
+
sendToAgentChat({
|
|
273
|
+
message,
|
|
274
|
+
context: contextTextFromMcpModelContext(modelContextRef.current),
|
|
275
|
+
images: imageDataUrlsFromMcpContent(params.content),
|
|
276
|
+
submit: true,
|
|
277
|
+
openSidebar: true,
|
|
278
|
+
...(mode ? { mode } : {}),
|
|
279
|
+
});
|
|
280
|
+
return {};
|
|
281
|
+
};
|
|
282
|
+
bridge.onupdatemodelcontext = async (params) => {
|
|
283
|
+
modelContextRef.current = params as McpAppModelContext;
|
|
284
|
+
return {};
|
|
285
|
+
};
|
|
199
286
|
|
|
200
287
|
const transport = new PostMessageTransport(
|
|
201
288
|
iframe.contentWindow,
|
|
202
289
|
iframe.contentWindow,
|
|
203
290
|
);
|
|
204
|
-
setReady(false);
|
|
205
291
|
setError(null);
|
|
206
|
-
bridge.connect(transport).catch((err: any) => {
|
|
292
|
+
void bridge.connect(transport).catch((err: any) => {
|
|
207
293
|
if (!closed) {
|
|
208
294
|
clearTimeout(initializeTimer);
|
|
209
295
|
setError(err?.message ?? "Failed to initialize MCP App.");
|
|
@@ -212,8 +298,8 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
212
298
|
|
|
213
299
|
return () => {
|
|
214
300
|
closed = true;
|
|
301
|
+
modelContextRef.current = null;
|
|
215
302
|
clearTimeout(initializeTimer);
|
|
216
|
-
setReady(false);
|
|
217
303
|
void bridge
|
|
218
304
|
.teardownResource({}, { timeout: 500 })
|
|
219
305
|
.catch(() => undefined)
|
|
@@ -221,14 +307,10 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
221
307
|
void (bridge as any).close?.().catch?.(() => undefined);
|
|
222
308
|
});
|
|
223
309
|
};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
srcDoc,
|
|
229
|
-
supportedPermissions,
|
|
230
|
-
uiMeta.csp,
|
|
231
|
-
]);
|
|
310
|
+
// The embedded resource identity is captured by `srcDoc`; `app`,
|
|
311
|
+
// `supportedPermissions`, and `uiMeta.csp` are read via refs so a
|
|
312
|
+
// new-but-equal `app` object reference does not tear down a live bridge.
|
|
313
|
+
}, [applyHeight, markReady, srcDoc]);
|
|
232
314
|
|
|
233
315
|
if (!resourceHtml) {
|
|
234
316
|
return (
|
|
@@ -277,12 +359,83 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
277
359
|
sandbox={SANDBOX_FLAGS}
|
|
278
360
|
allow={buildAllowAttribute(supportedPermissions)}
|
|
279
361
|
style={{ height }}
|
|
280
|
-
onLoad={() => setLoadedSrcDoc(srcDoc)}
|
|
281
362
|
/>
|
|
282
363
|
</div>
|
|
283
364
|
);
|
|
284
365
|
}
|
|
285
366
|
|
|
367
|
+
function messageTextFromMcpUiMessage(params: { content?: unknown }): string {
|
|
368
|
+
return textPartsFromMcpContent(params.content).join("\n\n").trim();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function requestModeFromMcpUiMessage(
|
|
372
|
+
params: unknown,
|
|
373
|
+
): AgentChatRequestMode | undefined {
|
|
374
|
+
const record =
|
|
375
|
+
params && typeof params === "object" && !Array.isArray(params)
|
|
376
|
+
? (params as { mode?: unknown; requestMode?: unknown })
|
|
377
|
+
: {};
|
|
378
|
+
const mode = record.requestMode ?? record.mode;
|
|
379
|
+
return mode === "act" || mode === "plan" ? mode : undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function contextTextFromMcpModelContext(
|
|
383
|
+
context: McpAppModelContext | null,
|
|
384
|
+
): string | undefined {
|
|
385
|
+
if (!context) return undefined;
|
|
386
|
+
const parts = textPartsFromMcpContent(context.content);
|
|
387
|
+
if (context.structuredContent !== undefined) {
|
|
388
|
+
parts.push(JSON.stringify(context.structuredContent, null, 2));
|
|
389
|
+
}
|
|
390
|
+
const text = parts.join("\n\n").trim();
|
|
391
|
+
return text || undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function textPartsFromMcpContent(content: unknown): string[] {
|
|
395
|
+
if (!Array.isArray(content)) return [];
|
|
396
|
+
return content
|
|
397
|
+
.map((part): string | null => {
|
|
398
|
+
const record = contentPartRecord(part);
|
|
399
|
+
if (
|
|
400
|
+
!record ||
|
|
401
|
+
record.type !== "text" ||
|
|
402
|
+
typeof record.text !== "string"
|
|
403
|
+
) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
const text = record.text.trim();
|
|
407
|
+
return text || null;
|
|
408
|
+
})
|
|
409
|
+
.filter((text): text is string => Boolean(text));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function imageDataUrlsFromMcpContent(content: unknown): string[] | undefined {
|
|
413
|
+
if (!Array.isArray(content)) return undefined;
|
|
414
|
+
const images = content
|
|
415
|
+
.map((part): string | null => {
|
|
416
|
+
const record = contentPartRecord(part);
|
|
417
|
+
if (!record || record.type !== "image") return null;
|
|
418
|
+
if (typeof record.url === "string" && record.url.trim()) {
|
|
419
|
+
return record.url;
|
|
420
|
+
}
|
|
421
|
+
if (typeof record.data !== "string" || !record.data.trim()) return null;
|
|
422
|
+
const mimeType =
|
|
423
|
+
typeof record.mimeType === "string" &&
|
|
424
|
+
record.mimeType.startsWith("image/")
|
|
425
|
+
? record.mimeType
|
|
426
|
+
: "image/png";
|
|
427
|
+
return `data:${mimeType};base64,${record.data}`;
|
|
428
|
+
})
|
|
429
|
+
.filter((image): image is string => Boolean(image));
|
|
430
|
+
return images.length ? images : undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function contentPartRecord(part: unknown): McpContentPart | null {
|
|
434
|
+
return part && typeof part === "object" && !Array.isArray(part)
|
|
435
|
+
? (part as McpContentPart)
|
|
436
|
+
: null;
|
|
437
|
+
}
|
|
438
|
+
|
|
286
439
|
function resourceUiMeta(app: AgentMcpAppPayload): ResourceUiMeta {
|
|
287
440
|
const meta = app.resource?._meta;
|
|
288
441
|
const ui =
|
|
@@ -341,8 +494,31 @@ export function supportedMcpAppPermissions(
|
|
|
341
494
|
return permissions?.clipboardWrite ? { clipboardWrite: {} } : {};
|
|
342
495
|
}
|
|
343
496
|
|
|
497
|
+
export function isMcpAppReadyMessage(data: unknown): boolean {
|
|
498
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
499
|
+
const record = data as {
|
|
500
|
+
protocol?: unknown;
|
|
501
|
+
version?: unknown;
|
|
502
|
+
type?: unknown;
|
|
503
|
+
};
|
|
504
|
+
if (
|
|
505
|
+
record.protocol === AGENT_NATIVE_EMBED_PROTOCOL &&
|
|
506
|
+
record.version === AGENT_NATIVE_EMBED_VERSION &&
|
|
507
|
+
record.type === AGENT_NATIVE_EMBED_MESSAGE_TYPES.READY
|
|
508
|
+
) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
const type = record.type;
|
|
512
|
+
return (
|
|
513
|
+
type === "agentNative.embeddedAppReady" ||
|
|
514
|
+
type === "agentNative.frameOrigin"
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
344
518
|
export function buildMcpAppCsp(csp: McpUiResourceCsp | undefined): string {
|
|
345
|
-
const connect =
|
|
519
|
+
const connect = withLocalWebSocketSources(
|
|
520
|
+
sanitizeCspSources(csp?.connectDomains),
|
|
521
|
+
);
|
|
346
522
|
const resources = sanitizeCspSources(csp?.resourceDomains);
|
|
347
523
|
const frames = sanitizeCspSources(csp?.frameDomains);
|
|
348
524
|
const base = sanitizeCspSources(csp?.baseUriDomains);
|
|
@@ -369,6 +545,24 @@ function sanitizeCspSources(values: string[] | undefined): string[] {
|
|
|
369
545
|
return [...new Set(out)];
|
|
370
546
|
}
|
|
371
547
|
|
|
548
|
+
function withLocalWebSocketSources(sources: string[]): string[] {
|
|
549
|
+
const out = [...sources];
|
|
550
|
+
for (const source of sources) {
|
|
551
|
+
try {
|
|
552
|
+
const url = new URL(source);
|
|
553
|
+
if (
|
|
554
|
+
url.protocol === "http:" &&
|
|
555
|
+
["localhost", "127.0.0.1", "::1"].includes(url.hostname)
|
|
556
|
+
) {
|
|
557
|
+
out.push(`ws://${url.host}`);
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
// Ignore non-URL CSP source expressions.
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return [...new Set(out)];
|
|
564
|
+
}
|
|
565
|
+
|
|
372
566
|
function sanitizeCspSource(value: string): string | null {
|
|
373
567
|
const source = value.trim();
|
|
374
568
|
if (!source || source.includes("'") || /[\s;]/.test(source)) return null;
|
|
@@ -33,7 +33,9 @@ export function DemoModeSection() {
|
|
|
33
33
|
const { data } = useQuery({
|
|
34
34
|
queryKey: ["agent-native", "demo-mode"],
|
|
35
35
|
queryFn: async () => {
|
|
36
|
-
const res = await fetch(DEMO_STATUS_URL, {
|
|
36
|
+
const res = await fetch(DEMO_STATUS_URL, {
|
|
37
|
+
credentials: "same-origin",
|
|
38
|
+
});
|
|
37
39
|
if (!res.ok) return null;
|
|
38
40
|
return (await res.json()) as DemoStatus | null;
|
|
39
41
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useLocation, useNavigate } from "react-router";
|
|
3
3
|
import { appBasePath } from "./api-path.js";
|
|
4
4
|
import {
|
|
@@ -21,6 +21,12 @@ export interface UseAgentChatHomeHandoffLinksOptions {
|
|
|
21
21
|
storageKey?: string | null;
|
|
22
22
|
/** Router-local path for the full-page chat route. Defaults to "/". */
|
|
23
23
|
chatPath?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Matches every router-local pathname owned by the full-page chat surface.
|
|
26
|
+
* Defaults to an exact match against `chatPath`. Pass this to cover deep
|
|
27
|
+
* links such as `/chat/:threadId`.
|
|
28
|
+
*/
|
|
29
|
+
isChatPath?: (pathname: string) => boolean;
|
|
24
30
|
/** Disable link interception without changing hook call order. */
|
|
25
31
|
enabled?: boolean;
|
|
26
32
|
}
|
|
@@ -120,13 +126,19 @@ export function useAgentChatHomeHandoff({
|
|
|
120
126
|
export function useAgentChatHomeHandoffLinks({
|
|
121
127
|
storageKey,
|
|
122
128
|
chatPath = "/",
|
|
129
|
+
isChatPath,
|
|
123
130
|
enabled = true,
|
|
124
131
|
}: UseAgentChatHomeHandoffLinksOptions): void {
|
|
125
132
|
const location = useLocation();
|
|
126
133
|
const navigate = useNavigate();
|
|
134
|
+
const matchesChatPathRef = useRef<(pathname: string) => boolean>(() => false);
|
|
135
|
+
matchesChatPathRef.current =
|
|
136
|
+
isChatPath ?? ((pathname: string) => pathname === chatPath);
|
|
127
137
|
|
|
128
138
|
useEffect(() => {
|
|
129
|
-
|
|
139
|
+
const matchesChatPath = (pathname: string) =>
|
|
140
|
+
matchesChatPathRef.current(pathname);
|
|
141
|
+
if (!enabled || !matchesChatPath(stripBasePath(location.pathname))) return;
|
|
130
142
|
if (typeof document === "undefined") return;
|
|
131
143
|
|
|
132
144
|
function handleClick(event: MouseEvent) {
|
|
@@ -141,7 +153,7 @@ export function useAgentChatHomeHandoffLinks({
|
|
|
141
153
|
const pathname = path ? pathnameFromLocalPath(path) : "";
|
|
142
154
|
if (
|
|
143
155
|
!path ||
|
|
144
|
-
pathname
|
|
156
|
+
matchesChatPath(pathname) ||
|
|
145
157
|
isFrameworkOrApiPath(pathname) ||
|
|
146
158
|
isStaticAssetPath(pathname)
|
|
147
159
|
) {
|
|
@@ -155,5 +167,5 @@ export function useAgentChatHomeHandoffLinks({
|
|
|
155
167
|
|
|
156
168
|
document.addEventListener("click", handleClick, true);
|
|
157
169
|
return () => document.removeEventListener("click", handleClick, true);
|
|
158
|
-
}, [
|
|
170
|
+
}, [enabled, location.pathname, navigate, storageKey]);
|
|
159
171
|
}
|
|
@@ -63,6 +63,12 @@ export interface UseChatThreadsOptions {
|
|
|
63
63
|
autoCreate?: boolean;
|
|
64
64
|
/** Restore the active thread from localStorage. Defaults to true. */
|
|
65
65
|
restoreActiveThread?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Route-owned active thread. `undefined` preserves the legacy localStorage
|
|
68
|
+
* source of truth; a string opens that thread; `null` means the URL is in
|
|
69
|
+
* create/new-chat mode.
|
|
70
|
+
*/
|
|
71
|
+
routeThreadId?: string | null;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
const ACTIVE_THREAD_KEY = "agent-chat-active-thread";
|
|
@@ -114,6 +120,12 @@ function createLocalThreadId(): string {
|
|
|
114
120
|
return `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
function normalizeThreadId(value: string | null | undefined): string | null {
|
|
124
|
+
if (typeof value !== "string") return null;
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
return trimmed || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
117
129
|
function scopesMatch(
|
|
118
130
|
a?: ChatThreadScope | null,
|
|
119
131
|
b?: ChatThreadScope | null,
|
|
@@ -155,6 +167,8 @@ export function useChatThreads(
|
|
|
155
167
|
) {
|
|
156
168
|
const autoCreate = options?.autoCreate !== false;
|
|
157
169
|
const restoreActiveThread = options?.restoreActiveThread !== false;
|
|
170
|
+
const routeControlsActiveThread = options?.routeThreadId !== undefined;
|
|
171
|
+
const routeThreadId = normalizeThreadId(options?.routeThreadId);
|
|
158
172
|
// Each (storageKey, scope) pair gets its own active-thread localStorage key
|
|
159
173
|
// for chats that belong to a resource. General chats keep using the unscoped
|
|
160
174
|
// key even while the user is looking at a resource, so clicking into a deck,
|
|
@@ -180,10 +194,16 @@ export function useChatThreads(
|
|
|
180
194
|
let id: string | null = null;
|
|
181
195
|
let isNew = false;
|
|
182
196
|
if (typeof window !== "undefined") {
|
|
183
|
-
|
|
184
|
-
id =
|
|
185
|
-
}
|
|
186
|
-
|
|
197
|
+
if (routeControlsActiveThread) {
|
|
198
|
+
id = routeThreadId;
|
|
199
|
+
} else {
|
|
200
|
+
try {
|
|
201
|
+
id = restoreActiveThread
|
|
202
|
+
? localStorage.getItem(activeThreadKey)
|
|
203
|
+
: null;
|
|
204
|
+
} catch {
|
|
205
|
+
id = null;
|
|
206
|
+
}
|
|
187
207
|
}
|
|
188
208
|
if (!id && autoCreate) {
|
|
189
209
|
id = createLocalThreadId();
|
|
@@ -323,6 +343,10 @@ export function useChatThreads(
|
|
|
323
343
|
const persistedKeyRef = useRef(activeThreadKey);
|
|
324
344
|
useEffect(() => {
|
|
325
345
|
if (persistedKeyRef.current !== activeThreadKey) {
|
|
346
|
+
if (routeControlsActiveThread) {
|
|
347
|
+
persistedKeyRef.current = activeThreadKey;
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
326
350
|
const currentId = activeThreadIdRef.current;
|
|
327
351
|
if (currentId) {
|
|
328
352
|
const currentThreadScope = readKnownThreadScope(currentId);
|
|
@@ -357,6 +381,11 @@ export function useChatThreads(
|
|
|
357
381
|
return;
|
|
358
382
|
}
|
|
359
383
|
try {
|
|
384
|
+
if (routeControlsActiveThread && !routeThreadId) {
|
|
385
|
+
localStorage.removeItem(activeThreadKey);
|
|
386
|
+
localStorage.removeItem(activeThreadSeenKey);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
360
389
|
if (activeThreadId) {
|
|
361
390
|
const threadScope = readKnownThreadScope(activeThreadId);
|
|
362
391
|
if (threadScope === undefined) return;
|
|
@@ -378,6 +407,8 @@ export function useChatThreads(
|
|
|
378
407
|
addOptimisticThread,
|
|
379
408
|
autoCreate,
|
|
380
409
|
readKnownThreadScope,
|
|
410
|
+
routeControlsActiveThread,
|
|
411
|
+
routeThreadId,
|
|
381
412
|
storageKey,
|
|
382
413
|
threads,
|
|
383
414
|
]);
|
|
@@ -484,6 +515,10 @@ export function useChatThreads(
|
|
|
484
515
|
const loadedHasSavedId = Boolean(
|
|
485
516
|
savedId && loadedThreads.some((t) => t.id === savedId),
|
|
486
517
|
);
|
|
518
|
+
const savedIdCameFromRoute =
|
|
519
|
+
Boolean(savedId) &&
|
|
520
|
+
routeControlsActiveThread &&
|
|
521
|
+
routeThreadId === savedId;
|
|
487
522
|
|
|
488
523
|
if (
|
|
489
524
|
savedId &&
|
|
@@ -491,6 +526,11 @@ export function useChatThreads(
|
|
|
491
526
|
!loadedHasSavedId
|
|
492
527
|
) {
|
|
493
528
|
addOptimisticThread(savedId, scopeRef.current ?? null);
|
|
529
|
+
} else if (savedId && savedIdCameFromRoute && !loadedHasSavedId) {
|
|
530
|
+
// A deep link may point to a thread that is not in the current list
|
|
531
|
+
// response. Keep it route-owned so AssistantChat can restore it via
|
|
532
|
+
// /threads/:id instead of reclassifying it as a new empty tab.
|
|
533
|
+
setActiveThreadId(savedId);
|
|
494
534
|
} else if (
|
|
495
535
|
savedId &&
|
|
496
536
|
!newlyCreatedRef.current.has(savedId) &&
|
|
@@ -526,7 +566,13 @@ export function useChatThreads(
|
|
|
526
566
|
}
|
|
527
567
|
setIsLoading(false);
|
|
528
568
|
})();
|
|
529
|
-
}, [
|
|
569
|
+
}, [
|
|
570
|
+
fetchThreads,
|
|
571
|
+
addOptimisticThread,
|
|
572
|
+
autoCreate,
|
|
573
|
+
routeControlsActiveThread,
|
|
574
|
+
routeThreadId,
|
|
575
|
+
]);
|
|
530
576
|
|
|
531
577
|
const createThread = useCallback(
|
|
532
578
|
(preferredId?: string): Promise<string | null> => {
|
|
@@ -543,6 +589,41 @@ export function useChatThreads(
|
|
|
543
589
|
[addOptimisticThread],
|
|
544
590
|
);
|
|
545
591
|
|
|
592
|
+
useEffect(() => {
|
|
593
|
+
if (!routeControlsActiveThread) return;
|
|
594
|
+
if (routeThreadId) {
|
|
595
|
+
if (activeThreadIdRef.current !== routeThreadId) {
|
|
596
|
+
setActiveThreadId(routeThreadId);
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const currentId = activeThreadIdRef.current;
|
|
602
|
+
const currentThread = currentId
|
|
603
|
+
? threadsRef.current.find((thread) => thread.id === currentId)
|
|
604
|
+
: undefined;
|
|
605
|
+
const currentIsUnsavedNewThread =
|
|
606
|
+
currentId !== null &&
|
|
607
|
+
newlyCreatedRef.current.has(currentId) &&
|
|
608
|
+
(currentThread?.messageCount ?? 0) === 0;
|
|
609
|
+
if (currentIsUnsavedNewThread) return;
|
|
610
|
+
|
|
611
|
+
if (!autoCreate) {
|
|
612
|
+
if (currentId !== null) setActiveThreadId(null);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const id = createLocalThreadId();
|
|
617
|
+
newlyCreatedRef.current.add(id);
|
|
618
|
+
addOptimisticThread(id, scopeRef.current ?? null);
|
|
619
|
+
setActiveThreadId(id);
|
|
620
|
+
}, [
|
|
621
|
+
addOptimisticThread,
|
|
622
|
+
autoCreate,
|
|
623
|
+
routeControlsActiveThread,
|
|
624
|
+
routeThreadId,
|
|
625
|
+
]);
|
|
626
|
+
|
|
546
627
|
// Drop a thread's scope so it becomes a general (cross-resource) chat.
|
|
547
628
|
// This is the "Detach from <deck>" escape hatch in the UI. The PUT
|
|
548
629
|
// also bumps the thread's updatedAt so it surfaces in the All Chats
|
|
@@ -91,7 +91,7 @@ function methodOf(input: RequestInfo | URL, init?: RequestInit): string {
|
|
|
91
91
|
async function refreshDemoFlag(): Promise<void> {
|
|
92
92
|
const f = originalFetch ?? fetch;
|
|
93
93
|
try {
|
|
94
|
-
const res = await f(STATUS_PATH, { credentials: "
|
|
94
|
+
const res = await f(STATUS_PATH, { credentials: "same-origin" });
|
|
95
95
|
if (!res.ok) return;
|
|
96
96
|
const json = (await res.json()) as {
|
|
97
97
|
enabled?: boolean;
|