@agent-native/core 0.84.0 → 0.84.2

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 (152) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/docs/content/evals.mdx +34 -12
  4. package/corpus/core/docs/content/locales/ar-SA/evals.mdx +7 -0
  5. package/corpus/core/docs/content/locales/de-DE/evals.mdx +8 -0
  6. package/corpus/core/docs/content/locales/es-ES/evals.mdx +8 -0
  7. package/corpus/core/docs/content/locales/fr-FR/evals.mdx +8 -0
  8. package/corpus/core/docs/content/locales/hi-IN/evals.mdx +8 -0
  9. package/corpus/core/docs/content/locales/ja-JP/evals.mdx +7 -0
  10. package/corpus/core/docs/content/locales/ko-KR/evals.mdx +7 -0
  11. package/corpus/core/docs/content/locales/pt-BR/evals.mdx +8 -0
  12. package/corpus/core/docs/content/locales/zh-CN/evals.mdx +7 -0
  13. package/corpus/core/docs/content/locales/zh-TW/evals.mdx +7 -0
  14. package/corpus/core/docs/design/durable-agent-runs.md +34 -35
  15. package/corpus/core/package.json +1 -1
  16. package/corpus/core/src/agent/engine/builder-engine.ts +81 -9
  17. package/corpus/core/src/agent/run-manager.ts +8 -7
  18. package/corpus/core/src/cli/design-connect.ts +175 -0
  19. package/corpus/core/src/cli/skills.ts +5 -7
  20. package/corpus/core/src/client/AgentPanel.tsx +8 -0
  21. package/corpus/core/src/client/MultiTabAssistantChat.tsx +4 -1
  22. package/corpus/core/src/client/agent-chat-adapter.ts +45 -15
  23. package/corpus/core/src/client/sse-event-processor.ts +31 -4
  24. package/corpus/core/src/eval/define-eval.ts +4 -1
  25. package/corpus/core/src/eval/report.ts +23 -8
  26. package/corpus/core/src/eval/runner.ts +41 -6
  27. package/corpus/core/src/eval/types.ts +11 -0
  28. package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  29. package/corpus/templates/content/AGENTS.md +9 -2
  30. package/corpus/templates/content/actions/_database-row-batch.ts +230 -0
  31. package/corpus/templates/content/actions/_database-utils.ts +17 -9
  32. package/corpus/templates/content/actions/_notion-action-utils.ts +26 -0
  33. package/corpus/templates/content/actions/connect-notion-status.ts +13 -4
  34. package/corpus/templates/content/actions/create-and-link-notion-page.ts +23 -0
  35. package/corpus/templates/content/actions/delete-database-items.ts +55 -0
  36. package/corpus/templates/content/actions/delete-document.ts +4 -4
  37. package/corpus/templates/content/actions/disconnect-notion.ts +16 -0
  38. package/corpus/templates/content/actions/duplicate-database-item.ts +1 -1
  39. package/corpus/templates/content/actions/duplicate-database-items.ts +191 -0
  40. package/corpus/templates/content/actions/link-notion-page.ts +10 -7
  41. package/corpus/templates/content/actions/list-notion-links.ts +3 -4
  42. package/corpus/templates/content/actions/pull-notion-page.ts +7 -9
  43. package/corpus/templates/content/actions/push-notion-page.ts +7 -9
  44. package/corpus/templates/content/actions/refresh-notion-sync-status.ts +24 -0
  45. package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +23 -0
  46. package/corpus/templates/content/actions/search-notion-pages.ts +67 -0
  47. package/corpus/templates/content/actions/unlink-notion-page.ts +23 -0
  48. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +27 -34
  49. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +16 -13
  50. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +10 -10
  51. package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +1 -1
  52. package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +18 -9
  53. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +27 -34
  54. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +10 -4
  55. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +32 -17
  56. package/corpus/templates/content/app/hooks/use-content-database.ts +35 -0
  57. package/corpus/templates/content/app/hooks/use-notion.ts +108 -99
  58. package/corpus/templates/content/package.json +3 -0
  59. package/corpus/templates/content/parity/README.md +69 -0
  60. package/corpus/templates/content/parity/eval-scenarios.ts +118 -0
  61. package/corpus/templates/content/parity/exceptions.allowlist.ts +17 -0
  62. package/corpus/templates/content/parity/matrix.md +28 -0
  63. package/corpus/templates/content/parity/matrix.ts +606 -0
  64. package/corpus/templates/content/parity/matrix.types.ts +42 -0
  65. package/corpus/templates/content/parity/parity-evals.eval.ts +4 -0
  66. package/corpus/templates/content/parity/render-matrix.ts +90 -0
  67. package/corpus/templates/content/parity/scenario-to-eval.ts +57 -0
  68. package/corpus/templates/content/shared/api.ts +11 -0
  69. package/corpus/templates/design/actions/apply-motion-edit.ts +50 -18
  70. package/corpus/templates/design/actions/get-motion-timeline.ts +4 -14
  71. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +156 -12
  72. package/corpus/templates/design/app/components/design/MotionDock.tsx +2 -3
  73. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +217 -75
  74. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +39 -39
  75. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +558 -37
  76. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +75 -3
  77. package/corpus/templates/design/app/components/design/types.ts +13 -0
  78. package/corpus/templates/design/app/components/layout/Layout.tsx +1 -0
  79. package/corpus/templates/design/app/i18n/zh-TW.ts +17 -0
  80. package/corpus/templates/design/app/i18n-data.ts +245 -0
  81. package/corpus/templates/design/app/pages/DesignEditor.tsx +762 -153
  82. package/corpus/templates/design/changelog/2026-06-30-copying-or-dragging-screen-elements-onto-the-infinite-canvas.md +6 -0
  83. package/corpus/templates/design/changelog/2026-06-30-design-questions-now-use-tighter-editor-typography-and-contr.md +6 -0
  84. package/corpus/templates/design/changelog/2026-06-30-element-drags-can-be-cancelled-with-escape-before-they-commi.md +6 -0
  85. package/corpus/templates/design/changelog/2026-06-30-pending-visual-style-edits-now-warn-before-you-leave-the-edi.md +6 -0
  86. package/corpus/templates/design/changelog/2026-06-30-visual-style-drags-stay-live-while-pending-edits-can-be-appl.md +6 -0
  87. package/corpus/templates/design/changelog/2026-07-01-design-chat-no-longer-shows-a-redundant-context-tab-above-the-composer.md +6 -0
  88. package/corpus/templates/design/changelog/2026-07-01-motion-track-creation-no-longer-fails-in-local-editors.md +6 -0
  89. package/corpus/templates/design/server/plugins/db.ts +10 -0
  90. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  91. package/dist/agent/engine/builder-engine.js +61 -10
  92. package/dist/agent/engine/builder-engine.js.map +1 -1
  93. package/dist/agent/run-manager.d.ts +8 -7
  94. package/dist/agent/run-manager.d.ts.map +1 -1
  95. package/dist/agent/run-manager.js +8 -7
  96. package/dist/agent/run-manager.js.map +1 -1
  97. package/dist/cli/design-connect.d.ts +2 -0
  98. package/dist/cli/design-connect.d.ts.map +1 -1
  99. package/dist/cli/design-connect.js +140 -0
  100. package/dist/cli/design-connect.js.map +1 -1
  101. package/dist/cli/skills.d.ts.map +1 -1
  102. package/dist/cli/skills.js +5 -7
  103. package/dist/cli/skills.js.map +1 -1
  104. package/dist/client/AgentPanel.d.ts +5 -1
  105. package/dist/client/AgentPanel.d.ts.map +1 -1
  106. package/dist/client/AgentPanel.js +4 -4
  107. package/dist/client/AgentPanel.js.map +1 -1
  108. package/dist/client/MultiTabAssistantChat.d.ts +3 -1
  109. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  110. package/dist/client/MultiTabAssistantChat.js +2 -2
  111. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  112. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  113. package/dist/client/agent-chat-adapter.js +37 -14
  114. package/dist/client/agent-chat-adapter.js.map +1 -1
  115. package/dist/client/sse-event-processor.d.ts +14 -1
  116. package/dist/client/sse-event-processor.d.ts.map +1 -1
  117. package/dist/client/sse-event-processor.js +21 -5
  118. package/dist/client/sse-event-processor.js.map +1 -1
  119. package/dist/collab/routes.d.ts +1 -1
  120. package/dist/eval/define-eval.d.ts.map +1 -1
  121. package/dist/eval/define-eval.js +2 -1
  122. package/dist/eval/define-eval.js.map +1 -1
  123. package/dist/eval/report.d.ts.map +1 -1
  124. package/dist/eval/report.js +19 -4
  125. package/dist/eval/report.js.map +1 -1
  126. package/dist/eval/runner.d.ts.map +1 -1
  127. package/dist/eval/runner.js +38 -6
  128. package/dist/eval/runner.js.map +1 -1
  129. package/dist/eval/types.d.ts +11 -0
  130. package/dist/eval/types.d.ts.map +1 -1
  131. package/dist/eval/types.js.map +1 -1
  132. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  133. package/dist/observability/routes.d.ts +2 -2
  134. package/dist/progress/routes.d.ts +1 -1
  135. package/dist/resources/handlers.d.ts +3 -3
  136. package/dist/server/transcribe-voice.d.ts +1 -1
  137. package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  138. package/docs/content/evals.mdx +34 -12
  139. package/docs/content/locales/ar-SA/evals.mdx +7 -0
  140. package/docs/content/locales/de-DE/evals.mdx +8 -0
  141. package/docs/content/locales/es-ES/evals.mdx +8 -0
  142. package/docs/content/locales/fr-FR/evals.mdx +8 -0
  143. package/docs/content/locales/hi-IN/evals.mdx +8 -0
  144. package/docs/content/locales/ja-JP/evals.mdx +7 -0
  145. package/docs/content/locales/ko-KR/evals.mdx +7 -0
  146. package/docs/content/locales/pt-BR/evals.mdx +8 -0
  147. package/docs/content/locales/zh-CN/evals.mdx +7 -0
  148. package/docs/content/locales/zh-TW/evals.mdx +7 -0
  149. package/docs/design/durable-agent-runs.md +34 -35
  150. package/package.json +1 -1
  151. package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +15 -17
  152. package/corpus/templates/content/server/routes/api/notion/disconnect.post.ts +0 -12
