@agent-native/core 0.80.9 → 0.80.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +28 -2
  5. package/corpus/core/src/cli/skills.ts +45 -24
  6. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  7. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  8. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  9. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  10. package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
  11. package/corpus/templates/design/actions/apply-design-state.ts +200 -0
  12. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  13. package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
  14. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  15. package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
  16. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  17. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  18. package/corpus/templates/design/actions/create-component.ts +447 -0
  19. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  20. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  21. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  22. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  23. package/corpus/templates/design/actions/get-component-details.ts +242 -0
  24. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  25. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  26. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  27. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  28. package/corpus/templates/design/actions/index-components.ts +258 -0
  29. package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
  30. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  31. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  32. package/corpus/templates/design/actions/list-design-states.ts +72 -0
  33. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  34. package/corpus/templates/design/actions/open-component-source.ts +237 -0
  35. package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
  36. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  37. package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
  38. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  39. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  40. package/corpus/templates/design/actions/run-design-audit.ts +421 -0
  41. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  42. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  43. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
  44. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
  45. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  46. package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
  47. package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
  48. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  49. package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
  50. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
  51. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
  52. package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
  53. package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
  54. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  55. package/corpus/templates/design/app/components/design/index.ts +16 -0
  56. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  57. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
  58. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  59. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  60. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  61. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  62. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
  63. package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
  64. package/corpus/templates/design/app/i18n-data.ts +269 -0
  65. package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
  66. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  67. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  68. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  69. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  70. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  71. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  72. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  73. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  74. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  75. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  76. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  77. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  78. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  79. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  80. package/corpus/templates/design/e2e/helpers.ts +10 -4
  81. package/corpus/templates/design/server/db/schema.ts +123 -0
  82. package/corpus/templates/design/server/plugins/db.ts +90 -0
  83. package/corpus/templates/design/shared/builder-app.ts +297 -0
  84. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  85. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  86. package/corpus/templates/design/shared/code-layer.ts +1016 -71
  87. package/corpus/templates/design/shared/component-model.ts +239 -0
  88. package/corpus/templates/design/shared/design-review.ts +275 -0
  89. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  90. package/corpus/templates/design/shared/design-state.ts +112 -0
  91. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  92. package/corpus/templates/design/shared/motion-compiler.ts +278 -0
  93. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  94. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  95. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  96. package/corpus/templates/design/shared/source-mode.ts +245 -0
  97. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  98. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  99. package/dist/cli/design-connect.d.ts.map +1 -1
  100. package/dist/cli/design-connect.js +28 -2
  101. package/dist/cli/design-connect.js.map +1 -1
  102. package/dist/cli/skills.d.ts.map +1 -1
  103. package/dist/cli/skills.js +37 -21
  104. package/dist/cli/skills.js.map +1 -1
  105. package/dist/collab/routes.d.ts +1 -1
  106. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  107. package/dist/notifications/routes.d.ts +2 -2
  108. package/dist/observability/routes.d.ts +7 -7
  109. package/dist/resources/handlers.d.ts +2 -2
  110. package/dist/server/transcribe-voice.d.ts +1 -1
  111. 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
+ });