@agent-native/core 0.80.8 → 0.80.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +6 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +8 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +10 -2
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +10 -2
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +7 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +7 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +7 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +9 -2
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +6 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +6 -0
  13. package/corpus/core/docs/content/template-design.mdx +9 -2
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/cli/design-connect.ts +28 -2
  16. package/corpus/core/src/cli/mcp.ts +10 -0
  17. package/corpus/core/src/cli/skills.ts +103 -36
  18. package/corpus/core/src/client/error-format.ts +25 -0
  19. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
  20. package/corpus/core/src/client/guided-questions.tsx +32 -4
  21. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
  22. package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
  23. package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
  24. package/corpus/core/src/client/session-replay.ts +34 -0
  25. package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
  26. package/corpus/core/src/coding-tools/index.ts +1 -1
  27. package/corpus/core/src/mcp/index.ts +2 -0
  28. package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
  29. package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
  30. package/corpus/core/src/mcp-client/index.ts +9 -0
  31. package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
  32. package/corpus/core/src/onboarding/types.ts +7 -0
  33. package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
  34. package/corpus/core/src/server/agents-bundle.ts +35 -7
  35. package/corpus/core/src/styles/agent-native.css +5 -0
  36. package/corpus/core/src/templates/default/app/global.css +35 -35
  37. package/corpus/templates/analytics/AGENTS.md +6 -0
  38. package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
  39. package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
  40. package/corpus/templates/analytics/app/global.css +144 -40
  41. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
  45. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
  46. package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  48. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
  49. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
  50. package/corpus/templates/assets/app/global.css +99 -35
  51. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  52. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
  53. package/corpus/templates/assets/app/routes/library.tsx +4 -4
  54. package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
  55. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  56. package/corpus/templates/brain/app/global.css +63 -35
  57. package/corpus/templates/brain/app/routes/search.tsx +1 -1
  58. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  59. package/corpus/templates/brain/app/routes/sources.tsx +2 -2
  60. package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
  61. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
  62. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
  63. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
  64. package/corpus/templates/calendar/app/global.css +69 -35
  65. package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  66. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  67. package/corpus/templates/chat/app/global.css +35 -35
  68. package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  69. package/corpus/templates/clips/AGENTS.md +13 -0
  70. package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
  71. package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
  72. package/corpus/templates/clips/actions/view-screen.ts +2 -2
  73. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  74. package/corpus/templates/clips/app/global.css +47 -35
  75. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
  76. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
  77. package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  78. package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
  79. package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
  80. package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
  81. package/corpus/templates/clips/desktop/src/app.tsx +290 -1
  82. package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
  83. package/corpus/templates/clips/desktop/src/styles.css +67 -52
  84. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
  87. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
  88. package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
  89. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
  90. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  91. package/corpus/templates/content/app/global.css +29 -15
  92. package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  93. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
  94. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  95. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
  96. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
  97. package/corpus/templates/design/AGENTS.md +20 -12
  98. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
  99. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
  100. package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
  101. package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
  102. package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
  103. package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
  104. package/corpus/templates/design/actions/apply-design-state.ts +200 -0
  105. package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
  106. package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
  107. package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
  108. package/corpus/templates/design/actions/apply-visual-edit.ts +214 -8
  109. package/corpus/templates/design/actions/capture-design-state.ts +224 -0
  110. package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
  111. package/corpus/templates/design/actions/create-component.ts +447 -0
  112. package/corpus/templates/design/actions/create-design-branch.ts +263 -0
  113. package/corpus/templates/design/actions/create-design-state.ts +162 -0
  114. package/corpus/templates/design/actions/delete-design-state.ts +55 -0
  115. package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
  116. package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
  117. package/corpus/templates/design/actions/generate-design.ts +6 -21
  118. package/corpus/templates/design/actions/generate-screens.ts +43 -2
  119. package/corpus/templates/design/actions/get-component-details.ts +242 -0
  120. package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
  121. package/corpus/templates/design/actions/get-design-review.ts +314 -0
  122. package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
  123. package/corpus/templates/design/actions/get-design.ts +1 -0
  124. package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
  125. package/corpus/templates/design/actions/index-components.ts +258 -0
  126. package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
  127. package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
  128. package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
  129. package/corpus/templates/design/actions/list-design-states.ts +72 -0
  130. package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
  131. package/corpus/templates/design/actions/open-component-source.ts +237 -0
  132. package/corpus/templates/design/actions/present-design-variants.ts +355 -30
  133. package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
  134. package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
  135. package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
  136. package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
  137. package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
  138. package/corpus/templates/design/actions/run-design-audit.ts +421 -0
  139. package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
  140. package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
  141. package/corpus/templates/design/actions/view-screen.ts +75 -31
  142. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1738 -135
  143. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
  144. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
  145. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
  146. package/corpus/templates/design/app/components/design/EditPanel.tsx +1873 -132
  147. package/corpus/templates/design/app/components/design/LayersPanel.tsx +384 -219
  148. package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
  149. package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
  150. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2103 -295
  151. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +83 -206
  152. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
  153. package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
  154. package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
  155. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +1 -1
  156. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
  157. package/corpus/templates/design/app/components/design/index.ts +16 -1
  158. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +238 -137
  159. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +188 -14
  160. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +24 -5
  161. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +57 -1
  162. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
  163. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
  164. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
  165. package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
  166. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +12 -5
  167. package/corpus/templates/design/app/components/design/types.ts +9 -0
  168. package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
  169. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
  170. package/corpus/templates/design/app/global.css +77 -49
  171. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  172. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
  173. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +11 -2
  174. package/corpus/templates/design/app/i18n/zh-TW.ts +34 -0
  175. package/corpus/templates/design/app/i18n-data.ts +314 -3
  176. package/corpus/templates/design/app/pages/DesignEditor.tsx +4929 -892
  177. package/corpus/templates/design/app/root.tsx +1 -1
  178. package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
  179. package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
  180. package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
  181. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
  182. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
  183. package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
  184. package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
  185. package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
  186. package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
  187. package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
  188. package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
  189. package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
  190. package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
  191. package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
  192. package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
  193. package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
  194. package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
  195. package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
  196. package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
  197. package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
  198. package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
  199. package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
  200. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
  201. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
  202. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
  203. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
  204. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
  205. package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
  206. package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
  207. package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
  208. package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
  209. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
  210. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
  211. package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
  212. package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
  213. package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
  214. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
  215. package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
  216. package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
  217. package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
  218. package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
  219. package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
  220. package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
  221. package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
  222. package/corpus/templates/design/e2e/README.md +3 -3
  223. package/corpus/templates/design/e2e/global-setup.ts +11 -22
  224. package/corpus/templates/design/e2e/helpers.ts +128 -11
  225. package/corpus/templates/design/playwright.config.ts +9 -3
  226. package/corpus/templates/design/server/db/schema.ts +123 -0
  227. package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
  228. package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
  229. package/corpus/templates/design/server/plugins/db.ts +90 -0
  230. package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
  231. package/corpus/templates/design/shared/builder-app.ts +297 -0
  232. package/corpus/templates/design/shared/capability-resolver.ts +123 -0
  233. package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
  234. package/corpus/templates/design/shared/code-layer.ts +1141 -93
  235. package/corpus/templates/design/shared/component-model.ts +239 -0
  236. package/corpus/templates/design/shared/design-review.ts +275 -0
  237. package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
  238. package/corpus/templates/design/shared/design-state.ts +112 -0
  239. package/corpus/templates/design/shared/design-surface-index.ts +258 -0
  240. package/corpus/templates/design/shared/generation-session.ts +41 -0
  241. package/corpus/templates/design/shared/motion-compiler.ts +278 -0
  242. package/corpus/templates/design/shared/motion-timeline.ts +193 -0
  243. package/corpus/templates/design/shared/responsive-classes.ts +452 -0
  244. package/corpus/templates/design/shared/shader-fill.ts +323 -0
  245. package/corpus/templates/design/shared/source-mode.ts +245 -0
  246. package/corpus/templates/dispatch/app/global.css +79 -35
  247. package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
  248. package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  249. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
  250. package/corpus/templates/forms/app/global.css +64 -35
  251. package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
  252. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
  253. package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
  254. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
  255. package/corpus/templates/macros/app/global.css +25 -9
  256. package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  257. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
  258. package/corpus/templates/mail/app/global.css +42 -36
  259. package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
  260. package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
  261. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
  262. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
  263. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
  264. package/corpus/templates/plan/app/global.css +74 -42
  265. package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
  266. package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
  267. package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
  268. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
  269. package/corpus/templates/slides/app/global.css +49 -33
  270. package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  271. package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
  272. package/corpus/templates/videos/app/global.css +35 -35
  273. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  274. package/dist/cli/design-connect.d.ts.map +1 -1
  275. package/dist/cli/design-connect.js +28 -2
  276. package/dist/cli/design-connect.js.map +1 -1
  277. package/dist/cli/mcp.d.ts.map +1 -1
  278. package/dist/cli/mcp.js +10 -0
  279. package/dist/cli/mcp.js.map +1 -1
  280. package/dist/cli/skills.d.ts.map +1 -1
  281. package/dist/cli/skills.js +95 -33
  282. package/dist/cli/skills.js.map +1 -1
  283. package/dist/client/error-format.d.ts.map +1 -1
  284. package/dist/client/error-format.js +17 -0
  285. package/dist/client/error-format.js.map +1 -1
  286. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  287. package/dist/client/extensions/ExtensionViewer.js +3 -4
  288. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  289. package/dist/client/guided-questions.d.ts +4 -0
  290. package/dist/client/guided-questions.d.ts.map +1 -1
  291. package/dist/client/guided-questions.js +16 -7
  292. package/dist/client/guided-questions.js.map +1 -1
  293. package/dist/client/onboarding/OnboardingPanel.js +26 -1
  294. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  295. package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
  296. package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
  297. package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
  298. package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
  299. package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
  300. package/dist/client/resources/use-builtin-capabilities.js +2 -1
  301. package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
  302. package/dist/client/session-replay.d.ts.map +1 -1
  303. package/dist/client/session-replay.js +31 -0
  304. package/dist/client/session-replay.js.map +1 -1
  305. package/dist/client/sharing/ShareButton.d.ts +14 -0
  306. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  307. package/dist/client/sharing/ShareButton.js +36 -7
  308. package/dist/client/sharing/ShareButton.js.map +1 -1
  309. package/dist/coding-tools/index.js +1 -1
  310. package/dist/coding-tools/index.js.map +1 -1
  311. package/dist/mcp/index.d.ts +2 -0
  312. package/dist/mcp/index.d.ts.map +1 -1
  313. package/dist/mcp/index.js +1 -0
  314. package/dist/mcp/index.js.map +1 -1
  315. package/dist/mcp/screen-memory-stdio.d.ts +6 -0
  316. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
  317. package/dist/mcp/screen-memory-stdio.js +213 -0
  318. package/dist/mcp/screen-memory-stdio.js.map +1 -0
  319. package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
  320. package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
  321. package/dist/mcp-client/builtin-capabilities.js +10 -0
  322. package/dist/mcp-client/builtin-capabilities.js.map +1 -1
  323. package/dist/mcp-client/index.d.ts +1 -0
  324. package/dist/mcp-client/index.d.ts.map +1 -1
  325. package/dist/mcp-client/index.js +1 -0
  326. package/dist/mcp-client/index.js.map +1 -1
  327. package/dist/mcp-client/screen-memory-local.d.ts +61 -0
  328. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
  329. package/dist/mcp-client/screen-memory-local.js +336 -0
  330. package/dist/mcp-client/screen-memory-local.js.map +1 -0
  331. package/dist/observability/routes.d.ts +3 -3
  332. package/dist/onboarding/types.d.ts +7 -0
  333. package/dist/onboarding/types.d.ts.map +1 -1
  334. package/dist/onboarding/types.js.map +1 -1
  335. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  336. package/dist/server/agent-chat-plugin.js +26 -0
  337. package/dist/server/agent-chat-plugin.js.map +1 -1
  338. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  339. package/dist/server/agents-bundle.d.ts.map +1 -1
  340. package/dist/server/agents-bundle.js +22 -7
  341. package/dist/server/agents-bundle.js.map +1 -1
  342. package/dist/styles/agent-native.css +5 -0
  343. package/dist/templates/default/app/global.css +35 -35
  344. package/docs/content/locales/ar-SA/template-design.mdx +6 -0
  345. package/docs/content/locales/de-DE/template-design.mdx +8 -0
  346. package/docs/content/locales/es-ES/template-design.mdx +10 -2
  347. package/docs/content/locales/fr-FR/template-design.mdx +10 -2
  348. package/docs/content/locales/hi-IN/template-design.mdx +7 -0
  349. package/docs/content/locales/ja-JP/template-design.mdx +7 -0
  350. package/docs/content/locales/ko-KR/template-design.mdx +7 -0
  351. package/docs/content/locales/pt-BR/template-design.mdx +9 -2
  352. package/docs/content/locales/zh-CN/template-design.mdx +6 -0
  353. package/docs/content/locales/zh-TW/template-design.mdx +6 -0
  354. package/docs/content/template-design.mdx +9 -2
  355. package/package.json +1 -1
  356. package/src/templates/default/app/global.css +35 -35
  357. package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
  358. package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
  359. package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -342
