@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.
Files changed (169) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +6 -6
  4. package/corpus/core/src/a2a/client.ts +7 -3
  5. package/corpus/core/src/client/AgentPanel.tsx +12 -1
  6. package/corpus/core/src/client/MultiTabAssistantChat.tsx +263 -13
  7. package/corpus/core/src/client/api-path.ts +28 -0
  8. package/corpus/core/src/client/embed-auth.ts +26 -0
  9. package/corpus/core/src/client/mcp-app-host.ts +6 -0
  10. package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
  11. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  12. package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
  13. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +16 -4
  14. package/corpus/core/src/client/use-chat-threads.ts +86 -5
  15. package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
  16. package/corpus/core/src/embedding/agent.ts +6 -1
  17. package/corpus/core/src/embedding/bridge.ts +34 -1
  18. package/corpus/core/src/mcp/build-server.ts +38 -10
  19. package/corpus/core/src/mcp/embed-app.ts +120 -45
  20. package/corpus/core/src/mcp/index.ts +5 -0
  21. package/corpus/core/src/mcp/org-directory.ts +70 -11
  22. package/corpus/core/src/mcp/server.ts +14 -0
  23. package/corpus/core/src/mcp-client/config.ts +12 -0
  24. package/corpus/core/src/mcp-client/index.ts +2 -0
  25. package/corpus/core/src/mcp-client/manager.ts +268 -9
  26. package/corpus/core/src/mcp-client/remote-store.ts +154 -0
  27. package/corpus/core/src/mcp-client/routes.ts +2 -0
  28. package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
  29. package/corpus/core/src/server/index.ts +1 -0
  30. package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
  31. package/corpus/core/src/vite/client.ts +53 -0
  32. package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
  33. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
  34. package/corpus/templates/assets/AGENTS.md +7 -5
  35. package/corpus/templates/assets/actions/generate-asset.ts +161 -0
  36. package/corpus/templates/assets/actions/list-assets.ts +4 -2
  37. package/corpus/templates/assets/actions/navigate.ts +2 -1
  38. package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
  39. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -2
  40. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +27 -4
  41. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +35 -5
  42. package/corpus/templates/assets/app/routes/_index.tsx +12 -0
  43. package/corpus/templates/assets/app/routes/chat.$threadId.tsx +1 -0
  44. package/corpus/templates/assets/app/routes/library.tsx +243 -40
  45. package/corpus/templates/chat/actions/navigate.ts +3 -0
  46. package/corpus/templates/chat/app/components/layout/Layout.tsx +7 -2
  47. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +27 -4
  48. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +38 -6
  49. package/corpus/templates/chat/app/routes/_index.tsx +13 -0
  50. package/corpus/templates/chat/app/routes/chat.$threadId.tsx +1 -0
  51. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
  52. package/corpus/templates/design/AGENTS.md +10 -5
  53. package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
  54. package/corpus/templates/design/actions/insert-asset.ts +219 -0
  55. package/corpus/templates/design/actions/view-screen.ts +3 -1
  56. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
  57. package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
  58. package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
  59. package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
  60. package/corpus/templates/dispatch/app/routes/chat.$threadId.tsx +26 -0
  61. package/dist/a2a/client.d.ts +1 -0
  62. package/dist/a2a/client.d.ts.map +1 -1
  63. package/dist/a2a/client.js +5 -3
  64. package/dist/a2a/client.js.map +1 -1
  65. package/dist/client/AgentPanel.d.ts +6 -2
  66. package/dist/client/AgentPanel.d.ts.map +1 -1
  67. package/dist/client/AgentPanel.js +4 -4
  68. package/dist/client/AgentPanel.js.map +1 -1
  69. package/dist/client/MultiTabAssistantChat.d.ts +22 -1
  70. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  71. package/dist/client/MultiTabAssistantChat.js +215 -13
  72. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  73. package/dist/client/api-path.d.ts.map +1 -1
  74. package/dist/client/api-path.js +27 -0
  75. package/dist/client/api-path.js.map +1 -1
  76. package/dist/client/embed-auth.d.ts.map +1 -1
  77. package/dist/client/embed-auth.js +27 -0
  78. package/dist/client/embed-auth.js.map +1 -1
  79. package/dist/client/mcp-app-host.d.ts.map +1 -1
  80. package/dist/client/mcp-app-host.js +5 -0
  81. package/dist/client/mcp-app-host.js.map +1 -1
  82. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  83. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  84. package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
  85. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  86. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  87. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  88. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  89. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  90. package/dist/client/settings/DemoModeSection.js +3 -1
  91. package/dist/client/settings/DemoModeSection.js.map +1 -1
  92. package/dist/client/use-agent-chat-home-handoff.d.ts +7 -1
  93. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  94. package/dist/client/use-agent-chat-home-handoff.js +9 -5
  95. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  96. package/dist/client/use-chat-threads.d.ts +6 -0
  97. package/dist/client/use-chat-threads.d.ts.map +1 -1
  98. package/dist/client/use-chat-threads.js +79 -5
  99. package/dist/client/use-chat-threads.js.map +1 -1
  100. package/dist/demo/fetch-interceptor.js +1 -1
  101. package/dist/demo/fetch-interceptor.js.map +1 -1
  102. package/dist/embedding/agent.d.ts +1 -1
  103. package/dist/embedding/agent.d.ts.map +1 -1
  104. package/dist/embedding/agent.js +1 -1
  105. package/dist/embedding/agent.js.map +1 -1
  106. package/dist/embedding/bridge.d.ts.map +1 -1
  107. package/dist/embedding/bridge.js +27 -1
  108. package/dist/embedding/bridge.js.map +1 -1
  109. package/dist/extensions/schema.d.ts +2 -2
  110. package/dist/mcp/build-server.d.ts +2 -0
  111. package/dist/mcp/build-server.d.ts.map +1 -1
  112. package/dist/mcp/build-server.js +32 -9
  113. package/dist/mcp/build-server.js.map +1 -1
  114. package/dist/mcp/embed-app.d.ts.map +1 -1
  115. package/dist/mcp/embed-app.js +120 -45
  116. package/dist/mcp/embed-app.js.map +1 -1
  117. package/dist/mcp/index.d.ts +1 -0
  118. package/dist/mcp/index.d.ts.map +1 -1
  119. package/dist/mcp/index.js +1 -0
  120. package/dist/mcp/index.js.map +1 -1
  121. package/dist/mcp/org-directory.d.ts +1 -0
  122. package/dist/mcp/org-directory.d.ts.map +1 -1
  123. package/dist/mcp/org-directory.js +59 -11
  124. package/dist/mcp/org-directory.js.map +1 -1
  125. package/dist/mcp/server.d.ts.map +1 -1
  126. package/dist/mcp/server.js +9 -0
  127. package/dist/mcp/server.js.map +1 -1
  128. package/dist/mcp-client/config.d.ts +12 -0
  129. package/dist/mcp-client/config.d.ts.map +1 -1
  130. package/dist/mcp-client/config.js.map +1 -1
  131. package/dist/mcp-client/index.d.ts +1 -1
  132. package/dist/mcp-client/index.d.ts.map +1 -1
  133. package/dist/mcp-client/index.js +1 -1
  134. package/dist/mcp-client/index.js.map +1 -1
  135. package/dist/mcp-client/manager.d.ts +2 -0
  136. package/dist/mcp-client/manager.d.ts.map +1 -1
  137. package/dist/mcp-client/manager.js +201 -9
  138. package/dist/mcp-client/manager.js.map +1 -1
  139. package/dist/mcp-client/remote-store.d.ts +27 -0
  140. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  141. package/dist/mcp-client/remote-store.js +105 -0
  142. package/dist/mcp-client/remote-store.js.map +1 -1
  143. package/dist/mcp-client/routes.d.ts +1 -0
  144. package/dist/mcp-client/routes.d.ts.map +1 -1
  145. package/dist/mcp-client/routes.js +1 -0
  146. package/dist/mcp-client/routes.js.map +1 -1
  147. package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
  148. package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
  149. package/dist/provider-api/corpus-jobs.d.ts +8 -8
  150. package/dist/server/agent-chat-plugin.d.ts +2 -0
  151. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  152. package/dist/server/agent-chat-plugin.js +8 -0
  153. package/dist/server/agent-chat-plugin.js.map +1 -1
  154. package/dist/server/index.d.ts +1 -1
  155. package/dist/server/index.d.ts.map +1 -1
  156. package/dist/server/index.js +1 -1
  157. package/dist/server/index.js.map +1 -1
  158. package/dist/shared/mcp-embed-headers.d.ts +3 -1
  159. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  160. package/dist/shared/mcp-embed-headers.js +40 -3
  161. package/dist/shared/mcp-embed-headers.js.map +1 -1
  162. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  163. package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
  164. package/dist/sharing/actions/share-resource.d.ts +1 -1
  165. package/dist/sharing/schema.d.ts +1 -1
  166. package/dist/vite/client.d.ts.map +1 -1
  167. package/dist/vite/client.js +44 -0
  168. package/dist/vite/client.js.map +1 -1
  169. package/package.json +6 -6
