@agent-native/core 0.80.10 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +108 -15
  5. package/corpus/core/src/cli/design-connect.ts +419 -19
  6. package/corpus/core/src/client/AssistantChat.tsx +123 -26
  7. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  8. package/corpus/core/src/client/chat/index.ts +1 -0
  9. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  10. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  11. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  12. package/corpus/core/src/client/index.ts +1 -0
  13. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  14. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  15. package/corpus/core/src/client/sse-event-processor.ts +9 -0
  16. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  17. package/corpus/core/src/db/client.ts +29 -2
  18. package/corpus/core/src/provider-api/index.ts +29 -0
  19. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  20. package/corpus/core/src/styles/agent-native.css +54 -0
  21. package/corpus/core/src/usage/store.ts +2 -2
  22. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  23. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  24. package/corpus/templates/analytics/app/global.css +8 -0
  25. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  26. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  27. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  28. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  29. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  30. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  31. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  36. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  37. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  38. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +9 -2
  39. package/corpus/templates/assets/AGENTS.md +4 -0
  40. package/corpus/templates/assets/actions/_tool-activity.ts +46 -0
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +26 -5
  42. package/corpus/templates/assets/actions/generate-image.ts +118 -67
  43. package/corpus/templates/assets/actions/list-draft-assets.ts +50 -0
  44. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +52 -31
  46. package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +100 -0
  47. package/corpus/templates/assets/app/global.css +15 -0
  48. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  49. package/corpus/templates/assets/app/i18n/zh-TW.ts +7 -0
  50. package/corpus/templates/assets/app/i18n-data.ts +61 -0
  51. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  52. package/corpus/templates/assets/app/routes/library.tsx +233 -52
  53. package/corpus/templates/assets/changelog/2026-06-29-image-generation-can-now-render-requested-text-and-respect-b.md +6 -0
  54. package/corpus/templates/assets/changelog/2026-06-30-image-generation-no-longer-looks-stuck-while-the-provider-is-still-working.md +6 -0
  55. package/corpus/templates/assets/changelog/2026-06-30-tagged-presets-now-control-the-image-aspect-ratio.md +6 -0
  56. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  57. package/corpus/templates/assets/server/lib/generation.ts +135 -11
  58. package/corpus/templates/assets/shared/api.ts +4 -0
  59. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  60. package/corpus/templates/clips/actions/list-ai-requests.ts +60 -0
  61. package/corpus/templates/clips/app/hooks/use-auto-title.ts +24 -25
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  64. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  65. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  66. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  67. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  68. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  69. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  70. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  71. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  72. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  76. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  77. package/corpus/templates/design/AGENTS.md +27 -0
  78. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  79. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  80. package/corpus/templates/design/actions/apply-component-prop-edit.ts +76 -25
  81. package/corpus/templates/design/actions/apply-design-state.ts +17 -4
  82. package/corpus/templates/design/actions/apply-motion-edit.ts +76 -115
  83. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  84. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  85. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  86. package/corpus/templates/design/actions/delete-design.ts +20 -0
  87. package/corpus/templates/design/actions/export-zip.ts +23 -8
  88. package/corpus/templates/design/actions/generate-design.ts +53 -1
  89. package/corpus/templates/design/actions/get-component-details.ts +21 -26
  90. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  91. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  92. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  93. package/corpus/templates/design/actions/index-design-tokens.ts +18 -5
  94. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  95. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  96. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  97. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  98. package/corpus/templates/design/actions/list-design-states.ts +19 -1
  99. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  100. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  101. package/corpus/templates/design/actions/navigate.ts +18 -3
  102. package/corpus/templates/design/actions/open-component-source.ts +7 -2
  103. package/corpus/templates/design/actions/preview-component-prop-edit.ts +32 -26
  104. package/corpus/templates/design/actions/preview-shader-fill.ts +9 -11
  105. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  106. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  107. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  108. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  109. package/corpus/templates/design/actions/run-design-audit.ts +74 -10
  110. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  111. package/corpus/templates/design/actions/view-screen.ts +1 -1
  112. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  113. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +244 -3197
  114. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  115. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +795 -289
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +635 -234
  117. package/corpus/templates/design/app/components/design/LayersPanel.tsx +124 -32
  118. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  119. package/corpus/templates/design/app/components/design/MotionDock.tsx +244 -266
  120. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +491 -56
  121. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +55 -67
  122. package/corpus/templates/design/app/components/design/StatesPanel.tsx +38 -63
  123. package/corpus/templates/design/app/components/design/TokensPanel.tsx +264 -8
  124. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  125. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  126. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  127. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  128. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  129. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  131. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  132. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  133. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  134. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  135. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  136. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  137. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  138. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  139. package/corpus/templates/design/app/global.css +7 -0
  140. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  141. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  142. package/corpus/templates/design/app/i18n-data.ts +369 -0
  143. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +3024 -616
  145. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  146. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-30-design-token-edits-now-stay-visible-in-previews-and-token-li.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  171. package/corpus/templates/design/e2e/global-setup.ts +94 -2
  172. package/corpus/templates/design/e2e/helpers.ts +168 -65
  173. package/corpus/templates/design/package.json +3 -1
  174. package/corpus/templates/design/server/db/schema.ts +23 -0
  175. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  176. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  177. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  178. package/corpus/templates/design/server/plugins/db.ts +39 -0
  179. package/corpus/templates/design/server/register-secrets.ts +38 -0
  180. package/corpus/templates/design/shared/board-file.ts +228 -0
  181. package/corpus/templates/design/shared/board-objects.ts +288 -0
  182. package/corpus/templates/design/shared/code-layer.ts +52 -0
  183. package/corpus/templates/design/shared/component-model.ts +35 -0
  184. package/corpus/templates/design/shared/motion-compiler.ts +137 -15
  185. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  186. package/corpus/templates/design/shared/resolve-tweaks.ts +32 -9
  187. package/corpus/templates/design/tsconfig.json +2 -1
  188. package/corpus/templates/slides/AGENTS.md +9 -0
  189. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  190. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  191. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  192. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  193. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  194. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  195. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  196. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  197. package/corpus/templates/slides/package.json +1 -0
  198. package/corpus/templates/slides/vite.config.ts +1 -0
  199. package/dist/agent/production-agent.d.ts.map +1 -1
  200. package/dist/agent/production-agent.js +94 -10
  201. package/dist/agent/production-agent.js.map +1 -1
  202. package/dist/cli/design-connect.d.ts +25 -0
  203. package/dist/cli/design-connect.d.ts.map +1 -1
  204. package/dist/cli/design-connect.js +357 -18
  205. package/dist/cli/design-connect.js.map +1 -1
  206. package/dist/client/AssistantChat.d.ts +14 -0
  207. package/dist/client/AssistantChat.d.ts.map +1 -1
  208. package/dist/client/AssistantChat.js +115 -22
  209. package/dist/client/AssistantChat.js.map +1 -1
  210. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  211. package/dist/client/ConnectBuilderCard.js +1 -0
  212. package/dist/client/ConnectBuilderCard.js.map +1 -1
  213. package/dist/client/chat/index.d.ts +1 -1
  214. package/dist/client/chat/index.d.ts.map +1 -1
  215. package/dist/client/chat/index.js +1 -1
  216. package/dist/client/chat/index.js.map +1 -1
  217. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  218. package/dist/client/chat/run-recovery.js +5 -5
  219. package/dist/client/chat/run-recovery.js.map +1 -1
  220. package/dist/client/chat-view-transition.d.ts +6 -0
  221. package/dist/client/chat-view-transition.d.ts.map +1 -1
  222. package/dist/client/chat-view-transition.js +23 -2
  223. package/dist/client/chat-view-transition.js.map +1 -1
  224. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  225. package/dist/client/composer/PromptComposer.js +3 -2
  226. package/dist/client/composer/PromptComposer.js.map +1 -1
  227. package/dist/client/index.d.ts +1 -1
  228. package/dist/client/index.d.ts.map +1 -1
  229. package/dist/client/index.js +1 -1
  230. package/dist/client/index.js.map +1 -1
  231. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  232. package/dist/client/org/OrgSwitcher.js +11 -4
  233. package/dist/client/org/OrgSwitcher.js.map +1 -1
  234. package/dist/client/sharing/ShareButton.js +3 -3
  235. package/dist/client/sharing/ShareButton.js.map +1 -1
  236. package/dist/client/sse-event-processor.d.ts.map +1 -1
  237. package/dist/client/sse-event-processor.js +10 -0
  238. package/dist/client/sse-event-processor.js.map +1 -1
  239. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  240. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  241. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  242. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  243. package/dist/collab/awareness.d.ts +2 -2
  244. package/dist/collab/awareness.d.ts.map +1 -1
  245. package/dist/collab/routes.d.ts +2 -2
  246. package/dist/db/client.d.ts +1 -0
  247. package/dist/db/client.d.ts.map +1 -1
  248. package/dist/db/client.js +28 -2
  249. package/dist/db/client.js.map +1 -1
  250. package/dist/notifications/routes.d.ts +3 -3
  251. package/dist/observability/routes.d.ts +3 -3
  252. package/dist/progress/routes.d.ts +1 -1
  253. package/dist/provider-api/index.d.ts +5 -4
  254. package/dist/provider-api/index.d.ts.map +1 -1
  255. package/dist/provider-api/index.js +28 -0
  256. package/dist/provider-api/index.js.map +1 -1
  257. package/dist/resources/handlers.d.ts +3 -3
  258. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  259. package/dist/server/core-routes-plugin.d.ts +25 -0
  260. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  261. package/dist/server/core-routes-plugin.js +43 -18
  262. package/dist/server/core-routes-plugin.js.map +1 -1
  263. package/dist/server/transcribe-voice.d.ts +1 -1
  264. package/dist/styles/agent-native.css +54 -0
  265. package/dist/usage/store.js +2 -2
  266. package/dist/usage/store.js.map +1 -1
  267. package/package.json +1 -1
