@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
@@ -104,8 +104,24 @@ function persistedActiveThreadId() {
104
104
  }
105
105
  }
106
106
 
107
+ function threadIdFromPath(pathname: string) {
108
+ const match = pathname.match(/^\/chat\/([^/]+)/);
109
+ if (!match) return null;
110
+ try {
111
+ const value = decodeURIComponent(match[1]).trim();
112
+ return value || null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function chatThreadPath(threadId: string) {
119
+ return `/chat/${encodeURIComponent(threadId)}`;
120
+ }
121
+
107
122
  function ChatThreadsSection() {
108
123
  const navigate = useNavigate();
124
+ const location = useLocation();
109
125
  const {
110
126
  threads,
111
127
  activeThreadId,
@@ -162,7 +178,10 @@ function ChatThreadsSection() {
162
178
 
163
179
  function openThread(threadId: string, options?: { isNew?: boolean }) {
164
180
  switchThread(threadId);
165
- navigateWithAgentChatViewTransition(navigate, "/");
181
+ navigateWithAgentChatViewTransition(
182
+ navigate,
183
+ options?.isNew ? "/" : chatThreadPath(threadId),
184
+ );
166
185
  window.requestAnimationFrame(() => {
167
186
  window.dispatchEvent(
168
187
  new CustomEvent("agent-chat:open-thread", {
@@ -244,7 +263,10 @@ function ChatThreadsSection() {
244
263
  </div>
245
264
  <div className="grid gap-0.5">
246
265
  {visibleThreads.map((thread) => {
247
- const isActive = thread.id === activeThreadId;
266
+ const isActive =
267
+ thread.id ===
268
+ (threadIdFromPath(location.pathname) ??
269
+ (location.pathname === "/" ? null : activeThreadId));
248
270
  const isRenaming = thread.id === renamingThreadId;
249
271
  return (
250
272
  <div
@@ -348,7 +370,8 @@ export function Sidebar({
348
370
  }: SidebarProps) {
349
371
  const location = useLocation();
350
372
  const navigate = useNavigate();
351
- const isChatRoute = location.pathname === "/";
373
+ const isChatRoute =
374
+ location.pathname === "/" || location.pathname.startsWith("/chat/");
352
375
  const ToggleIcon = collapsed
353
376
  ? IconLayoutSidebarLeftExpand
354
377
  : IconLayoutSidebarLeftCollapse;
@@ -440,7 +463,7 @@ export function Sidebar({
440
463
  const Icon = item.icon;
441
464
  const isActive =
442
465
  item.href === "/"
443
- ? location.pathname === "/"
466
+ ? isChatRoute
444
467
  : location.pathname.startsWith(item.href);
445
468
  const link = (
446
469
  <Link
@@ -10,6 +10,7 @@ import { TAB_ID } from "@/lib/tab-id";
10
10
  export interface NavigationState {
11
11
  view: string;
12
12
  path?: string;
13
+ threadId?: string;
13
14
  }
14
15
 
15
16
  export function useNavigationState() {
@@ -17,14 +18,21 @@ export function useNavigationState() {
17
18
  useAgentRouteState<NavigationState>({
18
19
  browserTabId: TAB_ID,
19
20
  requestSource: TAB_ID,
20
- getNavigationState: ({ pathname, search, hash }) => ({
21
- view: viewForPath(pathname),
22
- path: appPath(pathname),
23
- }),
21
+ getNavigationState: ({ pathname }) => {
22
+ const threadId = threadIdFromPath(pathname);
23
+ return {
24
+ view: viewForPath(pathname),
25
+ path: appPath(pathname),
26
+ ...(threadId ? { threadId } : {}),
27
+ };
28
+ },
24
29
  getCommandPath: (command) =>
25
- routerPath(command.path || pathForView(command.view)),
30
+ routerPath(command.path || pathForCommand(command)),
26
31
  onNavigate: (_command, path) => {
27
- if (location.pathname === "/" && pathnameFromPath(path) !== "/") {
32
+ if (
33
+ isChatPath(location.pathname) &&
34
+ !isChatPath(pathnameFromPath(path))
35
+ ) {
28
36
  markAgentChatHomeHandoff("chat");
29
37
  }
30
38
  },
@@ -35,7 +43,19 @@ function pathnameFromPath(path: string): string {
35
43
  return path.split(/[?#]/, 1)[0] || "/";
36
44
  }
37
45
 
46
+ function threadIdFromPath(pathname: string): string | null {
47
+ const match = pathname.match(/^\/chat\/([^/]+)/);
48
+ if (!match) return null;
49
+ try {
50
+ const value = decodeURIComponent(match[1]).trim();
51
+ return value || null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
38
57
  function viewForPath(pathname: string): string {
58
+ if (isChatPath(pathname)) return "chat";
39
59
  if (pathname.startsWith("/database")) return "database";
40
60
  if (pathname.startsWith("/extensions")) return "extensions";
41
61
  if (pathname.startsWith("/observability")) return "observability";
@@ -62,6 +82,14 @@ function pathForView(view?: string): string {
62
82
  }
63
83
  }
64
84
 
85
+ function pathForCommand(command: any): string {
86
+ const path = pathForView(command?.view);
87
+ if (path !== "/") return path;
88
+ const threadId =
89
+ typeof command?.threadId === "string" ? command.threadId.trim() : "";
90
+ return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/";
91
+ }
92
+
65
93
  function routerPath(path: string): string {
66
94
  const basePath = appBasePath();
67
95
  if (!basePath) return path;
@@ -71,3 +99,7 @@ function routerPath(path: string): string {
71
99
  }
72
100
  return path;
73
101
  }
102
+
103
+ function isChatPath(pathname: string): boolean {
104
+ return pathname === "/" || pathname.startsWith("/chat/");
105
+ }
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from "react";
2
+ import { useNavigate, useParams } from "react-router";
2
3
  import {
3
4
  AgentChatSurface,
4
5
  markAgentChatHomeHandoff,
@@ -25,7 +26,14 @@ export function meta() {
25
26
  ];
26
27
  }
27
28
 
29
+ function chatThreadPath(threadId: string | null) {
30
+ return threadId ? `/chat/${encodeURIComponent(threadId)}` : "/";
31
+ }
32
+
28
33
  export default function ChatRoute() {
34
+ const { threadId } = useParams();
35
+ const navigate = useNavigate();
36
+
29
37
  useEffect(() => {
30
38
  function handleChatRunning(event: Event) {
31
39
  const detail = (event as CustomEvent).detail;
@@ -45,6 +53,11 @@ export default function ChatRoute() {
45
53
  className="h-full"
46
54
  defaultMode="chat"
47
55
  storageKey="chat"
56
+ threadUrlSync={{
57
+ routeThreadId: threadId ?? null,
58
+ getPath: chatThreadPath,
59
+ navigate,
60
+ }}
48
61
  browserTabId={TAB_ID}
49
62
  showHeader={false}
50
63
  showTabBar={false}
@@ -0,0 +1 @@
1
+ export { default, meta } from "./_index";
@@ -597,6 +597,15 @@ pub(crate) mod macos {
597
597
  mic_device_label.as_deref(),
598
598
  )?;
599
599
  let input_node = unsafe { engine.inputNode() };
600
+ // Finalize input-device format negotiation before reading the format.
601
+ // When `configure_engine_input_device` pins a specific mic, the AUHAL's
602
+ // current device is switched; the input node's `outputFormatForBus(0)`
603
+ // only reflects the new device after `prepare()` runs. Reading it first
604
+ // returns a stale format, so the tap gets installed with a mismatched
605
+ // format and forwards no usable audio to the recognizer — the user
606
+ // speaks but SFSpeechRecognizer reports "No speech detected". Mirrors
607
+ // the working `start_raw_mic_capture` ordering.
608
+ unsafe { engine.prepare() };
600
609
  let format = unsafe { input_node.outputFormatForBus(0) };
601
610
 
602
611
  // Install a tap that forwards every PCM buffer into the recognition
@@ -26,11 +26,16 @@ patterns live in `.agents/skills/`.
26
26
  - Generated files must be complete, standalone HTML unless the user asks for a
27
27
  different export format. They should render in the iframe without a build step.
28
28
  - For raster image generation, restyling, or editing existing screenshots/photos,
29
- use the first-party Assets app via `call-agent` with agent `"assets"` when
30
- available instead of claiming Design has no image tools. If the user attached
31
- an image, use its hosted chat-attachment URL or call `upload-image` to create
32
- one before delegating. If no image/upload provider is configured, say that
33
- specific setup is needed and continue any non-image Design work separately.
29
+ use the available first-party Assets MCP tool such as `generate-asset` instead
30
+ of placeholders or generic stock imagery. When the Assets picker returns a
31
+ selected asset, preserve `assetId`, `runId`, and URLs verbatim; if a design is
32
+ open, call `insert-asset` with the chosen URL/id, then refine placement with
33
+ `get-design-snapshot` and `edit-design` as needed. If no Assets MCP tool is
34
+ available, use the first-party Assets app via `call-agent` with agent
35
+ `"assets"` when available. If the user attached an image, use its hosted
36
+ chat-attachment URL or call `upload-image` to create one before delegating. If
37
+ no image/upload provider is configured, say that specific setup is needed and
38
+ continue any non-image Design work separately.
34
39
  - Use Alpine.js and Tailwind CDN for interactive prototypes. Prefer Alpine
35
40
  directives over raw inline event handlers.
36
41
  - Navigate between prototype screens with Alpine state (`x-show`), a
@@ -0,0 +1,131 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getDbExec } from "@agent-native/core/db";
3
+ import {
4
+ getRequestOrgId,
5
+ getRequestUserEmail,
6
+ } from "@agent-native/core/server/request-context";
7
+ import {
8
+ addFirstPartyRemoteServer,
9
+ isFirstPartyRemoteEndpointTrusted,
10
+ listRemoteServers,
11
+ removeRemoteServer,
12
+ } from "@agent-native/core/mcp-client";
13
+ import { fetchOrgApps } from "@agent-native/core/mcp";
14
+ import { refreshGlobalMcpManager } from "@agent-native/core/server";
15
+ import { z } from "zod";
16
+
17
+ const SERVER_NAME = "assets";
18
+
19
+ function assetsMcpUrl(appUrl: string): string {
20
+ return `${appUrl.replace(/\/+$/, "")}/_agent-native/mcp`;
21
+ }
22
+
23
+ function sameEndpoint(a: string, b: string): boolean {
24
+ try {
25
+ const urlA = new URL(a);
26
+ const urlB = new URL(b);
27
+ return (
28
+ urlA.origin === urlB.origin &&
29
+ urlA.pathname.replace(/\/+$/, "") === urlB.pathname.replace(/\/+$/, "")
30
+ );
31
+ } catch {
32
+ return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
33
+ }
34
+ }
35
+
36
+ async function listAssetsServers(orgId: string) {
37
+ const servers = await listRemoteServers("org", orgId);
38
+ return servers.filter(
39
+ (server) => server.name === SERVER_NAME && server.firstParty === true,
40
+ );
41
+ }
42
+
43
+ async function assertCanManageOrgMcp(orgId: string, userEmail: string) {
44
+ const result = await getDbExec().execute({
45
+ sql: `SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
46
+ args: [orgId, userEmail.toLowerCase()],
47
+ });
48
+ const role = String(
49
+ (result.rows[0] as { role?: unknown } | undefined)?.role,
50
+ ).toLowerCase();
51
+ if (role !== "owner" && role !== "admin") {
52
+ throw new Error(
53
+ "Only organization owners and admins can connect org-scoped MCP servers.",
54
+ );
55
+ }
56
+ }
57
+
58
+ export default defineAction({
59
+ description:
60
+ "Connect the first-party Assets MCP server for the active Design organization. Use this explicit setup action instead of mutating onboarding status.",
61
+ schema: z.object({}),
62
+ publicAgent: { expose: true, readOnly: false, requiresAuth: true },
63
+ run: async () => {
64
+ const userEmail = getRequestUserEmail();
65
+ const orgId = getRequestOrgId();
66
+ if (!userEmail || !orgId) {
67
+ throw new Error("You must be signed in to an organization.");
68
+ }
69
+ await assertCanManageOrgMcp(orgId, userEmail);
70
+
71
+ const apps = await fetchOrgApps({ selfId: "design" });
72
+ const assets = apps.find((app) => app.id === "assets");
73
+ if (!assets?.url) {
74
+ throw new Error("Could not find the Assets app in this organization.");
75
+ }
76
+ const currentUrl = assetsMcpUrl(assets.url);
77
+
78
+ const existingServers = await listAssetsServers(orgId);
79
+ for (const existing of existingServers) {
80
+ const trust = await isFirstPartyRemoteEndpointTrusted(
81
+ orgId,
82
+ "assets",
83
+ existing.url,
84
+ );
85
+ if (trust.ok && sameEndpoint(existing.url, currentUrl)) {
86
+ const managerRefreshed = await refreshGlobalMcpManager();
87
+ return {
88
+ ok: true,
89
+ connected: true,
90
+ server: existing,
91
+ managerRefreshed,
92
+ message: "Assets MCP is already connected for this organization.",
93
+ };
94
+ }
95
+ }
96
+
97
+ // Verify the target endpoint is trusted before removing any existing
98
+ // server, so a transient org-directory/auth failure cannot turn a reconnect
99
+ // into a full disconnect for the org.
100
+ const targetTrust = await isFirstPartyRemoteEndpointTrusted(
101
+ orgId,
102
+ "assets",
103
+ currentUrl,
104
+ );
105
+ if (!targetTrust.ok) {
106
+ throw new Error(targetTrust.error);
107
+ }
108
+
109
+ for (const existing of existingServers) {
110
+ await removeRemoteServer("org", orgId, existing.id);
111
+ }
112
+
113
+ const result = await addFirstPartyRemoteServer(orgId, {
114
+ appId: "assets",
115
+ name: SERVER_NAME,
116
+ url: currentUrl,
117
+ description: "First-party Assets MCP server for on-brand media.",
118
+ });
119
+ if (result.ok !== true) {
120
+ throw new Error(result.error);
121
+ }
122
+ const managerRefreshed = await refreshGlobalMcpManager();
123
+ return {
124
+ ok: true,
125
+ connected: true,
126
+ server: result.server,
127
+ managerRefreshed,
128
+ message: "Assets MCP is connected for this organization.",
129
+ };
130
+ },
131
+ });
@@ -0,0 +1,219 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { readAppStateForCurrentTab } from "@agent-native/core/application-state";
3
+ import {
4
+ hasCollabState,
5
+ getText,
6
+ applyText,
7
+ seedFromText,
8
+ } from "@agent-native/core/collab";
9
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
10
+ import { and, eq } from "drizzle-orm";
11
+ import { z } from "zod";
12
+ import { getDb, schema } from "../server/db/index.js";
13
+
14
+ const schemaInput = z.object({
15
+ assetUrl: z
16
+ .string()
17
+ .url()
18
+ .refine((value) => {
19
+ try {
20
+ const protocol = new URL(value).protocol;
21
+ return protocol === "http:" || protocol === "https:";
22
+ } catch {
23
+ return false;
24
+ }
25
+ }, "Asset URL must use http or https.")
26
+ .describe("Chosen asset image/video URL."),
27
+ assetId: z.string().optional().describe("Chosen Assets asset id."),
28
+ title: z.string().optional().describe("Human-readable asset title."),
29
+ altText: z.string().optional().describe("Alt text for an image asset."),
30
+ mediaType: z.enum(["image", "video"]).default("image"),
31
+ designId: z
32
+ .string()
33
+ .optional()
34
+ .describe("Design id. Defaults to the current editor navigation state."),
35
+ fileId: z
36
+ .string()
37
+ .optional()
38
+ .describe("Design file id. Defaults to the active editor file."),
39
+ ownerId: z
40
+ .string()
41
+ .optional()
42
+ .describe("Design editor selection owner token from current screen state."),
43
+ });
44
+
45
+ function stringFromState(state: unknown, key: string): string | undefined {
46
+ if (!state || typeof state !== "object") return undefined;
47
+ const value = (state as Record<string, unknown>)[key];
48
+ return typeof value === "string" && value ? value : undefined;
49
+ }
50
+
51
+ function escapeHtml(value: string): string {
52
+ return value
53
+ .replace(/&/g, "&amp;")
54
+ .replace(/</g, "&lt;")
55
+ .replace(/>/g, "&gt;")
56
+ .replace(/"/g, "&quot;");
57
+ }
58
+
59
+ function insertBeforeClosingTag(
60
+ html: string,
61
+ closingTag: "main" | "body",
62
+ snippet: string,
63
+ ): string | null {
64
+ const pattern = new RegExp(`</${closingTag}>`, "i");
65
+ if (!pattern.test(html)) return null;
66
+ return html.replace(pattern, `${snippet}\n</${closingTag}>`);
67
+ }
68
+
69
+ function appendAssetMarkup(
70
+ html: string,
71
+ args: z.infer<typeof schemaInput>,
72
+ ): string {
73
+ const label = args.title?.trim() || args.altText?.trim() || "Generated asset";
74
+ const escapedUrl = escapeHtml(args.assetUrl);
75
+ const escapedLabel = escapeHtml(label);
76
+ const assetIdAttr = args.assetId
77
+ ? ` data-asset-id="${escapeHtml(args.assetId)}"`
78
+ : "";
79
+ const media =
80
+ args.mediaType === "video"
81
+ ? `<video src="${escapedUrl}" controls class="w-full rounded-xl object-cover"></video>`
82
+ : `<img src="${escapedUrl}" alt="${escapeHtml(args.altText?.trim() || label)}" class="w-full rounded-xl object-cover" />`;
83
+ const snippet = `
84
+ <section class="mx-auto my-8 max-w-5xl px-4" data-agent-native-asset${assetIdAttr}>
85
+ <figure class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
86
+ ${media}
87
+ <figcaption class="px-4 py-3 text-sm text-slate-600">${escapedLabel}</figcaption>
88
+ </figure>
89
+ </section>`;
90
+
91
+ return (
92
+ insertBeforeClosingTag(html, "main", snippet) ??
93
+ insertBeforeClosingTag(html, "body", snippet) ??
94
+ `${html}\n${snippet}`
95
+ );
96
+ }
97
+
98
+ async function resolveTarget(args: z.infer<typeof schemaInput>) {
99
+ const [navigation, selection] = await Promise.all([
100
+ readAppStateForCurrentTab("navigation").catch(() => null),
101
+ readAppStateForCurrentTab("design-selection").catch(() => null),
102
+ ]);
103
+ const navigationDesignId = stringFromState(navigation, "designId");
104
+ const selectionDesignId = stringFromState(selection, "designId");
105
+ const selectionOwnerId = stringFromState(selection, "ownerId");
106
+ const selectionMatchesOwner =
107
+ Boolean(args.ownerId) && selectionOwnerId === args.ownerId;
108
+ // Prefer the owner-matched selection over generic navigation so a tab-scoped
109
+ // picker handoff lands in the design that produced it.
110
+ const designId =
111
+ args.designId ??
112
+ (selectionMatchesOwner ? selectionDesignId : undefined) ??
113
+ navigationDesignId;
114
+ const canUseSelection =
115
+ selectionMatchesOwner &&
116
+ Boolean(designId) &&
117
+ selectionDesignId === designId;
118
+ const navigationActiveFileId =
119
+ designId && navigationDesignId === designId
120
+ ? stringFromState(navigation, "activeFileId")
121
+ : undefined;
122
+ return {
123
+ designId,
124
+ fileId:
125
+ args.fileId ??
126
+ (canUseSelection
127
+ ? stringFromState(selection, "activeFileId")
128
+ : undefined) ??
129
+ navigationActiveFileId,
130
+ };
131
+ }
132
+
133
+ function isHtmlFile(file: {
134
+ fileType: string | null;
135
+ filename: string | null;
136
+ }): boolean {
137
+ return file.fileType === "html" || file.filename?.endsWith(".html") === true;
138
+ }
139
+
140
+ export default defineAction({
141
+ description:
142
+ "Insert a chosen Assets image or video URL into a Design file. Use this after the Assets picker returns chooseAsset/chooseImage context; pass designId/fileId directly when known, or pass ownerId from view-screen.designSelection when targeting the current editor selection.",
143
+ schema: schemaInput,
144
+ publicAgent: { expose: true, readOnly: false, requiresAuth: true },
145
+ run: async (args) => {
146
+ const target = await resolveTarget(args);
147
+ if (!target.designId) {
148
+ throw new Error(
149
+ "No active design found. Open a design or pass designId.",
150
+ );
151
+ }
152
+
153
+ const db = getDb();
154
+ const files = await db
155
+ .select({
156
+ id: schema.designFiles.id,
157
+ designId: schema.designFiles.designId,
158
+ filename: schema.designFiles.filename,
159
+ fileType: schema.designFiles.fileType,
160
+ content: schema.designFiles.content,
161
+ })
162
+ .from(schema.designFiles)
163
+ .innerJoin(
164
+ schema.designs,
165
+ eq(schema.designFiles.designId, schema.designs.id),
166
+ )
167
+ .where(
168
+ and(
169
+ eq(schema.designFiles.designId, target.designId),
170
+ accessFilter(schema.designs, schema.designShares),
171
+ ),
172
+ );
173
+ const requestedFile = files.find(
174
+ (candidate) => candidate.id === target.fileId,
175
+ );
176
+ const file =
177
+ requestedFile && isHtmlFile(requestedFile)
178
+ ? requestedFile
179
+ : (files.find(isHtmlFile) ?? null);
180
+ if (!file) throw new Error("No editable HTML design file found.");
181
+ await assertAccess("design", file.designId, "editor");
182
+
183
+ let base = file.content ?? "";
184
+ try {
185
+ if (await hasCollabState(file.id)) {
186
+ const live = await getText(file.id, "content");
187
+ if (typeof live === "string") base = live;
188
+ }
189
+ } catch {
190
+ // Collab read is best-effort; fall back to stored content.
191
+ }
192
+
193
+ const content = appendAssetMarkup(base, args);
194
+ const now = new Date().toISOString();
195
+ await db
196
+ .update(schema.designFiles)
197
+ .set({ content, updatedAt: now })
198
+ .where(eq(schema.designFiles.id, file.id));
199
+ await db
200
+ .update(schema.designs)
201
+ .set({ updatedAt: now })
202
+ .where(eq(schema.designs.id, file.designId));
203
+
204
+ if (await hasCollabState(file.id)) {
205
+ await applyText(file.id, content, "content", "agent");
206
+ } else {
207
+ await seedFromText(file.id, content);
208
+ }
209
+
210
+ return {
211
+ designId: file.designId,
212
+ fileId: file.id,
213
+ filename: file.filename,
214
+ inserted: true,
215
+ assetId: args.assetId ?? null,
216
+ assetUrl: args.assetUrl,
217
+ };
218
+ },
219
+ });
@@ -20,9 +20,10 @@ export default defineAction({
20
20
  schema: z.object({}),
21
21
  http: false,
22
22
  run: async () => {
23
- const [navigation, designVariants] = await Promise.all([
23
+ const [navigation, designVariants, designSelection] = await Promise.all([
24
24
  readAppStateForCurrentTab("navigation"),
25
25
  readAppState("design-variants"),
26
+ readAppStateForCurrentTab("design-selection"),
26
27
  ]);
27
28
  const designId =
28
29
  navigation &&
@@ -37,6 +38,7 @@ export default defineAction({
37
38
 
38
39
  const screen: Record<string, unknown> = {};
39
40
  if (navigation) screen.navigation = navigation;
41
+ if (designSelection) screen.designSelection = designSelection;
40
42
  if (showQuestions) {
41
43
  screen.pendingQuestions = showQuestions;
42
44
  screen.note =
@@ -398,7 +398,7 @@ export default function PromptPopover({
398
398
  return;
399
399
  }
400
400
 
401
- if (name !== "chooseImage") return;
401
+ if (name !== "chooseImage" && name !== "chooseAsset") return;
402
402
  const url = pickedAssetImageSource(payload);
403
403
  if (!url) {
404
404
  toast.error("Assets did not return an image URL.");
@@ -43,6 +43,9 @@ import {
43
43
  ShareButton,
44
44
  isEmbedAuthActive,
45
45
  sendToAgentChat,
46
+ getBrowserTabId,
47
+ readClientAppState,
48
+ setClientAppState,
46
49
  useReconciledState,
47
50
  usePresence,
48
51
  useFollowUser,
@@ -113,6 +116,16 @@ import { cn } from "@/lib/utils";
113
116
  import { toast } from "sonner";
114
117
 
115
118
  const TAB_ID = generateTabId();
119
+
120
+ // Selection is tab-scoped (like navigation) so a second editor tab cannot
121
+ // overwrite this tab's selection context. The global key is mirrored as a
122
+ // fallback for CLI/external agents that do not send a browser tab id.
123
+ function designSelectionStateKeys(): string[] {
124
+ const tabId = getBrowserTabId();
125
+ return tabId
126
+ ? [`design-selection:${tabId}`, "design-selection"]
127
+ : ["design-selection"];
128
+ }
116
129
  // Stable symbol used as the Yjs transaction origin for all local user edits.
117
130
  // The UndoManager tracks only this origin so remote peers' and the agent's
118
131
  // edits are never undone by this user's Cmd+Z.
@@ -317,6 +330,8 @@ export default function DesignEditor() {
317
330
  const [canUndo, setCanUndo] = useState(false);
318
331
  const [canRedo, setCanRedo] = useState(false);
319
332
  const undoManagerRef = useRef<Y.UndoManager | null>(null);
333
+ const persistedSelectionStateRef = useRef<string | null>(null);
334
+ const designSelectionOwnerIdRef = useRef(`${TAB_ID}:${generateTabId()}`);
320
335
  const [tweakSaveActive, setTweakSaveActive] = useState(false);
321
336
  // Shared visual-editor modes (overlays the iframe). drawMode toggles the
322
337
  // pencil overlay, pinMode lets the user drop comment pins. They're
@@ -340,6 +355,26 @@ export default function DesignEditor() {
340
355
  const [promptDesignSystemId, setPromptDesignSystemId] = useState<
341
356
  string | null | undefined
342
357
  >(undefined);
358
+
359
+ useEffect(() => {
360
+ return () => {
361
+ void (async () => {
362
+ const keys = designSelectionStateKeys();
363
+ const current = await readClientAppState(keys[0]).catch(() => null);
364
+ const ownerId =
365
+ current && typeof current === "object"
366
+ ? (current as { ownerId?: unknown }).ownerId
367
+ : undefined;
368
+ if (ownerId !== designSelectionOwnerIdRef.current) return;
369
+ persistedSelectionStateRef.current = null;
370
+ for (const key of designSelectionStateKeys()) {
371
+ await setClientAppState(key, null, {
372
+ keepalive: true,
373
+ }).catch(() => {});
374
+ }
375
+ })();
376
+ };
377
+ }, []);
343
378
  // When generation stalls we keep the original prompt + files around so the
344
379
  // user can retry with one click instead of re-typing. Cleared as soon as the
345
380
  // user kicks off a new run (retry or fresh prompt).
@@ -1414,6 +1449,24 @@ export default function DesignEditor() {
1414
1449
  mode,
1415
1450
  };
1416
1451
  (window as any).__designSelection = selection;
1452
+ const persistedSelection = {
1453
+ designId: selection.designId,
1454
+ designTitle: selection.designTitle,
1455
+ activeFileId: selection.activeFileId,
1456
+ activeFilename: selection.activeFilename,
1457
+ selectedElement: selection.selectedElement,
1458
+ mode: selection.mode,
1459
+ ownerId: designSelectionOwnerIdRef.current,
1460
+ };
1461
+ const persistedKey = JSON.stringify(persistedSelection);
1462
+ if (persistedSelectionStateRef.current !== persistedKey) {
1463
+ persistedSelectionStateRef.current = persistedKey;
1464
+ for (const key of designSelectionStateKeys()) {
1465
+ setClientAppState(key, persistedSelection, {
1466
+ keepalive: true,
1467
+ }).catch(() => {});
1468
+ }
1469
+ }
1417
1470
  const el = document.documentElement;
1418
1471
  el.dataset.designId = id;
1419
1472
  if (activeFile?.id) el.dataset.fileId = activeFile.id;