@@ -1,3 +1,15 @@
1
+ import {
2
+ isComponentInstance,
3
+ instanceFromNode,
4
+ type ComponentInstance,
5
+ } from "./component-model";
6
+ import type { TailwindBreakpointPrefix } from "./design-state.js";
7
+ import {
8
+ parseClassGroups,
9
+ parseClassToken,
10
+ setPropertyClass,
11
+ removePropertyClass,
12
+ } from "./responsive-classes.js";
1
13
  import type { DesignSourceType } from "./source-mode";
2
14
 
3
15
  export type CodeLayerSourceKind =
@@ -137,10 +149,32 @@ export type VisualStyleProperty =
137
149
 
138
150
  export interface StyleToken {
139
151
  property: VisualStyleProperty;
152
+ /**
153
+ * The resolved value at the *base* breakpoint (unprefixed), or the inline
154
+ * style value. For class-sourced tokens this is the utility string of the
155
+ * base class (e.g. `"text-sm"`). Use `breakpointValues` to inspect how the
156
+ * value differs across responsive prefixes.
157
+ */
140
158
  value: string;
141
159
  token: string;
142
160
  source: "inline-style" | "class";
143
161
  confidence: number;
162
+ /**
163
+ * For class-sourced tokens only: the resolved utility string per responsive
164
+ * prefix. Only prefixes that have an explicit class token are included.
165
+ *
166
+ * @example
167
+ * // className="text-sm md:text-base lg:text-lg"
168
+ * // styleToken for "color" property would have:
169
+ * // breakpointValues = { base: "text-sm", md: "text-base", lg: "text-lg" }
170
+ */
171
+ breakpointValues?: Partial<Record<TailwindBreakpointPrefix, string>>;
172
+ /**
173
+ * For class-sourced tokens only: the prefixes (other than `"base"`) at which
174
+ * this property has a responsive override in the class list. Populated when
175
+ * `breakpointValues` has keys other than `"base"`.
176
+ */
177
+ overriddenAtPrefixes?: TailwindBreakpointPrefix[];
144
178
  }
145
179
 
146
180
  export interface LayoutContext {
@@ -177,6 +211,28 @@ export type EditCapability =
177
211
  confidence: number;
178
212
  reason?: string;
179
213
  }
