@agent-native/core 0.85.6 → 0.85.7
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 +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/use-pinch-zoom.ts +76 -11
- package/corpus/core/src/collab/presence.ts +63 -3
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +245 -77
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +190 -13
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +16 -4
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +46 -3
- package/corpus/templates/design/AGENTS.md +33 -1
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +20 -73
- package/corpus/templates/design/actions/apply-motion-edit.ts +29 -2
- package/corpus/templates/design/actions/apply-visual-edit.ts +4 -2
- package/corpus/templates/design/actions/connect-localhost.ts +25 -18
- package/corpus/templates/design/actions/export-pdf.ts +8 -1
- package/corpus/templates/design/actions/export-zip.ts +10 -1
- package/corpus/templates/design/actions/get-motion-timeline.ts +26 -20
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +10 -0
- package/corpus/templates/design/actions/insert-asset.ts +190 -7
- package/corpus/templates/design/actions/open-visual-edit.ts +27 -5
- package/corpus/templates/design/actions/present-design-variants.ts +2 -2
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +18 -14
- package/corpus/templates/design/actions/run-design-audit.ts +7 -4
- package/corpus/templates/design/actions/write-local-file.ts +47 -22
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +221 -66
- package/corpus/templates/design/app/components/design/EditPanel.tsx +650 -237
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +526 -127
- package/corpus/templates/design/app/components/design/MotionDock.tsx +234 -46
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1497 -408
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +25 -2
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +640 -72
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +605 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +50 -26
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +16 -4
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +90 -103
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +111 -5
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -2
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +42 -2
- package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +11 -2
- package/corpus/templates/design/app/components/design/types.ts +16 -0
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +24 -6
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +15 -1
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +77 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +18 -0
- package/corpus/templates/design/app/lib/design-import.ts +95 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +4013 -838
- package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-inspector-fixes-mixed-selections-show-mixed-instead-of-wrong.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-keyframe-timeline-works-reliably-drag-keyframes-smoothly-pic.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-much-faster-editor-smooth-dragging-zooming-and-panel-updates.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-pen-tool-refinements-click-the-first-point-to-close-with-a-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-text-editing-enter-adds-a-line-break-international-ime-typin.md +6 -0
- package/corpus/templates/design/changelog/2026-07-03-undo-and-redo-are-dependable-across-agent-edits-screen-switc.md +6 -0
- package/corpus/templates/design/server/lib/design-export.ts +43 -1
- package/corpus/templates/design/shared/board-file.ts +25 -5
- package/corpus/templates/design/shared/canvas-math.ts +754 -9
- package/corpus/templates/design/shared/code-layer.ts +304 -26
- package/corpus/templates/design/shared/color-utils.ts +84 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +16 -7
- package/corpus/templates/design/shared/motion-compiler.ts +75 -31
- package/corpus/templates/design/shared/motion-timeline.ts +335 -0
- package/corpus/templates/design/shared/pen-path.ts +200 -23
- package/dist/client/use-pinch-zoom.d.ts.map +1 -1
- package/dist/client/use-pinch-zoom.js +76 -11
- package/dist/client/use-pinch-zoom.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/presence.d.ts.map +1 -1
- package/dist/collab/presence.js +54 -3
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
getRequestOrgId,
|
|
4
4
|
getRequestUserEmail,
|
|
5
5
|
} from "@agent-native/core/server/request-context";
|
|
6
|
-
import {
|
|
6
|
+
import { eq } from "drizzle-orm";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
@@ -159,20 +159,26 @@ export default defineAction({
|
|
|
159
159
|
status: "available" as const,
|
|
160
160
|
}));
|
|
161
161
|
|
|
162
|
+
// The id may already be taken by a DIFFERENT user (legacy ids were derived
|
|
163
|
+
// from devServerUrl + rootPath without user scoping, so two users on the
|
|
164
|
+
// same devcontainer image collide). Detect that up front and fail with a
|
|
165
|
+
// clear error instead of crashing on the primary-key insert below.
|
|
162
166
|
const existing = await db
|
|
163
167
|
.select({
|
|
164
|
-
|
|
168
|
+
ownerEmail: schema.designLocalhostConnections.ownerEmail,
|
|
165
169
|
bridgeToken: schema.designLocalhostConnections.bridgeToken,
|
|
166
170
|
})
|
|
167
171
|
.from(schema.designLocalhostConnections)
|
|
168
|
-
.where(
|
|
169
|
-
and(
|
|
170
|
-
eq(schema.designLocalhostConnections.id, id),
|
|
171
|
-
eq(schema.designLocalhostConnections.ownerEmail, ownerEmail),
|
|
172
|
-
),
|
|
173
|
-
)
|
|
172
|
+
.where(eq(schema.designLocalhostConnections.id, id))
|
|
174
173
|
.limit(1);
|
|
175
174
|
|
|
175
|
+
if (existing[0] && existing[0].ownerEmail !== ownerEmail) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Connection id "${id}" already belongs to another user. ` +
|
|
178
|
+
"Omit id so a per-user connection id is derived instead.",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
176
182
|
const nextBridgeToken = args.bridgeToken?.trim() || undefined;
|
|
177
183
|
const values = {
|
|
178
184
|
id,
|
|
@@ -191,17 +197,18 @@ export default defineAction({
|
|
|
191
197
|
updatedAt: now,
|
|
192
198
|
};
|
|
193
199
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
// Atomic upsert keyed on the id primary key — no check-then-insert race.
|
|
201
|
+
// setWhere keeps a concurrent insert by another user (TOCTOU on the
|
|
202
|
+
// ownership check above) from being overwritten: on a cross-user conflict
|
|
203
|
+
// the update filters to a no-op instead of hijacking the other row.
|
|
204
|
+
await db
|
|
205
|
+
.insert(schema.designLocalhostConnections)
|
|
206
|
+
.values({ ...values, createdAt: now })
|
|
207
|
+
.onConflictDoUpdate({
|
|
208
|
+
target: schema.designLocalhostConnections.id,
|
|
209
|
+
set: values,
|
|
210
|
+
setWhere: eq(schema.designLocalhostConnections.ownerEmail, ownerEmail),
|
|
203
211
|
});
|
|
204
|
-
}
|
|
205
212
|
|
|
206
213
|
return {
|
|
207
214
|
id,
|
|
@@ -4,6 +4,7 @@ import { eq } from "drizzle-orm";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import { injectHiddenLayerExportStyle } from "../server/lib/design-export.js";
|
|
7
8
|
import { isBoardFile } from "../shared/board-file.js";
|
|
8
9
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
9
10
|
|
|
@@ -40,7 +41,13 @@ export default defineAction({
|
|
|
40
41
|
id: f.id,
|
|
41
42
|
filename: f.filename,
|
|
42
43
|
fileType: f.fileType,
|
|
43
|
-
|
|
44
|
+
// Layers toggled hidden in the editor are only suppressed by the live
|
|
45
|
+
// editor bridge; inject the same display:none rule so the client-side
|
|
46
|
+
// PDF render (html2canvas over this HTML) doesn't reveal them.
|
|
47
|
+
content:
|
|
48
|
+
f.fileType === "html" && f.content
|
|
49
|
+
? injectHiddenLayerExportStyle(f.content)
|
|
50
|
+
: f.content,
|
|
44
51
|
})),
|
|
45
52
|
exportInfo: {
|
|
46
53
|
format: "pdf",
|
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
7
|
import {
|
|
8
8
|
exportFilename,
|
|
9
|
+
injectHiddenLayerExportStyle,
|
|
9
10
|
trySaveExportFile,
|
|
10
11
|
} from "../server/lib/design-export.js";
|
|
11
12
|
import { isBoardFile } from "../shared/board-file.js";
|
|
@@ -71,7 +72,15 @@ export default defineAction({
|
|
|
71
72
|
file.filename,
|
|
72
73
|
`design-file-${index + 1}.txt`,
|
|
73
74
|
);
|
|
74
|
-
|
|
75
|
+
// Layers toggled hidden in the editor are only suppressed by the live
|
|
76
|
+
// editor bridge; inject the same display:none rule into exported HTML
|
|
77
|
+
// files so opening them directly from the zip doesn't reveal layers
|
|
78
|
+
// the user hid in the editor.
|
|
79
|
+
const content =
|
|
80
|
+
file.fileType === "html"
|
|
81
|
+
? injectHiddenLayerExportStyle(file.content ?? "")
|
|
82
|
+
: (file.content ?? "");
|
|
83
|
+
zip.file(filename, content);
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
// Add design data if present
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import { getText, hasCollabState } from "@agent-native/core/collab";
|
|
3
2
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
3
|
import { and, desc, eq } from "drizzle-orm";
|
|
5
4
|
import { z } from "zod";
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
extractManagedMotionCss,
|
|
11
10
|
hashCss,
|
|
12
11
|
parse,
|
|
12
|
+
parseFirstAnimationDurationMs,
|
|
13
13
|
} from "../shared/motion-compiler.js";
|
|
14
14
|
import type { MotionTrack } from "../shared/motion-timeline.js";
|
|
15
15
|
|
|
@@ -30,21 +30,6 @@ interface TimelineResult {
|
|
|
30
30
|
updatedAt: string | null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
async function liveFileContent(
|
|
34
|
-
fileId: string,
|
|
35
|
-
storedContent: string,
|
|
36
|
-
): Promise<string> {
|
|
37
|
-
try {
|
|
38
|
-
if (await hasCollabState(fileId)) {
|
|
39
|
-
const live = await getText(fileId, "content");
|
|
40
|
-
if (typeof live === "string") return live;
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
// Best-effort; SQL is the fallback.
|
|
44
|
-
}
|
|
45
|
-
return storedContent;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
33
|
function parseTrackJson(raw: string | null): MotionTrack[] {
|
|
49
34
|
try {
|
|
50
35
|
const parsed: unknown = JSON.parse(raw ?? "[]");
|
|
@@ -57,7 +42,12 @@ function parseTrackJson(raw: string | null): MotionTrack[] {
|
|
|
57
42
|
async function readManagedCssForSource(args: {
|
|
58
43
|
designId: string;
|
|
59
44
|
sourceRef?: string;
|
|
60
|
-
}): Promise<{
|
|
45
|
+
}): Promise<{
|
|
46
|
+
css: string;
|
|
47
|
+
hash: string;
|
|
48
|
+
tracks: MotionTrack[];
|
|
49
|
+
durationMs: number | null;
|
|
50
|
+
} | null> {
|
|
61
51
|
if (!args.sourceRef) return null;
|
|
62
52
|
|
|
63
53
|
const db = getDb();
|
|
@@ -77,7 +67,13 @@ async function readManagedCssForSource(args: {
|
|
|
77
67
|
|
|
78
68
|
if (!file) return null;
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
// Read the managed block from the SQL content, NOT a live collab snapshot:
|
|
71
|
+
// apply-motion-edit persists the managed <style> block to SQL only, so a
|
|
72
|
+
// live collab session's text lags the freshest motion CSS. Comparing the
|
|
73
|
+
// stored compiledHash against stale collab CSS would flag phantom
|
|
74
|
+
// "stored-css-drift" right after every save and replace fresh tracks with
|
|
75
|
+
// stale CSS-parsed ones. SQL is the motion block's source of truth.
|
|
76
|
+
const content = file.content ?? "";
|
|
81
77
|
const css = extractManagedMotionCss(content);
|
|
82
78
|
if (!css) return null;
|
|
83
79
|
|
|
@@ -88,7 +84,12 @@ async function readManagedCssForSource(args: {
|
|
|
88
84
|
);
|
|
89
85
|
if (tracks.length === 0) return null;
|
|
90
86
|
|
|
91
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
css,
|
|
89
|
+
hash: hashCss(css),
|
|
90
|
+
tracks,
|
|
91
|
+
durationMs: parseFirstAnimationDurationMs(css),
|
|
92
|
+
};
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
export default defineAction({
|
|
@@ -179,7 +180,9 @@ export default defineAction({
|
|
|
179
180
|
sourceRef: sourceRef ?? null,
|
|
180
181
|
filePath: null,
|
|
181
182
|
tracks: managedCss.tracks,
|
|
182
|
-
|
|
183
|
+
// Recover the compiled animation-duration instead of inventing a
|
|
184
|
+
// default the next save would silently persist.
|
|
185
|
+
durationMs: managedCss.durationMs ?? 1000,
|
|
183
186
|
defaultEase: "ease",
|
|
184
187
|
compiledHash: managedCss.hash,
|
|
185
188
|
cssHash: managedCss.hash,
|
|
@@ -191,6 +194,9 @@ export default defineAction({
|
|
|
191
194
|
timelines[0] = {
|
|
192
195
|
...first,
|
|
193
196
|
tracks: managedCss.tracks,
|
|
197
|
+
// In the drift case the CSS is the runtime truth — surface its
|
|
198
|
+
// compiled duration alongside its recovered tracks.
|
|
199
|
+
durationMs: managedCss.durationMs ?? first.durationMs,
|
|
194
200
|
cssHash: managedCss.hash,
|
|
195
201
|
source: "stored-css-drift",
|
|
196
202
|
};
|
|
@@ -19,6 +19,16 @@ export default defineAction({
|
|
|
19
19
|
"for a specific design + localhost connection. The grant scopes writes to the " +
|
|
20
20
|
"connection's rootPath and expires after 8 hours. Requires editor access on the design. " +
|
|
21
21
|
"The LocalhostWriteConsentDialog calls this after the user clicks 'Allow writes'.",
|
|
22
|
+
// This action mints/returns the real bridgeToken that unlocks the local
|
|
23
|
+
// bridge's unrestricted /read-file, /write-file, and /apply-edit. It must
|
|
24
|
+
// only ever be triggered by a human clicking "Allow writes" in
|
|
25
|
+
// LocalhostWriteConsentDialog — never by the agent itself, or an agent
|
|
26
|
+
// could self-grant local filesystem write/read access and bypass the
|
|
27
|
+
// human consent model entirely. `agentTool: false` hides it from every
|
|
28
|
+
// agent tool surface (in-app assistant, MCP, A2A) while keeping it
|
|
29
|
+
// callable from the frontend via `callAction` / `useActionMutation` and
|
|
30
|
+
// the raw `/_agent-native/actions/grant-localhost-write-consent` route.
|
|
31
|
+
agentTool: false,
|
|
22
32
|
schema: z.object({
|
|
23
33
|
designId: z.string().describe("Design ID."),
|
|
24
34
|
connectionId: z
|
|
@@ -41,6 +41,27 @@ const schemaInput = z.object({
|
|
|
41
41
|
.string()
|
|
42
42
|
.optional()
|
|
43
43
|
.describe("Design editor selection owner token from current screen state."),
|
|
44
|
+
targetNodeId: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe(
|
|
48
|
+
"data-agent-native-node-id of an existing element to target. Required " +
|
|
49
|
+
'for mode "replace-src" and "background-fill". Ignored for the ' +
|
|
50
|
+
'default "figure" mode.',
|
|
51
|
+
),
|
|
52
|
+
mode: z
|
|
53
|
+
.enum(["figure", "replace-src", "background-fill"])
|
|
54
|
+
.optional()
|
|
55
|
+
.default("figure")
|
|
56
|
+
.describe(
|
|
57
|
+
'How to place the asset. "figure" (default) appends a new figure/section ' +
|
|
58
|
+
"at the end of the file, styled with the design's own tokens — use " +
|
|
59
|
+
'this when there is no existing element to target. "replace-src" sets ' +
|
|
60
|
+
"the src of the <img>/<video> at targetNodeId (e.g. filling a hero " +
|
|
61
|
+
'image placeholder). "background-fill" sets a background-image style ' +
|
|
62
|
+
"on the element at targetNodeId (e.g. filling a hero/section background). " +
|
|
63
|
+
'Both "replace-src" and "background-fill" require targetNodeId.',
|
|
64
|
+
),
|
|
44
65
|
});
|
|
45
66
|
|
|
46
67
|
function stringFromState(state: unknown, key: string): string | undefined {
|
|
@@ -57,6 +78,26 @@ function escapeHtml(value: string): string {
|
|
|
57
78
|
.replace(/"/g, """);
|
|
58
79
|
}
|
|
59
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Prepare a URL for embedding inside a single-quoted CSS `url('...')` value
|
|
83
|
+
* that itself lives inside an HTML attribute. A literal `'` in the URL would
|
|
84
|
+
* otherwise close the CSS string early — and depending on whether the
|
|
85
|
+
* surrounding HTML attribute is single- or double-quoted, everything after it
|
|
86
|
+
* becomes live CSS or, worse, live HTML/JS in the script-enabled preview
|
|
87
|
+
* iframe. Percent-encoding the quote/backslash characters that are meaningful
|
|
88
|
+
* to the CSS string-literal grammar keeps the URL functionally identical
|
|
89
|
+
* (browsers resolve %27/%22/%5C the same as the raw characters) while making
|
|
90
|
+
* it impossible to break out of the `url('...')` string. HTML-escape on top
|
|
91
|
+
* so the value is also safe as an HTML attribute (covers the "..." case).
|
|
92
|
+
*/
|
|
93
|
+
function cssUrlValue(value: string): string {
|
|
94
|
+
const cssSafe = value
|
|
95
|
+
.replace(/\\/g, "%5C")
|
|
96
|
+
.replace(/'/g, "%27")
|
|
97
|
+
.replace(/"/g, "%22");
|
|
98
|
+
return escapeHtml(cssSafe);
|
|
99
|
+
}
|
|
100
|
+
|
|
60
101
|
function createInsertedNodeId(prefix: string): string {
|
|
61
102
|
const random =
|
|
62
103
|
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
@@ -88,13 +129,13 @@ function appendAssetMarkup(
|
|
|
88
129
|
: "";
|
|
89
130
|
const media =
|
|
90
131
|
args.mediaType === "video"
|
|
91
|
-
? `<video src="${escapedUrl}" controls class="w-full rounded-
|
|
92
|
-
: `<img src="${escapedUrl}" alt="${escapeHtml(args.altText?.trim() || label)}" class="w-full rounded-
|
|
132
|
+
? `<video src="${escapedUrl}" controls class="w-full rounded-[var(--radius,0.75rem)] object-cover"></video>`
|
|
133
|
+
: `<img src="${escapedUrl}" alt="${escapeHtml(args.altText?.trim() || label)}" class="w-full rounded-[var(--radius,0.75rem)] object-cover" />`;
|
|
93
134
|
const snippet = `
|
|
94
135
|
<section class="mx-auto my-8 max-w-5xl px-4" data-agent-native-asset data-agent-native-node-id="${escapeHtml(nodeId)}" data-agent-native-layer-name="${escapedLabel}"${assetIdAttr}>
|
|
95
|
-
<figure class="overflow-hidden rounded-
|
|
136
|
+
<figure class="overflow-hidden rounded-[var(--radius,1rem)] border border-[var(--color-border,rgba(0,0,0,0.1))] bg-[var(--color-surface,#fff)] shadow-sm">
|
|
96
137
|
${media}
|
|
97
|
-
<figcaption class="px-4 py-3 text-sm text-
|
|
138
|
+
<figcaption class="px-4 py-3 text-sm text-[var(--color-text-muted,#64748b)]">${escapedLabel}</figcaption>
|
|
98
139
|
</figure>
|
|
99
140
|
</section>`;
|
|
100
141
|
|
|
@@ -105,6 +146,119 @@ function appendAssetMarkup(
|
|
|
105
146
|
);
|
|
106
147
|
}
|
|
107
148
|
|
|
149
|
+
/** Find the raw tag string for the element carrying the given node id. */
|
|
150
|
+
function findTagByNodeId(html: string, nodeId: string): string | null {
|
|
151
|
+
const escapedId = nodeId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
152
|
+
const pattern = new RegExp(
|
|
153
|
+
`<[a-zA-Z][a-zA-Z0-9:-]*\\b[^>]*data-agent-native-node-id\\s*=\\s*(?:"${escapedId}"|'${escapedId}')[^>]*>`,
|
|
154
|
+
);
|
|
155
|
+
const match = html.match(pattern);
|
|
156
|
+
return match ? match[0] : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Replace the src attribute of the <img>/<video> tag at targetNodeId. */
|
|
160
|
+
function replaceSrcAtNode(
|
|
161
|
+
html: string,
|
|
162
|
+
args: z.infer<typeof schemaInput>,
|
|
163
|
+
): string {
|
|
164
|
+
const nodeId = args.targetNodeId!;
|
|
165
|
+
const tag = findTagByNodeId(html, nodeId);
|
|
166
|
+
if (!tag) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`No element found with data-agent-native-node-id="${nodeId}".`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const escapedUrl = escapeHtml(args.assetUrl);
|
|
172
|
+
let nextTag: string;
|
|
173
|
+
if (/\bsrc\s*=\s*(?:"[^"]*"|'[^']*')/i.test(tag)) {
|
|
174
|
+
nextTag = tag.replace(
|
|
175
|
+
/\bsrc\s*=\s*(?:"[^"]*"|'[^']*')/i,
|
|
176
|
+
`src="${escapedUrl}"`,
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
nextTag = tag.replace(/\/?>\s*$/, ` src="${escapedUrl}">`);
|
|
180
|
+
}
|
|
181
|
+
if (args.altText?.trim()) {
|
|
182
|
+
const escapedAlt = escapeHtml(args.altText.trim());
|
|
183
|
+
nextTag = /\balt\s*=\s*(?:"[^"]*"|'[^']*')/i.test(nextTag)
|
|
184
|
+
? nextTag.replace(
|
|
185
|
+
/\balt\s*=\s*(?:"[^"]*"|'[^']*')/i,
|
|
186
|
+
`alt="${escapedAlt}"`,
|
|
187
|
+
)
|
|
188
|
+
: nextTag.replace(/\/?>\s*$/, ` alt="${escapedAlt}">`);
|
|
189
|
+
}
|
|
190
|
+
if (args.assetId?.trim()) {
|
|
191
|
+
const escapedAssetId = escapeHtml(args.assetId.trim());
|
|
192
|
+
nextTag = /\bdata-asset-id\s*=/.test(nextTag)
|
|
193
|
+
? nextTag.replace(
|
|
194
|
+
/\bdata-asset-id\s*=\s*(?:"[^"]*"|'[^']*')/i,
|
|
195
|
+
`data-asset-id="${escapedAssetId}"`,
|
|
196
|
+
)
|
|
197
|
+
: nextTag.replace(/\/?>\s*$/, ` data-asset-id="${escapedAssetId}">`);
|
|
198
|
+
}
|
|
199
|
+
return html.replace(tag, nextTag);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Set a background-image inline style on the element at targetNodeId. */
|
|
203
|
+
function backgroundFillAtNode(
|
|
204
|
+
html: string,
|
|
205
|
+
args: z.infer<typeof schemaInput>,
|
|
206
|
+
): string {
|
|
207
|
+
const nodeId = args.targetNodeId!;
|
|
208
|
+
const tag = findTagByNodeId(html, nodeId);
|
|
209
|
+
if (!tag) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`No element found with data-agent-native-node-id="${nodeId}".`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const cssUrl = cssUrlValue(args.assetUrl);
|
|
215
|
+
const bgDeclaration = `background-image: url('${cssUrl}'); background-size: cover; background-position: center;`;
|
|
216
|
+
let nextTag: string;
|
|
217
|
+
if (/\bstyle\s*=\s*"/.test(tag)) {
|
|
218
|
+
nextTag = tag.replace(
|
|
219
|
+
/\bstyle\s*=\s*"([^"]*)"/i,
|
|
220
|
+
(_match, existing: string) => {
|
|
221
|
+
const trimmed = existing.trim();
|
|
222
|
+
const withoutBgImage = trimmed.replace(
|
|
223
|
+
/background-image\s*:[^;]*;?\s*/gi,
|
|
224
|
+
"",
|
|
225
|
+
);
|
|
226
|
+
const joined = withoutBgImage
|
|
227
|
+
? `${withoutBgImage.replace(/;\s*$/, "")}; ${bgDeclaration}`
|
|
228
|
+
: bgDeclaration;
|
|
229
|
+
return `style="${joined}"`;
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
} else if (/\bstyle\s*=\s*'/.test(tag)) {
|
|
233
|
+
nextTag = tag.replace(
|
|
234
|
+
/\bstyle\s*=\s*'([^']*)'/i,
|
|
235
|
+
(_match, existing: string) => {
|
|
236
|
+
const trimmed = existing.trim();
|
|
237
|
+
const withoutBgImage = trimmed.replace(
|
|
238
|
+
/background-image\s*:[^;]*;?\s*/gi,
|
|
239
|
+
"",
|
|
240
|
+
);
|
|
241
|
+
const joined = withoutBgImage
|
|
242
|
+
? `${withoutBgImage.replace(/;\s*$/, "")}; ${bgDeclaration}`
|
|
243
|
+
: bgDeclaration;
|
|
244
|
+
return `style='${joined}'`;
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
} else {
|
|
248
|
+
nextTag = tag.replace(/\/?>\s*$/, ` style="${bgDeclaration}">`);
|
|
249
|
+
}
|
|
250
|
+
if (args.assetId?.trim()) {
|
|
251
|
+
const escapedAssetId = escapeHtml(args.assetId.trim());
|
|
252
|
+
nextTag = /\bdata-asset-id\s*=/.test(nextTag)
|
|
253
|
+
? nextTag.replace(
|
|
254
|
+
/\bdata-asset-id\s*=\s*(?:"[^"]*"|'[^']*')/i,
|
|
255
|
+
`data-asset-id="${escapedAssetId}"`,
|
|
256
|
+
)
|
|
257
|
+
: nextTag.replace(/\/?>\s*$/, ` data-asset-id="${escapedAssetId}">`);
|
|
258
|
+
}
|
|
259
|
+
return html.replace(tag, nextTag);
|
|
260
|
+
}
|
|
261
|
+
|
|
108
262
|
async function resolveTarget(args: z.infer<typeof schemaInput>) {
|
|
109
263
|
const [navigation, selection] = await Promise.all([
|
|
110
264
|
readAppStateForCurrentTab("navigation").catch(() => null),
|
|
@@ -149,7 +303,18 @@ function isHtmlFile(file: {
|
|
|
149
303
|
|
|
150
304
|
export default defineAction({
|
|
151
305
|
description:
|
|
152
|
-
"Insert a chosen Assets image or video URL into a Design file. Use this
|
|
306
|
+
"Insert a chosen Assets image or video URL into a Design file. Use this " +
|
|
307
|
+
"after the Assets picker returns chooseAsset/chooseImage context; pass " +
|
|
308
|
+
"designId/fileId directly when known, or pass ownerId from " +
|
|
309
|
+
"view-screen.designSelection when targeting the current editor selection. " +
|
|
310
|
+
'Default `mode: "figure"` appends a new tokened figure/section at the end ' +
|
|
311
|
+
"of the file — use this when there is no existing element to fill. Use " +
|
|
312
|
+
'`mode: "replace-src"` with `targetNodeId` to set the src of an existing ' +
|
|
313
|
+
"<img>/<video> (e.g. filling a hero image placeholder), or " +
|
|
314
|
+
'`mode: "background-fill"` with `targetNodeId` to set a background-image ' +
|
|
315
|
+
"style on an existing element (e.g. filling a hero/section background). " +
|
|
316
|
+
"Both of those require targetNodeId (a data-agent-native-node-id) from " +
|
|
317
|
+
"get-code-layer-projection or the current selection.",
|
|
153
318
|
schema: schemaInput,
|
|
154
319
|
publicAgent: { expose: true, readOnly: false, requiresAuth: true },
|
|
155
320
|
run: async (args) => {
|
|
@@ -200,8 +365,25 @@ export default defineAction({
|
|
|
200
365
|
// Collab read is best-effort; fall back to stored content.
|
|
201
366
|
}
|
|
202
367
|
|
|
203
|
-
const
|
|
204
|
-
|
|
368
|
+
const mode = args.mode ?? "figure";
|
|
369
|
+
let content: string;
|
|
370
|
+
let insertedNodeId: string;
|
|
371
|
+
if (mode === "replace-src") {
|
|
372
|
+
if (!args.targetNodeId) {
|
|
373
|
+
throw new Error('targetNodeId is required for mode "replace-src".');
|
|
374
|
+
}
|
|
375
|
+
insertedNodeId = args.targetNodeId;
|
|
376
|
+
content = replaceSrcAtNode(base, args);
|
|
377
|
+
} else if (mode === "background-fill") {
|
|
378
|
+
if (!args.targetNodeId) {
|
|
379
|
+
throw new Error('targetNodeId is required for mode "background-fill".');
|
|
380
|
+
}
|
|
381
|
+
insertedNodeId = args.targetNodeId;
|
|
382
|
+
content = backgroundFillAtNode(base, args);
|
|
383
|
+
} else {
|
|
384
|
+
insertedNodeId = createInsertedNodeId("asset");
|
|
385
|
+
content = appendAssetMarkup(base, args, insertedNodeId);
|
|
386
|
+
}
|
|
205
387
|
const now = new Date().toISOString();
|
|
206
388
|
await db
|
|
207
389
|
.update(schema.designFiles)
|
|
@@ -223,6 +405,7 @@ export default defineAction({
|
|
|
223
405
|
fileId: file.id,
|
|
224
406
|
filename: file.filename,
|
|
225
407
|
inserted: true,
|
|
408
|
+
mode,
|
|
226
409
|
insertedNodeId,
|
|
227
410
|
insertedSelector: `[data-agent-native-node-id="${insertedNodeId}"]`,
|
|
228
411
|
assetId: args.assetId ?? null,
|
|
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import { defineAction, embedApp } from "@agent-native/core";
|
|
4
4
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
5
5
|
import { buildDeepLink } from "@agent-native/core/server";
|
|
6
|
+
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
6
7
|
import { eq } from "drizzle-orm";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
|
|
@@ -88,10 +89,23 @@ function isLoopbackUrl(value: string): boolean {
|
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Derive a stable per-user connection id for a devServerUrl + rootPath pair.
|
|
94
|
+
*
|
|
95
|
+
* The owner email is embedded in the hash so two users with an identical
|
|
96
|
+
* devServerUrl + rootPath (common with devcontainers/codespaces images) derive
|
|
97
|
+
* DIFFERENT ids and never collide on the shared primary key. Connections
|
|
98
|
+
* created before user scoping keep their old ids; those users simply
|
|
99
|
+
* reconnect fresh under the new per-user id.
|
|
100
|
+
*/
|
|
101
|
+
function stableConnectionId(
|
|
102
|
+
devServerUrl: string,
|
|
103
|
+
rootPath: string | undefined,
|
|
104
|
+
ownerEmail: string,
|
|
105
|
+
) {
|
|
92
106
|
const hash = crypto
|
|
93
107
|
.createHash("sha256")
|
|
94
|
-
.update(`${devServerUrl}\n${rootPath ?? ""}`)
|
|
108
|
+
.update(`${ownerEmail}\n${devServerUrl}\n${rootPath ?? ""}`)
|
|
95
109
|
.digest("base64url")
|
|
96
110
|
.slice(0, 16);
|
|
97
111
|
return `localhost_${hash}`;
|
|
@@ -147,7 +161,7 @@ export default defineAction({
|
|
|
147
161
|
.string()
|
|
148
162
|
.optional()
|
|
149
163
|
.describe(
|
|
150
|
-
"Existing localhost connection. Omit to reuse a stable connection for devServerUrl + rootPath.",
|
|
164
|
+
"Existing localhost connection. Omit to reuse a stable per-user connection for devServerUrl + rootPath.",
|
|
151
165
|
),
|
|
152
166
|
title: z
|
|
153
167
|
.string()
|
|
@@ -240,8 +254,16 @@ export default defineAction({
|
|
|
240
254
|
}) ?? [],
|
|
241
255
|
generatedAt: new Date().toISOString(),
|
|
242
256
|
};
|
|
243
|
-
|
|
244
|
-
|
|
257
|
+
let connectionId = args.connectionId;
|
|
258
|
+
if (!connectionId) {
|
|
259
|
+
const ownerEmail = getRequestUserEmail();
|
|
260
|
+
if (!ownerEmail) throw new Error("no authenticated user");
|
|
261
|
+
connectionId = stableConnectionId(
|
|
262
|
+
devServerUrl,
|
|
263
|
+
args.rootPath,
|
|
264
|
+
ownerEmail,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
245
267
|
|
|
246
268
|
const connection = await connectLocalhostAction.run({
|
|
247
269
|
id: connectionId,
|
|
@@ -250,7 +250,7 @@ function colorForVariant(
|
|
|
250
250
|
) {
|
|
251
251
|
const provided = variant.accentColor?.trim();
|
|
252
252
|
if (provided) return provided;
|
|
253
|
-
return ["#
|
|
253
|
+
return ["#f59e0b", "#06b6d4", "#10b981", "#f43f5e", "#d97706"][index % 5]!;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
function fallbackVariantContent(
|
|
@@ -307,7 +307,7 @@ function fallbackVariantContent(
|
|
|
307
307
|
<style>
|
|
308
308
|
:root { color-scheme: dark; --accent: ${accent}; --bg: #080a0f; --panel: rgba(18, 22, 33, 0.82); --line: rgba(255,255,255,.11); --muted: #94a3b8; }
|
|
309
309
|
* { box-sizing: border-box; }
|
|
310
|
-
body { margin: 0; width: ${screenWidth}px; min-height: ${screenHeight}px; overflow: hidden; font-family:
|
|
310
|
+
body { margin: 0; width: ${screenWidth}px; min-height: ${screenHeight}px; overflow: hidden; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #f8fafc; background:
|
|
311
311
|
radial-gradient(circle at 18% 8%, color-mix(in srgb, var(--accent) 42%, transparent), transparent 30%),
|
|
312
312
|
linear-gradient(140deg, #05070b 0%, #111827 48%, #05070b 100%); }
|
|
313
313
|
.shell { width: ${screenWidth}px; min-height: ${screenHeight}px; padding: ${compact ? "18" : "34"}px; display: grid; grid-template-columns: ${compact ? "1fr" : tablet ? "220px 1fr" : "258px 1fr 304px"}; gap: ${compact ? "14" : "22"}px; }
|
|
@@ -25,24 +25,28 @@ export default defineAction({
|
|
|
25
25
|
|
|
26
26
|
const db = getDb();
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
(
|
|
28
|
+
// The delete result's rows-affected shape varies by driver (and some
|
|
29
|
+
// drivers do not report it at all), so SELECT the scoped grant first and
|
|
30
|
+
// derive `revoked` from its existence — portable across dialects without
|
|
31
|
+
// assuming RETURNING support.
|
|
32
|
+
const scope = and(
|
|
33
|
+
eq(schema.designLocalhostWriteGrants.designId, designId),
|
|
34
|
+
eq(schema.designLocalhostWriteGrants.connectionId, connectionId),
|
|
35
|
+
eq(schema.designLocalhostWriteGrants.ownerEmail, ownerEmail),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const [grant] = await db
|
|
39
|
+
.select({ id: schema.designLocalhostWriteGrants.id })
|
|
40
|
+
.from(schema.designLocalhostWriteGrants)
|
|
41
|
+
.where(scope)
|
|
42
|
+
.limit(1);
|
|
43
|
+
|
|
44
|
+
await db.delete(schema.designLocalhostWriteGrants).where(scope);
|
|
41
45
|
|
|
42
46
|
return {
|
|
43
47
|
designId,
|
|
44
48
|
connectionId,
|
|
45
|
-
revoked:
|
|
49
|
+
revoked: Boolean(grant),
|
|
46
50
|
};
|
|
47
51
|
},
|
|
48
52
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* run-design-audit — read-only a11y audit over a design's rendered HTML/DOM.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Flags low-opacity text color classes as a contrast hint (real contrast
|
|
5
|
+
* ratios require a DOM/CSS cascade resolver that isn't available server-side,
|
|
6
|
+
* so this is not a computed ratio check), plus tap-target sizes, missing
|
|
7
|
+
* alt/labels, focus visibility, and reduced-motion concerns, all by static
|
|
8
|
+
* analysis of the stored HTML. Does NOT perform writes. Results are returned
|
|
9
|
+
* as `A11yFinding[]` and may be persisted by the caller via
|
|
10
|
+
* `create-design-review-snapshot`.
|
|
8
11
|
*
|
|
9
12
|
* See DESIGN-STUDIO-PLAN.md §6.5 + §7 (Review surface).
|
|
10
13
|
*/
|