@agent-native/core 0.79.27 → 0.80.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 (497) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +34 -0
  3. package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +55 -1
  5. package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +66 -1
  7. package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +65 -1
  9. package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +64 -1
  11. package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  12. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +59 -1
  13. package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  14. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +62 -1
  15. package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  16. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +58 -1
  17. package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  18. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +62 -1
  19. package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  20. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +50 -1
  21. package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  22. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +52 -1
  23. package/corpus/core/docs/content/skills-guide.mdx +5 -0
  24. package/corpus/core/docs/content/template-design.mdx +59 -1
  25. package/corpus/core/package.json +2 -1
  26. package/corpus/core/src/agent/engine/index.ts +1 -0
  27. package/corpus/core/src/agent/engine/registry.ts +40 -0
  28. package/corpus/core/src/agent/production-agent.ts +6 -2
  29. package/corpus/core/src/application-state/handlers.ts +1 -1
  30. package/corpus/core/src/brand-kit/index.ts +1 -1
  31. package/corpus/core/src/cli/design-connect.ts +490 -0
  32. package/corpus/core/src/cli/index.ts +16 -0
  33. package/corpus/core/src/cli/skills.ts +144 -3
  34. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  35. package/corpus/core/src/client/AssistantChat.tsx +43 -7
  36. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  37. package/corpus/core/src/client/agent-chat.ts +27 -3
  38. package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +1 -1
  39. package/corpus/core/src/client/blocks/library/diagram.tsx +2 -2
  40. package/corpus/core/src/client/blocks/library/wireframe.tsx +1 -1
  41. package/corpus/core/src/client/chat/message-components.tsx +1 -1
  42. package/corpus/core/src/client/chat-model-groups.ts +1 -2
  43. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +33 -66
  44. package/corpus/core/src/client/composer/TiptapComposer.tsx +99 -24
  45. package/corpus/core/src/client/composer/useVoiceDictation.ts +429 -260
  46. package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
  47. package/corpus/core/src/client/frame-protocol.ts +17 -2
  48. package/corpus/core/src/client/rich-markdown-editor/RegistryBlockNode.tsx +2 -2
  49. package/corpus/core/src/client/sharing/ShareButton.tsx +20 -15
  50. package/corpus/core/src/client/use-agent-engine-configured.ts +80 -28
  51. package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
  52. package/corpus/core/src/extensions/path.ts +1 -4
  53. package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
  54. package/corpus/core/src/mcp/build-server.ts +1 -1
  55. package/corpus/core/src/org/handlers.ts +5 -5
  56. package/corpus/core/src/org/plugin.ts +2 -2
  57. package/corpus/core/src/scripts/call-agent.ts +1 -1
  58. package/corpus/core/src/scripts/db/safety.ts +1 -1
  59. package/corpus/core/src/server/auth.ts +3 -0
  60. package/corpus/core/src/server/builder-design-systems.ts +308 -0
  61. package/corpus/core/src/server/core-routes-plugin.ts +29 -3
  62. package/corpus/core/src/server/index.ts +8 -0
  63. package/corpus/core/src/server/transcribe-voice.ts +57 -22
  64. package/corpus/core/src/shared/mcp-embed-headers.ts +23 -1
  65. package/corpus/core/src/styles/agent-native.css +60 -4
  66. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  67. package/corpus/core/src/usage/store.ts +2 -2
  68. package/corpus/core/src/vite/client.ts +2 -0
  69. package/corpus/core/src/voice/index.ts +18 -0
  70. package/corpus/core/src/voice/voice-cleanup-prompt.ts +38 -0
  71. package/corpus/core/src/voice/voice-context.ts +217 -0
  72. package/corpus/core/src/voice/voice-replacements.ts +75 -0
  73. package/corpus/templates/analytics/AGENTS.md +4 -3
  74. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
  75. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
  76. package/corpus/templates/analytics/actions/list-session-recordings.ts +6 -1
  77. package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
  78. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
  79. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +557 -400
  80. package/corpus/templates/analytics/app/global.css +0 -4
  81. package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
  82. package/corpus/templates/analytics/app/i18n/zh-TW.ts +39 -0
  83. package/corpus/templates/analytics/app/i18n-data.ts +424 -0
  84. package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
  85. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
  86. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
  87. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +82 -34
  88. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +7 -0
  89. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +144 -36
  90. package/corpus/templates/analytics/changelog/2026-06-28-ask-tab-now-matches-the-darker-app-background.md +6 -0
  91. package/corpus/templates/analytics/changelog/2026-06-28-dashboard-email-reports-now-use-cleaner-light-mode-snapshots.md +6 -0
  92. package/corpus/templates/analytics/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  93. package/corpus/templates/analytics/changelog/2026-06-28-sessions-now-show-only-signed-in-recordings-with-playable-re.md +6 -0
  94. package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
  95. package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
  96. package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
  97. package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
  98. package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
  99. package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
  100. package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
  101. package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
  102. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  103. package/corpus/templates/analytics/server/lib/dashboard-report.ts +75 -30
  104. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +5 -2
  105. package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
  106. package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
  107. package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
  108. package/corpus/templates/analytics/server/lib/session-replay.ts +53 -12
  109. package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
  110. package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
  111. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -4
  112. package/corpus/templates/assets/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  113. package/corpus/templates/brain/app/components/layout/Layout.tsx +29 -2
  114. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +131 -42
  115. package/corpus/templates/brain/app/i18n/zh-TW.ts +2 -0
  116. package/corpus/templates/brain/app/i18n-data.ts +20 -0
  117. package/corpus/templates/brain/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  118. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +29 -1
  119. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +417 -299
  120. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  121. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  122. package/corpus/templates/calendar/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  123. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +6 -19
  124. package/corpus/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  125. package/corpus/templates/clips/AGENTS.md +4 -0
  126. package/corpus/templates/clips/actions/cleanup-transcript.ts +39 -9
  127. package/corpus/templates/clips/actions/finalize-meeting.ts +81 -12
  128. package/corpus/templates/clips/actions/lib/agents-md-context.ts +4 -1
  129. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -3
  130. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +1 -1
  131. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  132. package/corpus/templates/clips/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  133. package/corpus/templates/clips/desktop/src/lib/personal-vocabulary.ts +13 -8
  134. package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
  135. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
  136. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +60 -1
  137. package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
  138. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1 -1
  139. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -1
  140. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -5
  141. package/corpus/templates/content/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  142. package/corpus/templates/content/shared/document-export.ts +1 -1
  143. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
  144. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +154 -0
  145. package/corpus/templates/design/AGENTS.md +95 -5
  146. package/corpus/templates/design/DESIGN.md +82 -0
  147. package/corpus/templates/design/DEVELOPING.md +43 -0
  148. package/corpus/templates/design/actions/add-localhost-screens.ts +453 -0
  149. package/corpus/templates/design/actions/apply-shader.ts +277 -0
  150. package/corpus/templates/design/actions/apply-visual-edit.ts +303 -0
  151. package/corpus/templates/design/actions/connect-localhost.ts +183 -0
  152. package/corpus/templates/design/actions/edit-design.ts +195 -42
  153. package/corpus/templates/design/actions/generate-design.ts +71 -1
  154. package/corpus/templates/design/actions/generate-screens.ts +193 -0
  155. package/corpus/templates/design/actions/get-code-layer-projection.ts +191 -0
  156. package/corpus/templates/design/actions/get-shader.ts +119 -0
  157. package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
  158. package/corpus/templates/design/actions/list-localhost-connections.ts +95 -0
  159. package/corpus/templates/design/actions/navigate.ts +55 -2
  160. package/corpus/templates/design/actions/update-file.ts +9 -2
  161. package/corpus/templates/design/actions/view-screen.ts +68 -1
  162. package/corpus/templates/design/agent-native.app-skill.json +10 -0
  163. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +719 -0
  164. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +2120 -103
  165. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +74 -38
  166. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +401 -0
  167. package/corpus/templates/design/app/components/design/EditPanel.tsx +3966 -440
  168. package/corpus/templates/design/app/components/design/LayersPanel.tsx +1473 -0
  169. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +3940 -147
  170. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +700 -13
  171. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +105 -63
  172. package/corpus/templates/design/app/components/design/index.ts +0 -5
  173. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +353 -0
  174. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1589 -0
  175. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +511 -0
  176. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +395 -0
  177. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +2010 -0
  178. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
  179. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
  180. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +420 -0
  181. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +238 -0
  182. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +71 -0
  183. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +575 -0
  184. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +106 -0
  185. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
  186. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +741 -0
  187. package/corpus/templates/design/app/components/design/inspector/index.ts +87 -0
  188. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +201 -0
  189. package/corpus/templates/design/app/components/design/types.ts +23 -0
  190. package/corpus/templates/design/app/components/layout/Layout.tsx +24 -13
  191. package/corpus/templates/design/app/components/layout/Sidebar.tsx +4 -4
  192. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +474 -36
  193. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +22 -5
  194. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +24 -11
  195. package/corpus/templates/design/app/global.css +149 -5
  196. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -0
  197. package/corpus/templates/design/app/hooks/use-navigation-state.ts +130 -8
  198. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +329 -0
  199. package/corpus/templates/design/app/i18n/zh-TW.ts +277 -0
  200. package/corpus/templates/design/app/i18n-data.ts +3028 -1
  201. package/corpus/templates/design/app/pages/DesignEditor.tsx +5831 -904
  202. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
  203. package/corpus/templates/design/changelog/2026-06-28-annotate-mode-now-combines-drawing-and-comment-pins-in-one-c.md +6 -0
  204. package/corpus/templates/design/changelog/2026-06-28-design-editor-panel-borders-are-cleaner-so-the-canvas-edge-s.md +6 -0
  205. package/corpus/templates/design/changelog/2026-06-28-drawing-prompts-now-reliably-open-and-submit-to-the-design-a.md +6 -0
  206. package/corpus/templates/design/changelog/2026-06-28-editor-app-menus-now-use-an-outline-agent-native-mark-with-b.md +6 -0
  207. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +6 -0
  208. package/corpus/templates/design/changelog/2026-06-28-layer-selection-now-updates-chat-context-without-opening-the.md +6 -0
  209. package/corpus/templates/design/changelog/2026-06-28-layer-selections-now-preserve-hidden-and-locked-canvas-state.md +6 -0
  210. package/corpus/templates/design/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  211. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +6 -0
  212. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +6 -0
  213. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-handles-zooming-drawing-opening-and-dele.md +6 -0
  214. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-selects-screens-on-click-and-opens-them-.md +6 -0
  215. package/corpus/templates/design/changelog/2026-06-28-screen-overview-zooms-into-the-active-screen-with-smoother-e.md +6 -0
  216. package/corpus/templates/design/changelog/2026-06-28-selected-element-chips-in-chat-are-shorter-and-easier-to-sca.md +6 -0
  217. package/corpus/templates/design/changelog/2026-06-28-the-design-editor-toolbar-is-more-compact-and-the-app-menu-m.md +6 -0
  218. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-at-the-top-of-the-design-sidebar-instead-of-.md +6 -0
  219. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-in-the-right-inspector-beside-the-design-tab.md +6 -0
  220. package/corpus/templates/design/changelog/2026-06-29-active-sidebar-rows-use-soft-highlights.md +6 -0
  221. package/corpus/templates/design/changelog/2026-06-29-agents-can-now-open-screen-overview-focus-screens-and-add.md +6 -0
  222. package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
  223. package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
  224. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-edit-broad-copy-changes-more-reliably.md +6 -0
  225. package/corpus/templates/design/changelog/2026-06-29-design-editor-extensions-now-run-inline-beside-the-artboard.md +6 -0
  226. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
  227. package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
  228. package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
  229. package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
  230. package/corpus/templates/design/changelog/2026-06-29-sidebars-use-darker-chrome-and-chat-warns-immediately.md +6 -0
  231. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
  232. package/corpus/templates/design/changelog/2026-06-29-visual-edit-can-open-localhost-routes-as-url-backed-screens.md +6 -0
  233. package/corpus/templates/design/package.json +1 -0
  234. package/corpus/templates/design/server/db/schema.ts +26 -0
  235. package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
  236. package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
  237. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -0
  238. package/corpus/templates/design/shared/api.ts +27 -0
  239. package/corpus/templates/design/shared/canvas-frames.ts +103 -0
  240. package/corpus/templates/design/shared/canvas-math.ts +1058 -0
  241. package/corpus/templates/design/shared/code-layer.ts +2412 -0
  242. package/corpus/templates/design/shared/color-utils.ts +180 -0
  243. package/corpus/templates/design/shared/generation-session.ts +61 -0
  244. package/corpus/templates/design/shared/html-content.ts +21 -0
  245. package/corpus/templates/design/shared/pen-path.ts +207 -0
  246. package/corpus/templates/design/shared/shader-presets.ts +782 -0
  247. package/corpus/templates/design/shared/shader-safety.ts +137 -0
  248. package/corpus/templates/design/shared/source-mode.ts +237 -0
  249. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +149 -6
  250. package/corpus/templates/forms/app/i18n/ar-SA.ts +2 -0
  251. package/corpus/templates/forms/app/i18n/de-DE.ts +2 -0
  252. package/corpus/templates/forms/app/i18n/en-US.ts +2 -0
  253. package/corpus/templates/forms/app/i18n/es-ES.ts +2 -0
  254. package/corpus/templates/forms/app/i18n/fr-FR.ts +2 -0
  255. package/corpus/templates/forms/app/i18n/hi-IN.ts +2 -0
  256. package/corpus/templates/forms/app/i18n/ja-JP.ts +2 -0
  257. package/corpus/templates/forms/app/i18n/ko-KR.ts +2 -0
  258. package/corpus/templates/forms/app/i18n/pt-BR.ts +2 -0
  259. package/corpus/templates/forms/app/i18n/zh-CN.ts +2 -0
  260. package/corpus/templates/forms/app/i18n/zh-TW.ts +2 -0
  261. package/corpus/templates/forms/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  262. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +2 -2
  263. package/corpus/templates/macros/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  264. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +408 -247
  265. package/corpus/templates/mail/app/i18n/ar-SA.ts +4 -0
  266. package/corpus/templates/mail/app/i18n/de-DE.ts +4 -0
  267. package/corpus/templates/mail/app/i18n/en-US.ts +4 -0
  268. package/corpus/templates/mail/app/i18n/es-ES.ts +4 -0
  269. package/corpus/templates/mail/app/i18n/fr-FR.ts +4 -0
  270. package/corpus/templates/mail/app/i18n/hi-IN.ts +4 -0
  271. package/corpus/templates/mail/app/i18n/ja-JP.ts +4 -0
  272. package/corpus/templates/mail/app/i18n/ko-KR.ts +4 -0
  273. package/corpus/templates/mail/app/i18n/pt-BR.ts +4 -0
  274. package/corpus/templates/mail/app/i18n/zh-CN.ts +4 -0
  275. package/corpus/templates/mail/app/i18n/zh-TW.ts +4 -0
  276. package/corpus/templates/mail/app/lib/utils.ts +2 -2
  277. package/corpus/templates/mail/changelog/2026-06-28-pinned-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  278. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +6 -8
  279. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +1 -1
  280. package/corpus/templates/plan/app/global.css +61 -78
  281. package/corpus/templates/plan/app/pages/PlansPage.tsx +8 -0
  282. package/corpus/templates/plan/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  283. package/corpus/templates/plan/changelog/2026-06-28-plans-use-the-shared-neutral-light-and-dark-theme.md +6 -0
  284. package/corpus/templates/plan/changelog/2026-06-28-sending-open-feedback-from-a-plan-now-opens-the-inline-plan-.md +6 -0
  285. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
  286. package/corpus/templates/slides/AGENTS.md +3 -2
  287. package/corpus/templates/slides/actions/import-file.ts +26 -17
  288. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
  289. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
  290. package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
  291. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
  292. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
  293. package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
  294. package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
  295. package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
  296. package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
  297. package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
  298. package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
  299. package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
  300. package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
  301. package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
  302. package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
  303. package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
  304. package/corpus/templates/slides/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  305. package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
  306. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  307. package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
  308. package/corpus/templates/videos/app/components/Sidebar.tsx +2 -2
  309. package/corpus/templates/videos/app/components/layout/Layout.tsx +29 -2
  310. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +113 -30
  311. package/corpus/templates/videos/app/i18n/ar-SA.ts +2 -0
  312. package/corpus/templates/videos/app/i18n/de-DE.ts +2 -0
  313. package/corpus/templates/videos/app/i18n/en-US.ts +2 -0
  314. package/corpus/templates/videos/app/i18n/es-ES.ts +2 -0
  315. package/corpus/templates/videos/app/i18n/fr-FR.ts +2 -0
  316. package/corpus/templates/videos/app/i18n/hi-IN.ts +2 -0
  317. package/corpus/templates/videos/app/i18n/ja-JP.ts +2 -0
  318. package/corpus/templates/videos/app/i18n/ko-KR.ts +2 -0
  319. package/corpus/templates/videos/app/i18n/pt-BR.ts +2 -0
  320. package/corpus/templates/videos/app/i18n/zh-CN.ts +2 -0
  321. package/corpus/templates/videos/app/i18n/zh-TW.ts +2 -0
  322. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  323. package/dist/agent/engine/index.d.ts +1 -1
  324. package/dist/agent/engine/index.d.ts.map +1 -1
  325. package/dist/agent/engine/index.js +1 -1
  326. package/dist/agent/engine/index.js.map +1 -1
  327. package/dist/agent/engine/registry.d.ts +9 -0
  328. package/dist/agent/engine/registry.d.ts.map +1 -1
  329. package/dist/agent/engine/registry.js +37 -0
  330. package/dist/agent/engine/registry.js.map +1 -1
  331. package/dist/agent/production-agent.d.ts.map +1 -1
  332. package/dist/agent/production-agent.js +4 -3
  333. package/dist/agent/production-agent.js.map +1 -1
  334. package/dist/application-state/handlers.js +1 -1
  335. package/dist/application-state/handlers.js.map +1 -1
  336. package/dist/brand-kit/index.d.ts +1 -1
  337. package/dist/brand-kit/index.js +1 -1
  338. package/dist/brand-kit/index.js.map +1 -1
  339. package/dist/cli/design-connect.d.ts +52 -0
  340. package/dist/cli/design-connect.d.ts.map +1 -0
  341. package/dist/cli/design-connect.js +409 -0
  342. package/dist/cli/design-connect.js.map +1 -0
  343. package/dist/cli/index.js +15 -0
  344. package/dist/cli/index.js.map +1 -1
  345. package/dist/cli/skills.d.ts +3 -0
  346. package/dist/cli/skills.d.ts.map +1 -1
  347. package/dist/cli/skills.js +139 -3
  348. package/dist/cli/skills.js.map +1 -1
  349. package/dist/client/AgentPanel.js +1 -1
  350. package/dist/client/AgentPanel.js.map +1 -1
  351. package/dist/client/AssistantChat.d.ts.map +1 -1
  352. package/dist/client/AssistantChat.js +38 -8
  353. package/dist/client/AssistantChat.js.map +1 -1
  354. package/dist/client/MultiTabAssistantChat.js +1 -1
  355. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  356. package/dist/client/agent-chat.d.ts +6 -0
  357. package/dist/client/agent-chat.d.ts.map +1 -1
  358. package/dist/client/agent-chat.js +17 -3
  359. package/dist/client/agent-chat.js.map +1 -1
  360. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  361. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  362. package/dist/client/blocks/library/diagram.js +1 -1
  363. package/dist/client/blocks/library/diagram.js.map +1 -1
  364. package/dist/client/blocks/library/wireframe.js +1 -1
  365. package/dist/client/blocks/library/wireframe.js.map +1 -1
  366. package/dist/client/chat/message-components.js +1 -1
  367. package/dist/client/chat/message-components.js.map +1 -1
  368. package/dist/client/chat-model-groups.d.ts.map +1 -1
  369. package/dist/client/chat-model-groups.js +1 -2
  370. package/dist/client/chat-model-groups.js.map +1 -1
  371. package/dist/client/components/LiveCursorOverlay.d.ts +1 -2
  372. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  373. package/dist/client/components/LiveCursorOverlay.js +19 -40
  374. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  375. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  376. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  377. package/dist/client/composer/TiptapComposer.js +86 -24
  378. package/dist/client/composer/TiptapComposer.js.map +1 -1
  379. package/dist/client/composer/useVoiceDictation.d.ts +9 -0
  380. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  381. package/dist/client/composer/useVoiceDictation.js +154 -32
  382. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  383. package/dist/client/extensions/portable-extension.d.ts +1 -1
  384. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  385. package/dist/client/extensions/portable-extension.js.map +1 -1
  386. package/dist/client/frame-protocol.d.ts +17 -2
  387. package/dist/client/frame-protocol.d.ts.map +1 -1
  388. package/dist/client/frame-protocol.js.map +1 -1
  389. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +2 -2
  390. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  391. package/dist/client/sharing/ShareButton.d.ts +2 -0
  392. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  393. package/dist/client/sharing/ShareButton.js +7 -6
  394. package/dist/client/sharing/ShareButton.js.map +1 -1
  395. package/dist/client/use-agent-engine-configured.d.ts +5 -0
  396. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  397. package/dist/client/use-agent-engine-configured.js +51 -25
  398. package/dist/client/use-agent-engine-configured.js.map +1 -1
  399. package/dist/code-agents/transcript-normalizer.js +1 -1
  400. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  401. package/dist/collab/routes.d.ts +1 -1
  402. package/dist/extensions/path.d.ts +1 -1
  403. package/dist/extensions/path.d.ts.map +1 -1
  404. package/dist/extensions/path.js.map +1 -1
  405. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  406. package/dist/integrations/a2a-continuation-processor.js +1 -1
  407. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  408. package/dist/mcp/build-server.d.ts +1 -1
  409. package/dist/mcp/build-server.d.ts.map +1 -1
  410. package/dist/mcp/build-server.js.map +1 -1
  411. package/dist/notifications/routes.d.ts +3 -3
  412. package/dist/observability/routes.d.ts +5 -5
  413. package/dist/org/handlers.js +5 -5
  414. package/dist/org/handlers.js.map +1 -1
  415. package/dist/org/plugin.js +2 -2
  416. package/dist/org/plugin.js.map +1 -1
  417. package/dist/scripts/call-agent.js +1 -1
  418. package/dist/scripts/call-agent.js.map +1 -1
  419. package/dist/scripts/db/safety.js +1 -1
  420. package/dist/scripts/db/safety.js.map +1 -1
  421. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  422. package/dist/server/auth.d.ts.map +1 -1
  423. package/dist/server/auth.js +3 -0
  424. package/dist/server/auth.js.map +1 -1
  425. package/dist/server/builder-design-systems.d.ts +28 -0
  426. package/dist/server/builder-design-systems.d.ts.map +1 -0
  427. package/dist/server/builder-design-systems.js +217 -0
  428. package/dist/server/builder-design-systems.js.map +1 -0
  429. package/dist/server/core-routes-plugin.d.ts +1 -0
  430. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  431. package/dist/server/core-routes-plugin.js +20 -3
  432. package/dist/server/core-routes-plugin.js.map +1 -1
  433. package/dist/server/index.d.ts +1 -0
  434. package/dist/server/index.d.ts.map +1 -1
  435. package/dist/server/index.js +1 -0
  436. package/dist/server/index.js.map +1 -1
  437. package/dist/server/transcribe-voice.d.ts +1 -1
  438. package/dist/server/transcribe-voice.d.ts.map +1 -1
  439. package/dist/server/transcribe-voice.js +40 -24
  440. package/dist/server/transcribe-voice.js.map +1 -1
  441. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  442. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  443. package/dist/shared/mcp-embed-headers.js +20 -1
  444. package/dist/shared/mcp-embed-headers.js.map +1 -1
  445. package/dist/styles/agent-native.css +60 -4
  446. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  447. package/dist/usage/store.js +2 -2
  448. package/dist/usage/store.js.map +1 -1
  449. package/dist/vite/client.d.ts.map +1 -1
  450. package/dist/vite/client.js +2 -0
  451. package/dist/vite/client.js.map +1 -1
  452. package/dist/voice/index.d.ts +5 -0
  453. package/dist/voice/index.d.ts.map +1 -0
  454. package/dist/voice/index.js +4 -0
  455. package/dist/voice/index.js.map +1 -0
  456. package/dist/voice/voice-cleanup-prompt.d.ts +6 -0
  457. package/dist/voice/voice-cleanup-prompt.d.ts.map +1 -0
  458. package/dist/voice/voice-cleanup-prompt.js +26 -0
  459. package/dist/voice/voice-cleanup-prompt.js.map +1 -0
  460. package/dist/voice/voice-context.d.ts +25 -0
  461. package/dist/voice/voice-context.d.ts.map +1 -0
  462. package/dist/voice/voice-context.js +156 -0
  463. package/dist/voice/voice-context.js.map +1 -0
  464. package/dist/voice/voice-replacements.d.ts +4 -0
  465. package/dist/voice/voice-replacements.d.ts.map +1 -0
  466. package/dist/voice/voice-replacements.js +52 -0
  467. package/dist/voice/voice-replacements.js.map +1 -0
  468. package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
  469. package/docs/content/locales/ar-SA/template-design.mdx +55 -1
  470. package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
  471. package/docs/content/locales/de-DE/template-design.mdx +66 -1
  472. package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
  473. package/docs/content/locales/es-ES/template-design.mdx +65 -1
  474. package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
  475. package/docs/content/locales/fr-FR/template-design.mdx +64 -1
  476. package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
  477. package/docs/content/locales/hi-IN/template-design.mdx +59 -1
  478. package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
  479. package/docs/content/locales/ja-JP/template-design.mdx +62 -1
  480. package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
  481. package/docs/content/locales/ko-KR/template-design.mdx +58 -1
  482. package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
  483. package/docs/content/locales/pt-BR/template-design.mdx +62 -1
  484. package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
  485. package/docs/content/locales/zh-CN/template-design.mdx +50 -1
  486. package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
  487. package/docs/content/locales/zh-TW/template-design.mdx +52 -1
  488. package/docs/content/skills-guide.mdx +5 -0
  489. package/docs/content/template-design.mdx +59 -1
  490. package/package.json +2 -1
  491. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  492. package/corpus/templates/design/actions/import-figma-file.ts +0 -166
  493. package/corpus/templates/design/app/components/design/DesignToolbar.tsx +0 -339
  494. package/corpus/templates/design/app/components/design/PresentMode.tsx +0 -68
  495. package/corpus/templates/design/app/components/design/ViewportTabs.tsx +0 -149
  496. package/corpus/templates/design/app/components/design/ZoomControls.tsx +0 -157
  497. package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
