@agent-native/core 0.92.10 → 0.92.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +23 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +181 -15
- package/corpus/core/src/agent/run-manager.ts +31 -4
- package/corpus/core/src/agent/run-store.ts +80 -0
- package/corpus/core/src/cli/design-connect.ts +433 -151
- package/corpus/core/src/cli/skills.ts +14 -0
- package/corpus/core/src/client/AssistantChat.tsx +25 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +40 -6
- package/corpus/core/src/client/agent-chat.ts +165 -1
- package/corpus/core/src/client/chat/index.ts +9 -1
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +38 -2
- package/corpus/core/src/client/index.ts +5 -0
- package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +18 -7
- package/corpus/core/src/collab/awareness-store.ts +23 -12
- package/corpus/core/src/collab/client.ts +16 -5
- package/corpus/core/src/collab/index.ts +4 -0
- package/corpus/core/src/collab/recent-edits.ts +65 -2
- package/corpus/core/src/index.browser.ts +1 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +98 -11
- package/corpus/core/src/server/builder-design-systems.ts +30 -4
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +1 -1
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +12 -6
- package/corpus/templates/clips/AGENTS.md +4 -0
- package/corpus/templates/clips/actions/create-recording.ts +2 -0
- package/corpus/templates/clips/actions/lib/create-recording-schema.ts +5 -3
- package/corpus/templates/clips/actions/request-transcript.ts +102 -33
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -0
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +59 -19
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +48 -4
- package/corpus/templates/clips/app/components/player/video-player.tsx +17 -12
- package/corpus/templates/clips/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -37
- package/corpus/templates/clips/app/routes/record.tsx +17 -4
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
- package/corpus/templates/clips/changelog/2026-07-09-desktop-popover-sits-closer-to-the-menu-bar.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-desktop-recording-optimization-progress-stays-visible-until-the-clip-opens.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-fixed-large-video-file-uploads-failing-before-their-resumabl.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-fixed-new-video-uploads-defaulting-to-private.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-long-recordings-now-get-enough-time-to-download-their-media-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-meeting-notes-no-longer-lowers-your-microphone-volume-for-ot.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-on-mobile-tapping-a-playing-clip-reveals-playback-controls-w.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-regenerate-any-clip-transcript-from-its-saved-recording-or-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-09-slack-previews-now-show-the-clip-length-alongside-its-descri.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +12 -2
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +8 -1
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +8 -5
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +5 -5
- package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +3 -0
- package/corpus/templates/clips/desktop/src/styles.css +6 -3
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +29 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +103 -21
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +117 -39
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +20 -1
- package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +10 -4
- package/corpus/templates/clips/shared/recording-core.ts +12 -0
- package/corpus/templates/content/AGENTS.md +7 -6
- package/corpus/templates/content/actions/_document-flush.ts +132 -17
- package/corpus/templates/content/actions/_notion-action-utils.ts +15 -0
- package/corpus/templates/content/actions/create-and-link-notion-page.ts +2 -0
- package/corpus/templates/content/actions/link-notion-page.ts +2 -0
- package/corpus/templates/content/actions/pull-document.ts +5 -7
- package/corpus/templates/content/actions/pull-notion-page.ts +2 -0
- package/corpus/templates/content/actions/push-notion-page.ts +10 -0
- package/corpus/templates/content/actions/resolve-notion-sync-conflict.ts +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +88 -23
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +32 -2
- package/corpus/templates/content/app/hooks/use-notion.ts +6 -6
- package/corpus/templates/content/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/content/app/i18n-data.ts +18 -0
- package/corpus/templates/content/changelog/2026-07-09-notion-conflict-choices-and-version-restores-preserve-docume.md +6 -0
- package/corpus/templates/content/server/lib/notion-sync.ts +80 -64
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +29 -2
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +86 -0
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +57 -0
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +11 -0
- package/corpus/templates/design/AGENTS.md +45 -10
- package/corpus/templates/design/actions/add-localhost-screens.ts +66 -13
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +45 -36
- package/corpus/templates/design/actions/apply-motion-edit.ts +25 -31
- package/corpus/templates/design/actions/apply-shader-fill.ts +29 -42
- package/corpus/templates/design/actions/detach-component-instance.ts +337 -0
- package/corpus/templates/design/actions/export-design-as-figma-svg.ts +219 -0
- package/corpus/templates/design/actions/generate-design.ts +79 -12
- package/corpus/templates/design/actions/generate-screens.ts +57 -12
- package/corpus/templates/design/actions/get-figma-styles.ts +115 -0
- package/corpus/templates/design/actions/go-to-main-component.ts +254 -0
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +4 -3
- package/corpus/templates/design/actions/import-design-source.ts +4 -31
- package/corpus/templates/design/actions/import-figma-clipboard.ts +134 -0
- package/corpus/templates/design/actions/import-figma-frame.ts +95 -0
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +16 -1
- package/corpus/templates/design/actions/list-design-components.ts +104 -0
- package/corpus/templates/design/actions/list-figma-library-assets.ts +1 -24
- package/corpus/templates/design/actions/open-component-source.ts +2 -4
- package/corpus/templates/design/actions/present-design-variants.ts +412 -16
- package/corpus/templates/design/actions/preview-source-edit.ts +14 -1
- package/corpus/templates/design/actions/run-design-audit.ts +25 -3
- package/corpus/templates/design/actions/show-design-questions.ts +10 -0
- package/corpus/templates/design/actions/swap-component-instance.ts +544 -0
- package/corpus/templates/design/actions/take-design-screenshot.ts +45 -60
- package/corpus/templates/design/actions/write-local-file.ts +25 -1
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +152 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1077 -58
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +5 -5
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +73 -21
- package/corpus/templates/design/app/components/design/EditPanel.tsx +16 -11
- package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +364 -0
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +180 -53
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +0 -2
- package/corpus/templates/design/app/components/design/MotionDock.tsx +144 -21
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1152 -167
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +27 -5
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +23 -2
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +63 -49
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1293 -54
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +7 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +6 -1
- package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +23 -6
- package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +7 -0
- package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -0
- package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +14 -2
- package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +37 -2
- package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open-guard.ts +25 -0
- package/corpus/templates/design/app/components/design/code-workbench/format/format-on-open.ts +19 -1
- package/corpus/templates/design/app/components/design/code-workbench/model-registry.ts +43 -5
- package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +27 -2
- package/corpus/templates/design/app/components/design/code-workbench/search/search-engine.ts +10 -1
- package/corpus/templates/design/app/components/design/code-workbench/store.tsx +19 -2
- package/corpus/templates/design/app/components/design/design-canvas/annotation-snapshot.ts +265 -0
- package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +16 -5
- package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +16 -2
- package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +7 -2
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +3 -1
- package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +78 -8
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +451 -73
- package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +12 -4
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +398 -272
- package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +41 -7
- package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +16 -3
- package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +24 -4
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +222 -4
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +171 -78
- package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +45 -5
- package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +299 -28
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +120 -150
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +53 -1
- package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +284 -117
- package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +81 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +19 -14
- package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +7 -0
- package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +155 -0
- package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +268 -46
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +350 -31
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +13 -4
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +233 -23
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +72 -2
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +51 -11
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +84 -11
- package/corpus/templates/design/app/components/design/inspector/index.ts +3 -0
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +552 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +157 -0
- package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +53 -0
- package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +184 -0
- package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +403 -26
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +20 -1
- package/corpus/templates/design/app/components/design/types.ts +33 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +59 -7
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +119 -17
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +275 -63
- package/corpus/templates/design/app/hooks/use-question-flow.ts +14 -3
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +67 -10
- package/corpus/templates/design/app/i18n/zh-TW.ts +91 -1
- package/corpus/templates/design/app/i18n-data.ts +1217 -10
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +577 -0
- package/corpus/templates/design/app/lib/agent-chat.ts +23 -0
- package/corpus/templates/design/app/lib/design-import.ts +24 -0
- package/corpus/templates/design/app/lib/figma-clipboard.ts +157 -0
- package/corpus/templates/design/app/lib/figma-svg-copy.ts +221 -0
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +143 -30
- package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +15 -0
- package/corpus/templates/design/app/pages/design-editor/editor-session.ts +33 -0
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +38 -0
- package/corpus/templates/design/app/pages/design-editor/overview-annotation-context.ts +157 -0
- package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +62 -6
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +245 -0
- package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +586 -0
- package/corpus/templates/design/app/pages/design-editor/screen-auto-layout.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +201 -1
- package/corpus/templates/design/changelog/2026-07-09-added-underline-strikethrough-and-text-case-uppercase-lowerc.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-annotate-now-draws-across-the-all-screens-canvas-without-swi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-auto-layout-children-can-now-be-dragged-into-freeform-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-board-shapes-now-stay-visible-and-nest-correctly-across-the-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-component-source-navigation-now-opens-reliably-from-the-insp.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-cross-screen-local-react-layer-moves-now-hand-off-exact-sour.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-cross-screen-moves-undo-and-redo-now-keep-both-screens-stabl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-detach-a-component-instance-into-plain-editable-eleme.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-dragging-layers-into-auto-layout-now-respects-ignore-auto-la.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-drawing-annotations-sent-to-the-agent-now-include-a-rendered.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-editor-sidebars-now-use-figma-matched-default-widths-for-a-r.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-empty-frames-and-rectangles-now-expose-full-auto-layout-cont.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-export-any-design-screen-as-a-real-vector-svg-for-figma-with.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-grid-auto-layout-now-uses-true-responsive-rows-and-columns-w.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-hidden-and-locked-screens-now-behave-consistently-between-la.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-image-fills-now-preserve-valid-urls-and-layered-fit-settings.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-import-figma-frames-as-pixel-accurate-design-screens-via-a-s.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-inspect-code-now-hides-internal-styling-metadata-and-wraps-l.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-inspector-blur-and-shadow-effects-now-preserve-other-effects.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-jump-to-a-components-other-instances-from-the-inspecto.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-keyboard-shortcuts-are-now-discoverable-in-a-figma-style-bot.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-layer-selection-now-keeps-cmd-or-ctrl-toggles-and-shift-rang.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-layers-dropped-into-auto-layout-now-participate-correctly-in.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-live-edit-screens-now-enter-and-leave-full-view-withou.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-preview-urls-now-live-in-the-inspector-instead-of-taki.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-react-layers-can-now-be-hidden-and-locked-from-layers-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-react-layers-now-keep-exact-source-provenance-and-supp.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-local-react-visual-editing-now-loads-reliably-and-shows-live.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-paste-from-figma-cmd-v-now-imports-exact-editable-nodes-when.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-pressing-escape-after-selecting-a-screen-now-reveals-page-pr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-right-click-overlapping-or-nested-objects-to-choose-the-exac.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-scale-edge-stretch-and-center-constraints-now-preserve-layer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-screen-rows-can-now-be-reordered-in-layers-with-matching-can.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-screen-selection-stays-responsive-in-designs-with-hundreds-o.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-shift-a-now-enables-auto-layout-on-a-selected-screen-with-fl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-swap-a-component-instance-for-another-from-the-libra.md +6 -0
- package/corpus/templates/design/changelog/2026-07-10-drawing-annotations-now-stay-available-to-retry-when-the.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +3 -6
- package/corpus/templates/design/e2e/helpers.ts +27 -6
- package/corpus/templates/design/server/lib/design-to-figma-svg.ts +1412 -0
- package/corpus/templates/design/server/lib/figma-clipboard-match.ts +196 -0
- package/corpus/templates/design/server/lib/figma-node-import.ts +311 -0
- package/corpus/templates/design/server/lib/figma-node-to-html.ts +1334 -0
- package/corpus/templates/design/server/lib/figma-paste-fallback.ts +64 -0
- package/corpus/templates/design/server/lib/playwright-runtime.ts +78 -0
- package/corpus/templates/design/server/plugins/db.ts +41 -0
- package/corpus/templates/design/server/source-workspace.ts +64 -0
- package/corpus/templates/design/shared/board-file.ts +13 -2
- package/corpus/templates/design/shared/canvas-math.ts +231 -33
- package/corpus/templates/design/shared/code-layer.ts +122 -8
- package/corpus/templates/design/shared/component-library.ts +161 -0
- package/corpus/templates/design/shared/db-conflict.ts +27 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +2 -1
- package/corpus/templates/design/shared/figma-url.ts +149 -0
- package/corpus/templates/design/shared/pen-path.ts +16 -7
- package/corpus/templates/design/shared/source-mode.ts +1 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +7 -19
- package/corpus/templates/plan/changelog/2026-07-09-canvas-panning-now-keeps-zoom-fixed-even-at-the-edges.md +6 -0
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +16 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +311 -70
- package/corpus/templates/slides/changelog/2026-07-09-fixed-the-slide-rail-going-permanently-stale-during-a-long-a.md +6 -0
- package/dist/agent/production-agent.d.ts +79 -6
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +161 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +29 -5
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +46 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +61 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/design-connect.d.ts +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +336 -139
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +14 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +19 -3
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +38 -18
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +60 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +122 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/chat/index.d.ts +1 -1
- package/dist/client/chat/index.d.ts.map +1 -1
- package/dist/client/chat/index.js +1 -1
- package/dist/client/chat/index.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts +12 -0
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +36 -2
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js +18 -6
- package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
- package/dist/collab/awareness-store.d.ts +5 -0
- package/dist/collab/awareness-store.d.ts.map +1 -1
- package/dist/collab/awareness-store.js +19 -12
- package/dist/collab/awareness-store.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +13 -5
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/index.d.ts +1 -0
- package/dist/collab/index.d.ts.map +1 -1
- package/dist/collab/index.js +1 -0
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/recent-edits.d.ts +3 -1
- package/dist/collab/recent-edits.d.ts.map +1 -1
- package/dist/collab/recent-edits.js +57 -2
- package/dist/collab/recent-edits.js.map +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +87 -10
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-design-systems.d.ts +15 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +9 -4
- package/dist/server/builder-design-systems.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
865
|
-
|
|
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
|
-
|
|
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
|
-
|
|
941
|
-
).replace(
|
|
942
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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(
|
|
2751
|
-
|
|
2752
|
-
|
|
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=
|
|
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
|
-
{
|
|
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
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
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
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
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(
|
|
3231
|
-
|
|
3232
|
-
|
|
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
|
|
3627
|
-
// no extra scale conversion is needed for
|
|
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(
|
|
3631
|
-
|
|
3632
|
-
|
|
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
|
-
? {
|
|
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" &&
|
|
4681
|
+
{tool === "pen" && displayedPenPathOverlay ? (
|
|
3663
4682
|
<SingleScreenPenPathOverlay
|
|
3664
|
-
path={
|
|
4683
|
+
path={displayedPenPathOverlay}
|
|
3665
4684
|
closeHover={penCloseHover}
|
|
3666
4685
|
iframeRef={iframeRef}
|
|
3667
4686
|
/>
|