@agent-native/core 0.165.4 → 0.167.1

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 (224) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/secrets/SKILL.md +5 -1
  3. package/corpus/templates/analytics/.agents/skills/workspace-conventions/SKILL.md +4 -1
  4. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +93 -56
  5. package/corpus/templates/analytics/app/i18n-data.ts +23 -20
  6. package/corpus/templates/analytics/app/pages/Settings.tsx +0 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +0 -1
  8. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  9. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +2 -1
  10. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +0 -1
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +0 -32
  12. package/corpus/templates/analytics/server/lib/session-replay.ts +1 -5
  13. package/corpus/templates/assets/.agents/skills/secrets/SKILL.md +5 -1
  14. package/corpus/templates/assets/.agents/skills/workspace-conventions/SKILL.md +4 -1
  15. package/corpus/templates/assets/actions/navigate.ts +2 -2
  16. package/corpus/templates/assets/actions/save-generated-image.ts +0 -5
  17. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +2 -262
  18. package/corpus/templates/assets/app/routes/library.tsx +1 -1
  19. package/corpus/templates/assets/app/routes/settings.tsx +0 -3
  20. package/corpus/templates/brain/.agents/skills/secrets/SKILL.md +5 -1
  21. package/corpus/templates/brain/.agents/skills/workspace-conventions/SKILL.md +4 -1
  22. package/corpus/templates/brain/actions/ask-brain.ts +0 -1
  23. package/corpus/templates/brain/actions/navigate.ts +2 -2
  24. package/corpus/templates/brain/app/routes/sources.tsx +0 -4
  25. package/corpus/templates/brain/server/lib/capture-sanitization.ts +0 -11
  26. package/corpus/templates/calendar/.agents/skills/secrets/SKILL.md +5 -1
  27. package/corpus/templates/calendar/.agents/skills/workspace-conventions/SKILL.md +4 -1
  28. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +0 -20
  29. package/corpus/templates/calendar/app/pages/CalendarView.tsx +0 -1
  30. package/corpus/templates/chat/.agents/skills/secrets/SKILL.md +5 -1
  31. package/corpus/templates/chat/.agents/skills/workspace-conventions/SKILL.md +4 -1
  32. package/corpus/templates/chat/actions/navigate.ts +2 -2
  33. package/corpus/templates/clips/.agents/skills/secrets/SKILL.md +5 -1
  34. package/corpus/templates/clips/.agents/skills/workspace-conventions/SKILL.md +4 -1
  35. package/corpus/templates/clips/CHANGELOG.md +2 -0
  36. package/corpus/templates/clips/actions/import-loom-recording.ts +16 -4
  37. package/corpus/templates/clips/actions/navigate.ts +2 -2
  38. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +0 -1
  39. package/corpus/templates/clips/app/components/library/space-dialogs.tsx +0 -1
  40. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +0 -1
  41. package/corpus/templates/clips/app/components/player/clips-share-trigger.tsx +17 -0
  42. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +3 -3
  43. package/corpus/templates/clips/app/components/player/video-player.tsx +2 -2
  44. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +0 -1
  45. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +0 -1
  46. package/corpus/templates/clips/app/global.css +31 -0
  47. package/corpus/templates/clips/app/hooks/use-playback-position.ts +0 -2
  48. package/corpus/templates/clips/app/lib/download-release-channel.ts +10 -0
  49. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +1 -3
  50. package/corpus/templates/clips/app/routes/download.tsx +67 -44
  51. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -12
  52. package/corpus/templates/clips/app/routes/record.tsx +6 -6
  53. package/corpus/templates/clips/app/routes/share.$shareId.tsx +26 -29
  54. package/corpus/templates/clips/changelog/2026-08-20-clip-viewer-share-actions.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-08-20-preparing-recording-countdown-overlay.md +1 -0
  56. package/corpus/templates/clips/chrome-extension/src/background.ts +0 -13
  57. package/corpus/templates/clips/chrome-extension/src/overlay.ts +0 -1
  58. package/corpus/templates/clips/chrome-extension/src/popup.ts +0 -20
  59. package/corpus/templates/clips/desktop/src/app.tsx +6 -18
  60. package/corpus/templates/clips/desktop/src/hooks/useMediaDevices.ts +0 -1
  61. package/corpus/templates/clips/desktop/src/lib/recorder.ts +0 -8
  62. package/corpus/templates/content/.agents/skills/secrets/SKILL.md +5 -1
  63. package/corpus/templates/content/.agents/skills/workspace-conventions/SKILL.md +4 -1
  64. package/corpus/templates/content/actions/_database-source-utils.ts +0 -3
  65. package/corpus/templates/content/actions/attach-content-database-source.ts +1 -4
  66. package/corpus/templates/content/actions/navigate.ts +2 -2
  67. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
  68. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +0 -6
  69. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +0 -580
  70. package/corpus/templates/content/app/components/editor/database/FormView.tsx +2 -2
  71. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +1 -1
  72. package/corpus/templates/content/app/components/editor/database/settings.tsx +0 -23
  73. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +0 -1
  74. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +8 -304
  75. package/corpus/templates/content/app/hooks/use-documents.ts +0 -1
  76. package/corpus/templates/content/shared/api.ts +0 -1
  77. package/corpus/templates/content/shared/builder-mdx.ts +0 -1
  78. package/corpus/templates/content/shared/nfm.ts +1 -7
  79. package/corpus/templates/crm/.agents/skills/secrets/SKILL.md +5 -1
  80. package/corpus/templates/crm/.agents/skills/workspace-conventions/SKILL.md +4 -1
  81. package/corpus/templates/crm/actions/merge-crm-records.ts +0 -4
  82. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +0 -1
  83. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +0 -1
  84. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +0 -1
  85. package/corpus/templates/crm/server/db/crm-store.ts +1 -1
  86. package/corpus/templates/design/.agents/skills/secrets/SKILL.md +5 -1
  87. package/corpus/templates/design/.agents/skills/workspace-conventions/SKILL.md +4 -1
  88. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -14
  89. package/corpus/templates/design/AGENTS.md +2 -1
  90. package/corpus/templates/design/actions/apply-component-prop-edit.ts +1 -5
  91. package/corpus/templates/design/actions/generate-design.ts +0 -1
  92. package/corpus/templates/design/actions/generate-screens.ts +5 -2
  93. package/corpus/templates/design/actions/go-to-main-component.ts +2 -2
  94. package/corpus/templates/design/actions/list-design-systems.ts +10 -0
  95. package/corpus/templates/design/actions/navigate.ts +2 -2
  96. package/corpus/templates/design/actions/open-component-source.ts +4 -4
  97. package/corpus/templates/design/actions/present-design-variants.ts +1 -2
  98. package/corpus/templates/design/actions/run-design-audit.ts +0 -22
  99. package/corpus/templates/design/actions/show-design-questions.ts +5 -2
  100. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -8
  101. package/corpus/templates/design/app/components/design/EditPanel.tsx +11 -183
  102. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +21 -4
  103. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +1 -5
  104. package/corpus/templates/design/app/components/design/bridge/codegen.ts +1 -1
  105. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +10 -19
  106. package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +1 -1
  107. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +13 -3
  108. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +38 -32
  109. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +1 -4
  110. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +0 -4
  111. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  112. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +2 -2
  113. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +0 -1
  114. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +1 -1
  115. package/corpus/templates/design/app/i18n-data.ts +122 -0
  116. package/corpus/templates/design/app/lib/clipboard-content-lineage.ts +11 -1
  117. package/corpus/templates/design/app/pages/DesignEditor.tsx +50 -254
  118. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +163 -8
  119. package/corpus/templates/design/app/pages/Index.tsx +17 -0
  120. package/corpus/templates/design/app/pages/design-editor/canvas-primitive-insert.ts +44 -32
  121. package/corpus/templates/design/app/pages/design-editor/canvas-primitives.ts +10 -0
  122. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +49 -3
  123. package/corpus/templates/design/app/pages/design-editor/clone-idrefs.ts +31 -0
  124. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +28 -0
  125. package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +3 -3
  126. package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +44 -14
  127. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +16 -5
  128. package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +1 -1
  129. package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +1 -1
  130. package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +1 -1
  131. package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +1 -1
  132. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +142 -17
  133. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +1 -0
  134. package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +1 -1
  135. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +1 -0
  136. package/corpus/templates/design/app/pages/design-editor/cross-screen-text-color.ts +25 -9
  137. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +1 -0
  138. package/corpus/templates/design/app/pages/design-editor/measure-child-rects.ts +83 -0
  139. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +66 -0
  140. package/corpus/templates/design/server/routes/[...page].get.ts +0 -1
  141. package/corpus/templates/design/shared/code-layer.ts +120 -70
  142. package/corpus/templates/design/shared/design-surface-index.ts +0 -1
  143. package/corpus/templates/design/shared/pen-path.ts +0 -4
  144. package/corpus/templates/dispatch/.agents/skills/secrets/SKILL.md +5 -1
  145. package/corpus/templates/dispatch/.agents/skills/workspace-conventions/SKILL.md +4 -1
  146. package/corpus/templates/dispatch/app/routes/integrations.tsx +0 -109
  147. package/corpus/templates/factory/.agents/skills/secrets/SKILL.md +5 -1
  148. package/corpus/templates/factory/.agents/skills/workspace-conventions/SKILL.md +4 -1
  149. package/corpus/templates/factory/actions/navigate.ts +2 -2
  150. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +0 -13
  151. package/corpus/templates/factory/app/routes/factory.tsx +0 -1
  152. package/corpus/templates/factory/server/factory-graph/store.ts +0 -1
  153. package/corpus/templates/forms/.agents/skills/secrets/SKILL.md +5 -1
  154. package/corpus/templates/forms/.agents/skills/workspace-conventions/SKILL.md +4 -1
  155. package/corpus/templates/macros/.agents/skills/secrets/SKILL.md +5 -1
  156. package/corpus/templates/macros/.agents/skills/workspace-conventions/SKILL.md +4 -1
  157. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -1
  158. package/corpus/templates/mail/.agents/skills/secrets/SKILL.md +5 -1
  159. package/corpus/templates/mail/.agents/skills/workspace-conventions/SKILL.md +4 -1
  160. package/corpus/templates/mail/app/components/email/EmailList.tsx +0 -1
  161. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +2 -2
  162. package/corpus/templates/mail/shared/markdown.ts +3 -0
  163. package/corpus/templates/plan/.agents/skills/secrets/SKILL.md +5 -1
  164. package/corpus/templates/plan/.agents/skills/workspace-conventions/SKILL.md +4 -1
  165. package/corpus/templates/slides/.agents/skills/secrets/SKILL.md +5 -1
  166. package/corpus/templates/slides/.agents/skills/workspace-conventions/SKILL.md +4 -1
  167. package/corpus/templates/slides/actions/add-slide.ts +0 -1
  168. package/corpus/templates/slides/actions/list-decks.ts +1 -1
  169. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +1 -4
  170. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +0 -1
  171. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
  172. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -6
  173. package/corpus/templates/tasks/.agents/skills/secrets/SKILL.md +5 -1
  174. package/corpus/templates/tasks/.agents/skills/workspace-conventions/SKILL.md +4 -1
  175. package/corpus/templates/tasks/app/components/layout/Layout.tsx +0 -1
  176. package/corpus/templates/tasks/app/hooks/use-visible-task-fields.ts +1 -4
  177. package/corpus/templates/tasks/server/custom-fields/store.ts +1 -6
  178. package/corpus/templates/tasks/shared/list-screen-snapshot.ts +2 -2
  179. package/dist/agent/engine/registry.js +27 -14
  180. package/dist/agent/index.d.ts +1 -1
  181. package/dist/agent/production-agent.d.ts +19 -6
  182. package/dist/agent/production-agent.js +57 -16
  183. package/dist/agent/types.d.ts +3 -0
  184. package/dist/changelog/parse.js +219 -8
  185. package/dist/client/EnvironmentBadge.d.ts +1 -0
  186. package/dist/client/EnvironmentBadge.js +7 -0
  187. package/dist/collab/routes.d.ts +1 -1
  188. package/dist/collab/struct-routes.d.ts +1 -1
  189. package/dist/config.d.ts +11 -0
  190. package/dist/config.js +248 -0
  191. package/dist/deploy/build.js +12 -1
  192. package/dist/index.d.ts +1 -1
  193. package/dist/observability/routes.d.ts +3 -3
  194. package/dist/progress/routes.d.ts +1 -1
  195. package/dist/resources/handlers.d.ts +2 -2
  196. package/dist/secrets/routes.d.ts +9 -9
  197. package/dist/server/agent-chat/plugin-options.d.ts +3 -2
  198. package/dist/server/agent-chat-plugin.js +5 -2
  199. package/dist/server/index.d.ts +1 -1
  200. package/dist/templates/chat/.agents/skills/secrets/SKILL.md +5 -1
  201. package/dist/templates/chat/.agents/skills/workspace-conventions/SKILL.md +4 -1
  202. package/dist/templates/chat/actions/navigate.ts +2 -2
  203. package/dist/templates/default/.agents/skills/secrets/SKILL.md +5 -1
  204. package/dist/templates/default/.agents/skills/workspace-conventions/SKILL.md +4 -1
  205. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +5 -1
  206. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +5 -1
  207. package/dist/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +4 -1
  208. package/dist/vite/agent-native-config-loader.d.ts +1 -0
  209. package/dist/vite/agent-native-config-loader.js +3 -3
  210. package/dist/vite/agents-bundle-plugin.d.ts +1 -1
  211. package/dist/vite/agents-bundle-plugin.js +13 -1
  212. package/dist/vite/client.js +14 -14
  213. package/docs/content/agent-native-config.mdx +62 -11
  214. package/docs/content/agent-surfaces.mdx +6 -0
  215. package/docs/content/environment-variables.mdx +5 -2
  216. package/package.json +3 -3
  217. package/src/templates/chat/.agents/skills/secrets/SKILL.md +5 -1
  218. package/src/templates/chat/.agents/skills/workspace-conventions/SKILL.md +4 -1
  219. package/src/templates/chat/actions/navigate.ts +2 -2
  220. package/src/templates/default/.agents/skills/secrets/SKILL.md +5 -1
  221. package/src/templates/default/.agents/skills/workspace-conventions/SKILL.md +4 -1
  222. package/src/templates/headless/.agents/skills/secrets/SKILL.md +5 -1
  223. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +5 -1
  224. package/src/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +4 -1