214
+ | {
215
+ /**
216
+ * Responsive-class editing — adds, replaces, or removes a Tailwind
217
+ * utility at a specific breakpoint prefix without touching other
218
+ * breakpoints. Uses the helpers in `responsive-classes.ts`
219
+ * (setPropertyClass / removePropertyClass) and the same deterministic
220
+ * HTML-patch path as `class` edits.
221
+ *
222
+ * The `prefix` field indicates the active breakpoint scope for which
223
+ * this capability was computed (derived from the canvas frame width via
224
+ * `widthToPrefix`). Callers may target any prefix — `prefix` is
225
+ * informational, not a constraint.
226
+ */
227
+ kind: "responsive-class";
228
+ /** Active breakpoint scope (informational). */
229
+ prefix: TailwindBreakpointPrefix;
230
+ operations: Array<"add" | "remove" | "replace">;
231
+ /** Properties that currently have per-breakpoint overrides (non-empty means overrides exist). */
232
+ overriddenProperties: string[];
233
+ confidence: number;
234
+ reason?: string;
235
+ }
180
236
  | {
181
237
  kind: "text";
182
238
  operations: Array<"setTextContent">;
@@ -211,6 +267,15 @@ export interface CodeLayerNode {
211
267
  capabilities: EditCapability[];
212
268
  confidence: number;
213
269
  source: CodeLayerSourceSpan | null;
270
+ /**
271
+ * Present when the node is the root of a component instance — i.e. it
272
+ * carries a `data-agent-native-component` attribute (Alpine-annotated or
273
+ * build-time-instrumented). The canvas uses this to draw the component
274
+ * outline and the inspector uses it to surface component-level controls.
275
+ *
276
+ * `undefined` when the node is not a component root.
277
+ */
278
+ componentInstance?: ComponentInstance;
214
279
  }
215
280
 
216
281
  export interface ProjectionDiagnostic {
@@ -324,11 +389,91 @@ export interface MoveNodeEditIntent {
324
389
  placement: "before" | "after" | "inside";
325
390
  }
326
391
 
392
+ /**
393
+ * GROUP: wrap sibling nodes sharing a parent inside a new <div> wrapper.
394
+ * The wrapper is inserted at the position of the first target; targets are
395
+ * reparented into it in source order. The new wrapper gets a fresh
396
+ * data-agent-native-node-id and data-agent-native-layer-name="Group".
397
+ *
398
+ * When autoLayout is true the wrapper also receives
399
+ * `display:flex; flex-direction:column; gap:8px` and
400
+ * position/left/top/right/bottom are stripped from each wrapped child.
401
+ *
402
+ * Returns "unsupported" if the targets don't share a common parent.
403
+ */
404
+ export interface WrapNodesEditIntent {
405
+ kind: "wrapNodes";
406
+ targetIds: string[];
407
+ autoLayout?: boolean;
408
+ }
409
+
410
+ /**
411
+ * UNGROUP: replace the wrapper node with its children, spliced into the
412
+ * wrapper's parent at the wrapper's position, then remove the wrapper.
413
+ */
414
+ export interface UnwrapEditIntent {
415
+ kind: "unwrap";
416
+ targetId: string;
417
+ }
418
+
419
+ /**
420
+ * CONVERT an existing container to/from auto-layout.
421
+ * When enabled, sets display:flex (+ flex-direction, gap) on the target and
422
+ * strips position:absolute/left/top/right/bottom from its DIRECT children.
423
+ * When !enabled, sets display:block (turns auto-layout off).
424
+ */
425
+ export interface AutoLayoutEditIntent {
426
+ kind: "autoLayout";
427
+ targetId: string;
428
+ enabled: boolean;
429
+ direction?: "row" | "column";
430
+ gap?: string;
431
+ }
432
+
433
+ /**
434
+ * Responsive-class edit intent — adds, replaces, or removes a single Tailwind
435
+ * utility at the given `prefix` (breakpoint scope) without touching classes at
436
+ * other breakpoints.
437
+ *
438
+ * Examples:
439
+ * - Add `text-base` at `md:` on a node that already has `text-sm` base:
440
+ * `{ kind: "responsive-class", target, prefix: "md", operation: "add", utility: "text-base" }`
441
+ *
442
+ * - Replace whatever `text-*` class currently lives at `lg:` with `text-xl`:
443
+ * `{ kind: "responsive-class", target, prefix: "lg", operation: "replace", utility: "text-xl" }`
444
+ *
445
+ * - Remove the `md:` override for the `text` stem, falling back to the base:
446
+ * `{ kind: "responsive-class", target, prefix: "md", operation: "remove", stem: "text" }`
447
+ *
448
+ * `utility` should be the bare utility without its prefix (e.g. `"text-lg"`,
449
+ * not `"md:text-lg"`). The prefix is applied automatically.
450
+ * `stem` is required only for `"remove"` operations.
451
+ */
452
+ export interface ResponsiveClassEditIntent {
453
+ kind: "responsive-class";
454
+ target: EditIntentTarget;
455
+ /** Target breakpoint prefix. Use `"base"` to edit the unprefixed class. */
456
+ prefix: TailwindBreakpointPrefix;
457
+ operation: "add" | "remove" | "replace";
458
+ /** The bare utility to add or replace (without prefix). Required for add/replace. */
459
+ utility?: string;
460
+ /**
461
+ * The CSS-property stem to remove (e.g. `"text"`, `"bg"`, `"p"`).
462
+ * Required for `"remove"` operations; ignored for add/replace (the stem is
463
+ * derived from `utility` instead).
464
+ */
465
+ stem?: string;
466
+ }
467
+
327
468
  export type EditIntent =
328
469
  | StyleEditIntent
329
470
  | ClassEditIntent
330
471
  | TextEditIntent
331
- | MoveNodeEditIntent;
472
+ | MoveNodeEditIntent
473
+ | WrapNodesEditIntent
474
+ | UnwrapEditIntent
475
+ | AutoLayoutEditIntent
476
+ | ResponsiveClassEditIntent;
332
477
 
333
478
  export interface EditIntentResolution {
334
479
  status: "resolved" | "conflict" | "unsupported";
@@ -372,6 +517,8 @@ export interface PatchResult {
372
517
  after?: PatchNodeSummary;
373
518
  changed: boolean;
374
519
  message?: string;
520
+ /** For wrapNodes: the data-agent-native-node-id of the newly created wrapper. */
521
+ wrapperNodeId?: string;
375
522
  }
376
523
 
377
524
  export interface ApplyVisualEditResult {
@@ -1234,6 +1381,8 @@ function stableSourceIdForElement(element: ParsedElement): string | null {
1234
1381
 
1235
1382
  function styleTokensFor(element: ParsedElement): StyleToken[] {
1236
1383
  const tokens: StyleToken[] = [];
1384
+
1385
+ // --- Inline styles (no breakpoint concept) ---
1237
1386
  for (const declaration of parseStyleDeclarations(
1238
1387
  attributeValue(element, "style"),
1239
1388
  )) {
@@ -1248,61 +1397,104 @@ function styleTokensFor(element: ParsedElement): StyleToken[] {
1248
1397
  });
1249
1398
  }
1250
1399
 
1251
- for (const token of classList(element)) {
1252
- const classStyle = classStyleToken(token);
1253
- if (classStyle) tokens.push(classStyle);
1254
- }
1400
+ // --- Class tokens responsive-aware ---
1401
+ // Group all class tokens by breakpoint prefix so we can build per-property
1402
+ // breakpointValues maps and detect overrides.
1403
+ const classValue = attributeValue(element, "class") ?? "";
1404
+ const groups = parseClassGroups(classValue);
1255
1405
 
1256
- return tokens;
1257
- }
1406
+ // For each property, collect the utility value at every prefix that has one.
1407
+ // We key by property so we emit one token per property, not one per class.
1408
+ const propertyMap = new Map<
1409
+ VisualStyleProperty,
1410
+ {
1411
+ property: VisualStyleProperty;
1412
+ confidence: number;
1413
+ breakpointValues: Partial<Record<TailwindBreakpointPrefix, string>>;
1414
+ /** The raw token for the base occurrence (for backward-compat `token` field). */
1415
+ baseToken: string;
1416
+ }
1417
+ >();
1418
+
1419
+ const allPrefixes: ReadonlyArray<TailwindBreakpointPrefix> = [
1420
+ "base",
1421
+ "sm",
1422
+ "md",
1423
+ "lg",
1424
+ "xl",
1425
+ "2xl",
1426
+ ];
1258
1427
 
1259
- function classStyleToken(token: string): StyleToken | null {
1260
- const normalized = token.replace(/^[a-z]+:/, "");
1261
- if (/^w-/.test(normalized)) {
1262
- return {
1263
- property: "width",
1264
- value: token,
1265
- token,
1266
- source: "class",
1267
- confidence: 0.64,
1268
- };
1269
- }
1270
- if (/^h-/.test(normalized)) {
1271
- return {
1272
- property: "height",
1273
- value: token,
1274
- token,
1275
- source: "class",
1276
- confidence: 0.64,
1277
- };
1278
- }
1279
- if (/^bg-/.test(normalized)) {
1280
- return {
1281
- property: "background",
1282
- value: token,
1283
- token,
1284
- source: "class",
1285
- confidence: 0.6,
1286
- };
1287
- }
1288
- if (/^(p|px|py|pt|pr|pb|pl)-/.test(normalized)) {
1289
- return {
1290
- property: "padding",
1291
- value: token,
1292
- token,
1293
- source: "class",
1294
- confidence: 0.62,
1295
- };
1428
+ for (const prefix of allPrefixes) {
1429
+ for (const rawToken of groups[prefix]) {
1430
+ const parsed = parseClassToken(rawToken);
1431
+ const mapped = utilityToStyleProperty(parsed.utility);
1432
+ if (!mapped) continue;
1433
+ const { property, confidence } = mapped;
1434
+
1435
+ // Resolve the display value the same way classStyleToken does.
1436
+ const resolvedValue =
1437
+ property === "display"
1438
+ ? parsed.utility === "hidden"
1439
+ ? "none"
1440
+ : parsed.utility
1441
+ : parsed.utility; // utility without prefix, e.g. "text-sm"
1442
+
1443
+ const existing = propertyMap.get(property);
1444
+ if (existing) {
1445
+ existing.breakpointValues[prefix] = resolvedValue;
1446
+ if (existing.confidence < confidence) existing.confidence = confidence;
1447
+ } else {
1448
+ propertyMap.set(property, {
1449
+ property,
1450
+ confidence,
1451
+ breakpointValues: { [prefix]: resolvedValue },
1452
+ baseToken: rawToken,
1453
+ });
1454
+ }
1455
+ }
1296
1456
  }
1297
- if (/^gap-/.test(normalized)) {
1298
- return {
1299
- property: "gap",
1300
- value: token,
1301
- token,
1457
+
1458
+ for (const entry of propertyMap.values()) {
1459
+ const baseValue = entry.breakpointValues["base"] ?? "";
1460
+ // The full original token for the base occurrence (for backward compat).
1461
+ const rawBaseToken = entry.baseToken;
1462
+ const overriddenAt = Object.keys(entry.breakpointValues).filter(
1463
+ (p) => p !== "base",
1464
+ ) as TailwindBreakpointPrefix[];
1465
+
1466
+ tokens.push({
1467
+ property: entry.property,
1468
+ // `value` = the base utility string (backward-compatible).
1469
+ value: baseValue || rawBaseToken,
1470
+ token: rawBaseToken,
1302
1471
  source: "class",
1303
- confidence: 0.62,
1304
- };
1472
+ confidence: entry.confidence,
1473
+ breakpointValues: { ...entry.breakpointValues },
1474
+ overriddenAtPrefixes: overriddenAt.length > 0 ? overriddenAt : undefined,
1475
+ });
1305
1476
  }
1477
+
1478
+ return tokens;
1479
+ }
1480
+
1481
+ /**
1482
+ * Map a bare Tailwind utility (without any responsive prefix, e.g. `"text-sm"`
1483
+ * not `"md:text-sm"`) to the `VisualStyleProperty` it most likely controls.
1484
+ * Returns `null` when the utility is not recognised.
1485
+ *
1486
+ * This is called by both the legacy `classStyleToken` path and the new
1487
+ * responsive-aware `styleTokensFor` implementation.
1488
+ */
1489
+ function utilityToStyleProperty(
1490
+ utility: string,
1491
+ ): { property: VisualStyleProperty; confidence: number } | null {
1492
+ if (/^w-/.test(utility)) return { property: "width", confidence: 0.64 };
1493
+ if (/^h-/.test(utility)) return { property: "height", confidence: 0.64 };
1494
+ if (/^bg-/.test(utility)) return { property: "background", confidence: 0.6 };
1495
+ if (/^(p|px|py|pt|pr|pb|pl)-/.test(utility))
1496
+ return { property: "padding", confidence: 0.62 };
1497
+ if (/^gap-/.test(utility)) return { property: "gap", confidence: 0.62 };
1306
1498
  if (
1307
1499
  [
1308
1500
  "block",
@@ -1313,28 +1505,26 @@ function classStyleToken(token: string): StyleToken | null {
1313
1505
  "grid",
1314
1506
  "inline-grid",
1315
1507
  "hidden",
1316
- ].includes(normalized)
1508
+ ].includes(utility)
1317
1509
  ) {
1318
- return {
1319
- property: "display",
1320
- value: normalized === "hidden" ? "none" : normalized,
1321
- token,
1322
- source: "class",
1323
- confidence: 0.68,
1324
- };
1325
- }
1326
- if (/^text-/.test(normalized)) {
1327
- return {
1328
- property: "color",
1329
- value: token,
1330
- token,
1331
- source: "class",
1332
- confidence: 0.45,
1333
- };
1510
+ return { property: "display", confidence: 0.68 };
1334
1511
  }
1512
+ if (/^text-/.test(utility)) return { property: "color", confidence: 0.45 };
1335
1513
  return null;
1336
1514
  }
1337
1515
 
1516
+ function classStyleToken(token: string): StyleToken | null {
1517
+ const { utility } = parseClassToken(token);
1518
+ const mapped = utilityToStyleProperty(utility);
1519
+ if (!mapped) return null;
1520
+ const { property, confidence } = mapped;
1521
+ // The legacy value field preserves the original full token (including prefix)
1522
+ // for backward compatibility. The responsive path uses `breakpointValues`.
1523
+ const value =
1524
+ property === "display" ? (utility === "hidden" ? "none" : utility) : token;
1525
+ return { property, value, token, source: "class", confidence };
1526
+ }
1527
+
1338
1528
  function layoutFor(
1339
1529
  element: ParsedElement,
1340
1530
  parent: ParsedElement | undefined,
@@ -1472,6 +1662,9 @@ function treeTypeForNode(node: CodeLayerNode): CodeLayerTreeNodeType {
1472
1662
  if (TEXT_LAYER_TAGS.has(node.tag)) return "text";
1473
1663
  if (IMAGE_LAYER_TAGS.has(node.tag)) return "image";
1474
1664
  if (SHAPE_LAYER_TAGS.has(node.tag)) return "shape";
1665
+ // A node annotated as a component instance is always classified as "component"
1666
+ // regardless of its tag — this is the canonical detection path.
1667
+ if (node.componentInstance) return "component";
1475
1668
  if (
1476
1669
  COMPONENT_LAYER_TAGS.has(node.tag) ||
1477
1670
  node.classes.some((item) => /component|card|button|control/.test(item))
@@ -1485,6 +1678,49 @@ function treeTypeForNode(node: CodeLayerNode): CodeLayerTreeNodeType {
1485
1678
  return "element";
1486
1679
  }
1487
1680
 
1681
+ function isCollapsibleDocumentShellNode(
1682
+ node: CodeLayerTreeNode,
1683
+ nodesById: Map<string, CodeLayerNode>,
1684
+ ): boolean {
1685
+ if (node.tag !== "html" && node.tag !== "body") return false;
1686
+ return nodesById.get(node.id)?.layerNameSource === "tag";
1687
+ }
1688
+
1689
+ function compactCodeLayerTreeNodes(
1690
+ nodes: CodeLayerTreeNode[],
1691
+ nodesById: Map<string, CodeLayerNode>,
1692
+ ancestors: Set<string> = new Set(),
1693
+ ): CodeLayerTreeNode[] {
1694
+ const compacted: CodeLayerTreeNode[] = [];
1695
+ const siblingIds = new Set<string>();
1696
+
1697
+ for (const node of nodes) {
1698
+ if (ancestors.has(node.id)) continue;
1699
+
1700
+ const nextAncestors = new Set(ancestors);
1701
+ nextAncestors.add(node.id);
1702
+ const children = compactCodeLayerTreeNodes(
1703
+ node.children,
1704
+ nodesById,
1705
+ nextAncestors,
1706
+ );
1707
+ const compactedNode: CodeLayerTreeNode = { ...node, children };
1708
+ const promotedNodes =
1709
+ isCollapsibleDocumentShellNode(compactedNode, nodesById) &&
1710
+ children.length > 0
1711
+ ? children
1712
+ : [compactedNode];
1713
+
1714
+ for (const promotedNode of promotedNodes) {
1715
+ if (siblingIds.has(promotedNode.id)) continue;
1716
+ siblingIds.add(promotedNode.id);
1717
+ compacted.push(promotedNode);
1718
+ }
1719
+ }
1720
+
1721
+ return compacted;
1722
+ }
1723
+
1488
1724
  function capabilitiesFor(element: ParsedElement): EditCapability[] {
1489
1725
  const capabilities: EditCapability[] = [
1490
1726
  {
@@ -1499,6 +1735,39 @@ function capabilitiesFor(element: ParsedElement): EditCapability[] {
1499
1735
  },
1500
1736
  ];
1501
1737
 
1738
+ // Responsive-class capability — detect which properties already carry
1739
+ // breakpoint overrides so the inspector can surface override indicators.
1740
+ const classValue = attributeValue(element, "class") ?? "";
1741
+ const groups = parseClassGroups(classValue);
1742
+ const overriddenProps: string[] = [];
1743
+ const responsivePrefixes: ReadonlyArray<TailwindBreakpointPrefix> = [
1744
+ "sm",
1745
+ "md",
1746
+ "lg",
1747
+ "xl",
1748
+ "2xl",
1749
+ ];
1750
+ for (const prefix of responsivePrefixes) {
1751
+ for (const rawToken of groups[prefix]) {
1752
+ const { utility } = parseClassToken(rawToken);
1753
+ // Derive a property stem to use as the override indicator label.
1754
+ const stemPart = utility.split("-")[0];
1755
+ if (stemPart && !overriddenProps.includes(stemPart)) {
1756
+ overriddenProps.push(stemPart);
1757
+ }
1758
+ }
1759
+ }
1760
+ // The prefix here is "base" as the default; callers that know the active
1761
+ // frame width should use widthToPrefix() from responsive-classes.ts to
1762
+ // determine the appropriate editing prefix.
1763
+ capabilities.push({
1764
+ kind: "responsive-class",
1765
+ prefix: "base",
1766
+ operations: ["add", "remove", "replace"],
1767
+ overriddenProperties: overriddenProps,
1768
+ confidence: 0.87,
1769
+ });
1770
+
1502
1771
  if (!element.selfClosing) {
1503
1772
  capabilities.push({
1504
1773
  kind: "text",
@@ -1518,6 +1787,9 @@ function buildProjection(
1518
1787
  html: string,
1519
1788
  source: CodeLayerSource,
1520
1789
  ): ProjectionBuild {
1790
+ // Tolerate non-string input from any caller (e.g. content not yet loaded):
1791
+ // an empty projection is correct; crashing the editor is not.
1792
+ if (typeof html !== "string") html = "";
1521
1793
  const elements = parseHtmlElements(html);
1522
1794
  const nodeIdByElementIndex = new Map<number, string>();
1523
1795
  const nodes: CodeLayerNode[] = [];
@@ -1559,7 +1831,7 @@ function buildProjection(
1559
1831
  ]),
1560
1832
  );
1561
1833
 
1562
- nodes.push({
1834
+ const node: CodeLayerNode = {
1563
1835
  id: nodeId,
1564
1836
  tag: element.tag,
1565
1837
  layerName: layerName.name,
@@ -1597,7 +1869,17 @@ function buildProjection(
1597
1869
  closeStart: element.closeStart,
1598
1870
  closeEnd: element.closeEnd,
1599
1871
  },
1600
- });
1872
+ };
1873
+
1874
+ // Detect component instances — nodes that carry data-agent-native-component.
1875
+ // Populate the metadata so the canvas can outline component roots and the
1876
+ // inspector can surface component-level controls.
1877
+ if (isComponentInstance(node)) {
1878
+ const instance = instanceFromNode(node);
1879
+ if (instance) node.componentInstance = instance;
1880
+ }
1881
+
1882
+ nodes.push(node);
1601
1883
  elementByNodeId.set(nodeId, element);
1602
1884
  }
1603
1885
 
@@ -1628,7 +1910,11 @@ export function buildCodeLayerProjection(
1628
1910
  html: string,
1629
1911
  options: { source?: CodeLayerSource } = {},
1630
1912
  ): CodeLayerProjection {
1631
- return buildProjection(html, options.source ?? { kind: "inline-html" })
1913
+ // Defensive: callers (memos/effects) may project before content has loaded
1914
+ // (e.g. `activeContent` is briefly undefined on first render). Projecting a
1915
+ // non-string must yield an empty projection, never crash the editor.
1916
+ const safeHtml = typeof html === "string" ? html : "";
1917
+ return buildProjection(safeHtml, options.source ?? { kind: "inline-html" })
1632
1918
  .projection;
1633
1919
  }
1634
1920
 
@@ -1768,26 +2054,36 @@ export function buildCodeLayerTree(
1768
2054
  });
1769
2055
  }
1770
2056
 
2057
+ const childIdsByParentId = new Map<string, Set<string>>();
1771
2058
  for (const node of projection.nodes) {
1772
2059
  const parent =
1773
2060
  node.parentId && nodesById.has(node.parentId)
1774
2061
  ? treeById.get(node.parentId)
1775
2062
  : undefined;
1776
2063
  const treeNode = treeById.get(node.id);
1777
- if (parent && treeNode) parent.children.push(treeNode);
1778
- }
2064
+ if (!parent || !treeNode || parent.id === treeNode.id) continue;
2065
+ const childIds = childIdsByParentId.get(parent.id) ?? new Set<string>();
2066
+ if (childIds.has(treeNode.id)) continue;
2067
+ childIds.add(treeNode.id);
2068
+ childIdsByParentId.set(parent.id, childIds);
2069
+ parent.children.push(treeNode);
2070
+ }
2071
+
2072
+ const roots: CodeLayerTreeNode[] = [];
2073
+ const rootIds = new Set<string>();
2074
+ const appendRoot = (id: string) => {
2075
+ if (rootIds.has(id)) return;
2076
+ const treeNode = treeById.get(id);
2077
+ if (!treeNode) return;
2078
+ rootIds.add(id);
2079
+ roots.push(treeNode);
2080
+ };
1779
2081
 
1780
- const roots = projection.rootNodeIds
1781
- .map((id) => treeById.get(id))
1782
- .filter((node): node is CodeLayerTreeNode => Boolean(node));
1783
- const rootIds = new Set(roots.map((node) => node.id));
2082
+ projection.rootNodeIds.forEach(appendRoot);
1784
2083
  for (const node of projection.nodes) {
1785
- if (!node.parentId && !rootIds.has(node.id)) {
1786
- const treeNode = treeById.get(node.id);
1787
- if (treeNode) roots.push(treeNode);
1788
- }
2084
+ if (!node.parentId) appendRoot(node.id);
1789
2085
  }
1790
- return roots;
2086
+ return compactCodeLayerTreeNodes(roots, nodesById);
1791
2087
  }
1792
2088
 
1793
2089
  function normalizeSelectorForMatch(selector: string): string {
@@ -1797,6 +2093,26 @@ function normalizeSelectorForMatch(selector: string): string {
1797
2093
  .replace(/\s+/g, " ");
1798
2094
  }
1799
2095
 
2096
+ function selectorPartTag(selectorPart: string): string | null {
2097
+ const match = selectorPart.trim().match(/^([A-Za-z][A-Za-z0-9:-]*)/);
2098
+ return match?.[1]?.toLowerCase() ?? null;
2099
+ }
2100
+
2101
+ function isDocumentRootSelectorPart(selectorPart: string): boolean {
2102
+ const tag = selectorPartTag(selectorPart);
2103
+ return tag === "html" || tag === "body";
2104
+ }
2105
+
2106
+ // Removes positional `:nth-of-type(n)` suffixes from a selector. Runtime
2107
+ // bridge selectors fall back to `:nth-of-type` for elements without a durable
2108
+ // id, and that position is computed against the live (possibly Alpine-mutated)
2109
+ // DOM. When the stored source order differs, the positional index no longer
2110
+ // lines up. Dropping the suffix lets resolution fall back to the element's
2111
+ // stable signal (tag, classes, attributes, ancestor path).
2112
+ function stripPositionalNthOfType(selector: string): string {
2113
+ return selector.replace(/:nth-of-type\(\d+\)/g, "");
2114
+ }
2115
+
1800
2116
  function lastSelectorPart(selector: string): string {
1801
2117
  const parts = normalizeSelectorForMatch(selector).split(" > ");
1802
2118
  return parts[parts.length - 1] ?? selector;
@@ -1926,7 +2242,11 @@ function selectorPathMatchesElement(
1926
2242
  let current: ParsedElement | undefined = element;
1927
2243
  for (let index = parts.length - 1; index >= 0; index -= 1) {
1928
2244
  const selectorPart = parts[index];
1929
- if (!current || !selectorPart) return false;
2245
+ if (!selectorPart) return false;
2246
+ if (!current) {
2247
+ if (isDocumentRootSelectorPart(selectorPart)) continue;
2248
+ return false;
2249
+ }
1930
2250
  if (!simpleSelectorMatchesElement(current, selectorPart)) return false;
1931
2251
  current =
1932
2252
  current.parentIndex === undefined
@@ -2024,26 +2344,52 @@ function resolveTarget(
2024
2344
  };
2025
2345
  }
2026
2346
 
2027
- const matches = projection.nodes.filter((node) =>
2028
- selectorMatches(
2029
- node,
2030
- target.selector ?? "",
2031
- elementByNodeId.get(node.id),
2032
- elementByIndex,
2033
- ),
2034
- );
2347
+ const selectorValue = target.selector ?? "";
2348
+ const matchesForSelector = (value: string): CodeLayerNode[] =>
2349
+ projection.nodes.filter((node) =>
2350
+ selectorMatches(
2351
+ node,
2352
+ value,
2353
+ elementByNodeId.get(node.id),
2354
+ elementByIndex,
2355
+ ),
2356
+ );
2357
+
2358
+ const matches = matchesForSelector(selectorValue);
2035
2359
  if (matches.length === 1 && matches[0]) {
2036
2360
  return { status: "resolved", node: matches[0] };
2037
2361
  }
2038
2362
  if (matches.length > 1) {
2039
2363
  return {
2040
2364
  status: "conflict",
2041
- message: `Selector "${target.selector}" matched ${matches.length} code layer nodes.`,
2365
+ message: `Selector "${selectorValue}" matched ${matches.length} code layer nodes.`,
2042
2366
  };
2043
2367
  }
2368
+
2369
+ // Strict matching found nothing. Runtime selectors anchor unstamped elements
2370
+ // with positional `:nth-of-type(n)` parts, which drift when the live DOM
2371
+ // order differs from the stored source (reordered or runtime-inserted nodes).
2372
+ // Retry once with the positional suffixes dropped so an element that is still
2373
+ // unique by its tag/classes/attributes resolves instead of surfacing a hard
2374
+ // "did not match" error. Genuinely ambiguous results stay a conflict rather
2375
+ // than silently editing the wrong node.
2376
+ const positionTolerantSelector = stripPositionalNthOfType(selectorValue);
2377
+ if (positionTolerantSelector && positionTolerantSelector !== selectorValue) {
2378
+ const tolerantMatches = matchesForSelector(positionTolerantSelector);
2379
+ if (tolerantMatches.length === 1 && tolerantMatches[0]) {
2380
+ return { status: "resolved", node: tolerantMatches[0] };
2381
+ }
2382
+ if (tolerantMatches.length > 1) {
2383
+ return {
2384
+ status: "conflict",
2385
+ message: `Selector "${selectorValue}" matched ${tolerantMatches.length} code layer nodes after ignoring positional :nth-of-type (the element may have been reordered or added at runtime). Re-select the element to refresh its id.`,
2386
+ };
2387
+ }
2388
+ }
2389
+
2044
2390
  return {
2045
2391
  status: "conflict",
2046
- message: `Selector "${target.selector}" did not match a code layer node.`,
2392
+ message: `Selector "${selectorValue}" did not match a code layer node.`,
2047
2393
  };
2048
2394
  }
2049
2395
 
@@ -2250,6 +2596,71 @@ function applyTextEdit(
2250
2596
  };
2251
2597
  }
2252
2598
 
2599
+ /**
2600
+ * Apply a responsive-class edit intent to the HTML source.
2601
+ *
2602
+ * - `"add"`: calls `setPropertyClass(className, prefix, utility)` — adds a
2603
+ * new token at the target prefix (or replaces the existing one for
2604
+ * the same stem).
2605
+ * - `"replace"`: same as `"add"` — `setPropertyClass` already handles the
2606
+ * replace-if-same-stem semantics.
2607
+ * - `"remove"`: calls `removePropertyClass(className, prefix, stem)` — strips
2608
+ * all tokens with the given property stem at the target prefix,
2609
+ * falling back to the base value (Tailwind cascade).
2610
+ *
2611
+ * Uses the helpers from `responsive-classes.ts` so the logic is shared with
2612
+ * the StatesPanel / inspector UI.
2613
+ */
2614
+ function applyResponsiveClassEdit(
2615
+ html: string,
2616
+ element: ParsedElement,
2617
+ intent: ResponsiveClassEditIntent,
2618
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
2619
+ const currentClass = attributeValue(element, "class") ?? "";
2620
+
2621
+ let nextClass: string;
2622
+ if (intent.operation === "remove") {
2623
+ if (!intent.stem) {
2624
+ // stem is required for remove
2625
+ return "unsupported";
2626
+ }
2627
+ if (!isSafeClassToken(intent.stem)) return "unsupported";
2628
+ nextClass = removePropertyClass(currentClass, intent.prefix, intent.stem);
2629
+ } else {
2630
+ // "add" and "replace" both use setPropertyClass
2631
+ if (!intent.utility) return "unsupported";
2632
+ if (!isSafeClassToken(intent.utility)) return "unsupported";
2633
+ nextClass = setPropertyClass(currentClass, intent.prefix, intent.utility);
2634
+ }
2635
+
2636
+ if (nextClass === currentClass) {
2637
+ // No-op — nothing to patch.
2638
+ return {
2639
+ content: html,
2640
+ capability: {
2641
+ kind: "responsive-class",
2642
+ prefix: intent.prefix,
2643
+ operations: [intent.operation],
2644
+ overriddenProperties: [],
2645
+ confidence: 0.87,
2646
+ },
2647
+ };
2648
+ }
2649
+
2650
+ return {
2651
+ content: replaceOrInsertAttribute(html, element, "class", nextClass),
2652
+ capability: {
2653
+ kind: "responsive-class",
2654
+ prefix: intent.prefix,
2655
+ operations: [intent.operation],
2656
+ overriddenProperties: intent.utility
2657
+ ? [intent.utility.split("-")[0] ?? ""]
2658
+ : [],
2659
+ confidence: 0.87,
2660
+ },
2661
+ };
2662
+ }
2663
+
2253
2664
  function applyMoveNodeEdit(
2254
2665
  html: string,
2255
2666
  element: ParsedElement,
@@ -2290,6 +2701,379 @@ function applyMoveNodeEdit(
2290
2701
  };
2291
2702
  }
2292
2703
 
2704
+ /** Generate a fresh unique data-agent-native-node-id value not already in the set. */
2705
+ function freshNodeId(usedIds: Set<string>, basis: string): string {
2706
+ const base = `an-${hashStable(basis)}`;
2707
+ let value = base;
2708
+ let suffix = 1;
2709
+ while (usedIds.has(value)) {
2710
+ value = `an-${hashStable(`${base}:${suffix}`)}`;
2711
+ suffix += 1;
2712
+ }
2713
+ usedIds.add(value);
2714
+ return value;
2715
+ }
2716
+
2717
+ /**
2718
+ * Strip the given CSS property names from an inline style attribute value.
2719
+ * Returns the new style string (may be empty if all declarations were removed).
2720
+ */
2721
+ function stripStyleProperties(
2722
+ styleValue: string | null,
2723
+ propertiesToRemove: string[],
2724
+ ): string {
2725
+ if (!styleValue) return "";
2726
+ const toRemove = new Set(propertiesToRemove.map((p) => p.toLowerCase()));
2727
+ const remaining = parseStyleDeclarations(styleValue).filter(
2728
+ (decl) => !toRemove.has(decl.property),
2729
+ );
2730
+ return serializeStyleDeclarations(remaining);
2731
+ }
2732
+
2733
+ /** Absolute-positioning properties stripped when converting a child to auto-layout flow. */
2734
+ const AUTO_LAYOUT_STRIP_PROPS = [
2735
+ "position",
2736
+ "left",
2737
+ "top",
2738
+ "right",
2739
+ "bottom",
2740
+ "inset",
2741
+ ] as const;
2742
+
2743
+ /**
2744
+ * Apply display:flex + direction + gap to a raw open-tag string and return it.
2745
+ * Only touches the style attribute.
2746
+ */
2747
+ function addAutoLayoutStyleToOpenTag(
2748
+ openTag: string,
2749
+ element: ParsedElement,
2750
+ html: string,
2751
+ direction: "row" | "column",
2752
+ gap: string,
2753
+ ): string {
2754
+ const currentStyle = attributeValue(element, "style");
2755
+ let declarations = parseStyleDeclarations(currentStyle);
2756
+ const setOrReplace = (prop: string, val: string) => {
2757
+ const existing = declarations.find((d) => d.property === prop);
2758
+ if (existing) {
2759
+ existing.value = val;
2760
+ } else {
2761
+ declarations.push({ property: prop, value: val });
2762
+ }
2763
+ };
2764
+ setOrReplace("display", "flex");
2765
+ setOrReplace("flex-direction", direction);
2766
+ setOrReplace("gap", gap);
2767
+ const nextStyle = serializeStyleDeclarations(declarations);
2768
+ // We work on a scratch copy relative to element boundaries
2769
+ const attr = getAttribute(element, "style");
2770
+ if (attr) {
2771
+ return `${openTag.slice(0, attr.start - element.start)}${attr.name}="${escapeHtmlAttribute(nextStyle)}"${openTag.slice(attr.end - element.start)}`;
2772
+ }
2773
+ // Insert before closing >
2774
+ const closeChar = openTag.endsWith("/>")
2775
+ ? openTag.length - 2
2776
+ : openTag.length - 1;
2777
+ return `${openTag.slice(0, closeChar)} style="${escapeHtmlAttribute(nextStyle)}"${openTag.slice(closeChar)}`;
2778
+ }
2779
+
2780
+ /**
2781
+ * Strip absolute-positioning properties from a child's inline style, applying
2782
+ * the edit directly to the html string at the child element's source spans.
2783
+ * Returns the updated html string.
2784
+ */
2785
+ function stripAbsolutePositioningFromChild(
2786
+ html: string,
2787
+ child: ParsedElement,
2788
+ ): string {
2789
+ const currentStyle = attributeValue(child, "style");
2790
+ if (!currentStyle) return html;
2791
+ const nextStyle = stripStyleProperties(currentStyle, [
2792
+ ...AUTO_LAYOUT_STRIP_PROPS,
2793
+ ]);
2794
+ return replaceOrInsertAttribute(html, child, "style", nextStyle);
2795
+ }
2796
+
2797
+ /**
2798
+ * GROUP: wrap targetted sibling elements (sharing a common parent) in a new
2799
+ * <div> wrapper. Targets must all share the same parent element.
2800
+ */
2801
+ function applyWrapNodes(
2802
+ html: string,
2803
+ build: ProjectionBuild,
2804
+ intent: WrapNodesEditIntent,
2805
+ ):
2806
+ | { content: string; capability: EditCapability; wrapperNodeId: string }
2807
+ | PatchResultStatus {
2808
+ const { targetIds, autoLayout = false } = intent;
2809
+ if (targetIds.length === 0) return "unsupported";
2810
+
2811
+ // Resolve all target nodes via nodeId attribute matching.
2812
+ const targetElements: ParsedElement[] = [];
2813
+ for (const id of targetIds) {
2814
+ const node = build.projection.nodes.find(
2815
+ (n) =>
2816
+ n.dataAttributes["data-agent-native-node-id"] === id || n.id === id,
2817
+ );
2818
+ if (!node) return "conflict";
2819
+ const el = build.elementByNodeId.get(node.id);
2820
+ if (!el) return "conflict";
2821
+ targetElements.push(el);
2822
+ }
2823
+
2824
+ // All targets must share the same parent.
2825
+ const parentIndexes = new Set(targetElements.map((el) => el.parentIndex));
2826
+ if (parentIndexes.size !== 1) return "unsupported";
2827
+
2828
+ // Sort targets by their source position (ascending).
2829
+ targetElements.sort((a, b) => a.start - b.start);
2830
+
2831
+ const siblingIndexes = targetElements
2832
+ .map((el) => el.siblingIndex)
2833
+ .sort((a, b) => a - b);
2834
+ const firstSiblingIndex = siblingIndexes[0];
2835
+ if (firstSiblingIndex === undefined) return "unsupported";
2836
+ const targetsAreContiguous = siblingIndexes.every(
2837
+ (siblingIndex, index) => siblingIndex === firstSiblingIndex + index,
2838
+ );
2839
+ if (!targetsAreContiguous) return "unsupported";
2840
+
2841
+ // Collect existing node ids so we can generate a unique one.
2842
+ const usedIds = new Set(
2843
+ build.projection.nodes.flatMap((n) => {
2844
+ const id = n.dataAttributes["data-agent-native-node-id"];
2845
+ return id ? [id] : [];
2846
+ }),
2847
+ );
2848
+
2849
+ const wrapperNodeId = freshNodeId(
2850
+ usedIds,
2851
+ `wrap:${targetElements.map((el) => el.start).join(":")}`,
2852
+ );
2853
+
2854
+ // Collect the source fragments for all targets.
2855
+ const fragments = targetElements.map((el) => {
2856
+ let frag = html.slice(el.start, el.end);
2857
+ // If autoLayout, strip positioning from each wrapped child.
2858
+ if (autoLayout) {
2859
+ // We need a ParsedElement that reflects the fragment's own positions.
2860
+ // Re-parse the fragment to find the root element and strip its style.
2861
+ const fragElements = parseHtmlElements(frag);
2862
+ const root = fragElements.find((fe) => fe.parentIndex === undefined);
2863
+ if (root) {
2864
+ frag = stripAbsolutePositioningFromChild(frag, root);
2865
+ }
2866
+ }
2867
+ return frag;
2868
+ });
2869
+
2870
+ const autoLayoutStyle = autoLayout
2871
+ ? ` style="display: flex; flex-direction: column; gap: 8px"`
2872
+ : "";
2873
+ const wrapperOpen = `<div data-agent-native-node-id="${escapeHtmlAttribute(wrapperNodeId)}" data-agent-native-layer-name="Group"${autoLayoutStyle}>`;
2874
+ const wrapperClose = `</div>`;
2875
+ const wrapperContent = `${wrapperOpen}${fragments.join("")}${wrapperClose}`;
2876
+
2877
+ // Build the replacement: remove all targets from html (back to front) then
2878
+ // insert the wrapper at the first target's position.
2879
+ // Sort by position descending to remove safely.
2880
+ const sorted = [...targetElements].sort((a, b) => b.start - a.start);
2881
+
2882
+ // Remove all targets from the html (back to front).
2883
+ let result = html;
2884
+ let firstTargetStart = targetElements[0]!.start;
2885
+
2886
+ for (const el of sorted) {
2887
+ const start = el.start;
2888
+ const end = el.end;
2889
+ if (start < firstTargetStart) {
2890
+ firstTargetStart = start;
2891
+ }
2892
+ result = `${result.slice(0, start)}${result.slice(end)}`;
2893
+ }
2894
+
2895
+ // Re-compute firstTargetStart relative to the modified string: all removals
2896
+ // before it shift it. Count how many bytes were removed before firstTargetStart.
2897
+ let bytesRemovedBefore = 0;
2898
+ for (const el of targetElements) {
2899
+ if (el.start < targetElements[0]!.start) {
2900
+ bytesRemovedBefore += el.end - el.start;
2901
+ }
2902
+ }
2903
+ const insertAt = firstTargetStart - bytesRemovedBefore;
2904
+
2905
+ result = `${result.slice(0, insertAt)}${wrapperContent}${result.slice(insertAt)}`;
2906
+
2907
+ return {
2908
+ content: result,
2909
+ capability: {
2910
+ kind: "structure",
2911
+ operations: ["moveNode"],
2912
+ confidence: 0.82,
2913
+ },
2914
+ wrapperNodeId,
2915
+ };
2916
+ }
2917
+
2918
+ /**
2919
+ * UNGROUP: replace the wrapper node with its children, spliced into the
2920
+ * wrapper's parent at the wrapper's position, then remove the wrapper.
2921
+ */
2922
+ function applyUnwrap(
2923
+ html: string,
2924
+ build: ProjectionBuild,
2925
+ intent: UnwrapEditIntent,
2926
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
2927
+ const { targetId } = intent;
2928
+ const node = build.projection.nodes.find(
2929
+ (n) =>
2930
+ n.dataAttributes["data-agent-native-node-id"] === targetId ||
2931
+ n.id === targetId,
2932
+ );
2933
+ if (!node) return "conflict";
2934
+ if (!node.source) return "needsAgent";
2935
+
2936
+ const element = build.elementByNodeId.get(node.id);
2937
+ if (!element) return "conflict";
2938
+ if (element.selfClosing || element.contentStart >= element.contentEnd) {
2939
+ // Nothing to unwrap from an empty/void element.
2940
+ return "unsupported";
2941
+ }
2942
+
2943
+ // The children's source content is the element's inner HTML.
2944
+ const innerContent = html.slice(element.contentStart, element.contentEnd);
2945
+
2946
+ // Replace the whole element (start..end) with its inner content.
2947
+ const result = `${html.slice(0, element.start)}${innerContent}${html.slice(element.end)}`;
2948
+
2949
+ return {
2950
+ content: result,
2951
+ capability: {
2952
+ kind: "structure",
2953
+ operations: ["moveNode"],
2954
+ confidence: 0.82,
2955
+ },
2956
+ };
2957
+ }
2958
+
2959
+ /**
2960
+ * CONVERT: toggle auto-layout (display:flex) on an existing container.
2961
+ */
2962
+ function applyAutoLayout(
2963
+ html: string,
2964
+ build: ProjectionBuild,
2965
+ intent: AutoLayoutEditIntent,
2966
+ ): { content: string; capability: EditCapability } | PatchResultStatus {
2967
+ const { targetId, enabled, direction = "column", gap = "8px" } = intent;
2968
+ const node = build.projection.nodes.find(
2969
+ (n) =>
2970
+ n.dataAttributes["data-agent-native-node-id"] === targetId ||
2971
+ n.id === targetId,
2972
+ );
2973
+ if (!node) return "conflict";
2974
+ if (!node.source) return "needsAgent";
2975
+
2976
+ const element = build.elementByNodeId.get(node.id);
2977
+ if (!element) return "conflict";
2978
+
2979
+ if (!enabled) {
2980
+ // Turn off auto-layout: set display:block.
2981
+ const currentStyle = attributeValue(element, "style");
2982
+ let declarations = parseStyleDeclarations(currentStyle);
2983
+ const displayDecl = declarations.find((d) => d.property === "display");
2984
+ if (displayDecl) {
2985
+ displayDecl.value = "block";
2986
+ } else {
2987
+ declarations.push({ property: "display", value: "block" });
2988
+ }
2989
+ const nextStyle = serializeStyleDeclarations(declarations);
2990
+ return {
2991
+ content: replaceOrInsertAttribute(html, element, "style", nextStyle),
2992
+ capability: {
2993
+ kind: "style",
2994
+ properties: ["display"],
2995
+ confidence: 0.9,
2996
+ },
2997
+ };
2998
+ }
2999
+
3000
+ // Enable auto-layout: set display:flex + direction + gap on the container.
3001
+ // Apply all three style properties in a single mutation against the already-
3002
+ // resolved element so that elements with no stable data attributes or HTML id
3003
+ // are handled correctly. Re-parsing after each individual property write
3004
+ // caused a silent no-op for those elements because the re-parse-based element
3005
+ // finder could not locate them after the first write changed the style attr.
3006
+ const currentStyle = attributeValue(element, "style");
3007
+ let declarations = parseStyleDeclarations(currentStyle);
3008
+ const setOrReplace = (prop: string, val: string) => {
3009
+ const existing = declarations.find((d) => d.property === prop);
3010
+ if (existing) {
3011
+ existing.value = val;
3012
+ } else {
3013
+ declarations.push({ property: prop, value: val });
3014
+ }
3015
+ };
3016
+ setOrReplace("display", "flex");
3017
+ setOrReplace("flex-direction", direction);
3018
+ setOrReplace("gap", gap);
3019
+ let result = replaceOrInsertAttribute(
3020
+ html,
3021
+ element,
3022
+ "style",
3023
+ serializeStyleDeclarations(declarations),
3024
+ );
3025
+
3026
+ // Strip absolute positioning from direct children.
3027
+ // Re-parse to get up-to-date child element positions after the style mutation.
3028
+ const updatedElements = parseHtmlElements(result);
3029
+ // Locate the target element in the updated parse. Prefer stable data
3030
+ // attributes and HTML id; fall back to matching by original source position
3031
+ // (safe because the container open-tag length only changed by the style attr
3032
+ // rewrite, which shifts nothing before element.start).
3033
+ const stableAttrPairs: Array<[string, string]> = [];
3034
+ for (const attrName of STABLE_NODE_ID_ATTRIBUTES) {
3035
+ const v = attributeValue(element, attrName);
3036
+ if (v) stableAttrPairs.push([attrName, v]);
3037
+ }
3038
+ const htmlIdValue = attributeValue(element, "id");
3039
+ const findElementInParsed = (
3040
+ elements: ParsedElement[],
3041
+ ): ParsedElement | undefined => {
3042
+ for (const attrPair of stableAttrPairs) {
3043
+ const found = elements.find(
3044
+ (fe) => attributeValue(fe, attrPair[0]) === attrPair[1],
3045
+ );
3046
+ if (found) return found;
3047
+ }
3048
+ if (htmlIdValue) {
3049
+ return elements.find((fe) => attributeValue(fe, "id") === htmlIdValue);
3050
+ }
3051
+ // Fallback: match by original start position. The container's start offset
3052
+ // is unchanged because only its open-tag content (style attr) was modified.
3053
+ return elements.find((fe) => fe.start === element.start);
3054
+ };
3055
+ const updatedTarget = findElementInParsed(updatedElements);
3056
+
3057
+ if (updatedTarget) {
3058
+ // Process children in reverse order so offsets stay valid.
3059
+ const childIndexes = [...updatedTarget.childIndexes].reverse();
3060
+ for (const childIndex of childIndexes) {
3061
+ const child = updatedElements[childIndex];
3062
+ if (!child) continue;
3063
+ result = stripAbsolutePositioningFromChild(result, child);
3064
+ }
3065
+ }
3066
+
3067
+ return {
3068
+ content: result,
3069
+ capability: {
3070
+ kind: "style",
3071
+ properties: ["display", "flex-direction", "gap"],
3072
+ confidence: 0.88,
3073
+ },
3074
+ };
3075
+ }
3076
+
2293
3077
  function findAfterNode(
2294
3078
  projection: CodeLayerProjection,
2295
3079
  before: CodeLayerNode,
@@ -2333,6 +3117,121 @@ export function applyVisualEdit(
2333
3117
  }
2334
3118
 
2335
3119
  const initial = buildProjection(html, source);
3120
+
3121
+ // --- Structural intents that don't resolve a single target node ---
3122
+
3123
+ if (intent.kind === "wrapNodes") {
3124
+ const wrapEdit = applyWrapNodes(html, initial, intent);
3125
+ if (typeof wrapEdit === "string") {
3126
+ return {
3127
+ content: html,
3128
+ projection: initial.projection,
3129
+ result: {
3130
+ ...patchResult(
3131
+ wrapEdit,
3132
+ source,
3133
+ intent,
3134
+ false,
3135
+ wrapEdit === "unsupported"
3136
+ ? "wrapNodes requires all targets to share a common parent element."
3137
+ : "Could not resolve one or more target nodes for wrapNodes.",
3138
+ ),
3139
+ },
3140
+ };
3141
+ }
3142
+ const nextProjection = buildCodeLayerProjection(wrapEdit.content, {
3143
+ source,
3144
+ });
3145
+ return {
3146
+ content: wrapEdit.content,
3147
+ projection: nextProjection,
3148
+ result: {
3149
+ ...patchResult(
3150
+ "applied",
3151
+ source,
3152
+ intent,
3153
+ wrapEdit.content !== html,
3154
+ wrapEdit.content === html
3155
+ ? "No source change was needed."
3156
+ : "Nodes wrapped.",
3157
+ ),
3158
+ wrapperNodeId: wrapEdit.wrapperNodeId,
3159
+ },
3160
+ };
3161
+ }
3162
+
3163
+ if (intent.kind === "unwrap") {
3164
+ const unwrapEdit = applyUnwrap(html, initial, intent);
3165
+ if (typeof unwrapEdit === "string") {
3166
+ return {
3167
+ content: html,
3168
+ projection: initial.projection,
3169
+ result: patchResult(
3170
+ unwrapEdit,
3171
+ source,
3172
+ intent,
3173
+ false,
3174
+ unwrapEdit === "conflict"
3175
+ ? `Could not resolve unwrap target "${intent.targetId}".`
3176
+ : "Cannot unwrap a self-closing or empty element.",
3177
+ ),
3178
+ };
3179
+ }
3180
+ const nextProjection = buildCodeLayerProjection(unwrapEdit.content, {
3181
+ source,
3182
+ });
3183
+ return {
3184
+ content: unwrapEdit.content,
3185
+ projection: nextProjection,
3186
+ result: patchResult(
3187
+ "applied",
3188
+ source,
3189
+ intent,
3190
+ unwrapEdit.content !== html,
3191
+ unwrapEdit.content === html
3192
+ ? "No source change was needed."
3193
+ : "Node unwrapped.",
3194
+ ),
3195
+ };
3196
+ }
3197
+
3198
+ if (intent.kind === "autoLayout") {
3199
+ const alEdit = applyAutoLayout(html, initial, intent);
3200
+ if (typeof alEdit === "string") {
3201
+ return {
3202
+ content: html,
3203
+ projection: initial.projection,
3204
+ result: patchResult(
3205
+ alEdit,
3206
+ source,
3207
+ intent,
3208
+ false,
3209
+ alEdit === "conflict"
3210
+ ? `Could not resolve autoLayout target "${intent.targetId}".`
3211
+ : "Cannot apply autoLayout to this element.",
3212
+ ),
3213
+ };
3214
+ }
3215
+ const nextProjection = buildCodeLayerProjection(alEdit.content, { source });
3216
+ return {
3217
+ content: alEdit.content,
3218
+ projection: nextProjection,
3219
+ result: patchResult(
3220
+ "applied",
3221
+ source,
3222
+ intent,
3223
+ alEdit.content !== html,
3224
+ alEdit.content === html
3225
+ ? "No source change was needed."
3226
+ : intent.enabled
3227
+ ? "Auto-layout enabled."
3228
+ : "Auto-layout disabled.",
3229
+ ),
3230
+ };
3231
+ }
3232
+
3233
+ // --- Target-resolved intents (style / class / textContent / moveNode) ---
3234
+
2336
3235
  const resolution = resolveTarget(initial, intent.target);
2337
3236
  if (resolution.status !== "resolved" || !resolution.node) {
2338
3237
  return {
@@ -2376,6 +3275,8 @@ export function applyVisualEdit(
2376
3275
  edit = applyClassEdit(html, element, intent);
2377
3276
  } else if (intent.kind === "textContent") {
2378
3277
  edit = applyTextEdit(html, element, intent);
3278
+ } else if (intent.kind === "responsive-class") {
3279
+ edit = applyResponsiveClassEdit(html, element, intent);
2379
3280
  } else {
2380
3281
  const anchorResolution = resolveTarget(initial, intent.anchor);
2381
3282
  if (anchorResolution.status !== "resolved" || !anchorResolution.node) {
@@ -2469,3 +3370,150 @@ export function applyVisualEdit(
2469
3370
  ),
2470
3371
  };
2471
3372
  }
3373
+
3374
+ export interface MoveNodeBetweenDocumentsOptions {
3375
+ nodeId: string;
3376
+ anchorNodeId?: string;
3377
+ placement?: "before" | "after" | "inside";
3378
+ }
3379
+
3380
+ export interface MoveNodeBetweenDocumentsResult {
3381
+ sourceHtml: string;
3382
+ destHtml: string;
3383
+ status: "applied" | "unsupported";
3384
+ message?: string;
3385
+ /**
3386
+ * The data-agent-native-node-id of the moved node in destHtml.
3387
+ * May differ from the original nodeId when a collision caused a re-stamp.
3388
+ * Only present when status is "applied".
3389
+ */
3390
+ movedNodeId?: string;
3391
+ }
3392
+
3393
+ /**
3394
+ * Move a node (by data-agent-native-node-id) from sourceHtml into destHtml.
3395
+ * The node's serialized subtree is removed from sourceHtml and inserted into
3396
+ * destHtml relative to anchorNodeId (default: append to <body> or end of doc).
3397
+ * Any node ids in the moved subtree that already exist in destHtml are
3398
+ * re-stamped to stay unique. No external dependencies.
3399
+ */
3400
+ export function moveNodeBetweenDocuments(
3401
+ sourceHtml: string,
3402
+ destHtml: string,
3403
+ opts: MoveNodeBetweenDocumentsOptions,
3404
+ ): MoveNodeBetweenDocumentsResult {
3405
+ const { nodeId, anchorNodeId, placement = "inside" } = opts;
3406
+
3407
+ // --- Locate the node in sourceHtml ---
3408
+ const sourceElements = parseHtmlElements(sourceHtml);
3409
+ const sourceTarget = sourceElements.find(
3410
+ (el) => attributeValue(el, "data-agent-native-node-id") === nodeId,
3411
+ );
3412
+ if (!sourceTarget) {
3413
+ return {
3414
+ sourceHtml,
3415
+ destHtml,
3416
+ status: "unsupported",
3417
+ message: `Node with data-agent-native-node-id="${nodeId}" not found in sourceHtml.`,
3418
+ };
3419
+ }
3420
+
3421
+ // --- Extract the subtree fragment ---
3422
+ let fragment = sourceHtml.slice(sourceTarget.start, sourceTarget.end);
3423
+
3424
+ // --- Collect all existing node ids in destHtml to avoid collisions ---
3425
+ const destElements = parseHtmlElements(destHtml);
3426
+ const destUsedIds = new Set<string>(
3427
+ destElements
3428
+ .map((el) => attributeValue(el, "data-agent-native-node-id"))
3429
+ .filter((v): v is string => v !== null),
3430
+ );
3431
+
3432
+ // --- Re-stamp any colliding node ids in the fragment ---
3433
+ // We do this by parsing the fragment as its own HTML and replacing ids.
3434
+ const fragElements = parseHtmlElements(fragment);
3435
+ // Build replacement map: old id → new id
3436
+ const idRemap = new Map<string, string>();
3437
+ for (const fragEl of fragElements) {
3438
+ const existingId = attributeValue(fragEl, "data-agent-native-node-id");
3439
+ if (!existingId) continue;
3440
+ if (destUsedIds.has(existingId)) {
3441
+ const newId = freshNodeId(
3442
+ destUsedIds,
3443
+ `moved:${existingId}:${fragEl.start}`,
3444
+ );
3445
+ idRemap.set(existingId, newId);
3446
+ } else {
3447
+ destUsedIds.add(existingId);
3448
+ }
3449
+ }
3450
+
3451
+ // Apply id remaps to the fragment (back to front by attribute position).
3452
+ if (idRemap.size > 0) {
3453
+ const remapEdits: Array<{ start: number; end: number; value: string }> = [];
3454
+ for (const fragEl of fragElements) {
3455
+ const attr = getAttribute(fragEl, "data-agent-native-node-id");
3456
+ if (!attr || typeof attr.value !== "string") continue;
3457
+ const newId = idRemap.get(attr.value);
3458
+ if (!newId) continue;
3459
+ remapEdits.push({
3460
+ start: attr.start,
3461
+ end: attr.end,
3462
+ value: `data-agent-native-node-id="${escapeHtmlAttribute(newId)}"`,
3463
+ });
3464
+ }
3465
+ // Apply back to front.
3466
+ remapEdits.sort((a, b) => b.start - a.start);
3467
+ for (const edit of remapEdits) {
3468
+ fragment = `${fragment.slice(0, edit.start)}${edit.value}${fragment.slice(edit.end)}`;
3469
+ }
3470
+ }
3471
+
3472
+ // --- Remove node from sourceHtml ---
3473
+ const nextSourceHtml = `${sourceHtml.slice(0, sourceTarget.start)}${sourceHtml.slice(sourceTarget.end)}`;
3474
+
3475
+ // --- Insert fragment into destHtml ---
3476
+ let nextDestHtml: string;
3477
+
3478
+ if (anchorNodeId) {
3479
+ const anchor = destElements.find(
3480
+ (el) => attributeValue(el, "data-agent-native-node-id") === anchorNodeId,
3481
+ );
3482
+ if (!anchor) {
3483
+ return {
3484
+ sourceHtml,
3485
+ destHtml,
3486
+ status: "unsupported",
3487
+ message: `Anchor node with data-agent-native-node-id="${anchorNodeId}" not found in destHtml.`,
3488
+ };
3489
+ }
3490
+ const insertAt =
3491
+ placement === "before"
3492
+ ? anchor.start
3493
+ : placement === "after"
3494
+ ? anchor.end
3495
+ : anchor.selfClosing
3496
+ ? anchor.end
3497
+ : anchor.contentEnd;
3498
+ nextDestHtml = `${destHtml.slice(0, insertAt)}${fragment}${destHtml.slice(insertAt)}`;
3499
+ } else {
3500
+ // Default: find <body> and append inside it, or append at end of doc.
3501
+ const bodyEl = destElements.find((el) => el.tag === "body");
3502
+ const insertAt = bodyEl
3503
+ ? bodyEl.selfClosing
3504
+ ? bodyEl.end
3505
+ : bodyEl.contentEnd
3506
+ : destHtml.length;
3507
+ nextDestHtml = `${destHtml.slice(0, insertAt)}${fragment}${destHtml.slice(insertAt)}`;
3508
+ }
3509
+
3510
+ // The moved node keeps its original id unless it collided and was re-stamped.
3511
+ const movedNodeId = idRemap.get(nodeId) ?? nodeId;
3512
+
3513
+ return {
3514
+ sourceHtml: nextSourceHtml,
3515
+ destHtml: nextDestHtml,
3516
+ status: "applied",
3517
+ movedNodeId,
3518
+ };
3519
+ }