@@ -0,0 +1,2412 @@
1
+ import type { DesignSourceType } from "./source-mode";
2
+
3
+ export type CodeLayerSourceKind =
4
+ | "design-file"
5
+ | "inline-html"
6
+ | "local-file"
7
+ | "remote-url";
8
+
9
+ export interface CodeLayerSource {
10
+ kind: CodeLayerSourceKind;
11
+ sourceType?: DesignSourceType;
12
+ designId?: string;
13
+ fileId?: string;
14
+ filename?: string;
15
+ path?: string;
16
+ url?: string;
17
+ connectionId?: string;
18
+ routeId?: string;
19
+ artboardId?: string;
20
+ bridgeUrl?: string;
21
+ revision?: string;
22
+ }
23
+
24
+ export interface CodeLayerSourceSpan {
25
+ start: number;
26
+ end: number;
27
+ openStart: number;
28
+ openEnd: number;
29
+ contentStart?: number;
30
+ contentEnd?: number;
31
+ closeStart?: number;
32
+ closeEnd?: number;
33
+ }
34
+
35
+ export type VisualStyleProperty =
36
+ | "width"
37
+ | "height"
38
+ | "min-width"
39
+ | "max-width"
40
+ | "min-height"
41
+ | "max-height"
42
+ | "left"
43
+ | "top"
44
+ | "right"
45
+ | "bottom"
46
+ | "inset"
47
+ | "position"
48
+ | "display"
49
+ | "color"
50
+ | "background"
51
+ | "background-color"
52
+ | "background-image"
53
+ | "background-blend-mode"
54
+ | "fill"
55
+ | "fill-opacity"
56
+ | "opacity"
57
+ | "mix-blend-mode"
58
+ | "font-size"
59
+ | "font-weight"
60
+ | "font-family"
61
+ | "font-style"
62
+ | "letter-spacing"
63
+ | "line-height"
64
+ | "text-align"
65
+ | "text-decoration"
66
+ | "text-transform"
67
+ | "white-space"
68
+ | "overflow"
69
+ | "overflow-x"
70
+ | "overflow-y"
71
+ | "text-overflow"
72
+ | "border"
73
+ | "border-width"
74
+ | "border-style"
75
+ | "border-color"
76
+ | "border-radius"
77
+ | "border-top-left-radius"
78
+ | "border-top-right-radius"
79
+ | "border-bottom-left-radius"
80
+ | "border-bottom-right-radius"
81
+ | "stroke"
82
+ | "stroke-width"
83
+ | "stroke-opacity"
84
+ | "stroke-dasharray"
85
+ | "stroke-linecap"
86
+ | "stroke-linejoin"
87
+ | "outline"
88
+ | "outline-width"
89
+ | "outline-style"
90
+ | "outline-color"
91
+ | "outline-offset"
92
+ | "box-shadow"
93
+ | "text-shadow"
94
+ | "filter"
95
+ | "backdrop-filter"
96
+ | "transform"
97
+ | "transform-origin"
98
+ | "rotate"
99
+ | "scale"
100
+ | "translate"
101
+ | "padding"
102
+ | "padding-top"
103
+ | "padding-right"
104
+ | "padding-bottom"
105
+ | "padding-left"
106
+ | "margin"
107
+ | "margin-top"
108
+ | "margin-right"
109
+ | "margin-bottom"
110
+ | "margin-left"
111
+ | "gap"
112
+ | "row-gap"
113
+ | "column-gap"
114
+ | "flex"
115
+ | "flex-direction"
116
+ | "flex-wrap"
117
+ | "flex-grow"
118
+ | "flex-shrink"
119
+ | "flex-basis"
120
+ | "order"
121
+ | "align-self"
122
+ | "align-items"
123
+ | "align-content"
124
+ | "justify-content"
125
+ | "justify-items"
126
+ | "justify-self"
127
+ | "grid-column"
128
+ | "grid-row"
129
+ | "grid-template-columns"
130
+ | "grid-template-rows"
131
+ | "grid-auto-flow"
132
+ | "grid-auto-columns"
133
+ | "grid-auto-rows"
134
+ | "box-sizing"
135
+ | "aspect-ratio"
136
+ | "z-index";
137
+
138
+ export interface StyleToken {
139
+ property: VisualStyleProperty;
140
+ value: string;
141
+ token: string;
142
+ source: "inline-style" | "class";
143
+ confidence: number;
144
+ }
145
+
146
+ export interface LayoutContext {
147
+ parentId?: string;
148
+ parentSelector?: string;
149
+ siblingIndex: number;
150
+ nthOfType: number;
151
+ display?: string;
152
+ position?: string;
153
+ width?: string;
154
+ height?: string;
155
+ flexDirection?: string;
156
+ alignItems?: string;
157
+ justifyContent?: string;
158
+ gap?: string;
159
+ padding?: string;
160
+ parentDisplay?: string;
161
+ parentFlexDirection?: string;
162
+ parentGap?: string;
163
+ isFlexContainer: boolean;
164
+ isGridContainer: boolean;
165
+ }
166
+
167
+ export type EditCapability =
168
+ | {
169
+ kind: "style";
170
+ properties: VisualStyleProperty[];
171
+ confidence: number;
172
+ reason?: string;
173
+ }
174
+ | {
175
+ kind: "class";
176
+ operations: Array<"add" | "remove" | "replace" | "set">;
177
+ confidence: number;
178
+ reason?: string;
179
+ }
180
+ | {
181
+ kind: "text";
182
+ operations: Array<"setTextContent">;
183
+ confidence: number;
184
+ reason?: string;
185
+ }
186
+ | {
187
+ kind: "structure";
188
+ operations: Array<"moveNode">;
189
+ confidence: number;
190
+ reason?: string;
191
+ };
192
+
193
+ export interface CodeLayerNode {
194
+ id: string;
195
+ tag: string;
196
+ layerName: string;
197
+ layerNameSource: "attribute" | "semantic" | "text" | "selector" | "tag";
198
+ layerNameAttribute?: string;
199
+ selector: string;
200
+ selectors: string[];
201
+ path: string;
202
+ attributes: Record<string, string | true>;
203
+ dataAttributes: Record<string, string>;
204
+ classes: string[];
205
+ textSnippet: string | null;
206
+ style: Partial<Record<VisualStyleProperty | string, string>>;
207
+ styleTokens: StyleToken[];
208
+ parentId?: string;
209
+ children: string[];
210
+ layout: LayoutContext;
211
+ capabilities: EditCapability[];
212
+ confidence: number;
213
+ source: CodeLayerSourceSpan | null;
214
+ }
215
+
216
+ export interface ProjectionDiagnostic {
217
+ severity: "info" | "warning";
218
+ code: string;
219
+ message: string;
220
+ span?: { start: number; end: number };
221
+ }
222
+
223
+ export interface CodeLayerProjection {
224
+ version: 1;
225
+ projectionId: string;
226
+ source: CodeLayerSource;
227
+ rootNodeIds: string[];
228
+ nodes: CodeLayerNode[];
229
+ diagnostics: ProjectionDiagnostic[];
230
+ }
231
+
232
+ export type CodeLayerTreeNodeType =
233
+ | "frame"
234
+ | "group"
235
+ | "component"
236
+ | "shape"
237
+ | "text"
238
+ | "image"
239
+ | "element";
240
+
241
+ export interface CodeLayerTreeNode {
242
+ id: string;
243
+ name: string;
244
+ type: CodeLayerTreeNodeType;
245
+ tag: string;
246
+ selector: string;
247
+ detail: string;
248
+ layout?: Pick<
249
+ LayoutContext,
250
+ | "display"
251
+ | "flexDirection"
252
+ | "alignItems"
253
+ | "justifyContent"
254
+ | "isFlexContainer"
255
+ | "isGridContainer"
256
+ >;
257
+ badge?: string;
258
+ renamable: boolean;
259
+ children: CodeLayerTreeNode[];
260
+ }
261
+
262
+ export interface PreviewBridgeProjectionPayload {
263
+ type: "code-layer-projection";
264
+ projection: CodeLayerProjection;
265
+ }
266
+
267
+ export interface PreviewBridgeSelectionPayload {
268
+ type: "code-layer-selection";
269
+ source: CodeLayerSource;
270
+ nodeId?: string;
271
+ selector?: string;
272
+ bounds?: {
273
+ x: number;
274
+ y: number;
275
+ width: number;
276
+ height: number;
277
+ };
278
+ }
279
+
280
+ export interface PreviewBridgeEditPayload {
281
+ type: "code-layer-edit-intent";
282
+ source: CodeLayerSource;
283
+ intent: EditIntent;
284
+ }
285
+
286
+ export type PreviewBridgePayload =
287
+ | PreviewBridgeProjectionPayload
288
+ | PreviewBridgeSelectionPayload
289
+ | PreviewBridgeEditPayload;
290
+
291
+ export interface EditIntentTarget {
292
+ nodeId?: string;
293
+ selector?: string;
294
+ }
295
+
296
+ export interface StyleEditIntent {
297
+ kind: "style";
298
+ target: EditIntentTarget;
299
+ property: VisualStyleProperty | string;
300
+ value: string;
301
+ }
302
+
303
+ export interface ClassEditIntent {
304
+ kind: "class";
305
+ target: EditIntentTarget;
306
+ operation: "add" | "remove" | "replace" | "set";
307
+ className?: string;
308
+ classNames?: string[];
309
+ from?: string;
310
+ to?: string;
311
+ }
312
+
313
+ export interface TextEditIntent {
314
+ kind: "textContent";
315
+ target: EditIntentTarget;
316
+ value: string;
317
+ html?: string;
318
+ }
319
+
320
+ export interface MoveNodeEditIntent {
321
+ kind: "moveNode";
322
+ target: EditIntentTarget;
323
+ anchor: EditIntentTarget;
324
+ placement: "before" | "after" | "inside";
325
+ }
326
+
327
+ export type EditIntent =
328
+ | StyleEditIntent
329
+ | ClassEditIntent
330
+ | TextEditIntent
331
+ | MoveNodeEditIntent;
332
+
333
+ export interface EditIntentResolution {
334
+ status: "resolved" | "conflict" | "unsupported";
335
+ node?: CodeLayerNode;
336
+ message?: string;
337
+ }
338
+
339
+ export interface EditIntentResolver {
340
+ resolve(
341
+ intent: EditIntent,
342
+ projection: CodeLayerProjection,
343
+ ): EditIntentResolution | Promise<EditIntentResolution>;
344
+ }
345
+
346
+ export type PatchResultStatus =
347
+ | "applied"
348
+ | "needsAgent"
349
+ | "conflict"
350
+ | "unsupported";
351
+
352
+ export interface PatchNodeSummary {
353
+ nodeId: string;
354
+ selector: string;
355
+ tag: string;
356
+ classes: string[];
357
+ style: Partial<Record<VisualStyleProperty | string, string>>;
358
+ textSnippet: string | null;
359
+ }
360
+
361
+ export interface PatchResult {
362
+ status: PatchResultStatus;
363
+ source: CodeLayerSource;
364
+ intent: EditIntent;
365
+ target?: {
366
+ nodeId: string;
367
+ selector: string;
368
+ tag: string;
369
+ };
370
+ capability?: EditCapability;
371
+ before?: PatchNodeSummary;
372
+ after?: PatchNodeSummary;
373
+ changed: boolean;
374
+ message?: string;
375
+ }
376
+
377
+ export interface ApplyVisualEditResult {
378
+ content: string;
379
+ projection: CodeLayerProjection;
380
+ result: PatchResult;
381
+ }
382
+
383
+ interface ParsedAttribute {
384
+ name: string;
385
+ lowerName: string;
386
+ value: string | true;
387
+ start: number;
388
+ end: number;
389
+ }
390
+
391
+ interface ParsedElement {
392
+ index: number;
393
+ tag: string;
394
+ start: number;
395
+ openEnd: number;
396
+ end: number;
397
+ contentStart: number;
398
+ contentEnd: number;
399
+ closeStart?: number;
400
+ closeEnd?: number;
401
+ selfClosing: boolean;
402
+ attributes: ParsedAttribute[];
403
+ parentIndex?: number;
404
+ childIndexes: number[];
405
+ siblingIndex: number;
406
+ nthOfType: number;
407
+ }
408
+
409
+ interface ProjectionBuild {
410
+ projection: CodeLayerProjection;
411
+ elementByNodeId: Map<string, ParsedElement>;
412
+ }
413
+
414
+ const STYLE_PROPERTIES = [
415
+ "width",
416
+ "height",
417
+ "min-width",
418
+ "max-width",
419
+ "min-height",
420
+ "max-height",
421
+ "left",
422
+ "top",
423
+ "right",
424
+ "bottom",
425
+ "inset",
426
+ "position",
427
+ "display",
428
+ "color",
429
+ "background",
430
+ "background-color",
431
+ "background-image",
432
+ "background-blend-mode",
433
+ "fill",
434
+ "fill-opacity",
435
+ "opacity",
436
+ "mix-blend-mode",
437
+ "font-size",
438
+ "font-weight",
439
+ "font-family",
440
+ "font-style",
441
+ "letter-spacing",
442
+ "line-height",
443
+ "text-align",
444
+ "text-decoration",
445
+ "text-transform",
446
+ "white-space",
447
+ "overflow",
448
+ "overflow-x",
449
+ "overflow-y",
450
+ "text-overflow",
451
+ "border",
452
+ "border-width",
453
+ "border-style",
454
+ "border-color",
455
+ "border-radius",
456
+ "border-top-left-radius",
457
+ "border-top-right-radius",
458
+ "border-bottom-left-radius",
459
+ "border-bottom-right-radius",
460
+ "stroke",
461
+ "stroke-width",
462
+ "stroke-opacity",
463
+ "stroke-dasharray",
464
+ "stroke-linecap",
465
+ "stroke-linejoin",
466
+ "outline",
467
+ "outline-width",
468
+ "outline-style",
469
+ "outline-color",
470
+ "outline-offset",
471
+ "box-shadow",
472
+ "text-shadow",
473
+ "filter",
474
+ "backdrop-filter",
475
+ "transform",
476
+ "transform-origin",
477
+ "rotate",
478
+ "scale",
479
+ "translate",
480
+ "padding",
481
+ "padding-top",
482
+ "padding-right",
483
+ "padding-bottom",
484
+ "padding-left",
485
+ "margin",
486
+ "margin-top",
487
+ "margin-right",
488
+ "margin-bottom",
489
+ "margin-left",
490
+ "gap",
491
+ "row-gap",
492
+ "column-gap",
493
+ "flex",
494
+ "flex-direction",
495
+ "flex-wrap",
496
+ "flex-grow",
497
+ "flex-shrink",
498
+ "flex-basis",
499
+ "order",
500
+ "align-self",
501
+ "align-items",
502
+ "align-content",
503
+ "justify-content",
504
+ "justify-items",
505
+ "justify-self",
506
+ "grid-column",
507
+ "grid-row",
508
+ "grid-template-columns",
509
+ "grid-template-rows",
510
+ "grid-auto-flow",
511
+ "grid-auto-columns",
512
+ "grid-auto-rows",
513
+ "box-sizing",
514
+ "aspect-ratio",
515
+ "z-index",
516
+ ] as const satisfies readonly VisualStyleProperty[];
517
+
518
+ const STYLE_PROPERTY_SET = new Set<string>(STYLE_PROPERTIES);
519
+
520
+ const STYLE_PROPERTY_ALIASES: Record<string, VisualStyleProperty> = {
521
+ backgroundColor: "background-color",
522
+ bg: "background",
523
+ cornerRadius: "border-radius",
524
+ dropShadow: "box-shadow",
525
+ radius: "border-radius",
526
+ rotation: "rotate",
527
+ shadow: "box-shadow",
528
+ };
529
+
530
+ const VOID_TAGS = new Set([
531
+ "area",
532
+ "base",
533
+ "br",
534
+ "col",
535
+ "embed",
536
+ "hr",
537
+ "img",
538
+ "input",
539
+ "link",
540
+ "meta",
541
+ "param",
542
+ "source",
543
+ "track",
544
+ "wbr",
545
+ ]);
546
+
547
+ const NON_VISUAL_TAGS = new Set([
548
+ "head",
549
+ "script",
550
+ "style",
551
+ "meta",
552
+ "link",
553
+ "title",
554
+ "template",
555
+ "noscript",
556
+ ]);
557
+
558
+ const RAW_TEXT_VISUAL_TAGS = new Set(["textarea"]);
559
+
560
+ const DATA_SELECTOR_PRIORITY = [
561
+ "data-agent-native-node-id",
562
+ "data-code-layer-id",
563
+ "data-layer-id",
564
+ "data-builder-id",
565
+ "data-loc",
566
+ "data-testid",
567
+ "data-test-id",
568
+ "data-component",
569
+ "data-name",
570
+ "data-screen",
571
+ ];
572
+
573
+ const STABLE_NODE_ID_ATTRIBUTES = [
574
+ "data-agent-native-node-id",
575
+ "data-code-layer-id",
576
+ "data-layer-id",
577
+ "data-builder-id",
578
+ "data-loc",
579
+ ] as const;
580
+
581
+ const LAYER_NAME_ATTRIBUTE_PRIORITY = [
582
+ "data-agent-native-layer-name",
583
+ "data-layer-name",
584
+ ] as const;
585
+
586
+ const SEMANTIC_LABEL_ATTRIBUTE_PRIORITY = [
587
+ "aria-label",
588
+ "title",
589
+ "data-code-layer-id",
590
+ "data-layer-id",
591
+ "data-name",
592
+ "data-component",
593
+ "data-screen",
594
+ "data-testid",
595
+ "data-test-id",
596
+ ] as const;
597
+
598
+ const TEXT_LAYER_TAGS = new Set([
599
+ "a",
600
+ "button",
601
+ "em",
602
+ "h1",
603
+ "h2",
604
+ "h3",
605
+ "h4",
606
+ "h5",
607
+ "h6",
608
+ "label",
609
+ "li",
610
+ "p",
611
+ "span",
612
+ "strong",
613
+ ]);
614
+
615
+ const IMAGE_LAYER_TAGS = new Set(["canvas", "figure", "img", "picture"]);
616
+ const SHAPE_LAYER_TAGS = new Set([
617
+ "circle",
618
+ "line",
619
+ "path",
620
+ "polygon",
621
+ "rect",
622
+ "svg",
623
+ ]);
624
+ const COMPONENT_LAYER_TAGS = new Set(["button", "input", "select", "textarea"]);
625
+
626
+ function hashStable(value: string): string {
627
+ let hash = 0x811c9dc5;
628
+ for (let i = 0; i < value.length; i += 1) {
629
+ hash ^= value.charCodeAt(i);
630
+ hash = Math.imul(hash, 0x01000193);
631
+ }
632
+ return (hash >>> 0).toString(36);
633
+ }
634
+
635
+ function stableAttributeValueForNode(node: CodeLayerNode): string {
636
+ const basis = [
637
+ node.id,
638
+ node.tag,
639
+ node.path,
640
+ node.source?.openStart ?? 0,
641
+ node.source?.openEnd ?? 0,
642
+ ].join(":");
643
+ return `an-${hashStable(basis)}`;
644
+ }
645
+
646
+ function collapseWhitespace(value: string): string {
647
+ return value.replace(/\s+/g, " ").trim();
648
+ }
649
+
650
+ function cssEscape(value: string): string {
651
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
652
+ }
653
+
654
+ function cssIdent(value: string): string | null {
655
+ if (/^-?[A-Za-z_][A-Za-z0-9_-]*$/.test(value)) return value;
656
+ return null;
657
+ }
658
+
659
+ function unquoteHtmlAttributeValue(value: string): string {
660
+ const trimmed = value.trim();
661
+ if (
662
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
663
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
664
+ ) {
665
+ return trimmed.slice(1, -1);
666
+ }
667
+ return trimmed;
668
+ }
669
+
670
+ function escapeHtmlAttribute(value: string): string {
671
+ return value
672
+ .replace(/&/g, "&amp;")
673
+ .replace(/"/g, "&quot;")
674
+ .replace(/</g, "&lt;")
675
+ .replace(/>/g, "&gt;");
676
+ }
677
+
678
+ function escapeHtmlText(value: string): string {
679
+ return value
680
+ .replace(/&/g, "&amp;")
681
+ .replace(/</g, "&lt;")
682
+ .replace(/>/g, "&gt;");
683
+ }
684
+
685
+ function decodeBasicHtmlEntities(value: string): string {
686
+ return value
687
+ .replace(/&nbsp;/g, " ")
688
+ .replace(/&amp;/g, "&")
689
+ .replace(/&lt;/g, "<")
690
+ .replace(/&gt;/g, ">")
691
+ .replace(/&quot;/g, '"')
692
+ .replace(/&#39;/g, "'");
693
+ }
694
+
695
+ function truncateLayerName(value: string): string {
696
+ const normalized = collapseWhitespace(decodeBasicHtmlEntities(value));
697
+ if (normalized.length <= 72) return normalized;
698
+ return `${normalized.slice(0, 69)}...`;
699
+ }
700
+
701
+ function prettifyIdentifier(value: string): string {
702
+ return collapseWhitespace(
703
+ value
704
+ .replace(/[_-]+/g, " ")
705
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
706
+ .toLowerCase()
707
+ .replace(/\b\w/g, (char) => char.toUpperCase()),
708
+ );
709
+ }
710
+
711
+ function stripTags(value: string): string {
712
+ return value.replace(/<[^>]*>/g, " ");
713
+ }
714
+
715
+ function getAttribute(
716
+ element: ParsedElement,
717
+ name: string,
718
+ ): ParsedAttribute | undefined {
719
+ const lowerName = name.toLowerCase();
720
+ return element.attributes.find((attr) => attr.lowerName === lowerName);
721
+ }
722
+
723
+ function attributeValue(element: ParsedElement, name: string): string | null {
724
+ const value = getAttribute(element, name)?.value;
725
+ if (typeof value === "string") return value;
726
+ if (value === true) return "";
727
+ return null;
728
+ }
729
+
730
+ function explicitLayerNameFor(element: ParsedElement): {
731
+ name: string;
732
+ source: CodeLayerNode["layerNameSource"];
733
+ attribute?: string;
734
+ } | null {
735
+ for (const attribute of LAYER_NAME_ATTRIBUTE_PRIORITY) {
736
+ const value = attributeValue(element, attribute);
737
+ if (value) {
738
+ const name = truncateLayerName(value);
739
+ if (name) return { name, source: "attribute", attribute };
740
+ }
741
+ }
742
+ return null;
743
+ }
744
+
745
+ function semanticLayerNameFor(element: ParsedElement): {
746
+ name: string;
747
+ source: CodeLayerNode["layerNameSource"];
748
+ attribute?: string;
749
+ } | null {
750
+ for (const attribute of SEMANTIC_LABEL_ATTRIBUTE_PRIORITY) {
751
+ const value = attributeValue(element, attribute);
752
+ if (value) {
753
+ const name =
754
+ attribute === "aria-label" || attribute === "title"
755
+ ? truncateLayerName(value)
756
+ : prettifyIdentifier(value);
757
+ if (name) return { name, source: "semantic", attribute };
758
+ }
759
+ }
760
+
761
+ const id = attributeValue(element, "id");
762
+ if (id) {
763
+ return {
764
+ name: prettifyIdentifier(id),
765
+ source: "selector",
766
+ attribute: "id",
767
+ };
768
+ }
769
+
770
+ const meaningfulClass = classList(element).find(
771
+ (token) =>
772
+ !/^(flex|grid|block|inline|hidden|relative|absolute|fixed|sticky)$/.test(
773
+ token,
774
+ ) &&
775
+ !/^(sm|md|lg|xl|2xl):/.test(token) &&
776
+ !/^(p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml|w|h|min|max|text|bg|border|rounded|shadow|gap)-/.test(
777
+ token,
778
+ ),
779
+ );
780
+ if (meaningfulClass) {
781
+ return { name: prettifyIdentifier(meaningfulClass), source: "selector" };
782
+ }
783
+
784
+ return null;
785
+ }
786
+
787
+ function fallbackTagLayerName(tag: string): string {
788
+ switch (tag) {
789
+ case "article":
790
+ return "Article";
791
+ case "aside":
792
+ return "Aside";
793
+ case "body":
794
+ return "Body";
795
+ case "button":
796
+ return "Button";
797
+ case "div":
798
+ return "Frame";
799
+ case "footer":
800
+ return "Footer";
801
+ case "form":
802
+ return "Form";
803
+ case "header":
804
+ return "Header";
805
+ case "img":
806
+ case "picture":
807
+ return "Image";
808
+ case "main":
809
+ return "Main";
810
+ case "nav":
811
+ return "Navigation";
812
+ case "section":
813
+ return "Section";
814
+ case "svg":
815
+ return "Vector";
816
+ case "ul":
817
+ case "ol":
818
+ return "List";
819
+ case "li":
820
+ return "List item";
821
+ default:
822
+ if (TEXT_LAYER_TAGS.has(tag)) return "Text";
823
+ return tag.toUpperCase();
824
+ }
825
+ }
826
+
827
+ function attributeRecord(
828
+ element: ParsedElement,
829
+ ): Record<string, string | true> {
830
+ const record: Record<string, string | true> = {};
831
+ for (const attr of element.attributes) {
832
+ record[attr.lowerName] = attr.value;
833
+ }
834
+ return record;
835
+ }
836
+
837
+ function dataAttributeRecord(element: ParsedElement): Record<string, string> {
838
+ const record: Record<string, string> = {};
839
+ for (const attr of element.attributes) {
840
+ if (attr.lowerName.startsWith("data-") && typeof attr.value === "string") {
841
+ record[attr.lowerName] = attr.value;
842
+ }
843
+ }
844
+ return record;
845
+ }
846
+
847
+ function classList(element: ParsedElement): string[] {
848
+ return collapseWhitespace(attributeValue(element, "class") ?? "")
849
+ .split(" ")
850
+ .filter(Boolean);
851
+ }
852
+
853
+ function parseStyle(value: string | null): Record<string, string> {
854
+ const style: Record<string, string> = {};
855
+ if (!value) return style;
856
+ for (const part of value.split(";")) {
857
+ const index = part.indexOf(":");
858
+ if (index === -1) continue;
859
+ const property = part.slice(0, index).trim().toLowerCase();
860
+ const propertyValue = part.slice(index + 1).trim();
861
+ if (property && propertyValue) style[property] = propertyValue;
862
+ }
863
+ return style;
864
+ }
865
+
866
+ function parseStyleDeclarations(value: string | null): Array<{
867
+ property: string;
868
+ value: string;
869
+ }> {
870
+ if (!value) return [];
871
+ return value
872
+ .split(";")
873
+ .map((part) => {
874
+ const index = part.indexOf(":");
875
+ if (index === -1) return null;
876
+ const property = part.slice(0, index).trim().toLowerCase();
877
+ const propertyValue = part.slice(index + 1).trim();
878
+ if (!property || !propertyValue) return null;
879
+ return { property, value: propertyValue };
880
+ })
881
+ .filter((part): part is { property: string; value: string } =>
882
+ Boolean(part),
883
+ );
884
+ }
885
+
886
+ function serializeStyleDeclarations(
887
+ declarations: Array<{ property: string; value: string }>,
888
+ ): string {
889
+ return declarations
890
+ .map((item) => `${item.property}: ${item.value}`)
891
+ .join("; ");
892
+ }
893
+
894
+ function normalizeStyleProperty(property: string): VisualStyleProperty | null {
895
+ const normalized =
896
+ STYLE_PROPERTY_ALIASES[property] ??
897
+ property
898
+ .replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)
899
+ .toLowerCase();
900
+ if (!STYLE_PROPERTY_SET.has(normalized)) return null;
901
+ return normalized as VisualStyleProperty;
902
+ }
903
+
904
+ function isSafeStyleValue(
905
+ property: VisualStyleProperty,
906
+ value: string,
907
+ ): boolean {
908
+ const trimmed = value.trim();
909
+ if (!trimmed) return false;
910
+ if (/[<>{};]/.test(trimmed)) return false;
911
+ if (/expression\s*\(/i.test(trimmed)) return false;
912
+ if (/javascript\s*:/i.test(trimmed)) return false;
913
+ if (/url\s*\(/i.test(trimmed)) return false;
914
+ if (property === "display") {
915
+ return [
916
+ "block",
917
+ "inline",
918
+ "inline-block",
919
+ "flex",
920
+ "inline-flex",
921
+ "grid",
922
+ "inline-grid",
923
+ "none",
924
+ "contents",
925
+ ].includes(trimmed);
926
+ }
927
+ return true;
928
+ }
929
+
930
+ function isSafeClassToken(value: string): boolean {
931
+ return value.length > 0 && !/[\s"'<>`=]/.test(value);
932
+ }
933
+
934
+ function classTokensFromIntent(intent: ClassEditIntent): string[] {
935
+ if (intent.classNames) return intent.classNames;
936
+ if (intent.className) return [intent.className];
937
+ return [];
938
+ }
939
+
940
+ function parseAttributes(rawTag: string, tagStart: number): ParsedAttribute[] {
941
+ const nameMatch = rawTag.match(/^<\s*\/?\s*([A-Za-z][A-Za-z0-9:-]*)/);
942
+ if (!nameMatch?.[0]) return [];
943
+ const attrTextStart = nameMatch[0].length;
944
+ const attrTextEnd = rawTag.endsWith(">") ? rawTag.length - 1 : rawTag.length;
945
+ const attrText = rawTag.slice(attrTextStart, attrTextEnd);
946
+ const attrOffset = tagStart + attrTextStart;
947
+ const attrs: ParsedAttribute[] = [];
948
+ const attrRe =
949
+ /([:@A-Za-z_][A-Za-z0-9_:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
950
+ let match: RegExpExecArray | null;
951
+ while ((match = attrRe.exec(attrText))) {
952
+ const name = match[1];
953
+ if (!name || name === "/") continue;
954
+ const value = match[2] ?? match[3] ?? match[4] ?? true;
955
+ attrs.push({
956
+ name,
957
+ lowerName: name.toLowerCase(),
958
+ value,
959
+ start: attrOffset + match.index,
960
+ end: attrOffset + match.index + match[0].length,
961
+ });
962
+ }
963
+ return attrs;
964
+ }
965
+
966
+ function findHtmlTagEnd(html: string, start: number): number {
967
+ let quote: '"' | "'" | null = null;
968
+ for (let index = start; index < html.length; index += 1) {
969
+ const char = html[index];
970
+ if (quote) {
971
+ if (char === quote) quote = null;
972
+ continue;
973
+ }
974
+ if (char === '"' || char === "'") {
975
+ quote = char;
976
+ continue;
977
+ }
978
+ if (char === ">") return index + 1;
979
+ }
980
+ return html.length;
981
+ }
982
+
983
+ function findClosingTag(
984
+ html: string,
985
+ tag: string,
986
+ from: number,
987
+ ): { closeStart: number; closeEnd: number } | null {
988
+ const closeRe = new RegExp(`<\\/\\s*${tag}\\s*>`, "gi");
989
+ closeRe.lastIndex = from;
990
+ const match = closeRe.exec(html);
991
+ if (!match) return null;
992
+ return {
993
+ closeStart: match.index,
994
+ closeEnd: match.index + match[0].length,
995
+ };
996
+ }
997
+
998
+ function parseHtmlElements(html: string): ParsedElement[] {
999
+ const elements: ParsedElement[] = [];
1000
+ const stack: number[] = [];
1001
+ const sameTypeCounts = new Map<string, number>();
1002
+ const tagRe =
1003
+ /<!--[\s\S]*?-->|<![A-Za-z][^>]*>|<\/?\s*([A-Za-z][A-Za-z0-9:-]*)\b/g;
1004
+ let match: RegExpExecArray | null;
1005
+
1006
+ while ((match = tagRe.exec(html))) {
1007
+ const raw =
1008
+ match[0].startsWith("<!--") || match[0].startsWith("<!")
1009
+ ? match[0]
1010
+ : html.slice(match.index, findHtmlTagEnd(html, match.index));
1011
+ tagRe.lastIndex = match.index + raw.length;
1012
+ const tag = match[1]?.toLowerCase();
1013
+ if (!tag || raw.startsWith("<!--") || raw.startsWith("<!")) continue;
1014
+
1015
+ if (raw.startsWith("</")) {
1016
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
1017
+ const element = elements[stack[i]];
1018
+ if (!element) continue;
1019
+ stack.pop();
1020
+ element.closeStart = match.index;
1021
+ element.closeEnd = match.index + raw.length;
1022
+ element.contentEnd = match.index;
1023
+ element.end = match.index + raw.length;
1024
+ if (element.tag === tag) break;
1025
+ }
1026
+ continue;
1027
+ }
1028
+
1029
+ const parentIndex = stack.length > 0 ? stack[stack.length - 1] : undefined;
1030
+ const parentKey = `${parentIndex ?? "root"}:${tag}`;
1031
+ const nthOfType = (sameTypeCounts.get(parentKey) ?? 0) + 1;
1032
+ sameTypeCounts.set(parentKey, nthOfType);
1033
+ const selfClosing = raw.endsWith("/>") || VOID_TAGS.has(tag);
1034
+
1035
+ if (NON_VISUAL_TAGS.has(tag)) {
1036
+ if (!selfClosing) {
1037
+ const close = findClosingTag(html, tag, match.index + raw.length);
1038
+ tagRe.lastIndex = close ? close.closeEnd : html.length;
1039
+ }
1040
+ continue;
1041
+ }
1042
+
1043
+ const index = elements.length;
1044
+ const rawTextClose =
1045
+ !selfClosing && RAW_TEXT_VISUAL_TAGS.has(tag)
1046
+ ? findClosingTag(html, tag, match.index + raw.length)
1047
+ : null;
1048
+ const element: ParsedElement = {
1049
+ index,
1050
+ tag,
1051
+ start: match.index,
1052
+ openEnd: match.index + raw.length,
1053
+ end: rawTextClose
1054
+ ? rawTextClose.closeEnd
1055
+ : selfClosing
1056
+ ? match.index + raw.length
1057
+ : html.length,
1058
+ contentStart: match.index + raw.length,
1059
+ contentEnd: rawTextClose
1060
+ ? rawTextClose.closeStart
1061
+ : selfClosing
1062
+ ? match.index + raw.length
1063
+ : html.length,
1064
+ selfClosing,
1065
+ attributes: parseAttributes(raw, match.index),
1066
+ parentIndex,
1067
+ childIndexes: [],
1068
+ siblingIndex:
1069
+ parentIndex === undefined
1070
+ ? elements.filter((item) => item.parentIndex === undefined).length
1071
+ : (elements[parentIndex]?.childIndexes.length ?? 0),
1072
+ nthOfType,
1073
+ };
1074
+ elements.push(element);
1075
+ if (parentIndex !== undefined) {
1076
+ elements[parentIndex]?.childIndexes.push(index);
1077
+ }
1078
+ if (rawTextClose) {
1079
+ element.closeStart = rawTextClose.closeStart;
1080
+ element.closeEnd = rawTextClose.closeEnd;
1081
+ tagRe.lastIndex = rawTextClose.closeEnd;
1082
+ continue;
1083
+ }
1084
+ if (!selfClosing) stack.push(index);
1085
+ }
1086
+
1087
+ return elements;
1088
+ }
1089
+
1090
+ function candidateDataSelector(
1091
+ element: ParsedElement,
1092
+ ): { selector: string; confidence: number } | null {
1093
+ const data = dataAttributeRecord(element);
1094
+ for (const name of DATA_SELECTOR_PRIORITY) {
1095
+ const value = data[name];
1096
+ if (value) {
1097
+ return {
1098
+ selector: `[${name}="${cssEscape(value)}"]`,
1099
+ confidence: name === "data-code-layer-id" ? 0.95 : 0.86,
1100
+ };
1101
+ }
1102
+ }
1103
+ const [firstName, firstValue] = Object.entries(data)[0] ?? [];
1104
+ if (firstName && firstValue) {
1105
+ return {
1106
+ selector: `[${firstName}="${cssEscape(firstValue)}"]`,
1107
+ confidence: 0.78,
1108
+ };
1109
+ }
1110
+ return null;
1111
+ }
1112
+
1113
+ function selectorPart(element: ParsedElement): string {
1114
+ const dataSelector = candidateDataSelector(element);
1115
+ if (dataSelector) return `${element.tag}${dataSelector.selector}`;
1116
+
1117
+ const id = attributeValue(element, "id");
1118
+ const escapedId = id ? cssIdent(id) : null;
1119
+ if (escapedId) return `#${escapedId}`;
1120
+
1121
+ const safeClasses = classList(element)
1122
+ .map(cssIdent)
1123
+ .filter((value): value is string => Boolean(value))
1124
+ .slice(0, 2);
1125
+ const classes = safeClasses.map((value) => `.${value}`).join("");
1126
+ const nth = element.nthOfType > 1 ? `:nth-of-type(${element.nthOfType})` : "";
1127
+ return `${element.tag}${classes}${nth}`;
1128
+ }
1129
+
1130
+ function pathSelector(
1131
+ element: ParsedElement,
1132
+ elements: ParsedElement[],
1133
+ ): string {
1134
+ const parts: string[] = [];
1135
+ let current: ParsedElement | undefined = element;
1136
+ while (current) {
1137
+ parts.unshift(selectorPart(current));
1138
+ current =
1139
+ current.parentIndex === undefined
1140
+ ? undefined
1141
+ : elements[current.parentIndex];
1142
+ }
1143
+ return parts.slice(-5).join(" > ");
1144
+ }
1145
+
1146
+ function primarySelector(
1147
+ element: ParsedElement,
1148
+ elements: ParsedElement[],
1149
+ ): { selector: string; confidence: number } {
1150
+ const dataSelector = candidateDataSelector(element);
1151
+ if (dataSelector) return dataSelector;
1152
+
1153
+ const id = attributeValue(element, "id");
1154
+ const escapedId = id ? cssIdent(id) : null;
1155
+ if (escapedId) return { selector: `#${escapedId}`, confidence: 0.96 };
1156
+
1157
+ const safeClasses = classList(element)
1158
+ .map(cssIdent)
1159
+ .filter((value): value is string => Boolean(value))
1160
+ .slice(0, 3);
1161
+ if (safeClasses.length > 0) {
1162
+ return {
1163
+ selector: `${element.tag}${safeClasses.map((item) => `.${item}`).join("")}`,
1164
+ confidence: 0.72,
1165
+ };
1166
+ }
1167
+
1168
+ return { selector: pathSelector(element, elements), confidence: 0.58 };
1169
+ }
1170
+
1171
+ function nodeIdFor(
1172
+ element: ParsedElement,
1173
+ elements: ParsedElement[],
1174
+ source: CodeLayerSource,
1175
+ ): string {
1176
+ const sourceKey =
1177
+ source.fileId ??
1178
+ source.filename ??
1179
+ source.path ??
1180
+ source.url ??
1181
+ source.kind;
1182
+ const codeLayerId = stableSourceIdForElement(element);
1183
+ if (codeLayerId) {
1184
+ return `html:${hashStable(`${sourceKey}:data:${codeLayerId}`)}`;
1185
+ }
1186
+ const id = attributeValue(element, "id");
1187
+ if (id) return `html:${hashStable(`${sourceKey}:id:${id}`)}`;
1188
+ const path = pathSelector(element, elements);
1189
+ return `html:${hashStable(`${sourceKey}:${path}:${element.start}`)}`;
1190
+ }
1191
+
1192
+ function stableSourceIdForElement(element: ParsedElement): string | null {
1193
+ for (const attribute of STABLE_NODE_ID_ATTRIBUTES) {
1194
+ const value = attributeValue(element, attribute);
1195
+ if (value) return value;
1196
+ }
1197
+ return null;
1198
+ }
1199
+
1200
+ function styleTokensFor(element: ParsedElement): StyleToken[] {
1201
+ const tokens: StyleToken[] = [];
1202
+ for (const declaration of parseStyleDeclarations(
1203
+ attributeValue(element, "style"),
1204
+ )) {
1205
+ const property = normalizeStyleProperty(declaration.property);
1206
+ if (!property) continue;
1207
+ tokens.push({
1208
+ property,
1209
+ value: declaration.value,
1210
+ token: `${declaration.property}: ${declaration.value}`,
1211
+ source: "inline-style",
1212
+ confidence: 0.95,
1213
+ });
1214
+ }
1215
+
1216
+ for (const token of classList(element)) {
1217
+ const classStyle = classStyleToken(token);
1218
+ if (classStyle) tokens.push(classStyle);
1219
+ }
1220
+
1221
+ return tokens;
1222
+ }
1223
+
1224
+ function classStyleToken(token: string): StyleToken | null {
1225
+ const normalized = token.replace(/^[a-z]+:/, "");
1226
+ if (/^w-/.test(normalized)) {
1227
+ return {
1228
+ property: "width",
1229
+ value: token,
1230
+ token,
1231
+ source: "class",
1232
+ confidence: 0.64,
1233
+ };
1234
+ }
1235
+ if (/^h-/.test(normalized)) {
1236
+ return {
1237
+ property: "height",
1238
+ value: token,
1239
+ token,
1240
+ source: "class",
1241
+ confidence: 0.64,
1242
+ };
1243
+ }
1244
+ if (/^bg-/.test(normalized)) {
1245
+ return {
1246
+ property: "background",
1247
+ value: token,
1248
+ token,
1249
+ source: "class",
1250
+ confidence: 0.6,
1251
+ };
1252
+ }
1253
+ if (/^(p|px|py|pt|pr|pb|pl)-/.test(normalized)) {
1254
+ return {
1255
+ property: "padding",
1256
+ value: token,
1257
+ token,
1258
+ source: "class",
1259
+ confidence: 0.62,
1260
+ };
1261
+ }
1262
+ if (/^gap-/.test(normalized)) {
1263
+ return {
1264
+ property: "gap",
1265
+ value: token,
1266
+ token,
1267
+ source: "class",
1268
+ confidence: 0.62,
1269
+ };
1270
+ }
1271
+ if (
1272
+ [
1273
+ "block",
1274
+ "inline",
1275
+ "inline-block",
1276
+ "flex",
1277
+ "inline-flex",
1278
+ "grid",
1279
+ "inline-grid",
1280
+ "hidden",
1281
+ ].includes(normalized)
1282
+ ) {
1283
+ return {
1284
+ property: "display",
1285
+ value: normalized === "hidden" ? "none" : normalized,
1286
+ token,
1287
+ source: "class",
1288
+ confidence: 0.68,
1289
+ };
1290
+ }
1291
+ if (/^text-/.test(normalized)) {
1292
+ return {
1293
+ property: "color",
1294
+ value: token,
1295
+ token,
1296
+ source: "class",
1297
+ confidence: 0.45,
1298
+ };
1299
+ }
1300
+ return null;
1301
+ }
1302
+
1303
+ function layoutFor(
1304
+ element: ParsedElement,
1305
+ parent: ParsedElement | undefined,
1306
+ ): Omit<LayoutContext, "parentId" | "parentSelector"> {
1307
+ const style = parseStyle(attributeValue(element, "style"));
1308
+ const parentStyle = parent
1309
+ ? parseStyle(attributeValue(parent, "style"))
1310
+ : undefined;
1311
+ const classes = new Set(classList(element));
1312
+ const parentClasses = parent ? new Set(classList(parent)) : undefined;
1313
+ const display =
1314
+ style.display ??
1315
+ (classes.has("flex")
1316
+ ? "flex"
1317
+ : classes.has("grid")
1318
+ ? "grid"
1319
+ : classes.has("hidden")
1320
+ ? "none"
1321
+ : classes.has("block")
1322
+ ? "block"
1323
+ : classes.has("inline-block")
1324
+ ? "inline-block"
1325
+ : undefined);
1326
+ const parentDisplay =
1327
+ parentStyle?.display ??
1328
+ (parentClasses?.has("flex")
1329
+ ? "flex"
1330
+ : parentClasses?.has("grid")
1331
+ ? "grid"
1332
+ : parentClasses?.has("hidden")
1333
+ ? "none"
1334
+ : undefined);
1335
+ const flexDirection =
1336
+ style["flex-direction"] ??
1337
+ (classes.has("flex-col")
1338
+ ? "column"
1339
+ : classes.has("flex-row")
1340
+ ? "row"
1341
+ : undefined);
1342
+ const alignItems =
1343
+ style["align-items"] ??
1344
+ (classes.has("items-start")
1345
+ ? "flex-start"
1346
+ : classes.has("items-center")
1347
+ ? "center"
1348
+ : classes.has("items-end")
1349
+ ? "flex-end"
1350
+ : classes.has("items-stretch")
1351
+ ? "stretch"
1352
+ : classes.has("items-baseline")
1353
+ ? "baseline"
1354
+ : undefined);
1355
+ const justifyContent =
1356
+ style["justify-content"] ??
1357
+ (classes.has("justify-start")
1358
+ ? "flex-start"
1359
+ : classes.has("justify-center")
1360
+ ? "center"
1361
+ : classes.has("justify-end")
1362
+ ? "flex-end"
1363
+ : classes.has("justify-between")
1364
+ ? "space-between"
1365
+ : classes.has("justify-around")
1366
+ ? "space-around"
1367
+ : classes.has("justify-evenly")
1368
+ ? "space-evenly"
1369
+ : undefined);
1370
+ const parentFlexDirection =
1371
+ parentStyle?.["flex-direction"] ??
1372
+ (parentClasses?.has("flex-col")
1373
+ ? "column"
1374
+ : parentClasses?.has("flex-row")
1375
+ ? "row"
1376
+ : undefined);
1377
+
1378
+ return {
1379
+ siblingIndex: element.siblingIndex,
1380
+ nthOfType: element.nthOfType,
1381
+ display,
1382
+ position: style.position,
1383
+ width: style.width,
1384
+ height: style.height,
1385
+ flexDirection,
1386
+ alignItems,
1387
+ justifyContent,
1388
+ gap: style.gap,
1389
+ padding: style.padding,
1390
+ parentDisplay,
1391
+ parentFlexDirection,
1392
+ parentGap: parentStyle?.gap,
1393
+ isFlexContainer: display === "flex" || display === "inline-flex",
1394
+ isGridContainer: display === "grid" || display === "inline-grid",
1395
+ };
1396
+ }
1397
+
1398
+ function textSnippetFor(html: string, element: ParsedElement): string | null {
1399
+ if (element.selfClosing) return null;
1400
+ const inner = html.slice(element.contentStart, element.contentEnd);
1401
+ const text = collapseWhitespace(decodeBasicHtmlEntities(stripTags(inner)));
1402
+ if (!text) return null;
1403
+ return text.length > 160 ? `${text.slice(0, 157)}...` : text;
1404
+ }
1405
+
1406
+ function layerNameFor(
1407
+ html: string,
1408
+ element: ParsedElement,
1409
+ ): {
1410
+ name: string;
1411
+ source: CodeLayerNode["layerNameSource"];
1412
+ attribute?: string;
1413
+ } {
1414
+ const explicit = explicitLayerNameFor(element);
1415
+ if (explicit) return explicit;
1416
+
1417
+ const semantic = semanticLayerNameFor(element);
1418
+ if (semantic) return semantic;
1419
+
1420
+ if (TEXT_LAYER_TAGS.has(element.tag)) {
1421
+ const text = textSnippetFor(html, element);
1422
+ if (text) return { name: truncateLayerName(text), source: "text" };
1423
+ }
1424
+
1425
+ return { name: fallbackTagLayerName(element.tag), source: "tag" };
1426
+ }
1427
+
1428
+ function treeTypeForNode(node: CodeLayerNode): CodeLayerTreeNodeType {
1429
+ if (TEXT_LAYER_TAGS.has(node.tag)) return "text";
1430
+ if (IMAGE_LAYER_TAGS.has(node.tag)) return "image";
1431
+ if (SHAPE_LAYER_TAGS.has(node.tag)) return "shape";
1432
+ if (
1433
+ COMPONENT_LAYER_TAGS.has(node.tag) ||
1434
+ node.classes.some((item) => /component|card|button|control/.test(item))
1435
+ ) {
1436
+ return "component";
1437
+ }
1438
+ if (node.layout.isFlexContainer || node.layout.isGridContainer) {
1439
+ return "frame";
1440
+ }
1441
+ if (node.children.length > 0) return "group";
1442
+ return "element";
1443
+ }
1444
+
1445
+ function capabilitiesFor(element: ParsedElement): EditCapability[] {
1446
+ const capabilities: EditCapability[] = [
1447
+ {
1448
+ kind: "style",
1449
+ properties: [...STYLE_PROPERTIES],
1450
+ confidence: 0.9,
1451
+ },
1452
+ {
1453
+ kind: "class",
1454
+ operations: ["add", "remove", "replace", "set"],
1455
+ confidence: 0.88,
1456
+ },
1457
+ ];
1458
+
1459
+ if (!element.selfClosing) {
1460
+ capabilities.push({
1461
+ kind: "text",
1462
+ operations: ["setTextContent"],
1463
+ confidence: element.childIndexes.length === 0 ? 0.82 : 0.35,
1464
+ reason:
1465
+ element.childIndexes.length === 0
1466
+ ? undefined
1467
+ : "Text edits on mixed-content elements should be escalated.",
1468
+ });
1469
+ }
1470
+
1471
+ return capabilities;
1472
+ }
1473
+
1474
+ function buildProjection(
1475
+ html: string,
1476
+ source: CodeLayerSource,
1477
+ ): ProjectionBuild {
1478
+ const elements = parseHtmlElements(html);
1479
+ const nodeIdByElementIndex = new Map<number, string>();
1480
+ const nodes: CodeLayerNode[] = [];
1481
+ const diagnostics: ProjectionDiagnostic[] = [];
1482
+
1483
+ for (const element of elements) {
1484
+ if (NON_VISUAL_TAGS.has(element.tag)) continue;
1485
+ const nodeId = nodeIdFor(element, elements, source);
1486
+ nodeIdByElementIndex.set(element.index, nodeId);
1487
+ }
1488
+
1489
+ const elementByNodeId = new Map<string, ParsedElement>();
1490
+
1491
+ for (const element of elements) {
1492
+ const nodeId = nodeIdByElementIndex.get(element.index);
1493
+ if (!nodeId) continue;
1494
+
1495
+ const parent =
1496
+ element.parentIndex === undefined
1497
+ ? undefined
1498
+ : elements[element.parentIndex];
1499
+ const parentId =
1500
+ element.parentIndex === undefined
1501
+ ? undefined
1502
+ : nodeIdByElementIndex.get(element.parentIndex);
1503
+ const selector = primarySelector(element, elements);
1504
+ const path = pathSelector(element, elements);
1505
+ const classes = classList(element);
1506
+ const style = parseStyle(attributeValue(element, "style"));
1507
+ const dataAttributes = dataAttributeRecord(element);
1508
+ const layerName = layerNameFor(html, element);
1509
+ const selectors = Array.from(
1510
+ new Set([
1511
+ selector.selector,
1512
+ path,
1513
+ ...Object.entries(dataAttributes).map(
1514
+ ([name, value]) => `[${name}="${cssEscape(value)}"]`,
1515
+ ),
1516
+ ]),
1517
+ );
1518
+
1519
+ nodes.push({
1520
+ id: nodeId,
1521
+ tag: element.tag,
1522
+ layerName: layerName.name,
1523
+ layerNameSource: layerName.source,
1524
+ layerNameAttribute: layerName.attribute,
1525
+ selector: selector.selector,
1526
+ selectors,
1527
+ path,
1528
+ attributes: attributeRecord(element),
1529
+ dataAttributes,
1530
+ classes,
1531
+ textSnippet: textSnippetFor(html, element),
1532
+ style,
1533
+ styleTokens: styleTokensFor(element),
1534
+ parentId,
1535
+ children: element.childIndexes
1536
+ .map((index) => nodeIdByElementIndex.get(index))
1537
+ .filter((id): id is string => Boolean(id)),
1538
+ layout: {
1539
+ parentId,
1540
+ parentSelector: parent
1541
+ ? primarySelector(parent, elements).selector
1542
+ : undefined,
1543
+ ...layoutFor(element, parent),
1544
+ },
1545
+ capabilities: capabilitiesFor(element),
1546
+ confidence: selector.confidence,
1547
+ source: {
1548
+ start: element.start,
1549
+ end: element.end,
1550
+ openStart: element.start,
1551
+ openEnd: element.openEnd,
1552
+ contentStart: element.selfClosing ? undefined : element.contentStart,
1553
+ contentEnd: element.selfClosing ? undefined : element.contentEnd,
1554
+ closeStart: element.closeStart,
1555
+ closeEnd: element.closeEnd,
1556
+ },
1557
+ });
1558
+ elementByNodeId.set(nodeId, element);
1559
+ }
1560
+
1561
+ if (nodes.length === 0 && html.trim()) {
1562
+ diagnostics.push({
1563
+ severity: "warning",
1564
+ code: "no-projectable-elements",
1565
+ message: "No visual HTML elements were found in this source.",
1566
+ });
1567
+ }
1568
+
1569
+ return {
1570
+ projection: {
1571
+ version: 1,
1572
+ projectionId: `clp_${hashStable(`${source.kind}:${source.fileId ?? ""}:${source.filename ?? ""}:${html}`)}`,
1573
+ source,
1574
+ rootNodeIds: nodes
1575
+ .filter((node) => !node.parentId)
1576
+ .map((node) => node.id),
1577
+ nodes,
1578
+ diagnostics,
1579
+ },
1580
+ elementByNodeId,
1581
+ };
1582
+ }
1583
+
1584
+ export function buildCodeLayerProjection(
1585
+ html: string,
1586
+ options: { source?: CodeLayerSource } = {},
1587
+ ): CodeLayerProjection {
1588
+ return buildProjection(html, options.source ?? { kind: "inline-html" })
1589
+ .projection;
1590
+ }
1591
+
1592
+ export function ensureCodeLayerNodeIdsInHtml(
1593
+ html: string,
1594
+ options: { source?: CodeLayerSource } = {},
1595
+ ): { content: string; changed: boolean; stamped: number } {
1596
+ const projection = buildCodeLayerProjection(html, options);
1597
+ const usedIds = new Set<string>();
1598
+ const edits: Array<{ start: number; end: number; value: string }> = [];
1599
+
1600
+ const uniqueValueFor = (base: string) => {
1601
+ let value = base;
1602
+ let suffix = 1;
1603
+ while (usedIds.has(value)) {
1604
+ value = `an-${hashStable(`${base}:${suffix}`)}`;
1605
+ suffix += 1;
1606
+ }
1607
+ usedIds.add(value);
1608
+ return value;
1609
+ };
1610
+
1611
+ for (const node of projection.nodes) {
1612
+ if (
1613
+ !node.source ||
1614
+ node.source.openEnd <= node.source.openStart ||
1615
+ !node.source
1616
+ ) {
1617
+ continue;
1618
+ }
1619
+ const source = node.source;
1620
+ const existing = node.dataAttributes["data-agent-native-node-id"]?.trim();
1621
+ const openTag = html.slice(source.openStart, source.openEnd);
1622
+ const stableIdMatches = Array.from(
1623
+ openTag.matchAll(
1624
+ /\sdata-agent-native-node-id\s*=\s*(?:"[^"]*"|'[^']*'|[^\s/>]+)/gi,
1625
+ ),
1626
+ );
1627
+ const hasSingleCleanStableId =
1628
+ existing && stableIdMatches.length === 1 && !usedIds.has(existing);
1629
+ if (hasSingleCleanStableId) {
1630
+ usedIds.add(existing);
1631
+ continue;
1632
+ }
1633
+
1634
+ const nextValue = uniqueValueFor(
1635
+ existing
1636
+ ? `an-${hashStable(
1637
+ `${existing}:${node.id}:${source.openStart}:${source.openEnd}`,
1638
+ )}`
1639
+ : stableAttributeValueForNode(node),
1640
+ );
1641
+ if (stableIdMatches.length > 0) {
1642
+ const [firstMatch, ...duplicateMatches] = stableIdMatches;
1643
+ if (!firstMatch || firstMatch.index === undefined) continue;
1644
+ edits.push({
1645
+ start: source.openStart + firstMatch.index,
1646
+ end: source.openStart + firstMatch.index + firstMatch[0].length,
1647
+ value: ` data-agent-native-node-id="${escapeHtmlAttribute(nextValue)}"`,
1648
+ });
1649
+ for (const duplicate of duplicateMatches) {
1650
+ if (duplicate.index === undefined) continue;
1651
+ edits.push({
1652
+ start: source.openStart + duplicate.index,
1653
+ end: source.openStart + duplicate.index + duplicate[0].length,
1654
+ value: "",
1655
+ });
1656
+ }
1657
+ continue;
1658
+ }
1659
+
1660
+ const insertAt = source.openEnd - (openTag.endsWith("/>") ? 2 : 1);
1661
+ if (insertAt <= 0 || insertAt > html.length) continue;
1662
+ edits.push({
1663
+ start: insertAt,
1664
+ end: insertAt,
1665
+ value: ` data-agent-native-node-id="${escapeHtmlAttribute(nextValue)}"`,
1666
+ });
1667
+ }
1668
+
1669
+ const orderedEdits = edits.sort((a, b) => b.start - a.start);
1670
+
1671
+ if (orderedEdits.length === 0) {
1672
+ return { content: html, changed: false, stamped: 0 };
1673
+ }
1674
+
1675
+ let content = html;
1676
+ for (const edit of orderedEdits) {
1677
+ content = `${content.slice(0, edit.start)}${edit.value}${content.slice(edit.end)}`;
1678
+ }
1679
+ return { content, changed: true, stamped: orderedEdits.length };
1680
+ }
1681
+
1682
+ export function removeCodeLayerNodeFromHtml(
1683
+ html: string,
1684
+ node: CodeLayerNode,
1685
+ ): string | null {
1686
+ if (!node.source) return null;
1687
+ if (node.tag === "html" || node.tag === "body") return null;
1688
+ const start = node.source.start;
1689
+ const end = node.source.end;
1690
+ if (start < 0 || end <= start || end > html.length) return null;
1691
+ return `${html.slice(0, start)}${html.slice(end)}`;
1692
+ }
1693
+
1694
+ export function buildCodeLayerTree(
1695
+ projection: CodeLayerProjection,
1696
+ ): CodeLayerTreeNode[] {
1697
+ const nodesById = new Map(projection.nodes.map((node) => [node.id, node]));
1698
+ const treeById = new Map<string, CodeLayerTreeNode>();
1699
+
1700
+ for (const node of projection.nodes) {
1701
+ treeById.set(node.id, {
1702
+ id: node.id,
1703
+ name: node.layerName,
1704
+ type: treeTypeForNode(node),
1705
+ tag: node.tag,
1706
+ selector: node.selector,
1707
+ detail: `<${node.tag}>`,
1708
+ layout: {
1709
+ display: node.layout.display,
1710
+ flexDirection: node.layout.flexDirection,
1711
+ alignItems: node.layout.alignItems,
1712
+ justifyContent: node.layout.justifyContent,
1713
+ isFlexContainer: node.layout.isFlexContainer,
1714
+ isGridContainer: node.layout.isGridContainer,
1715
+ },
1716
+ badge:
1717
+ node.layerNameSource === "attribute" && node.layerNameAttribute
1718
+ ? node.layerNameAttribute
1719
+ : undefined,
1720
+ // Safe rename persistence belongs in the caller's edit action. The
1721
+ // preferred write target is data-agent-native-layer-name; projection is
1722
+ // intentionally read-only and never mutates source by itself.
1723
+ renamable: node.source != null,
1724
+ children: [],
1725
+ });
1726
+ }
1727
+
1728
+ for (const node of projection.nodes) {
1729
+ const parent =
1730
+ node.parentId && nodesById.has(node.parentId)
1731
+ ? treeById.get(node.parentId)
1732
+ : undefined;
1733
+ const treeNode = treeById.get(node.id);
1734
+ if (parent && treeNode) parent.children.push(treeNode);
1735
+ }
1736
+
1737
+ const roots = projection.rootNodeIds
1738
+ .map((id) => treeById.get(id))
1739
+ .filter((node): node is CodeLayerTreeNode => Boolean(node));
1740
+ const rootIds = new Set(roots.map((node) => node.id));
1741
+ for (const node of projection.nodes) {
1742
+ if (!node.parentId && !rootIds.has(node.id)) {
1743
+ const treeNode = treeById.get(node.id);
1744
+ if (treeNode) roots.push(treeNode);
1745
+ }
1746
+ }
1747
+ return roots;
1748
+ }
1749
+
1750
+ function normalizeSelectorForMatch(selector: string): string {
1751
+ return selector
1752
+ .trim()
1753
+ .replace(/\s*>\s*/g, " > ")
1754
+ .replace(/\s+/g, " ");
1755
+ }
1756
+
1757
+ function lastSelectorPart(selector: string): string {
1758
+ const parts = normalizeSelectorForMatch(selector).split(" > ");
1759
+ return parts[parts.length - 1] ?? selector;
1760
+ }
1761
+
1762
+ function simpleSelectorMatches(node: CodeLayerNode, selector: string): boolean {
1763
+ if (!selector) return false;
1764
+ if (selector.startsWith("#")) {
1765
+ return node.attributes.id === selector.slice(1);
1766
+ }
1767
+ const tagIdMatch = selector.match(/^([A-Za-z][A-Za-z0-9:-]*)#(.+)$/);
1768
+ if (tagIdMatch?.[1]) {
1769
+ return (
1770
+ node.tag === tagIdMatch[1].toLowerCase() &&
1771
+ node.attributes.id === tagIdMatch[2]
1772
+ );
1773
+ }
1774
+ if (selector.startsWith(".")) {
1775
+ const required = selector
1776
+ .split(".")
1777
+ .map((item) => item.trim())
1778
+ .filter(Boolean);
1779
+ return required.every((item) => node.classes.includes(item));
1780
+ }
1781
+ const dataMatch = selector.match(
1782
+ /^(?:([A-Za-z][A-Za-z0-9:-]*)?)?\[([A-Za-z_][A-Za-z0-9_:.-]*)=(?:"([^"]*)"|'([^']*)')\]$/,
1783
+ );
1784
+ if (dataMatch?.[2]) {
1785
+ const tag = dataMatch[1]?.toLowerCase();
1786
+ const attribute = dataMatch[2].toLowerCase();
1787
+ const expected = dataMatch[3] ?? dataMatch[4] ?? "";
1788
+ const actual = attribute.startsWith("data-")
1789
+ ? node.dataAttributes[attribute]
1790
+ : node.attributes[attribute];
1791
+ return (!tag || node.tag === tag) && actual === expected;
1792
+ }
1793
+ const tagClassMatch = selector.match(
1794
+ /^([A-Za-z][A-Za-z0-9:-]*)(\.[A-Za-z0-9_-]+)+$/,
1795
+ );
1796
+ if (tagClassMatch?.[1]) {
1797
+ const tag = tagClassMatch[1].toLowerCase();
1798
+ const required = selector.slice(tag.length).split(".").filter(Boolean);
1799
+ return (
1800
+ node.tag === tag && required.every((item) => node.classes.includes(item))
1801
+ );
1802
+ }
1803
+ const nthMatch = selector.match(
1804
+ /^([A-Za-z][A-Za-z0-9:-]*)(?::nth-of-type\((\d+)\))$/,
1805
+ );
1806
+ if (nthMatch?.[1]) {
1807
+ return (
1808
+ node.tag === nthMatch[1].toLowerCase() &&
1809
+ lastSelectorPart(node.path).endsWith(`:nth-of-type(${nthMatch[2]})`)
1810
+ );
1811
+ }
1812
+ return node.tag === selector.toLowerCase();
1813
+ }
1814
+
1815
+ function unescapeCssAttributeValue(value: string): string {
1816
+ return value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1817
+ }
1818
+
1819
+ function simpleSelectorMatchesElement(
1820
+ element: ParsedElement,
1821
+ selector: string,
1822
+ ): boolean {
1823
+ let remaining = selector.trim();
1824
+ if (!remaining) return false;
1825
+
1826
+ const nthMatch = remaining.match(/:nth-of-type\((\d+)\)$/);
1827
+ if (nthMatch?.[1]) {
1828
+ if (element.nthOfType !== Number(nthMatch[1])) return false;
1829
+ remaining = remaining.slice(0, nthMatch.index).trim();
1830
+ }
1831
+
1832
+ const tagMatch = remaining.match(/^([A-Za-z][A-Za-z0-9:-]*)/);
1833
+ if (tagMatch?.[1] && element.tag !== tagMatch[1].toLowerCase()) {
1834
+ return false;
1835
+ }
1836
+
1837
+ const idMatch = remaining.match(/#([A-Za-z_][A-Za-z0-9_-]*)/);
1838
+ if (idMatch?.[1] && attributeValue(element, "id") !== idMatch[1]) {
1839
+ return false;
1840
+ }
1841
+
1842
+ const attributes = Array.from(
1843
+ remaining.matchAll(
1844
+ /\[([A-Za-z_][A-Za-z0-9_:.-]*)=(?:"((?:\\"|[^"])*)"|'((?:\\'|[^'])*)')\]/g,
1845
+ ),
1846
+ );
1847
+ for (const match of attributes) {
1848
+ const name = match[1];
1849
+ if (!name) return false;
1850
+ const expected = unescapeCssAttributeValue(match[2] ?? match[3] ?? "");
1851
+ if (attributeValue(element, name) !== expected) return false;
1852
+ }
1853
+
1854
+ const classes = classList(element);
1855
+ const requiredClasses = Array.from(
1856
+ remaining.matchAll(/\.([A-Za-z_][A-Za-z0-9_-]*)/g),
1857
+ (match) => match[1],
1858
+ ).filter((value): value is string => Boolean(value));
1859
+ if (requiredClasses.some((className) => !classes.includes(className))) {
1860
+ return false;
1861
+ }
1862
+
1863
+ return Boolean(
1864
+ tagMatch ||
1865
+ idMatch ||
1866
+ attributes.length > 0 ||
1867
+ requiredClasses.length > 0 ||
1868
+ nthMatch,
1869
+ );
1870
+ }
1871
+
1872
+ function selectorPathMatchesElement(
1873
+ element: ParsedElement | undefined,
1874
+ selector: string,
1875
+ elementByIndex: Map<number, ParsedElement>,
1876
+ ): boolean {
1877
+ const parts = normalizeSelectorForMatch(selector)
1878
+ .split(" > ")
1879
+ .map((part) => part.trim())
1880
+ .filter(Boolean);
1881
+ if (parts.length <= 1) return false;
1882
+
1883
+ let current: ParsedElement | undefined = element;
1884
+ for (let index = parts.length - 1; index >= 0; index -= 1) {
1885
+ const selectorPart = parts[index];
1886
+ if (!current || !selectorPart) return false;
1887
+ if (!simpleSelectorMatchesElement(current, selectorPart)) return false;
1888
+ current =
1889
+ current.parentIndex === undefined
1890
+ ? undefined
1891
+ : elementByIndex.get(current.parentIndex);
1892
+ }
1893
+ return true;
1894
+ }
1895
+
1896
+ function nodeMatchesStableSourceId(
1897
+ node: CodeLayerNode,
1898
+ sourceId: string,
1899
+ ): boolean {
1900
+ if (!sourceId) return false;
1901
+ if (node.id === sourceId) return true;
1902
+ for (const attribute of STABLE_NODE_ID_ATTRIBUTES) {
1903
+ if (node.dataAttributes[attribute] === sourceId) return true;
1904
+ }
1905
+ return node.attributes.id === sourceId;
1906
+ }
1907
+
1908
+ function selectorMatches(
1909
+ node: CodeLayerNode,
1910
+ selector: string,
1911
+ element: ParsedElement | undefined,
1912
+ elementByIndex: Map<number, ParsedElement>,
1913
+ ): boolean {
1914
+ const normalizedSelector = normalizeSelectorForMatch(selector);
1915
+ const normalizedNodeSelectors = [
1916
+ node.selector,
1917
+ node.path,
1918
+ ...node.selectors,
1919
+ ].map(normalizeSelectorForMatch);
1920
+ if (normalizedNodeSelectors.includes(normalizedSelector)) return true;
1921
+ const selectorHasDirectPath = normalizedSelector.includes(" > ");
1922
+ if (
1923
+ selectorHasDirectPath &&
1924
+ normalizedNodeSelectors.some(
1925
+ (candidate) =>
1926
+ candidate.endsWith(` > ${normalizedSelector}`) ||
1927
+ (candidate.includes(" > ") &&
1928
+ normalizedSelector.endsWith(` > ${candidate}`)),
1929
+ )
1930
+ ) {
1931
+ return true;
1932
+ }
1933
+ if (
1934
+ selectorHasDirectPath &&
1935
+ selectorPathMatchesElement(element, normalizedSelector, elementByIndex)
1936
+ ) {
1937
+ return true;
1938
+ }
1939
+ if (selectorHasDirectPath) return false;
1940
+ if (simpleSelectorMatches(node, normalizedSelector)) return true;
1941
+ const lastPart = lastSelectorPart(normalizedSelector);
1942
+ return (
1943
+ lastPart !== normalizedSelector && simpleSelectorMatches(node, lastPart)
1944
+ );
1945
+ }
1946
+
1947
+ function resolveTarget(
1948
+ build: ProjectionBuild,
1949
+ target: EditIntentTarget,
1950
+ ): EditIntentResolution {
1951
+ const { projection, elementByNodeId } = build;
1952
+ const elementByIndex = new Map(
1953
+ Array.from(elementByNodeId.values()).map((element) => [
1954
+ element.index,
1955
+ element,
1956
+ ]),
1957
+ );
1958
+ if (target.nodeId) {
1959
+ const matches = projection.nodes.filter((candidate) =>
1960
+ nodeMatchesStableSourceId(candidate, target.nodeId ?? ""),
1961
+ );
1962
+ if (matches.length === 1 && matches[0]) {
1963
+ return { status: "resolved", node: matches[0] };
1964
+ }
1965
+ if (matches.length > 1) {
1966
+ return {
1967
+ status: "conflict",
1968
+ message: `Node id "${target.nodeId}" matched ${matches.length} code layer nodes.`,
1969
+ };
1970
+ }
1971
+ if (!target.selector) {
1972
+ return {
1973
+ status: "conflict",
1974
+ message: `No code layer node exists for nodeId "${target.nodeId}".`,
1975
+ };
1976
+ }
1977
+ }
1978
+
1979
+ if (!target.selector) {
1980
+ return {
1981
+ status: "conflict",
1982
+ message:
1983
+ "Edit intent must include either target.nodeId or target.selector.",
1984
+ };
1985
+ }
1986
+
1987
+ const matches = projection.nodes.filter((node) =>
1988
+ selectorMatches(
1989
+ node,
1990
+ target.selector ?? "",
1991
+ elementByNodeId.get(node.id),
1992
+ elementByIndex,
1993
+ ),
1994
+ );
1995
+ if (matches.length === 1 && matches[0]) {
1996
+ return { status: "resolved", node: matches[0] };
1997
+ }
1998
+ if (matches.length > 1) {
1999
+ return {
2000
+ status: "conflict",
2001
+ message: `Selector "${target.selector}" matched ${matches.length} code layer nodes.`,
2002
+ };
2003
+ }
2004
+ return {
2005
+ status: "conflict",
2006
+ message: `Selector "${target.selector}" did not match a code layer node.`,
2007
+ };
2008
+ }
2009
+
2010
+ function summarizeNode(node: CodeLayerNode): PatchNodeSummary {
2011
+ return {
2012
+ nodeId: node.id,
2013
+ selector: node.selector,
2014
+ tag: node.tag,
2015
+ classes: [...node.classes],
2016
+ style: { ...node.style },
2017
+ textSnippet: node.textSnippet,
2018
+ };
2019
+ }
2020
+
2021
+ function patchResult(
2022
+ status: PatchResultStatus,
2023
+ source: CodeLayerSource,
2024
+ intent: EditIntent,
2025
+ changed: boolean,
2026
+ message: string,
2027
+ node?: CodeLayerNode,
2028
+ capability?: EditCapability,
2029
+ before?: PatchNodeSummary,
2030
+ after?: PatchNodeSummary,
2031
+ ): PatchResult {
2032
+ return {
2033
+ status,
2034
+ source,
2035
+ intent,
2036
+ target: node
2037
+ ? { nodeId: node.id, selector: node.selector, tag: node.tag }
2038
+ : undefined,
2039
+ capability,
2040
+ before,
2041
+ after,
2042
+ changed,
2043
+ message,
2044
+ };
2045
+ }
2046
+
2047
+ function replaceOrInsertAttribute(
2048
+ html: string,
2049
+ element: ParsedElement,
2050
+ name: string,
2051
+ value: string,
2052
+ ): string {
2053
+ const escaped = escapeHtmlAttribute(value);
2054
+ const existing = getAttribute(element, name);
2055
+ if (existing) {
2056
+ return `${html.slice(0, existing.start)}${existing.name}="${escaped}"${html.slice(existing.end)}`;
2057
+ }
2058
+
2059
+ const rawOpen = html.slice(element.start, element.openEnd);
2060
+ const closeIndex = element.openEnd - 1;
2061
+ const slashIndex = rawOpen.trimEnd().endsWith("/>")
2062
+ ? html.lastIndexOf("/", closeIndex)
2063
+ : -1;
2064
+ const insertAt = slashIndex > element.start ? slashIndex : closeIndex;
2065
+ return `${html.slice(0, insertAt)} ${name}="${escaped}"${html.slice(insertAt)}`;
2066
+ }
2067
+
2068
+ function setStyleValue(
2069
+ currentStyle: string | null,
2070
+ property: VisualStyleProperty,
2071
+ value: string,
2072
+ ): string {
2073
+ const declarations = parseStyleDeclarations(currentStyle);
2074
+ const existing = declarations.find((item) => item.property === property);
2075
+ if (existing) {
2076
+ existing.value = value;
2077
+ } else {
2078
+ declarations.push({ property, value });
2079
+ }
2080
+ return serializeStyleDeclarations(declarations);
2081
+ }
2082
+
2083
+ function applyStyleEdit(
2084
+ html: string,
2085
+ element: ParsedElement,
2086
+ intent: StyleEditIntent,
2087
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
2088
+ const property = normalizeStyleProperty(intent.property);
2089
+ if (!property || !isSafeStyleValue(property, intent.value))
2090
+ return "unsupported";
2091
+ const nextStyle = setStyleValue(
2092
+ attributeValue(element, "style"),
2093
+ property,
2094
+ intent.value.trim(),
2095
+ );
2096
+ return {
2097
+ content: replaceOrInsertAttribute(html, element, "style", nextStyle),
2098
+ capability: {
2099
+ kind: "style",
2100
+ properties: [property],
2101
+ confidence: 0.9,
2102
+ },
2103
+ };
2104
+ }
2105
+
2106
+ function applyClassEdit(
2107
+ html: string,
2108
+ element: ParsedElement,
2109
+ intent: ClassEditIntent,
2110
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
2111
+ const classes = classList(element);
2112
+ let nextClasses = [...classes];
2113
+
2114
+ if (intent.operation === "add") {
2115
+ const additions = classTokensFromIntent(intent);
2116
+ if (
2117
+ additions.length === 0 ||
2118
+ additions.some((token) => !isSafeClassToken(token))
2119
+ ) {
2120
+ return "unsupported";
2121
+ }
2122
+ nextClasses = Array.from(new Set([...classes, ...additions]));
2123
+ } else if (intent.operation === "remove") {
2124
+ const removals = classTokensFromIntent(intent);
2125
+ if (
2126
+ removals.length === 0 ||
2127
+ removals.some((token) => !isSafeClassToken(token))
2128
+ ) {
2129
+ return "unsupported";
2130
+ }
2131
+ nextClasses = classes.filter((token) => !removals.includes(token));
2132
+ } else if (intent.operation === "replace") {
2133
+ if (
2134
+ !intent.from ||
2135
+ !intent.to ||
2136
+ !isSafeClassToken(intent.from) ||
2137
+ !isSafeClassToken(intent.to)
2138
+ ) {
2139
+ return "unsupported";
2140
+ }
2141
+ if (!classes.includes(intent.from)) return "conflict";
2142
+ nextClasses = classes.map((token) =>
2143
+ token === intent.from ? (intent.to ?? token) : token,
2144
+ );
2145
+ } else {
2146
+ const replacement = classTokensFromIntent(intent);
2147
+ if (
2148
+ replacement.length === 0 ||
2149
+ replacement.some((token) => !isSafeClassToken(token))
2150
+ ) {
2151
+ return "unsupported";
2152
+ }
2153
+ nextClasses = replacement;
2154
+ }
2155
+
2156
+ return {
2157
+ content: replaceOrInsertAttribute(
2158
+ html,
2159
+ element,
2160
+ "class",
2161
+ nextClasses.join(" "),
2162
+ ),
2163
+ capability: {
2164
+ kind: "class",
2165
+ operations: [intent.operation],
2166
+ confidence: 0.88,
2167
+ },
2168
+ };
2169
+ }
2170
+
2171
+ function sanitizeTextEditHtml(html: string): string {
2172
+ return html
2173
+ .replace(
2174
+ /<\s*(script|style|iframe|object|embed|link|meta|base)\b[\s\S]*?<\s*\/\s*\1\s*>/gi,
2175
+ "",
2176
+ )
2177
+ .replace(
2178
+ /<\s*(script|style|iframe|object|embed|link|meta|base)\b[^>]*\/?\s*>/gi,
2179
+ "",
2180
+ )
2181
+ .replace(/\s+on[A-Za-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/g, "")
2182
+ .replace(
2183
+ /\s+(href|src|xlink:href)\s*=\s*(["'])\s*javascript:[\s\S]*?\2/gi,
2184
+ "",
2185
+ );
2186
+ }
2187
+
2188
+ function applyTextEdit(
2189
+ html: string,
2190
+ element: ParsedElement,
2191
+ intent: TextEditIntent,
2192
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
2193
+ if (element.selfClosing || element.contentStart > element.contentEnd) {
2194
+ return "unsupported";
2195
+ }
2196
+ if (element.childIndexes.length > 0 && intent.html === undefined) {
2197
+ return "needsAgent";
2198
+ }
2199
+ const replacement =
2200
+ intent.html !== undefined
2201
+ ? sanitizeTextEditHtml(intent.html)
2202
+ : escapeHtmlText(intent.value);
2203
+ return {
2204
+ content: `${html.slice(0, element.contentStart)}${replacement}${html.slice(element.contentEnd)}`,
2205
+ capability: {
2206
+ kind: "text",
2207
+ operations: ["setTextContent"],
2208
+ confidence: 0.82,
2209
+ },
2210
+ };
2211
+ }
2212
+
2213
+ function applyMoveNodeEdit(
2214
+ html: string,
2215
+ element: ParsedElement,
2216
+ anchor: ParsedElement,
2217
+ intent: MoveNodeEditIntent,
2218
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
2219
+ if (element.index === anchor.index) return "conflict";
2220
+ if (anchor.start >= element.start && anchor.end <= element.end) {
2221
+ return "conflict";
2222
+ }
2223
+ if (intent.placement === "inside" && anchor.selfClosing) {
2224
+ return "unsupported";
2225
+ }
2226
+
2227
+ const fragment = html.slice(element.start, element.end);
2228
+ const withoutTarget = `${html.slice(0, element.start)}${html.slice(
2229
+ element.end,
2230
+ )}`;
2231
+ const removedLength = element.end - element.start;
2232
+ const rawInsertAt =
2233
+ intent.placement === "before"
2234
+ ? anchor.start
2235
+ : intent.placement === "after"
2236
+ ? anchor.end
2237
+ : anchor.contentEnd;
2238
+ const insertAt =
2239
+ element.start < rawInsertAt ? rawInsertAt - removedLength : rawInsertAt;
2240
+
2241
+ if (insertAt < 0 || insertAt > withoutTarget.length) return "conflict";
2242
+
2243
+ return {
2244
+ content: `${withoutTarget.slice(0, insertAt)}${fragment}${withoutTarget.slice(insertAt)}`,
2245
+ capability: {
2246
+ kind: "structure",
2247
+ operations: ["moveNode"],
2248
+ confidence: 0.78,
2249
+ },
2250
+ };
2251
+ }
2252
+
2253
+ function findAfterNode(
2254
+ projection: CodeLayerProjection,
2255
+ before: CodeLayerNode,
2256
+ ): CodeLayerNode | undefined {
2257
+ return (
2258
+ projection.nodes.find((node) => node.id === before.id) ??
2259
+ projection.nodes.find(
2260
+ (node) =>
2261
+ node.tag === before.tag &&
2262
+ node.source?.openStart === before.source?.openStart,
2263
+ )
2264
+ );
2265
+ }
2266
+
2267
+ export function applyVisualEdit(
2268
+ html: string,
2269
+ intent: EditIntent,
2270
+ options: { source?: CodeLayerSource } = {},
2271
+ ): ApplyVisualEditResult {
2272
+ const source = options.source ?? { kind: "inline-html" };
2273
+ if (source.kind !== "inline-html" && source.kind !== "design-file") {
2274
+ const projection = buildCodeLayerProjection(html, { source });
2275
+ return {
2276
+ content: html,
2277
+ projection,
2278
+ result: patchResult(
2279
+ "unsupported",
2280
+ source,
2281
+ intent,
2282
+ false,
2283
+ `Source kind "${source.kind}" is not supported by the deterministic HTML editor yet.`,
2284
+ ),
2285
+ };
2286
+ }
2287
+
2288
+ const initial = buildProjection(html, source);
2289
+ const resolution = resolveTarget(initial, intent.target);
2290
+ if (resolution.status !== "resolved" || !resolution.node) {
2291
+ return {
2292
+ content: html,
2293
+ projection: initial.projection,
2294
+ result: patchResult(
2295
+ "conflict",
2296
+ source,
2297
+ intent,
2298
+ false,
2299
+ resolution.message ?? "Could not resolve the edit target.",
2300
+ ),
2301
+ };
2302
+ }
2303
+
2304
+ const beforeNode = resolution.node;
2305
+ const before = summarizeNode(beforeNode);
2306
+ const element = initial.elementByNodeId.get(beforeNode.id);
2307
+ if (!element || !beforeNode.source) {
2308
+ return {
2309
+ content: html,
2310
+ projection: initial.projection,
2311
+ result: patchResult(
2312
+ "needsAgent",
2313
+ source,
2314
+ intent,
2315
+ false,
2316
+ "The target node does not have editable source spans.",
2317
+ beforeNode,
2318
+ undefined,
2319
+ before,
2320
+ ),
2321
+ };
2322
+ }
2323
+
2324
+ let edit: { content: string; capability: EditCapability } | PatchResultStatus;
2325
+ if (intent.kind === "style") {
2326
+ edit = applyStyleEdit(html, element, intent);
2327
+ } else if (intent.kind === "class") {
2328
+ edit = applyClassEdit(html, element, intent);
2329
+ } else if (intent.kind === "textContent") {
2330
+ edit = applyTextEdit(html, element, intent);
2331
+ } else {
2332
+ const anchorResolution = resolveTarget(initial, intent.anchor);
2333
+ if (anchorResolution.status !== "resolved" || !anchorResolution.node) {
2334
+ return {
2335
+ content: html,
2336
+ projection: initial.projection,
2337
+ result: patchResult(
2338
+ "conflict",
2339
+ source,
2340
+ intent,
2341
+ false,
2342
+ anchorResolution.message ?? "Could not resolve the move anchor.",
2343
+ beforeNode,
2344
+ undefined,
2345
+ before,
2346
+ ),
2347
+ };
2348
+ }
2349
+ const anchorElement = initial.elementByNodeId.get(anchorResolution.node.id);
2350
+ if (!anchorElement || !anchorResolution.node.source) {
2351
+ return {
2352
+ content: html,
2353
+ projection: initial.projection,
2354
+ result: patchResult(
2355
+ "needsAgent",
2356
+ source,
2357
+ intent,
2358
+ false,
2359
+ "The move anchor does not have editable source spans.",
2360
+ beforeNode,
2361
+ undefined,
2362
+ before,
2363
+ ),
2364
+ };
2365
+ }
2366
+ edit = applyMoveNodeEdit(html, element, anchorElement, intent);
2367
+ }
2368
+
2369
+ if (typeof edit === "string") {
2370
+ const status = edit;
2371
+ return {
2372
+ content: html,
2373
+ projection: initial.projection,
2374
+ result: patchResult(
2375
+ status,
2376
+ source,
2377
+ intent,
2378
+ false,
2379
+ status === "conflict"
2380
+ ? "The requested edit conflicts with the current source."
2381
+ : status === "needsAgent"
2382
+ ? "The requested edit needs agent-level source rewriting."
2383
+ : "The requested edit is not supported by the deterministic editor.",
2384
+ beforeNode,
2385
+ undefined,
2386
+ before,
2387
+ ),
2388
+ };
2389
+ }
2390
+
2391
+ const nextProjection = buildCodeLayerProjection(edit.content, { source });
2392
+ const afterNode = findAfterNode(nextProjection, beforeNode);
2393
+ const after = afterNode ? summarizeNode(afterNode) : undefined;
2394
+
2395
+ return {
2396
+ content: edit.content,
2397
+ projection: nextProjection,
2398
+ result: patchResult(
2399
+ "applied",
2400
+ source,
2401
+ intent,
2402
+ edit.content !== html,
2403
+ edit.content === html
2404
+ ? "No source change was needed."
2405
+ : "Visual edit applied.",
2406
+ beforeNode,
2407
+ edit.capability,
2408
+ before,
2409
+ after,
2410
+ ),
2411
+ };
2412
+ }