@@ -0,0 +1,90 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { parityMatrix } from "./matrix";
5
+ import type { ParityRow } from "./matrix.types";
6
+
7
+ function escapeCell(value: string) {
8
+ return value.replace(/\|/g, "\\|").replace(/\n/g, "<br>");
9
+ }
10
+
11
+ function cell(value: string | string[] | null | undefined) {
12
+ if (Array.isArray(value))
13
+ return escapeCell(value.length ? value.join(", ") : "-");
14
+ if (value === null || value === undefined || value === "") return "-";
15
+ return escapeCell(value);
16
+ }
17
+
18
+ const headers = [
19
+ "ID",
20
+ "Surface",
21
+ "User-visible action",
22
+ "Status",
23
+ "Actions",
24
+ "UI entrypoints",
25
+ "Durable effect",
26
+ "Exception / gap",
27
+ "Reliability risk",
28
+ "Spine priority",
29
+ "Test coverage",
30
+ "Coverage refs",
31
+ "Eval scenarios",
32
+ "Follow-up",
33
+ ];
34
+
35
+ function renderRow(row: ParityRow) {
36
+ return [
37
+ row.id,
38
+ row.surface,
39
+ row.label,
40
+ row.status,
41
+ row.actions.length
42
+ ? row.actions.map((action) => `\`${action}\``).join(", ")
43
+ : "-",
44
+ row.uiEntrypoints.map((entry) => `\`${entry}\``).join(", "),
45
+ row.durableEffect,
46
+ row.exception,
47
+ row.reliabilityRisk === "none" ? "-" : row.reliabilityRisk,
48
+ row.spinePriority,
49
+ row.testCoverage,
50
+ row.coverageRefs?.map((ref) => `\`${ref}\``).join(", "),
51
+ row.evalScenarioIds?.map((id) => `\`${id}\``).join(", "),
52
+ row.followUpPR,
53
+ ].map(cell);
54
+ }
55
+
56
+ function renderTable(rows: string[][]) {
57
+ const widths = headers.map((header, index) =>
58
+ Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)),
59
+ );
60
+ const renderCells = (cells: string[]) =>
61
+ `| ${cells.map((value, index) => value.padEnd(widths[index])).join(" | ")} |`;
62
+ return [
63
+ renderCells(headers),
64
+ renderCells(widths.map((width) => "-".repeat(width))),
65
+ ...rows.map(renderCells),
66
+ ];
67
+ }
68
+
69
+ export function renderParityMatrixMarkdown(rows: ParityRow[] = parityMatrix) {
70
+ const sortedRows = [...rows].sort((a, b) => a.id.localeCompare(b.id));
71
+ return [
72
+ "# Content Agent/UI Action Parity Matrix",
73
+ "",
74
+ "This generated matrix tracks whether high-value Content UI operations use the same action surface agents can call, or have an explicit exception. Edit `matrix.ts`, then regenerate this file.",
75
+ "",
76
+ ...renderTable(sortedRows.map(renderRow)),
77
+ "",
78
+ ].join("\n");
79
+ }
80
+
81
+ const isCli = process.argv[1] === fileURLToPath(import.meta.url);
82
+
83
+ if (isCli) {
84
+ const markdown = renderParityMatrixMarkdown();
85
+ if (process.argv.includes("--write")) {
86
+ writeFileSync(new URL("./matrix.md", import.meta.url), markdown);
87
+ } else {
88
+ process.stdout.write(markdown);
89
+ }
90
+ }
@@ -0,0 +1,57 @@
1
+ import {
2
+ contains,
3
+ createScorer,
4
+ defineEval,
5
+ type AgentRunOutput,
6
+ type Eval,
7
+ } from "@agent-native/core/eval";
8
+
9
+ import type { ParityEvalScenario } from "./eval-scenarios.ts";
10
+
11
+ function expectedToolScorer(expectedTools: string[]) {
12
+ return createScorer<AgentRunOutput, { used: string[]; missing: string[] }>({
13
+ name: "expected_tools",
14
+ analyze(run) {
15
+ const usedTools = new Set(run.toolCalls);
16
+ return {
17
+ used: expectedTools.filter((tool) => usedTools.has(tool)),
18
+ missing: expectedTools.filter((tool) => !usedTools.has(tool)),
19
+ };
20
+ },
21
+ generateScore({ missing }) {
22
+ return expectedTools.length === 0 || missing.length === 0 ? 1 : 0;
23
+ },
24
+ generateReason({ analysis: { used, missing } }) {
25
+ if (missing.length === 0) {
26
+ return `Agent called all expected tool(s): ${used.join(", ")}`;
27
+ }
28
+ return `Called expected tool(s): ${used.join(", ") || "none"}; missing: ${missing.join(", ")}`;
29
+ },
30
+ });
31
+ }
32
+
33
+ export function scenarioToEval(scenario: ParityEvalScenario): Eval {
34
+ const name = `content-parity:${scenario.id}`;
35
+
36
+ if (!process.env[scenario.gateEnv]) {
37
+ return defineEval({
38
+ name,
39
+ input: { prompt: scenario.prompt },
40
+ threshold: 1,
41
+ skipReason: `Skipped because ${scenario.gateEnv} is unset`,
42
+ scorers: [],
43
+ });
44
+ }
45
+
46
+ return defineEval({
47
+ name,
48
+ input: { prompt: scenario.prompt },
49
+ threshold: 0.6,
50
+ scorers: [
51
+ contains(scenario.successSignals),
52
+ ...(scenario.expectedTools?.length
53
+ ? [expectedToolScorer(scenario.expectedTools)]
54
+ : []),
55
+ ],
56
+ });
57
+ }
@@ -646,6 +646,10 @@ export interface ContentDatabaseResponse {
646
646
  createdDocumentId?: string;
647
647
  duplicatedItemId?: string;
648
648
  duplicatedDocumentId?: string;
649
+ duplicatedItemIds?: string[];
650
+ duplicatedDocumentIds?: string[];
651
+ deletedItemIds?: string[];
652
+ deletedDocumentIds?: string[];
649
653
  }