@@ -506,11 +506,39 @@ export function camelCaseCssProperty(property: string): string {
506
506
  );
507
507
  }
508
508
 
509
+ // The inspector reads font longhands, and a class-less element's computed
510
+ // styles are discarded by refreshedComputedStyles — so an authored `font`
511
+ // shorthand is the only place a family/size/weight can come from.
512
+ function fontShorthandLonghands(value: string): Record<string, string> {
513
+ const match =
514
+ /^\s*(.*?)\s*(-?[\d.]+(?:px|r?em|%|pt)|x{1,2}-(?:small|large)|small|medium|large)\s*(?:\/\s*([^\s]+)\s*)?(\S.*)$/.exec(
515
+ value,
516
+ );
517
+ if (!match) return {};
518
+ const [, leading, size, lineHeight, family] = match;
519
+ const longhands: Record<string, string> = {
520
+ fontSize: size,
521
+ fontFamily: family.trim(),
522
+ };
523
+ if (lineHeight) longhands.lineHeight = lineHeight;
524
+ for (const token of leading.split(/\s+/).filter(Boolean)) {
525
+ if (/^(?:normal|italic|oblique)$/.test(token)) longhands.fontStyle = token;
526
+ else if (/^(?:\d{3}|bold|bolder|lighter)$/.test(token)) {
527
+ longhands.fontWeight = token;
528
+ } else if (/^small-caps$/.test(token)) longhands.fontVariant = token;
529
+ }
530
+ return longhands;
531
+ }
532
+
509
533
  export function cssStyleAliases(
510
534
  styles: Record<string, string>,
511
535
  ): Record<string, string> {
512
536
  const result: Record<string, string> = {};
513
537
  for (const [property, value] of Object.entries(styles)) {
538
+ // Expanded in source order so a later explicit longhand still wins.
539
+ if (property === "font") {
540
+ Object.assign(result, fontShorthandLonghands(value));
541
+ }
514
542
  result[property] = value;
515
543
  if (property.includes("-")) {
516
544
  result[camelCaseCssProperty(property)] = value;
@@ -282,7 +282,7 @@ export function runAddAutoLayout({
282
282
  }
283
283
  }
284
284
  }
285
- applyLocalContentUpdate(nextContent, { skipPreview: true });
285
+ applyLocalContentUpdate(nextContent, { forcePreviewFullDocument: true });
286
286
  if (wrapperId) {
287
287
  const taggedProjection = buildCodeLayerProjection(nextContent);
288
288
  const wrapperNode = taggedProjection.nodes.find(
@@ -338,7 +338,7 @@ export function runAddAutoLayout({
338
338
  if (styled.result.status === "applied") nextContent = styled.content;
339
339
  }
340
340
  }
341
- applyLocalContentUpdate(nextContent, { skipPreview: true });
341
+ applyLocalContentUpdate(nextContent, { forcePreviewFullDocument: true });
342
342
  if (wrapperId) {
343
343
  const projectionAfter = buildCodeLayerProjection(nextContent);
344
344
  const wrapperNode = projectionAfter.nodes.find(
@@ -412,5 +412,5 @@ export function runAddAutoLayout({
412
412
  nextContent = paddingPatch.content;
413
413
  }
414
414
  }
415
- applyLocalContentUpdate(nextContent, { skipPreview: true });
415
+ applyLocalContentUpdate(nextContent, { forcePreviewFullDocument: true });
416
416
  }
@@ -66,8 +66,11 @@ export function runAlignSelection(
66
66
  }: AlignSelectionArgs,
67
67
  edge: DesignHotkeyAlignEdge,
68
68
  ) {
69
+ const abandon = (reason: string, data?: Record<string, unknown>) => {
70
+ trace("structure", "align-abandoned", { reason, edge, ...data });
71
+ };
69
72
  trace("structure", "align", { layers: selectedLayerIdsState.length });
70
- if (!canEditDesign) return;
73
+ if (!canEditDesign) return abandon("read-only");
71
74
 
72
75
  // Overview, 2+ selected SCREENS: align each screen's frame geometry to
73
76
  // the selection's combined bounding box through the same
@@ -82,7 +85,11 @@ export function runAlignSelection(
82
85
  fileIds: files.map((file) => file.id),
83
86
  })
84
87
  ) {
85
- if (overviewSelectedScreenIds.length < 2) return;
88
+ if (overviewSelectedScreenIds.length < 2) {
89
+ return abandon("overview: needs 2+ screens", {
90
+ selected: overviewSelectedScreenIds.length,
91
+ });
92
+ }
86
93
  const before = getCanvasFrameGeometry(designDataJsonRef.current);
87
94
  const screenRects: AlignableRect[] = [];
88
95
  overviewSelectedScreenIds.forEach((screenId) => {
@@ -100,7 +107,10 @@ export function runAlignSelection(
100
107
  : boardFileId === screenId
101
108
  ? boardFrameGeometry
102
109
  : undefined;
103
- if (!fallbackGeometry) return;
110
+ if (!fallbackGeometry) {
111
+ abandon("overview: screen has no geometry", { screenId });
112
+ return;
113
+ }
104
114
  const geometry = { ...fallbackGeometry, ...before[screenId] };
105
115
  screenRects.push({
106
116
  id: screenId,
@@ -110,9 +120,13 @@ export function runAlignSelection(
110
120
  height: geometry.height,
111
121
  });
112
122
  });
113
- if (screenRects.length < 2) return;
123
+ if (screenRects.length < 2) {
124
+ return abandon("overview: fewer than 2 measurable screens", {
125
+ measured: screenRects.length,
126
+ });
127
+ }
114
128
  const bounds = getFrameGroupBounds(screenRects);
115
- if (!bounds) return;
129
+ if (!bounds) return abandon("no combined bounds for selection");
116
130
  const positions = computeAlignedPositions(
117
131
  screenRects,
118
132
  {
@@ -123,7 +137,9 @@ export function runAlignSelection(
123
137
  },
124
138
  edge,
125
139
  );
126
- if (positions.size === 0) return;
140
+ if (positions.size === 0) {
141
+ return abandon("already aligned; nothing to move", { edge });
142
+ }
127
143
  const after = cloneCanvasFrameGeometry(before);
128
144
  positions.forEach((position, screenId) => {
129
145
  after[screenId] = { ...after[screenId]!, ...position };
@@ -133,22 +149,28 @@ export function runAlignSelection(
133
149
  }
134
150
 
135
151
  // Single-screen mode: in-screen DOM-node layers.
136
- if (!activeFile) return;
152
+ if (!activeFile) return abandon("no active file");
137
153
  const baseContent = getFreshActiveContent();
138
154
  const nodeIds = getActiveFileSelectedNodeIds(baseContent);
139
- if (nodeIds.length === 0) return;
155
+ if (nodeIds.length === 0) {
156
+ return abandon("selection has no nodes in the active file", {
157
+ selectedLayerIds: selectedLayerIdsState.length,
158
+ });
159
+ }
140
160
  const projection = buildCodeLayerProjection(baseContent);
141
161
  const nodesById = new Map(projection.nodes.map((node) => [node.id, node]));
142
162
  const selectedNodes = nodeIds
143
163
  .map((nodeId) => nodesById.get(nodeId))
144
164
  .filter((node): node is CodeLayerNode => Boolean(node));
145
- if (selectedNodes.length === 0) return;
165
+ if (selectedNodes.length === 0) {
166
+ return abandon("selected ids resolve to no projection nodes", { nodeIds });
167
+ }
146
168
  const selectedRects = selectedNodes.map(rectFromCodeLayerNode);
147
169
 
148
170
  if (selectedRects.length >= 2) {
149
171
  // Multi-selection: align to the selection's own combined bbox.
150
172
  const bounds = getFrameGroupBounds(selectedRects);
151
- if (!bounds) return;
173
+ if (!bounds) return abandon("no combined bounds for selection");
152
174
  const positions = computeAlignedPositions(
153
175
  selectedRects,
154
176
  {
@@ -159,7 +181,9 @@ export function runAlignSelection(
159
181
  },
160
182
  edge,
161
183
  );
162
- if (positions.size === 0) return;
184
+ if (positions.size === 0) {
185
+ return abandon("already aligned; nothing to move", { edge });
186
+ }
163
187
  commitNodePositions(baseContent, positions);
164
188
  return;
165
189
  }
@@ -169,15 +193,21 @@ export function runAlignSelection(
169
193
  // Figma (there's nothing to align a lone top-level frame against).
170
194
  const soleNode = selectedNodes[0]!;
171
195
  const parentId = soleNode.parentId;
172
- if (!parentId) return;
196
+ if (!parentId) {
197
+ return abandon("single selection has no parent to align against", {
198
+ nodeId: soleNode.id,
199
+ });
200
+ }
173
201
  const parentNode = nodesById.get(parentId);
174
- if (!parentNode) return;
202
+ if (!parentNode) return abandon("parent id not in projection", { parentId });
175
203
  const parentRect = rectFromCodeLayerNode(parentNode);
176
204
  const positions = computeAlignedPositions(
177
205
  [selectedRects[0]!],
178
206
  { x: 0, y: 0, width: parentRect.width, height: parentRect.height },
179
207
  edge,
180
208
  );
181
- if (positions.size === 0) return;
209
+ if (positions.size === 0) {
210
+ return abandon("already aligned; nothing to move", { edge });
211
+ }
182
212
  commitNodePositions(baseContent, positions);
183
213
  }
@@ -104,6 +104,12 @@ export function runApplyLocalContentUpdate(
104
104
  nextContent: string,
105
105
  options: {
106
106
  refreshPreview?: boolean;
107
+ /**
108
+ * Requires the caller to own the preview: it already patched the live
109
+ * iframe, or its target is not the rendered document. Set on a
110
+ * host-computed edit to the active screen and the canvas renders stale
111
+ * content until a reload.
112
+ */
107
113
  skipPreview?: boolean;
108
114
  forcePreviewFullDocument?: boolean;
109
115
  immediateSave?: boolean;
@@ -258,7 +264,7 @@ export function runApplyLocalContentUpdate(
258
264
  // couldn't run (bridge not registered for this surface yet, or an
259
265
  // explicit forceRefresh request).
260
266
  const replacedPreview = options.skipPreview
261
- ? "applied"
267
+ ? "skipped-caller-owns-preview"
262
268
  : forceRefresh
263
269
  ? "unavailable"
264
270
  : replacePreviewContent(
@@ -268,10 +274,15 @@ export function runApplyLocalContentUpdate(
268
274
  ? { forceFullDocument: true }
269
275
  : undefined,
270
276
  );
271
- if (
272
- forceRefresh ||
273
- previewContentReplaceNeedsRenderFallback(replacedPreview)
274
- ) {
277
+ const renderFallback =
278
+ forceRefresh || previewContentReplaceNeedsRenderFallback(replacedPreview);
279
+ trace("persist", "preview", {
280
+ outcome: replacedPreview,
281
+ forceFullDocument: options.forcePreviewFullDocument === true,
282
+ renderFallback,
283
+ bytes: nextContent.length,
284
+ });
285
+ if (renderFallback) {
275
286
  setContentRenderRevision((revision) => revision + 1);
276
287
  }
277
288
  if (ydoc && isSynced) {
@@ -200,7 +200,7 @@ export function runChangeSelectedZIndex(
200
200
  zIndexFallback();
201
201
  return;
202
202
  }
203
- applyLocalContentUpdate(patch.content, { skipPreview: true });
203
+ applyLocalContentUpdate(patch.content, { forcePreviewFullDocument: true });
204
204
  const movedNode = patch.projection.nodes.find(
205
205
  (n) =>
206
206
  n.dataAttributes["data-agent-native-node-id"] === targetId ||
@@ -111,7 +111,7 @@ export function runFrameSelection({
111
111
  patch.result.wrapperNodeId,
112
112
  ) ?? wrapperNode;
113
113
  }
114
- applyLocalContentUpdate(nextContent, { skipPreview: true });
114
+ applyLocalContentUpdate(nextContent, { forcePreviewFullDocument: true });
115
115
  if (wrapperNode) {
116
116
  setSelectedLayerIdsState([wrapperNode.id]);
117
117
  setSelectedElement(elementInfoFromCodeLayerNode(wrapperNode));
@@ -111,7 +111,7 @@ export function runGroupSelection({
111
111
  );
112
112
  return;
113
113
  }
114
- applyLocalContentUpdate(patch.content, { skipPreview: true });
114
+ applyLocalContentUpdate(patch.content, { forcePreviewFullDocument: true });
115
115
  // Select the new wrapper node if the substrate reported its id.
116
116
  const wrapperId = patch.result.wrapperNodeId;
117
117
  if (wrapperId) {
@@ -180,7 +180,7 @@ export function runNudgeSelection(
180
180
  placement: intent.placement,
181
181
  } satisfies MoveNodeEditIntent);
182
182
  if (patch.result.status !== "applied") return;
183
- applyLocalContentUpdate(patch.content, { skipPreview: true });
183
+ applyLocalContentUpdate(patch.content, { forcePreviewFullDocument: true });
184
184
  // A node with no stable `data-agent-native-node-id` has its id derived
185
185
  // from path/offset, and the move changes both — so the pre-move id
186
186
  // finds nothing and the selection has to be re-resolved by identity.
@@ -2,6 +2,8 @@ import type { Dispatch, RefObject, SetStateAction } from "react";
2
2
  import { toast } from "sonner";
3
3
  import * as Y from "yjs";
4
4
 
5
+ import { trace } from "@/components/design/design-trace";
6
+ import { findCanvasIframeForScreen } from "@/components/design/multi-screen/iframe-targeting";
5
7
  import type {
6
8
  ElementInfo,
7
9
  RuntimeStructureInsertRequest,
@@ -21,9 +23,15 @@ import type { CanvasLayerClipboardEntry } from "@/pages/design-editor/command-ty
21
23
  import { isStandaloneHttpUrl } from "@/pages/design-editor/editor-state";
22
24
  import type { ContentHistoryChange } from "@/pages/design-editor/history";
23
25
  import { MAX_DESIGN_UNDO_STACK } from "@/pages/design-editor/history";
24
- import { resolvePastePlacementForSelection } from "@/pages/design-editor/paste-placement";
26
+ import {
27
+ resolvePastePlacementForSelection,
28
+ resolvePasteSourceAnchor,
29
+ } from "@/pages/design-editor/paste-placement";
25
30
  import type { DesignFile } from "@/pages/design-editor/types";
26
31
 
32
+ /** Inset for a copy whose original parent is gone, so it lands on screen. */
33
+ const ORPHANED_PASTE_INSET = 24;
34
+
27
35
  export interface PasteSelectionArgs {
28
36
  activeFile: DesignFile;
29
37
  applyFileContentUpdate: (
@@ -83,6 +91,7 @@ export interface PasteSelectionArgs {
83
91
  baseContent: string;
84
92
  nextContent: string;
85
93
  origin: ClipboardContentMutationOrigin;
94
+ baseSource?: "lineage" | "document";
86
95
  }) => ClipboardContentMutationPublication | null;
87
96
  refreshClipboardFromSystemClipboard: () => Promise<void>;
88
97
  remapMotionTracksForClone: (
@@ -153,10 +162,6 @@ export async function runPasteSelection(
153
162
  undoManagerRef.current?.stopCapturing();
154
163
  await refreshClipboardFromSystemClipboard();
155
164
  const entries = getCanvasClipboardEntries();
156
- const targetFileId =
157
- viewModeRef.current === "overview" && position && boardFileId
158
- ? boardFileId
159
- : activeFile?.id;
160
165
  if (entries.length === 0) {
161
166
  // No layer-level clipboard content — fall back to whole-screen paste
162
167
  // (U6) when the clipboard instead carries copied screen snapshots.
@@ -166,7 +171,35 @@ export async function runPasteSelection(
166
171
  }
167
172
  return;
168
173
  }
174
+ const sourceAnchor = position
175
+ ? null
176
+ : resolvePasteSourceAnchor({ entries, getContent: getScreenContent });
177
+ const activeSurfaceFileId =
178
+ viewModeRef.current === "overview" && position && boardFileId
179
+ ? boardFileId
180
+ : activeFile?.id;
181
+ // The board is an infinite canvas layer behind every screen, so a screen
182
+ // layer dropped on it renders under a frame at coordinates no camera or
183
+ // selection outline resolves. A keyboard paste goes back to its source.
184
+ const returnToSourceFileId =
185
+ sourceAnchor &&
186
+ activeSurfaceFileId === boardFileId &&
187
+ sourceAnchor.fileId !== boardFileId
188
+ ? sourceAnchor.fileId
189
+ : null;
190
+ const targetFileId = returnToSourceFileId ?? activeSurfaceFileId;
169
191
  if (!targetFileId || !canEditDesign) return;
192
+ if (!position && targetFileId === boardFileId && !sourceAnchor) {
193
+ trace("structure", "paste-refused", {
194
+ reason: "clipboard has no readable source file to return the copy to",
195
+ targetFileId,
196
+ entries: entries.length,
197
+ });
198
+ toast.error(t("designEditor.toasts.primitiveInsertFailed"), {
199
+ duration: 4000,
200
+ });
201
+ return;
202
+ }
170
203
  // The pending-local map is the synchronous write-through source for
171
204
  // same-task/repeated operations. React query/collab mirrors can lag one
172
205
  // render behind a just-completed paste even after its save is already
@@ -174,13 +207,39 @@ export async function runPasteSelection(
174
207
  // mirror makes its history `before` skip the first clone, so one undo
175
208
  // removes both. Prefer the pending snapshot exactly like primitive and
176
209
  // cross-screen structure writes do elsewhere in this editor.
210
+ const pendingBase =
211
+ pendingLocalFileContentsRef.current.get(targetFileId)?.content;
212
+ const lineageBase =
213
+ latestClipboardMutationContentRef.current.get(targetFileId)?.content;
214
+ // The lineage is NOT a content source: it advances only on a clipboard
215
+ // mutation, so after a delete it still describes the pre-delete document
216
+ // and rebasing there resurrects what was removed.
177
217
  const baseContent =
178
- latestClipboardMutationContentRef.current.get(targetFileId)?.content ??
179
- pendingLocalFileContentsRef.current.get(targetFileId)?.content ??
218
+ pendingBase ??
180
219
  (targetFileId === activeFile?.id
181
220
  ? getFreshActiveContent()
182
221
  : (getScreenContent(targetFileId) ?? ""));
183
- if (!baseContent && targetFileId !== boardFileId) return;
222
+ trace("structure", "paste-base", {
223
+ targetFileId,
224
+ rebasedOn: pendingBase
225
+ ? "pending-local"
226
+ : targetFileId === activeFile?.id
227
+ ? "fresh-active"
228
+ : "screen-content",
229
+ bytes: baseContent.length,
230
+ lineageBytes: lineageBase?.length ?? null,
231
+ entries: entries.length,
232
+ });
233
+ if (!baseContent && targetFileId !== boardFileId) {
234
+ trace("structure", "paste-refused", {
235
+ reason: "destination file has no readable content",
236
+ targetFileId,
237
+ });
238
+ toast.error(t("designEditor.toasts.primitiveInsertFailed"), {
239
+ duration: 4000,
240
+ });
241
+ return;
242
+ }
184
243
  const layerHtmls = entries.map((entry) => entry.html);
185
244
  const styleSnapshots = entries.map((entry) => entry.portableStyleSnapshot);
186
245
  const managedStyleSnapshots = entries.map(
@@ -292,8 +351,18 @@ export async function runPasteSelection(
292
351
  baseContent,
293
352
  nextContent,
294
353
  origin: "clipboard-paste",
354
+ baseSource: "document",
295
355
  });
296
- if (!clipboardMutation) return false;
356
+ if (!clipboardMutation) {
357
+ trace("structure", "paste-refused", {
358
+ reason: "clipboard lineage refused the publication",
359
+ targetFileId,
360
+ });
361
+ toast.error(t("designEditor.toasts.primitiveInsertFailed"), {
362
+ duration: 4000,
363
+ });
364
+ return false;
365
+ }
297
366
  if (nextContent !== baseContent) {
298
367
  // Capture the exact immutable pre-paste document here, before the
299
368
  // optimistic cache/collab mirrors can advance independently. The
@@ -335,7 +404,12 @@ export async function runPasteSelection(
335
404
  // Inside a frame, after an object — but always into normal flow: a
336
405
  // container is not a free canvas, so carrying the source's left/top
337
406
  // across drops the clone on top of the target's content.
338
- if (!position && targetFileId !== boardFileId && selectedElement?.selector) {
407
+ if (
408
+ !position &&
409
+ !returnToSourceFileId &&
410
+ targetFileId !== boardFileId &&
411
+ selectedElement?.selector
412
+ ) {
339
413
  const selector = selectedCanvasSelector ?? selectedElement.selector;
340
414
  const decision = resolvePastePlacementForSelection({
341
415
  content: baseContent,
@@ -384,24 +458,56 @@ export async function runPasteSelection(
384
458
  const pastingIntoSourceScreen = entries.every(
385
459
  (entry) => entry.sourceFileId === targetFileId,
386
460
  );
461
+ const sourceParentSelectors =
462
+ sourceAnchor?.fileId === targetFileId ? sourceAnchor.parentSelectors : null;
463
+ // The copy is going to the screen root because its parent is gone. Its
464
+ // stored left/top belong to that parent, so reusing them can place it off
465
+ // the screen entirely; a fixed inset is always somewhere the user can see.
466
+ const rootFallbackPlacement =
467
+ sourceAnchor !== null &&
468
+ sourceAnchor.fileId === targetFileId &&
469
+ sourceParentSelectors === null;
387
470
  const viewportCenter = (() => {
471
+ const container = canvasContainerRef.current;
472
+ const factor = zoom / 100;
473
+ if (factor <= 0) return { x: 120, y: 120 };
388
474
  if (viewModeRef.current === "single") {
389
- const iframe = canvasContainerRef.current?.querySelector<HTMLElement>(
475
+ const iframe = container?.querySelector<HTMLElement>(
390
476
  "[data-design-preview-iframe]",
391
477
  );
392
478
  if (iframe) {
393
479
  const iframeRect = iframe.getBoundingClientRect();
394
- const factor = zoom / 100;
395
480
  return {
396
481
  x: Math.max(0, iframeRect.width / 2 / factor),
397
482
  y: Math.max(0, iframeRect.height / 2 / factor),
398
483
  };
399
484
  }
400
485
  }
401
- const rect = canvasContainerRef.current?.getBoundingClientRect();
402
- return rect
403
- ? { x: Math.max(0, rect.width / 2), y: Math.max(0, rect.height / 2) }
404
- : { x: 120, y: 120 };
486
+ // The container's rect is screen pixels; left/top are document pixels.
487
+ // Only the destination frame's own rect converts between them — the
488
+ // board's cannot, because its iframe is a render window, not its origin.
489
+ const frameRect =
490
+ targetFileId !== boardFileId
491
+ ? findCanvasIframeForScreen(
492
+ container,
493
+ targetFileId,
494
+ )?.getBoundingClientRect()
495
+ : null;
496
+ const containerRect = container?.getBoundingClientRect();
497
+ if (!frameRect || !containerRect) return { x: 120, y: 120 };
498
+ const clamp = (value: number, extent: number) =>
499
+ Math.max(0, Math.min(value, extent / factor));
500
+ return {
501
+ x: clamp(
502
+ (containerRect.left + containerRect.width / 2 - frameRect.left) /
503
+ factor,
504
+ frameRect.width,
505
+ ),
506
+ y: clamp(
507
+ (containerRect.top + containerRect.height / 2 - frameRect.top) / factor,
508
+ frameRect.height,
509
+ ),
510
+ };
405
511
  })();
406
512
  const positions = entries.map((_, index) => {
407
513
  const source = sourcePositions[index];
@@ -413,6 +519,12 @@ export async function runPasteSelection(
413
519
  }
414
520
  : { x: position.x + index * 16, y: position.y + index * 16 };
415
521
  }
522
+ if (rootFallbackPlacement) {
523
+ return {
524
+ x: ORPHANED_PASTE_INSET + cascadeOffset + index * 16,
525
+ y: ORPHANED_PASTE_INSET + cascadeOffset + index * 16,
526
+ };
527
+ }
416
528
  return source && pastingIntoSourceScreen
417
529
  ? {
418
530
  x: source.x + 10 + cascadeOffset,
@@ -427,8 +539,21 @@ export async function runPasteSelection(
427
539
  positions,
428
540
  styleSnapshots,
429
541
  managedStyleSnapshots,
542
+ ...(sourceParentSelectors
543
+ ? { targetSelectors: sourceParentSelectors, placement: "inside" as const }
544
+ : {}),
430
545
  });
431
- if (!result) return;
546
+ if (!result) {
547
+ trace("structure", "paste-refused", {
548
+ reason: "destination document refused the clone",
549
+ targetFileId,
550
+ anchors: sourceParentSelectors?.length ?? 0,
551
+ });
552
+ toast.error(t("designEditor.toasts.primitiveInsertFailed"), {
553
+ duration: 4000,
554
+ });
555
+ return;
556
+ }
432
557
  if (!position) pasteCascadeRef.current += 1;
433
558
  if (!applyPasteContentUpdate(result.content)) return;
434
559
  remapMotionTracksForClone(result.nodeIdMap, targetFileId);
@@ -96,6 +96,7 @@ export function runScreenVisualDuplicateChange(
96
96
  ? [details.anchorSelector]
97
97
  : undefined,
98
98
  placement: details?.placement ?? "after",
99
+ preserveIncomingNodeIds: true,
99
100
  });
100
101
  if (!nextContent) {
101
102
  toast.error(t("designEditor.toasts.layerMoveFailed"), {
@@ -150,7 +150,7 @@ export function runUngroupSelection({
150
150
  );
151
151
  }
152
152
 
153
- applyLocalContentUpdate(content, { skipPreview: true });
153
+ applyLocalContentUpdate(content, { forcePreviewFullDocument: true });
154
154
 
155
155
  const finalProjection = buildCodeLayerProjection(content);
156
156
  const releasedNodes = finalProjection.nodes.filter((n) => {
@@ -87,6 +87,7 @@ export function runVisualDuplicateChange(
87
87
  ? [details.anchorSelector]
88
88
  : undefined,
89
89
  placement: details?.placement ?? "after",
90
+ preserveIncomingNodeIds: true,
90
91
  });
91
92
  if (!nextContent) {
92
93
  toast.error(t("designEditor.toasts.layerMoveFailed"), {
@@ -110,12 +110,12 @@ export function shouldAdaptAutoTextColorForCrossScreenMove(params: {
110
110
  * treated as transparent so the walk keeps climbing instead of trusting a
111
111
  * near-invisible tint. A dark-class hint only counts when no color signal
112
112
  * was present on that element (a real color always wins over a guessed
113
- * class). No signal anywhere in the chain conservatively reports "light" so
114
- * the default-white heuristic can still fire and prevent invisible text.
113
+ * class). No signal anywhere in the chain yields null — distinct from a
114
+ * measured light surface, so each caller picks its own fallback.
115
115
  */
116
- export function resolveDestinationBackgroundLightness(
116
+ export function resolveDestinationBackgroundLightnessOrNull(
117
117
  chain: ReadonlyArray<{ color: string | null } | { darkClassHint: boolean }>,
118
- ): boolean {
118
+ ): boolean | null {
119
119
  for (const entry of chain) {
120
120
  if ("color" in entry && entry.color) {
121
121
  const rgba = parseCssColorExtended(entry.color);
@@ -130,7 +130,14 @@ export function resolveDestinationBackgroundLightness(
130
130
  return false;
131
131
  }
132
132
  }
133
- return true;
133
+ return null;
134
+ }
135
+
136
+ /** Unreadable counts as light, so the default-white heuristic can still fire. */
137
+ export function resolveDestinationBackgroundLightness(
138
+ chain: ReadonlyArray<{ color: string | null } | { darkClassHint: boolean }>,
139
+ ): boolean {
140
+ return resolveDestinationBackgroundLightnessOrNull(chain) ?? true;
134
141
  }
135
142
 
136
143
  // Cheap, best-effort utility-class signal for "this element's classes look
@@ -209,10 +216,11 @@ function collectDestinationBackgroundSignals(
209
216
  * dark-class-hint chain when no live document is available (e.g. the
210
217
  * destination screen isn't currently mounted).
211
218
  */
212
- export function destinationBackgroundIsLightForNode(
219
+ /** null means "no readable signal", so callers can pick their own fallback. */
220
+ export function destinationBackgroundLightness(
213
221
  element: Element,
214
222
  liveDoc?: Document | null,
215
- ): boolean {
223
+ ): boolean | null {
216
224
  try {
217
225
  const liveElement =
218
226
  liveDoc && element.hasAttribute("data-agent-native-node-id")
@@ -223,12 +231,20 @@ export function destinationBackgroundIsLightForNode(
223
231
  )
224
232
  : null;
225
233
  const chain = collectDestinationBackgroundSignals(element, liveElement);
226
- return resolveDestinationBackgroundLightness(chain);
234
+ return resolveDestinationBackgroundLightnessOrNull(chain);
227
235
  } catch {
228
- return true;
236
+ // coercion-ok: null is the typed "unreadable" value callers branch on
237
+ return null;
229
238
  }
230
239
  }
231
240
 
241
+ export function destinationBackgroundIsLightForNode(
242
+ element: Element,
243
+ liveDoc?: Document | null,
244
+ ): boolean {
245
+ return destinationBackgroundLightness(element, liveDoc) ?? true;
246
+ }
247
+
232
248
  /**
233
249
  * Cross-screen counterpart to adaptAutoTextColorForNest — runs HOST-SIDE
234
250
  * after moveNodeBetweenDocuments has already re-parented the text node into
@@ -21,6 +21,7 @@ export { isStandaloneHttpUrl };
21
21
  export type PreviewContentReplaceResult =
22
22
  | "applied"
23
23
  | "skipped-live-route"
24
+ | "skipped-caller-owns-preview"
24
25
  | "unavailable";
25
26
 
26
27
  export function previewContentReplaceNeedsRenderFallback(