@agent-native/core 0.92.10 → 0.92.12

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 (354) 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 +214 -17
  5. package/corpus/core/src/agent/run-manager.ts +72 -14
  6. package/corpus/core/src/agent/run-store.ts +152 -27
  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-adapter.ts +56 -12
  12. package/corpus/core/src/client/agent-chat.ts +165 -1
  13. package/corpus/core/src/client/chat/index.ts +9 -1
  14. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +38 -2
  15. package/corpus/core/src/client/index.ts +5 -0
  16. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +18 -7
  17. package/corpus/core/src/collab/awareness-store.ts +23 -12
  18. package/corpus/core/src/collab/client.ts +16 -5
  19. package/corpus/core/src/collab/index.ts +4 -0
  20. package/corpus/core/src/collab/recent-edits.ts +65 -2
  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 +75 -9
  33. package/corpus/templates/clips/app/components/player/video-player.tsx +17 -12
  34. package/corpus/templates/clips/app/hooks/use-player-shortcuts.ts +1 -0
  35. package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
  36. package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
  37. package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
  38. package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
  39. package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
  40. package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
  41. package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
  42. package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
  43. package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
  44. package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
  45. package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
  46. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -37
  47. package/corpus/templates/clips/app/routes/record.tsx +17 -4
  48. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  49. package/corpus/templates/clips/changelog/2026-07-09-desktop-popover-sits-closer-to-the-menu-bar.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-09-desktop-recording-optimization-progress-stays-visible-until-the-clip-opens.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-09-fixed-large-video-file-uploads-failing-before-their-resumabl.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-09-fixed-new-video-uploads-defaulting-to-private.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-07-09-long-recordings-now-get-enough-time-to-download-their-media-.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-09-meeting-notes-no-longer-lowers-your-microphone-volume-for-ot.md +6 -0
  55. package/corpus/templates/clips/changelog/2026-07-09-on-mobile-tapping-a-playing-clip-reveals-playback-controls-w.md +6 -0
  56. package/corpus/templates/clips/changelog/2026-07-09-regenerate-any-clip-transcript-from-its-saved-recording-or-r.md +6 -0
  57. package/corpus/templates/clips/changelog/2026-07-09-slack-previews-now-show-the-clip-length-alongside-its-descri.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-07-10-fixed-copying-agent-responses-and-transcript-text-from-recor.md +6 -0
  59. package/corpus/templates/clips/desktop/src/app.tsx +12 -2
  60. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +8 -1
  61. package/corpus/templates/clips/desktop/src/lib/recorder.ts +8 -5
  62. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +5 -5
  63. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +3 -0
  64. package/corpus/templates/clips/desktop/src/styles.css +6 -3
  65. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +29 -11
  66. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  67. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +103 -21
  68. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +117 -39
  69. package/corpus/templates/clips/server/lib/slack-unfurls.ts +20 -1
  70. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +10 -4
  71. package/corpus/templates/clips/shared/recording-core.ts +12 -0
  72. package/corpus/templates/content/AGENTS.md +7 -6
  73. package/corpus/templates/content/actions/_builder-cms-read-client.ts +82 -7
  74. package/corpus/templates/content/actions/_database-source-utils.ts +149 -30
  75. package/corpus/templates/content/actions/_document-flush.ts +132 -17
  76. package/corpus/templates/content/actions/_notion-action-utils.ts +15 -0
  77. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +4 -7
  78. package/corpus/templates/content/actions/attach-content-database-source.ts +41 -17
  79. package/corpus/templates/content/actions/change-content-database-source-role.ts +46 -8
  80. package/corpus/templates/content/actions/create-and-link-notion-page.ts +2 -0
  81. package/corpus/templates/content/actions/link-notion-page.ts +2 -0
  82. package/corpus/templates/content/actions/pull-document.ts +5 -7
  83. package/corpus/templates/content/actions/pull-notion-page.ts +2 -0
  84. package/corpus/templates/content/actions/push-notion-page.ts +10 -0
  85. package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +2 -0
  86. package/corpus/templates/content/actions/suggest-source-join-key.ts +8 -1
  87. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +88 -23
  88. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +32 -2
  89. package/corpus/templates/content/app/hooks/use-notion.ts +6 -6
  90. package/corpus/templates/content/app/i18n/zh-TW.ts +1 -0
  91. package/corpus/templates/content/app/i18n-data.ts +18 -0
  92. package/corpus/templates/content/changelog/2026-07-09-builder-source-refreshes-now-keep-mapped-fields-such-as-topi.md +6 -0
  93. package/corpus/templates/content/changelog/2026-07-09-notion-conflict-choices-and-version-restores-preserve-docume.md +6 -0
  94. package/corpus/templates/content/server/lib/notion-sync.ts +80 -64
  95. package/corpus/templates/content/shared/api.ts +1 -0
  96. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +29 -2
  97. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +86 -0
  98. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +57 -0
  99. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +11 -0
  100. package/corpus/templates/design/AGENTS.md +45 -10
  101. package/corpus/templates/design/actions/add-localhost-screens.ts +66 -13
  102. package/corpus/templates/design/actions/apply-component-prop-edit.ts +45 -36
  103. package/corpus/templates/design/actions/apply-motion-edit.ts +25 -31
  104. package/corpus/templates/design/actions/apply-shader-fill.ts +29 -42
  105. package/corpus/templates/design/actions/detach-component-instance.ts +337 -0
  106. package/corpus/templates/design/actions/export-design-as-figma-svg.ts +219 -0
  107. package/corpus/templates/design/actions/generate-design.ts +79 -12
  108. package/corpus/templates/design/actions/generate-screens.ts +57 -12
  109. package/corpus/templates/design/actions/get-figma-styles.ts +115 -0
  110. package/corpus/templates/design/actions/go-to-main-component.ts +254 -0
  111. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +4 -3
  112. package/corpus/templates/design/actions/import-design-source.ts +4 -31
  113. package/corpus/templates/design/actions/import-figma-clipboard.ts +134 -0
  114. package/corpus/templates/design/actions/import-figma-frame.ts +95 -0
  115. package/corpus/templates/design/actions/index-design-system-with-builder.ts +16 -1
  116. package/corpus/templates/design/actions/list-design-components.ts +104 -0
  117. package/corpus/templates/design/actions/list-figma-library-assets.ts +1 -24
  118. package/corpus/templates/design/actions/open-component-source.ts +2 -4
  119. package/corpus/templates/design/actions/present-design-variants.ts +412 -16
  120. package/corpus/templates/design/actions/preview-source-edit.ts +14 -1
  121. package/corpus/templates/design/actions/run-design-audit.ts +25 -3
  122. package/corpus/templates/design/actions/show-design-questions.ts +10 -0
  123. package/corpus/templates/design/actions/swap-component-instance.ts +544 -0
  124. package/corpus/templates/design/actions/take-design-screenshot.ts +45 -60
  125. package/corpus/templates/design/actions/write-local-file.ts +25 -1
  126. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +152 -0
  127. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1077 -58
  128. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +5 -5
  129. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +73 -21
  130. package/corpus/templates/design/app/components/design/EditPanel.tsx +16 -11
  131. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +364 -0
  132. package/corpus/templates/design/app/components/design/LayersPanel.tsx +180 -53
  133. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +0 -2
  134. package/corpus/templates/design/app/components/design/MotionDock.tsx +144 -21
  135. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1152 -167
  136. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +27 -5
  137. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +23 -2
  138. package/corpus/templates/design/app/components/design/TokensPanel.tsx +63 -49
  139. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1293 -54
  140. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +7 -0
  141. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +6 -1
  142. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +23 -6
  143. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +7 -0
  144. package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -0
  145. package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +14 -2
  146. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +37 -2
  147. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open-guard.ts +25 -0
  148. package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +19 -1
  149. package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +43 -5
  150. package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +27 -2
  151. package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +10 -1
  152. package/corpus/templates/design/app/components/design/code-workbench/store.tsx +19 -2
  153. package/corpus/templates/design/app/components/design/design-canvas/annotation-snapshot.ts +265 -0
  154. package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +16 -5
  155. package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +16 -2
  156. package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +7 -2
  157. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +3 -1
  158. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +78 -8
  159. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +451 -73
  160. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +12 -4
  161. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +398 -272
  162. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +41 -7
  163. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +16 -3
  164. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +24 -4
  165. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +222 -4
  166. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +171 -78
  167. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +45 -5
  168. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +299 -28
  169. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +120 -150
  170. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +53 -1
  171. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +284 -117
  172. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +81 -0
  173. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +19 -14
  174. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +7 -0
  175. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +155 -0
  176. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +268 -46
  177. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +350 -31
  178. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +13 -4
  179. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +233 -23
  180. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +72 -2
  181. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +51 -11
  182. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +84 -11
  183. package/corpus/templates/design/app/components/design/inspector/index.ts +3 -0
  184. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +552 -0
  185. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +157 -0
  186. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +53 -0
  187. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +184 -0
  188. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +403 -26
  189. package/corpus/templates/design/app/components/design/multi-screen/types.ts +20 -1
  190. package/corpus/templates/design/app/components/design/types.ts +33 -0
  191. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +59 -7
  192. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +119 -17
  193. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +275 -63
  194. package/corpus/templates/design/app/hooks/use-question-flow.ts +14 -3
  195. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +67 -10
  196. package/corpus/templates/design/app/i18n/zh-TW.ts +91 -1
  197. package/corpus/templates/design/app/i18n-data.ts +1217 -10
  198. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +577 -0
  199. package/corpus/templates/design/app/lib/agent-chat.ts +23 -0
  200. package/corpus/templates/design/app/lib/design-import.ts +24 -0
  201. package/corpus/templates/design/app/lib/figma-clipboard.ts +157 -0
  202. package/corpus/templates/design/app/lib/figma-svg-copy.ts +221 -0
  203. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +143 -30
  204. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +15 -0
  205. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +33 -0
  206. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +38 -0
  207. package/corpus/templates/design/app/pages/design-editor/overview-annotation-context.ts +157 -0
  208. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +62 -6
  209. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +245 -0
  210. package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +586 -0
  211. package/corpus/templates/design/app/pages/design-editor/screen-auto-layout.ts +169 -0
  212. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +201 -1
  213. package/corpus/templates/design/changelog/2026-07-09-added-underline-strikethrough-and-text-case-uppercase-lowerc.md +6 -0
  214. package/corpus/templates/design/changelog/2026-07-09-annotate-now-draws-across-the-all-screens-canvas-without-swi.md +6 -0
  215. package/corpus/templates/design/changelog/2026-07-09-auto-layout-children-can-now-be-dragged-into-freeform-screen.md +6 -0
  216. package/corpus/templates/design/changelog/2026-07-09-board-shapes-now-stay-visible-and-nest-correctly-across-the-.md +6 -0
  217. package/corpus/templates/design/changelog/2026-07-09-component-source-navigation-now-opens-reliably-from-the-insp.md +6 -0
  218. package/corpus/templates/design/changelog/2026-07-09-cross-screen-local-react-layer-moves-now-hand-off-exact-sour.md +6 -0
  219. package/corpus/templates/design/changelog/2026-07-09-cross-screen-moves-undo-and-redo-now-keep-both-screens-stabl.md +6 -0
  220. package/corpus/templates/design/changelog/2026-07-09-detach-a-component-instance-into-plain-editable-eleme.md +6 -0
  221. package/corpus/templates/design/changelog/2026-07-09-dragging-layers-into-auto-layout-now-respects-ignore-auto-la.md +6 -0
  222. package/corpus/templates/design/changelog/2026-07-09-drawing-annotations-sent-to-the-agent-now-include-a-rendered.md +6 -0
  223. package/corpus/templates/design/changelog/2026-07-09-editor-sidebars-now-use-figma-matched-default-widths-for-a-r.md +6 -0
  224. package/corpus/templates/design/changelog/2026-07-09-empty-frames-and-rectangles-now-expose-full-auto-layout-cont.md +6 -0
  225. package/corpus/templates/design/changelog/2026-07-09-export-any-design-screen-as-a-real-vector-svg-for-figma-with.md +6 -0
  226. package/corpus/templates/design/changelog/2026-07-09-grid-auto-layout-now-uses-true-responsive-rows-and-columns-w.md +6 -0
  227. package/corpus/templates/design/changelog/2026-07-09-hidden-and-locked-screens-now-behave-consistently-between-la.md +6 -0
  228. package/corpus/templates/design/changelog/2026-07-09-image-fills-now-preserve-valid-urls-and-layered-fit-settings.md +6 -0
  229. package/corpus/templates/design/changelog/2026-07-09-import-figma-frames-as-pixel-accurate-design-screens-via-a-s.md +6 -0
  230. package/corpus/templates/design/changelog/2026-07-09-inspect-code-now-hides-internal-styling-metadata-and-wraps-l.md +6 -0
  231. package/corpus/templates/design/changelog/2026-07-09-inspector-blur-and-shadow-effects-now-preserve-other-effects.md +6 -0
  232. package/corpus/templates/design/changelog/2026-07-09-jump-to-a-components-other-instances-from-the-inspecto.md +6 -0
  233. package/corpus/templates/design/changelog/2026-07-09-keyboard-shortcuts-are-now-discoverable-in-a-figma-style-bot.md +6 -0
  234. package/corpus/templates/design/changelog/2026-07-09-layer-selection-now-keeps-cmd-or-ctrl-toggles-and-shift-rang.md +6 -0
  235. package/corpus/templates/design/changelog/2026-07-09-layers-dropped-into-auto-layout-now-participate-correctly-in.md +6 -0
  236. package/corpus/templates/design/changelog/2026-07-09-local-live-edit-screens-now-enter-and-leave-full-view-withou.md +6 -0
  237. package/corpus/templates/design/changelog/2026-07-09-local-preview-urls-now-live-in-the-inspector-instead-of-taki.md +6 -0
  238. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-can-now-be-hidden-and-locked-from-layers-.md +6 -0
  239. package/corpus/templates/design/changelog/2026-07-09-local-react-layers-now-keep-exact-source-provenance-and-supp.md +6 -0
  240. package/corpus/templates/design/changelog/2026-07-09-local-react-visual-editing-now-loads-reliably-and-shows-live.md +6 -0
  241. package/corpus/templates/design/changelog/2026-07-09-paste-from-figma-cmd-v-now-imports-exact-editable-nodes-when.md +6 -0
  242. package/corpus/templates/design/changelog/2026-07-09-pressing-escape-after-selecting-a-screen-now-reveals-page-pr.md +6 -0
  243. package/corpus/templates/design/changelog/2026-07-09-right-click-overlapping-or-nested-objects-to-choose-the-exac.md +6 -0
  244. package/corpus/templates/design/changelog/2026-07-09-scale-edge-stretch-and-center-constraints-now-preserve-layer.md +6 -0
  245. package/corpus/templates/design/changelog/2026-07-09-screen-rows-can-now-be-reordered-in-layers-with-matching-can.md +6 -0
  246. package/corpus/templates/design/changelog/2026-07-09-screen-selection-stays-responsive-in-designs-with-hundreds-o.md +6 -0
  247. package/corpus/templates/design/changelog/2026-07-09-shift-a-now-enables-auto-layout-on-a-selected-screen-with-fl.md +6 -0
  248. package/corpus/templates/design/changelog/2026-07-09-swap-a-component-instance-for-another-from-the-libra.md +6 -0
  249. package/corpus/templates/design/changelog/2026-07-10-drawing-annotations-now-stay-available-to-retry-when-the.md +6 -0
  250. package/corpus/templates/design/e2e/global-setup.ts +3 -6
  251. package/corpus/templates/design/e2e/helpers.ts +27 -6
  252. package/corpus/templates/design/server/lib/design-to-figma-svg.ts +1412 -0
  253. package/corpus/templates/design/server/lib/figma-clipboard-match.ts +196 -0
  254. package/corpus/templates/design/server/lib/figma-node-import.ts +311 -0
  255. package/corpus/templates/design/server/lib/figma-node-to-html.ts +1334 -0
  256. package/corpus/templates/design/server/lib/figma-paste-fallback.ts +64 -0
  257. package/corpus/templates/design/server/lib/playwright-runtime.ts +78 -0
  258. package/corpus/templates/design/server/plugins/db.ts +41 -0
  259. package/corpus/templates/design/server/source-workspace.ts +64 -0
  260. package/corpus/templates/design/shared/board-file.ts +13 -2
  261. package/corpus/templates/design/shared/canvas-math.ts +231 -33
  262. package/corpus/templates/design/shared/code-layer.ts +122 -8
  263. package/corpus/templates/design/shared/component-library.ts +161 -0
  264. package/corpus/templates/design/shared/db-conflict.ts +27 -0
  265. package/corpus/templates/design/shared/design-source-capabilities.ts +2 -1
  266. package/corpus/templates/design/shared/figma-url.ts +149 -0
  267. package/corpus/templates/design/shared/pen-path.ts +16 -7
  268. package/corpus/templates/design/shared/source-mode.ts +1 -0
  269. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +7 -19
  270. package/corpus/templates/plan/changelog/2026-07-09-canvas-panning-now-keeps-zoom-fixed-even-at-the-edges.md +6 -0
  271. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +16 -1
  272. package/corpus/templates/slides/app/context/DeckContext.tsx +311 -70
  273. package/corpus/templates/slides/changelog/2026-07-09-fixed-the-slide-rail-going-permanently-stale-during-a-long-a.md +6 -0
  274. package/dist/agent/production-agent.d.ts +82 -7
  275. package/dist/agent/production-agent.d.ts.map +1 -1
  276. package/dist/agent/production-agent.js +180 -17
  277. package/dist/agent/production-agent.js.map +1 -1
  278. package/dist/agent/run-manager.d.ts.map +1 -1
  279. package/dist/agent/run-manager.js +63 -14
  280. package/dist/agent/run-manager.js.map +1 -1
  281. package/dist/agent/run-store.d.ts +46 -0
  282. package/dist/agent/run-store.d.ts.map +1 -1
  283. package/dist/agent/run-store.js +129 -22
  284. package/dist/agent/run-store.js.map +1 -1
  285. package/dist/cli/design-connect.d.ts +6 -0
  286. package/dist/cli/design-connect.d.ts.map +1 -1
  287. package/dist/cli/design-connect.js +336 -139
  288. package/dist/cli/design-connect.js.map +1 -1
  289. package/dist/cli/skills.d.ts.map +1 -1
  290. package/dist/cli/skills.js +14 -0
  291. package/dist/cli/skills.js.map +1 -1
  292. package/dist/client/AssistantChat.d.ts +2 -0
  293. package/dist/client/AssistantChat.d.ts.map +1 -1
  294. package/dist/client/AssistantChat.js +19 -3
  295. package/dist/client/AssistantChat.js.map +1 -1
  296. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  297. package/dist/client/MultiTabAssistantChat.js +38 -18
  298. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  299. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  300. package/dist/client/agent-chat-adapter.js +48 -12
  301. package/dist/client/agent-chat-adapter.js.map +1 -1
  302. package/dist/client/agent-chat.d.ts +60 -0
  303. package/dist/client/agent-chat.d.ts.map +1 -1
  304. package/dist/client/agent-chat.js +122 -1
  305. package/dist/client/agent-chat.js.map +1 -1
  306. package/dist/client/chat/index.d.ts +1 -1
  307. package/dist/client/chat/index.d.ts.map +1 -1
  308. package/dist/client/chat/index.js +1 -1
  309. package/dist/client/chat/index.js.map +1 -1
  310. package/dist/client/extensions/EmbeddedExtension.d.ts +12 -0
  311. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  312. package/dist/client/extensions/EmbeddedExtension.js +36 -2
  313. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  314. package/dist/client/index.d.ts +1 -1
  315. package/dist/client/index.d.ts.map +1 -1
  316. package/dist/client/index.js +1 -1
  317. package/dist/client/index.js.map +1 -1
  318. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  319. package/dist/client/rich-markdown-editor/useCollabReconcile.js +18 -6
  320. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  321. package/dist/collab/awareness-store.d.ts +5 -0
  322. package/dist/collab/awareness-store.d.ts.map +1 -1
  323. package/dist/collab/awareness-store.js +19 -12
  324. package/dist/collab/awareness-store.js.map +1 -1
  325. package/dist/collab/awareness.d.ts +2 -2
  326. package/dist/collab/awareness.d.ts.map +1 -1
  327. package/dist/collab/client.d.ts.map +1 -1
  328. package/dist/collab/client.js +13 -5
  329. package/dist/collab/client.js.map +1 -1
  330. package/dist/collab/index.d.ts +1 -0
  331. package/dist/collab/index.d.ts.map +1 -1
  332. package/dist/collab/index.js +1 -0
  333. package/dist/collab/index.js.map +1 -1
  334. package/dist/collab/recent-edits.d.ts +3 -1
  335. package/dist/collab/recent-edits.d.ts.map +1 -1
  336. package/dist/collab/recent-edits.js +57 -2
  337. package/dist/collab/recent-edits.js.map +1 -1
  338. package/dist/collab/struct-routes.d.ts +1 -1
  339. package/dist/index.browser.d.ts +1 -1
  340. package/dist/index.browser.d.ts.map +1 -1
  341. package/dist/index.browser.js +1 -1
  342. package/dist/index.browser.js.map +1 -1
  343. package/dist/observability/routes.d.ts +5 -5
  344. package/dist/progress/routes.d.ts +1 -1
  345. package/dist/resources/handlers.d.ts +3 -3
  346. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  347. package/dist/server/agent-chat-plugin.js +87 -10
  348. package/dist/server/agent-chat-plugin.js.map +1 -1
  349. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  350. package/dist/server/builder-design-systems.d.ts +15 -0
  351. package/dist/server/builder-design-systems.d.ts.map +1 -1
  352. package/dist/server/builder-design-systems.js +9 -4
  353. package/dist/server/builder-design-systems.js.map +1 -1
  354. package/package.json +1 -1
