@agent-native/core 0.92.9 → 0.92.11

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 (345) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +29 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +181 -15
  5. package/corpus/core/src/agent/run-manager.ts +31 -4
  6. package/corpus/core/src/agent/run-store.ts +80 -0
  7. package/corpus/core/src/cli/design-connect.ts +433 -151
  8. package/corpus/core/src/cli/skills.ts +14 -0
  9. package/corpus/core/src/client/AssistantChat.tsx +25 -0
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +40 -6
  11. package/corpus/core/src/client/agent-chat.ts +165 -1
  12. package/corpus/core/src/client/chat/index.ts +9 -1
  13. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +38 -2
  14. package/corpus/core/src/client/index.ts +5 -0
  15. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +18 -7
  16. package/corpus/core/src/collab/awareness-store.ts +23 -12
  17. package/corpus/core/src/collab/client.ts +16 -5
  18. package/corpus/core/src/collab/index.ts +4 -0
  19. package/corpus/core/src/collab/recent-edits.ts +65 -2
  20. package/corpus/core/src/deploy/build.ts +133 -5
  21. package/corpus/core/src/index.browser.ts +1 -0
  22. package/corpus/core/src/server/agent-chat-plugin.ts +98 -11
  23. package/corpus/core/src/server/builder-design-systems.ts +30 -4
  24. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +1 -1
  25. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +12 -6
  26. package/corpus/templates/clips/AGENTS.md +4 -0
  27. package/corpus/templates/clips/actions/create-recording.ts +2 -0
  28. package/corpus/templates/clips/actions/lib/create-recording-schema.ts +5 -3
  29. package/corpus/templates/clips/actions/request-transcript.ts +102 -33
  30. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -0
  31. package/corpus/templates/clips/app/components/player/share-dialog.tsx +59 -19
  32. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +48 -4
  33. package/corpus/templates/clips/app/components/player/video-player.tsx +17 -12
  34. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
  35. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
  36. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  37. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
  38. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
  39. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
  40. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
  41. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
  42. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
  43. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
  44. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
  45. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -37
  46. package/corpus/templates/clips/app/routes/record.tsx +17 -4
  47. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  48. package/corpus/templates/clips/changelog/2026-07-09-desktop-popover-sits-closer-to-the-menu-bar.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-09-desktop-recording-optimization-progress-stays-visible-until-the-clip-opens.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-09-fixed-large-video-file-uploads-failing-before-their-resumabl.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-09-fixed-new-video-uploads-defaulting-to-private.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-09-long-recordings-now-get-enough-time-to-download-their-media-.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-09-meeting-notes-no-longer-lowers-your-microphone-volume-for-ot.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-09-on-mobile-tapping-a-playing-clip-reveals-playback-controls-w.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-07-09-regenerate-any-clip-transcript-from-its-saved-recording-or-r.md +6 -0
  56. package/corpus/templates/clips/changelog/2026-07-09-slack-previews-now-show-the-clip-length-alongside-its-descri.md +6 -0
  57. package/corpus/templates/clips/desktop/src/app.tsx +12 -2
  58. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +8 -1
  59. package/corpus/templates/clips/desktop/src/lib/recorder.ts +8 -5
  60. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +5 -5
  61. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +3 -0
  62. package/corpus/templates/clips/desktop/src/styles.css +6 -3
  63. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +29 -11
  64. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  65. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +103 -21
  66. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +117 -39
  67. package/corpus/templates/clips/server/lib/slack-unfurls.ts +20 -1
  68. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +10 -4
  69. package/corpus/templates/clips/shared/recording-core.ts +12 -0
  70. package/corpus/templates/content/AGENTS.md +7 -6
  71. package/corpus/templates/content/actions/_document-flush.ts +132 -17
  72. package/corpus/templates/content/actions/_notion-action-utils.ts +15 -0
  73. package/corpus/templates/content/actions/create-and-link-notion-page.ts +2 -0
  74. package/corpus/templates/content/actions/link-notion-page.ts +2 -0
  75. package/corpus/templates/content/actions/pull-document.ts +5 -7
  76. package/corpus/templates/content/actions/pull-notion-page.ts +2 -0
  77. package/corpus/templates/content/actions/push-notion-page.ts +10 -0
  78. package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +2 -0
  79. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +88 -23
  80. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +32 -2
  81. package/corpus/templates/content/app/hooks/use-notion.ts +6 -6
  82. package/corpus/templates/content/app/i18n/zh-TW.ts +1 -0
  83. package/corpus/templates/content/app/i18n-data.ts +18 -0
  84. package/corpus/templates/content/changelog/2026-07-09-notion-conflict-choices-and-version-restores-preserve-docume.md +6 -0
  85. package/corpus/templates/content/server/lib/notion-sync.ts +80 -64
  86. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +29 -2
  87. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +86 -0
  88. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +57 -0
  89. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +11 -0
  90. package/corpus/templates/design/AGENTS.md +45 -10
  91. package/corpus/templates/design/actions/add-localhost-screens.ts +66 -13
  92. package/corpus/templates/design/actions/apply-component-prop-edit.ts +45 -36
  93. package/corpus/templates/design/actions/apply-motion-edit.ts +25 -31
  94. package/corpus/templates/design/actions/apply-shader-fill.ts +29 -42
  95. package/corpus/templates/design/actions/detach-component-instance.ts +337 -0
  96. package/corpus/templates/design/actions/export-design-as-figma-svg.ts +219 -0
  97. package/corpus/templates/design/actions/generate-design.ts +79 -12
  98. package/corpus/templates/design/actions/generate-screens.ts +57 -12
  99. package/corpus/templates/design/actions/get-figma-styles.ts +115 -0
  100. package/corpus/templates/design/actions/go-to-main-component.ts +254 -0
  101. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +4 -3
  102. package/corpus/templates/design/actions/import-design-source.ts +4 -31
  103. package/corpus/templates/design/actions/import-figma-clipboard.ts +134 -0
  104. package/corpus/templates/design/actions/import-figma-frame.ts +95 -0
  105. package/corpus/templates/design/actions/index-design-system-with-builder.ts +16 -1
  106. package/corpus/templates/design/actions/list-design-components.ts +104 -0
  107. package/corpus/templates/design/actions/list-figma-library-assets.ts +1 -24
  108. package/corpus/templates/design/actions/open-component-source.ts +2 -4
  109. package/corpus/templates/design/actions/present-design-variants.ts +412 -16
  110. package/corpus/templates/design/actions/preview-source-edit.ts +14 -1
  111. package/corpus/templates/design/actions/run-design-audit.ts +25 -3
  112. package/corpus/templates/design/actions/show-design-questions.ts +10 -0
  113. package/corpus/templates/design/actions/swap-component-instance.ts +544 -0
  114. package/corpus/templates/design/actions/take-design-screenshot.ts +45 -60
  115. package/corpus/templates/design/actions/write-local-file.ts +25 -1
  116. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +152 -0
  117. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1077 -58
  118. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +5 -5
  119. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +73 -21
  120. package/corpus/templates/design/app/components/design/EditPanel.tsx +16 -11
  121. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +364 -0
  122. package/corpus/templates/design/app/components/design/LayersPanel.tsx +180 -53
  123. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +0 -2
  124. package/corpus/templates/design/app/components/design/MotionDock.tsx +144 -21
  125. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1152 -167
  126. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +27 -5
  127. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +23 -2
  128. package/corpus/templates/design/app/components/design/TokensPanel.tsx +63 -49
  129. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1293 -54
  130. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +7 -0
  131. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +6 -1
  132. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +23 -6
  133. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +7 -0
  134. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -0
  135. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +14 -2
  136. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +37 -2
  137. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open-guard.ts +25 -0
  138. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +19 -1
  139. package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +43 -5
  140. package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +27 -2
  141. package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +10 -1
  142. package/corpus/templates/design/app/components/design/code-workbench/store.tsx +19 -2
  143. package/corpus/templates/design/app/components/design/design-canvas/annotation-snapshot.ts +265 -0
  144. package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +16 -5
  145. package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +16 -2
  146. package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +7 -2
  147. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +3 -1
  148. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +78 -8
  149. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +451 -73
  150. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +12 -4
  151. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +398 -272
  152. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +41 -7
  153. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +16 -3
  154. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +24 -4
  155. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +222 -4
  156. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +171 -78
  157. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +45 -5
  158. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +299 -28
  159. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +120 -150
  160. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +53 -1
  161. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +284 -117
  162. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +81 -0
  163. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +19 -14
  164. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +7 -0
  165. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +155 -0
  166. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +268 -46
  167. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +350 -31
  168. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +13 -4
  169. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +233 -23
  170. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +72 -2
  171. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +51 -11
  172. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +84 -11
  173. package/corpus/templates/design/app/components/design/inspector/index.ts +3 -0
  174. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +552 -0
  175. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +157 -0
  176. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +53 -0
  177. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +184 -0
  178. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +403 -26
  179. package/corpus/templates/design/app/components/design/multi-screen/types.ts +20 -1
  180. package/corpus/templates/design/app/components/design/types.ts +33 -0
  181. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +59 -7
  182. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +119 -17
  183. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +275 -63
  184. package/corpus/templates/design/app/hooks/use-question-flow.ts +14 -3
  185. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +67 -10
  186. package/corpus/templates/design/app/i18n/zh-TW.ts +91 -1
  187. package/corpus/templates/design/app/i18n-data.ts +1217 -10
  188. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +577 -0
  189. package/corpus/templates/design/app/lib/agent-chat.ts +23 -0
  190. package/corpus/templates/design/app/lib/design-import.ts +24 -0
  191. package/corpus/templates/design/app/lib/figma-clipboard.ts +157 -0
  192. package/corpus/templates/design/app/lib/figma-svg-copy.ts +221 -0
  193. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +143 -30
  194. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +15 -0
  195. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +33 -0
  196. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +38 -0
  197. package/corpus/templates/design/app/pages/design-editor/overview-annotation-context.ts +157 -0
  198. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +62 -6
  199. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +245 -0
  200. package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +586 -0
  201. package/corpus/templates/design/app/pages/design-editor/screen-auto-layout.ts +169 -0
  202. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +201 -1
  203. package/corpus/templates/design/changelog/2026-07-09-added-underline-strikethrough-and-text-case-uppercase-lowerc.md +6 -0
  204. package/corpus/templates/design/changelog/2026-07-09-annotate-now-draws-across-the-all-screens-canvas-without-swi.md +6 -0
  205. package/corpus/templates/design/changelog/2026-07-09-auto-layout-children-can-now-be-dragged-into-freeform-screen.md +6 -0
  206. package/corpus/templates/design/changelog/2026-07-09-board-shapes-now-stay-visible-and-nest-correctly-across-the-.md +6 -0
  207. package/corpus/templates/design/changelog/2026-07-09-component-source-navigation-now-opens-reliably-from-the-insp.md +6 -0
  208. package/corpus/templates/design/changelog/2026-07-09-cross-screen-local-react-layer-moves-now-hand-off-exact-sour.md +6 -0
  209. package/corpus/templates/design/changelog/2026-07-09-cross-screen-moves-undo-and-redo-now-keep-both-screens-stabl.md +6 -0
  210. package/corpus/templates/design/changelog/2026-07-09-detach-a-component-instance-into-plain-editable-eleme.md +6 -0
  211. package/corpus/templates/design/changelog/2026-07-09-dragging-layers-into-auto-layout-now-respects-ignore-auto-la.md +6 -0
  212. package/corpus/templates/design/changelog/2026-07-09-drawing-annotations-sent-to-the-agent-now-include-a-rendered.md +6 -0
  213. package/corpus/templates/design/changelog/2026-07-09-editor-sidebars-now-use-figma-matched-default-widths-for-a-r.md +6 -0
  214. package/corpus/templates/design/changelog/2026-07-09-empty-frames-and-rectangles-now-expose-full-auto-layout-cont.md +6 -0
  215. package/corpus/templates/design/changelog/2026-07-09-export-any-design-screen-as-a-real-vector-svg-for-figma-with.md +6 -0
  216. package/corpus/templates/design/changelog/2026-07-09-grid-auto-layout-now-uses-true-responsive-rows-and-columns-w.md +6 -0
  217. package/corpus/templates/design/changelog/2026-07-09-hidden-and-locked-screens-now-behave-consistently-between-la.md +6 -0
  218. package/corpus/templates/design/changelog/2026-07-09-image-fills-now-preserve-valid-urls-and-layered-fit-settings.md +6 -0
  219. package/corpus/templates/design/changelog/2026-07-09-import-figma-frames-as-pixel-accurate-design-screens-via-a-s.md +6 -0
  220. package/corpus/templates/design/changelog/2026-07-09-inspect-code-now-hides-internal-styling-metadata-and-wraps-l.md +6 -0
  221. package/corpus/templates/design/changelog/2026-07-09-inspector-blur-and-shadow-effects-now-preserve-other-effects.md +6 -0
  222. package/corpus/templates/design/changelog/2026-07-09-jump-to-a-components-other-instances-from-the-inspecto.md +6 -0
  223. package/corpus/templates/design/changelog/2026-07-09-keyboard-shortcuts-are-now-discoverable-in-a-figma-style-bot.md +6 -0
  224. package/corpus/templates/design/changelog/2026-07-09-layer-selection-now-keeps-cmd-or-ctrl-toggles-and-shift-rang.md +6 -0
  225. package/corpus/templates/design/changelog/2026-07-09-layers-dropped-into-auto-layout-now-participate-correctly-in.md +6 -0
  226. package/corpus/templates/design/changelog/2026-07-09-local-live-edit-screens-now-enter-and-leave-full-view-withou.md +6 -0
  227. package/corpus/templates/design/changelog/2026-07-09-local-preview-urls-now-live-in-the-inspector-instead-of-taki.md +6 -0
  228. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-can-now-be-hidden-and-locked-from-layers-.md +6 -0
  229. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-now-keep-exact-source-provenance-and-supp.md +6 -0
  230. package/corpus/templates/design/changelog/2026-07-09-local-react-visual-editing-now-loads-reliably-and-shows-live.md +6 -0
  231. package/corpus/templates/design/changelog/2026-07-09-paste-from-figma-cmd-v-now-imports-exact-editable-nodes-when.md +6 -0
  232. package/corpus/templates/design/changelog/2026-07-09-pressing-escape-after-selecting-a-screen-now-reveals-page-pr.md +6 -0
  233. package/corpus/templates/design/changelog/2026-07-09-right-click-overlapping-or-nested-objects-to-choose-the-exac.md +6 -0
  234. package/corpus/templates/design/changelog/2026-07-09-scale-edge-stretch-and-center-constraints-now-preserve-layer.md +6 -0
  235. package/corpus/templates/design/changelog/2026-07-09-screen-rows-can-now-be-reordered-in-layers-with-matching-can.md +6 -0
  236. package/corpus/templates/design/changelog/2026-07-09-screen-selection-stays-responsive-in-designs-with-hundreds-o.md +6 -0
  237. package/corpus/templates/design/changelog/2026-07-09-shift-a-now-enables-auto-layout-on-a-selected-screen-with-fl.md +6 -0
  238. package/corpus/templates/design/changelog/2026-07-09-swap-a-component-instance-for-another-from-the-libra.md +6 -0
  239. package/corpus/templates/design/changelog/2026-07-10-drawing-annotations-now-stay-available-to-retry-when-the.md +6 -0
  240. package/corpus/templates/design/e2e/global-setup.ts +3 -6
  241. package/corpus/templates/design/e2e/helpers.ts +27 -6
  242. package/corpus/templates/design/server/lib/design-to-figma-svg.ts +1412 -0
  243. package/corpus/templates/design/server/lib/figma-clipboard-match.ts +196 -0
  244. package/corpus/templates/design/server/lib/figma-node-import.ts +311 -0
  245. package/corpus/templates/design/server/lib/figma-node-to-html.ts +1334 -0
  246. package/corpus/templates/design/server/lib/figma-paste-fallback.ts +64 -0
  247. package/corpus/templates/design/server/lib/playwright-runtime.ts +78 -0
  248. package/corpus/templates/design/server/plugins/db.ts +41 -0
  249. package/corpus/templates/design/server/source-workspace.ts +64 -0
  250. package/corpus/templates/design/shared/board-file.ts +13 -2
  251. package/corpus/templates/design/shared/canvas-math.ts +231 -33
  252. package/corpus/templates/design/shared/code-layer.ts +122 -8
  253. package/corpus/templates/design/shared/component-library.ts +161 -0
  254. package/corpus/templates/design/shared/db-conflict.ts +27 -0
  255. package/corpus/templates/design/shared/design-source-capabilities.ts +2 -1
  256. package/corpus/templates/design/shared/figma-url.ts +149 -0
  257. package/corpus/templates/design/shared/pen-path.ts +16 -7
  258. package/corpus/templates/design/shared/source-mode.ts +1 -0
  259. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +7 -19
  260. package/corpus/templates/plan/changelog/2026-07-09-canvas-panning-now-keeps-zoom-fixed-even-at-the-edges.md +6 -0
  261. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +16 -1
  262. package/corpus/templates/slides/app/context/DeckContext.tsx +311 -70
  263. package/corpus/templates/slides/changelog/2026-07-09-fixed-the-slide-rail-going-permanently-stale-during-a-long-a.md +6 -0
  264. package/dist/agent/production-agent.d.ts +79 -6
  265. package/dist/agent/production-agent.d.ts.map +1 -1
  266. package/dist/agent/production-agent.js +161 -14
  267. package/dist/agent/production-agent.js.map +1 -1
  268. package/dist/agent/run-manager.d.ts.map +1 -1
  269. package/dist/agent/run-manager.js +29 -5
  270. package/dist/agent/run-manager.js.map +1 -1
  271. package/dist/agent/run-store.d.ts +46 -0
  272. package/dist/agent/run-store.d.ts.map +1 -1
  273. package/dist/agent/run-store.js +61 -0
  274. package/dist/agent/run-store.js.map +1 -1
  275. package/dist/cli/design-connect.d.ts +6 -0
  276. package/dist/cli/design-connect.d.ts.map +1 -1
  277. package/dist/cli/design-connect.js +336 -139
  278. package/dist/cli/design-connect.js.map +1 -1
  279. package/dist/cli/skills.d.ts.map +1 -1
  280. package/dist/cli/skills.js +14 -0
  281. package/dist/cli/skills.js.map +1 -1
  282. package/dist/client/AssistantChat.d.ts +2 -0
  283. package/dist/client/AssistantChat.d.ts.map +1 -1
  284. package/dist/client/AssistantChat.js +19 -3
  285. package/dist/client/AssistantChat.js.map +1 -1
  286. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  287. package/dist/client/MultiTabAssistantChat.js +38 -18
  288. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  289. package/dist/client/agent-chat.d.ts +60 -0
  290. package/dist/client/agent-chat.d.ts.map +1 -1
  291. package/dist/client/agent-chat.js +122 -1
  292. package/dist/client/agent-chat.js.map +1 -1
  293. package/dist/client/chat/index.d.ts +1 -1
  294. package/dist/client/chat/index.d.ts.map +1 -1
  295. package/dist/client/chat/index.js +1 -1
  296. package/dist/client/chat/index.js.map +1 -1
  297. package/dist/client/extensions/EmbeddedExtension.d.ts +12 -0
  298. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  299. package/dist/client/extensions/EmbeddedExtension.js +36 -2
  300. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  301. package/dist/client/index.d.ts +1 -1
  302. package/dist/client/index.d.ts.map +1 -1
  303. package/dist/client/index.js +1 -1
  304. package/dist/client/index.js.map +1 -1
  305. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  306. package/dist/client/rich-markdown-editor/useCollabReconcile.js +18 -6
  307. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  308. package/dist/collab/awareness-store.d.ts +5 -0
  309. package/dist/collab/awareness-store.d.ts.map +1 -1
  310. package/dist/collab/awareness-store.js +19 -12
  311. package/dist/collab/awareness-store.js.map +1 -1
  312. package/dist/collab/awareness.d.ts +2 -2
  313. package/dist/collab/awareness.d.ts.map +1 -1
  314. package/dist/collab/client.d.ts.map +1 -1
  315. package/dist/collab/client.js +13 -5
  316. package/dist/collab/client.js.map +1 -1
  317. package/dist/collab/index.d.ts +1 -0
  318. package/dist/collab/index.d.ts.map +1 -1
  319. package/dist/collab/index.js +1 -0
  320. package/dist/collab/index.js.map +1 -1
  321. package/dist/collab/recent-edits.d.ts +3 -1
  322. package/dist/collab/recent-edits.d.ts.map +1 -1
  323. package/dist/collab/recent-edits.js +57 -2
  324. package/dist/collab/recent-edits.js.map +1 -1
  325. package/dist/collab/routes.d.ts +1 -1
  326. package/dist/deploy/build.d.ts +24 -0
  327. package/dist/deploy/build.d.ts.map +1 -1
  328. package/dist/deploy/build.js +102 -5
  329. package/dist/deploy/build.js.map +1 -1
  330. package/dist/index.browser.d.ts +1 -1
  331. package/dist/index.browser.d.ts.map +1 -1
  332. package/dist/index.browser.js +1 -1
  333. package/dist/index.browser.js.map +1 -1
  334. package/dist/notifications/routes.d.ts +1 -1
  335. package/dist/observability/routes.d.ts +5 -5
  336. package/dist/progress/routes.d.ts +1 -1
  337. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  338. package/dist/server/agent-chat-plugin.js +87 -10
  339. package/dist/server/agent-chat-plugin.js.map +1 -1
  340. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  341. package/dist/server/builder-design-systems.d.ts +15 -0
  342. package/dist/server/builder-design-systems.d.ts.map +1 -1
  343. package/dist/server/builder-design-systems.js +9 -4
  344. package/dist/server/builder-design-systems.js.map +1 -1
  345. package/package.json +1 -1
