@agent-native/core 0.90.4 → 0.90.7
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/README.md +34 -7
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
- package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
- package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
- package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
- package/corpus/core/docs/content/toolkit-history.mdx +1 -1
- package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
- package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
- package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
- package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
- package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
- package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +18 -18
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
- package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
- package/corpus/core/src/agent/production-agent.ts +22 -11
- package/corpus/core/src/agent/run-manager.ts +17 -6
- package/corpus/core/src/agent/run-store.ts +16 -11
- package/corpus/core/src/cli/design-connect.ts +16 -2
- package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
- package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
- package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
- package/corpus/core/src/client/use-db-sync.ts +99 -1
- package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
- package/corpus/core/src/collab/client.ts +49 -4
- package/corpus/core/src/deploy/build.ts +103 -0
- package/corpus/core/src/server/core-routes-plugin.ts +137 -8
- package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
- package/corpus/templates/content/AGENTS.md +1 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
- package/corpus/templates/content/actions/_database-utils.ts +2 -1
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
- package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
- package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
- package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
- package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/content/app/i18n-data.ts +31 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
- package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
- package/corpus/templates/content/parity/matrix.md +24 -24
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +59 -4
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
- package/corpus/templates/design/actions/connect-localhost.ts +26 -3
- package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
- package/corpus/templates/design/actions/read-source-file.ts +28 -0
- package/corpus/templates/design/actions/update-file.ts +76 -7
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +207 -11
- package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
- package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +19 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +27 -3
- package/corpus/templates/design/shared/board-file.ts +359 -0
- package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
- package/corpus/templates/design/shared/code-layer.ts +145 -10
- package/dist/agent/durable-background.d.ts +12 -12
- package/dist/agent/durable-background.js +18 -18
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +8 -2
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
- package/dist/agent/engine/anthropic-engine.js +11 -1
- package/dist/agent/engine/anthropic-engine.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -11
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +7 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +6 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +15 -10
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +12 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/KeepTabOpenNotice.d.ts +9 -10
- package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
- package/dist/client/KeepTabOpenNotice.js +8 -4
- package/dist/client/KeepTabOpenNotice.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +14 -13
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +25 -17
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +18 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +88 -1
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +6 -4
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +43 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +1 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +57 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +10 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +103 -9
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-app-adapters.mdx +1 -1
- package/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/docs/content/toolkit-collaboration.mdx +1 -1
- package/docs/content/toolkit-command-navigation.mdx +2 -2
- package/docs/content/toolkit-comments-review.mdx +1 -1
- package/docs/content/toolkit-history.mdx +1 -1
- package/docs/content/toolkit-observability.mdx +1 -1
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +1 -1
- package/docs/content/toolkit-settings.mdx +1 -1
- package/docs/content/toolkit-setup-connections.mdx +1 -1
- package/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/docs/content/toolkit-sharing.mdx +1 -1
- package/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/docs/content/toolkit-ui.mdx +1 -1
- package/package.json +1 -1
|
@@ -612,6 +612,365 @@ function _inferSvgPrimitiveKind(inner: string): string | null {
|
|
|
612
612
|
return null;
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
// Poisoned nested-coordinate normalization
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Half-size of the board's oversized iframe surface (131072 / 2). The board
|
|
621
|
+
* renderer shifts top-level board elements by this amount via a
|
|
622
|
+
* `body > [data-agent-native-node-id]{translate:65536px 65536px}` rule
|
|
623
|
+
* (DesignCanvas.tsx embeddedContentOffsetStyle) so board coordinate (0,0)
|
|
624
|
+
* lands at the center of the surface.
|
|
625
|
+
*
|
|
626
|
+
* Historic bugs wrote NESTED board children's left/top in board-IFRAME
|
|
627
|
+
* viewport coordinates (board coordinate + 65536) instead of parent-relative
|
|
628
|
+
* coordinates — and, before the offset rule was scoped to top-level children,
|
|
629
|
+
* the compounding translate could stack the offset more than once. The
|
|
630
|
+
* helpers below detect and repair those persisted values.
|
|
631
|
+
*/
|
|
632
|
+
export const BOARD_SURFACE_CONTENT_OFFSET_PX = 65_536;
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* How far from an exact 65536-multiple a coordinate may sit and still be
|
|
636
|
+
* classified as "poisoned by the surface offset". Real poisoned values look
|
|
637
|
+
* like `k * 65536 + boardCoordinate` where the board coordinate of actual
|
|
638
|
+
* content is at most a few thousand px from origin; real parent-relative
|
|
639
|
+
* coordinates of nested children are bounded by their parent's size (far
|
|
640
|
+
* below this). A quarter of the offset is a comfortable margin on both sides.
|
|
641
|
+
*/
|
|
642
|
+
const BOARD_COORD_POISON_REMAINDER_MAX_PX = 16_384;
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Largest magnitude a rebased nested coordinate may have (when the parent's
|
|
646
|
+
* size is unknown) before it is considered garbage and clamped back into the
|
|
647
|
+
* parent's box. No sane nested child sits 16k px from its parent's origin.
|
|
648
|
+
*/
|
|
649
|
+
const BOARD_NESTED_COORD_SANE_MAX_PX = 16_384;
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Returns true when a persisted left/top value carries the board-surface
|
|
653
|
+
* offset fingerprint: within BOARD_COORD_POISON_REMAINDER_MAX_PX of a
|
|
654
|
+
* non-zero multiple of 65536.
|
|
655
|
+
*/
|
|
656
|
+
export function isBoardSurfacePoisonedCoord(value: number): boolean {
|
|
657
|
+
if (!Number.isFinite(value)) return false;
|
|
658
|
+
const k = Math.round(value / BOARD_SURFACE_CONTENT_OFFSET_PX);
|
|
659
|
+
if (k === 0) return false;
|
|
660
|
+
return (
|
|
661
|
+
Math.abs(value - k * BOARD_SURFACE_CONTENT_OFFSET_PX) <=
|
|
662
|
+
BOARD_COORD_POISON_REMAINDER_MAX_PX
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Strips the board-surface offset from a coordinate when (and only when) it
|
|
668
|
+
* carries the poison fingerprint; all other values pass through untouched.
|
|
669
|
+
* `66904 → 1368`, `-65420 → 116`, `131172 → 100`, `250 → 250`.
|
|
670
|
+
*/
|
|
671
|
+
export function stripBoardSurfaceOffsetFromCoord(value: number): number {
|
|
672
|
+
if (!isBoardSurfacePoisonedCoord(value)) return value;
|
|
673
|
+
const k = Math.round(value / BOARD_SURFACE_CONTENT_OFFSET_PX);
|
|
674
|
+
return value - k * BOARD_SURFACE_CONTENT_OFFSET_PX;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Parent-relative position for a node being reparented INTO a target
|
|
679
|
+
* container, from the two elements' persisted absolute positions in the same
|
|
680
|
+
* coordinate space. This is the pure seam used by
|
|
681
|
+
* `handleOverviewPrimitiveReparent` (DesignEditor.tsx).
|
|
682
|
+
*
|
|
683
|
+
* Both inputs are normalized with `stripBoardSurfaceOffsetFromCoord` first:
|
|
684
|
+
* a source that was persisted in board-iframe viewport coordinates
|
|
685
|
+
* (boardCoord + 65536 — see BOARD_SURFACE_CONTENT_OFFSET_PX) is rebased to
|
|
686
|
+
* board space before subtracting the target's origin, so the child's new
|
|
687
|
+
* left/top always comes out parent-relative regardless of which upstream
|
|
688
|
+
* path produced the source position. For in-screen reparenting (small
|
|
689
|
+
* screen-root coordinates on both sides) the strip is a no-op and this is a
|
|
690
|
+
* plain subtraction.
|
|
691
|
+
*/
|
|
692
|
+
export function computeReparentedChildPosition(
|
|
693
|
+
source: { x: number; y: number },
|
|
694
|
+
target: { x: number; y: number },
|
|
695
|
+
): { x: number; y: number } {
|
|
696
|
+
return {
|
|
697
|
+
x:
|
|
698
|
+
stripBoardSurfaceOffsetFromCoord(source.x) -
|
|
699
|
+
stripBoardSurfaceOffsetFromCoord(target.x),
|
|
700
|
+
y:
|
|
701
|
+
stripBoardSurfaceOffsetFromCoord(source.y) -
|
|
702
|
+
stripBoardSurfaceOffsetFromCoord(target.y),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
interface BoardWalkFrame {
|
|
707
|
+
tagName: string;
|
|
708
|
+
isNodeIdElement: boolean;
|
|
709
|
+
left: number | null;
|
|
710
|
+
top: number | null;
|
|
711
|
+
width: number | null;
|
|
712
|
+
height: number | null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const BOARD_HTML_TAG_RE =
|
|
716
|
+
/<!--[\s\S]*?-->|<\/?([a-zA-Z][\w:-]*)((?:"[^"]*"|'[^']*'|[^"'<>])*)\/?>/g;
|
|
717
|
+
|
|
718
|
+
/** Tags whose raw text content must not be tag-walked. */
|
|
719
|
+
const BOARD_RAW_TEXT_TAGS = new Set(["script", "style", "textarea"]);
|
|
720
|
+
|
|
721
|
+
function parseInlineStylePx(
|
|
722
|
+
style: string,
|
|
723
|
+
prop: "left" | "top" | "width" | "height",
|
|
724
|
+
): number | null {
|
|
725
|
+
const match = style.match(
|
|
726
|
+
new RegExp(
|
|
727
|
+
`(?:^|;)\\s*${prop}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)px\\s*(?=;|$)`,
|
|
728
|
+
"i",
|
|
729
|
+
),
|
|
730
|
+
);
|
|
731
|
+
if (!match?.[1]) return null;
|
|
732
|
+
const parsed = Number(match[1]);
|
|
733
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function replaceInlineStylePx(
|
|
737
|
+
style: string,
|
|
738
|
+
prop: "left" | "top",
|
|
739
|
+
next: number,
|
|
740
|
+
): string {
|
|
741
|
+
return style.replace(
|
|
742
|
+
new RegExp(`((?:^|;)\\s*${prop}\\s*:\\s*)(-?\\d+(?:\\.\\d+)?)px`, "i"),
|
|
743
|
+
`$1${next}px`,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function rebaseNestedBoardCoord(args: {
|
|
748
|
+
value: number;
|
|
749
|
+
ancestorOrigin: number;
|
|
750
|
+
parentSize: number | null;
|
|
751
|
+
childSize: number | null;
|
|
752
|
+
}): number {
|
|
753
|
+
const { value, ancestorOrigin, parentSize, childSize } = args;
|
|
754
|
+
if (!isBoardSurfacePoisonedCoord(value)) return value;
|
|
755
|
+
// Two distinct historic poison shapes, distinguishable by the sign of the
|
|
756
|
+
// offset multiple:
|
|
757
|
+
// - k >= 1 (positive): value = k*65536 + boardCoordinate — the child's
|
|
758
|
+
// board/viewport-space position was written verbatim (container-drop
|
|
759
|
+
// persist path). Parent-relative = board coordinate minus the parent's
|
|
760
|
+
// accumulated board-space origin.
|
|
761
|
+
// - k <= -1 (negative): value = parentRelative - 65536 — the bridge's
|
|
762
|
+
// rect-space nest rebase pre-compensated the (old, blanket) content
|
|
763
|
+
// translate. The stripped remainder already IS parent-relative; no
|
|
764
|
+
// ancestor subtraction. (A genuine board coordinate can never reach
|
|
765
|
+
// k <= -1 here: that would put content beyond the surface edge.)
|
|
766
|
+
const k = Math.round(value / BOARD_SURFACE_CONTENT_OFFSET_PX);
|
|
767
|
+
let rebased =
|
|
768
|
+
stripBoardSurfaceOffsetFromCoord(value) - (k > 0 ? ancestorOrigin : 0);
|
|
769
|
+
// Sanity clamp: exact recovery is possible for the common single-poison
|
|
770
|
+
// case, but compounded corruption (the parent moved after the child's
|
|
771
|
+
// position was written, multiple stacked offsets, user "fixes" made while
|
|
772
|
+
// the content rendered off-world) can leave a rebased value that is still
|
|
773
|
+
// far outside the parent. Pull those back into the parent's box so the
|
|
774
|
+
// child is at least visible and re-editable — the alternative is an
|
|
775
|
+
// element parked tens of thousands of px off-world.
|
|
776
|
+
const sane =
|
|
777
|
+
parentSize !== null
|
|
778
|
+
? rebased >= -parentSize && rebased <= 2 * parentSize
|
|
779
|
+
: Math.abs(rebased) <= BOARD_NESTED_COORD_SANE_MAX_PX;
|
|
780
|
+
if (!sane) {
|
|
781
|
+
const max =
|
|
782
|
+
parentSize !== null ? Math.max(0, parentSize - (childSize ?? 0)) : 0;
|
|
783
|
+
rebased = Math.min(Math.max(0, rebased), max);
|
|
784
|
+
}
|
|
785
|
+
return Math.round(rebased);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Repairs already-persisted board content whose NESTED children carry
|
|
790
|
+
* left/top values poisoned by the board-surface offset (near-65536-multiple
|
|
791
|
+
* values — see BOARD_SURFACE_CONTENT_OFFSET_PX).
|
|
792
|
+
*
|
|
793
|
+
* For each element that (a) carries `data-agent-native-node-id`, (b) has at
|
|
794
|
+
* least one node-id-bearing ancestor inside `<body>` (i.e. is a nested board
|
|
795
|
+
* child, never a top-level one — top-level children legitimately use raw
|
|
796
|
+
* board coordinates), and (c) has a poisoned inline left and/or top:
|
|
797
|
+
*
|
|
798
|
+
* 1. the nearest 65536-multiple is stripped (recovering the board-space
|
|
799
|
+
* coordinate the value was written from),
|
|
800
|
+
* 2. the accumulated origin of its node-id ancestor chain (using
|
|
801
|
+
* already-normalized ancestor values, walking outer→inner) is subtracted
|
|
802
|
+
* to produce a parent-relative coordinate, and
|
|
803
|
+
* 3. values that are still implausible after rebasing are clamped into the
|
|
804
|
+
* parent's box.
|
|
805
|
+
*
|
|
806
|
+
* Everything else — non-poisoned coordinates, top-level children, elements
|
|
807
|
+
* without node ids, all other markup — passes through byte-identical. The
|
|
808
|
+
* function is pure, string-level (no DOM), and idempotent: normalized output
|
|
809
|
+
* contains no poisoned values, so a second pass returns `changed: false`.
|
|
810
|
+
*
|
|
811
|
+
* Applied on board-content load/adopt (DesignEditor) so designs corrupted by
|
|
812
|
+
* the historic nest-on-drop bugs self-heal the next time an editor opens
|
|
813
|
+
* them, and as a post-reparent safety net.
|
|
814
|
+
*
|
|
815
|
+
* Detectability (finding 4): this repair is a heuristic rewrite of
|
|
816
|
+
* already-persisted content with no built-in trace — a bad heuristic could
|
|
817
|
+
* silently mis-rebase real content and nobody would know. The return value
|
|
818
|
+
* includes `fixedNodeCount` and a small `samples` list (each node's original
|
|
819
|
+
* vs. rebased left/top) so callers can log what actually happened; this
|
|
820
|
+
* function itself stays side-effect-free (no console usage) so it remains
|
|
821
|
+
* safely callable from any context (including the post-reparent safety-net
|
|
822
|
+
* call sites, not just the load effect) — logging is the caller's job.
|
|
823
|
+
*/
|
|
824
|
+
const NORMALIZE_POISONED_COORDS_SAMPLE_LIMIT = 5;
|
|
825
|
+
|
|
826
|
+
export function normalizePoisonedBoardNestedCoords(html: string): {
|
|
827
|
+
html: string;
|
|
828
|
+
changed: boolean;
|
|
829
|
+
fixedNodeCount: number;
|
|
830
|
+
samples: Array<{
|
|
831
|
+
nodeId: string | null;
|
|
832
|
+
before: { left: number | null; top: number | null };
|
|
833
|
+
after: { left: number | null; top: number | null };
|
|
834
|
+
}>;
|
|
835
|
+
} {
|
|
836
|
+
if (!html || !html.includes("data-agent-native-node-id")) {
|
|
837
|
+
return { html, changed: false, fixedNodeCount: 0, samples: [] };
|
|
838
|
+
}
|
|
839
|
+
// Cheap pre-scan: a poisoned coordinate needs at least 5 digits
|
|
840
|
+
// (|value| >= 65536 - 16384 = 49152).
|
|
841
|
+
if (!/(?:left|top)\s*:\s*-?\d{5,}/i.test(html)) {
|
|
842
|
+
return { html, changed: false, fixedNodeCount: 0, samples: [] };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const bodyStart = html.indexOf("<body");
|
|
846
|
+
const bodyTagEnd = bodyStart === -1 ? -1 : html.indexOf(">", bodyStart);
|
|
847
|
+
const walkStart = bodyTagEnd === -1 ? 0 : bodyTagEnd + 1;
|
|
848
|
+
const bodyClose = html.lastIndexOf("</body>");
|
|
849
|
+
const walkEnd = bodyClose === -1 ? html.length : bodyClose;
|
|
850
|
+
|
|
851
|
+
const stack: BoardWalkFrame[] = [];
|
|
852
|
+
let out = "";
|
|
853
|
+
let cursor = 0;
|
|
854
|
+
let changed = false;
|
|
855
|
+
let fixedNodeCount = 0;
|
|
856
|
+
const samples: Array<{
|
|
857
|
+
nodeId: string | null;
|
|
858
|
+
before: { left: number | null; top: number | null };
|
|
859
|
+
after: { left: number | null; top: number | null };
|
|
860
|
+
}> = [];
|
|
861
|
+
|
|
862
|
+
BOARD_HTML_TAG_RE.lastIndex = walkStart;
|
|
863
|
+
let match: RegExpExecArray | null;
|
|
864
|
+
while ((match = BOARD_HTML_TAG_RE.exec(html)) !== null) {
|
|
865
|
+
if (match.index >= walkEnd) break;
|
|
866
|
+
const token = match[0];
|
|
867
|
+
const tagName = match[1]?.toLowerCase();
|
|
868
|
+
if (!tagName) continue; // comment
|
|
869
|
+
|
|
870
|
+
if (token.startsWith("</")) {
|
|
871
|
+
const index = stack.map((frame) => frame.tagName).lastIndexOf(tagName);
|
|
872
|
+
if (index >= 0) stack.splice(index);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Raw-text elements: skip their content wholesale so stray "<" inside
|
|
877
|
+
// scripts/styles can't desync the walk.
|
|
878
|
+
if (BOARD_RAW_TEXT_TAGS.has(tagName) && !token.endsWith("/>")) {
|
|
879
|
+
const close = html.indexOf(`</${tagName}`, BOARD_HTML_TAG_RE.lastIndex);
|
|
880
|
+
if (close === -1) break;
|
|
881
|
+
BOARD_HTML_TAG_RE.lastIndex = close;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const attrs = match[2] ?? "";
|
|
886
|
+
const isNodeIdElement = /\bdata-agent-native-node-id\s*=/.test(attrs);
|
|
887
|
+
const styleMatch = token.match(/(\bstyle\s*=\s*)("([^"]*)"|'([^']*)')/i);
|
|
888
|
+
const style = styleMatch?.[3] ?? styleMatch?.[4] ?? "";
|
|
889
|
+
let left = parseInlineStylePx(style, "left");
|
|
890
|
+
let top = parseInlineStylePx(style, "top");
|
|
891
|
+
const width = parseInlineStylePx(style, "width");
|
|
892
|
+
const height = parseInlineStylePx(style, "height");
|
|
893
|
+
|
|
894
|
+
const nodeIdAncestors = stack.filter((frame) => frame.isNodeIdElement);
|
|
895
|
+
const isNestedBoardChild = isNodeIdElement && nodeIdAncestors.length > 0;
|
|
896
|
+
const leftPoisoned =
|
|
897
|
+
isNestedBoardChild && left !== null && isBoardSurfacePoisonedCoord(left);
|
|
898
|
+
const topPoisoned =
|
|
899
|
+
isNestedBoardChild && top !== null && isBoardSurfacePoisonedCoord(top);
|
|
900
|
+
|
|
901
|
+
if ((leftPoisoned || topPoisoned) && styleMatch) {
|
|
902
|
+
const parent = nodeIdAncestors[nodeIdAncestors.length - 1];
|
|
903
|
+
const originX = nodeIdAncestors.reduce(
|
|
904
|
+
(sum, frame) => sum + (frame.left ?? 0),
|
|
905
|
+
0,
|
|
906
|
+
);
|
|
907
|
+
const originY = nodeIdAncestors.reduce(
|
|
908
|
+
(sum, frame) => sum + (frame.top ?? 0),
|
|
909
|
+
0,
|
|
910
|
+
);
|
|
911
|
+
const beforeLeft = left;
|
|
912
|
+
const beforeTop = top;
|
|
913
|
+
let nextStyle = style;
|
|
914
|
+
if (leftPoisoned && left !== null) {
|
|
915
|
+
left = rebaseNestedBoardCoord({
|
|
916
|
+
value: left,
|
|
917
|
+
ancestorOrigin: originX,
|
|
918
|
+
parentSize: parent?.width ?? null,
|
|
919
|
+
childSize: width,
|
|
920
|
+
});
|
|
921
|
+
nextStyle = replaceInlineStylePx(nextStyle, "left", left);
|
|
922
|
+
}
|
|
923
|
+
if (topPoisoned && top !== null) {
|
|
924
|
+
top = rebaseNestedBoardCoord({
|
|
925
|
+
value: top,
|
|
926
|
+
ancestorOrigin: originY,
|
|
927
|
+
parentSize: parent?.height ?? null,
|
|
928
|
+
childSize: height,
|
|
929
|
+
});
|
|
930
|
+
nextStyle = replaceInlineStylePx(nextStyle, "top", top);
|
|
931
|
+
}
|
|
932
|
+
if (nextStyle !== style) {
|
|
933
|
+
fixedNodeCount += 1;
|
|
934
|
+
if (samples.length < NORMALIZE_POISONED_COORDS_SAMPLE_LIMIT) {
|
|
935
|
+
const nodeIdMatch =
|
|
936
|
+
/\bdata-agent-native-node-id\s*=\s*("([^"]*)"|'([^']*)')/.exec(
|
|
937
|
+
attrs,
|
|
938
|
+
);
|
|
939
|
+
samples.push({
|
|
940
|
+
nodeId: nodeIdMatch?.[2] ?? nodeIdMatch?.[3] ?? null,
|
|
941
|
+
before: { left: beforeLeft, top: beforeTop },
|
|
942
|
+
after: { left, top },
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
const quote = styleMatch[2]?.startsWith("'") ? "'" : '"';
|
|
946
|
+
const rewrittenToken =
|
|
947
|
+
token.slice(0, styleMatch.index ?? 0) +
|
|
948
|
+
styleMatch[1] +
|
|
949
|
+
quote +
|
|
950
|
+
nextStyle +
|
|
951
|
+
quote +
|
|
952
|
+
token.slice((styleMatch.index ?? 0) + styleMatch[0].length);
|
|
953
|
+
out += html.slice(cursor, match.index) + rewrittenToken;
|
|
954
|
+
cursor = match.index + token.length;
|
|
955
|
+
changed = true;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const selfClosing = token.endsWith("/>") || VOID_ELEMENTS.has(tagName);
|
|
960
|
+
if (!selfClosing) {
|
|
961
|
+
stack.push({ tagName, isNodeIdElement, left, top, width, height });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (!changed) return { html, changed: false, fixedNodeCount: 0, samples: [] };
|
|
966
|
+
return {
|
|
967
|
+
html: out + html.slice(cursor),
|
|
968
|
+
changed: true,
|
|
969
|
+
fixedNodeCount,
|
|
970
|
+
samples,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
615
974
|
// ---------------------------------------------------------------------------
|
|
616
975
|
// Private helpers
|
|
617
976
|
// ---------------------------------------------------------------------------
|
|
@@ -254,6 +254,12 @@ export function parseBreakpointMediaCss(css: string): BreakpointMediaModel {
|
|
|
254
254
|
if (body === null) continue;
|
|
255
255
|
mediaRe.lastIndex = bodyStart + body.length + 1;
|
|
256
256
|
|
|
257
|
+
// Matches the LAST [data-agent-native-node-id="…"] attribute directly
|
|
258
|
+
// before the `{`, so it accepts BOTH the legacy single-attribute selector
|
|
259
|
+
// and the current doubled-attribute selector (see
|
|
260
|
+
// serializeBreakpointMediaModel below) without double-counting: on a
|
|
261
|
+
// doubled selector the scan skips the first attribute occurrence (no `{`
|
|
262
|
+
// after it) and captures the nodeId exactly once from the second.
|
|
257
263
|
const ruleRe =
|
|
258
264
|
/\[data-agent-native-node-id="((?:\\.|[^"\\])*)"\]\s*\{([^}]*)\}/g;
|
|
259
265
|
let ruleMatch: RegExpExecArray | null;
|
|
@@ -310,10 +316,22 @@ export function serializeBreakpointMediaModel(
|
|
|
310
316
|
const lines = properties.map(
|
|
311
317
|
(property) => ` ${property}: ${declarations[property]};`,
|
|
312
318
|
);
|
|
319
|
+
// Doubled attribute selector → specificity (0,2,0), beating any single
|
|
320
|
+
// Tailwind utility class (0,1,0) REGARDLESS of stylesheet order. This
|
|
321
|
+
// matters because designs load Tailwind via the Play CDN script, which
|
|
322
|
+
// injects its generated utility sheet at RUNTIME — always after this
|
|
323
|
+
// static managed block — so an equal-specificity (0,1,0) single
|
|
324
|
+
// attribute selector always lost by source order and the breakpoint
|
|
325
|
+
// override silently never rendered (verified on real designs: the
|
|
326
|
+
// @media rule was live and matching, but .px-5 won). Doubling the
|
|
327
|
+
// attribute survives DOM reorder, Tailwind re-injection, and any
|
|
328
|
+
// future stylesheet ordering change; parseBreakpointMediaCss above
|
|
329
|
+
// accepts both formats, and because every write re-serializes the
|
|
330
|
+
// whole block, legacy single-attribute rules are upgraded to this
|
|
331
|
+
// format on the next edit automatically.
|
|
332
|
+
const attrSelector = `[data-agent-native-node-id="${escAttr(nodeId)}"]`;
|
|
313
333
|
rules.push(
|
|
314
|
-
`
|
|
315
|
-
`${lines.join("\n")}\n` +
|
|
316
|
-
` }`,
|
|
334
|
+
` ${attrSelector}${attrSelector} {\n${lines.join("\n")}\n }`,
|
|
317
335
|
);
|
|
318
336
|
}
|
|
319
337
|
if (rules.length === 0) continue;
|
|
@@ -785,6 +785,11 @@ const STYLE_PROPERTY_ALIASES: Record<string, VisualStyleProperty> = {
|
|
|
785
785
|
radius: "border-radius",
|
|
786
786
|
rotation: "rotate",
|
|
787
787
|
shadow: "box-shadow",
|
|
788
|
+
// Vendor-prefixed longhands need explicit aliases: the generic camel→kebab
|
|
789
|
+
// pass in normalizeStyleProperty yields "webkit-text-stroke-*" WITHOUT the
|
|
790
|
+
// required leading dash, which would miss the allow-list entirely.
|
|
791
|
+
webkitTextStrokeColor: "-webkit-text-stroke-color",
|
|
792
|
+
webkitTextStrokeWidth: "-webkit-text-stroke-width",
|
|
788
793
|
};
|
|
789
794
|
|
|
790
795
|
// Matches url(...) in double-quoted, single-quoted, or unquoted form so each
|
|
@@ -1299,19 +1304,107 @@ function findHtmlTagEnd(html: string, start: number): number {
|
|
|
1299
1304
|
return html.length;
|
|
1300
1305
|
}
|
|
1301
1306
|
|
|
1307
|
+
// Depth-aware closing-tag scan: used for NON_VISUAL_TAGS (script/style/
|
|
1308
|
+
// template/etc) whose interiors are skipped wholesale rather than descended
|
|
1309
|
+
// into by the main parser loop. A naive "first </tag> after `from`" search
|
|
1310
|
+
// (the previous implementation) breaks the moment the same tag nests inside
|
|
1311
|
+
// itself — e.g. `<template x-if><ul><template x-for>…</template></ul>
|
|
1312
|
+
// </template>` (a completely ordinary Alpine x-if-wrapping-x-for pattern) —
|
|
1313
|
+
// because it matches the INNER `</template>` and resumes the main loop right
|
|
1314
|
+
// after it, leaving the outer element's true `</ul></template>` closes to be
|
|
1315
|
+
// mis-parsed as stray/unmatched tags against whatever unrelated element is
|
|
1316
|
+
// on the stack. That corrupted contentEnd tracking for enclosing elements
|
|
1317
|
+
// (observed: body-append/insertion offsets computed from the wrong node,
|
|
1318
|
+
// splicing moved content into template interiors). Track same-tag open/close
|
|
1319
|
+
// depth so only the tag that actually balances the ORIGINAL opening tag is
|
|
1320
|
+
// returned, matching real nested-template documents correctly.
|
|
1302
1321
|
function findClosingTag(
|
|
1303
1322
|
html: string,
|
|
1304
1323
|
tag: string,
|
|
1305
1324
|
from: number,
|
|
1306
1325
|
): { closeStart: number; closeEnd: number } | null {
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1326
|
+
const tagRe = new RegExp(`<(\\/?)\\s*${tag}\\b[^>]*>`, "gi");
|
|
1327
|
+
tagRe.lastIndex = from;
|
|
1328
|
+
let depth = 0;
|
|
1329
|
+
let match: RegExpExecArray | null;
|
|
1330
|
+
while ((match = tagRe.exec(html))) {
|
|
1331
|
+
const isClose = match[1] === "/";
|
|
1332
|
+
const selfClosing = !isClose && /\/\s*>$/.test(match[0]);
|
|
1333
|
+
if (isClose) {
|
|
1334
|
+
if (depth === 0) {
|
|
1335
|
+
return {
|
|
1336
|
+
closeStart: match.index,
|
|
1337
|
+
closeEnd: match.index + match[0].length,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
depth -= 1;
|
|
1341
|
+
} else if (!selfClosing) {
|
|
1342
|
+
depth += 1;
|
|
1343
|
+
}
|
|
1344
|
+
// Guard against zero-length matches causing an infinite loop (not
|
|
1345
|
+
// expected given the tag-name-anchored pattern, but cheap to keep safe).
|
|
1346
|
+
if (tagRe.lastIndex === match.index) {
|
|
1347
|
+
tagRe.lastIndex += 1;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Defense-in-depth safety net for moveNodeBetweenDocuments: `<template>`
|
|
1354
|
+
// interiors (x-if/x-for/x-show templates and friends) are opaque to
|
|
1355
|
+
// parseHtmlElements (NON_VISUAL_TAGS) and, per the DOM spec, live in a
|
|
1356
|
+
// detached DocumentFragment (`template.content`) — a node inserted into a
|
|
1357
|
+
// template's raw markup range renders nowhere, can't be selected/queried by
|
|
1358
|
+
// the runtime DOM, and is invisible to every downstream querySelector-based
|
|
1359
|
+
// pass (getElementInfo, setAbsolutePositioningForNodeInHtml, etc). A correct
|
|
1360
|
+
// findClosingTag (see above) prevents the offset MISCALCULATION that used to
|
|
1361
|
+
// cause this, but this function is kept as an independent second guard —
|
|
1362
|
+
// even if some other insertion-point calculation ever computes an offset
|
|
1363
|
+
// that lands inside a real `<template>` block, this catches it and callers
|
|
1364
|
+
// redirect to a real DOM slot instead of silently splicing into markup that
|
|
1365
|
+
// will never render or be selectable again.
|
|
1366
|
+
//
|
|
1367
|
+
// Finding 8: when it fires, callers used to always redirect to the end of
|
|
1368
|
+
// <body> (or end of document) — a silent teleport that can land an anchored
|
|
1369
|
+
// insert far from where the user was working. `findEnclosingTemplateClose`
|
|
1370
|
+
// below returns the ENCLOSING outer template's closeEnd position (the
|
|
1371
|
+
// offset immediately after its `</template>`) when `offset` is inside a
|
|
1372
|
+
// template interior, so callers can redirect there instead: still a
|
|
1373
|
+
// guaranteed-safe real-DOM slot (immediately after a closing tag, a sibling
|
|
1374
|
+
// of the template rather than jumping to doc end), just much closer to the
|
|
1375
|
+
// anchor the caller actually asked for.
|
|
1376
|
+
function isOffsetInsideTemplateInterior(html: string, offset: number): boolean {
|
|
1377
|
+
return findEnclosingTemplateClose(html, offset) !== null;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// Exported ONLY for the finding-8 redirect-target unit test below: with
|
|
1381
|
+
// findClosingTag's offset-miscalculation bug fixed (see the doc comment
|
|
1382
|
+
// above), every insertAt this module's own callers compute through
|
|
1383
|
+
// parseHtmlElements-derived positions (anchor.start/end/contentEnd,
|
|
1384
|
+
// bodyEl.contentEnd) already lands OUTSIDE template interiors in practice —
|
|
1385
|
+
// NON_VISUAL_TAGS like <template> are skipped wholesale, so a template can
|
|
1386
|
+
// never itself become part of another element's registered content range.
|
|
1387
|
+
// That makes this guard a true defense-in-depth backstop with no reachable
|
|
1388
|
+
// integration-level repro through moveNodeBetweenDocuments today; testing
|
|
1389
|
+
// the redirect target directly against a synthetic offset is the honest way
|
|
1390
|
+
// to pin its behavior instead of contriving a fragile call into the guard.
|
|
1391
|
+
export function findEnclosingTemplateClose(
|
|
1392
|
+
html: string,
|
|
1393
|
+
offset: number,
|
|
1394
|
+
): { closeEnd: number } | null {
|
|
1395
|
+
const templateOpenRe = /<template\b[^>]*>/gi;
|
|
1396
|
+
let match: RegExpExecArray | null;
|
|
1397
|
+
while ((match = templateOpenRe.exec(html))) {
|
|
1398
|
+
const openEnd = match.index + match[0].length;
|
|
1399
|
+
if (openEnd > offset) break;
|
|
1400
|
+
const close = findClosingTag(html, "template", openEnd);
|
|
1401
|
+
const contentEnd = close ? close.closeStart : html.length;
|
|
1402
|
+
if (offset > openEnd && offset <= contentEnd) {
|
|
1403
|
+
return { closeEnd: close ? close.closeEnd : html.length };
|
|
1404
|
+
}
|
|
1405
|
+
templateOpenRe.lastIndex = close ? close.closeEnd : html.length;
|
|
1406
|
+
}
|
|
1407
|
+
return null;
|
|
1315
1408
|
}
|
|
1316
1409
|
|
|
1317
1410
|
function parseHtmlElements(html: string): ParsedElement[] {
|
|
@@ -4031,6 +4124,17 @@ export interface MoveNodeBetweenDocumentsResult {
|
|
|
4031
4124
|
* Only present when status is "applied".
|
|
4032
4125
|
*/
|
|
4033
4126
|
movedNodeId?: string;
|
|
4127
|
+
/**
|
|
4128
|
+
* Finding 8: true when the requested anchor placement fell inside a
|
|
4129
|
+
* `<template>` interior and the insert was redirected to a real DOM slot
|
|
4130
|
+
* instead (immediately after the enclosing template's `</template>` when
|
|
4131
|
+
* that could be located, otherwise the pre-existing doc-end/body-end
|
|
4132
|
+
* fallback). Hosts can use this to toast a "landed near, not exactly
|
|
4133
|
+
* where you dropped it" notice instead of the previous fully silent
|
|
4134
|
+
* teleport. Only meaningful when status is "applied" and an anchor was
|
|
4135
|
+
* requested.
|
|
4136
|
+
*/
|
|
4137
|
+
anchorRedirected?: boolean;
|
|
4034
4138
|
}
|
|
4035
4139
|
|
|
4036
4140
|
/**
|
|
@@ -4117,6 +4221,7 @@ export function moveNodeBetweenDocuments(
|
|
|
4117
4221
|
|
|
4118
4222
|
// --- Insert fragment into destHtml ---
|
|
4119
4223
|
let nextDestHtml: string;
|
|
4224
|
+
let anchorRedirected = false;
|
|
4120
4225
|
|
|
4121
4226
|
if (anchorNodeId) {
|
|
4122
4227
|
const anchor = destElements.find(
|
|
@@ -4130,7 +4235,7 @@ export function moveNodeBetweenDocuments(
|
|
|
4130
4235
|
message: `Anchor node with data-agent-native-node-id="${anchorNodeId}" not found in destHtml.`,
|
|
4131
4236
|
};
|
|
4132
4237
|
}
|
|
4133
|
-
|
|
4238
|
+
let insertAt =
|
|
4134
4239
|
placement === "before"
|
|
4135
4240
|
? anchor.start
|
|
4136
4241
|
: placement === "after"
|
|
@@ -4138,15 +4243,44 @@ export function moveNodeBetweenDocuments(
|
|
|
4138
4243
|
: anchor.selfClosing
|
|
4139
4244
|
? anchor.end
|
|
4140
4245
|
: anchor.contentEnd;
|
|
4246
|
+
// Never splice into a <template> interior — it renders nowhere and is
|
|
4247
|
+
// unselectable afterward (see isOffsetInsideTemplateInterior doc above).
|
|
4248
|
+
// Finding 8: redirect to immediately AFTER the ENCLOSING outer
|
|
4249
|
+
// </template> when it can be located — still a guaranteed-safe real-DOM
|
|
4250
|
+
// slot, just a sibling of the template instead of a jump all the way to
|
|
4251
|
+
// the end of <body>/the document. Falls back to the old doc-end/body-end
|
|
4252
|
+
// behavior only if the enclosing template's close somehow can't be
|
|
4253
|
+
// resolved (defense-in-depth for a guard that should always agree with
|
|
4254
|
+
// itself here).
|
|
4255
|
+
const enclosingTemplate = findEnclosingTemplateClose(destHtml, insertAt);
|
|
4256
|
+
if (enclosingTemplate) {
|
|
4257
|
+
insertAt = enclosingTemplate.closeEnd;
|
|
4258
|
+
anchorRedirected = true;
|
|
4259
|
+
} else if (isOffsetInsideTemplateInterior(destHtml, insertAt)) {
|
|
4260
|
+
const bodyEl = destElements.find((el) => el.tag === "body");
|
|
4261
|
+
insertAt = bodyEl
|
|
4262
|
+
? bodyEl.selfClosing
|
|
4263
|
+
? bodyEl.end
|
|
4264
|
+
: bodyEl.contentEnd
|
|
4265
|
+
: destHtml.length;
|
|
4266
|
+
anchorRedirected = true;
|
|
4267
|
+
}
|
|
4141
4268
|
nextDestHtml = `${destHtml.slice(0, insertAt)}${fragment}${destHtml.slice(insertAt)}`;
|
|
4142
4269
|
} else {
|
|
4143
4270
|
// Default: find <body> and append inside it, or append at end of doc.
|
|
4144
4271
|
const bodyEl = destElements.find((el) => el.tag === "body");
|
|
4145
|
-
|
|
4272
|
+
let insertAt = bodyEl
|
|
4146
4273
|
? bodyEl.selfClosing
|
|
4147
4274
|
? bodyEl.end
|
|
4148
4275
|
: bodyEl.contentEnd
|
|
4149
4276
|
: destHtml.length;
|
|
4277
|
+
// Same template-interior guard as the anchored branch above — a
|
|
4278
|
+
// miscomputed bodyEl.contentEnd (or a body that itself is only reachable
|
|
4279
|
+
// through a template, e.g. a fragment being treated as a full document)
|
|
4280
|
+
// must never land inside template markup.
|
|
4281
|
+
if (isOffsetInsideTemplateInterior(destHtml, insertAt)) {
|
|
4282
|
+
insertAt = destHtml.length;
|
|
4283
|
+
}
|
|
4150
4284
|
nextDestHtml = `${destHtml.slice(0, insertAt)}${fragment}${destHtml.slice(insertAt)}`;
|
|
4151
4285
|
}
|
|
4152
4286
|
|
|
@@ -4158,5 +4292,6 @@ export function moveNodeBetweenDocuments(
|
|
|
4158
4292
|
destHtml: nextDestHtml,
|
|
4159
4293
|
status: "applied",
|
|
4160
4294
|
movedNodeId,
|
|
4295
|
+
...(anchorRedirected ? { anchorRedirected: true } : {}),
|
|
4161
4296
|
};
|
|
4162
4297
|
}
|
|
@@ -117,27 +117,27 @@ export declare function isAgentChatDurableBackgroundEnabled(options?: {
|
|
|
117
117
|
appOptIn?: boolean;
|
|
118
118
|
}): boolean;
|
|
119
119
|
/**
|
|
120
|
-
* Env flag for the FOREGROUND server-driven self-chain. DEFAULT-
|
|
121
|
-
*
|
|
122
|
-
* (`
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
120
|
+
* Env flag for the FOREGROUND server-driven self-chain. DEFAULT-ON for hosted
|
|
121
|
+
* deployments with `A2A_SECRET`: unset/empty/unknown means enabled, and an app
|
|
122
|
+
* opts OUT with an explicit falsy value (`false`/`0`/`no`/`off`). Deliberately
|
|
123
|
+
* kept separate from `AGENT_CHAT_DURABLE_BACKGROUND` so this narrower
|
|
124
|
+
* capability can be disabled independently of the full durable-background
|
|
125
|
+
* worker path.
|
|
126
126
|
*/
|
|
127
127
|
export declare const AGENT_CHAT_FOREGROUND_SELF_CHAIN_ENV = "AGENT_CHAT_FOREGROUND_SELF_CHAIN";
|
|
128
128
|
/**
|
|
129
|
-
* Gate for the
|
|
129
|
+
* Gate for the foreground self-chain: a normal (non-durable-background)
|
|
130
130
|
* agent-chat turn that hits its soft-timeout chunk boundary continues via a
|
|
131
131
|
* server-side self-dispatch on the REGULAR function (not a Netlify
|
|
132
132
|
* `-background` function) instead of depending on the client to re-POST
|
|
133
133
|
* `auto_continue`. Composes exactly like `isAgentChatDurableBackgroundEnabled`:
|
|
134
|
-
* true
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
134
|
+
* true when the runtime is hosted AND `A2A_SECRET` is configured (the HMAC
|
|
135
|
+
* handoff authenticates the self-dispatch), unless the env flag is explicitly
|
|
136
|
+
* falsy. False otherwise — and false means the existing client-driven
|
|
137
|
+
* `auto_continue` re-POST path is used unchanged, byte-for-byte.
|
|
138
138
|
*
|
|
139
139
|
* Deliberately independent of `isAgentChatDurableBackgroundEnabled`: an app can
|
|
140
|
-
*
|
|
140
|
+
* use this narrower capability without opting into the full 15-min
|
|
141
141
|
* background-function worker path, and the two gates never need to agree.
|
|
142
142
|
* When BOTH would be true for a given run, the durable-background dispatch
|
|
143
143
|
* decision in `production-agent.ts` is evaluated first and takes precedence —
|