650
654
 
651
655
  export interface ContentDatabaseUnavailableResponse {
@@ -701,6 +705,13 @@ export interface DuplicateDatabaseItemRequest {
701
705
  title?: string;
702
706
  }
703
707
 
708
+ export interface DatabaseItemsBatchRequest {
709
+ databaseId?: string;
710
+ documentId?: string;
711
+ itemIds?: string[];
712
+ documentIds?: string[];
713
+ }
714
+
704
715
  export interface MoveDatabaseItemRequest {
705
716
  itemId?: string;
706
717
  documentId?: string;
@@ -20,7 +20,7 @@ import {
20
20
  getRequestOrgId,
21
21
  getRequestUserEmail,
22
22
  } from "@agent-native/core/server/request-context";
23
- import { accessFilter, assertAccess } from "@agent-native/core/sharing";
23
+ import { assertAccess } from "@agent-native/core/sharing";
24
24
  import { and, desc, eq } from "drizzle-orm";
25
25
  import { nanoid } from "nanoid";
26
26
  import { z } from "zod";
@@ -70,6 +70,33 @@ const trackSchema = z.object({
70
70
 
71
71
  // ─── Helpers ──────────────────────────────────────────────────────────────────
72
72
 
73
+ function nonEmptyString(value: unknown): string | null {
74
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
75
+ }
76
+
77
+ export function resolveMotionTimelineInsertOwnership(args: {
78
+ requestUserEmail?: string | null;
79
+ requestOrgId?: string | null;
80
+ designOwnerEmail?: unknown;
81
+ designOrgId?: unknown;
82
+ }): { ownerEmail: string; orgId: string | null } {
83
+ const ownerEmail =
84
+ nonEmptyString(args.requestUserEmail) ??
85
+ nonEmptyString(args.designOwnerEmail);
86
+
87
+ if (!ownerEmail) throw new Error("no authenticated user");
88
+
89
+ return {
90
+ ownerEmail,
91
+ orgId:
92
+ nonEmptyString(args.requestOrgId) ?? nonEmptyString(args.designOrgId),
93
+ };
94
+ }
95
+
96
+ export function canPatchManagedMotionCss(content: string): boolean {
97
+ return /<\s*(?:!doctype|[a-z][a-z0-9:-]*(?:\s|>|\/>))/i.test(content);
98
+ }
99
+
73
100
  async function persistFileContent(
74
101
  fileId: string,
75
102
  designId: string,
@@ -179,16 +206,13 @@ export default defineAction({
179
206
  includeContent,
180
207
  currentContent: currentContentInput,
181
208
  }) => {
182
- await assertAccess("design", designId, "editor");
209
+ const access = await assertAccess("design", designId, "editor");
183
210
 
184
211
  const db = getDb();
185
212
  const now = new Date().toISOString();
186
213
 
187
214
  // ── 1. Resolve the target design file ──────────────────────────────────
188
- const conditions = [
189
- accessFilter(schema.designs, schema.designShares),
190
- eq(schema.designFiles.designId, designId),
191
- ];
215
+ const conditions = [eq(schema.designFiles.designId, designId)];
192
216
  if (fileIdInput) {
193
217
  conditions.push(eq(schema.designFiles.id, fileIdInput));
194
218
  } else {
@@ -256,7 +280,10 @@ export default defineAction({
256
280
  });
257
281
 
258
282
  // ── 3. Inject the managed CSS block into the HTML ───────────────────────
259
- const patchedContent = injectManagedMotionCss(currentContent, css);
283
+ const contentPatched = canPatchManagedMotionCss(currentContent);
284
+ const patchedContent = contentPatched
285
+ ? injectManagedMotionCss(currentContent, css)
286
+ : currentContent;
260
287
  const bytesBefore = currentContent.length;
261
288
  const bytesAfter = patchedContent.length;
262
289
 
@@ -303,11 +330,18 @@ export default defineAction({
303
330
  if (existingForSource) {
304
331
  existingTimelineId = existingForSource.id;
305
332
  } else {
306
- // Insert new row — derive ownership from the request context (same
307
- // pattern as create-design-state and other create actions).
308
- insertOwnerEmail = getRequestUserEmail() ?? null;
309
- if (!insertOwnerEmail) throw new Error("no authenticated user");
310
- insertOrgId = getRequestOrgId() ?? null;
333
+ // Insert new row — derive ownership from the request context, falling
334
+ // back to the already-authorized design owner for local/public editor
335
+ // sessions that do not carry an authenticated request user.
336
+ const insertOwnership = resolveMotionTimelineInsertOwnership({
337
+ requestUserEmail: getRequestUserEmail(),
338
+ requestOrgId: getRequestOrgId(),
339
+ designOwnerEmail: (access.resource as { ownerEmail?: unknown })
340
+ .ownerEmail,
341
+ designOrgId: (access.resource as { orgId?: unknown }).orgId,
342
+ });
343
+ insertOwnerEmail = insertOwnership.ownerEmail;
344
+ insertOrgId = insertOwnership.orgId;
311
345
  }
312
346
  }
313
347
 
@@ -352,12 +386,9 @@ export default defineAction({
352
386
  // Written after the row so a SQL failure here leaves the timeline row
353
387
  // accurate (correct tracks + hash) and the stale HTML can be recompiled on
354
388
  // the next apply-motion-edit call via compiledHash drift detection.
355
- const updatedAt = await persistFileContent(
356
- fileId,
357
- designId,
358
- patchedContent,
359
- now,
360
- );
389
+ const updatedAt = contentPatched
390
+ ? await persistFileContent(fileId, designId, patchedContent, now)
391
+ : now;
361
392
 
362
393
  return {
363
394
  timelineId: resolvedTimelineId,
@@ -371,6 +402,7 @@ export default defineAction({
371
402
  bytesAfter,
372
403
  bytesDelta: bytesAfter - bytesBefore,
373
404
  persisted: true,
405
+ contentPatched,
374
406
  patchedContent: includeContent ? patchedContent : undefined,
375
407
  };
376
408
  },
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { getText, hasCollabState } from "@agent-native/core/collab";
3
- import { accessFilter } from "@agent-native/core/sharing";
3
+ import { assertAccess } from "@agent-native/core/sharing";
4
4
  import { and, desc, eq } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
@@ -67,13 +67,8 @@ async function readManagedCssForSource(args: {
67
67
  content: schema.designFiles.content,
68
68
  })
69
69
  .from(schema.designFiles)
70
- .innerJoin(
71
- schema.designs,
72
- eq(schema.designFiles.designId, schema.designs.id),
73
- )
74
70
  .where(
75
71
  and(
76
- accessFilter(schema.designs, schema.designShares),
77
72
  eq(schema.designFiles.designId, args.designId),
78
73
  eq(schema.designFiles.id, args.sourceRef),
79
74
  ),
@@ -124,12 +119,11 @@ export default defineAction({
124
119
  ),
125
120
  }),
126
121
  run: async ({ designId, timelineId, sourceRef }) => {
122
+ await assertAccess("design", designId, "viewer");
123
+
127
124
  const db = getDb();
128
125
 
129
- const conditions = [
130
- accessFilter(schema.designs, schema.designShares),
131
- eq(schema.motionTimeline.designId, designId),
132
- ];
126
+ const conditions = [eq(schema.motionTimeline.designId, designId)];
133
127
 
134
128
  if (timelineId) {
135
129
  conditions.push(eq(schema.motionTimeline.id, timelineId));
@@ -151,10 +145,6 @@ export default defineAction({
151
145
  updatedAt: schema.motionTimeline.updatedAt,
152
146
  })
153
147
  .from(schema.motionTimeline)
154
- .innerJoin(
155
- schema.designs,
156
- eq(schema.motionTimeline.designId, schema.designs.id),
157
- )
158
148
  .where(and(...conditions))
159
149
  .orderBy(desc(schema.motionTimeline.updatedAt))
160
150
  .limit(timelineId ? 1 : 100);
@@ -333,6 +333,13 @@ interface DesignCanvasProps {
333
333
  contentOffsetY?: number;
334
334
  };
335
335
  boardSurface?: boolean;
336
+ /**
337
+ * Optional live document replacement channel. When paired with
338
+ * `runtimeReplacementKey`, this lets callers update iframe DOM through the
339
+ * editor bridge without changing `srcDoc` and reloading the iframe.
340
+ */
341
+ runtimeReplacementContent?: string;
342
+ runtimeReplacementKey?: string;
336
343
  embeddedFrameBackground?: string;
337
344
  transparentBackground?: boolean;
338
345
  editorChromeScaleX?: number;
@@ -367,6 +374,7 @@ interface DesignCanvasProps {
367
374
  onElementDblClickText?: (info: ElementInfo) => void;
368
375
  onIframeHotkey?: (event: IframeHotkeyPayload) => void;
369
376
  onIframeContextMenu?: (event: IframeContextMenuPayload) => void;
377
+ onEditorDragStateChange?: (active: boolean) => void;
370
378
  onVisualStructureChange?: (
371
379
  selector: string,
372
380
  anchorSelector: string,
@@ -376,6 +384,9 @@ interface DesignCanvasProps {
376
384
  sourceId?: string;
377
385
  anchorSourceId?: string;
378
386
  requestId?: string;
387
+ dropMode?: "flow-insert" | "absolute-container";
388
+ sourceRect?: { x: number; y: number; width: number; height: number };
389
+ anchorRect?: { x: number; y: number; width: number; height: number };
379
390
  },
380
391
  ) => boolean | void;
381
392
  onVisualDuplicateChange?: (
@@ -571,6 +582,26 @@ function injectEmbeddedFrameStyle(content: string, style: string): string {
571
582
  return `${style}${content}`;
572
583
  }
573
584
 
585
+ export function getEmbeddedFrameDocumentContent(args: {
586
+ content: string;
587
+ embeddedFrameBackground?: string;
588
+ transparentBackground?: boolean;
589
+ contentOffsetX?: number;
590
+ contentOffsetY?: number;
591
+ }): string {
592
+ const frameStyle = [
593
+ getEmbeddedFrameBackgroundStyle({
594
+ embeddedFrameBackground: args.embeddedFrameBackground,
595
+ transparentBackground: args.transparentBackground,
596
+ }),
597
+ embeddedContentOffsetStyle(
598
+ args.contentOffsetX ?? 0,
599
+ args.contentOffsetY ?? 0,
600
+ ),
601
+ ].join("");
602
+ return injectEmbeddedFrameStyle(args.content, frameStyle);
603
+ }
604
+
574
605
  export interface IframeHotkeyPayload {
575
606
  key: string;
576
607
  code: string;
@@ -602,6 +633,8 @@ export function DesignCanvas({
602
633
  deviceFrame,
603
634
  embeddedFrame,
604
635
  boardSurface = false,
636
+ runtimeReplacementContent,
637
+ runtimeReplacementKey,
605
638
  embeddedFrameBackground,
606
639
  transparentBackground = false,
607
640
  editorChromeScaleX = 1,
@@ -621,6 +654,7 @@ export function DesignCanvas({
621
654
  onElementDblClickText,
622
655
  onIframeHotkey,
623
656
  onIframeContextMenu,
657
+ onEditorDragStateChange,
624
658
  onVisualStructureChange,
625
659
  onVisualDuplicateChange,
626
660
  tweakValues,
@@ -652,6 +686,9 @@ export function DesignCanvas({
652
686
  const scrollContainerRef = useRef<HTMLDivElement>(null);
653
687
  const zoomRef = useRef(zoom);
654
688
  const previousContentKeyRef = useRef(contentKey);
689
+ const runtimeReplacementContentRef = useRef(runtimeReplacementContent);
690
+ const runtimeReplacementKeyRef = useRef(runtimeReplacementKey);
691
+ const lastRuntimeReplacementKeyRef = useRef(runtimeReplacementKey);
655
692
  const [renderedContent, setRenderedContent] = useState(content);
656
693
  const [annotationPins, setAnnotationPins] = useState<CanvasPin[]>([]);
657
694
  const [pinSubmitSignal, setPinSubmitSignal] = useState(0);
@@ -707,6 +744,8 @@ export function DesignCanvas({
707
744
  const waitingForEditableExternalSnapshot =
708
745
  requiresEditableExternalSnapshot && !activeExternalSnapshotHtml;
709
746
  zoomRef.current = zoom;
747
+ runtimeReplacementContentRef.current = runtimeReplacementContent;
748
+ runtimeReplacementKeyRef.current = runtimeReplacementKey;
710
749
 
711
750
  useEffect(() => {
712
751
  onExternalContentSnapshotRef.current = onExternalContentSnapshot;
@@ -819,6 +858,7 @@ export function DesignCanvas({
819
858
  useEffect(() => {
820
859
  if (previousContentKeyRef.current !== contentKey) {
821
860
  previousContentKeyRef.current = contentKey;
861
+ lastRuntimeReplacementKeyRef.current = runtimeReplacementKey;
822
862
  setRenderedContent(content);
823
863
  }
824
864
  // Same-screen visual edits are already applied optimistically inside the
@@ -826,7 +866,7 @@ export function DesignCanvas({
826
866
  // reloads the iframe, flashes unstyled content, and drops selection. Only a
827
867
  // content-key change (screen switch / explicit remount) should replace the
828
868
  // iframe document here; the bridge replays inspector state after that load.
829
- }, [content, contentKey]);
869
+ }, [content, contentKey, runtimeReplacementKey]);
830
870
 
831
871
  usePinchZoom({
832
872
  containerRef: scrollContainerRef,
@@ -879,6 +919,20 @@ export function DesignCanvas({
879
919
  NAV_BRIDGE_SCRIPT +
880
920
  embeddedWheelBridge +
881
921
  editorChromeBridge;
922
+ const frameContent = getEmbeddedFrameDocumentContent({
923
+ content: iframeRenderContent,
924
+ embeddedFrameBackground,
925
+ transparentBackground,
926
+ contentOffsetX: embeddedFrame?.contentOffsetX ?? 0,
927
+ contentOffsetY: embeddedFrame?.contentOffsetY ?? 0,
928
+ });
929
+ if (frameContent.includes("</body>")) {
930
+ return frameContent.replace("</body>", bridgeToInject + "</body>"); // i18n-ignore generated iframe HTML injection
931
+ }
932
+ if (frameContent.includes("</html>")) {
933
+ return frameContent.replace("</html>", bridgeToInject + "</html>"); // i18n-ignore generated iframe HTML injection
934
+ }
935
+ // No body/html tags — wrap it
882
936
  const frameStyle = [
883
937
  getEmbeddedFrameBackgroundStyle({
884
938
  embeddedFrameBackground,
@@ -889,17 +943,6 @@ export function DesignCanvas({
889
943
  embeddedFrame?.contentOffsetY ?? 0,
890
944
  ),
891
945
  ].join("");
892
- const frameContent = injectEmbeddedFrameStyle(
893
- iframeRenderContent,
894
- frameStyle,
895
- );
896
- if (frameContent.includes("</body>")) {
897
- return frameContent.replace("</body>", bridgeToInject + "</body>"); // i18n-ignore generated iframe HTML injection
898
- }
899
- if (frameContent.includes("</html>")) {
900
- return frameContent.replace("</html>", bridgeToInject + "</html>"); // i18n-ignore generated iframe HTML injection
901
- }
902
- // No body/html tags — wrap it
903
946
  return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${frameStyle}</head><body>${iframeRenderContent}${bridgeToInject}</body></html>`;
904
947
  // editorChromeScaleX/Y are intentionally NOT deps: they only seed the initial
905
948
  // baked chrome scale. Live zoom updates flow through the set-editor-chrome-scale
@@ -968,6 +1011,10 @@ export function DesignCanvas({
968
1011
  if (e.data.type === "element-hover") {
969
1012
  onElementHover(e.data.payload);
970
1013
  }
1014
+ if (e.data.type === "agent-native:editor-drag-state") {
1015
+ onEditorDragStateChange?.(Boolean(e.data.active));
1016
+ return;
1017
+ }
971
1018
  if (e.data.type === "visual-style-change") {
972
1019
  const selector = String(e.data.selector || "");
973
1020
  const styles =
@@ -1001,6 +1048,39 @@ export function DesignCanvas({
1001
1048
  typeof e.data.anchorSourceId === "string"
1002
1049
  ? e.data.anchorSourceId
1003
1050
  : undefined;
1051
+ const dropMode =
1052
+ e.data.dropMode === "flow-insert" ||
1053
+ e.data.dropMode === "absolute-container"
1054
+ ? e.data.dropMode
1055
+ : undefined;
1056
+ const sourceRect =
1057
+ e.data.sourceRect &&
1058
+ typeof e.data.sourceRect === "object" &&
1059
+ Number.isFinite(e.data.sourceRect.x) &&
1060
+ Number.isFinite(e.data.sourceRect.y) &&
1061
+ Number.isFinite(e.data.sourceRect.width) &&
1062
+ Number.isFinite(e.data.sourceRect.height)
1063
+ ? {
1064
+ x: Number(e.data.sourceRect.x),
1065
+ y: Number(e.data.sourceRect.y),
1066
+ width: Number(e.data.sourceRect.width),
1067
+ height: Number(e.data.sourceRect.height),
1068
+ }
1069
+ : undefined;
1070
+ const anchorRect =
1071
+ e.data.anchorRect &&
1072
+ typeof e.data.anchorRect === "object" &&
1073
+ Number.isFinite(e.data.anchorRect.x) &&
1074
+ Number.isFinite(e.data.anchorRect.y) &&
1075
+ Number.isFinite(e.data.anchorRect.width) &&
1076
+ Number.isFinite(e.data.anchorRect.height)
1077
+ ? {
1078
+ x: Number(e.data.anchorRect.x),
1079
+ y: Number(e.data.anchorRect.y),
1080
+ width: Number(e.data.anchorRect.width),
1081
+ height: Number(e.data.anchorRect.height),
1082
+ }
1083
+ : undefined;
1004
1084
  if (
1005
1085
  (selector || sourceId) &&
1006
1086
  (anchorSelector || anchorSourceId) &&
@@ -1017,6 +1097,9 @@ export function DesignCanvas({
1017
1097
  requestId,
1018
1098
  sourceId,
1019
1099
  anchorSourceId,
1100
+ dropMode,
1101
+ sourceRect,
1102
+ anchorRect,
1020
1103
  },
1021
1104
  );
1022
1105
  if (requestId) {
@@ -1209,6 +1292,7 @@ export function DesignCanvas({
1209
1292
  onElementDblClickText,
1210
1293
  onIframeHotkey,
1211
1294
  onIframeContextMenu,
1295
+ onEditorDragStateChange,
1212
1296
  onVisualStructureChange,
1213
1297
  onVisualDuplicateChange,
1214
1298
  onZoomChange,
@@ -1519,6 +1603,66 @@ export function DesignCanvas({
1519
1603
  [],
1520
1604
  );
1521
1605
 
1606
+ const replaceRuntimeContentInPlace = useCallback(
1607
+ (nextContent: string) => {
1608
+ if (externalPreviewUrl) return false;
1609
+ return replacePreviewContent(
1610
+ getEmbeddedFrameDocumentContent({
1611
+ content: nextContent,
1612
+ embeddedFrameBackground,
1613
+ transparentBackground,
1614
+ contentOffsetX: embeddedFrame?.contentOffsetX ?? 0,
1615
+ contentOffsetY: embeddedFrame?.contentOffsetY ?? 0,
1616
+ }),
1617
+ null,
1618
+ [],
1619
+ { forceFullDocument: true },
1620
+ );
1621
+ },
1622
+ [
1623
+ embeddedFrame?.contentOffsetX,
1624
+ embeddedFrame?.contentOffsetY,
1625
+ embeddedFrameBackground,
1626
+ externalPreviewUrl,
1627
+ replacePreviewContent,
1628
+ transparentBackground,
1629
+ ],
1630
+ );
1631
+
1632
+ useEffect(() => {
1633
+ if (
1634
+ runtimeReplacementKey === undefined ||
1635
+ runtimeReplacementContent === undefined ||
1636
+ lastRuntimeReplacementKeyRef.current === runtimeReplacementKey
1637
+ ) {
1638
+ return;
1639
+ }
1640
+ if (replaceRuntimeContentInPlace(runtimeReplacementContent)) {
1641
+ lastRuntimeReplacementKeyRef.current = runtimeReplacementKey;
1642
+ }
1643
+ }, [
1644
+ replaceRuntimeContentInPlace,
1645
+ runtimeReplacementContent,
1646
+ runtimeReplacementKey,
1647
+ ]);
1648
+
1649
+ const runtimeReplacementEnabled = runtimeReplacementKey !== undefined;
1650
+ useEffect(() => {
1651
+ if (!runtimeReplacementEnabled) return;
1652
+ const iframe = iframeRef.current;
1653
+ if (!iframe) return;
1654
+ const replaceLatestRuntimeContent = () => {
1655
+ const nextContent = runtimeReplacementContentRef.current;
1656
+ if (nextContent === undefined) return;
1657
+ if (replaceRuntimeContentInPlace(nextContent)) {
1658
+ lastRuntimeReplacementKeyRef.current = runtimeReplacementKeyRef.current;
1659
+ }
1660
+ };
1661
+ iframe.addEventListener("load", replaceLatestRuntimeContent);
1662
+ return () =>
1663
+ iframe.removeEventListener("load", replaceLatestRuntimeContent);
1664
+ }, [replaceRuntimeContentInPlace, runtimeReplacementEnabled]);
1665
+
1522
1666
  const deleteRuntimeElement = useCallback(
1523
1667
  (selector?: string | null, candidates?: string[]) => {
1524
1668
  const iframe = iframeRef.current;
@@ -449,15 +449,14 @@ export function MotionDock({
449
449
 
450
450
  {/* Dock toolbar */}
451
451
  <div className="flex h-8 shrink-0 items-center gap-1 border-b border-border px-2">
452
- {/* Collapse toggle. The label and nearby space are part of the target. */}
452
+ {/* Collapse toggle. The rail owns the visible Motion label. */}
453
453
  <button
454
454
  type="button"
455
- className="-ml-1 flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-1.5 !text-[11px] font-medium uppercase tracking-wide text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
455
+ className="-ml-1 flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]"
456
456
  onClick={() => setOpen(false)}
457
457
  aria-label="Collapse motion dock"
458
458
  >
459
459
  <IconChevronDown className="size-3.5" />
460
- <span>Motion</span>
461
460
  </button>
462
461
 
463
462
  <>