@@ -1007,7 +1007,11 @@ function getAttribute(
1007
1007
 
1008
1008
  function attributeValue(element: ParsedElement, name: string): string | null {
1009
1009
  const value = getAttribute(element, name)?.value;
1010
- if (typeof value === "string") return value;
1010
+ // Parsed attributes contain source text, so quoted values may still carry
1011
+ // entities emitted by an earlier deterministic patch. Decode before using
1012
+ // them semantically or reserializing; otherwise sequential style edits turn
1013
+ // `"` into `"` on every pass and corrupt quoted CSS url() values.
1014
+ if (typeof value === "string") return decodeBasicHtmlEntities(value);
1011
1015
  if (value === true) return "";
1012
1016
  return null;
1013
1017
  }
@@ -3143,6 +3147,7 @@ function applyMoveNodeEdit(
3143
3147
  element: ParsedElement,
3144
3148
  anchor: ParsedElement,
3145
3149
  intent: MoveNodeEditIntent,
3150
+ destinationParent?: ParsedElement,
3146
3151
  ): { content: string; capability: EditCapability } | PatchResultStatus {
3147
3152
  if (element.index === anchor.index) return "conflict";
3148
3153
  if (anchor.start >= element.start && anchor.end <= element.end) {
@@ -3152,7 +3157,14 @@ function applyMoveNodeEdit(
3152
3157
  return "unsupported";
3153
3158
  }
3154
3159
 
3155
- const fragment = html.slice(element.start, element.end);
3160
+ const sourceParentIndex = element.parentIndex;
3161
+ const entersNewParent =
3162
+ destinationParent !== undefined &&
3163
+ sourceParentIndex !== destinationParent.index;
3164
+ const rawFragment = html.slice(element.start, element.end);
3165
+ const fragment = entersNewParent
3166
+ ? prepareMovedFragmentForParent(rawFragment, destinationParent)
3167
+ : rawFragment;
3156
3168
  const withoutTarget = `${html.slice(0, element.start)}${html.slice(
3157
3169
  element.end,
3158
3170
  )}`;
@@ -3178,6 +3190,51 @@ function applyMoveNodeEdit(
3178
3190
  };
3179
3191
  }
3180
3192
 
3193
+ /**
3194
+ * Whether children of this element participate in normal flex/grid flow.
3195
+ * Inline style is authoritative for inspector-created auto layout; the class
3196
+ * checks cover authored Tailwind/utility layouts in standalone Alpine files.
3197
+ */
3198
+ function isFlowLayoutContainer(element: ParsedElement | undefined): boolean {
3199
+ if (!element) return false;
3200
+ const display = parseStyle(attributeValue(element, "style")).display;
3201
+ if (
3202
+ display === "flex" ||
3203
+ display === "inline-flex" ||
3204
+ display === "grid" ||
3205
+ display === "inline-grid"
3206
+ ) {
3207
+ return true;
3208
+ }
3209
+ const classes = new Set(classList(element));
3210
+ return (
3211
+ classes.has("flex") ||
3212
+ classes.has("inline-flex") ||
3213
+ classes.has("grid") ||
3214
+ classes.has("inline-grid")
3215
+ );
3216
+ }
3217
+
3218
+ /**
3219
+ * A Figma auto-layout drop makes the moved layer a flow child. Carrying its
3220
+ * former `position:absolute` offsets into the new flex/grid parent leaves it
3221
+ * visually detached from ordering, gap, and alignment even though the layer
3222
+ * tree says it was reparented. Normalize only the moved fragment's root; its
3223
+ * descendants keep their own positioning contexts unchanged.
3224
+ */
3225
+ function prepareMovedFragmentForParent(
3226
+ fragment: string,
3227
+ destinationParent: ParsedElement | undefined,
3228
+ ): string {
3229
+ if (!isFlowLayoutContainer(destinationParent)) return fragment;
3230
+ const fragmentRoot = parseHtmlElements(fragment).find(
3231
+ (element) => element.parentIndex === undefined,
3232
+ );
3233
+ return fragmentRoot
3234
+ ? stripAbsolutePositioningFromChild(fragment, fragmentRoot)
3235
+ : fragment;
3236
+ }
3237
+
3181
3238
  /** Generate a fresh unique data-agent-native-node-id value not already in the set. */
3182
3239
  function freshNodeId(usedIds: Set<string>, basis: string): string {
3183
3240
  const base = `an-${hashStable(basis)}`;
@@ -3264,11 +3321,42 @@ function stripAbsolutePositioningFromChild(
3264
3321
  child: ParsedElement,
3265
3322
  ): string {
3266
3323
  const currentStyle = attributeValue(child, "style");
3267
- if (!currentStyle) return html;
3268
- const nextStyle = stripStyleProperties(currentStyle, [
3269
- ...AUTO_LAYOUT_STRIP_PROPS,
3270
- ]);
3271
- return replaceOrInsertAttribute(html, child, "style", nextStyle);
3324
+ let nextHtml = currentStyle
3325
+ ? replaceOrInsertAttribute(
3326
+ html,
3327
+ child,
3328
+ "style",
3329
+ stripStyleProperties(currentStyle, [...AUTO_LAYOUT_STRIP_PROPS]),
3330
+ )
3331
+ : html;
3332
+
3333
+ // Source-backed Alpine/Tailwind designs commonly express positioning as
3334
+ // utility classes instead of inline CSS. Once the layer moves into a new
3335
+ // flex/grid parent, those utilities would keep it out of flow even though
3336
+ // the Layers tree shows it as a child. Remove only position-mode utilities;
3337
+ // inset utilities can remain because they are inert for a statically
3338
+ // positioned flex/grid item, and preserving them avoids needless source
3339
+ // churn if the user later makes the layer absolute again.
3340
+ const reparsedRoot = parseHtmlElements(nextHtml).find(
3341
+ (element) => element.parentIndex === undefined,
3342
+ );
3343
+ if (!reparsedRoot) return nextHtml;
3344
+ const classes = classList(reparsedRoot);
3345
+ const flowClasses = classes.filter((token) => {
3346
+ const variants = token.split(":");
3347
+ const utility = variants[variants.length - 1]?.replace(/^!/, "");
3348
+ return (
3349
+ utility !== "absolute" && utility !== "fixed" && utility !== "sticky"
3350
+ );
3351
+ });
3352
+ if (flowClasses.length === classes.length) return nextHtml;
3353
+ nextHtml = replaceOrInsertAttribute(
3354
+ nextHtml,
3355
+ reparsedRoot,
3356
+ "class",
3357
+ flowClasses.join(" "),
3358
+ );
3359
+ return nextHtml;
3272
3360
  }
3273
3361
 
3274
3362
  /**
@@ -4018,7 +4106,19 @@ export function applyVisualEdit(
4018
4106
  const removedLength = element.end - element.start;
4019
4107
  moveInsertAt =
4020
4108
  element.start < rawInsertAt ? rawInsertAt - removedLength : rawInsertAt;
4021
- edit = applyMoveNodeEdit(html, element, anchorElement, intent);
4109
+ const destinationParent =
4110
+ intent.placement === "inside"
4111
+ ? anchorElement
4112
+ : anchorResolution.node.parentId
4113
+ ? initial.elementByNodeId.get(anchorResolution.node.parentId)
4114
+ : undefined;
4115
+ edit = applyMoveNodeEdit(
4116
+ html,
4117
+ element,
4118
+ anchorElement,
4119
+ intent,
4120
+ destinationParent,
4121
+ );
4022
4122
  }
4023
4123
 
4024
4124
  if (typeof edit === "string") {
@@ -4265,6 +4365,13 @@ export function moveNodeBetweenDocuments(
4265
4365
  : destHtml.length;
4266
4366
  anchorRedirected = true;
4267
4367
  }
4368
+ const destinationParent =
4369
+ placement === "inside"
4370
+ ? anchor
4371
+ : anchor.parentIndex === undefined
4372
+ ? undefined
4373
+ : destElements[anchor.parentIndex];
4374
+ fragment = prepareMovedFragmentForParent(fragment, destinationParent);
4268
4375
  nextDestHtml = `${destHtml.slice(0, insertAt)}${fragment}${destHtml.slice(insertAt)}`;
4269
4376
  } else {
4270
4377
  // Default: find <body> and append inside it, or append at end of doc.
@@ -4281,6 +4388,13 @@ export function moveNodeBetweenDocuments(
4281
4388
  if (isOffsetInsideTemplateInterior(destHtml, insertAt)) {
4282
4389
  insertAt = destHtml.length;
4283
4390
  }
4391
+ // Appending inside <body> makes the moved node a flow child of the body,
4392
+ // exactly like the anchored `placement: "inside"` branch above. When the
4393
+ // destination body is a flex/grid container, carrying the fragment's
4394
+ // former absolute offsets along would leave it visually detached from
4395
+ // the body's ordering/gap/alignment, so run the same normalization
4396
+ // (prepareMovedFragmentForParent no-ops for non-flow bodies).
4397
+ fragment = prepareMovedFragmentForParent(fragment, bodyEl);
4284
4398
  nextDestHtml = `${destHtml.slice(0, insertAt)}${fragment}${destHtml.slice(insertAt)}`;
4285
4399
  }
4286
4400
 
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Component library scan — cross-file component instance discovery.
3
+ *
4
+ * `index-components` (and `get-component-details`) only ever look at ONE
5
+ * design file at a time (the active file, or an explicit `fileId`).
6
+ * `component_index` metadata is therefore scoped to whichever file was last
7
+ * indexed, and its `runtimeSelectors` do not reliably span every screen in a
8
+ * multi-file design.
9
+ *
10
+ * "Go to main component" and "Swap instance" both need to find OTHER
11
+ * instances of a component name that may live on a different screen (design
12
+ * file) than the one currently open. This module scans a caller-supplied set
13
+ * of already-fetched design files (any order the caller wants — typically
14
+ * `createdAt` ascending, oldest/"most original" first) and returns every
15
+ * component instance found across all of them, in that same order.
16
+ *
17
+ * Pure — no DB / IO. Callers fetch file rows (respecting `accessFilter`) and
18
+ * pass the rows in here.
19
+ */
20
+
21
+ import {
22
+ buildCodeLayerProjection,
23
+ type CodeLayerSource,
24
+ } from "./code-layer.js";
25
+ import { componentNameFor, isComponentInstance } from "./component-model.js";
26
+
27
+ export interface ComponentLibraryFile {
28
+ id: string;
29
+ designId: string;
30
+ filename: string;
31
+ content: string | null;
32
+ }
33
+
34
+ export interface ComponentLibraryEntry {
35
+ /** Component name from `data-agent-native-component`. */
36
+ name: string;
37
+ fileId: string;
38
+ filename: string;
39
+ /** `data-agent-native-node-id` (or fallback) of the instance root. */
40
+ nodeId: string;
41
+ /** CSS selector that addresses this instance on its screen. */
42
+ selector: string;
43
+ }
44
+
45
+ /**
46
+ * Scan every file for component instances, preserving the caller's file
47
+ * order and each file's document order. Files with empty/missing content are
48
+ * skipped rather than throwing.
49
+ */
50
+ export function scanComponentLibrary(
51
+ files: ComponentLibraryFile[],
52
+ ): ComponentLibraryEntry[] {
53
+ const entries: ComponentLibraryEntry[] = [];
54
+
55
+ for (const file of files) {
56
+ const html = file.content ?? "";
57
+ if (!html) continue;
58
+
59
+ const codeLayerSource: CodeLayerSource = {
60
+ kind: "design-file",
61
+ designId: file.designId,
62
+ fileId: file.id,
63
+ filename: file.filename,
64
+ };
65
+
66
+ const projection = buildCodeLayerProjection(html, {
67
+ source: codeLayerSource,
68
+ });
69
+
70
+ for (const node of projection.nodes) {
71
+ if (!isComponentInstance(node)) continue;
72
+ const name = componentNameFor(node);
73
+ if (!name) continue;
74
+
75
+ // `node.id` is an ephemeral id scoped to this one projection call — it
76
+ // is NOT the same as the durable `data-agent-native-node-id` attribute
77
+ // stamped into the HTML (see `ensureCodeLayerNodeIdsInHtml`). Callers
78
+ // that navigate to / re-select this entry on a LATER read (a different
79
+ // projection call — e.g. after a `navigate` round-trip) need the stable
80
+ // attribute value, not this run's ephemeral id, so prefer it here and
81
+ // only fall back to `node.id` for the rare case a component-annotated
82
+ // node hasn't been stamped with a durable id yet.
83
+ const nodeId =
84
+ node.dataAttributes["data-agent-native-node-id"] ?? node.id;
85
+
86
+ entries.push({
87
+ name,
88
+ fileId: file.id,
89
+ filename: file.filename,
90
+ nodeId,
91
+ selector: node.selector,
92
+ });
93
+ }
94
+ }
95
+
96
+ return entries;
97
+ }
98
+
99
+ /**
100
+ * Filter a scanned library to instances of one component name, optionally
101
+ * excluding a specific (fileId, nodeId) — typically the instance the caller
102
+ * is acting on, so it doesn't get offered back as its own "main"/swap
103
+ * source.
104
+ */
105
+ export function entriesForComponent(
106
+ entries: ComponentLibraryEntry[],
107
+ name: string,
108
+ exclude?: { fileId: string; nodeId: string },
109
+ ): ComponentLibraryEntry[] {
110
+ return entries.filter((entry) => {
111
+ if (entry.name !== name) return false;
112
+ if (
113
+ exclude &&
114
+ entry.fileId === exclude.fileId &&
115
+ entry.nodeId === exclude.nodeId
116
+ ) {
117
+ return false;
118
+ }
119
+ return true;
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Aggregate scanned entries into one row per distinct component name — the
125
+ * shape the Swap Instance picker needs (a name to pick, a count, and a
126
+ * representative instance to source markup/props from).
127
+ */
128
+ export interface ComponentLibrarySummary {
129
+ name: string;
130
+ instanceCount: number;
131
+ sampleFileId: string;
132
+ sampleFilename: string;
133
+ sampleNodeId: string;
134
+ sampleSelector: string;
135
+ }
136
+
137
+ export function summarizeComponentLibrary(
138
+ entries: ComponentLibraryEntry[],
139
+ ): ComponentLibrarySummary[] {
140
+ const byName = new Map<string, ComponentLibrarySummary>();
141
+
142
+ for (const entry of entries) {
143
+ const existing = byName.get(entry.name);
144
+ if (existing) {
145
+ existing.instanceCount += 1;
146
+ continue;
147
+ }
148
+ byName.set(entry.name, {
149
+ name: entry.name,
150
+ instanceCount: 1,
151
+ sampleFileId: entry.fileId,
152
+ sampleFilename: entry.filename,
153
+ sampleNodeId: entry.nodeId,
154
+ sampleSelector: entry.selector,
155
+ });
156
+ }
157
+
158
+ return Array.from(byName.values()).sort((a, b) =>
159
+ a.name.localeCompare(b.name),
160
+ );
161
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * True when `err` is a UNIQUE / PRIMARY KEY constraint violation from either
3
+ * supported driver (Postgres 23505, SQLite SQLITE_CONSTRAINT_UNIQUE /
4
+ * _PRIMARYKEY). Shared by any action that inserts a row guarded by a
5
+ * best-effort unique index and needs to recover from a losing race instead of
6
+ * failing outright — see `design_files_design_filename_unique_idx` in
7
+ * `server/plugins/db.ts` for the index this is meant to catch a conflict
8
+ * against, and `add-localhost-screens.ts` / `present-design-variants.ts` for
9
+ * the adopt-the-winning-row recovery pattern built on top of this check.
10
+ */
11
+ export function isUniqueConstraintViolation(err: unknown): boolean {
12
+ const e = err as { code?: unknown; message?: unknown } | null;
13
+ if (e?.code === "23505") return true;
14
+ const code = String(e?.code ?? "");
15
+ if (
16
+ code === "SQLITE_CONSTRAINT_PRIMARYKEY" ||
17
+ code === "SQLITE_CONSTRAINT_UNIQUE"
18
+ ) {
19
+ return true;
20
+ }
21
+ const msg = String(e?.message ?? "").toLowerCase();
22
+ return (
23
+ msg.includes("unique constraint") ||
24
+ msg.includes("primary key constraint") ||
25
+ msg.includes("duplicate key")
26
+ );
27
+ }
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * Relation to `source-mode.ts`:
9
9
  * - `DesignBridgeOperation` ("select" | "resolveNodeToFile" | "readFile" |
10
- * "applyEdit" | "writeFile" | "captureSnapshot" | "captureState") describes
10
+ * "applyEdit" | "writeFile" | "captureSnapshot" | "captureState" |
11
+ * "listFiles") describes
11
12
  * the low-level bridge RPC surface.
12
13
  * - `DesignCapabilityName` below is the *higher-level* capability vocabulary
13
14
  * that UI panels and agent actions read. Several capabilities build on one
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Shared Figma URL / node-id parsing helpers.
3
+ *
4
+ * Figma file URLs come in a few shapes:
5
+ * https://www.figma.com/design/<fileKey>/<name>
6
+ * https://www.figma.com/file/<fileKey>/<name> (legacy)
7
+ * https://www.figma.com/proto/<fileKey>/<name> (prototype share link)
8
+ * https://www.figma.com/design/<fileKey>/<name>/branch/<branchKey>/<name>
9
+ *
10
+ * The branch shape is the important gotcha: a branch is a *separate* file for
11
+ * REST API purposes. `/v1/files/:fileKey` etc. must be called with the
12
+ * **branch key**, not the parent file key that appears earlier in the path —
13
+ * calling with the parent key silently returns the parent file's content,
14
+ * not the branch the user is actually looking at. See
15
+ * https://developers.figma.com/docs/rest-api/ (branches share the same file
16
+ * shape as regular files once you have the branch's own key).
17
+ *
18
+ * `node-id` is carried as a query param and Figma writes it with dashes
19
+ * (`1-2`) even though the REST API's node ids use colons (`1:2`). Grouped /
20
+ * instance-swap node ids can look like `1:2;3:4` — only the plain
21
+ * `<number>-<number>` shape needs dash-to-colon conversion; anything already
22
+ * containing a colon is passed through untouched.
23
+ */
24
+
25
+ const FIGMA_FILE_KEY_RE = /^[A-Za-z0-9_-]{8,}$/;
26
+ const FILE_PATH_SEGMENTS = ["design", "file", "proto"] as const;
27
+
28
+ export interface ParsedFigmaUrl {
29
+ /** Effective file key to use for REST API calls (branch key when present). */
30
+ fileKey: string | null;
31
+ /** Node id in REST API colon form (e.g. "1:2"), or null when absent. */
32
+ nodeId: string | null;
33
+ /** True when the URL pointed at a branch and fileKey is that branch's key. */
34
+ isBranch: boolean;
35
+ }
36
+
37
+ function candidateFileKey(value: string): string | null {
38
+ return FIGMA_FILE_KEY_RE.test(value) ? value : null;
39
+ }
40
+
41
+ /**
42
+ * Extract the effective Figma file key from a raw file key or a Figma URL.
43
+ * Resolves `/branch/:branchKey/` to the branch key when present, since a
44
+ * branch is a distinct file from the REST API's point of view.
45
+ */
46
+ export function parseFigmaFileKey(input: string | undefined): string | null {
47
+ const value = input?.trim();
48
+ if (!value) return null;
49
+
50
+ const direct = candidateFileKey(value);
51
+ if (direct) return direct;
52
+
53
+ try {
54
+ const url = new URL(value);
55
+ const parts = url.pathname.split("/").filter(Boolean);
56
+
57
+ const branchIndex = parts.indexOf("branch");
58
+ if (branchIndex >= 0) {
59
+ const branchKey = candidateFileKey(parts[branchIndex + 1] ?? "");
60
+ if (branchKey) return branchKey;
61
+ }
62
+
63
+ for (const segment of FILE_PATH_SEGMENTS) {
64
+ const index = parts.indexOf(segment);
65
+ if (index >= 0) {
66
+ const key = candidateFileKey(parts[index + 1] ?? "");
67
+ if (key) return key;
68
+ }
69
+ }
70
+ } catch {
71
+ return null;
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ /** True when the given file URL points at a branch (not the main file). */
78
+ export function isFigmaBranchUrl(input: string | undefined): boolean {
79
+ const value = input?.trim();
80
+ if (!value) return false;
81
+ try {
82
+ const url = new URL(value);
83
+ return url.pathname.split("/").filter(Boolean).includes("branch");
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Normalize a raw node-id-ish string to the REST API's colon form. Accepts:
91
+ * "1:2" -> "1:2" (already normalized)
92
+ * "1-2" -> "1:2" (dash form, as seen in `node-id` query params)
93
+ * "1-2;3-4" -> "1:2;3:4" (instance-swap grouped ids)
94
+ * "I1:2;3:4" -> "I1:2;3:4" (already normalized, leading instance marker)
95
+ */
96
+ function normalizeNodeIdToken(token: string): string | null {
97
+ const trimmed = token.trim();
98
+ if (!trimmed) return null;
99
+ // Already colon form (optionally prefixed with "I" for instance overrides).
100
+ if (/^I?\d+:\d+$/.test(trimmed)) return trimmed;
101
+ // Dash form -> colon form.
102
+ if (/^I?\d+-\d+$/.test(trimmed)) return trimmed.replace("-", ":");
103
+ return null;
104
+ }
105
+
106
+ function normalizeNodeId(raw: string): string | null {
107
+ const decoded = (() => {
108
+ try {
109
+ return decodeURIComponent(raw);
110
+ } catch {
111
+ return raw;
112
+ }
113
+ })();
114
+ const parts = decoded
115
+ .split(";")
116
+ .map((part) => normalizeNodeIdToken(part))
117
+ .filter((part): part is string => part !== null);
118
+ return parts.length > 0 ? parts.join(";") : null;
119
+ }
120
+
121
+ /**
122
+ * Extract a Figma node id from a `node-id` query param or a raw id string,
123
+ * converting Figma's URL dash form (`1-2`) to the REST API's colon form
124
+ * (`1:2`).
125
+ */
126
+ export function parseFigmaNodeId(input: string | undefined): string | null {
127
+ const value = input?.trim();
128
+ if (!value) return null;
129
+
130
+ const direct = normalizeNodeId(value);
131
+ if (direct) return direct;
132
+
133
+ try {
134
+ const url = new URL(value);
135
+ const nodeParam = url.searchParams.get("node-id");
136
+ if (nodeParam) return normalizeNodeId(nodeParam);
137
+ } catch {
138
+ return null;
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ /** Parse a Figma URL (or bare file key) into its effective file key and node id. */
145
+ export function parseFigmaUrl(input: string | undefined): ParsedFigmaUrl {
146
+ const fileKey = parseFigmaFileKey(input);
147
+ const nodeId = parseFigmaNodeId(input);
148
+ return { fileKey, nodeId, isBranch: isFigmaBranchUrl(input) };
149
+ }
@@ -115,13 +115,22 @@ export function snapPenAnchorPoint(
115
115
  path: PenPath | null,
116
116
  options: { hitRadius: number; zoom: number },
117
117
  ): PenPoint {
118
- const existingAnchor = path?.nodes.find(
119
- (node) =>
120
- Math.hypot(node.point.x - point.x, node.point.y - point.y) <=
121
- options.hitRadius,
122
- );
123
- if (existingAnchor) {
124
- return { ...existingAnchor.point };
118
+ // Nearest anchor within radius wins (matching hitTestPenAnchor), not the
119
+ // first one found in node order — two anchors can easily both sit within
120
+ // the hit radius (e.g. a tightly drawn shape), and a "first match" scan
121
+ // would snap to whichever happens to have the lower node index rather than
122
+ // the one actually closest to the cursor.
123
+ let nearestAnchor: PenPoint | null = null;
124
+ let nearestDistance = Infinity;
125
+ for (const node of path?.nodes ?? []) {
126
+ const distance = Math.hypot(node.point.x - point.x, node.point.y - point.y);
127
+ if (distance <= options.hitRadius && distance < nearestDistance) {
128
+ nearestDistance = distance;
129
+ nearestAnchor = node.point;
130
+ }
131
+ }
132
+ if (nearestAnchor) {
133
+ return { ...nearestAnchor };
125
134
  }
126
135
 
127
136
  if (options.zoom >= 100) {
@@ -97,6 +97,7 @@ export const DESIGN_BRIDGE_OPERATIONS = [
97
97
  "writeFile",
98
98
  "captureSnapshot",
99
99
  "captureState",
100
+ "listFiles",
100
101
  ] as const;
101
102
 
102
103
  export type DesignBridgeOperation = (typeof DESIGN_BRIDGE_OPERATIONS)[number];
@@ -41,8 +41,6 @@ const useIsomorphicLayoutEffect =
41
41
  const DEFAULT_VIEW = { zoom: 0.72, pan: { x: 96, y: 64 } };
42
42
  const MIN_ZOOM = 0.18;
43
43
  const MAX_ZOOM = 2.4;
44
- /** Notched mouse-wheel fixed-ratio step (design-canvas.jsx feel). */
45
- const WHEEL_ZOOM_STEP = 0.16;
46
44
  /** Trackpad pinch sensitivity. */
47
45
  const PINCH_ZOOM_SENSITIVITY = 0.01;
48
46
  /** Base CSS grid cell, scaled by zoom. */
@@ -534,9 +532,11 @@ export function CanvasArea({
534
532
  [buildMarkupContext, onCanvasMarkupCreate, pendingMarkup],
535
533
  );
536
534
 
537
- // Wheel: cursor-over-canvas never scrolls the page. Notched wheel zooms with
538
- // a fixed ratio per click; ctrl/cmd/alt (or trackpad pinch) zoom at the
539
- // cursor; everything else pans.
535
+ // Wheel: cursor-over-canvas never scrolls the page. Match Figma's input
536
+ // contract: unmodified wheel/trackpad gestures always pan, while an explicit
537
+ // ctrl/cmd modifier (including the ctrlKey emitted by trackpad pinch) zooms
538
+ // at the cursor. Never infer zoom intent from the delta shape — trackpad pan
539
+ // momentum can look exactly like a notched mouse wheel near a canvas edge.
540
540
  useEffect(() => {
541
541
  const element = viewportRef.current;
542
542
  if (!element) return;
@@ -559,24 +559,12 @@ export function CanvasArea({
559
559
  const deltaX = event.deltaX * lineScale;
560
560
  const deltaY = event.deltaY * lineScale;
561
561
 
562
- // Notched mouse wheel: line-mode, or large integer pixel deltas with no
563
- // horizontal component (Chrome/Safari). Fixed-ratio step per click.
564
- const isNotchedWheel =
565
- event.deltaMode !== 0 ||
566
- (event.deltaX === 0 &&
567
- Number.isInteger(event.deltaY) &&
568
- Math.abs(event.deltaY) >= 40);
569
-
570
- if (event.ctrlKey || event.metaKey || event.altKey) {
562
+ if (event.ctrlKey || event.metaKey) {
571
563
  // Trackpad pinch / explicit zoom modifier — smooth exponential.
572
564
  zoomByFactor(Math.exp(-deltaY * PINCH_ZOOM_SENSITIVITY), anchor);
573
565
  return;
574
566
  }
575
- if (isNotchedWheel) {
576
- zoomByFactor(Math.exp(-Math.sign(deltaY) * WHEEL_ZOOM_STEP), anchor);
577
- return;
578
- }
579
- // Trackpad two-finger scroll → pan.
567
+ // Mouse wheel / trackpad two-finger scroll -> pan.
580
568
  updateView((current) => ({
581
569
  ...current,
582
570
  pan: {
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-09
4
+ ---
5
+
6
+ Canvas panning now keeps zoom fixed, even at the edges.
@@ -13,8 +13,23 @@ import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-sys
13
13
 
14
14
  const codeFileSchema = z.object({
15
15
  filename: z.string().trim().min(1).describe("File name or relative path"),
16
- content: z.string().describe("Raw text content of the code/design file"),
16
+ content: z
17
+ .string()
18
+ .describe(
19
+ "File content. Text files (code, CSS, markdown, JSON, SVG): raw text. " +
20
+ "Binary files -- most importantly `.fig` (a zip/kiwi binary container, " +
21
+ 'never valid text) -- MUST be base64-encoded with encoding: "base64"; ' +
22
+ "sending raw/binary-as-string content with the default utf8 encoding " +
23
+ "corrupts the file before it reaches Builder.",
24
+ ),
17
25
  mimeType: z.string().trim().optional().describe("Optional MIME type"),
26
+ encoding: z
27
+ .enum(["utf8", "base64"])
28
+ .optional()
29
+ .describe(
30
+ "Encoding of `content`. Defaults to utf8. Set to base64 for `.fig` and " +
31
+ "other binary files.",
32
+ ),
18
33
  });
19
34
 
20
35
  export default defineAction({