@agent-native/core 0.72.3 → 0.73.0

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 (223) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +38 -0
  3. package/corpus/core/docs/content/skills-guide.md +14 -0
  4. package/corpus/core/docs/content/template-clips.md +7 -1
  5. package/corpus/core/package.json +6 -6
  6. package/corpus/core/src/a2a/client.ts +7 -3
  7. package/corpus/core/src/agent/durable-background.ts +25 -12
  8. package/corpus/core/src/cli/create.ts +1 -1
  9. package/corpus/core/src/cli/skills.ts +450 -19
  10. package/corpus/core/src/client/api-path.ts +28 -0
  11. package/corpus/core/src/client/embed-auth.ts +26 -0
  12. package/corpus/core/src/client/mcp-app-host.ts +6 -0
  13. package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
  14. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  15. package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
  16. package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
  17. package/corpus/core/src/embedding/agent.ts +6 -1
  18. package/corpus/core/src/embedding/bridge.ts +34 -1
  19. package/corpus/core/src/mcp/build-server.ts +38 -10
  20. package/corpus/core/src/mcp/embed-app.ts +120 -45
  21. package/corpus/core/src/mcp/index.ts +5 -0
  22. package/corpus/core/src/mcp/org-directory.ts +70 -11
  23. package/corpus/core/src/mcp/server.ts +14 -0
  24. package/corpus/core/src/mcp-client/config.ts +12 -0
  25. package/corpus/core/src/mcp-client/index.ts +2 -0
  26. package/corpus/core/src/mcp-client/manager.ts +268 -9
  27. package/corpus/core/src/mcp-client/remote-store.ts +154 -0
  28. package/corpus/core/src/mcp-client/routes.ts +2 -0
  29. package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
  30. package/corpus/core/src/server/auth.ts +7 -0
  31. package/corpus/core/src/server/index.ts +1 -0
  32. package/corpus/core/src/server/onboarding-html.ts +95 -9
  33. package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
  34. package/corpus/core/src/templates/default/AGENTS.md +4 -0
  35. package/corpus/core/src/templates/default/package.json +2 -1
  36. package/corpus/core/src/templates/headless/AGENTS.md +4 -0
  37. package/corpus/core/src/templates/headless/package.json +1 -0
  38. package/corpus/core/src/templates/workspace-root/AGENTS.md +4 -0
  39. package/corpus/core/src/templates/workspace-root/README.md +4 -0
  40. package/corpus/core/src/templates/workspace-root/package.json +1 -0
  41. package/corpus/core/src/vite/client.ts +53 -0
  42. package/corpus/templates/analytics/actions/update-dashboard.ts +3 -4
  43. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +144 -40
  44. package/corpus/templates/analytics/app/global.css +15 -13
  45. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +9 -32
  46. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +0 -26
  47. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +0 -23
  48. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/pivot.ts +65 -2
  49. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +3 -4
  50. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-charts-now-fill-sparse-template-time-series-suppor.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-rows-now-auto-fit-panels-so-one-two-or-three-chart.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-traffic-tables-now-show-top-urls-and-shared-clips-.md +6 -0
  53. package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +5 -0
  54. package/corpus/templates/analytics/changelog/2026-06-24-first-party-template-dashboards-now-include-repeat-users-ret.md +6 -0
  55. package/corpus/templates/analytics/changelog/2026-06-24-retention-charts-now-break-out-1-day-and-7-day-return-rates-.md +6 -0
  56. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +256 -44
  57. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +7 -2
  58. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +35 -8
  59. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +287 -42
  60. package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
  61. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
  62. package/corpus/templates/assets/AGENTS.md +7 -5
  63. package/corpus/templates/assets/actions/generate-asset.ts +161 -0
  64. package/corpus/templates/assets/actions/list-assets.ts +4 -2
  65. package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
  66. package/corpus/templates/assets/app/routes/library.tsx +243 -40
  67. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +1 -1
  68. package/corpus/templates/calendar/server/plugins/auth.ts +12 -0
  69. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +7 -7
  70. package/corpus/templates/clips/AGENTS.md +6 -2
  71. package/corpus/templates/clips/app/components/capture-install-options.tsx +20 -4
  72. package/corpus/templates/clips/app/components/player/player-controls.tsx +62 -32
  73. package/corpus/templates/clips/app/lib/capture-install-options.ts +39 -5
  74. package/corpus/templates/clips/app/routes/download.tsx +2 -2
  75. package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
  76. package/corpus/templates/clips/changelog/2026-06-24-inline-slack-playback-controls-no-longer-shift-when-adjustin.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-console-log-stream-all-le.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-fetch-xhr-network-request.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-06-24-the-chrome-extension-option-now-only-appears-on-supported-hosts.md +5 -0
  80. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +13 -8
  81. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  82. package/corpus/templates/clips/chrome-extension/scripts/package.ts +15 -2
  83. package/corpus/templates/clips/chrome-extension/src/overlay.css +4 -25
  84. package/corpus/templates/clips/chrome-extension/src/overlay.ts +1 -3
  85. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
  86. package/corpus/templates/clips/server/lib/public-agent-context.ts +67 -24
  87. package/corpus/templates/design/AGENTS.md +10 -5
  88. package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
  89. package/corpus/templates/design/actions/insert-asset.ts +219 -0
  90. package/corpus/templates/design/actions/view-screen.ts +3 -1
  91. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
  92. package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
  93. package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
  94. package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
  95. package/corpus/templates/mail/server/plugins/auth.ts +4 -1
  96. package/corpus/templates/plan/app/pages/PlansPage.tsx +271 -197
  97. package/corpus/templates/plan/server/plan-content.ts +45 -2
  98. package/dist/a2a/client.d.ts +1 -0
  99. package/dist/a2a/client.d.ts.map +1 -1
  100. package/dist/a2a/client.js +5 -3
  101. package/dist/a2a/client.js.map +1 -1
  102. package/dist/agent/durable-background.d.ts +9 -4
  103. package/dist/agent/durable-background.d.ts.map +1 -1
  104. package/dist/agent/durable-background.js +24 -11
  105. package/dist/agent/durable-background.js.map +1 -1
  106. package/dist/cli/create.js +1 -1
  107. package/dist/cli/create.js.map +1 -1
  108. package/dist/cli/skills.d.ts.map +1 -1
  109. package/dist/cli/skills.js +361 -19
  110. package/dist/cli/skills.js.map +1 -1
  111. package/dist/client/api-path.d.ts.map +1 -1
  112. package/dist/client/api-path.js +27 -0
  113. package/dist/client/api-path.js.map +1 -1
  114. package/dist/client/embed-auth.d.ts.map +1 -1
  115. package/dist/client/embed-auth.js +27 -0
  116. package/dist/client/embed-auth.js.map +1 -1
  117. package/dist/client/mcp-app-host.d.ts.map +1 -1
  118. package/dist/client/mcp-app-host.js +5 -0
  119. package/dist/client/mcp-app-host.js.map +1 -1
  120. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  121. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  122. package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
  123. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  124. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  125. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  126. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  127. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  128. package/dist/client/settings/DemoModeSection.js +3 -1
  129. package/dist/client/settings/DemoModeSection.js.map +1 -1
  130. package/dist/demo/fetch-interceptor.js +1 -1
  131. package/dist/demo/fetch-interceptor.js.map +1 -1
  132. package/dist/embedding/agent.d.ts +1 -1
  133. package/dist/embedding/agent.d.ts.map +1 -1
  134. package/dist/embedding/agent.js +1 -1
  135. package/dist/embedding/agent.js.map +1 -1
  136. package/dist/embedding/bridge.d.ts.map +1 -1
  137. package/dist/embedding/bridge.js +27 -1
  138. package/dist/embedding/bridge.js.map +1 -1
  139. package/dist/extensions/schema.d.ts +2 -2
  140. package/dist/mcp/build-server.d.ts +2 -0
  141. package/dist/mcp/build-server.d.ts.map +1 -1
  142. package/dist/mcp/build-server.js +32 -9
  143. package/dist/mcp/build-server.js.map +1 -1
  144. package/dist/mcp/embed-app.d.ts.map +1 -1
  145. package/dist/mcp/embed-app.js +120 -45
  146. package/dist/mcp/embed-app.js.map +1 -1
  147. package/dist/mcp/index.d.ts +1 -0
  148. package/dist/mcp/index.d.ts.map +1 -1
  149. package/dist/mcp/index.js +1 -0
  150. package/dist/mcp/index.js.map +1 -1
  151. package/dist/mcp/org-directory.d.ts +1 -0
  152. package/dist/mcp/org-directory.d.ts.map +1 -1
  153. package/dist/mcp/org-directory.js +59 -11
  154. package/dist/mcp/org-directory.js.map +1 -1
  155. package/dist/mcp/server.d.ts.map +1 -1
  156. package/dist/mcp/server.js +9 -0
  157. package/dist/mcp/server.js.map +1 -1
  158. package/dist/mcp-client/config.d.ts +12 -0
  159. package/dist/mcp-client/config.d.ts.map +1 -1
  160. package/dist/mcp-client/config.js.map +1 -1
  161. package/dist/mcp-client/index.d.ts +1 -1
  162. package/dist/mcp-client/index.d.ts.map +1 -1
  163. package/dist/mcp-client/index.js +1 -1
  164. package/dist/mcp-client/index.js.map +1 -1
  165. package/dist/mcp-client/manager.d.ts +2 -0
  166. package/dist/mcp-client/manager.d.ts.map +1 -1
  167. package/dist/mcp-client/manager.js +201 -9
  168. package/dist/mcp-client/manager.js.map +1 -1
  169. package/dist/mcp-client/remote-store.d.ts +27 -0
  170. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  171. package/dist/mcp-client/remote-store.js +105 -0
  172. package/dist/mcp-client/remote-store.js.map +1 -1
  173. package/dist/mcp-client/routes.d.ts +1 -0
  174. package/dist/mcp-client/routes.d.ts.map +1 -1
  175. package/dist/mcp-client/routes.js +1 -0
  176. package/dist/mcp-client/routes.js.map +1 -1
  177. package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
  178. package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
  179. package/dist/provider-api/corpus-jobs.d.ts +8 -8
  180. package/dist/server/agent-chat-plugin.d.ts +2 -0
  181. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  182. package/dist/server/agent-chat-plugin.js +8 -0
  183. package/dist/server/agent-chat-plugin.js.map +1 -1
  184. package/dist/server/auth.d.ts +7 -0
  185. package/dist/server/auth.d.ts.map +1 -1
  186. package/dist/server/auth.js +1 -0
  187. package/dist/server/auth.js.map +1 -1
  188. package/dist/server/index.d.ts +1 -1
  189. package/dist/server/index.d.ts.map +1 -1
  190. package/dist/server/index.js +1 -1
  191. package/dist/server/index.js.map +1 -1
  192. package/dist/server/onboarding-html.d.ts +15 -0
  193. package/dist/server/onboarding-html.d.ts.map +1 -1
  194. package/dist/server/onboarding-html.js +73 -9
  195. package/dist/server/onboarding-html.js.map +1 -1
  196. package/dist/shared/mcp-embed-headers.d.ts +3 -1
  197. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  198. package/dist/shared/mcp-embed-headers.js +40 -3
  199. package/dist/shared/mcp-embed-headers.js.map +1 -1
  200. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  201. package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
  202. package/dist/sharing/actions/share-resource.d.ts +1 -1
  203. package/dist/sharing/schema.d.ts +1 -1
  204. package/dist/templates/default/AGENTS.md +4 -0
  205. package/dist/templates/default/package.json +2 -1
  206. package/dist/templates/headless/AGENTS.md +4 -0
  207. package/dist/templates/headless/package.json +1 -0
  208. package/dist/templates/workspace-root/AGENTS.md +4 -0
  209. package/dist/templates/workspace-root/README.md +4 -0
  210. package/dist/templates/workspace-root/package.json +1 -0
  211. package/dist/vite/client.d.ts.map +1 -1
  212. package/dist/vite/client.js +44 -0
  213. package/dist/vite/client.js.map +1 -1
  214. package/docs/content/skills-guide.md +14 -0
  215. package/docs/content/template-clips.md +7 -1
  216. package/package.json +6 -6
  217. package/src/templates/default/AGENTS.md +4 -0
  218. package/src/templates/default/package.json +2 -1
  219. package/src/templates/headless/AGENTS.md +4 -0
  220. package/src/templates/headless/package.json +1 -0
  221. package/src/templates/workspace-root/AGENTS.md +4 -0
  222. package/src/templates/workspace-root/README.md +4 -0
  223. package/src/templates/workspace-root/package.json +1 -0