@@ -14,6 +14,7 @@ import {
14
14
  createSmoothNode,
15
15
  isPenCloseTarget,
16
16
  serializePenPath,
17
+ translatePenPath,
17
18
  type PenPath,
18
19
  type PenPoint,
19
20
  } from "@shared/pen-path";
@@ -31,7 +32,7 @@ import {
31
32
  CanvasCommentPins,
32
33
  type CanvasPin,
33
34
  } from "@/components/visual-editor";
34
- import { sendToDesignAgentChat } from "@/lib/agent-chat";
35
+ import { sendToDesignAgentChatAndConfirm } from "@/lib/agent-chat";
35
36
  import { cn } from "@/lib/utils";
36
37
 
37
38
  import { editorChromeBridgeScript } from "../../../.generated/bridge/editor-chrome.generated";
@@ -43,6 +44,7 @@ import { shaderRuntimeBridgeScript } from "../../../.generated/bridge/shader-run
43
44
  import { tweakBridgeScript } from "../../../.generated/bridge/tweak.generated";
44
45
  import { zoomBridgeScript } from "../../../.generated/bridge/zoom.generated";
45
46
  import { isTrustedCanvasBridgeMessage } from "./bridge-security";
47
+ import { captureAnnotatedScreenshot } from "./design-canvas/annotation-snapshot";
46
48
  import { submitDesignAnnotations } from "./design-canvas/annotation-submit";
