@agent-native/core 0.165.3 → 0.167.0

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 (249) 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/CHANGELOG.md +740 -2
  5. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +93 -56
  6. package/corpus/templates/analytics/app/i18n-data.ts +23 -20
  7. package/corpus/templates/analytics/app/pages/Settings.tsx +0 -1
  8. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +0 -1
  9. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  10. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +2 -1
  11. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +0 -1
  12. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +0 -32
  13. package/corpus/templates/analytics/server/lib/session-replay.ts +1 -5
  14. package/corpus/templates/assets/.agents/skills/secrets/SKILL.md +5 -1
  15. package/corpus/templates/assets/.agents/skills/workspace-conventions/SKILL.md +4 -1
  16. package/corpus/templates/assets/CHANGELOG.md +204 -2
  17. package/corpus/templates/assets/actions/navigate.ts +2 -2
  18. package/corpus/templates/assets/actions/save-generated-image.ts +0 -5
  19. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +2 -262
  20. package/corpus/templates/assets/app/routes/library.tsx +1 -1
  21. package/corpus/templates/assets/app/routes/settings.tsx +0 -3
  22. package/corpus/templates/brain/.agents/skills/secrets/SKILL.md +5 -1
  23. package/corpus/templates/brain/.agents/skills/workspace-conventions/SKILL.md +4 -1
  24. package/corpus/templates/brain/CHANGELOG.md +145 -2
  25. package/corpus/templates/brain/actions/ask-brain.ts +0 -1
  26. package/corpus/templates/brain/actions/navigate.ts +2 -2
  27. package/corpus/templates/brain/app/routes/sources.tsx +0 -4
  28. package/corpus/templates/brain/server/lib/capture-sanitization.ts +0 -11
  29. package/corpus/templates/calendar/.agents/skills/secrets/SKILL.md +5 -1
  30. package/corpus/templates/calendar/.agents/skills/workspace-conventions/SKILL.md +4 -1
  31. package/corpus/templates/calendar/CHANGELOG.md +272 -2
  32. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +0 -20
  33. package/corpus/templates/calendar/app/pages/CalendarView.tsx +0 -1
  34. package/corpus/templates/chat/.agents/skills/secrets/SKILL.md +5 -1
  35. package/corpus/templates/chat/.agents/skills/workspace-conventions/SKILL.md +4 -1
  36. package/corpus/templates/chat/CHANGELOG.md +89 -2
  37. package/corpus/templates/chat/actions/navigate.ts +2 -2
  38. package/corpus/templates/clips/.agents/skills/secrets/SKILL.md +5 -1
  39. package/corpus/templates/clips/.agents/skills/workspace-conventions/SKILL.md +4 -1
  40. package/corpus/templates/clips/CHANGELOG.md +865 -2
  41. package/corpus/templates/clips/actions/import-loom-recording.ts +16 -4
  42. package/corpus/templates/clips/actions/navigate.ts +2 -2
  43. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +0 -1
  44. package/corpus/templates/clips/app/components/library/space-dialogs.tsx +0 -1
  45. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +0 -1
  46. package/corpus/templates/clips/app/components/player/clips-share-trigger.tsx +17 -0
  47. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +3 -3
  48. package/corpus/templates/clips/app/components/player/video-player.tsx +2 -2
  49. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +0 -1
  50. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +0 -1
  51. package/corpus/templates/clips/app/global.css +31 -0
  52. package/corpus/templates/clips/app/hooks/use-playback-position.ts +0 -2
  53. package/corpus/templates/clips/app/lib/download-release-channel.ts +10 -0
  54. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +1 -3
  55. package/corpus/templates/clips/app/routes/download.tsx +67 -44
  56. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -12
  57. package/corpus/templates/clips/app/routes/record.tsx +6 -6
  58. package/corpus/templates/clips/app/routes/share.$shareId.tsx +26 -29
  59. package/corpus/templates/clips/changelog/2026-08-20-clip-viewer-share-actions.md +6 -0
  60. package/corpus/templates/clips/changelog/2026-08-20-preparing-recording-countdown-overlay.md +6 -0
  61. package/corpus/templates/clips/chrome-extension/src/background.ts +0 -13
  62. package/corpus/templates/clips/chrome-extension/src/overlay.ts +0 -1
  63. package/corpus/templates/clips/chrome-extension/src/popup.ts +0 -20
  64. package/corpus/templates/clips/desktop/src/app.tsx +6 -18
  65. package/corpus/templates/clips/desktop/src/hooks/useMediaDevices.ts +0 -1
  66. package/corpus/templates/clips/desktop/src/lib/recorder.ts +0 -8
  67. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +4 -6
  68. package/corpus/templates/clips/desktop/src/styles.css +18 -20
  69. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +9 -17
  70. package/corpus/templates/content/.agents/skills/secrets/SKILL.md +5 -1
  71. package/corpus/templates/content/.agents/skills/workspace-conventions/SKILL.md +4 -1
  72. package/corpus/templates/content/CHANGELOG.md +503 -2
  73. package/corpus/templates/content/actions/_database-source-utils.ts +0 -3
  74. package/corpus/templates/content/actions/attach-content-database-source.ts +1 -4
  75. package/corpus/templates/content/actions/navigate.ts +2 -2
  76. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
  77. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +0 -6
  78. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +0 -580
  79. package/corpus/templates/content/app/components/editor/database/FormView.tsx +2 -2
  80. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +1 -1
  81. package/corpus/templates/content/app/components/editor/database/settings.tsx +0 -23
  82. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +0 -1
  83. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +8 -304
  84. package/corpus/templates/content/app/hooks/use-documents.ts +0 -1
  85. package/corpus/templates/content/shared/api.ts +0 -1
  86. package/corpus/templates/content/shared/builder-mdx.ts +0 -1
  87. package/corpus/templates/content/shared/nfm.ts +1 -7
  88. package/corpus/templates/crm/.agents/skills/secrets/SKILL.md +5 -1
  89. package/corpus/templates/crm/.agents/skills/workspace-conventions/SKILL.md +4 -1
  90. package/corpus/templates/crm/CHANGELOG.md +31 -2
  91. package/corpus/templates/crm/actions/merge-crm-records.ts +0 -4
  92. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +0 -1
  93. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +0 -1
  94. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +0 -1
  95. package/corpus/templates/crm/server/db/crm-store.ts +1 -1
  96. package/corpus/templates/design/.agents/skills/secrets/SKILL.md +5 -1
  97. package/corpus/templates/design/.agents/skills/workspace-conventions/SKILL.md +4 -1
  98. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -14
  99. package/corpus/templates/design/AGENTS.md +2 -1
  100. package/corpus/templates/design/CHANGELOG.md +870 -2
  101. package/corpus/templates/design/actions/apply-component-prop-edit.ts +1 -5
  102. package/corpus/templates/design/actions/generate-design.ts +0 -1
  103. package/corpus/templates/design/actions/generate-screens.ts +5 -2
  104. package/corpus/templates/design/actions/go-to-main-component.ts +2 -2
  105. package/corpus/templates/design/actions/list-design-systems.ts +10 -0
  106. package/corpus/templates/design/actions/navigate.ts +2 -2
  107. package/corpus/templates/design/actions/open-component-source.ts +4 -4
  108. package/corpus/templates/design/actions/present-design-variants.ts +1 -2
  109. package/corpus/templates/design/actions/run-design-audit.ts +0 -22
  110. package/corpus/templates/design/actions/show-design-questions.ts +5 -2
  111. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -8
  112. package/corpus/templates/design/app/components/design/EditPanel.tsx +11 -183
  113. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +21 -4
  114. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +1 -5
  115. package/corpus/templates/design/app/components/design/bridge/codegen.ts +1 -1
  116. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +10 -19
  117. package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +1 -1
  118. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +13 -3
  119. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +38 -32
  120. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +1 -4
  121. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +0 -4
  122. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
  123. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +2 -2
  124. package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +0 -1
  125. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +1 -1
  126. package/corpus/templates/design/app/i18n-data.ts +122 -0
  127. package/corpus/templates/design/app/lib/clipboard-content-lineage.ts +11 -1
  128. package/corpus/templates/design/app/pages/DesignEditor.tsx +50 -254
  129. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +163 -8
  130. package/corpus/templates/design/app/pages/Index.tsx +17 -0
  131. package/corpus/templates/design/app/pages/design-editor/canvas-primitive-insert.ts +44 -32
  132. package/corpus/templates/design/app/pages/design-editor/canvas-primitives.ts +10 -0
  133. package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +49 -3
  134. package/corpus/templates/design/app/pages/design-editor/clone-idrefs.ts +31 -0
  135. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +28 -0
  136. package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +3 -3
  137. package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +44 -14
  138. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +16 -5
  139. package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +1 -1
  140. package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +1 -1
  141. package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +1 -1
  142. package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +1 -1
  143. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +142 -17
  144. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +1 -0
  145. package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +1 -1
  146. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +1 -0
  147. package/corpus/templates/design/app/pages/design-editor/cross-screen-text-color.ts +25 -9
  148. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +1 -0
  149. package/corpus/templates/design/app/pages/design-editor/measure-child-rects.ts +83 -0
  150. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +66 -0
  151. package/corpus/templates/design/server/routes/[...page].get.ts +0 -1
  152. package/corpus/templates/design/shared/code-layer.ts +120 -70
  153. package/corpus/templates/design/shared/design-surface-index.ts +0 -1
  154. package/corpus/templates/design/shared/pen-path.ts +0 -4
  155. package/corpus/templates/dispatch/.agents/skills/secrets/SKILL.md +5 -1
  156. package/corpus/templates/dispatch/.agents/skills/workspace-conventions/SKILL.md +4 -1
  157. package/corpus/templates/dispatch/CHANGELOG.md +196 -2
  158. package/corpus/templates/dispatch/app/routes/integrations.tsx +0 -109
  159. package/corpus/templates/factory/.agents/skills/secrets/SKILL.md +5 -1
  160. package/corpus/templates/factory/.agents/skills/workspace-conventions/SKILL.md +4 -1
  161. package/corpus/templates/factory/CHANGELOG.md +121 -2
  162. package/corpus/templates/factory/actions/navigate.ts +2 -2
  163. package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +0 -13
  164. package/corpus/templates/factory/app/routes/factory.tsx +0 -1
  165. package/corpus/templates/factory/server/factory-graph/store.ts +0 -1
  166. package/corpus/templates/forms/.agents/skills/secrets/SKILL.md +5 -1
  167. package/corpus/templates/forms/.agents/skills/workspace-conventions/SKILL.md +4 -1
  168. package/corpus/templates/forms/CHANGELOG.md +166 -2
  169. package/corpus/templates/macros/.agents/skills/secrets/SKILL.md +5 -1
  170. package/corpus/templates/macros/.agents/skills/workspace-conventions/SKILL.md +4 -1
  171. package/corpus/templates/macros/CHANGELOG.md +49 -2
  172. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +0 -1
  173. package/corpus/templates/mail/.agents/skills/secrets/SKILL.md +5 -1
  174. package/corpus/templates/mail/.agents/skills/workspace-conventions/SKILL.md +4 -1
  175. package/corpus/templates/mail/CHANGELOG.md +201 -2
  176. package/corpus/templates/mail/app/components/email/EmailList.tsx +0 -1
  177. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +2 -2
  178. package/corpus/templates/mail/shared/markdown.ts +3 -0
  179. package/corpus/templates/plan/.agents/skills/secrets/SKILL.md +5 -1
  180. package/corpus/templates/plan/.agents/skills/workspace-conventions/SKILL.md +4 -1
  181. package/corpus/templates/plan/CHANGELOG.md +277 -2
  182. package/corpus/templates/slides/.agents/skills/secrets/SKILL.md +5 -1
  183. package/corpus/templates/slides/.agents/skills/workspace-conventions/SKILL.md +4 -1
  184. package/corpus/templates/slides/CHANGELOG.md +437 -2
  185. package/corpus/templates/slides/actions/add-slide.ts +0 -1
  186. package/corpus/templates/slides/actions/list-decks.ts +1 -1
  187. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +1 -4
  188. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +0 -1
  189. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
  190. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -6
  191. package/corpus/templates/tasks/.agents/skills/secrets/SKILL.md +5 -1
  192. package/corpus/templates/tasks/.agents/skills/workspace-conventions/SKILL.md +4 -1
  193. package/corpus/templates/tasks/CHANGELOG.md +32 -2
  194. package/corpus/templates/tasks/app/components/layout/Layout.tsx +0 -1
  195. package/corpus/templates/tasks/app/hooks/use-visible-task-fields.ts +1 -4
  196. package/corpus/templates/tasks/server/custom-fields/store.ts +1 -6
  197. package/corpus/templates/tasks/shared/list-screen-snapshot.ts +2 -2
  198. package/dist/agent/engine/registry.js +27 -14
  199. package/dist/agent/index.d.ts +1 -1
  200. package/dist/agent/production-agent.d.ts +19 -6
  201. package/dist/agent/production-agent.js +57 -16
  202. package/dist/agent/types.d.ts +3 -0
  203. package/dist/changelog/parse.d.ts +3 -3
  204. package/dist/changelog/parse.js +35 -16
  205. package/dist/client/EnvironmentBadge.d.ts +1 -0
  206. package/dist/client/EnvironmentBadge.js +7 -0
  207. package/dist/collab/awareness.d.ts +2 -2
  208. package/dist/collab/routes.d.ts +1 -1
  209. package/dist/collab/struct-routes.d.ts +1 -1
  210. package/dist/config.d.ts +11 -0
  211. package/dist/config.js +248 -0
  212. package/dist/deploy/build.js +12 -1
  213. package/dist/index.d.ts +1 -1
  214. package/dist/notifications/routes.d.ts +3 -3
  215. package/dist/observability/routes.d.ts +6 -6
  216. package/dist/resources/handlers.d.ts +2 -2
  217. package/dist/secrets/routes.d.ts +9 -9
  218. package/dist/server/agent-chat/plugin-options.d.ts +3 -2
  219. package/dist/server/agent-chat-plugin.js +5 -2
  220. package/dist/server/index.d.ts +1 -1
  221. package/dist/server/realtime-token.d.ts +1 -1
  222. package/dist/server/transcribe-voice.d.ts +1 -1
  223. package/dist/templates/chat/.agents/skills/secrets/SKILL.md +5 -1
  224. package/dist/templates/chat/.agents/skills/workspace-conventions/SKILL.md +4 -1
  225. package/dist/templates/chat/CHANGELOG.md +89 -2
  226. package/dist/templates/chat/actions/navigate.ts +2 -2
  227. package/dist/templates/default/.agents/skills/secrets/SKILL.md +5 -1
  228. package/dist/templates/default/.agents/skills/workspace-conventions/SKILL.md +4 -1
  229. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +5 -1
  230. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +5 -1
  231. package/dist/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +4 -1
  232. package/dist/vite/agent-native-config-loader.d.ts +1 -0
  233. package/dist/vite/agent-native-config-loader.js +3 -3
  234. package/dist/vite/agents-bundle-plugin.d.ts +1 -1
  235. package/dist/vite/agents-bundle-plugin.js +13 -1
  236. package/dist/vite/client.js +14 -14
  237. package/docs/content/agent-native-config.mdx +62 -11
  238. package/docs/content/agent-surfaces.mdx +6 -0
  239. package/docs/content/environment-variables.mdx +5 -2
  240. package/package.json +3 -3
  241. package/src/templates/chat/.agents/skills/secrets/SKILL.md +5 -1
  242. package/src/templates/chat/.agents/skills/workspace-conventions/SKILL.md +4 -1
  243. package/src/templates/chat/CHANGELOG.md +89 -2
  244. package/src/templates/chat/actions/navigate.ts +2 -2
  245. package/src/templates/default/.agents/skills/secrets/SKILL.md +5 -1
  246. package/src/templates/default/.agents/skills/workspace-conventions/SKILL.md +4 -1
  247. package/src/templates/headless/.agents/skills/secrets/SKILL.md +5 -1
  248. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +5 -1
  249. package/src/templates/workspace-core/.agents/skills/workspace-conventions/SKILL.md +4 -1