@@ -32,7 +32,11 @@ import { getDb, schema } from "../server/db/index.js";
32
32
  import "../server/db/index.js"; // ensure registerShareableResource runs
33
33
  import { buildCodeLayerProjection } from "../shared/code-layer.js";
34
34
  import type { CodeLayerSource } from "../shared/code-layer.js";
35
- import { componentNameFor, extractProps } from "../shared/component-model.js";
35
+ import {
36
+ componentNameFor,
37
+ componentNodeIdMatches,
38
+ extractProps,
39
+ } from "../shared/component-model.js";
36
40
  import { normalizeDesignSourceType } from "../shared/source-mode.js";
37
41
 
38
42
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -62,7 +66,15 @@ async function liveContent(
62
66
  export interface ComponentPropPreviewMessage {
63
67
  /** Matches the existing parent→iframe postMessage type vocabulary. */
64
68
  type: "style-change" | "replace-document-content" | "select-element";
65
- payload: Record<string, unknown>;
69
+ selector?: string;
70
+ nodeId?: string;
71
+ attributeOverrides?: Record<string, string>;
72
+ classEdit?: {
73
+ kind: "class";
74
+ operation: "replace";
75
+ from: string;
76
+ to: string;
77
+ };
66
78
  }
67
79
 
68
80
  // ─── Action ───────────────────────────────────────────────────────────────────
@@ -156,7 +168,9 @@ export default defineAction({
156
168
  source: codeLayerSource,
157
169
  });
158
170
 
159
- const node = projection.nodes.find((n) => n.id === nodeId);
171
+ const node = projection.nodes.find((n) =>
172
+ componentNodeIdMatches(n, nodeId),
173
+ );
160
174
  if (!node) {
161
175
  throw new Error(
162
176
  `Node "${nodeId}" not found. Run get-code-layer-projection to list current ids.`,
@@ -182,35 +196,29 @@ export default defineAction({
182
196
  // attribute without a full HTML replace.
183
197
  messages.push({
184
198
  type: "style-change",
185
- payload: {
186
- selector: node.selector,
187
- nodeId,
188
- attributeOverrides: { "x-data": edit.value },
189
- },
199
+ selector: node.selector,
200
+ nodeId,
201
+ attributeOverrides: { "x-data": edit.value },
190
202
  });
191
203
  } else if (edit.kind === "attribute") {
192
204
  messages.push({
193
205
  type: "style-change",
194
- payload: {
195
- selector: node.selector,
196
- nodeId,
197
- attributeOverrides: { [edit.attribute]: edit.value },
198
- },
206
+ selector: node.selector,
207
+ nodeId,
208
+ attributeOverrides: { [edit.attribute]: edit.value },
199
209
  });
200
210
  } else if (edit.kind === "classReplace") {
201
211
  // Communicate as a responsive-class intent so the bridge applies it via
202
212
  // the existing deterministic class-patch path.
203
213
  messages.push({
204
214
  type: "style-change",
205
- payload: {
206
- selector: node.selector,
207
- nodeId,
208
- classEdit: {
209
- kind: "class",
210
- operation: "replace",
211
- from: edit.from,
212
- to: edit.to,
213
- },
215
+ selector: node.selector,
216
+ nodeId,
217
+ classEdit: {
218
+ kind: "class",
219
+ operation: "replace",
220
+ from: edit.from,
221
+ to: edit.to,
214
222
  },
215
223
  });
216
224
  }
@@ -218,10 +226,8 @@ export default defineAction({
218
226
  // Also emit a select-element message so the canvas highlights the node.
219
227
  messages.push({
220
228
  type: "select-element",
221
- payload: {
222
- selector: node.selector,
223
- nodeId,
224
- },
229
+ selector: node.selector,
230
+ nodeId,
225
231
  });
226
232
 
227
233
  const sourceType =
@@ -2,8 +2,8 @@
2
2
  * preview-shader-fill — preview-only action.
3
3
  *
4
4
  * Returns the CSS mesh-gradient approximation for a shader fill so the caller
5
- * can apply it to the selected node via the bridge (`tweak-values` /
6
- * `style-change` messages) **without persisting anything**.
5
+ * can apply it to the selected node via the `shader-fill-preview` bridge
6
+ * message **without persisting anything**.
7
7
  *
8
8
  * Deliberately lightweight:
9
9
  * - No DB access.
@@ -84,11 +84,11 @@ export default defineAction({
84
84
  Preview a CSS mesh-gradient shader fill on the selected design node without persisting anything.
85
85
 
86
86
  Returns:
87
- - previewCss — a CSS \`background\` value for the live preview (inject via bridge style-change or tweak-values).
87
+ - previewCss — a CSS \`background\` value for the live preview (inject via the shader-fill-preview bridge message).
88
88
  - fallbackCss — a simpler static CSS \`background\` for export / PDF / SSR contexts.
89
89
  - fallbackBlock — a complete CSS rule block (selector + fallback background) ready to embed.
90
90
  - descriptor — the resolved and validated ShaderDescriptor.
91
- - bridgeMessage — a ready-to-use JSON bridge payload for the iframe style-change message.
91
+ - bridgeMessage — a ready-to-use JSON bridge payload for the iframe shader-fill-preview message.
92
92
 
93
93
  Call this before apply-shader-fill. The preview is purely CSS — no WebGL, no canvas, no writes.
94
94
 
@@ -139,14 +139,12 @@ support; it has no effect on the static CSS preview output today.
139
139
  const fallbackBlock = buildShaderFillFallbackBlock(selector, descriptor);
140
140
 
141
141
  // Build a ready-to-post bridge payload for the iframe.
142
- // The `style-change` message type is already handled by DesignCanvas.tsx.
142
+ // The `shader-fill-preview` message type is handled by DesignCanvas.tsx.
143
143
  const bridgeMessage = {
144
- type: "style-change",
145
- nodeId: target?.nodeId ?? null,
144
+ type: "shader-fill-preview",
146
145
  selector: target?.selector ?? null,
147
- styles: {
148
- background: previewCss,
149
- },
146
+ nodeId: target?.nodeId ?? null,
147
+ css: previewCss,
150
148
  // Metadata so the client can attach a "preview only" badge.
151
149
  _preview: true,
152
150
  _source: "shader-fill",
@@ -167,7 +165,7 @@ support; it has no effect on the static CSS preview output today.
167
165
  JSON.stringify(bridgeMessage, null, 2),
168
166
  ``,
169
167
  `Apply steps:`,
170
- `1. Inject the previewCss via a bridge "style-change" message (no write, no persist).`,
168
+ `1. Inject the previewCss via a bridge "shader-fill-preview" message (no write, no persist).`,
171
169
  `2. If the user approves, call apply-shader-fill — but note that action is currently GATED`,
172
170
  ` and will return a clear not-yet-available result until runtime rendering +`,
173
171
  ` source-write + diff proof are all in place.`,
@@ -10,7 +10,7 @@ const ProviderSchema = z.enum(DESIGN_PROVIDER_API_IDS);
10
10
 
11
11
  export default defineAction({
12
12
  description:
13
- "List raw HTTP API capabilities for Design-connected providers. Use before provider-api-request when design-token import convenience actions are too narrow. Returns provider base URLs, auth style, credential key names, docs/spec URLs, placeholders, and examples; never returns secret values.",
13
+ "List raw HTTP API capabilities for Design-connected providers such as GitHub and Figma. Use before provider-api-request when design-token, repository, or Figma-library convenience actions are too narrow. Returns provider base URLs, auth style, credential key names, docs/spec URLs, placeholders, and examples; never returns secret values.",
14
14
  schema: z.object({
15
15
  provider: ProviderSchema.optional().describe(
16
16
  "Optional provider id to inspect. Omit to list every Design provider API escape hatch.",
@@ -22,7 +22,7 @@ export default defineAction({
22
22
  return {
23
23
  providers: await listProviderApiCatalog(provider),
24
24
  guidance:
25
- "Design actions like import-github are workflow shortcuts, not capability limits. When the GitHub API can answer the question with a better endpoint, query, body, pagination mode, metadata field, or API version, inspect docs/spec URLs and call provider-api-request directly. GitHub auth uses the saved GITHUB_TOKEN secret and never exposes the token value.",
25
+ "Design actions like import-github and list-figma-library-assets are workflow shortcuts, not capability limits. When GitHub or Figma can answer the question with a better endpoint, query, body, pagination mode, metadata field, or API version, inspect docs/spec URLs and call provider-api-request directly. Auth uses saved scoped secrets such as GITHUB_TOKEN or FIGMA_ACCESS_TOKEN and never exposes token values.",
26
26
  };
27
27
  },
28
28
  });
@@ -25,7 +25,7 @@ const WebContentSearchSchema = z.object({
25
25
 
26
26
  export default defineAction({
27
27
  description:
28
- "Inspect provider API docs/spec metadata, or fetch any public API documentation page, OpenAPI spec, changelog, or web page. Use this before provider-api-request when the exact GitHub endpoint, payload shape, pagination, or API version is uncertain.",
28
+ "Inspect provider API docs/spec metadata, or fetch any public API documentation page, OpenAPI spec, changelog, or web page. Use this before provider-api-request when the exact GitHub/Figma endpoint, payload shape, pagination, or API version is uncertain.",
29
29
  schema: z.object({
30
30
  provider: ProviderSchema.describe(
31
31
  "Provider whose API docs/spec to inspect.",
@@ -71,8 +71,8 @@ const PaginationSchema = z
71
71
  export default defineAction({
72
72
  description:
73
73
  "Make an arbitrary authenticated HTTP request to a Design-connected provider API. " +
74
- "Use this as the flexible escape hatch when a convenience action cannot express the needed GitHub endpoint, file tree query, issue/search query, request body, pagination mode, payload shape, or API version. " +
75
- "The request is constrained to the provider host, uses the saved GITHUB_TOKEN automatically, blocks private/internal URLs, and redacts secrets from responses. " +
74
+ "Use this as the flexible escape hatch when a convenience action cannot express the needed GitHub endpoint, Figma file/library endpoint, request body, pagination mode, payload shape, or API version. " +
75
+ "The request is constrained to the provider host, uses saved scoped credentials such as GITHUB_TOKEN or FIGMA_ACCESS_TOKEN automatically, blocks private/internal URLs, and redacts secrets from responses. " +
76
76
  "\n\nSTAGING MODE (preferred for large responses): Pass stageAs to write response items into a scratch dataset instead of returning the raw body. " +
77
77
  "Returns { dataset, rowCount, columns, sampleRows } so only a compact summary enters context. Use query-staged-dataset to aggregate, filter, and project the data without re-fetching. " +
78
78
  "\n\nPAGINATION: When stageAs is set, pass pagination config to fetch all pages server-side into the same dataset. For GitHub page pagination, use pageParam='page', pageSize from per_page, and itemsPath when the response object contains an items array.",
@@ -83,7 +83,7 @@ export default defineAction({
83
83
  .string()
84
84
  .min(1)
85
85
  .describe(
86
- "Provider API path such as /repos/{owner}/{repo}/contents/{path}, /search/code, /issues, or a full URL on an allowed provider host. Use placeholders from provider-api-catalog when provided.",
86
+ "Provider API path such as /repos/{owner}/{repo}/contents/{path}, /search/code, /files/{fileKey}/components, /images/{fileKey}, or a full URL on an allowed provider host. Use placeholders from provider-api-catalog when provided.",
87
87
  ),
88
88
  query: z
89
89
  .unknown()
@@ -0,0 +1,48 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
+ import { assertAccess } from "@agent-native/core/sharing";
4
+ import { and, eq } from "drizzle-orm";
5
+ import { z } from "zod";
6
+
7
+ import { getDb, schema } from "../server/db/index.js";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "Revoke a previously granted localhost write-consent for a design + connection. " +
12
+ "After revocation the agent can no longer write local files until the user re-grants consent. " +
13
+ "Requires editor access on the design.",
14
+ schema: z.object({
15
+ designId: z.string().describe("Design ID."),
16
+ connectionId: z
17
+ .string()
18
+ .describe("Localhost connection ID whose grant should be revoked."),
19
+ }),
20
+ run: async ({ designId, connectionId }) => {
21
+ await assertAccess("design", designId, "editor");
22
+
23
+ const ownerEmail = getRequestUserEmail();
24
+ if (!ownerEmail) throw new Error("no authenticated user");
25
+
26
+ const db = getDb();
27
+
28
+ const deleted = await db
29
+ .delete(schema.designLocalhostWriteGrants)
30
+ .where(
31
+ and(
32
+ eq(schema.designLocalhostWriteGrants.designId, designId),
33
+ eq(schema.designLocalhostWriteGrants.connectionId, connectionId),
34
+ eq(schema.designLocalhostWriteGrants.ownerEmail, ownerEmail),
35
+ ),
36
+ );
37
+
38
+ // `deleted` shape varies by dialect; check rows affected via the object.
39
+ const rowsAffected =
40
+ (deleted as unknown as { rowsAffected?: number })?.rowsAffected ?? 0;
41
+
42
+ return {
43
+ designId,
44
+ connectionId,
45
+ revoked: rowsAffected > 0,
46
+ };
47
+ },
48
+ });
@@ -108,6 +108,8 @@ function checkMissingLabels(html: string): A11yFinding[] {
108
108
  let idx = 0;
109
109
  for (const m of html.matchAll(inputPattern)) {
110
110
  const tag = m[0];
111
+ const inputStart = m.index ?? 0;
112
+ const inputEnd = inputStart + tag.length;
111
113
  const typeMatch = tag.match(/\btype\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i);
112
114
  const type = (typeMatch?.[1] ?? typeMatch?.[2] ?? "text").toLowerCase();
113
115
  // Hidden and submit/button/image inputs don't need visible labels
@@ -123,8 +125,9 @@ function checkMissingLabels(html: string): A11yFinding[] {
123
125
  const hasExplicitLabel = inputId
124
126
  ? new RegExp(`for\\s*=\\s*(?:"${inputId}"|'${inputId}')`, "i").test(html)
125
127
  : false;
128
+ const hasImplicitLabel = isWrappedByLabel(html, inputStart, inputEnd);
126
129
 
127
- if (!hasAriaLabel && !hasExplicitLabel) {
130
+ if (!hasAriaLabel && !hasExplicitLabel && !hasImplicitLabel) {
128
131
  findings.push({
129
132
  id: `missing-label:input-${idx}`,
130
133
  severity: "error" as A11ySeverity,
@@ -143,8 +146,53 @@ function checkMissingLabels(html: string): A11yFinding[] {
143
146
  return findings;
144
147
  }
145
148
 
146
- /** Check interactive elements that are likely too small for touch targets (< ~44px heuristic via Tailwind class). */
147
- function checkTapTargets(html: string): A11yFinding[] {
149
+ function isWrappedByLabel(
150
+ html: string,
151
+ inputStart: number,
152
+ inputEnd: number,
153
+ ): boolean {
154
+ const labelOpen = html.lastIndexOf("<label", inputStart);
155
+ if (labelOpen === -1) return false;
156
+ const labelCloseBeforeInput = html.lastIndexOf("</label", inputStart);
157
+ if (labelCloseBeforeInput > labelOpen) return false;
158
+ const labelCloseAfterInput = html.indexOf("</label", inputEnd);
159
+ return labelCloseAfterInput !== -1;
160
+ }
161
+
162
+ /**
163
+ * Whether an interactive element already declares a minimum size that meets the
164
+ * 44px tap-target floor. This recognises exactly what the inline auto-fix adds
165
+ * (`min-h-[44px] min-w-[44px]`) plus equivalents — arbitrary `min-h`/`min-w`
166
+ * values in px/rem/em ≥ 44px, the Tailwind spacing scale (`min-h-11` = 44px on a
167
+ * 4px step), and full-bleed minimums (`min-h-full` / `min-h-screen`). Without
168
+ * this, a fixed element keeps its original tiny `h-4` class and the audit would
169
+ * re-flag it forever, so the audit↔fix loop would never converge.
170
+ */
171
+ function hasAdequateMinTapSize(tag: string): boolean {
172
+ // Arbitrary values: min-h-[44px], min-w-[2.75rem], etc.
173
+ const arbitraryPattern = /\bmin-(?:h|w)-\[([\d.]+)(px|rem|em)\]/gi;
174
+ let m: RegExpExecArray | null;
175
+ while ((m = arbitraryPattern.exec(tag)) !== null) {
176
+ const value = Number.parseFloat(m[1] ?? "");
177
+ if (!Number.isFinite(value)) continue;
178
+ const px = m[2]?.toLowerCase() === "px" ? value : value * 16;
179
+ if (px >= 44) return true;
180
+ }
181
+ // Tailwind spacing scale: min-h-11 / min-w-11 = 2.75rem = 44px (4px per step).
182
+ const scalePattern = /\bmin-(?:h|w)-(\d+)\b/gi;
183
+ while ((m = scalePattern.exec(tag)) !== null) {
184
+ if (Number.parseInt(m[1] ?? "", 10) * 4 >= 44) return true;
185
+ }
186
+ // Full-bleed minimums always clear the tap floor.
187
+ return /\bmin-(?:h|w)-(?:full|screen)\b/.test(tag);
188
+ }
189
+
190
+ /**
191
+ * Check interactive elements that are likely too small for touch targets
192
+ * (< ~44px heuristic via Tailwind class). Exported for unit tests that assert
193
+ * the audit↔fix loop converges (a fixed element must stop being flagged).
194
+ */
195
+ export function checkTapTargets(html: string): A11yFinding[] {
148
196
  const findings: A11yFinding[] = [];
149
197
  // Heuristic: buttons/links with explicit tiny size classes (h-4, h-5, w-4, w-5, size-4, size-5)
150
198
  // and no explicit larger override or sr-only are flagged.
@@ -158,7 +206,8 @@ function checkTapTargets(html: string): A11yFinding[] {
158
206
  if (
159
207
  tinyPattern.test(tag) &&
160
208
  !largePattern.test(tag) &&
161
- !srOnlyPattern.test(tag)
209
+ !srOnlyPattern.test(tag) &&
210
+ !hasAdequateMinTapSize(tag)
162
211
  ) {
163
212
  findings.push({
164
213
  id: `tap-target:interactive-${idx}`,
@@ -170,7 +219,7 @@ function checkTapTargets(html: string): A11yFinding[] {
170
219
  nodeId: extractNodeId(tag),
171
220
  selector: extractSelector(tag, "button"),
172
221
  wcag: "2.5.5",
173
- fixAvailable: false,
222
+ fixAvailable: true,
174
223
  });
175
224
  }
176
225
  idx++;
@@ -272,7 +321,7 @@ function checkFocusVisibility(html: string): A11yFinding[] {
272
321
  nodeId: extractNodeId(tag),
273
322
  selector: extractSelector(tag, tag.match(/^<([a-z]+)/i)?.[1] ?? "*"),
274
323
  wcag: "2.4.7",
275
- fixAvailable: false,
324
+ fixAvailable: true,
276
325
  });
277
326
  }
278
327
  inlineIdx++;
@@ -286,10 +335,23 @@ function checkContrastHint(html: string): A11yFinding[] {
286
335
  // Static analysis cannot compute real contrast ratios without a DOM/CSS
287
336
  // cascade resolver. We flag the presence of explicit low-opacity text colors
288
337
  // as a human-review hint — the UI will show these as "info" prompts.
289
- const lowOpacityTextPattern =
290
- /class\s*=\s*"[^"]*\btext-(?:white|black|gray-\d+)\s+(?:opacity-[0-3]\d|text-opacity-[0-3]\d)[^"]*"/gi;
338
+ const tagPattern = /<([a-z][a-z0-9:-]*)\b[^>]*>/gi;
339
+ const textColorPattern = /\btext-(?:white|black|gray-\d+)\b/i;
340
+ const lowOpacityPattern = /\b(?:opacity-[0-3]\d|text-opacity-[0-3]\d)\b/i;
291
341
  let idx = 0;
292
- for (const m of html.matchAll(lowOpacityTextPattern)) {
342
+ for (const m of html.matchAll(tagPattern)) {
343
+ const tag = m[0];
344
+ const classMatch = tag.match(/\bclass\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i);
345
+ const className = classMatch?.[1] ?? classMatch?.[2] ?? "";
346
+ if (
347
+ !textColorPattern.test(className) ||
348
+ !lowOpacityPattern.test(className)
349
+ ) {
350
+ continue;
351
+ }
352
+ const tagName = m[1] ?? "*";
353
+ const nodeId = extractNodeId(tag);
354
+ const selector = extractSelector(tag, tagName);
293
355
  findings.push({
294
356
  id: `contrast:low-opacity-${idx}`,
295
357
  severity: "info" as A11ySeverity,
@@ -298,6 +360,8 @@ function checkContrastHint(html: string): A11yFinding[] {
298
360
  "Text element has a low-opacity modifier — verify contrast ratio meets 4.5:1 minimum.",
299
361
  detail:
300
362
  "Low-opacity text can fail WCAG 1.4.3. Run a live contrast check in the browser.",
363
+ nodeId,
364
+ selector,
301
365
  wcag: "1.4.3",
302
366
  fixAvailable: false,
303
367
  });
@@ -353,7 +417,7 @@ export default defineAction({
353
417
  ),
354
418
  }),
355
419
  readOnly: true,
356
- http: { method: "GET" },
420
+ http: { method: "POST" },
357
421
  run: async ({ designId, fileId, filename }) => {
358
422
  const db = getDb();
359
423
 
@@ -146,7 +146,7 @@ const CAPABILITY_ROUTES: Record<string, CapabilityRoute> = {
146
146
  "design.motion-presets:apply": {
147
147
  action: "apply-motion-edit",
148
148
  paramHint:
149
- "Call apply-motion-edit with { designId, fileId?, timeline: { tracks, durationMs } } for an atomic Write to CSS commit.",
149
+ "Call apply-motion-edit with { designId, fileId?, timeline: { tracks, durationMs } } for an atomic managed CSS timeline save.",
150
150
  readOnly: false,
151
151
  },
152
152
  "design.motion-presets:write-source": {
@@ -95,7 +95,7 @@ function resolveActiveScreen(
95
95
 
96
96
  export default defineAction({
97
97
  description:
98
- "See what the user is currently looking at on screen. Returns the current navigation state including which design is open, which view they are on (list, editor, design-systems, present, templates, settings), active/focused design screen, selected element, active inspector tab (design, tweaks, or extensions), overview canvas state, plus any pending question overlay. Always call this first before taking any action.",
98
+ "See what the user is currently looking at on screen. Returns the current navigation state including which design is open, which view they are on (list, editor, design-systems, present, templates, settings), active/focused design screen, selected element, active inspector tab (design or tweaks), active left rail panel (file, agent, assets, tools, or tokens), overview canvas state, plus any pending question overlay. Always call this first before taking any action.",
99
99
  schema: z.object({}),
100
100
  http: false,
101
101
  run: async () => {
@@ -0,0 +1,213 @@
1
+ /**
2
+ * write-local-file — write or patch a local file through the design bridge.
3
+ *
4
+ * Security gates (in order):
5
+ * 1. assertAccess: the caller must have editor access to the design.
6
+ * 2. File extension: only .html, .htm, and .css files are allowed.
7
+ * 3. verifyWriteGrant: a valid (non-expired) user-approved write-consent grant
8
+ * must exist. The agent CANNOT bypass this check.
9
+ * 4. Path confinement: assertPathInside ensures the target stays inside
10
+ * rootPath (pre-bridge check; bridge also validates with realpath).
11
+ * 5. Bridge token: the X-Bridge-Token header is set to the per-grant token
12
+ * minted at consent time so the bridge can reject unauthorized calls.
13
+ */
14
+
15
+ import path from "node:path";
16
+
17
+ import { defineAction } from "@agent-native/core";
18
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
19
+ import { assertAccess } from "@agent-native/core/sharing";
20
+ import { and, eq } from "drizzle-orm";
21
+ import { z } from "zod";
22
+
23
+ import { getDb, schema } from "../server/db/index.js";
24
+ import { verifyWriteGrant } from "../server/lib/verify-write-grant.js";
25
+
26
+ /** File extensions the agent is permitted to write via the bridge. */
27
+ const ALLOWED_EXTENSIONS = new Set([".html", ".htm", ".css"]);
28
+
29
+ function assertAllowedExtension(relPath: string): void {
30
+ const ext = path.extname(relPath).toLowerCase();
31
+ if (!ALLOWED_EXTENSIONS.has(ext)) {
32
+ throw new Error(
33
+ `File "${relPath}" has extension "${ext}" which is not allowed. ` +
34
+ "Only .html, .htm, and .css files may be written through the bridge.",
35
+ );
36
+ }
37
+ }
38
+
39
+ function isLoopbackHostname(hostname: string): boolean {
40
+ const normalized = hostname.toLowerCase();
41
+ if (
42
+ normalized === "localhost" ||
43
+ normalized === "::1" ||
44
+ normalized === "[::1]"
45
+ ) {
46
+ return true;
47
+ }
48
+ const parts = normalized.split(".");
49
+ return (
50
+ parts.length === 4 &&
51
+ parts[0] === "127" &&
52
+ parts.every((part) => /^\d+$/.test(part) && Number(part) <= 255)
53
+ );
54
+ }
55
+
56
+ function normalizeBridgeUrl(value: string): string {
57
+ const parsed = new URL(value.trim());
58
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
59
+ throw new Error("bridgeUrl must be an http(s) URL");
60
+ }
61
+ if (parsed.username || parsed.password) {
62
+ throw new Error("bridgeUrl must not include credentials");
63
+ }
64
+ if (parsed.pathname !== "/" && parsed.pathname !== "") {
65
+ throw new Error("bridgeUrl must not include a path");
66
+ }
67
+ if (!isLoopbackHostname(parsed.hostname)) {
68
+ throw new Error("bridgeUrl must use localhost or a loopback IP address");
69
+ }
70
+ parsed.search = "";
71
+ parsed.hash = "";
72
+ parsed.pathname = "";
73
+ return parsed.toString().replace(/\/$/, "");
74
+ }
75
+
76
+ export default defineAction({
77
+ description:
78
+ "Write or patch a local file (HTML or CSS only) via the localhost design bridge. " +
79
+ "The user MUST have already granted write consent via grant-localhost-write-consent; " +
80
+ "this action will reject the request if no valid grant exists. " +
81
+ "Pass content for a full file write, or {search, replace} for a targeted patch. " +
82
+ "Requires editor access on the design.",
83
+ schema: z.object({
84
+ designId: z.string().describe("Design ID."),
85
+ connectionId: z
86
+ .string()
87
+ .describe("Localhost connection ID (must have an active write grant)."),
88
+ relPath: z
89
+ .string()
90
+ .describe(
91
+ "Path to the file relative to the connection rootPath. " +
92
+ "Only .html, .htm, and .css files are accepted.",
93
+ ),
94
+ content: z
95
+ .string()
96
+ .optional()
97
+ .describe(
98
+ "Full replacement file content. Use for new files or complete rewrites.",
99
+ ),
100
+ patch: z
101
+ .object({
102
+ search: z
103
+ .string()
104
+ .describe("Exact text to search for (must appear exactly once)."),
105
+ replace: z.string().describe("Replacement text."),
106
+ })
107
+ .optional()
108
+ .describe(
109
+ "Search-and-replace patch. Use for targeted edits. " +
110
+ "Mutually exclusive with content.",
111
+ ),
112
+ }),
113
+ run: async ({ designId, connectionId, relPath, content, patch }) => {
114
+ // --- Gate 1: access ---
115
+ await assertAccess("design", designId, "editor");
116
+
117
+ const ownerEmail = getRequestUserEmail();
118
+ if (!ownerEmail) throw new Error("no authenticated user");
119
+
120
+ // --- Gate 2: extension whitelist ---
121
+ assertAllowedExtension(relPath);
122
+
123
+ // --- Gate 3: valid write-consent grant ---
124
+ const grant = await verifyWriteGrant({
125
+ designId,
126
+ connectionId,
127
+ ownerEmail,
128
+ targetPath: relPath,
129
+ });
130
+
131
+ // --- Gate 4: exactly one of content/patch must be provided ---
132
+ if (content === undefined && patch === undefined) {
133
+ throw new Error(
134
+ "Either content (full file write) or patch (search/replace) must be provided.",
135
+ );
136
+ }
137
+ if (content !== undefined && patch !== undefined) {
138
+ throw new Error(
139
+ "content and patch are mutually exclusive. Provide one or the other.",
140
+ );
141
+ }
142
+
143
+ // --- Resolve bridge URL ---
144
+ const db = getDb();
145
+ const [connection] = await db
146
+ .select({ bridgeUrl: schema.designLocalhostConnections.bridgeUrl })
147
+ .from(schema.designLocalhostConnections)
148
+ .where(
149
+ and(
150
+ eq(schema.designLocalhostConnections.id, connectionId),
151
+ eq(schema.designLocalhostConnections.ownerEmail, ownerEmail),
152
+ ),
153
+ )
154
+ .limit(1);
155
+
156
+ if (!connection?.bridgeUrl) {
157
+ throw new Error(
158
+ `No bridge URL found for connection "${connectionId}". ` +
159
+ "Ensure the design bridge is running (npx @agent-native/core@latest design connect).",
160
+ );
161
+ }
162
+
163
+ const bridgeUrl = normalizeBridgeUrl(connection.bridgeUrl);
164
+ const headers: Record<string, string> = {
165
+ "Content-Type": "application/json",
166
+ "X-Bridge-Token": grant.bridgeToken,
167
+ };
168
+
169
+ if (content !== undefined) {
170
+ // Full file write
171
+ const res = await fetch(`${bridgeUrl}/write-file`, {
172
+ method: "POST",
173
+ headers,
174
+ body: JSON.stringify({ relPath, content }),
175
+ });
176
+ if (!res.ok) {
177
+ const errText = await res.text().catch(() => res.statusText);
178
+ throw new Error(`Bridge write-file failed (${res.status}): ${errText}`);
179
+ }
180
+ return { designId, relPath, operation: "write" as const, written: true };
181
+ } else {
182
+ // Search-and-replace patch — first read, then apply-edit
183
+ const readRes = await fetch(`${bridgeUrl}/read-file`, {
184
+ method: "POST",
185
+ headers,
186
+ body: JSON.stringify({ relPath }),
187
+ });
188
+ if (!readRes.ok) {
189
+ const errText = await readRes.text().catch(() => readRes.statusText);
190
+ throw new Error(
191
+ `Bridge read-file failed (${readRes.status}): ${errText}`,
192
+ );
193
+ }
194
+
195
+ const applyRes = await fetch(`${bridgeUrl}/apply-edit`, {
196
+ method: "POST",
197
+ headers,
198
+ body: JSON.stringify({
199
+ relPath,
200
+ search: patch!.search,
201
+ replace: patch!.replace,
202
+ }),
203
+ });
204
+ if (!applyRes.ok) {
205
+ const errText = await applyRes.text().catch(() => applyRes.statusText);
206
+ throw new Error(
207
+ `Bridge apply-edit failed (${applyRes.status}): ${errText}`,
208
+ );
209
+ }
210
+ return { designId, relPath, operation: "patch" as const, written: true };
211
+ }
212
+ },
213
+ });