@@ -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;
@@ -14,6 +14,8 @@ const INITIAL_TOOL_NAMES = [
14
14
  "edit-design",
15
15
  "generate-design",
16
16
  "present-design-variants",
17
+ "insert-asset",
18
+ "connect-assets-mcp",
17
19
  "apply-tweaks",
18
20
  "update-design",
19
21
  "list-files",
@@ -38,5 +40,7 @@ export default createAgentChatPlugin({
38
40
 
39
41
  Provider-specific Design actions are shortcuts, not limits. If a first-class action cannot express the exact GitHub endpoint, repository tree query, code search, issue or pull request query, request body, pagination mode, payload shape, metadata field, or API version needed, call provider-api-catalog and provider-api-docs as needed, then call provider-api-request against the real GitHub API. Use the raw provider API escape hatch instead of weakening the answer or claiming Design cannot do something the underlying GitHub API can do.
40
42
 
41
- Design's GitHub provider API uses the saved GITHUB_TOKEN secret when present. Never ask the user to paste tokens into chat. For large GitHub search results or repository scans, pass stageAs and pagination options to provider-api-request, then use query-staged-dataset to count, filter, group, or project the staged rows.`,
43
+ Design's GitHub provider API uses the saved GITHUB_TOKEN secret when present. Never ask the user to paste tokens into chat. For large GitHub search results or repository scans, pass stageAs and pagination options to provider-api-request, then use query-staged-dataset to count, filter, group, or project the staged rows.
44
+
45
+ For raster image generation, use available first-party Assets MCP tools such as generate-asset instead of placeholders or generic stock-image descriptions. When the Assets picker returns selectedAsset/chooseAsset/chooseImage context while a design is open, call insert-asset with the chosen asset URL/id, then refine placement with normal Design edit tools if needed. Preserve Assets assetId, runId, and URLs verbatim.`,
42
46
  });
@@ -0,0 +1,60 @@
1
+ import { defineNitroPlugin } from "@agent-native/core/server";
2
+ import { registerOnboardingStep } from "@agent-native/core/onboarding";
3
+ import {
4
+ isFirstPartyRemoteEndpointTrusted,
5
+ listRemoteServers,
6
+ } from "@agent-native/core/mcp-client";
7
+ import { fetchOrgApps } from "@agent-native/core/mcp";
8
+
9
+ const SERVER_NAME = "assets";
10
+
11
+ async function hasAssetsServer(orgId: string) {
12
+ const apps = await fetchOrgApps({ selfId: "design" });
13
+ const assets = apps.find((app) => app.id === "assets");
14
+ if (!assets?.url) return false;
15
+ const currentUrl = `${assets.url.replace(/\/+$/, "")}/_agent-native/mcp`;
16
+ const servers = await listRemoteServers("org", orgId);
17
+ for (const server of servers) {
18
+ if (server.name !== SERVER_NAME || server.firstParty !== true) continue;
19
+ if (server.url.replace(/\/+$/, "") !== currentUrl) continue;
20
+ const trust = await isFirstPartyRemoteEndpointTrusted(
21
+ orgId,
22
+ "assets",
23
+ server.url,
24
+ );
25
+ if (trust.ok) return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ async function ensureAssetsServer(ctx?: {
31
+ userEmail?: string;
32
+ orgId?: string | null;
33
+ }) {
34
+ if (!ctx?.userEmail || !ctx.orgId) return false;
35
+ return hasAssetsServer(ctx.orgId);
36
+ }
37
+
38
+ export default defineNitroPlugin(() => {
39
+ registerOnboardingStep({
40
+ id: "assets-mcp",
41
+ order: 70,
42
+ title: "Connect Assets",
43
+ description:
44
+ "Enable first-party Assets MCP tools so Design can generate and insert on-brand media.",
45
+ required: false,
46
+ methods: [
47
+ {
48
+ id: "agent",
49
+ kind: "agent-task",
50
+ label: "Connect Assets",
51
+ primary: true,
52
+ payload: {
53
+ prompt:
54
+ "Call connect-assets-mcp to connect the first-party Assets MCP server for this Design organization, then verify generate-asset is visible.",
55
+ },
56
+ },
57
+ ],
58
+ isComplete: ensureAssetsServer,
59
+ });
60
+ });
@@ -39,7 +39,10 @@ export default createAuthPlugin({
39
39
  googleSignInNotice: {
40
40
  host: "mail.agent-native.com",
41
41
  title: "Google may show a warning",
42
- body: "This hosted demo uses Agent-Native's shared Google app for Gmail access, so Google may ask you to confirm before continuing.",
42
+ body: [
43
+ "You'll see this screen because this demo uses Agent-Native's Google app, not a Google-reviewed public app.",
44
+ "It's safe to continue: click Advanced, then “Go to … (unsafe)” to finish signing in.",
45
+ ],
43
46
  continueLabel: "Continue to Google",
44
47
  cancelLabel: "Run locally",
45
48
  },