@@ -21,7 +21,6 @@ import {
21
21
  IconPhoto,
22
22
  IconComponents,
23
23
  IconCheck,
24
- IconChevronDown,
25
24
  IconExternalLink,
26
25
  } from "@tabler/icons-react";
27
26
  import { useState, useCallback, useRef, useMemo, useEffect } from "react";
@@ -38,7 +37,6 @@ import {
38
37
  pollDecodeJobStatus,
39
38
  type DecodeJobStatus,
40
39
  } from "@/lib/builder-design-system-upload";
41
- import { cn } from "@/lib/utils";
42
40
 
43
41
  interface GitHubLink {
44
42
  id: string;
@@ -55,7 +53,13 @@ interface UploadedFile {
55
53
  textContent?: string;
56
54
  }
57
55
 
58
- type OtherSource = "brand" | "code" | "files" | "existing" | "notes";
56
+ type OtherSource =
57
+ | "brand"
58
+ | "code"
59
+ | "design-md"
60
+ | "files"
61
+ | "existing"
62
+ | "notes";
59
63
 
60
64
  interface BuilderIndexResult {
61
65
  ok: boolean;
@@ -91,6 +95,8 @@ interface BuilderIndexInput {
91
95
  designMd?: string;
92
96
  }
93
97
 
98
+ const MAX_INLINE_DESIGN_MD_BYTES = 2 * 1024 * 1024;
99
+
94
100
  export default function DesignSystemSetup() {
95
101
  const t = useT();
96
102
  const navigate = useNavigate();
@@ -105,6 +111,7 @@ export default function DesignSystemSetup() {
105
111
  const [githubPaths, setGithubPaths] = useState("");
106
112
  const [githubLinks, setGithubLinks] = useState<GitHubLink[]>([]);
107
113
  const [codeFiles, setCodeFiles] = useState<UploadedFile[]>([]);
114
+ const [designMdFiles, setDesignMdFiles] = useState<UploadedFile[]>([]);
108
115
  const [docFiles, setDocFiles] = useState<UploadedFile[]>([]);
109
116
  const [imageFiles, setImageFiles] = useState<UploadedFile[]>([]);
110
117
  const [assets, setAssets] = useState<UploadedFile[]>([]);
@@ -119,6 +126,8 @@ export default function DesignSystemSetup() {
119
126
  const imageInputRef = useRef<HTMLInputElement>(null);
120
127
  const assetInputRef = useRef<HTMLInputElement>(null);
121
128
  const codeInputRef = useRef<HTMLInputElement>(null);
129
+ const designMdInputRef = useRef<HTMLInputElement>(null);
130
+ const designMdUploadGenerationRef = useRef(0);
122
131
  const appliedSourceIdRef = useRef<string | null>(null);
123
132
 
124
133
  const { data: designsData } = useActionQuery<{
@@ -264,6 +273,7 @@ export default function DesignSystemSetup() {
264
273
  githubUrl.trim() ||
265
274
  githubLinks.length > 0 ||
266
275
  codeFiles.length > 0 ||
276
+ designMdFiles.length > 0 ||
267
277
  builderIndexResult ||
268
278
  docFiles.length > 0 ||
269
279
  imageFiles.length > 0 ||
@@ -279,6 +289,7 @@ export default function DesignSystemSetup() {
279
289
  githubUrl,
280
290
  githubLinks,
281
291
  codeFiles,
292
+ designMdFiles,
282
293
  builderIndexResult,
283
294
  docFiles,
284
295
  imageFiles,
@@ -390,6 +401,48 @@ export default function DesignSystemSetup() {
390
401
  [readTextFiles],
391
402
  );
392
403
 
404
+ const handleDesignMdUpload = useCallback(
405
+ (e: React.ChangeEvent<HTMLInputElement>) => {
406
+ const files = e.target.files;
407
+ if (!files) return;
408
+ const file = files[0];
409
+ e.target.value = "";
410
+ if (!file) return;
411
+ const uploadGeneration = ++designMdUploadGenerationRef.current;
412
+ setDesignMdFiles([]);
413
+ if (!isDesignMdFile({ name: file.name })) {
414
+ setValidationError(t("designSystemSetup.errors.chooseDesignMd"));
415
+ return;
416
+ }
417
+ if (file.size > MAX_INLINE_DESIGN_MD_BYTES) {
418
+ setValidationError(t("designSystemSetup.errors.designMdTooLarge"));
419
+ return;
420
+ }
421
+ const uploadedFile: UploadedFile = {
422
+ id: crypto.randomUUID(),
423
+ name: file.name,
424
+ type: file.type || "text/markdown",
425
+ size: file.size,
426
+ };
427
+ file
428
+ .text()
429
+ .then((text) => {
430
+ if (designMdUploadGenerationRef.current !== uploadGeneration) {
431
+ return;
432
+ }
433
+ setDesignMdFiles([{ ...uploadedFile, textContent: text }]);
434
+ setValidationError(null);
435
+ })
436
+ .catch(() => {
437
+ if (designMdUploadGenerationRef.current !== uploadGeneration) {
438
+ return;
439
+ }
440
+ setValidationError(t("designSystemSetup.errors.readDesignMd"));
441
+ });
442
+ },
443
+ [t],
444
+ );
445
+
393
446
  const handleDocUpload = useCallback(
394
447
  (e: React.ChangeEvent<HTMLInputElement>) => {
395
448
  if (!e.target.files) return;
@@ -487,6 +540,7 @@ export default function DesignSystemSetup() {
487
540
  normalizedGithubLinks.length > 0 &&
488
541
  normalizedWebsiteUrls.length === 0 &&
489
542
  codeFiles.length === 0 &&
543
+ designMdFiles.length === 0 &&
490
544
  !builderIndexResult &&
491
545
  docFiles.length === 0 &&
492
546
  imageFiles.length === 0 &&
@@ -521,11 +575,53 @@ export default function DesignSystemSetup() {
521
575
  }
522
576
 
523
577
  const readableCodeFiles = codeFiles.filter((f) => f.textContent);
524
- const designMdFiles = readableCodeFiles.filter(isDesignMdFile);
578
+ const codeDesignMdFiles = readableCodeFiles.filter(isDesignMdFile);
525
579
  const builderCodeFiles = readableCodeFiles.filter(
526
580
  (file) => !isDesignMdFile(file),
527
581
  );
528
582
  const unreadableCodeFiles = codeFiles.filter((f) => !f.textContent);
583
+ const readableDesignMdFiles = designMdFiles.filter(
584
+ (file) => file.textContent,
585
+ );
586
+
587
+ if (designMdFiles.length > 0 && readableDesignMdFiles.length === 0) {
588
+ setValidationError(t("designSystemSetup.errors.readDesignMd"));
589
+ return;
590
+ }
591
+
592
+ const isDesignMdOnlySource =
593
+ readableDesignMdFiles.length > 0 &&
594
+ normalizedGithubLinks.length === 0 &&
595
+ normalizedWebsiteUrls.length === 0 &&
596
+ codeFiles.length === 0 &&
597
+ !builderIndexResult &&
598
+ docFiles.length === 0 &&
599
+ imageFiles.length === 0 &&
600
+ assets.length === 0 &&
601
+ !selectedProjectId;
602
+
603
+ if (isDesignMdOnlySource) {
604
+ setValidationError(null);
605
+ try {
606
+ await indexSystemMutation.mutateAsync({
607
+ projectName: companyInfo.trim() || undefined,
608
+ description:
609
+ [notes.trim(), customInstructions.trim()]
610
+ .filter(Boolean)
611
+ .join("\n\n") || undefined,
612
+ designMd: readableDesignMdFiles[0]?.textContent,
613
+ });
614
+ toast.success(t("designSystemSetup.designMdIndexStarted"));
615
+ navigate("/design-systems");
616
+ } catch (error) {
617
+ setValidationError(
618
+ error instanceof Error
619
+ ? error.message
620
+ : t("designSystemSetup.errors.designMdIndex"),
621
+ );
622
+ }
623
+ return;
624
+ }
529
625
 
530
626
  const parts: string[] = [];
531
627
  parts.push(
@@ -569,11 +665,11 @@ export default function DesignSystemSetup() {
569
665
  );
570
666
  }
571
667
  }
572
- if (designMdFiles.length > 0) {
668
+ if (codeDesignMdFiles.length > 0) {
573
669
  parts.push(
574
- `\n## Optional design.md (${designMdFiles.length} file${designMdFiles.length === 1 ? "" : "s"})\nPass this content as the \`designMd\` argument to \`index-design-system-with-builder\` alongside any Figma/code sources:`,
670
+ `\n## Optional design.md (${codeDesignMdFiles.length} file${codeDesignMdFiles.length === 1 ? "" : "s"})\nPass this content as the \`designMd\` argument to \`index-design-system-with-builder\` alongside any Figma/code sources:`,
575
671
  );
576
- for (const f of designMdFiles) {
672
+ for (const f of codeDesignMdFiles) {
577
673
  parts.push(
578
674
  `\n### ${f.name}\n\`\`\`md\n${f.textContent!.slice(0, 5000)}\n\`\`\``,
579
675
  );
@@ -586,6 +682,17 @@ export default function DesignSystemSetup() {
586
682
  }
587
683
  }
588
684
 
685
+ if (designMdFiles.length > 0) {
686
+ parts.push(
687
+ `\n## design.md\nPass this content as the \`designMd\` argument to \`index-design-system-with-builder\` alongside any Figma/code sources:`,
688
+ );
689
+ for (const f of readableDesignMdFiles) {
690
+ parts.push(
691
+ `\n### ${f.name}\n\`\`\`md\n${f.textContent!.slice(0, 5000)}\n\`\`\``,
692
+ );
693
+ }
694
+ }
695
+
589
696
  if (builderIndexResult) {
590
697
  parts.push(
591
698
  `\n## Connect Figma: Builder-Indexed Figma File\nBuilder DSI indexing has already started.\n- Design system: ${builderIndexResult.designSystemId}\n- Local selectable design system: ${builderIndexResult.localDesignSystemId ?? "(not returned)"}\n- Project: ${builderIndexResult.projectId}\n- Job: ${builderIndexResult.jobId}\n- URL: ${builderIndexResult.builderUrl}\n\nUse Builder as the source of truth for indexed tokens, assets, components, and guidance. Do not call \`create-design-system\` again for this Builder-indexed source.`,
@@ -680,6 +787,7 @@ export default function DesignSystemSetup() {
680
787
  githubPaths,
681
788
  githubLinks,
682
789
  codeFiles,
790
+ designMdFiles,
683
791
  builderIndexResult,
684
792
  docFiles,
685
793
  imageFiles,
@@ -782,6 +890,14 @@ export default function DesignSystemSetup() {
782
890
  selected={sourcePanel === "other" && otherSource === "code"}
783
891
  onClick={() => selectOtherSource("code")}
784
892
  />
893
+ <SourceChoice
894
+ icon={IconFileDescription}
895
+ title={t("designSystemSetup.sections.designMd.title")}
896
+ selected={
897
+ sourcePanel === "other" && otherSource === "design-md"
898
+ }
899
+ onClick={() => selectOtherSource("design-md")}
900
+ />
785
901
  <SourceChoice
786
902
  icon={IconFileDescription}
787
903
  title={t("designSystemSetup.sections.designFiles.title")}
@@ -1103,6 +1219,45 @@ export default function DesignSystemSetup() {
1103
1219
  </div>
1104
1220
  </Section>
1105
1221
 
1222
+ {/* Import design.md guidance directly into Builder DSI. */}
1223
+ <Section
1224
+ title={t("designSystemSetup.sections.designMd.title")}
1225
+ description={t("designSystemSetup.sections.designMd.description")}
1226
+ hidden={sourcePanel !== "other" || otherSource !== "design-md"}
1227
+ hideHeading
1228
+ id="design-system-design-md-source"
1229
+ className="rounded-lg border border-border bg-card p-4"
1230
+ >
1231
+ <button
1232
+ type="button"
1233
+ onClick={() => designMdInputRef.current?.click()}
1234
+ className="w-full rounded-xl border border-dashed border-border bg-card p-8 text-center hover:border-foreground/15 cursor-pointer"
1235
+ >
1236
+ <div className="flex flex-col items-center gap-2">
1237
+ <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
1238
+ <IconFileDescription className="size-6 text-primary" />
1239
+ </div>
1240
+ <p className="text-sm font-medium text-foreground/90">
1241
+ {t("designSystemSetup.designMdUpload")}
1242
+ </p>
1243
+ <p className="text-xs text-muted-foreground/70">
1244
+ {t("designSystemSetup.designMdHelp")}
1245
+ </p>
1246
+ </div>
1247
+ </button>
1248
+ <input
1249
+ ref={designMdInputRef}
1250
+ type="file"
1251
+ accept=".md,.mdx"
1252
+ onChange={handleDesignMdUpload}
1253
+ className="hidden"
1254
+ />
1255
+ <FileList
1256
+ files={designMdFiles}
1257
+ onRemove={() => setDesignMdFiles([])}
1258
+ />
1259
+ </Section>
1260
+
1106
1261
  {/* Design Files */}
1107
1262
  <Section
1108
1263
  title={t("designSystemSetup.sections.designFiles.title")}
@@ -1507,7 +1662,7 @@ function formatSize(bytes: number): string {
1507
1662
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1508
1663
  }
1509
1664
 
1510
- function isDesignMdFile(file: UploadedFile): boolean {
1665
+ function isDesignMdFile(file: Pick<UploadedFile, "name">): boolean {
1511
1666
  const name = file.name.split(/[\\/]/).pop()?.toLowerCase() ?? file.name;
1512
1667
  return name === "design.md" || name === "design.mdx";
1513
1668
  }
@@ -42,6 +42,7 @@ import { useState, useRef, useCallback, useEffect, useMemo } from "react";
42
42
  import { useNavigate, Link, useSearchParams } from "react-router";
43
43
  import { toast } from "sonner";
44
44
 
45
+ import { trace } from "@/components/design/design-trace";
45
46
  import PromptPopover from "@/components/editor/PromptDialog";
46
47
  import type {
47
48
  PromptTemplateOption,
@@ -624,8 +625,24 @@ export default function Index() {
624
625
  skipQuestions: pendingOptions?.skipQuestions,
625
626
  ...options,
626
627
  });
628
+ // Rejecting here is what lets PromptPopover restore the typed prompt.
629
+ // Navigating first strands the user in an empty editor with no error.
630
+ try {
631
+ await ready;
632
+ } catch (error) {
633
+ clearPendingGeneration(id);
634
+ setNewDesignHandoffPending(false);
635
+ trace("persist", "create-design-failed", {
636
+ id,
637
+ designSystemId,
638
+ message: error instanceof Error ? error.message : String(error),
639
+ });
640
+ toast.error(t("home.failedToCreateDesign"));
641
+ throw error;
642
+ }
627
643
  }
628
644
 
645
+ trace("persist", "new-design-handoff", { id, designSystemId });
629
646
  setNewDesignHandoffPending(true);
630
647
  navigate(`/design/${id}`);
631
648
  },
@@ -9,11 +9,12 @@ import type { CanvasPrimitiveInsert } from "@/components/design/multi-screen/typ
9
9
 
10
10
  import {
11
11
  CANVAS_TEXT_DEFAULT_FONT_FAMILY,
12
+ defaultCanvasFrameFill,
12
13
  defaultCanvasTextColor,
13
14
  } from "./canvas-primitives";
14
15
  import {
15
16
  BOARD_TEXT_AUTO_COLOR_MARKER,
16
- destinationBackgroundIsLightForNode,
17
+ destinationBackgroundLightness,
17
18
  } from "./cross-screen-text-color";
18
19
  import { escapeHtmlAttributeValue, escapeHtmlText } from "./dom-utils";
19
20
  import { isStandaloneHttpUrl } from "./editor-state";
@@ -219,8 +220,9 @@ function absoluteRect(
219
220
  ) {
220
221
  continue;
221
222
  }
222
- originX += read(ancestor.style.left) ?? 0;
223
- originY += read(ancestor.style.top) ?? 0;
223
+ const inset = inlineBorderInset(ancestor);
224
+ originX += (read(ancestor.style.left) ?? 0) + inset.x;
225
+ originY += (read(ancestor.style.top) ?? 0) + inset.y;
224
226
  }
225
227
  return { x: originX + x, y: originY + y, w, h };
226
228
  }
@@ -230,12 +232,20 @@ function absoluteRect(
230
232
  * `data-an-primitive="frame"` adopts. Bounds come from inline geometry
231
233
  * because this document is parsed, never laid out.
232
234
  */
235
+ /** Inline border widths, which an absolute child's offsets resolve inside of. */
236
+ function inlineBorderInset(element: Element): { x: number; y: number } {
237
+ const style = (element as HTMLElement).style;
238
+ const read = (value: string) => {
239
+ const parsed = Number.parseFloat(value);
240
+ return Number.isFinite(parsed) ? parsed : 0;
241
+ };
242
+ return { x: read(style.borderLeftWidth), y: read(style.borderTopWidth) };
243
+ }
244
+
233
245
  function deepestFrameContaining(
234
246
  root: Element,
235
247
  x: number,
236
248
  y: number,
237
- w: number,
238
- h: number,
239
249
  ): { element: Element; x: number; y: number } | null {
240
250
  const contained = Array.from(
241
251
  root.querySelectorAll('[data-an-primitive="frame"]'),
@@ -248,11 +258,14 @@ function deepestFrameContaining(
248
258
  element: Element;
249
259
  rect: { x: number; y: number; w: number; h: number };
250
260
  } =>
261
+ // Nest on the origin, as Figma does. Requiring the whole box to fit
262
+ // drops a click-created text (default width) out to the root, where
263
+ // it overlaps the frame it looks like it belongs to.
251
264
  candidate.rect !== null &&
252
265
  x >= candidate.rect.x &&
253
266
  y >= candidate.rect.y &&
254
- x + w <= candidate.rect.x + candidate.rect.w &&
255
- y + h <= candidate.rect.y + candidate.rect.h,
267
+ x <= candidate.rect.x + candidate.rect.w &&
268
+ y <= candidate.rect.y + candidate.rect.h,
256
269
  )
257
270
  .sort((a, b) => a.rect.w * a.rect.h - b.rect.w * b.rect.h);
258
271
  const best = contained[0];
@@ -288,8 +301,14 @@ export function appendCanvasPrimitiveToHtml(
288
301
  const layerName = primitiveLayerName(primitive);
289
302
  // Resolved once so every primitive kind nests identically, and so text can
290
303
  // pick a fill that is legible against its actual container.
291
- const host = deepestFrameContaining(doc.body, left, top, width, height);
304
+ const host = deepestFrameContaining(doc.body, left, top);
292
305
  const hostOrBody: Element = host?.element ?? doc.body;
306
+ // An absolute child resolves against the host's PADDING box, while
307
+ // host.x/host.y are its border-box origin — so a bordered frame shifts
308
+ // everything dropped into it by the border width.
309
+ const hostBorder = host ? inlineBorderInset(host.element) : { x: 0, y: 0 };
310
+ const hostLeft = host ? left - host.x - hostBorder.x : left;
311
+ const hostTop = host ? top - host.y - hostBorder.y : top;
293
312
 
294
313
  if (
295
314
  primitive.kind === "path" ||
@@ -398,8 +417,8 @@ export function appendCanvasPrimitiveToHtml(
398
417
  "style",
399
418
  [
400
419
  "position:absolute",
401
- `left:${left}px`,
402
- `top:${top}px`,
420
+ `left:${hostLeft}px`,
421
+ `top:${hostTop}px`,
403
422
  `width:${width}px`,
404
423
  `height:${height}px`,
405
424
  "overflow:visible",
@@ -441,8 +460,8 @@ export function appendCanvasPrimitiveToHtml(
441
460
  "style",
442
461
  [
443
462
  "position:absolute",
444
- `left:${left}px`,
445
- `top:${top}px`,
463
+ `left:${hostLeft}px`,
464
+ `top:${hostTop}px`,
446
465
  `width:${width}px`,
447
466
  `height:${height}px`,
448
467
  "overflow:visible",
@@ -464,8 +483,8 @@ export function appendCanvasPrimitiveToHtml(
464
483
  // glyph. Read by treeTypeForNode in shared/code-layer.ts.
465
484
  element.setAttribute("data-an-primitive", primitive.kind);
466
485
  element.style.position = "absolute";
467
- element.style.left = `${left}px`;
468
- element.style.top = `${top}px`;
486
+ element.style.left = `${hostLeft}px`;
487
+ element.style.top = `${hostTop}px`;
469
488
  if (!(primitive.kind === "text" && primitive.autoSize)) {
470
489
  element.style.width = `${width}px`;
471
490
  element.style.height = `${height}px`;
@@ -482,19 +501,12 @@ export function appendCanvasPrimitiveToHtml(
482
501
  primitive.kind === "rectangle" ? "rect" : primitive.kind,
483
502
  );
484
503
  if (primitive.kind === "frame") {
485
- // A committed frame is a BARE container <div> — no default fill,
486
- // border, or radius — so the markup this code-first editor emits stays
487
- // clean (a Figma frame reads as unstyled structure, and a dashed
488
- // border/tint baked into the design's real HTML would be styling
489
- // pollution). This deliberately diverges from the draft PREVIEW's
490
- // faint-tint/dashed look (canvas-primitive-style.ts), which is editor
491
- // affordance chrome during the drag; on commit the new frame is
492
- // immediately selected, so its bounds stay visible via selection
493
- // chrome instead. Explicit user-chosen fill/stroke still applies.
504
+ // A committed frame carries a real surface, not the draft preview's
505
+ // dashed tint (editor chrome, canvas-primitive-style.ts): selection
506
+ // chrome only covers the frame while it stays selected, and a bare
507
+ // container is invisible the moment it is not.
494
508
  // overflow:hidden matches Figma frames clipping their content.
495
- if (primitive.fill) {
496
- element.style.background = primitive.fill;
497
- }
509
+ element.style.background = primitive.fill ?? defaultCanvasFrameFill();
498
510
  if (
499
511
  primitive.stroke !== undefined ||
500
512
  primitive.strokeWidth !== undefined
@@ -516,9 +528,13 @@ export function appendCanvasPrimitiveToHtml(
516
528
  // "currentColor" so text still inherits their theme.
517
529
  // Measure the frame the text actually lands in: a dark frame on a light
518
530
  // page would otherwise keep currentColor and render invisible.
531
+ // isBoardTarget only says which surface is behind the text, and the
532
+ // board is not always dark — a measured background always wins.
533
+ const measuredLightness = destinationBackgroundLightness(hostOrBody);
519
534
  const autoTextNeedsLightFill =
520
- options?.isBoardTarget === true ||
521
- !destinationBackgroundIsLightForNode(hostOrBody);
535
+ measuredLightness === null
536
+ ? options?.isBoardTarget === true
537
+ : !measuredLightness;
522
538
  const resolvedTextColor =
523
539
  primitive.fill ?? defaultCanvasTextColor(autoTextNeedsLightFill);
524
540
  element.style.color = resolvedTextColor;
@@ -560,10 +576,6 @@ export function appendCanvasPrimitiveToHtml(
560
576
  element.style.borderRadius = canonical.borderRadius;
561
577
  }
562
578
 
563
- if (host) {
564
- element.style.left = `${left - host.x}px`;
565
- element.style.top = `${top - host.y}px`;
566
- }
567
579
  hostOrBody.appendChild(element);
568
580
  return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
569
581
  } catch {
@@ -222,6 +222,16 @@ export function defaultCanvasTextColor(isBoardTarget: boolean): string {
222
222
  return isBoardTarget ? "#ffffff" : "currentColor";
223
223
  }
224
224
 
225
+ /**
226
+ * A committed frame needs a surface of its own: once deselected, a bare
227
+ * container is indistinguishable from empty canvas, and so is the next one
228
+ * drawn beside it. White on every destination, as in Figma.
229
+ */
230
+ export function defaultCanvasFrameFill(): string {
231
+ // guard:allow-raw-color — written into the user's design HTML, where the editor's theme tokens do not exist
232
+ return "#ffffff";
233
+ }
234
+
225
235
  /** Default font stack for board-drawn text — Inter with the app's standard
226
236
  * system-font fallback chain, matching the rest of the editor's UI type
227
237
  * instead of the browser's serif default for an unstyled <div>. */
@@ -12,7 +12,10 @@ import {
12
12
  } from "@/lib/design-clipboard-managed-styles";
13
13
 
14
14
  import { uniqueLayerId } from "./canvas-primitive-insert";
15
- import { reassignClonedAuthoredIds } from "./clone-idrefs";
15
+ import {
16
+ reassignClonedAuthoredIds,
17
+ reassignClonedSourceIdentity,
18
+ } from "./clone-idrefs";
16
19
  import { queryUniqueSelector } from "./dom-utils";
17
20
  import { isStandaloneHttpUrl } from "./editor-state";
18
21
  import {
@@ -184,10 +187,27 @@ function setRootLayerPosition(
184
187
  host.style.bottom = "";
185
188
  }
186
189
 
190
+ /**
191
+ * Keep an incoming node id the destination does not already use, so a clone
192
+ * the caller already rendered stays addressable by the id it carries.
193
+ */
194
+ function claimClonedNodeId(
195
+ previousId: string | null,
196
+ fallbackPrefix: string,
197
+ reservedNodeIds: Set<string> | null,
198
+ ): string {
199
+ if (!reservedNodeIds || !previousId || reservedNodeIds.has(previousId)) {
200
+ return uniqueLayerId(fallbackPrefix);
201
+ }
202
+ reservedNodeIds.add(previousId);
203
+ return previousId;
204
+ }
205
+
187
206
  function prepareClonedHtmlLayer(
188
207
  doc: Document,
189
208
  layerHtml: string,
190
209
  styleSnapshot?: PortableStyleSnapshot,
210
+ reservedNodeIds: Set<string> | null = null,
191
211
  ): {
192
212
  element: Element;
193
213
  rootNodeId: string;
@@ -215,13 +235,21 @@ function prepareClonedHtmlLayer(
215
235
  }
216
236
  const nodeIdMap = new Map<string, string>();
217
237
  const previousRootNodeId = clone.getAttribute("data-agent-native-node-id");
218
- const rootNodeId = uniqueLayerId("copy");
238
+ const rootNodeId = claimClonedNodeId(
239
+ previousRootNodeId,
240
+ "copy",
241
+ reservedNodeIds,
242
+ );
219
243
  clone.setAttribute("data-agent-native-node-id", rootNodeId);
220
244
  if (previousRootNodeId) nodeIdMap.set(previousRootNodeId, rootNodeId);
221
245
  Array.from(clone.querySelectorAll("[data-agent-native-node-id]")).forEach(
222
246
  (node) => {
223
247
  const previousChildId = node.getAttribute("data-agent-native-node-id");
224
- const nextChildId = uniqueLayerId("copy-child");
248
+ const nextChildId = claimClonedNodeId(
249
+ previousChildId,
250
+ "copy-child",
251
+ reservedNodeIds,
252
+ );
225
253
  node.setAttribute("data-agent-native-node-id", nextChildId);
226
254
  if (previousChildId) nodeIdMap.set(previousChildId, nextChildId);
227
255
  },
@@ -234,6 +262,7 @@ function prepareClonedHtmlLayer(
234
262
  // happens to match first (typically the ORIGINAL, not the new copy),
235
263
  // silently misapplying edits meant for the duplicate.
236
264
  reassignClonedAuthoredIds(clone, () => uniqueLayerId("copy-id"));
265
+ reassignClonedSourceIdentity(clone, () => uniqueLayerId("copy-child"));
237
266
  return { element: clone, rootNodeId, nodeIdMap };
238
267
  }
239
268
 
@@ -345,6 +374,13 @@ export function insertClonedHtmlLayers(
345
374
  managedStyleSnapshots?: Array<
346
375
  DesignClipboardManagedStyleSnapshot | null | undefined
347
376
  >;
377
+ /**
378
+ * The layer html describes an element the caller has ALREADY rendered
379
+ * under these ids. Re-minting them strands that element: every later
380
+ * message addresses an id the document lacks, and the fuzzy resolver
381
+ * behind it lands on the node the clone was copied from.
382
+ */
383
+ preserveIncomingNodeIds?: boolean;
348
384
  } = {},
349
385
  ): {
350
386
  content: string;
@@ -365,11 +401,19 @@ export function insertClonedHtmlLayers(
365
401
  const fragment = doc.createDocumentFragment();
366
402
  const rootNodeIds: string[] = [];
367
403
  const nodeIdMap = new Map<string, string>();
404
+ const reservedNodeIds = options.preserveIncomingNodeIds
405
+ ? new Set(
406
+ Array.from(doc.querySelectorAll("[data-agent-native-node-id]"))
407
+ .map((node) => node.getAttribute("data-agent-native-node-id"))
408
+ .filter((value): value is string => Boolean(value)),
409
+ )
410
+ : null;
368
411
  layerHtmls.forEach((layerHtml, index) => {
369
412
  const prepared = prepareClonedHtmlLayer(
370
413
  doc,
371
414
  layerHtml,
372
415
  options.styleSnapshots?.[index] ?? undefined,
416
+ reservedNodeIds,
373
417
  );
374
418
  if (!prepared) return;
375
419
  const position = options.positions?.[index];
@@ -447,6 +491,7 @@ export function insertClonedHtmlLayer(
447
491
  targetSelectors: string[];
448
492
  anchorSelectors?: string[];
449
493
  placement?: "before" | "after" | "inside";
494
+ preserveIncomingNodeIds?: boolean;
450
495
  },
451
496
  ): string | null {
452
497
  return (
@@ -454,6 +499,7 @@ export function insertClonedHtmlLayer(
454
499
  targetSelectors: options.targetSelectors,
455
500
  anchorSelectors: options.anchorSelectors,
456
501
  placement: options.placement,
502
+ preserveIncomingNodeIds: options.preserveIncomingNodeIds,
457
503
  })?.content ?? null
458
504
  );
459
505
  }
@@ -12,6 +12,16 @@ const SPACE_SEPARATED_IDREF_ATTRIBUTES = [
12
12
  const SINGLE_IDREF_ATTRIBUTES = ["for", "form", "list"] as const;
13
13
  const FRAGMENT_REFERENCE_ATTRIBUTES = ["href", "xlink:href"] as const;
14
14
 
15
+ // Lookups test every stable id attribute, so re-stamping
16
+ // `data-agent-native-node-id` does not shadow an inherited one.
17
+ // `data-source-file`/`-line`/`-column` are provenance, not identity — keep them.
18
+ const INHERITED_SOURCE_IDENTITY_ATTRIBUTES = [
19
+ "data-code-layer-id",
20
+ "data-layer-id",
21
+ "data-builder-id",
22
+ "data-loc",
23
+ ] as const;
24
+
15
25
  function allElements(root: Element): Element[] {
16
26
  return [root, ...Array.from(root.querySelectorAll("*"))];
17
27
  }
@@ -111,3 +121,24 @@ export function reassignClonedAuthoredIds(
111
121
 
112
122
  return idMap;
113
123
  }
124
+
125
+ /**
126
+ * Replace a cloned subtree's inherited source identity with fresh node ids, so
127
+ * edits and deletes addressed to the clone cannot resolve to the original.
128
+ */
129
+ export function reassignClonedSourceIdentity(
130
+ root: Element,
131
+ createNodeId: () => string,
132
+ ): void {
133
+ for (const element of allElements(root)) {
134
+ let inheritedIdentity = false;
135
+ for (const attribute of INHERITED_SOURCE_IDENTITY_ATTRIBUTES) {
136
+ if (!element.hasAttribute(attribute)) continue;
137
+ element.removeAttribute(attribute);
138
+ inheritedIdentity = true;
139
+ }
140
+ if (!inheritedIdentity) continue;
141
+ if (element.hasAttribute("data-agent-native-node-id")) continue;
142
+ element.setAttribute("data-agent-native-node-id", createNodeId());
143
+ }
144
+ }