47
49
  import {
48
50
  getScreenContentPointFromClient,
@@ -55,6 +57,7 @@ import {
55
57
  } from "./design-canvas/creation";
56
58
  import { isElementInfoPayload } from "./design-canvas/element-payload";
57
59
  import {
60
+ embeddedContentOffsetCss,
58
61
  embeddedContentOffsetStyle,
59
62
  getEmbeddedFrameBackgroundStyle,
60
63
  getEmbeddedFrameDocumentContent,
@@ -91,6 +94,7 @@ import type {
91
94
  ElementInfo,
92
95
  ElementSelectionIntent,
93
96
  DeviceFrameType,
97
+ RuntimeStructureMoveRequest,
94
98
  } from "./types";
95
99
 
96
100
  /**
@@ -320,6 +324,10 @@ interface DesignCanvasProps {
320
324
  status?: number;
321
325
  contentType?: string;
322
326
  }) => void;
327
+ onRuntimeLayerSnapshot?: (snapshot: {
328
+ html: string;
329
+ nodeCount: number;
330
+ }) => void;
323
331
  /**
324
332
  * Explicit Builder-hosted app URL for fusion source rendering.
325
333
  *
@@ -381,6 +389,8 @@ interface DesignCanvasProps {
381
389
  requestId: number;
382
390
  acks: Array<{ requestId: string; applied: boolean }>;
383
391
  } | null;
392
+ /** One-shot host request to optimistically move a runtime-only layer. */
393
+ runtimeStructureMoveRequest?: RuntimeStructureMoveRequest | null;
384
394
  embeddedFrameBackground?: string;
385
395
  transparentBackground?: boolean;
386
396
  editorChromeScaleX?: number;
@@ -432,8 +442,10 @@ interface DesignCanvasProps {
432
442
  anchorSourceId?: string;
433
443
  requestId?: string;
434
444
  dropMode?: "flow-insert" | "absolute-container";
445
+ forceFlowPositionOverride?: boolean;
435
446
  sourceRect?: { x: number; y: number; width: number; height: number };
436
447
  anchorRect?: { x: number; y: number; width: number; height: number };
448
+ anchorElementInfo?: ElementInfo;
437
449
  },
438
450
  ) => boolean | "pending" | void;
439
451
  onVisualDuplicateChange?: (
@@ -452,6 +464,20 @@ interface DesignCanvasProps {
452
464
  drawMode?: boolean;
453
465
  /** Called when the user exits draw mode (X / Escape / after Send). */
454
466
  onExitDrawMode?: () => void;
467
+ /**
468
+ * Bumped by the parent exactly when the user deliberately discards the
469
+ * current annotation batch (the same moment `onExitDrawMode` fires from
470
+ * the overlay's own X button or a confirmed Send). SharedDrawOverlay only
471
+ * clears its strokes/text annotations when this changes — NOT merely
472
+ * because `drawMode` toggled off for some other reason (switching tools,
473
+ * views, or side panels) — so unsent annotation work survives an
474
+ * unrelated exit instead of being silently discarded.
475
+ */
476
+ drawOverlayResetSignal?: number;
477
+ /** Keeps the active focused overlay bitmap alive across mode/view hides. */
478
+ retainDrawOverlayWhenHidden?: boolean;
479
+ /** Reports focused annotation delivery state so global Escape stays inert too. */
480
+ onAnnotationSendingChange?: (sending: boolean) => void;
455
481
  /** Whether comment-pin drop mode is active. */
456
482
  pinMode?: boolean;
457
483
  /**
@@ -685,6 +711,120 @@ function snapshotEndpointUrl(bridgeUrl: string, previewUrl: string): string {
685
711
  return endpoint.toString();
686
712
  }
687
713
 
714
+ function healthEndpointUrl(bridgeUrl: string): string {
715
+ return new URL("/health", bridgeUrl).toString();
716
+ }
717
+
718
+ /**
719
+ * Classifies a suspected `unknown-bridge-key` failure on the localhost
720
+ * live-edit bridge, based on a `/health` probe fired when the
721
+ * "agent-native:editor-chrome-ready" handshake hasn't arrived in time.
722
+ *
723
+ * The bridge mints a fresh `bridgeInstanceId` every time its process boots
724
+ * (see `startDesignConnectBridge` in packages/core/src/cli/design-connect.ts)
725
+ * and echoes it on `/health`, on a successful `/live-edit-bridge`
726
+ * registration, and on the "unknown bridge key" 409 from `/live-edit`. The
727
+ * 409 itself is usually unreadable here: the live iframe navigates directly
728
+ * to the bridge's `/live-edit` URL (a real cross-origin request), so this
729
+ * component can't inspect the response body or status code the way a
730
+ * same-origin `fetch` could. Instead, callers probe `/health` — a route that
731
+ * never requires the preview token — and pass its `bridgeInstanceId` in as
732
+ * `responseBridgeInstanceId`.
733
+ *
734
+ * - Different id than what we last registered with ⇒ the bridge PROCESS
735
+ * restarted (crash, machine sleep/wake, manual restart) since our last
736
+ * successful registration. That's not a caller bug: silently re-POST the
737
+ * same script/key and reload the frame with no user-facing error
738
+ * ("reregister").
739
+ * - Same id ⇒ the process that accepted our registration is still running
740
+ * and reachable — the iframe just hasn't finished loading yet (e.g. a
741
+ * 6-10s cold dev-server compile), NOT a genuine failure. Callers must NOT
742
+ * tear the iframe down for this outcome: re-arm the ready-handshake
743
+ * watchdog with a longer wait instead ("escalate") — see
744
+ * handleSuspectedBridgeRestart's escalation loop, which only gives up and
745
+ * shows a (non-destructive) error after a generous total ceiling.
746
+ * - No id at all (missing from either side) ⇒ inconclusive — `/health`
747
+ * responded but we can't confirm process identity either way. Treat this
748
+ * like a real, unresolved failure and surface the existing error/Retry UI
749
+ * ("error").
750
+ *
751
+ * Deliberately NOT exported: every other pure helper in this file
752
+ * (getExternalPreviewUrl, snapshotEndpointUrl, buildEditorChromeBridgeScript,
753
+ * contentHash, isAllowedFusionOrigin, originFromUrl, ...) stays module-private
754
+ * so DesignCanvas.tsx keeps exporting only the DesignCanvas component itself
755
+ * — see DesignCanvas.refreshBoundary.test.ts, which guards the Fast Refresh
756
+ * boundary this file relies on given how often it's edited during live design
757
+ * sessions. Its decision logic is covered by behavioral tests in
758
+ * DesignCanvas.bridge-restart.test.tsx instead of a direct unit import.
759
+ */
760
+ function classifyLiveEditHealthProbe(
761
+ cachedBridgeInstanceId: string | null | undefined,
762
+ responseBridgeInstanceId: string | null | undefined,
763
+ ): "reregister" | "escalate" | "error" {
764
+ if (!cachedBridgeInstanceId || !responseBridgeInstanceId) return "error";
765
+ return cachedBridgeInstanceId === responseBridgeInstanceId
766
+ ? "escalate"
767
+ : "reregister";
768
+ }
769
+
770
+ // Grace period after the live-edit bridge is (re)registered and the real
771
+ // iframe document is mounted before a missing "agent-native:editor-chrome-ready"
772
+ // handshake is treated as a suspected bridge restart rather than an in-flight
773
+ // page load. Generous relative to typical localhost load times so a slow but
774
+ // healthy dev server is never misdiagnosed as restarted.
775
+ const LIVE_EDIT_READY_TIMEOUT_MS = 4000;
776
+ // Caps the silent auto-reregister loop below so a pathological bridge that
777
+ // keeps minting a new bridgeInstanceId on every probe (or that never manages
778
+ // to actually register) can't retry forever without ever surfacing an error.
779
+ const MAX_LIVE_EDIT_RESTART_ATTEMPTS = 3;
780
+ // When a /health probe confirms the SAME bridgeInstanceId as last registered
781
+ // (bridge process healthy, page just slow to post ready), each re-arm of the
782
+ // watchdog waits longer than the last — 4s, 8s, 16s — capped here so the
783
+ // backoff doesn't grow unbounded on a very long-running compile.
784
+ const LIVE_EDIT_SAME_INSTANCE_MAX_REARM_DELAY_MS = 16_000;
785
+ // Total cumulative time to keep silently re-arming on a same-instance-id
786
+ // "still healthy" response before finally surfacing a non-destructive error
787
+ // affordance. Generous relative to even a slow cold dev-server compile so a
788
+ // genuinely-loading page is never misdiagnosed as failed; a legitimately
789
+ // long compile that finishes after this point still clears the error the
790
+ // moment the ready handshake arrives (see the message-handler branch below).
791
+ const LIVE_EDIT_SAME_INSTANCE_ERROR_CEILING_MS = 48_000;
792
+
793
+ // Successful bridge registrations belong to the localhost bridge process,
794
+ // not to one React DesignCanvas instance. Overview -> Full view currently
795
+ // replaces the overview canvas component with a focused canvas component; a
796
+ // component-local registration flag made that transition mount an empty
797
+ // srcdoc first and then replace it with the live URL after an identical POST.
798
+ // Keep a deliberately short handoff cache so the focused instance can mount
799
+ // the already-registered live document exactly once. A stale entry during
800
+ // HMR is harmless: the ready watchdog probes the bridge instance and silently
801
+ // re-registers, while the ready-gated fallback below prevents blank content.
802
+ const LIVE_EDIT_REGISTRATION_HANDOFF_TTL_MS = 30_000;
803
+ const liveEditRegistrationHandoff = new Map<string, number>();
804
+
805
+ function liveEditRegistrationHandoffKey(
806
+ bridgeUrl: string | undefined,
807
+ bridgeKey: string,
808
+ ): string | null {
809
+ if (!bridgeUrl) return null;
810
+ try {
811
+ return `${new URL(bridgeUrl).origin}|${bridgeKey}`;
812
+ } catch {
813
+ return null;
814
+ }
815
+ }
816
+
817
+ function hasRecentLiveEditRegistration(key: string | null): boolean {
818
+ if (!key) return false;
819
+ const registeredAt = liveEditRegistrationHandoff.get(key);
820
+ if (registeredAt === undefined) return false;
821
+ if (Date.now() - registeredAt <= LIVE_EDIT_REGISTRATION_HANDOFF_TTL_MS) {
822
+ return true;
823
+ }
824
+ liveEditRegistrationHandoff.delete(key);
825
+ return false;
826
+ }
827
+
688
828
  function originFromUrl(value: string | undefined): string | null {
689
829
  if (!value) return null;
690
830
  try {
@@ -701,6 +841,9 @@ function buildEditorChromeBridgeScript(args: {
701
841
  editorChromeScaleY: number;
702
842
  screenId: string;
703
843
  boardSurface: boolean;
844
+ contentOffsetX: number;
845
+ contentOffsetY: number;
846
+ runtimeLayerSnapshotEnabled: boolean;
704
847
  }) {
705
848
  return (
706
849
  createEditorBridgeThemeScript(readEditorBridgeThemeVars()) +
@@ -716,6 +859,18 @@ function buildEditorChromeBridgeScript(args: {
716
859
  "__DESIGN_CANVAS_BOARD_SURFACE__",
717
860
  args.boardSurface ? "true" : "false",
718
861
  )
862
+ .replace(
863
+ "__DESIGN_CANVAS_CONTENT_OFFSET_X__",
864
+ String(Math.round(args.contentOffsetX)),
865
+ )
866
+ .replace(
867
+ "__DESIGN_CANVAS_CONTENT_OFFSET_Y__",
868
+ String(Math.round(args.contentOffsetY)),
869
+ )
870
+ .replace(
871
+ "__RUNTIME_LAYER_SNAPSHOT_ENABLED__",
872
+ args.runtimeLayerSnapshotEnabled ? "true" : "false",
873
+ )
719
874
  );
720
875
  }
721
876
 
@@ -727,6 +882,29 @@ function contentHash(value: string): string {
727
882
  return `${value.length}:${hash >>> 0}`;
728
883
  }
729
884
 
885
+ /**
886
+ * Safely reads an embedded screen iframe's own internal document scroll
887
+ * position, in the iframe's own unscaled content pixels — the same units
888
+ * `getScreenContentPointFromClient`'s `scrollOffset` param folds in. Inline
889
+ * (srcdoc) screens are same-origin, so the happy path below is the common
890
+ * case, but externally-hosted localhost/fusion previews can be cross-origin,
891
+ * and `contentWindow.scrollX`/`scrollY` THROWS (it is not merely
892
+ * `undefined`) when accessed cross-origin. Always degrade to `{0, 0}` rather
893
+ * than letting that throw escape into a render or event handler.
894
+ */
895
+ function readIframeScrollOffset(iframe: HTMLIFrameElement | null | undefined): {
896
+ left: number;
897
+ top: number;
898
+ } {
899
+ try {
900
+ const win = iframe?.contentWindow;
901
+ if (!win) return { left: 0, top: 0 };
902
+ return { left: win.scrollX || 0, top: win.scrollY || 0 };
903
+ } catch {
904
+ return { left: 0, top: 0 };
905
+ }
906
+ }
907
+
730
908
  export function DesignCanvas({
731
909
  content,
732
910
  contentKey,
@@ -734,6 +912,7 @@ export function DesignCanvas({
734
912
  bridgeUrl,
735
913
  externalSnapshotHtml,
736
914
  onExternalContentSnapshot,
915
+ onRuntimeLayerSnapshot,
737
916
  fusionUrl,
738
917
  previewToken,
739
918
  zoom,
@@ -747,6 +926,7 @@ export function DesignCanvas({
747
926
  styleBaselineResetRequest,
748
927
  textRevertRequest,
749
928
  structureAckRequest,
929
+ runtimeStructureMoveRequest,
750
930
  embeddedFrameBackground,
751
931
  transparentBackground = false,
752
932
  editorChromeScaleX = 1,
@@ -773,6 +953,9 @@ export function DesignCanvas({
773
953
  tweakValues,
774
954
  drawMode,
775
955
  onExitDrawMode,
956
+ drawOverlayResetSignal,
957
+ retainDrawOverlayWhenHidden = false,
958
+ onAnnotationSendingChange,
776
959
  pinMode,
777
960
  commentPinsHidden,
778
961
  selectedSelector,
@@ -847,6 +1030,8 @@ export function DesignCanvas({
847
1030
  // fallback for older/interact-mode documents that never inject the chrome
848
1031
  // bridge and thus never post ready) and flush in order.
849
1032
  const bridgeReadyRef = useRef(false);
1033
+ const [readyIframeDocumentIdentity, setReadyIframeDocumentIdentity] =
1034
+ useState<string | null>(null);
850
1035
  const previousIframeDocumentIdentityRef = useRef<string | null>(null);
851
1036
  const pendingOneShotMessagesRef = useRef<unknown[]>([]);
852
1037
  const flushPendingOneShotMessages = useCallback(() => {
@@ -861,8 +1046,11 @@ export function DesignCanvas({
861
1046
  const postOneShotBridgeMessage = useCallback((message: unknown) => {
862
1047
  const iframe = iframeRef.current;
863
1048
  const win = iframe?.contentWindow;
864
- if (!win) return false;
865
- if (!bridgeReadyRef.current) {
1049
+ // A one-shot prop can arrive in the same render that first creates the
1050
+ // iframe. Keep it queued even when the ref/contentWindow is not attached
1051
+ // yet; otherwise the effect records its request id as handled and the
1052
+ // command is lost permanently before the bridge can announce readiness.
1053
+ if (!win || !bridgeReadyRef.current) {
866
1054
  pendingOneShotMessagesRef.current.push(message);
867
1055
  return true;
868
1056
  }
@@ -872,6 +1060,12 @@ export function DesignCanvas({
872
1060
  const [renderedContent, setRenderedContent] = useState(content);
873
1061
  const [annotationPins, setAnnotationPins] = useState<CanvasPin[]>([]);
874
1062
  const [pinSubmitSignal, setPinSubmitSignal] = useState(0);
1063
+ // True while a drawing send is capturing/compositing/uploading the
1064
+ // annotated screenshot (see design-canvas/annotation-snapshot.ts). Drives
1065
+ // SharedDrawOverlay's busy Send state so a slow capture can't be triggered
1066
+ // twice from the same drawing.
1067
+ const [annotationCaptureBusy, setAnnotationCaptureBusy] = useState(false);
1068
+ const annotationCaptureBusyRef = useRef(false);
875
1069
  const [fetchedExternalSnapshot, setFetchedExternalSnapshot] = useState<{
876
1070
  url: string;
877
1071
  html: string;
@@ -891,6 +1085,83 @@ export function DesignCanvas({
891
1085
  // manual retry click (see handleManualSnapshotRetry) so the next failure
892
1086
  // starts the backoff over rather than continuing to climb.
893
1087
  const snapshotRetryAttemptRef = useRef(0);
1088
+ // Mirrors the snapshot fetch's own retry/error state above: the
1089
+ // /live-edit-bridge registration POST used to have no retry/backoff or
1090
+ // error surface at all — a transient failure (dev server hiccup, brief
1091
+ // network blip) left registeredLiveEditBridgeKey stuck at null forever,
1092
+ // which pins waitingForLiveEditBridge true and the user stares at
1093
+ // "Preparing live editor..." with no explanation and no way to recover
1094
+ // short of reloading the whole page.
1095
+ const bridgeRegistrationRetryAttemptRef = useRef(0);
1096
+ const [bridgeRegistrationRetryNonce, setBridgeRegistrationRetryNonce] =
1097
+ useState(0);
1098
+ const [bridgeRegistrationError, setBridgeRegistrationError] = useState<{
1099
+ bridgeKey: string;
1100
+ message: string;
1101
+ } | null>(null);
1102
+ // Cache of the bridgeInstanceId returned by the client's LAST successful
1103
+ // /live-edit-bridge registration POST (see the registration effect below).
1104
+ // Compared against a later /health probe's bridgeInstanceId to tell "the
1105
+ // bridge process restarted since we registered" apart from "the same
1106
+ // process really doesn't know this key" — see classifyLiveEditHealthProbe
1107
+ // above.
1108
+ const bridgeInstanceIdRef = useRef<string | null>(null);
1109
+ // A destructive watchdog outcome replaces the live iframe with the neutral
1110
+ // pending document. The live document can still have queued its ready
1111
+ // handshake immediately before that replacement; once unmounted, its
1112
+ // WindowProxy no longer equals iframeRef.current.contentWindow and would be
1113
+ // rejected by the normal bridge trust check. Preserve exactly that retired
1114
+ // generation so its late ready can restore the matching bridge key. Both
1115
+ // source-window identity and bridge-key identity must match, preventing a
1116
+ // stale document from reviving a different screen/script generation.
1117
+ const lateLiveEditReadyRecoveryRef = useRef<{
1118
+ source: Window;
1119
+ bridgeKey: string;
1120
+ registrationHandoffKey: string | null;
1121
+ } | null>(null);
1122
+ // Guards the auto-reregister probe below against overlapping runs and bounds
1123
+ // how many times it will silently retry before giving up and surfacing the
1124
+ // existing error/Retry UI (MAX_LIVE_EDIT_RESTART_ATTEMPTS). This budget is
1125
+ // specific to the "reregister" (different bridgeInstanceId, genuine bridge
1126
+ // restart) loop — the same-instance-id "escalate" loop below has its own,
1127
+ // separate ceiling and does not consume this budget.
1128
+ const liveEditRestartInFlightRef = useRef(false);
1129
+ const liveEditRestartAttemptRef = useRef(0);
1130
+ // Same-instance-id ("escalate") loop state: how long we've cumulatively
1131
+ // waited on THIS bridge key while /health keeps confirming the bridge
1132
+ // process is healthy, the current per-arm wait, and the pending re-arm
1133
+ // timer (so it can be cancelled on ready/unmount/key-change). See
1134
+ // handleSuspectedBridgeRestart and classifyLiveEditHealthProbe above.
1135
+ const liveEditSameInstanceElapsedMsRef = useRef(0);
1136
+ const liveEditSameInstanceDelayRef = useRef(LIVE_EDIT_READY_TIMEOUT_MS);
1137
+ const liveEditSameInstanceRearmTimerRef = useRef<number | undefined>(
1138
+ undefined,
1139
+ );
1140
+ // Non-destructive counterpart to bridgeRegistrationError: shown as an
1141
+ // overlay error/Retry card WITHOUT nulling registeredLiveEditBridgeKey, so
1142
+ // the still-loading iframe is never torn down while it's shown. Only set
1143
+ // once the same-instance-id escalation loop above exceeds its ceiling; a
1144
+ // late ready handshake clears it (see the message-handler branch below).
1145
+ const [
1146
+ liveEditSameInstanceStalledError,
1147
+ setLiveEditSameInstanceStalledError,
1148
+ ] = useState<{
1149
+ bridgeKey: string;
1150
+ message: string;
1151
+ } | null>(null);
1152
+ // Set on unmount so the async /health probe (and its escalation re-arm
1153
+ // timer) never touches state after this component is gone.
1154
+ const isUnmountedRef = useRef(false);
1155
+ useEffect(
1156
+ () => () => {
1157
+ isUnmountedRef.current = true;
1158
+ if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
1159
+ window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
1160
+ liveEditSameInstanceRearmTimerRef.current = undefined;
1161
+ }
1162
+ },
1163
+ [],
1164
+ );
894
1165
  const onExternalContentSnapshotRef = useRef(onExternalContentSnapshot);
895
1166
  const isEmbeddedFrame = Boolean(embeddedFrame);
896
1167
  // Resolve the URL to render in the iframe:
@@ -912,6 +1183,10 @@ export function DesignCanvas({
912
1183
  }
913
1184
  return getExternalPreviewUrl(renderedContent);
914
1185
  }, [fusionUrl, renderedContent, sourceType]);
1186
+ const runtimeLayerSnapshotEnabled =
1187
+ sourceType === "localhost" &&
1188
+ Boolean(rawExternalPreviewUrl) &&
1189
+ Boolean(onRuntimeLayerSnapshot);
915
1190
  const activeExternalSnapshotHtml =
916
1191
  externalSnapshotHtml ??
917
1192
  (fetchedExternalSnapshot?.url === rawExternalPreviewUrl
@@ -921,6 +1196,21 @@ export function DesignCanvas({
921
1196
  // registered with the localhost bridge via a large POST, so folding live zoom
922
1197
  // in would re-fire the registration effect on every zoom tick. Live scale is
923
1198
  // pushed separately over the `set-editor-chrome-scale` postMessage below.
1199
+ //
1200
+ // readOnly and editMode are ALSO intentionally excluded from the deps below
1201
+ // (only read for their FIRST-render baked value) — mirroring the srcdoc
1202
+ // useMemo's own "readOnly and editMode are intentionally NOT deps" comment
1203
+ // for inline screens further down this file. Before this fix, this memo
1204
+ // (unlike the inline srcdoc one) DID list them as deps: every readOnly/
1205
+ // editMode change rebuilt this script, which changed liveEditBridgeKey
1206
+ // (its contentHash), which is embedded in the localhost iframe's `src` via
1207
+ // resolveLiveEditPreviewUrl — so a routine Edit ⇄ Preview toggle (or a
1208
+ // canEditDesign permission flip) forced a real cross-origin navigation of
1209
+ // the embedded dev-server iframe, losing its in-app route/scroll/state.
1210
+ // Live readOnly/editMode changes flow through the set-read-only and
1211
+ // set-text-editing-enabled postMessages (see the useEffects below) exactly
1212
+ // like the inline path, so no live-update behavior is lost by removing
1213
+ // them here.
924
1214
  const editorChromeBridgeForCurrentState = useMemo(
925
1215
  () =>
926
1216
  buildEditorChromeBridgeScript({
@@ -930,19 +1220,24 @@ export function DesignCanvas({
930
1220
  editorChromeScaleY: 1,
931
1221
  screenId: screenId ?? contentKey ?? "",
932
1222
  boardSurface,
1223
+ contentOffsetX: embeddedFrame?.contentOffsetX ?? 0,
1224
+ contentOffsetY: embeddedFrame?.contentOffsetY ?? 0,
1225
+ runtimeLayerSnapshotEnabled,
933
1226
  }),
934
- [boardSurface, contentKey, editMode, readOnly, screenId],
1227
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1228
+ [boardSurface, contentKey, runtimeLayerSnapshotEnabled, screenId],
935
1229
  );
1230
+ // Keep the installed gesture script identical between overview and focused
1231
+ // mode. The live flags are posted below; baking `isEmbeddedFrame` into the
1232
+ // script changed the bridge key during Full view and defeated the
1233
+ // registration handoff cache, forcing an avoidable iframe navigation.
936
1234
  const embeddedGestureBridgeForCurrentState = useMemo(
937
1235
  () =>
938
1236
  EMBEDDED_WHEEL_BRIDGE_SCRIPT.replace(
939
1237
  "__EMBEDDED_WHEEL_FORWARDING_ENABLED__",
940
- isEmbeddedFrame ? "true" : "false",
941
- ).replace(
942
- "__EMBEDDED_SPACE_KEY_FORWARDING_ENABLED__",
943
- interactMode || readOnly ? "true" : "false",
944
- ),
945
- [interactMode, isEmbeddedFrame, readOnly],
1238
+ "false",
1239
+ ).replace("__EMBEDDED_SPACE_KEY_FORWARDING_ENABLED__", "false"),
1240
+ [],
946
1241
  );
947
1242
  const includeLiveEditEditorChrome = !interactMode && !readOnly;
948
1243
  const liveEditBridgeScript = useMemo(
@@ -966,6 +1261,10 @@ export function DesignCanvas({
966
1261
  () => contentHash(liveEditBridgeScript),
967
1262
  [liveEditBridgeScript],
968
1263
  );
1264
+ const registrationHandoffKey = liveEditRegistrationHandoffKey(
1265
+ bridgeUrl,
1266
+ liveEditBridgeKey,
1267
+ );
969
1268
  const hasLiveEditExternalFrame =
970
1269
  sourceType === "localhost" && Boolean(bridgeUrl && rawExternalPreviewUrl);
971
1270
  const hasAuthenticatedLiveEditExternalFrame =
@@ -975,9 +1274,14 @@ export function DesignCanvas({
975
1274
  Boolean(bridgeUrl && previewToken && rawExternalPreviewUrl);
976
1275
  const usesLiveEditEditorBridge =
977
1276
  usesLiveEditInjectedBridge && includeLiveEditEditorChrome;
1277
+ const effectiveRegisteredLiveEditBridgeKey =
1278
+ registeredLiveEditBridgeKey ??
1279
+ (hasRecentLiveEditRegistration(registrationHandoffKey)
1280
+ ? liveEditBridgeKey
1281
+ : null);
978
1282
  const liveEditBridgeRegistered =
979
1283
  usesLiveEditInjectedBridge &&
980
- registeredLiveEditBridgeKey === liveEditBridgeKey;
1284
+ effectiveRegisteredLiveEditBridgeKey === liveEditBridgeKey;
981
1285
  // The live iframe and the editable source model are deliberately separate:
982
1286
  // render exactly one authenticated /live-edit document while fetching
983
1287
  // /snapshot in parallel so DesignEditor patches real HTML rather than the
@@ -995,7 +1299,7 @@ export function DesignCanvas({
995
1299
  previewToken,
996
1300
  previewUrl: rawExternalPreviewUrl,
997
1301
  bridgeKey: liveEditBridgeKey,
998
- registeredBridgeKey: registeredLiveEditBridgeKey,
1302
+ registeredBridgeKey: effectiveRegisteredLiveEditBridgeKey,
999
1303
  });
1000
1304
  const iframeRenderContent =
1001
1305
  !hasAuthenticatedLiveEditExternalFrame && activeExternalSnapshotHtml
@@ -1024,16 +1328,45 @@ export function DesignCanvas({
1024
1328
 
1025
1329
  // Register the editor bridge script with the localhost live-edit bridge so
1026
1330
  // it gets injected into the proxied preview. Re-runs only when the script
1027
- // content (liveEditBridgeKey) or the bridge target changes.
1331
+ // content (liveEditBridgeKey) or the bridge target changes (or a retry —
1332
+ // auto or manual — bumps bridgeRegistrationRetryNonce).
1333
+ //
1334
+ // Mirrors the external-source-snapshot fetch effect's own retry/backoff:
1335
+ // a transient failure used to leave registeredLiveEditBridgeKey stuck at
1336
+ // null forever (console.warn only, no retry, no error UI), pinning
1337
+ // waitingForLiveEditBridge true with no way to recover short of a full
1338
+ // page reload.
1028
1339
  useEffect(() => {
1029
1340
  if (!usesLiveEditInjectedBridge || !bridgeUrl || !previewToken) {
1341
+ bridgeRegistrationRetryAttemptRef.current = 0;
1342
+ liveEditRestartAttemptRef.current = 0;
1343
+ liveEditSameInstanceElapsedMsRef.current = 0;
1344
+ liveEditSameInstanceDelayRef.current = LIVE_EDIT_READY_TIMEOUT_MS;
1345
+ if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
1346
+ window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
1347
+ liveEditSameInstanceRearmTimerRef.current = undefined;
1348
+ }
1030
1349
  setRegisteredLiveEditBridgeKey(null);
1350
+ setBridgeRegistrationError(null);
1351
+ setLiveEditSameInstanceStalledError(null);
1352
+ lateLiveEditReadyRecoveryRef.current = null;
1031
1353
  return;
1032
1354
  }
1033
1355
  let cancelled = false;
1356
+ let retryTimer: number | undefined;
1034
1357
  setRegisteredLiveEditBridgeKey((current) =>
1035
1358
  current === liveEditBridgeKey ? current : null,
1036
1359
  );
1360
+ const scheduleRetry = () => {
1361
+ if (cancelled) return;
1362
+ const delay = getSnapshotRetryDelayMs(
1363
+ bridgeRegistrationRetryAttemptRef.current,
1364
+ );
1365
+ bridgeRegistrationRetryAttemptRef.current += 1;
1366
+ retryTimer = window.setTimeout(() => {
1367
+ setBridgeRegistrationRetryNonce((nonce) => nonce + 1);
1368
+ }, delay);
1369
+ };
1037
1370
  void (async () => {
1038
1371
  const endpoint = new URL("/live-edit-bridge", bridgeUrl).toString();
1039
1372
  try {
@@ -1051,25 +1384,320 @@ export function DesignCanvas({
1051
1384
  if (!response.ok) {
1052
1385
  throw new Error(`Bridge registration failed (${response.status})`);
1053
1386
  }
1054
- if (!cancelled) setRegisteredLiveEditBridgeKey(liveEditBridgeKey);
1387
+ const payload = (await response.json().catch(() => null)) as {
1388
+ bridgeInstanceId?: string;
1389
+ } | null;
1390
+ if (payload && typeof payload.bridgeInstanceId === "string") {
1391
+ // Cache the instance id from THIS successful registration so a
1392
+ // later suspected-restart probe (see handleSuspectedBridgeRestart)
1393
+ // can tell a genuinely restarted bridge process apart from the same
1394
+ // process rejecting a stale key.
1395
+ bridgeInstanceIdRef.current = payload.bridgeInstanceId;
1396
+ }
1397
+ if (cancelled) return;
1398
+ bridgeRegistrationRetryAttemptRef.current = 0;
1399
+ if (registrationHandoffKey) {
1400
+ liveEditRegistrationHandoff.set(registrationHandoffKey, Date.now());
1401
+ }
1402
+ // liveEditRestartAttemptRef is intentionally NOT reset here: a
1403
+ // successful registration POST only proves the bridge accepted the
1404
+ // script, not that the live document actually loaded it (that's what
1405
+ // the ready-handshake watchdog below still has to confirm). Resetting
1406
+ // the restart budget on every registration success — rather than only
1407
+ // on a genuine agent-native:editor-chrome-ready — would let a
1408
+ // pathological bridge that keeps minting a new bridgeInstanceId
1409
+ // reregister forever, defeating MAX_LIVE_EDIT_RESTART_ATTEMPTS.
1410
+ setBridgeRegistrationError(null);
1411
+ lateLiveEditReadyRecoveryRef.current = null;
1412
+ setRegisteredLiveEditBridgeKey(liveEditBridgeKey);
1055
1413
  } catch (error) {
1056
1414
  if (!cancelled) {
1415
+ if (registrationHandoffKey) {
1416
+ liveEditRegistrationHandoff.delete(registrationHandoffKey);
1417
+ }
1057
1418
  console.warn("live-edit bridge registration failed", error);
1058
1419
  setRegisteredLiveEditBridgeKey(null);
1420
+ setBridgeRegistrationError({
1421
+ bridgeKey: liveEditBridgeKey,
1422
+ message: error instanceof Error ? error.message : String(error),
1423
+ });
1424
+ scheduleRetry();
1059
1425
  }
1060
1426
  }
1061
1427
  })();
1062
1428
  return () => {
1063
1429
  cancelled = true;
1430
+ if (retryTimer) window.clearTimeout(retryTimer);
1064
1431
  };
1065
1432
  }, [
1433
+ bridgeRegistrationRetryNonce,
1066
1434
  bridgeUrl,
1067
1435
  liveEditBridgeKey,
1068
1436
  liveEditBridgeScript,
1069
1437
  previewToken,
1438
+ registrationHandoffKey,
1070
1439
  usesLiveEditInjectedBridge,
1071
1440
  ]);
1072
1441
 
1442
+ // A liveEditBridgeKey change means the registered script content changed
1443
+ // (different screen, mode, or scale bake) — a completely new registration
1444
+ // target. Reset the reregister-attempt budget and the same-instance-id
1445
+ // escalation state (elapsed wait, backoff delay, pending re-arm timer, and
1446
+ // any stalled-error card) so a previous screen's exhausted retries/backoff
1447
+ // never leak into this new screen's fresh watchdog cycle. Deliberately
1448
+ // separate from the watchdog-arm effect below, which resets the escalation
1449
+ // state on every successful (re)registration INCLUDING same-key reregister
1450
+ // cycles — liveEditRestartAttemptRef must NOT reset on those, or the
1451
+ // MAX_LIVE_EDIT_RESTART_ATTEMPTS budget it guards would never trip (see
1452
+ // that effect's own comment).
1453
+ useEffect(() => {
1454
+ liveEditRestartAttemptRef.current = 0;
1455
+ liveEditSameInstanceElapsedMsRef.current = 0;
1456
+ liveEditSameInstanceDelayRef.current = LIVE_EDIT_READY_TIMEOUT_MS;
1457
+ if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
1458
+ window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
1459
+ liveEditSameInstanceRearmTimerRef.current = undefined;
1460
+ }
1461
+ setLiveEditSameInstanceStalledError(null);
1462
+ lateLiveEditReadyRecoveryRef.current = null;
1463
+ }, [liveEditBridgeKey]);
1464
+
1465
+ // Manual retry (offline-state "Retry" button): reset the backoff so the
1466
+ // user-initiated attempt fires immediately, mirroring
1467
+ // handleManualSnapshotRetry below. Also clears the non-destructive
1468
+ // same-instance-id stalled-error card and its escalation state, since a
1469
+ // manual retry (from either error card) should start every counter fresh.
1470
+ const handleManualBridgeRegistrationRetry = useCallback(() => {
1471
+ bridgeRegistrationRetryAttemptRef.current = 0;
1472
+ liveEditRestartAttemptRef.current = 0;
1473
+ liveEditSameInstanceElapsedMsRef.current = 0;
1474
+ liveEditSameInstanceDelayRef.current = LIVE_EDIT_READY_TIMEOUT_MS;
1475
+ if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
1476
+ window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
1477
+ liveEditSameInstanceRearmTimerRef.current = undefined;
1478
+ }
1479
+ setLiveEditSameInstanceStalledError(null);
1480
+ setBridgeRegistrationRetryNonce((nonce) => nonce + 1);
1481
+ }, []);
1482
+
1483
+ // The registered iframe's `src` is a real cross-origin navigation straight
1484
+ // to the bridge's authenticated /live-edit URL, so this component can never
1485
+ // read that response's status or body (a 409 "unknown-bridge-key" looks
1486
+ // identical to any other document from here). What IS observable is
1487
+ // whether the editor-chrome bridge's "agent-native:editor-chrome-ready"
1488
+ // handshake (see the message handler below) arrives within a reasonable
1489
+ // window after we mounted the real document. A healthy load posts ready
1490
+ // almost immediately; a 409 never injects the bridge script at all, so
1491
+ // ready never arrives. Treat a stuck non-ready state as a suspected restart
1492
+ // and settle it with a /health probe instead of guessing from the error UI.
1493
+ const handleSuspectedBridgeRestart = useCallback(async () => {
1494
+ if (!bridgeUrl || !previewToken) return;
1495
+ if (liveEditRestartInFlightRef.current) return;
1496
+ liveEditRestartInFlightRef.current = true;
1497
+ try {
1498
+ const response = await fetch(healthEndpointUrl(bridgeUrl));
1499
+ const payload = (await response.json().catch(() => null)) as {
1500
+ bridgeInstanceId?: string;
1501
+ } | null;
1502
+ if (isUnmountedRef.current) return;
1503
+ const responseBridgeInstanceId =
1504
+ payload && typeof payload.bridgeInstanceId === "string"
1505
+ ? payload.bridgeInstanceId
1506
+ : null;
1507
+ const decision = classifyLiveEditHealthProbe(
1508
+ bridgeInstanceIdRef.current,
1509
+ responseBridgeInstanceId,
1510
+ );
1511
+ if (decision === "reregister") {
1512
+ if (
1513
+ liveEditRestartAttemptRef.current >= MAX_LIVE_EDIT_RESTART_ATTEMPTS
1514
+ ) {
1515
+ if (registrationHandoffKey) {
1516
+ liveEditRegistrationHandoff.delete(registrationHandoffKey);
1517
+ }
1518
+ const lateReadySource = iframeRef.current?.contentWindow;
1519
+ lateLiveEditReadyRecoveryRef.current = lateReadySource
1520
+ ? {
1521
+ source: lateReadySource,
1522
+ bridgeKey: liveEditBridgeKey,
1523
+ registrationHandoffKey,
1524
+ }
1525
+ : null;
1526
+ setRegisteredLiveEditBridgeKey(null);
1527
+ setBridgeRegistrationError({
1528
+ bridgeKey: liveEditBridgeKey,
1529
+ message: t("designCanvas.localBridge.confirmationRetryExhausted"),
1530
+ });
1531
+ return;
1532
+ }
1533
+ liveEditRestartAttemptRef.current += 1;
1534
+ if (registrationHandoffKey) {
1535
+ liveEditRegistrationHandoff.delete(registrationHandoffKey);
1536
+ }
1537
+ // The bridge process restarted since our last registration — silently
1538
+ // re-POST the same script/key and reload the frame. Resetting
1539
+ // registeredLiveEditBridgeKey (without setting an error) re-arms the
1540
+ // "Preparing live editor..." placeholder — the same neutral state
1541
+ // already shown on first connect — and bumping the retry nonce
1542
+ // re-triggers the registration effect above.
1543
+ bridgeInstanceIdRef.current = responseBridgeInstanceId;
1544
+ setRegisteredLiveEditBridgeKey(null);
1545
+ setBridgeRegistrationError(null);
1546
+ setLiveEditSameInstanceStalledError(null);
1547
+ // A genuine restart makes any same-instance-id wait we'd accumulated
1548
+ // against the OLD process meaningless — reset the escalation clock so
1549
+ // the fresh document this reregister produces gets the full ceiling,
1550
+ // not whatever was left over from the previous one.
1551
+ liveEditSameInstanceElapsedMsRef.current = 0;
1552
+ liveEditSameInstanceDelayRef.current = LIVE_EDIT_READY_TIMEOUT_MS;
1553
+ if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
1554
+ window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
1555
+ liveEditSameInstanceRearmTimerRef.current = undefined;
1556
+ }
1557
+ setBridgeRegistrationRetryNonce((nonce) => nonce + 1);
1558
+ return;
1559
+ }
1560
+ if (decision === "escalate") {
1561
+ // Same bridge process, still healthy and reachable — the page is
1562
+ // just slow (e.g. a cold dev-server compile), not actually failing.
1563
+ // Do NOT touch registeredLiveEditBridgeKey or bridgeRegistrationError
1564
+ // here: that would tear down the still-legitimately-loading iframe
1565
+ // (the regression this fix addresses). Re-arm with a longer wait
1566
+ // instead, up to a generous total ceiling, before finally surfacing a
1567
+ // separate NON-destructive error card that leaves the iframe alone.
1568
+ liveEditSameInstanceElapsedMsRef.current +=
1569
+ liveEditSameInstanceDelayRef.current;
1570
+ if (
1571
+ liveEditSameInstanceElapsedMsRef.current >=
1572
+ LIVE_EDIT_SAME_INSTANCE_ERROR_CEILING_MS
1573
+ ) {
1574
+ setLiveEditSameInstanceStalledError({
1575
+ bridgeKey: liveEditBridgeKey,
1576
+ message: t("designCanvas.localBridge.connectionNotConfirmed"),
1577
+ });
1578
+ return;
1579
+ }
1580
+ const nextDelay = Math.min(
1581
+ liveEditSameInstanceDelayRef.current * 2,
1582
+ LIVE_EDIT_SAME_INSTANCE_MAX_REARM_DELAY_MS,
1583
+ );
1584
+ liveEditSameInstanceDelayRef.current = nextDelay;
1585
+ if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
1586
+ window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
1587
+ }
1588
+ liveEditSameInstanceRearmTimerRef.current = window.setTimeout(() => {
1589
+ liveEditSameInstanceRearmTimerRef.current = undefined;
1590
+ if (isUnmountedRef.current || bridgeReadyRef.current) return;
1591
+ void handleSuspectedBridgeRestart();
1592
+ }, nextDelay);
1593
+ return;
1594
+ }
1595
+ // decision === "error": no bridgeInstanceId on one or both sides, so we
1596
+ // can't confirm the same-process-still-healthy story above. Treat this
1597
+ // as a genuine, unresolved failure and surface the existing
1598
+ // (destructive) error/Retry UI instead of looping forever.
1599
+ if (registrationHandoffKey) {
1600
+ liveEditRegistrationHandoff.delete(registrationHandoffKey);
1601
+ }
1602
+ const lateReadySource = iframeRef.current?.contentWindow;
1603
+ lateLiveEditReadyRecoveryRef.current = lateReadySource
1604
+ ? {
1605
+ source: lateReadySource,
1606
+ bridgeKey: liveEditBridgeKey,
1607
+ registrationHandoffKey,
1608
+ }
1609
+ : null;
1610
+ setRegisteredLiveEditBridgeKey(null);
1611
+ setBridgeRegistrationError({
1612
+ bridgeKey: liveEditBridgeKey,
1613
+ message: t("designCanvas.localBridge.connectionNotConfirmed"),
1614
+ });
1615
+ } catch (error) {
1616
+ if (isUnmountedRef.current) return;
1617
+ // /health itself is unreachable (network error / thrown before a
1618
+ // response) — the dev server process is actually down, not just slow.
1619
+ // This destructive path (tear down + surface the error) is justified.
1620
+ if (registrationHandoffKey) {
1621
+ liveEditRegistrationHandoff.delete(registrationHandoffKey);
1622
+ }
1623
+ const lateReadySource = iframeRef.current?.contentWindow;
1624
+ lateLiveEditReadyRecoveryRef.current = lateReadySource
1625
+ ? {
1626
+ source: lateReadySource,
1627
+ bridgeKey: liveEditBridgeKey,
1628
+ registrationHandoffKey,
1629
+ }
1630
+ : null;
1631
+ setRegisteredLiveEditBridgeKey(null);
1632
+ setBridgeRegistrationError({
1633
+ bridgeKey: liveEditBridgeKey,
1634
+ message: error instanceof Error ? error.message : String(error),
1635
+ });
1636
+ } finally {
1637
+ liveEditRestartInFlightRef.current = false;
1638
+ }
1639
+ }, [bridgeUrl, liveEditBridgeKey, previewToken, registrationHandoffKey, t]);
1640
+
1641
+ // Manual retry for the NON-destructive same-instance-id stalled card only
1642
+ // (see liveEditSameInstanceStalledError below): unlike
1643
+ // handleManualBridgeRegistrationRetry, registeredLiveEditBridgeKey was
1644
+ // never nulled here, so bumping bridgeRegistrationRetryNonce would not
1645
+ // reschedule a fresh watchdog probe (liveEditBridgeRegistered never flips
1646
+ // false→true to rearm that effect). Reset the backoff and probe /health
1647
+ // again directly instead.
1648
+ const handleManualLiveEditSameInstanceRetry = useCallback(() => {
1649
+ liveEditSameInstanceElapsedMsRef.current = 0;
1650
+ liveEditSameInstanceDelayRef.current = LIVE_EDIT_READY_TIMEOUT_MS;
1651
+ if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
1652
+ window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
1653
+ liveEditSameInstanceRearmTimerRef.current = undefined;
1654
+ }
1655
+ setLiveEditSameInstanceStalledError(null);
1656
+ void handleSuspectedBridgeRestart();
1657
+ }, [handleSuspectedBridgeRestart]);
1658
+
1659
+ // Arm the ready-handshake watchdog whenever the real authenticated live-edit
1660
+ // document is (re)mounted in editor-chrome mode (the only mode that ever
1661
+ // posts agent-native:editor-chrome-ready — see usesLiveEditEditorBridge).
1662
+ // liveEditBridgeRegistered flips false→true on every fresh mount (including
1663
+ // the reregister cycle above), so this effect reliably rearms each time.
1664
+ //
1665
+ // NOTE: deliberately does NOT reset the same-instance-id escalation refs
1666
+ // (liveEditSameInstance*) here even though this effect conceptually marks a
1667
+ // fresh mount: handleSuspectedBridgeRestart depends on `t`, whose identity
1668
+ // is not guaranteed stable across renders (see useT), so this effect's own
1669
+ // dependency array can cause it to re-run on unrelated re-renders — not
1670
+ // only on a genuine fresh mount. Resetting escalation state here would
1671
+ // then race with (and can clobber) the very state update that triggered
1672
+ // that extra re-render, e.g. immediately wiping the non-destructive stalled
1673
+ // error the moment it's set. The escalation refs are instead reset from
1674
+ // known-good, less-churny signals: the liveEditBridgeKey-keyed effect below
1675
+ // (genuinely new script/screen) and the "reregister" branch inside
1676
+ // handleSuspectedBridgeRestart itself (genuine bridge-process restart).
1677
+ useEffect(() => {
1678
+ if (
1679
+ !usesLiveEditEditorBridge ||
1680
+ !liveEditBridgeRegistered ||
1681
+ !externalPreviewUrl
1682
+ ) {
1683
+ return;
1684
+ }
1685
+ let cancelled = false;
1686
+ const timer = window.setTimeout(() => {
1687
+ if (cancelled || bridgeReadyRef.current) return;
1688
+ void handleSuspectedBridgeRestart();
1689
+ }, LIVE_EDIT_READY_TIMEOUT_MS);
1690
+ return () => {
1691
+ cancelled = true;
1692
+ window.clearTimeout(timer);
1693
+ };
1694
+ }, [
1695
+ usesLiveEditEditorBridge,
1696
+ liveEditBridgeRegistered,
1697
+ externalPreviewUrl,
1698
+ handleSuspectedBridgeRestart,
1699
+ ]);
1700
+
1073
1701
  useEffect(() => {
1074
1702
  const previewUrl = rawExternalPreviewUrl;
1075
1703
  if (
@@ -1326,7 +1954,16 @@ export function DesignCanvas({
1326
1954
  .replace(
1327
1955
  "__DESIGN_CANVAS_BOARD_SURFACE__",
1328
1956
  boardSurface ? "true" : "false",
1329
- );
1957
+ )
1958
+ .replace(
1959
+ "__DESIGN_CANVAS_CONTENT_OFFSET_X__",
1960
+ String(Math.round(embeddedFrame?.contentOffsetX ?? 0)),
1961
+ )
1962
+ .replace(
1963
+ "__DESIGN_CANVAS_CONTENT_OFFSET_Y__",
1964
+ String(Math.round(embeddedFrame?.contentOffsetY ?? 0)),
1965
+ )
1966
+ .replace("__RUNTIME_LAYER_SNAPSHOT_ENABLED__", "false");
1330
1967
  // ALWAYS injected (like the other always-on bridges above) so
1331
1968
  // MultiScreenCanvas's cross-screen drag hit-testing
1332
1969
  // (agent-native:hit-test / agent-native:hit-test-result) resolves an
@@ -1380,8 +2017,6 @@ export function DesignCanvas({
1380
2017
  externalPreviewUrl,
1381
2018
  interactMode,
1382
2019
  isEmbeddedFrame,
1383
- embeddedFrame?.contentOffsetX,
1384
- embeddedFrame?.contentOffsetY,
1385
2020
  embeddedFrameBackground,
1386
2021
  embeddedGestureBridgeForCurrentState,
1387
2022
  iframeRenderContent,
@@ -1398,11 +2033,22 @@ export function DesignCanvas({
1398
2033
  previousIframeDocumentIdentityRef.current = iframeDocumentIdentity;
1399
2034
  bridgeReadyRef.current = false;
1400
2035
  }
2036
+ const liveEditDocumentPending =
2037
+ usesLiveEditEditorBridge &&
2038
+ Boolean(externalPreviewUrl) &&
2039
+ readyIframeDocumentIdentity !== iframeDocumentIdentity;
2040
+ const liveEditTransitionFallbackHtml = liveEditDocumentPending
2041
+ ? activeExternalSnapshotHtml
2042
+ : undefined;
1401
2043
 
1402
2044
  // Listen for messages from the iframe
1403
2045
  useEffect(() => {
1404
2046
  function handleMessage(e: MessageEvent) {
1405
2047
  const iframeWindow = iframeRef.current?.contentWindow;
2048
+ const lateReadyRecovery =
2049
+ e.data?.type === "agent-native:editor-chrome-ready"
2050
+ ? lateLiveEditReadyRecoveryRef.current
2051
+ : null;
1406
2052
  // For fusion sources the Builder-hosted app is cross-origin, so the strict
1407
2053
  // `origin === parentOrigin` check can never match. We still require window
1408
2054
  // identity (the message must come from our own iframe window, not any
@@ -1411,7 +2057,7 @@ export function DesignCanvas({
1411
2057
  // *.builder.io family) before relaxing the origin check. If the origin is
1412
2058
  // not on the allowlist we keep the strict check so a hostile frame that
1413
2059
  // somehow shares our window reference still can't be trusted.
1414
- const trusted =
2060
+ const trustedCurrentFrame =
1415
2061
  sourceType === "fusion"
1416
2062
  ? iframeWindow !== null &&
1417
2063
  e.source === iframeWindow &&
@@ -1428,12 +2074,80 @@ export function DesignCanvas({
1428
2074
  )
1429
2075
  : [],
1430
2076
  });
2077
+ const trustedLateLiveEditReady =
2078
+ sourceType === "localhost" &&
2079
+ lateReadyRecovery !== null &&
2080
+ lateReadyRecovery.bridgeKey === liveEditBridgeKey &&
2081
+ isTrustedCanvasBridgeMessage({
2082
+ source: e.source,
2083
+ origin: e.origin,
2084
+ iframeWindow: lateReadyRecovery.source,
2085
+ parentOrigin: window.location.origin,
2086
+ allowedOrigins: [originFromUrl(bridgeUrl)].filter(
2087
+ (origin): origin is string => Boolean(origin),
2088
+ ),
2089
+ });
2090
+ const trusted = trustedCurrentFrame || trustedLateLiveEditReady;
1431
2091
  if (!trusted) {
1432
2092
  return;
1433
2093
  }
1434
2094
  if (!e.data || !e.data.type) return;
2095
+ if (e.data.type === "agent-native:runtime-layer-snapshot") {
2096
+ const payload = e.data.payload;
2097
+ if (
2098
+ payload &&
2099
+ typeof payload.html === "string" &&
2100
+ payload.html.length <= 2_000_000 &&
2101
+ Number.isFinite(payload.nodeCount)
2102
+ ) {
2103
+ onRuntimeLayerSnapshot?.({
2104
+ html: payload.html,
2105
+ nodeCount: Math.max(0, Math.floor(payload.nodeCount)),
2106
+ });
2107
+ }
2108
+ return;
2109
+ }
1435
2110
  if (e.data.type === "agent-native:editor-chrome-ready") {
2111
+ if (trustedLateLiveEditReady && lateReadyRecovery) {
2112
+ // This handshake belongs to the one retired live document saved by
2113
+ // the destructive watchdog path, not the pending iframe now in the
2114
+ // DOM. Restore its exact registration generation and let the newly
2115
+ // mounted live document send the normal ready signal; do not mark
2116
+ // the pending document ready or flush one-shot messages into it.
2117
+ lateLiveEditReadyRecoveryRef.current = null;
2118
+ if (lateReadyRecovery.registrationHandoffKey) {
2119
+ liveEditRegistrationHandoff.set(
2120
+ lateReadyRecovery.registrationHandoffKey,
2121
+ Date.now(),
2122
+ );
2123
+ }
2124
+ setBridgeRegistrationError((current) =>
2125
+ current?.bridgeKey === lateReadyRecovery.bridgeKey ? null : current,
2126
+ );
2127
+ setRegisteredLiveEditBridgeKey(lateReadyRecovery.bridgeKey);
2128
+ return;
2129
+ }
2130
+ lateLiveEditReadyRecoveryRef.current = null;
1436
2131
  bridgeReadyRef.current = true;
2132
+ setReadyIframeDocumentIdentity(iframeDocumentIdentity);
2133
+ // A confirmed ready handshake proves this bridgeInstanceId/key pair
2134
+ // is genuinely live — clear the suspected-restart attempt counter so
2135
+ // a later transient hiccup gets the full retry budget again instead
2136
+ // of inheriting an already-elevated count from an unrelated earlier
2137
+ // load.
2138
+ liveEditRestartAttemptRef.current = 0;
2139
+ // A late ready handshake wins even after the same-instance-id
2140
+ // escalation loop gave up and showed its non-destructive error card:
2141
+ // cancel any pending re-arm timer, reset the backoff, and clear the
2142
+ // error so the now-loaded iframe (which was never torn down) reads
2143
+ // as fully connected.
2144
+ if (liveEditSameInstanceRearmTimerRef.current !== undefined) {
2145
+ window.clearTimeout(liveEditSameInstanceRearmTimerRef.current);
2146
+ liveEditSameInstanceRearmTimerRef.current = undefined;
2147
+ }
2148
+ liveEditSameInstanceElapsedMsRef.current = 0;
2149
+ liveEditSameInstanceDelayRef.current = LIVE_EDIT_READY_TIMEOUT_MS;
2150
+ setLiveEditSameInstanceStalledError(null);
1437
2151
  flushPendingOneShotMessages();
1438
2152
  return;
1439
2153
  }
@@ -1577,8 +2291,13 @@ export function DesignCanvas({
1577
2291
  sourceId,
1578
2292
  anchorSourceId,
1579
2293
  dropMode,
2294
+ forceFlowPositionOverride:
2295
+ e.data.forceFlowPositionOverride === true,
1580
2296
  sourceRect,
1581
2297
  anchorRect,
2298
+ anchorElementInfo: isElementInfoPayload(e.data.anchorPayload)
2299
+ ? e.data.anchorPayload
2300
+ : undefined,
1582
2301
  },
1583
2302
  );
1584
2303
  if (requestId && applied !== "pending") {
@@ -1705,11 +2424,39 @@ export function DesignCanvas({
1705
2424
  ? iframeRect.height / iframe.clientHeight
1706
2425
  : 1;
1707
2426
  onIframeContextMenu?.({
2427
+ screenId:
2428
+ typeof e.data.screenId === "string" ? e.data.screenId : undefined,
1708
2429
  clientX,
1709
2430
  clientY,
1710
2431
  viewportClientX: (iframeRect?.left ?? 0) + clientX * scaleX,
1711
2432
  viewportClientY: (iframeRect?.top ?? 0) + clientY * scaleY,
1712
2433
  info: e.data.payload ?? null,
2434
+ layerCandidates: Array.isArray(e.data.layerCandidates)
2435
+ ? e.data.layerCandidates
2436
+ .filter(
2437
+ (candidate: unknown) =>
2438
+ candidate &&
2439
+ typeof candidate === "object" &&
2440
+ isElementInfoPayload(
2441
+ (candidate as { info?: unknown }).info,
2442
+ ),
2443
+ )
2444
+ .map((candidate: any, index: number) => ({
2445
+ key:
2446
+ typeof candidate.key === "string"
2447
+ ? candidate.key
2448
+ : `layer-hit-${index}`,
2449
+ label:
2450
+ typeof candidate.label === "string"
2451
+ ? candidate.label.slice(0, 80)
2452
+ : "Layer",
2453
+ screenId:
2454
+ typeof e.data.screenId === "string"
2455
+ ? e.data.screenId
2456
+ : undefined,
2457
+ info: candidate.info,
2458
+ }))
2459
+ : [],
1713
2460
  });
1714
2461
  }
1715
2462
  return;
@@ -1842,6 +2589,7 @@ export function DesignCanvas({
1842
2589
  return () => window.removeEventListener("message", handleMessage);
1843
2590
  }, [
1844
2591
  onElementSelect,
2592
+ onRuntimeLayerSnapshot,
1845
2593
  onElementMarqueeSelect,
1846
2594
  onElementHover,
1847
2595
  onClearSelection,
@@ -1863,6 +2611,7 @@ export function DesignCanvas({
1863
2611
  isEmbeddedFrame,
1864
2612
  sourceType,
1865
2613
  bridgeUrl,
2614
+ liveEditBridgeKey,
1866
2615
  fusionUrl,
1867
2616
  flushPendingOneShotMessages,
1868
2617
  postOneShotBridgeMessage,
@@ -2127,6 +2876,66 @@ export function DesignCanvas({
2127
2876
  postOneShotBridgeMessage,
2128
2877
  ]);
2129
2878
 
2879
+ // Overview/focused placement is presentation state, not document identity.
2880
+ // Update gesture routing in place so entering Full view reuses the same
2881
+ // registered bridge key instead of rebuilding the injected script.
2882
+ useEffect(() => {
2883
+ postOneShotBridgeMessage({
2884
+ type: "embedded-canvas-gesture-mode",
2885
+ wheelEnabled: isEmbeddedFrame,
2886
+ spaceKeyForwardingEnabled: interactMode || readOnly,
2887
+ });
2888
+ }, [interactMode, isEmbeddedFrame, postOneShotBridgeMessage, readOnly]);
2889
+
2890
+ // The board iframe is a finite paint window over an infinite logical
2891
+ // canvas. Re-centering that window must update its CSS/bridge coordinate
2892
+ // offset in place; rebuilding srcdoc here would reload the iframe, flash,
2893
+ // and discard transient Alpine state on every chunk crossing.
2894
+ const embeddedContentOffsetX = embeddedFrame?.contentOffsetX ?? 0;
2895
+ const embeddedContentOffsetY = embeddedFrame?.contentOffsetY ?? 0;
2896
+ useEffect(() => {
2897
+ const iframe = iframeRef.current;
2898
+ if (!iframe) return;
2899
+ const applyOffset = () => {
2900
+ try {
2901
+ const doc = iframe.contentDocument;
2902
+ if (doc) {
2903
+ const css = embeddedContentOffsetCss(
2904
+ embeddedContentOffsetX,
2905
+ embeddedContentOffsetY,
2906
+ );
2907
+ let style = doc.querySelector<HTMLStyleElement>(
2908
+ "style[data-agent-native-content-offset]",
2909
+ );
2910
+ if (!css) {
2911
+ style?.remove();
2912
+ } else {
2913
+ if (!style) {
2914
+ style = doc.createElement("style");
2915
+ style.setAttribute("data-agent-native-content-offset", "");
2916
+ (doc.head ?? doc.documentElement).appendChild(style);
2917
+ }
2918
+ style.textContent = css;
2919
+ }
2920
+ }
2921
+ } catch {
2922
+ // Cross-origin localhost/fusion frames are updated only through their
2923
+ // own bridge below; direct document access is intentionally optional.
2924
+ }
2925
+ iframe.contentWindow?.postMessage(
2926
+ {
2927
+ type: "set-content-offset",
2928
+ x: embeddedContentOffsetX,
2929
+ y: embeddedContentOffsetY,
2930
+ },
2931
+ "*",
2932
+ );
2933
+ };
2934
+ applyOffset();
2935
+ iframe.addEventListener("load", applyOffset);
2936
+ return () => iframe.removeEventListener("load", applyOffset);
2937
+ }, [embeddedContentOffsetX, embeddedContentOffsetY]);
2938
+
2130
2939
  // Sync readOnly to the bridge IN-PLACE via postMessage so switching the active
2131
2940
  // surface (board ↔ screen) does not rebuild srcdoc / reload the iframe.
2132
2941
  // The initial baked __READ_ONLY__ placeholder covers first paint; subsequent
@@ -2372,6 +3181,27 @@ export function DesignCanvas({
2372
3181
  }
2373
3182
  }, [postOneShotBridgeMessage, structureAckRequest]);
2374
3183
 
3184
+ const lastRuntimeStructureMoveRequestIdRef = useRef<number | null>(null);
3185
+ useEffect(() => {
3186
+ if (!runtimeStructureMoveRequest) return;
3187
+ if (
3188
+ lastRuntimeStructureMoveRequestIdRef.current ===
3189
+ runtimeStructureMoveRequest.requestId
3190
+ ) {
3191
+ return;
3192
+ }
3193
+ lastRuntimeStructureMoveRequestIdRef.current =
3194
+ runtimeStructureMoveRequest.requestId;
3195
+ postOneShotBridgeMessage({
3196
+ type: "runtime-structure-move",
3197
+ subjectSelector: runtimeStructureMoveRequest.subject.selector,
3198
+ subjectSourceId: runtimeStructureMoveRequest.subject.sourceId,
3199
+ anchorSelector: runtimeStructureMoveRequest.anchor.selector,
3200
+ anchorSourceId: runtimeStructureMoveRequest.anchor.sourceId,
3201
+ placement: runtimeStructureMoveRequest.placement,
3202
+ });
3203
+ }, [postOneShotBridgeMessage, runtimeStructureMoveRequest]);
3204
+
2375
3205
  /**
2376
3206
  * Send a motion-preview scrub tick to the iframe. `t` is the normalised
2377
3207
  * playhead position in [0, 1]. Tracks must have been loaded first via the
@@ -2747,10 +3577,13 @@ export function DesignCanvas({
2747
3577
  const rect = iframe?.getBoundingClientRect();
2748
3578
  const screenContentPoint =
2749
3579
  iframe && rect
2750
- ? getScreenContentPointFromClient(e.clientX, e.clientY, rect, {
2751
- width: iframe.clientWidth,
2752
- height: iframe.clientHeight,
2753
- })
3580
+ ? getScreenContentPointFromClient(
3581
+ e.clientX,
3582
+ e.clientY,
3583
+ rect,
3584
+ { width: iframe.clientWidth, height: iframe.clientHeight },
3585
+ readIframeScrollOffset(iframe),
3586
+ )
2754
3587
  : { x: e.clientX, y: e.clientY };
2755
3588
  onDropFiles(files, { screenContentPoint, screenId });
2756
3589
  },
@@ -2810,6 +3643,21 @@ export function DesignCanvas({
2810
3643
  : null),
2811
3644
  }}
2812
3645
  >
3646
+ {liveEditTransitionFallbackHtml ? (
3647
+ <iframe
3648
+ data-live-edit-transition-fallback
3649
+ srcDoc={liveEditTransitionFallbackHtml}
3650
+ sandbox=""
3651
+ aria-hidden="true"
3652
+ tabIndex={-1}
3653
+ className="pointer-events-none absolute inset-0 block h-full w-full border-0 bg-transparent"
3654
+ style={{
3655
+ background: iframeBackgroundColor,
3656
+ backgroundColor: iframeBackgroundColor,
3657
+ }}
3658
+ title=""
3659
+ />
3660
+ ) : null}
2813
3661
  <iframe
2814
3662
  key={iframeDocumentIdentity}
2815
3663
  ref={iframeRef}
@@ -2829,7 +3677,10 @@ export function DesignCanvas({
2829
3677
  ? "localhost" // inferred — content is a URL
2830
3678
  : "inline")
2831
3679
  }
2832
- className="block h-full w-full border-0 bg-transparent"
3680
+ className={cn(
3681
+ "relative block h-full w-full border-0 bg-transparent",
3682
+ liveEditTransitionFallbackHtml && "pointer-events-none opacity-0",
3683
+ )}
2833
3684
  style={{
2834
3685
  background: iframeBackgroundColor,
2835
3686
  backgroundColor: iframeBackgroundColor,
@@ -2868,9 +3719,84 @@ export function DesignCanvas({
2868
3719
  </div>
2869
3720
  </div>
2870
3721
  ) : null}
2871
- {waitingForEditableExternalSnapshot || waitingForLiveEditBridge ? (
3722
+ {liveEditSameInstanceStalledError?.bridgeKey === liveEditBridgeKey ? (
3723
+ // Non-destructive counterpart to the registration-failure card below:
3724
+ // the same-instance-id escalation loop in handleSuspectedBridgeRestart
3725
+ // hit its ceiling, but /health confirmed the bridge process is still
3726
+ // the one we registered with — registeredLiveEditBridgeKey was never
3727
+ // nulled, so the iframe underneath is still the real, still-loading
3728
+ // document rather than the blank placeholder. Render as a small
3729
+ // floating banner (not an opaque full-cover overlay like the cards
3730
+ // below) so that still-loading iframe stays visible, and a late
3731
+ // agent-native:editor-chrome-ready handshake clears this card (see
3732
+ // the message-handler branch above) even after this point.
3733
+ <div className="pointer-events-none absolute inset-x-0 bottom-4 z-10 flex justify-center px-4">
3734
+ <div className="pointer-events-auto flex max-w-[28rem] flex-col items-center gap-2 rounded-md border bg-card px-4 py-3 shadow-sm">
3735
+ <div className="flex items-center gap-1.5 font-medium text-foreground">
3736
+ <IconPlugConnectedX className="size-4 shrink-0 text-destructive" />
3737
+ {
3738
+ "Live editor connection failed" /* i18n-ignore local dev bridge same-instance stall title, mirrors the registration-failure card's copy */
3739
+ }
3740
+ </div>
3741
+ <div className="text-xs text-muted-foreground">
3742
+ {
3743
+ "Is the local dev server still running?" /* i18n-ignore local dev bridge same-instance stall subtitle */
3744
+ }
3745
+ </div>
3746
+ <div className="w-full truncate rounded bg-muted px-2 py-1 font-mono text-[11px] text-muted-foreground">
3747
+ {liveEditSameInstanceStalledError.message}
3748
+ </div>
3749
+ <Button
3750
+ type="button"
3751
+ variant="outline"
3752
+ size="sm"
3753
+ onClick={handleManualLiveEditSameInstanceRetry}
3754
+ >
3755
+ <IconRefresh className="size-3.5" />
3756
+ {"Retry" /* i18n-ignore local dev bridge retry button */}
3757
+ </Button>
3758
+ </div>
3759
+ </div>
3760
+ ) : null}
3761
+ {waitingForEditableExternalSnapshot ||
3762
+ waitingForLiveEditBridge ||
3763
+ (liveEditDocumentPending && !liveEditTransitionFallbackHtml) ? (
2872
3764
  <div className="pointer-events-auto absolute inset-0 z-10 flex items-center justify-center bg-background/85 px-4 text-center text-sm text-muted-foreground">
2873
- {waitingForLiveEditBridge ? (
3765
+ {waitingForLiveEditBridge &&
3766
+ bridgeRegistrationError?.bridgeKey === liveEditBridgeKey ? (
3767
+ // Mirrors the snapshot-fetch offline card below: a stuck
3768
+ // registration used to look identical to ordinary "still
3769
+ // registering" with no explanation and no way to recover short
3770
+ // of a full page reload. Only shown for the CURRENT
3771
+ // liveEditBridgeKey — a stale error from a previous script
3772
+ // revision must not linger after content/mode changes move on
3773
+ // to registering a new one.
3774
+ <div className="pointer-events-auto flex max-w-[28rem] flex-col items-center gap-2 rounded-md border bg-card px-4 py-3 shadow-sm">
3775
+ <div className="flex items-center gap-1.5 font-medium text-foreground">
3776
+ <IconPlugConnectedX className="size-4 shrink-0 text-destructive" />
3777
+ {
3778
+ "Live editor connection failed" /* i18n-ignore local dev bridge registration failure title */
3779
+ }
3780
+ </div>
3781
+ <div className="text-xs text-muted-foreground">
3782
+ {
3783
+ "Is the local dev server still running?" /* i18n-ignore local dev bridge registration failure subtitle */
3784
+ }
3785
+ </div>
3786
+ <div className="w-full truncate rounded bg-muted px-2 py-1 font-mono text-[11px] text-muted-foreground">
3787
+ {bridgeRegistrationError.message}
3788
+ </div>
3789
+ <Button
3790
+ type="button"
3791
+ variant="outline"
3792
+ size="sm"
3793
+ onClick={handleManualBridgeRegistrationRetry}
3794
+ >
3795
+ <IconRefresh className="size-3.5" />
3796
+ {"Retry" /* i18n-ignore local dev bridge retry button */}
3797
+ </Button>
3798
+ </div>
3799
+ ) : waitingForLiveEditBridge ? (
2874
3800
  <div className="max-w-[28rem] rounded-md border bg-card px-4 py-3 shadow-sm">
2875
3801
  {
2876
3802
  "Preparing live editor..." /* i18n-ignore transient localhost live-edit bridge loading state */
@@ -2923,11 +3849,16 @@ export function DesignCanvas({
2923
3849
  {/* Draw-to-prompt overlay — sits over the iframe, NOT inside it. */}
2924
3850
  <SharedDrawOverlay
2925
3851
  visible={!!drawMode}
3852
+ clearSignal={drawOverlayResetSignal}
3853
+ scopeKey={screenId}
3854
+ retainSurfaceWhenHidden={retainDrawOverlayWhenHidden}
2926
3855
  canvasInteractive={!pinMode}
2927
3856
  queuedAnnotationCount={queuedAnnotationPins.length}
2928
3857
  zoom={zoom}
3858
+ sending={annotationCaptureBusy}
2929
3859
  onClose={() => onExitDrawMode?.()}
2930
3860
  onSend={(annotations, instruction, canvasSize) => {
3861
+ if (annotationCaptureBusyRef.current) return;
2931
3862
  const summary = annotations
2932
3863
  .map((a) =>
2933
3864
  a.type === "path"
@@ -2960,25 +3891,70 @@ export function DesignCanvas({
2960
3891
  "",
2961
3892
  instruction || "Apply these annotations to the design.",
2962
3893
  ];
2963
- submitDesignAnnotations({
2964
- message: lines.join("\n"),
2965
- hasQueuedPins: queuedAnnotationPins.length > 0,
2966
- send: (message) => {
2967
- sendToDesignAgentChat({
3894
+ const message = lines.join("\n");
3895
+ const hasQueuedPins = queuedAnnotationPins.length > 0;
3896
+
3897
+ // Best-effort: render the annotated screen + composited drawing as
3898
+ // ONE image the agent can see (see design-canvas/annotation-snapshot.ts),
3899
+ // then send it alongside the existing text summary in the same chat
3900
+ // turn. Never blocks or drops the annotation — any capture failure
3901
+ // (no Chromium in hosted deploys, network blip, upload not
3902
+ // configured, timeout) silently resolves to `null` and the message
3903
+ // still goes out text-only, exactly as it did before this feature.
3904
+ annotationCaptureBusyRef.current = true;
3905
+ setAnnotationCaptureBusy(true);
3906
+ onAnnotationSendingChange?.(true);
3907
+ captureAnnotatedScreenshot({
3908
+ designId,
3909
+ fileId: screenId,
3910
+ sourceType:
3911
+ sourceType ?? (externalPreviewUrl ? "localhost" : "inline"),
3912
+ annotations,
3913
+ canvasSize,
3914
+ })
3915
+ .catch(() => null)
3916
+ .then((imageUrl) =>
3917
+ submitDesignAnnotations({
2968
3918
  message,
2969
- submit: true,
2970
- openSidebar: true,
2971
- });
2972
- },
2973
- markQueuedPinsSubmitted: () => {
2974
- setPinSubmitSignal((signal) => signal + 1);
2975
- },
2976
- exitDrawMode: () => onExitDrawMode?.(),
2977
- onError: (error) => {
2978
- console.error("[DesignCanvas] failed to submit drawing:", error);
2979
- toast.error(t("designEditor.toasts.annotationSendError"));
2980
- },
2981
- });
3919
+ hasQueuedPins,
3920
+ // Ack-confirmed send: only exit draw mode / mark pins
3921
+ // submitted once the message is CONFIRMED to have reached
3922
+ // the chat (became a visible turn). A fire-and-forget send
3923
+ // here would let the overlay tear down and the pins clear
3924
+ // even when delivery is silently dropped — e.g. no LLM/agent
3925
+ // engine configured, or the panel never finished mounting —
3926
+ // discarding the user's drawing with no way to retry it.
3927
+ send: async (message) => {
3928
+ const result = await sendToDesignAgentChatAndConfirm({
3929
+ message,
3930
+ images: imageUrl ? [imageUrl] : undefined,
3931
+ submit: true,
3932
+ openSidebar: true,
3933
+ });
3934
+ if (!result.delivered) {
3935
+ throw new Error(
3936
+ `Annotation message was not delivered to the agent chat (${result.reason ?? "unknown"})`,
3937
+ );
3938
+ }
3939
+ },
3940
+ markQueuedPinsSubmitted: () => {
3941
+ setPinSubmitSignal((signal) => signal + 1);
3942
+ },
3943
+ exitDrawMode: () => onExitDrawMode?.(),
3944
+ onError: (error) => {
3945
+ console.error(
3946
+ "[DesignCanvas] failed to submit drawing:",
3947
+ error,
3948
+ );
3949
+ toast.error(t("designEditor.toasts.annotationSendError"));
3950
+ },
3951
+ }),
3952
+ )
3953
+ .finally(() => {
3954
+ annotationCaptureBusyRef.current = false;
3955
+ setAnnotationCaptureBusy(false);
3956
+ onAnnotationSendingChange?.(false);
3957
+ });
2982
3958
  }}
2983
3959
  />
2984
3960
  </div>
@@ -3222,15 +4198,28 @@ function SingleScreenCreationOverlay({
3222
4198
  const [penPointer, setPenPointer] = useState<PenPoint | null>(null);
3223
4199
  const [penCloseHover, setPenCloseHover] = useState(false);
3224
4200
 
4201
+ // Returns the click/pointer point in SCREEN-CONTENT (document-absolute)
4202
+ // coordinates — the same "screen's own pixel coordinate system" contract
4203
+ // `onCreatePrimitive` documents, which already folds in the embedded
4204
+ // screen's own internal scroll (see `getScreenContentPointFromClient`).
4205
+ // Anything that renders these points directly as this overlay's own CSS
4206
+ // position (the drag/line preview below, and `SingleScreenPenPathOverlay`)
4207
+ // must convert back to overlay-local (viewport-relative) coordinates by
4208
+ // subtracting the same scroll offset — the overlay itself never scrolls
4209
+ // with the embedded screen's internal document, it only tracks the
4210
+ // iframe's host-page box.
3225
4211
  const toContentPoint = useCallback(
3226
4212
  (clientX: number, clientY: number) => {
3227
4213
  const iframe = iframeRef.current;
3228
4214
  const rect = iframe?.getBoundingClientRect();
3229
4215
  if (!iframe || !rect) return { x: clientX, y: clientY };
3230
- return getScreenContentPointFromClient(clientX, clientY, rect, {
3231
- width: iframe.clientWidth,
3232
- height: iframe.clientHeight,
3233
- });
4216
+ return getScreenContentPointFromClient(
4217
+ clientX,
4218
+ clientY,
4219
+ rect,
4220
+ { width: iframe.clientWidth, height: iframe.clientHeight },
4221
+ readIframeScrollOffset(iframe),
4222
+ );
3234
4223
  },
3235
4224
  [iframeRef],
3236
4225
  );
@@ -3524,7 +4513,7 @@ function SingleScreenCreationOverlay({
3524
4513
  e.currentTarget.releasePointerCapture(e.pointerId);
3525
4514
  }
3526
4515
  if (gesture.closing) {
3527
- finishPenPath(nextPath);
4516
+ finishPenPath(nextPath, { preserveActiveTool: true });
3528
4517
  } else {
3529
4518
  updatePenPath(nextPath);
3530
4519
  }
@@ -3602,7 +4591,7 @@ function SingleScreenCreationOverlay({
3602
4591
  event.preventDefault();
3603
4592
  event.stopPropagation();
3604
4593
  event.stopImmediatePropagation();
3605
- finishPenPath(path);
4594
+ finishPenPath(path, { preserveActiveTool: true });
3606
4595
  };
3607
4596
 
3608
4597
  window.addEventListener("keydown", handleKeyDown, true);
@@ -3623,19 +4612,49 @@ function SingleScreenCreationOverlay({
3623
4612
  // overlay's own (unscaled, layout-space) coordinates. The overlay div is
3624
4613
  // sized to the iframe's layout box (via inset-0 on a wrapper that already
3625
4614
  // matches iframe.clientWidth/Height 1:1 before the outer zoom transform),
3626
- // so screen-content coordinates ARE this overlay's local coordinates
3627
- // no extra scale conversion is needed for the preview's own CSS position.
4615
+ // so screen-content coordinates equal this overlay's local coordinates
4616
+ // when the screen is unscrolled — no extra scale conversion is needed for
4617
+ // the preview's own CSS position. When the embedded screen IS scrolled,
4618
+ // `drag.startContent`/`currentContent` (from `toContentPoint`) carry that
4619
+ // scroll baked in (screen-content space is document-absolute, per
4620
+ // `onCreatePrimitive`'s contract), but this overlay div never scrolls with
4621
+ // the iframe's internal document — it only tracks the iframe's fixed host-
4622
+ // page box — so the scroll must be subtracted back out here to keep the
4623
+ // live preview aligned under the cursor.
4624
+ const previewScrollOffset = readIframeScrollOffset(iframeRef.current);
4625
+ const toOverlayLocal = (point: { x: number; y: number }) => ({
4626
+ x: point.x - previewScrollOffset.left,
4627
+ y: point.y - previewScrollOffset.top,
4628
+ });
3628
4629
  const previewRect =
3629
4630
  drag && drag.moved && !isLineTool && tool !== "pen"
3630
- ? getDraftGeometryFromPoints(drag.startContent, drag.currentContent, {
3631
- minWidth: tool === "text" ? 24 : 8,
3632
- minHeight: tool === "text" ? 18 : 8,
3633
- })
4631
+ ? getDraftGeometryFromPoints(
4632
+ toOverlayLocal(drag.startContent),
4633
+ toOverlayLocal(drag.currentContent),
4634
+ {
4635
+ minWidth: tool === "text" ? 24 : 8,
4636
+ minHeight: tool === "text" ? 18 : 8,
4637
+ },
4638
+ )
3634
4639
  : null;
3635
4640
  const previewLine =
3636
4641
  drag && drag.moved && isLineTool
3637
- ? { start: drag.startContent, end: drag.currentContent }
4642
+ ? {
4643
+ start: toOverlayLocal(drag.startContent),
4644
+ end: toOverlayLocal(drag.currentContent),
4645
+ }
3638
4646
  : null;
4647
+ // Same overlay-local conversion for the live pen path — its anchors/
4648
+ // handles are stored in screen-content (document-absolute) space via
4649
+ // `toContentPoint`/`getPenAnchor`, but `SingleScreenPenPathOverlay` renders
4650
+ // them directly as this overlay's own (unscrolled) SVG/CSS coordinates.
4651
+ const displayedPenPathOverlay = displayedPenPath
4652
+ ? translatePenPath(
4653
+ displayedPenPath,
4654
+ -previewScrollOffset.left,
4655
+ -previewScrollOffset.top,
4656
+ )
4657
+ : null;
3639
4658
 
3640
4659
  return (
3641
4660
  <div
@@ -3659,9 +4678,9 @@ function SingleScreenCreationOverlay({
3659
4678
  }
3660
4679
  }}
3661
4680
  >
3662
- {tool === "pen" && displayedPenPath ? (
4681
+ {tool === "pen" && displayedPenPathOverlay ? (
3663
4682
  <SingleScreenPenPathOverlay
3664
- path={displayedPenPath}
4683
+ path={displayedPenPathOverlay}
3665
4684
  closeHover={penCloseHover}
3666
4685
  iframeRef={iframeRef}
3667
4686
  />