@agent-native/core 0.72.2 → 0.72.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +16 -0
- package/corpus/core/package.json +6 -6
- package/corpus/core/src/a2a/client.ts +7 -3
- package/corpus/core/src/client/AgentPanel.tsx +12 -1
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +263 -13
- package/corpus/core/src/client/api-path.ts +28 -0
- package/corpus/core/src/client/embed-auth.ts +26 -0
- package/corpus/core/src/client/mcp-app-host.ts +6 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
- package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +16 -4
- package/corpus/core/src/client/use-chat-threads.ts +86 -5
- package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
- package/corpus/core/src/embedding/agent.ts +6 -1
- package/corpus/core/src/embedding/bridge.ts +34 -1
- package/corpus/core/src/mcp/build-server.ts +38 -10
- package/corpus/core/src/mcp/embed-app.ts +120 -45
- package/corpus/core/src/mcp/index.ts +5 -0
- package/corpus/core/src/mcp/org-directory.ts +70 -11
- package/corpus/core/src/mcp/server.ts +14 -0
- package/corpus/core/src/mcp-client/config.ts +12 -0
- package/corpus/core/src/mcp-client/index.ts +2 -0
- package/corpus/core/src/mcp-client/manager.ts +268 -9
- package/corpus/core/src/mcp-client/remote-store.ts +154 -0
- package/corpus/core/src/mcp-client/routes.ts +2 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
- package/corpus/core/src/server/index.ts +1 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
- package/corpus/core/src/vite/client.ts +53 -0
- package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
- package/corpus/templates/assets/AGENTS.md +7 -5
- package/corpus/templates/assets/actions/generate-asset.ts +161 -0
- package/corpus/templates/assets/actions/list-assets.ts +4 -2
- package/corpus/templates/assets/actions/navigate.ts +2 -1
- package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -2
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +27 -4
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +35 -5
- package/corpus/templates/assets/app/routes/_index.tsx +12 -0
- package/corpus/templates/assets/app/routes/chat.$threadId.tsx +1 -0
- package/corpus/templates/assets/app/routes/library.tsx +243 -40
- package/corpus/templates/chat/actions/navigate.ts +3 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +7 -2
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +27 -4
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +38 -6
- package/corpus/templates/chat/app/routes/_index.tsx +13 -0
- package/corpus/templates/chat/app/routes/chat.$threadId.tsx +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
- package/corpus/templates/design/AGENTS.md +10 -5
- package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
- package/corpus/templates/design/actions/insert-asset.ts +219 -0
- package/corpus/templates/design/actions/view-screen.ts +3 -1
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
- package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
- package/corpus/templates/dispatch/app/routes/chat.$threadId.tsx +26 -0
- package/dist/a2a/client.d.ts +1 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +5 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +6 -2
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +4 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts +22 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +215 -13
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/api-path.d.ts.map +1 -1
- package/dist/client/api-path.js +27 -0
- package/dist/client/api-path.js.map +1 -1
- package/dist/client/embed-auth.d.ts.map +1 -1
- package/dist/client/embed-auth.js +27 -0
- package/dist/client/embed-auth.js.map +1 -1
- package/dist/client/mcp-app-host.d.ts.map +1 -1
- package/dist/client/mcp-app-host.js +5 -0
- package/dist/client/mcp-app-host.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
- package/dist/client/resources/use-mcp-servers.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +3 -1
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +7 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +9 -5
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts +6 -0
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +79 -5
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/demo/fetch-interceptor.js +1 -1
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/embedding/agent.d.ts +1 -1
- package/dist/embedding/agent.d.ts.map +1 -1
- package/dist/embedding/agent.js +1 -1
- package/dist/embedding/agent.js.map +1 -1
- package/dist/embedding/bridge.d.ts.map +1 -1
- package/dist/embedding/bridge.js +27 -1
- package/dist/embedding/bridge.js.map +1 -1
- package/dist/extensions/schema.d.ts +2 -2
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +32 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +120 -45
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/org-directory.d.ts +1 -0
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +59 -11
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +9 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp-client/config.d.ts +12 -0
- package/dist/mcp-client/config.d.ts.map +1 -1
- package/dist/mcp-client/config.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/manager.d.ts +2 -0
- package/dist/mcp-client/manager.d.ts.map +1 -1
- package/dist/mcp-client/manager.js +201 -9
- package/dist/mcp-client/manager.js.map +1 -1
- package/dist/mcp-client/remote-store.d.ts +27 -0
- package/dist/mcp-client/remote-store.d.ts.map +1 -1
- package/dist/mcp-client/remote-store.js +105 -0
- package/dist/mcp-client/remote-store.js.map +1 -1
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +1 -0
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
- package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
- package/dist/provider-api/corpus-jobs.d.ts +8 -8
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +8 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +3 -1
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +40 -3
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
- package/dist/sharing/actions/share-resource.d.ts +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +44 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +6 -6
|
@@ -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(
|
|
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 =
|
|
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 =
|
|
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
|
-
?
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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 ||
|
|
30
|
+
routerPath(command.path || pathForCommand(command)),
|
|
26
31
|
onNavigate: (_command, path) => {
|
|
27
|
-
if (
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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, "&")
|
|
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;
|