@agent-native/core 0.80.9 → 0.80.11
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 +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -15
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/skills.ts +45 -24
- package/corpus/core/src/client/AssistantChat.tsx +123 -26
- package/corpus/core/src/client/sse-event-processor.ts +9 -0
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
- package/corpus/templates/assets/AGENTS.md +4 -0
- package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
- package/corpus/templates/assets/actions/generate-image.ts +118 -67
- package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
- package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/assets/app/i18n-data.ts +61 -0
- package/corpus/templates/assets/app/routes/_index.tsx +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +145 -38
- package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +135 -11
- package/corpus/templates/assets/shared/api.ts +4 -0
- package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +441 -0
- package/corpus/templates/design/actions/apply-design-state.ts +213 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +413 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/delete-design.ts +20 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/get-component-details.ts +251 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +277 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +90 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +242 -0
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +260 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +187 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +436 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1550 -82
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +649 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1788 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +114 -33
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1071 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1053 -99
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +766 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +581 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +578 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
- package/corpus/templates/design/app/i18n-data.ts +280 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +3087 -490
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +94 -2
- package/corpus/templates/design/e2e/helpers.ts +10 -4
- package/corpus/templates/design/server/db/schema.ts +123 -0
- package/corpus/templates/design/server/plugins/db.ts +90 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1016 -71
- package/corpus/templates/design/shared/component-model.ts +274 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/motion-compiler.ts +349 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
- package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +94 -10
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +37 -21
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +115 -22
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +10 -0
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { getText, hasCollabState } from "@agent-native/core/collab";
|
|
3
|
+
import { accessFilter, resolveAccess } from "@agent-native/core/sharing";
|
|
4
|
+
import { and, eq } from "drizzle-orm";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
8
|
+
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
9
|
+
import { resolveSourceCapabilities } from "../shared/capability-resolver.js";
|
|
10
|
+
import {
|
|
11
|
+
buildCodeLayerProjection,
|
|
12
|
+
type CodeLayerSource,
|
|
13
|
+
} from "../shared/code-layer.js";
|
|
14
|
+
import { hasCapability } from "../shared/design-source-capabilities.js";
|
|
15
|
+
import type { DesignCapabilityName } from "../shared/design-source-capabilities.js";
|
|
16
|
+
import type {
|
|
17
|
+
DesignSurfaceIndex,
|
|
18
|
+
DesignSurfaceNode,
|
|
19
|
+
DesignSurfaceComponent,
|
|
20
|
+
DesignSurfaceToken,
|
|
21
|
+
DesignSurfaceMotionTimeline,
|
|
22
|
+
DesignSurfaceMotionTrack,
|
|
23
|
+
DesignSurfaceState,
|
|
24
|
+
DesignSurfaceReview,
|
|
25
|
+
DesignBreakpoint,
|
|
26
|
+
DesignStateKind,
|
|
27
|
+
} from "../shared/design-surface-index.js";
|
|
28
|
+
import { normalizeDesignSourceType } from "../shared/source-mode.js";
|
|
29
|
+
|
|
30
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
async function liveContent(
|
|
33
|
+
fileId: string,
|
|
34
|
+
storedContent: string,
|
|
35
|
+
): Promise<string> {
|
|
36
|
+
try {
|
|
37
|
+
if (await hasCollabState(fileId)) {
|
|
38
|
+
const live = await getText(fileId, "content");
|
|
39
|
+
if (typeof live === "string") return live;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Collab reads are best-effort; SQL content is the deterministic fallback.
|
|
43
|
+
}
|
|
44
|
+
return storedContent;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Lightweight hash for change detection — djb2 over the UTF-16 code units. */
|
|
48
|
+
function contentHash(s: string): string {
|
|
49
|
+
let h = 5381;
|
|
50
|
+
for (let i = 0; i < s.length; i++) {
|
|
51
|
+
h = ((h << 5) + h) ^ s.charCodeAt(i);
|
|
52
|
+
h |= 0; // coerce to int32
|
|
53
|
+
}
|
|
54
|
+
return (h >>> 0).toString(16);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseJson<T>(raw: string | null | undefined, fallback: T): T {
|
|
58
|
+
if (!raw) return fallback;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(raw) as T;
|
|
61
|
+
} catch {
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Node extraction from projection ─────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function extractNodes(
|
|
69
|
+
html: string,
|
|
70
|
+
codeLayerSource: CodeLayerSource,
|
|
71
|
+
selectedNodeId?: string,
|
|
72
|
+
): Record<string, DesignSurfaceNode> {
|
|
73
|
+
const projection = buildCodeLayerProjection(html, {
|
|
74
|
+
source: codeLayerSource,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const nodeMap: Record<string, DesignSurfaceNode> = {};
|
|
78
|
+
for (const node of projection.nodes) {
|
|
79
|
+
nodeMap[node.id] = {
|
|
80
|
+
nodeId: node.id,
|
|
81
|
+
layerName: node.layerName,
|
|
82
|
+
tag: node.tag,
|
|
83
|
+
selector: node.selector,
|
|
84
|
+
parentNodeId: node.parentId,
|
|
85
|
+
childNodeIds: node.children,
|
|
86
|
+
selected: selectedNodeId ? node.id === selectedNodeId : undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return nodeMap;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Alpine component extraction ──────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function extractAlpineComponents(
|
|
95
|
+
html: string,
|
|
96
|
+
codeLayerSource: CodeLayerSource,
|
|
97
|
+
): DesignSurfaceComponent[] {
|
|
98
|
+
const projection = buildCodeLayerProjection(html, {
|
|
99
|
+
source: codeLayerSource,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const componentMap: Map<string, DesignSurfaceComponent> = new Map();
|
|
103
|
+
|
|
104
|
+
for (const node of projection.nodes) {
|
|
105
|
+
const compAttr =
|
|
106
|
+
node.dataAttributes["data-agent-native-component"] ??
|
|
107
|
+
node.attributes["data-agent-native-component"];
|
|
108
|
+
const compName = typeof compAttr === "string" ? compAttr : undefined;
|
|
109
|
+
if (!compName) continue;
|
|
110
|
+
|
|
111
|
+
const existing = componentMap.get(compName);
|
|
112
|
+
if (existing) {
|
|
113
|
+
existing.instanceNodeIds.push(node.id);
|
|
114
|
+
} else {
|
|
115
|
+
componentMap.set(compName, {
|
|
116
|
+
componentId: `alpine-${compName.toLowerCase().replace(/\s+/g, "-")}`,
|
|
117
|
+
kind: "alpine-annotation",
|
|
118
|
+
name: compName,
|
|
119
|
+
instanceNodeIds: [node.id],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Array.from(componentMap.values());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Token extraction from CSS :root vars ─────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
function guessTokenKind(
|
|
130
|
+
varName: string,
|
|
131
|
+
):
|
|
132
|
+
| "color"
|
|
133
|
+
| "typography"
|
|
134
|
+
| "spacing"
|
|
135
|
+
| "radius"
|
|
136
|
+
| "shadow"
|
|
137
|
+
| "motion"
|
|
138
|
+
| "other" {
|
|
139
|
+
const n = varName.toLowerCase();
|
|
140
|
+
if (
|
|
141
|
+
n.includes("color") ||
|
|
142
|
+
n.includes("bg") ||
|
|
143
|
+
n.includes("fill") ||
|
|
144
|
+
n.includes("accent") ||
|
|
145
|
+
n.includes("brand") ||
|
|
146
|
+
n.includes("primary") ||
|
|
147
|
+
n.includes("secondary") ||
|
|
148
|
+
n.includes("muted") ||
|
|
149
|
+
n.includes("foreground") ||
|
|
150
|
+
n.includes("background")
|
|
151
|
+
)
|
|
152
|
+
return "color";
|
|
153
|
+
if (
|
|
154
|
+
n.includes("font") ||
|
|
155
|
+
n.includes("text") ||
|
|
156
|
+
n.includes("size") ||
|
|
157
|
+
n.includes("weight") ||
|
|
158
|
+
n.includes("line-height") ||
|
|
159
|
+
n.includes("letter")
|
|
160
|
+
)
|
|
161
|
+
return "typography";
|
|
162
|
+
if (n.includes("radius") || n.includes("rounded")) return "radius";
|
|
163
|
+
if (
|
|
164
|
+
n.includes("spacing") ||
|
|
165
|
+
n.includes("gap") ||
|
|
166
|
+
n.includes("padding") ||
|
|
167
|
+
n.includes("margin")
|
|
168
|
+
)
|
|
169
|
+
return "spacing";
|
|
170
|
+
if (n.includes("shadow")) return "shadow";
|
|
171
|
+
if (
|
|
172
|
+
n.includes("duration") ||
|
|
173
|
+
n.includes("delay") ||
|
|
174
|
+
n.includes("ease") ||
|
|
175
|
+
n.includes("animation") ||
|
|
176
|
+
n.includes("transition")
|
|
177
|
+
)
|
|
178
|
+
return "motion";
|
|
179
|
+
return "other";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function friendlyLabel(cssVar: string): string {
|
|
183
|
+
return cssVar
|
|
184
|
+
.replace(/^--/, "")
|
|
185
|
+
.replace(/[-_]/g, " ")
|
|
186
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
187
|
+
.trim();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function extractTokensFromHtml(html: string): DesignSurfaceToken[] {
|
|
191
|
+
const tokens: DesignSurfaceToken[] = [];
|
|
192
|
+
// Match :root { ... } block(s) and parse CSS custom-property declarations.
|
|
193
|
+
const rootBlocks = html.match(/:root\s*\{([^}]*)\}/g) ?? [];
|
|
194
|
+
const seen = new Set<string>();
|
|
195
|
+
|
|
196
|
+
for (const block of rootBlocks) {
|
|
197
|
+
const inner = block.replace(/:root\s*\{/, "").replace(/\}$/, "");
|
|
198
|
+
const declarations = inner.match(/--[a-zA-Z0-9_-]+\s*:[^;]+;/g) ?? [];
|
|
199
|
+
for (const decl of declarations) {
|
|
200
|
+
const colonIdx = decl.indexOf(":");
|
|
201
|
+
if (colonIdx === -1) continue;
|
|
202
|
+
const varName = decl.slice(0, colonIdx).trim();
|
|
203
|
+
const value = decl
|
|
204
|
+
.slice(colonIdx + 1)
|
|
205
|
+
.replace(/;$/, "")
|
|
206
|
+
.trim();
|
|
207
|
+
if (!varName.startsWith("--") || seen.has(varName)) continue;
|
|
208
|
+
seen.add(varName);
|
|
209
|
+
tokens.push({
|
|
210
|
+
tokenId: varName,
|
|
211
|
+
kind: guessTokenKind(varName),
|
|
212
|
+
label: friendlyLabel(varName),
|
|
213
|
+
cssVar: varName,
|
|
214
|
+
resolvedValue: value,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return tokens;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Motion timeline extraction ───────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
async function fetchMotionTimelines(
|
|
224
|
+
db: ReturnType<typeof getDb>,
|
|
225
|
+
designId: string,
|
|
226
|
+
fileId?: string,
|
|
227
|
+
): Promise<Record<string, DesignSurfaceMotionTimeline>> {
|
|
228
|
+
// guard:allow-unscoped — run() resolves design access via
|
|
229
|
+
// resolveAccess("design", designId) and throws on null before calling this
|
|
230
|
+
// helper; rows are scoped by designId (motion timelines are children of the
|
|
231
|
+
// design, not independently shareable).
|
|
232
|
+
const rows = await db
|
|
233
|
+
.select({
|
|
234
|
+
id: schema.motionTimeline.id,
|
|
235
|
+
sourceRef: schema.motionTimeline.sourceRef,
|
|
236
|
+
tracks: schema.motionTimeline.tracks,
|
|
237
|
+
durationMs: schema.motionTimeline.durationMs,
|
|
238
|
+
compiledHash: schema.motionTimeline.compiledHash,
|
|
239
|
+
})
|
|
240
|
+
.from(schema.motionTimeline)
|
|
241
|
+
.where(eq(schema.motionTimeline.designId, designId));
|
|
242
|
+
|
|
243
|
+
const result: Record<string, DesignSurfaceMotionTimeline> = {};
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
// Optionally filter to the active file's source ref when provided.
|
|
246
|
+
if (fileId && row.sourceRef && row.sourceRef !== fileId) continue;
|
|
247
|
+
|
|
248
|
+
const rawTracks = parseJson<
|
|
249
|
+
Array<{
|
|
250
|
+
target_node_id?: string;
|
|
251
|
+
targetNodeId?: string;
|
|
252
|
+
property?: string;
|
|
253
|
+
keyframes?: unknown[];
|
|
254
|
+
}>
|
|
255
|
+
>(row.tracks, []);
|
|
256
|
+
|
|
257
|
+
const tracks: DesignSurfaceMotionTrack[] = rawTracks
|
|
258
|
+
.filter(
|
|
259
|
+
(t) =>
|
|
260
|
+
(t.target_node_id ?? t.targetNodeId) !== undefined &&
|
|
261
|
+
t.property !== undefined,
|
|
262
|
+
)
|
|
263
|
+
.map((t) => ({
|
|
264
|
+
targetNodeId: String(t.target_node_id ?? t.targetNodeId ?? ""),
|
|
265
|
+
property: String(t.property ?? ""),
|
|
266
|
+
keyframeCount: Array.isArray(t.keyframes) ? t.keyframes.length : 0,
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
result[row.id] = {
|
|
270
|
+
timelineId: row.id,
|
|
271
|
+
sourceRef: row.sourceRef ?? fileId ?? "",
|
|
272
|
+
durationMs: row.durationMs ?? 300,
|
|
273
|
+
tracks,
|
|
274
|
+
compiledHash: row.compiledHash ?? undefined,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Design state extraction ──────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
async function fetchDesignStates(
|
|
283
|
+
db: ReturnType<typeof getDb>,
|
|
284
|
+
designId: string,
|
|
285
|
+
): Promise<DesignSurfaceState[]> {
|
|
286
|
+
// guard:allow-unscoped — run() resolves design access via
|
|
287
|
+
// resolveAccess("design", designId) and throws on null before calling this
|
|
288
|
+
// helper; rows are scoped by designId (design states are children of the
|
|
289
|
+
// design, not independently shareable).
|
|
290
|
+
const rows = await db
|
|
291
|
+
.select({
|
|
292
|
+
id: schema.designState.id,
|
|
293
|
+
kind: schema.designState.kind,
|
|
294
|
+
name: schema.designState.name,
|
|
295
|
+
breakpoint: schema.designState.breakpoint,
|
|
296
|
+
route: schema.designState.route,
|
|
297
|
+
fixtureData: schema.designState.fixtureData,
|
|
298
|
+
captureData: schema.designState.captureData,
|
|
299
|
+
previewRef: schema.designState.previewRef,
|
|
300
|
+
})
|
|
301
|
+
.from(schema.designState)
|
|
302
|
+
.where(eq(schema.designState.designId, designId));
|
|
303
|
+
|
|
304
|
+
return rows.map((row) => ({
|
|
305
|
+
stateId: row.id,
|
|
306
|
+
kind: (row.kind ?? "state") as DesignStateKind,
|
|
307
|
+
name: row.name,
|
|
308
|
+
breakpoint: (row.breakpoint ?? "auto") as DesignBreakpoint,
|
|
309
|
+
route: row.route ?? undefined,
|
|
310
|
+
hasData: Boolean(row.fixtureData ?? row.captureData),
|
|
311
|
+
previewRef: row.previewRef ?? undefined,
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ─── Review snapshot extraction ───────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
async function fetchLatestReview(
|
|
318
|
+
db: ReturnType<typeof getDb>,
|
|
319
|
+
designId: string,
|
|
320
|
+
): Promise<DesignSurfaceReview | undefined> {
|
|
321
|
+
// guard:allow-unscoped — run() resolves design access via
|
|
322
|
+
// resolveAccess("design", designId) and throws on null before calling this
|
|
323
|
+
// helper; rows are scoped by designId (review snapshots are children of the
|
|
324
|
+
// design, not independently shareable).
|
|
325
|
+
const rows = await db
|
|
326
|
+
.select({
|
|
327
|
+
id: schema.designReviewSnapshot.id,
|
|
328
|
+
a11yFindings: schema.designReviewSnapshot.a11yFindings,
|
|
329
|
+
baseVersionId: schema.designReviewSnapshot.baseVersionId,
|
|
330
|
+
compareVersionId: schema.designReviewSnapshot.compareVersionId,
|
|
331
|
+
createdAt: schema.designReviewSnapshot.createdAt,
|
|
332
|
+
status: schema.designReviewSnapshot.status,
|
|
333
|
+
})
|
|
334
|
+
.from(schema.designReviewSnapshot)
|
|
335
|
+
.where(
|
|
336
|
+
and(
|
|
337
|
+
eq(schema.designReviewSnapshot.designId, designId),
|
|
338
|
+
eq(schema.designReviewSnapshot.status, "ready"),
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
.limit(1);
|
|
342
|
+
|
|
343
|
+
if (!rows.length) return undefined;
|
|
344
|
+
|
|
345
|
+
const row = rows[0];
|
|
346
|
+
const rawFindings = parseJson<
|
|
347
|
+
Array<{
|
|
348
|
+
id?: string;
|
|
349
|
+
severity?: string;
|
|
350
|
+
kind?: string;
|
|
351
|
+
message?: string;
|
|
352
|
+
nodeId?: string;
|
|
353
|
+
selector?: string;
|
|
354
|
+
fixAvailable?: boolean;
|
|
355
|
+
}>
|
|
356
|
+
>(row.a11yFindings ?? null, []);
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
snapshotId: row.id,
|
|
360
|
+
auditedAt: row.createdAt ?? null,
|
|
361
|
+
findings: rawFindings.map((f, idx) => ({
|
|
362
|
+
findingId: f.id ?? `finding-${idx}`,
|
|
363
|
+
severity: (f.severity as "error" | "warning" | "info") ?? "info",
|
|
364
|
+
kind:
|
|
365
|
+
(f.kind as
|
|
366
|
+
| "contrast"
|
|
367
|
+
| "tap-target"
|
|
368
|
+
| "focus-visibility"
|
|
369
|
+
| "missing-alt"
|
|
370
|
+
| "missing-label"
|
|
371
|
+
| "missing-role"
|
|
372
|
+
| "reduced-motion"
|
|
373
|
+
| "other") ?? "other",
|
|
374
|
+
message: f.message ?? "",
|
|
375
|
+
nodeId: f.nodeId,
|
|
376
|
+
selector: f.selector,
|
|
377
|
+
fixAvailable: Boolean(f.fixAvailable),
|
|
378
|
+
})),
|
|
379
|
+
baseVersionId: row.baseVersionId ?? undefined,
|
|
380
|
+
compareVersionId: row.compareVersionId ?? undefined,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ─── Action definition ────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
export default defineAction({
|
|
387
|
+
description:
|
|
388
|
+
"Return a lazily-built DesignSurfaceIndex for a design and its active " +
|
|
389
|
+
"source/screen. The index normalizes the code-layer projection (nodes), " +
|
|
390
|
+
"CSS-var tokens, motion timelines, design states/captures, and the most " +
|
|
391
|
+
"recent accessibility review into one queryable surface that UI panels and " +
|
|
392
|
+
"agent actions read. " +
|
|
393
|
+
"Components are extracted from Alpine data-agent-native-component annotations " +
|
|
394
|
+
"for inline designs; for real-app sources the index is populated by the " +
|
|
395
|
+
"index-components action. " +
|
|
396
|
+
"Pass includeNodes=false to skip the full node map when you only need tokens " +
|
|
397
|
+
"or capabilities. No cache — always built fresh from the live source.",
|
|
398
|
+
schema: z.object({
|
|
399
|
+
designId: z.string().describe("Design project ID"),
|
|
400
|
+
fileId: z
|
|
401
|
+
.string()
|
|
402
|
+
.optional()
|
|
403
|
+
.describe(
|
|
404
|
+
"Design file ID to read HTML from (defaults to index.html when omitted).",
|
|
405
|
+
),
|
|
406
|
+
filename: z
|
|
407
|
+
.string()
|
|
408
|
+
.optional()
|
|
409
|
+
.default("index.html")
|
|
410
|
+
.describe("Filename to resolve when fileId is not provided."),
|
|
411
|
+
selectedNodeId: z
|
|
412
|
+
.string()
|
|
413
|
+
.optional()
|
|
414
|
+
.describe(
|
|
415
|
+
"Currently selected node id — marks the matching node as selected in the index.",
|
|
416
|
+
),
|
|
417
|
+
includeNodes: z
|
|
418
|
+
.boolean()
|
|
419
|
+
.optional()
|
|
420
|
+
.default(true)
|
|
421
|
+
.describe(
|
|
422
|
+
"Include the full node map derived from the code-layer projection. " +
|
|
423
|
+
"Set to false when you only need tokens/states/capabilities.",
|
|
424
|
+
),
|
|
425
|
+
includeReview: z
|
|
426
|
+
.boolean()
|
|
427
|
+
.optional()
|
|
428
|
+
.default(false)
|
|
429
|
+
.describe(
|
|
430
|
+
"Include the most recent accessibility review snapshot if one exists. " +
|
|
431
|
+
"Omitted by default for speed.",
|
|
432
|
+
),
|
|
433
|
+
}),
|
|
434
|
+
readOnly: true,
|
|
435
|
+
http: { method: "GET" },
|
|
436
|
+
run: async ({
|
|
437
|
+
designId,
|
|
438
|
+
fileId,
|
|
439
|
+
filename,
|
|
440
|
+
selectedNodeId,
|
|
441
|
+
includeNodes,
|
|
442
|
+
includeReview,
|
|
443
|
+
}) => {
|
|
444
|
+
const access = await resolveAccess("design", designId);
|
|
445
|
+
if (!access) {
|
|
446
|
+
throw new Error("Design not found");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const db = getDb();
|
|
450
|
+
|
|
451
|
+
// ── Resolve source type ──────────────────────────────────────────────────
|
|
452
|
+
let rawSourceType: unknown = "inline";
|
|
453
|
+
const rawData = (access.resource as { data?: unknown }).data;
|
|
454
|
+
if (typeof rawData === "string") {
|
|
455
|
+
try {
|
|
456
|
+
const parsed: unknown = JSON.parse(rawData);
|
|
457
|
+
if (
|
|
458
|
+
parsed !== null &&
|
|
459
|
+
typeof parsed === "object" &&
|
|
460
|
+
!Array.isArray(parsed) &&
|
|
461
|
+
"sourceType" in (parsed as object)
|
|
462
|
+
) {
|
|
463
|
+
rawSourceType = (parsed as { sourceType: unknown }).sourceType;
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Stale/invalid JSON — default to "inline".
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const sourceType = normalizeDesignSourceType(rawSourceType) ?? "inline";
|
|
470
|
+
const capabilities = resolveSourceCapabilities(sourceType);
|
|
471
|
+
const availableCapabilities = Object.entries(capabilities)
|
|
472
|
+
.filter(([, entry]) => entry.status === "available")
|
|
473
|
+
.map(([name]) => name as DesignCapabilityName);
|
|
474
|
+
|
|
475
|
+
// ── Resolve HTML file ────────────────────────────────────────────────────
|
|
476
|
+
const fileConditions = [
|
|
477
|
+
accessFilter(schema.designs, schema.designShares),
|
|
478
|
+
fileId
|
|
479
|
+
? eq(schema.designFiles.id, fileId)
|
|
480
|
+
: eq(schema.designFiles.designId, designId),
|
|
481
|
+
];
|
|
482
|
+
if (!fileId) {
|
|
483
|
+
fileConditions.push(
|
|
484
|
+
eq(schema.designFiles.filename, filename ?? "index.html"),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const [file] = await db
|
|
489
|
+
.select({
|
|
490
|
+
id: schema.designFiles.id,
|
|
491
|
+
designId: schema.designFiles.designId,
|
|
492
|
+
filename: schema.designFiles.filename,
|
|
493
|
+
fileType: schema.designFiles.fileType,
|
|
494
|
+
content: schema.designFiles.content,
|
|
495
|
+
})
|
|
496
|
+
.from(schema.designFiles)
|
|
497
|
+
.innerJoin(
|
|
498
|
+
schema.designs,
|
|
499
|
+
eq(schema.designFiles.designId, schema.designs.id),
|
|
500
|
+
)
|
|
501
|
+
.where(and(...fileConditions))
|
|
502
|
+
.limit(1);
|
|
503
|
+
|
|
504
|
+
if (!file) {
|
|
505
|
+
throw new Error("Design HTML file not found.");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const html = await liveContent(file.id, file.content ?? "");
|
|
509
|
+
const hash = contentHash(html);
|
|
510
|
+
|
|
511
|
+
const codeLayerSource: CodeLayerSource = {
|
|
512
|
+
kind: "design-file",
|
|
513
|
+
sourceType: "inline",
|
|
514
|
+
designId: file.designId,
|
|
515
|
+
fileId: file.id,
|
|
516
|
+
filename: file.filename,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// ── Build sections in parallel ───────────────────────────────────────────
|
|
520
|
+
const [motionTimelines, designStates, review] = await Promise.all([
|
|
521
|
+
fetchMotionTimelines(db, designId, file.id),
|
|
522
|
+
fetchDesignStates(db, designId),
|
|
523
|
+
includeReview
|
|
524
|
+
? fetchLatestReview(db, designId)
|
|
525
|
+
: Promise.resolve(undefined),
|
|
526
|
+
]);
|
|
527
|
+
|
|
528
|
+
// Nodes and tokens parse from HTML — only for inline sources today.
|
|
529
|
+
let nodes: Record<string, DesignSurfaceNode> | undefined;
|
|
530
|
+
let components: DesignSurfaceComponent[] | undefined;
|
|
531
|
+
let tokens: DesignSurfaceToken[] | undefined;
|
|
532
|
+
|
|
533
|
+
if (sourceType === "inline" && file.fileType === "html") {
|
|
534
|
+
if (includeNodes) {
|
|
535
|
+
nodes = extractNodes(html, codeLayerSource, selectedNodeId);
|
|
536
|
+
}
|
|
537
|
+
components = extractAlpineComponents(html, codeLayerSource);
|
|
538
|
+
if (hasCapability(capabilities, "indexTokens")) {
|
|
539
|
+
tokens = extractTokensFromHtml(html);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const index: DesignSurfaceIndex = {
|
|
544
|
+
version: 1,
|
|
545
|
+
source: {
|
|
546
|
+
sourceType,
|
|
547
|
+
sourceRef: file.id,
|
|
548
|
+
contentHash: hash,
|
|
549
|
+
indexedAt: new Date().toISOString(),
|
|
550
|
+
availableCapabilities,
|
|
551
|
+
},
|
|
552
|
+
...(nodes !== undefined ? { nodes } : {}),
|
|
553
|
+
...(components !== undefined && components.length > 0
|
|
554
|
+
? { components }
|
|
555
|
+
: {}),
|
|
556
|
+
...(tokens !== undefined && tokens.length > 0 ? { tokens } : {}),
|
|
557
|
+
...(Object.keys(motionTimelines).length > 0
|
|
558
|
+
? { motion: motionTimelines }
|
|
559
|
+
: {}),
|
|
560
|
+
...(designStates.length > 0 ? { states: designStates } : {}),
|
|
561
|
+
...(review !== undefined ? { review } : {}),
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
index,
|
|
566
|
+
designId,
|
|
567
|
+
fileId: file.id,
|
|
568
|
+
filename: file.filename,
|
|
569
|
+
sourceType,
|
|
570
|
+
availableCapabilities,
|
|
571
|
+
summary: {
|
|
572
|
+
nodeCount: nodes ? Object.keys(nodes).length : null,
|
|
573
|
+
componentCount: components?.length ?? 0,
|
|
574
|
+
tokenCount: tokens?.length ?? 0,
|
|
575
|
+
timelineCount: Object.keys(motionTimelines).length,
|
|
576
|
+
stateCount: designStates.length,
|
|
577
|
+
hasReview: review !== undefined,
|
|
578
|
+
reviewFindingCount: review?.findings.length ?? 0,
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
},
|
|
582
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
8
|
+
|
|
9
|
+
export default defineAction({
|
|
10
|
+
description:
|
|
11
|
+
"Read one or all motion timelines for a design. " +
|
|
12
|
+
"Returns timeline metadata (id, sourceRef, filePath, durationMs, " +
|
|
13
|
+
"defaultEase, compiledHash) and the full tracks array (each track has " +
|
|
14
|
+
"targetNodeId, property, and keyframes). Read-only.",
|
|
15
|
+
readOnly: true,
|
|
16
|
+
http: { method: "GET" },
|
|
17
|
+
schema: z.object({
|
|
18
|
+
designId: z.string().describe("Design project ID to read timelines for."),
|
|
19
|
+
timelineId: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(
|
|
23
|
+
"If provided, return only this specific timeline row. " +
|
|
24
|
+
"Omit to list all timelines for the design.",
|
|
25
|
+
),
|
|
26
|
+
sourceRef: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(
|
|
30
|
+
"Filter by source ref (fileId for inline designs, routeId for " +
|
|
31
|
+
"localhost/fusion). Ignored when timelineId is provided.",
|
|
32
|
+
),
|
|
33
|
+
}),
|
|
34
|
+
run: async ({ designId, timelineId, sourceRef }) => {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
|
|
37
|
+
const conditions = [
|
|
38
|
+
accessFilter(schema.designs, schema.designShares),
|
|
39
|
+
eq(schema.motionTimeline.designId, designId),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
if (timelineId) {
|
|
43
|
+
conditions.push(eq(schema.motionTimeline.id, timelineId));
|
|
44
|
+
} else if (sourceRef) {
|
|
45
|
+
conditions.push(eq(schema.motionTimeline.sourceRef, sourceRef));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rows = await db
|
|
49
|
+
.select({
|
|
50
|
+
id: schema.motionTimeline.id,
|
|
51
|
+
designId: schema.motionTimeline.designId,
|
|
52
|
+
sourceRef: schema.motionTimeline.sourceRef,
|
|
53
|
+
filePath: schema.motionTimeline.filePath,
|
|
54
|
+
tracks: schema.motionTimeline.tracks,
|
|
55
|
+
durationMs: schema.motionTimeline.durationMs,
|
|
56
|
+
defaultEase: schema.motionTimeline.defaultEase,
|
|
57
|
+
compiledHash: schema.motionTimeline.compiledHash,
|
|
58
|
+
createdAt: schema.motionTimeline.createdAt,
|
|
59
|
+
updatedAt: schema.motionTimeline.updatedAt,
|
|
60
|
+
})
|
|
61
|
+
.from(schema.motionTimeline)
|
|
62
|
+
.innerJoin(
|
|
63
|
+
schema.designs,
|
|
64
|
+
eq(schema.motionTimeline.designId, schema.designs.id),
|
|
65
|
+
)
|
|
66
|
+
.where(and(...conditions))
|
|
67
|
+
.limit(timelineId ? 1 : 100);
|
|
68
|
+
|
|
69
|
+
const timelines = rows.map((row) => {
|
|
70
|
+
let tracks: unknown = [];
|
|
71
|
+
try {
|
|
72
|
+
tracks = JSON.parse(row.tracks ?? "[]");
|
|
73
|
+
} catch {
|
|
74
|
+
tracks = [];
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
id: row.id,
|
|
78
|
+
designId: row.designId,
|
|
79
|
+
sourceRef: row.sourceRef ?? null,
|
|
80
|
+
filePath: row.filePath ?? null,
|
|
81
|
+
tracks,
|
|
82
|
+
durationMs: row.durationMs,
|
|
83
|
+
defaultEase: row.defaultEase,
|
|
84
|
+
compiledHash: row.compiledHash ?? null,
|
|
85
|
+
createdAt: row.createdAt,
|
|
86
|
+
updatedAt: row.updatedAt,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (timelineId) {
|
|
91
|
+
if (timelines.length === 0) {
|
|
92
|
+
throw new Error(`Motion timeline not found: ${timelineId}`);
|
|
93
|
+
}
|
|
94
|
+
return { timeline: timelines[0] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { designId, timelines, count: timelines.length };
|
|
98
|
+
},
|
|
99
|
+
});
|