@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +38 -0
- package/corpus/core/docs/content/skills-guide.md +14 -0
- package/corpus/core/docs/content/template-clips.md +7 -1
- package/corpus/core/package.json +6 -6
- package/corpus/core/src/a2a/client.ts +7 -3
- package/corpus/core/src/agent/durable-background.ts +25 -12
- package/corpus/core/src/cli/create.ts +1 -1
- package/corpus/core/src/cli/skills.ts +450 -19
- package/corpus/core/src/client/api-path.ts +28 -0
- package/corpus/core/src/client/embed-auth.ts +26 -0
- package/corpus/core/src/client/mcp-app-host.ts +6 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
- package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
- package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
- package/corpus/core/src/embedding/agent.ts +6 -1
- package/corpus/core/src/embedding/bridge.ts +34 -1
- package/corpus/core/src/mcp/build-server.ts +38 -10
- package/corpus/core/src/mcp/embed-app.ts +120 -45
- package/corpus/core/src/mcp/index.ts +5 -0
- package/corpus/core/src/mcp/org-directory.ts +70 -11
- package/corpus/core/src/mcp/server.ts +14 -0
- package/corpus/core/src/mcp-client/config.ts +12 -0
- package/corpus/core/src/mcp-client/index.ts +2 -0
- package/corpus/core/src/mcp-client/manager.ts +268 -9
- package/corpus/core/src/mcp-client/remote-store.ts +154 -0
- package/corpus/core/src/mcp-client/routes.ts +2 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
- package/corpus/core/src/server/auth.ts +7 -0
- package/corpus/core/src/server/index.ts +1 -0
- package/corpus/core/src/server/onboarding-html.ts +95 -9
- package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
- package/corpus/core/src/templates/default/AGENTS.md +4 -0
- package/corpus/core/src/templates/default/package.json +2 -1
- package/corpus/core/src/templates/headless/AGENTS.md +4 -0
- package/corpus/core/src/templates/headless/package.json +1 -0
- package/corpus/core/src/templates/workspace-root/AGENTS.md +4 -0
- package/corpus/core/src/templates/workspace-root/README.md +4 -0
- package/corpus/core/src/templates/workspace-root/package.json +1 -0
- package/corpus/core/src/vite/client.ts +53 -0
- package/corpus/templates/analytics/actions/update-dashboard.ts +3 -4
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +144 -40
- package/corpus/templates/analytics/app/global.css +15 -13
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +9 -32
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +0 -26
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +0 -23
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/pivot.ts +65 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +3 -4
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-charts-now-fill-sparse-template-time-series-suppor.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-rows-now-auto-fit-panels-so-one-two-or-three-chart.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-traffic-tables-now-show-top-urls-and-shared-clips-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +5 -0
- package/corpus/templates/analytics/changelog/2026-06-24-first-party-template-dashboards-now-include-repeat-users-ret.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-retention-charts-now-break-out-1-day-and-7-day-return-rates-.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +256 -44
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +7 -2
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +35 -8
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +287 -42
- package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
- package/corpus/templates/assets/AGENTS.md +7 -5
- package/corpus/templates/assets/actions/generate-asset.ts +161 -0
- package/corpus/templates/assets/actions/list-assets.ts +4 -2
- package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
- package/corpus/templates/assets/app/routes/library.tsx +243 -40
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +1 -1
- package/corpus/templates/calendar/server/plugins/auth.ts +12 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +7 -7
- package/corpus/templates/clips/AGENTS.md +6 -2
- package/corpus/templates/clips/app/components/capture-install-options.tsx +20 -4
- package/corpus/templates/clips/app/components/player/player-controls.tsx +62 -32
- package/corpus/templates/clips/app/lib/capture-install-options.ts +39 -5
- package/corpus/templates/clips/app/routes/download.tsx +2 -2
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-06-24-inline-slack-playback-controls-no-longer-shift-when-adjustin.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-console-log-stream-all-le.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-fetch-xhr-network-request.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-24-the-chrome-extension-option-now-only-appears-on-supported-hosts.md +5 -0
- package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +13 -8
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/scripts/package.ts +15 -2
- package/corpus/templates/clips/chrome-extension/src/overlay.css +4 -25
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +1 -3
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +67 -24
- package/corpus/templates/design/AGENTS.md +10 -5
- package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
- package/corpus/templates/design/actions/insert-asset.ts +219 -0
- package/corpus/templates/design/actions/view-screen.ts +3 -1
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
- package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
- package/corpus/templates/mail/server/plugins/auth.ts +4 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +271 -197
- package/corpus/templates/plan/server/plan-content.ts +45 -2
- package/dist/a2a/client.d.ts +1 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +5 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/durable-background.d.ts +9 -4
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +24 -11
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/cli/create.js +1 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +361 -19
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/api-path.d.ts.map +1 -1
- package/dist/client/api-path.js +27 -0
- package/dist/client/api-path.js.map +1 -1
- package/dist/client/embed-auth.d.ts.map +1 -1
- package/dist/client/embed-auth.js +27 -0
- package/dist/client/embed-auth.js.map +1 -1
- package/dist/client/mcp-app-host.d.ts.map +1 -1
- package/dist/client/mcp-app-host.js +5 -0
- package/dist/client/mcp-app-host.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
- package/dist/client/resources/use-mcp-servers.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +3 -1
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/demo/fetch-interceptor.js +1 -1
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/embedding/agent.d.ts +1 -1
- package/dist/embedding/agent.d.ts.map +1 -1
- package/dist/embedding/agent.js +1 -1
- package/dist/embedding/agent.js.map +1 -1
- package/dist/embedding/bridge.d.ts.map +1 -1
- package/dist/embedding/bridge.js +27 -1
- package/dist/embedding/bridge.js.map +1 -1
- package/dist/extensions/schema.d.ts +2 -2
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +32 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +120 -45
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/org-directory.d.ts +1 -0
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +59 -11
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +9 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp-client/config.d.ts +12 -0
- package/dist/mcp-client/config.d.ts.map +1 -1
- package/dist/mcp-client/config.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/manager.d.ts +2 -0
- package/dist/mcp-client/manager.d.ts.map +1 -1
- package/dist/mcp-client/manager.js +201 -9
- package/dist/mcp-client/manager.js.map +1 -1
- package/dist/mcp-client/remote-store.d.ts +27 -0
- package/dist/mcp-client/remote-store.d.ts.map +1 -1
- package/dist/mcp-client/remote-store.js +105 -0
- package/dist/mcp-client/remote-store.js.map +1 -1
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +1 -0
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
- package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
- package/dist/provider-api/corpus-jobs.d.ts +8 -8
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +8 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts +7 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +1 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/onboarding-html.d.ts +15 -0
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +73 -9
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +3 -1
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +40 -3
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
- package/dist/sharing/actions/share-resource.d.ts +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/dist/templates/default/AGENTS.md +4 -0
- package/dist/templates/default/package.json +2 -1
- package/dist/templates/headless/AGENTS.md +4 -0
- package/dist/templates/headless/package.json +1 -0
- package/dist/templates/workspace-root/AGENTS.md +4 -0
- package/dist/templates/workspace-root/README.md +4 -0
- package/dist/templates/workspace-root/package.json +1 -0
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +44 -0
- package/dist/vite/client.js.map +1 -1
- package/docs/content/skills-guide.md +14 -0
- package/docs/content/template-clips.md +7 -1
- package/package.json +6 -6
- package/src/templates/default/AGENTS.md +4 -0
- package/src/templates/default/package.json +2 -1
- package/src/templates/headless/AGENTS.md +4 -0
- package/src/templates/headless/package.json +1 -0
- package/src/templates/workspace-root/AGENTS.md +4 -0
- package/src/templates/workspace-root/README.md +4 -0
- 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, "&")
|
|
54
|
+
.replace(/</g, "<")
|
|
55
|
+
.replace(/>/g, ">")
|
|
56
|
+
.replace(/"/g, """);
|
|
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:
|
|
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
|
},
|