@@ -1,4 +1,11 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
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 [loadedSrcDoc, setLoadedSrcDoc] = useState<string | null>(null);
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
- setLoadedSrcDoc(null);
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 || loadedSrcDoc !== srcDoc) return;
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: supportedPermissions,
135
- csp: uiMeta.csp ?? {},
202
+ permissions: supportedPermissionsRef.current,
203
+ csp: uiCspRef.current ?? {},
136
204
  },
137
205
  },
138
206
  {
139
207
  hostContext: buildHostContext(
140
- app,
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
- setReady(true);
156
- // Clear any error the initialize timeout may have set before a slow app
157
- // finished otherwise the error overlay stays stuck over a working app.
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(app.serverId, name);
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: app.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: app.serverId });
259
+ postMcpAppEndpoint("list-tools", { serverId: appRef.current.serverId });
192
260
  bridge.onreadresource = async ({ uri }) =>
193
- postMcpAppEndpoint("read-resource", { serverId: app.serverId, uri });
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 () => ({ isError: true });
198
- bridge.onupdatemodelcontext = async () => ({});
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
- app,
226
- applyHeight,
227
- loadedSrcDoc,
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 = sanitizeCspSources(csp?.connectDomains);
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;
@@ -20,6 +20,7 @@ export interface McpServer {
20
20
  url: string;
21
21
  headers?: Record<string, { set: true }>;
22
22
  description?: string;
23
+ firstParty?: boolean;
23
24
  createdAt: number;
24
25
  mergedId: string;
25
26
  status:
@@ -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, { credentials: "include" });
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
- if (!enabled || stripBasePath(location.pathname) !== chatPath) return;
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 === chatPath ||
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
- }, [chatPath, enabled, location.pathname, navigate, storageKey]);
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
- try {
184
- id = restoreActiveThread ? localStorage.getItem(activeThreadKey) : null;
185
- } catch {
186
- id = null;
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
- }, [fetchThreads, addOptimisticThread, autoCreate]);
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: "include" });
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;
@@ -1,4 +1,9 @@
1
- import { A2AClient, callAgent, type Message, type Task } from "../a2a/index.js";
1
+ import {
2
+ A2AClient,
3
+ callAgent,
4
+ type Message,
5
+ type Task,
6
+ } from "@agent-native/core/a2a";
2
7
 
3
8
  export interface AgentEndpointOptions {
4
9
  /**