@parhelia/core 0.1.12927 → 0.1.12928

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/client-components/index.d.ts +1 -1
  2. package/dist/client-components/index.js +1 -1
  3. package/dist/client-components/index.js.map +1 -1
  4. package/dist/editor/FieldListField.js +8 -0
  5. package/dist/editor/FieldListField.js.map +1 -1
  6. package/dist/editor/MainLayout.js +2 -1
  7. package/dist/editor/MainLayout.js.map +1 -1
  8. package/dist/editor/PerfProfiler.d.ts +11 -0
  9. package/dist/editor/PerfProfiler.js +29 -0
  10. package/dist/editor/PerfProfiler.js.map +1 -0
  11. package/dist/editor/ai/GuidanceOverlay.js +4 -9
  12. package/dist/editor/ai/GuidanceOverlay.js.map +1 -1
  13. package/dist/editor/ai/InlineAiDialog.js +2 -2
  14. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  15. package/dist/editor/ai/dialogs/capturePageScreenshot.js +1 -1
  16. package/dist/editor/ai/dialogs/capturePageScreenshot.js.map +1 -1
  17. package/dist/editor/ai/terminal/components/AgentSettingsContent.d.ts +2 -1
  18. package/dist/editor/ai/terminal/components/AgentSettingsContent.js.map +1 -1
  19. package/dist/editor/ai/terminal/components/AgentSettingsSections.d.ts +2 -1
  20. package/dist/editor/ai/terminal/components/AgentSettingsSections.js +9 -2
  21. package/dist/editor/ai/terminal/components/AgentSettingsSections.js.map +1 -1
  22. package/dist/editor/ai/terminal/components/ContextInfoBar.js +4 -3
  23. package/dist/editor/ai/terminal/components/ContextInfoBar.js.map +1 -1
  24. package/dist/editor/ai/terminal/useAgentSettingsData.d.ts +8 -1
  25. package/dist/editor/ai/terminal/useAgentSettingsData.js +42 -3
  26. package/dist/editor/ai/terminal/useAgentSettingsData.js.map +1 -1
  27. package/dist/editor/ai/terminal/useAgentSettingsPanel.d.ts +1 -1
  28. package/dist/editor/bridge/BridgeClient.js +9 -1
  29. package/dist/editor/bridge/BridgeClient.js.map +1 -1
  30. package/dist/editor/bridge/protocol.d.ts +19 -0
  31. package/dist/editor/bridge/protocol.js +1 -1
  32. package/dist/editor/bridge/protocol.js.map +1 -1
  33. package/dist/editor/client/EditorShell.js +156 -20
  34. package/dist/editor/client/EditorShell.js.map +1 -1
  35. package/dist/editor/client/editContext.d.ts +32 -1
  36. package/dist/editor/client/editContext.js +8 -0
  37. package/dist/editor/client/editContext.js.map +1 -1
  38. package/dist/editor/client/fieldModificationStore.js +64 -0
  39. package/dist/editor/client/fieldModificationStore.js.map +1 -1
  40. package/dist/editor/client/itemsRepository.js +95 -3
  41. package/dist/editor/client/itemsRepository.js.map +1 -1
  42. package/dist/editor/client/operations.d.ts +1 -0
  43. package/dist/editor/client/operations.js +147 -13
  44. package/dist/editor/client/operations.js.map +1 -1
  45. package/dist/editor/client/ui/EditorChrome.js +6 -5
  46. package/dist/editor/client/ui/EditorChrome.js.map +1 -1
  47. package/dist/editor/commands/keyboardCommands.js +1 -1
  48. package/dist/editor/commands/keyboardCommands.js.map +1 -1
  49. package/dist/editor/componentTreeHelper.js +15 -5
  50. package/dist/editor/componentTreeHelper.js.map +1 -1
  51. package/dist/editor/field-types/SingleLineText.js +79 -9
  52. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  53. package/dist/editor/fieldValueDiagnostics.d.ts +79 -0
  54. package/dist/editor/fieldValueDiagnostics.js +133 -0
  55. package/dist/editor/fieldValueDiagnostics.js.map +1 -0
  56. package/dist/editor/page-editor-chrome/DragInsertionOverlay.js +51 -19
  57. package/dist/editor/page-editor-chrome/DragInsertionOverlay.js.map +1 -1
  58. package/dist/editor/page-editor-chrome/FrameMenu.d.ts +2 -0
  59. package/dist/editor/page-editor-chrome/FrameMenu.js +8 -6
  60. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  61. package/dist/editor/page-editor-chrome/FrameMenus.d.ts +3 -1
  62. package/dist/editor/page-editor-chrome/FrameMenus.js +4 -4
  63. package/dist/editor/page-editor-chrome/FrameMenus.js.map +1 -1
  64. package/dist/editor/page-editor-chrome/PageEditorChrome.js +17 -4
  65. package/dist/editor/page-editor-chrome/PageEditorChrome.js.map +1 -1
  66. package/dist/editor/page-editor-chrome/PictureEditorOverlay.js +4 -7
  67. package/dist/editor/page-editor-chrome/PictureEditorOverlay.js.map +1 -1
  68. package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +8 -1
  69. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +14 -3
  70. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  71. package/dist/editor/page-editor-chrome/PlaceholderDropZones.d.ts +3 -1
  72. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +2 -2
  73. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
  74. package/dist/editor/page-editor-chrome/overlay/placeholderZones.js +43 -1
  75. package/dist/editor/page-editor-chrome/overlay/placeholderZones.js.map +1 -1
  76. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js +121 -48
  77. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js.map +1 -1
  78. package/dist/editor/page-viewer/EditorForm.js +54 -5
  79. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  80. package/dist/editor/page-viewer/MiniMap.js +4 -4
  81. package/dist/editor/page-viewer/MiniMap.js.map +1 -1
  82. package/dist/editor/page-viewer/PageViewer.js +9 -7
  83. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  84. package/dist/editor/page-viewer/PageViewerFrame.js +267 -25
  85. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  86. package/dist/editor/page-viewer/bridgeFieldPatch.js +49 -2
  87. package/dist/editor/page-viewer/bridgeFieldPatch.js.map +1 -1
  88. package/dist/editor/page-viewer/pageViewContext.d.ts +1 -0
  89. package/dist/editor/page-viewer/pageViewContext.js +7 -0
  90. package/dist/editor/page-viewer/pageViewContext.js.map +1 -1
  91. package/dist/editor/perfTrace.d.ts +46 -0
  92. package/dist/editor/perfTrace.js +213 -0
  93. package/dist/editor/perfTrace.js.map +1 -0
  94. package/dist/editor/reviews/CommentPopover.js +2 -3
  95. package/dist/editor/reviews/CommentPopover.js.map +1 -1
  96. package/dist/editor/reviews/commentAi.js +1 -1
  97. package/dist/editor/reviews/commentAi.js.map +1 -1
  98. package/dist/editor/services/agentService.d.ts +2 -0
  99. package/dist/editor/services/agentService.js.map +1 -1
  100. package/dist/editor/services/aiService.d.ts +1 -0
  101. package/dist/editor/services/aiService.js.map +1 -1
  102. package/dist/editor/sidebar/ComponentTree.js +10 -9
  103. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  104. package/dist/editor/sidebar/ContentTreePanel.d.ts +2 -2
  105. package/dist/editor/sidebar/ContentTreePanel.js +6 -2
  106. package/dist/editor/sidebar/ContentTreePanel.js.map +1 -1
  107. package/dist/editor/sidebar/MainContentTree.js +3 -2
  108. package/dist/editor/sidebar/MainContentTree.js.map +1 -1
  109. package/dist/editor/utils/keyboardNavigation.js +2 -2
  110. package/dist/editor/utils/keyboardNavigation.js.map +1 -1
  111. package/dist/index.d.ts +1 -1
  112. package/dist/index.js +1 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/revision.d.ts +2 -2
  115. package/dist/revision.js +2 -2
  116. package/package.json +1 -1
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { BRIDGE_DOM_UPDATED_EVENT, MiniMap } from "./MiniMap";
4
- import { useEditContext, useEditContextRef, useFieldsEditContext, useFieldsEditContextRef, } from "../client/editContext";
4
+ import { useCanvasSelection, useEditContext, useEditContextRef, useFieldsEditContext, useFieldsEditContextRef, } from "../client/editContext";
5
5
  import { useSlotContext } from "../views/editorSlotContext";
6
6
  import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
7
7
  import { PageEditorChrome } from "../page-editor-chrome/PageEditorChrome";
8
8
  import { useBridgeInlineEditing, } from "../page-editor-chrome/useBridgeInlineEditing";
9
9
  import { getInlineAiAnchorFromBridgeToolbar, getVisibleBridgeToolbarRect, waitForVisibleBridgeToolbarRect, } from "../page-editor-chrome/bridgeInlineFormatToolbarLayout";
10
10
  import { IFRAME_OVERLAY_BRIDGE_DRAG_GESTURE_EVENT, IFRAME_OVERLAY_BRIDGE_GEOMETRY_EVENT, IFRAME_OVERLAY_BRIDGE_SCROLL_EVENT, IframeOverlayProvider, } from "../page-editor-chrome/overlay/IframeOverlayProvider";
11
- import { DEVICE_CHANGE_EVENT, useViewportChangeSignal, } from "./pageViewContext";
11
+ import { CANVAS_REFRESH_COMPLETED_EVENT, DEVICE_CHANGE_EVENT, useViewportChangeSignal, } from "./pageViewContext";
12
12
  import uuid from "react-uuid";
13
13
  import { cn } from "../../lib/utils";
14
14
  import { normalizeMarkerId } from "../utils";
@@ -26,8 +26,10 @@ import { DeviceToolbar } from "./DeviceToolbar";
26
26
  import { FullscreenControls } from "../client/ui/FullscreenControls";
27
27
  import { toSitecoreDate } from "../utils/sitecoreDate";
28
28
  import { BridgeClient } from "../bridge/BridgeClient";
29
+ import { beginPerfTrace, isPerfTraceActive, isPerfTraceEnabled, perfTraceStep, } from "../perfTrace";
29
30
  import { getSuggestionDisplayValue } from "../reviews/suggestionDisplayValue";
30
31
  import { buildBridgeFieldPatchPayload, getBridgeFieldPatchValue, isBridgePatchableTextField, } from "./bridgeFieldPatch";
32
+ import { logFieldValueDiagnostic, summarizeField, summarizeFieldDescriptor, summarizeFieldValue, } from "../fieldValueDiagnostics";
31
33
  import { createKeyboardEventFromBridgeInteraction } from "./bridgeKeyboardShortcuts";
32
34
  import { formatParentBridgeInjectionDiagnostic, injectParheliaBridgeFromParent, } from "./parentBridgeInjection";
33
35
  import { getTextCompletion, normalizeTextCompletion, } from "../services/textCompletionService";
@@ -39,6 +41,9 @@ const BRIDGE_INLINE_EDIT_RELEASE_EVENT = "parhelia:bridge-inline-edit-release";
39
41
  const INLINE_AI_CLOSE_EVENT = "inline-ai-close";
40
42
  const BRIDGE_INLINE_LINK_OPEN_EVENT = "bridge-inline-link-open";
41
43
  const SCROLL_INTO_VIEW_BRIDGE_SOURCE = "scroll-into-view";
44
+ // How long a requested canvas refresh may suppress viewport reactions
45
+ // (selection auto-scroll, scrollIntoView) before it is presumed lost.
46
+ const CANVAS_REFRESH_IN_FLIGHT_MAX_MS = 15000;
42
47
  // Suggestions-mode cursor: a text I-beam (the click insertion point) badged with
43
48
  // the lucide PenLine icon from the "Suggestions mode" toolbar button. A white
44
49
  // halo keeps it visible on any page background; the hotspot is the I-beam centre.
@@ -602,6 +607,7 @@ export function PageViewerFrame(props) {
602
607
  function PageViewerFrameContent({ compareView, pageViewContext, editContext, className, slotCloseButton, }) {
603
608
  const fieldsContext = useFieldsEditContext();
604
609
  const slotContext = useSlotContext();
610
+ const canvasSelection = useCanvasSelection();
605
611
  const pathname = usePathname();
606
612
  const pageViewContextRef = useRef(undefined);
607
613
  useEffect(() => {
@@ -636,6 +642,27 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
636
642
  const directOverlayScrollSyncRef = useRef(false);
637
643
  const lastBridgeHoverInteractionRef = useRef(undefined);
638
644
  const pendingRefreshScrollRef = useRef(undefined);
645
+ // Set when a canvas refresh (bridge patch or full reload) has been requested
646
+ // and its result has not landed yet. While in flight, bridgeGeometry still
647
+ // describes the PRE-refresh DOM — a moved component is still measured at its
648
+ // old position — so viewport-affecting reactions (selection auto-scroll,
649
+ // scrollIntoView) must wait for the refreshed geometry instead of scrolling
650
+ // toward stale rects. Timestamped so a refresh that never completes cannot
651
+ // suppress scrolling forever.
652
+ const canvasRefreshInFlightSinceRef = useRef(null);
653
+ // Set when refreshCompleted arrived with applied:"host-app": the page's own
654
+ // app accepted the refresh but its re-render lands later as DOM mutations.
655
+ // The in-flight window then stays open until the next structureUpdated.
656
+ const awaitingHostAppRefreshRenderRef = useRef(false);
657
+ const isCanvasRefreshInFlight = useCallback(() => {
658
+ const since = canvasRefreshInFlightSinceRef.current;
659
+ return (since !== null && Date.now() - since < CANVAS_REFRESH_IN_FLIGHT_MAX_MS);
660
+ }, []);
661
+ const completeCanvasRefresh = useCallback(() => {
662
+ awaitingHostAppRefreshRenderRef.current = false;
663
+ canvasRefreshInFlightSinceRef.current = null;
664
+ window.dispatchEvent(new CustomEvent(CANVAS_REFRESH_COMPLETED_EVENT));
665
+ }, []);
639
666
  const restorePendingRefreshScroll = useCallback((iframe, targetScroll, observedScroll) => {
640
667
  if (!targetScroll)
641
668
  return;
@@ -775,16 +802,34 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
775
802
  // Iframe blur is handled by the bridge inline-edit lifecycle now.
776
803
  }, []);
777
804
  const sendBeginInlineEdit = useCallback((payload) => {
778
- return (bridgeClientRef.current?.sendCommand("beginInlineEdit", payload) ??
779
- false);
805
+ const sent = bridgeClientRef.current?.sendCommand("beginInlineEdit", payload) ??
806
+ false;
807
+ logFieldValueDiagnostic("bridge-frame", "beginInlineEdit command", {
808
+ sent,
809
+ payload,
810
+ });
811
+ return sent;
780
812
  }, []);
781
813
  const sendCancelInlineEdit = useCallback((payload) => {
782
- return (bridgeClientRef.current?.sendCommand("cancelInlineEdit", payload) ??
783
- false);
814
+ const sent = bridgeClientRef.current?.sendCommand("cancelInlineEdit", payload) ??
815
+ false;
816
+ logFieldValueDiagnostic("bridge-frame", "cancelInlineEdit command", {
817
+ sent,
818
+ payload,
819
+ });
820
+ return sent;
784
821
  }, []);
785
822
  const sendApplyFieldPatchCommand = useCallback((payload) => {
786
- return (bridgeClientRef.current?.sendCommand("applyFieldPatch", payload) ??
787
- false);
823
+ const sent = bridgeClientRef.current?.sendCommand("applyFieldPatch", payload) ??
824
+ false;
825
+ logFieldValueDiagnostic("bridge-frame", "applyFieldPatch command", {
826
+ sent,
827
+ payload: {
828
+ ...payload,
829
+ value: summarizeFieldValue(payload.value),
830
+ },
831
+ });
832
+ return sent;
788
833
  }, []);
789
834
  const sendApplyRichTextCommand = useCallback((payload) => {
790
835
  return (bridgeClientRef.current?.sendCommand("applyRichTextCommand", payload) ??
@@ -833,6 +878,18 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
833
878
  sendApplyFieldPatch: sendApplyFieldPatchCommand,
834
879
  });
835
880
  const beginTrackedBridgeInlineEdit = useCallback(async (interaction) => {
881
+ logFieldValueDiagnostic("bridge-frame", "begin tracked inline edit", {
882
+ previousActiveInlineEdit: activeBridgeInlineEditRef.current,
883
+ interaction: {
884
+ kind: interaction.kind,
885
+ elementKey: interaction.elementKey,
886
+ fieldId: interaction.fieldId,
887
+ item: interaction.item,
888
+ selectedText: summarizeFieldValue(interaction.selectedText),
889
+ selectionStartOffset: interaction.selectionStartOffset,
890
+ selectionEndOffset: interaction.selectionEndOffset,
891
+ },
892
+ });
836
893
  clearActiveBridgeInlineEdit();
837
894
  const started = await beginBridgeInlineEdit(interaction);
838
895
  if (started && interaction.elementKey && interaction.fieldId) {
@@ -857,6 +914,10 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
857
914
  };
858
915
  setIsBridgeInlineEditing(true);
859
916
  }
917
+ logFieldValueDiagnostic("bridge-frame", "tracked inline edit result", {
918
+ started,
919
+ nextActiveInlineEdit: activeBridgeInlineEditRef.current,
920
+ });
860
921
  return started;
861
922
  }, [
862
923
  beginBridgeInlineEdit,
@@ -865,9 +926,17 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
865
926
  editContextRef,
866
927
  ]);
867
928
  const endTrackedBridgeFieldFocus = useCallback(() => {
929
+ logFieldValueDiagnostic("bridge-frame", "ending tracked field focus", {
930
+ activeInlineEdit: activeBridgeInlineEditRef.current,
931
+ });
868
932
  clearActiveBridgeInlineEdit();
869
933
  endBridgeFieldFocus();
870
934
  }, [clearActiveBridgeInlineEdit, endBridgeFieldFocus]);
935
+ const beginTrackedBridgeInlineEditAfterSelection = useCallback((interaction) => {
936
+ window.setTimeout(() => {
937
+ void beginTrackedBridgeInlineEdit(interaction);
938
+ }, 0);
939
+ }, [beginTrackedBridgeInlineEdit]);
871
940
  useEffect(() => {
872
941
  const handleBridgeInlineEditRelease = (event) => {
873
942
  const detail = event
@@ -881,6 +950,10 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
881
950
  if (activeEdit.item && !bridgeItemMatches(activeEdit.item, field.item)) {
882
951
  return;
883
952
  }
953
+ logFieldValueDiagnostic("bridge-frame", "inline edit released by form", {
954
+ activeInlineEdit: activeEdit,
955
+ field: summarizeFieldDescriptor(field),
956
+ });
884
957
  clearActiveBridgeInlineEdit();
885
958
  bridgePatchSignatureRef.current.delete(`${activeEdit.elementKey}:${activeEdit.fieldId}`);
886
959
  };
@@ -932,12 +1005,33 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
932
1005
  },
933
1006
  mode: showSuggestions ? "suggestions" : "baseline",
934
1007
  });
935
- return {
1008
+ const result = {
936
1009
  value: showSuggestions ? display.mergedValue : display.baselineValue,
937
1010
  source: showSuggestions && display.hasSuggestions
938
1011
  ? "suggestion-preview"
939
1012
  : "real",
940
1013
  };
1014
+ logFieldValueDiagnostic("bridge-patch", "display value resolved", {
1015
+ showSuggestions,
1016
+ preferRepositoryValue,
1017
+ field: summarizeField(field),
1018
+ structureField: {
1019
+ elementKey: structureField.elementKey,
1020
+ fieldId: structureField.fieldId,
1021
+ item: structureField.item,
1022
+ textContent: summarizeFieldValue(structureField.textContent),
1023
+ isRichText: structureField.isRichText,
1024
+ },
1025
+ itemDescriptor,
1026
+ repositoryValue: summarizeFieldValue(repositoryValue),
1027
+ result: {
1028
+ ...result,
1029
+ value: summarizeFieldValue(result.value),
1030
+ },
1031
+ modifiedFieldCount: fieldsContext?.modifiedFields?.length,
1032
+ suggestedEditCount: editContext.suggestedEdits?.length,
1033
+ });
1034
+ return result;
941
1035
  }, [
942
1036
  editContext.mode,
943
1037
  editContext.showSuggestedEdits,
@@ -972,10 +1066,28 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
972
1066
  source: patch.source ?? "",
973
1067
  observedTextContent: observedTextContent ?? "",
974
1068
  });
975
- if (bridgePatchSignatureRef.current.get(cacheKey) === signature) {
1069
+ const previousSignature = bridgePatchSignatureRef.current.get(cacheKey);
1070
+ if (previousSignature === signature) {
1071
+ logFieldValueDiagnostic("bridge-patch", "skipped duplicate send", {
1072
+ cacheKey,
1073
+ patch: {
1074
+ ...patch,
1075
+ value: summarizeFieldValue(patch.value),
1076
+ },
1077
+ observedTextContent: summarizeFieldValue(observedTextContent),
1078
+ });
976
1079
  return false;
977
1080
  }
978
1081
  bridgePatchSignatureRef.current.set(cacheKey, signature);
1082
+ logFieldValueDiagnostic("bridge-patch", "sending patch", {
1083
+ cacheKey,
1084
+ patch: {
1085
+ ...patch,
1086
+ value: summarizeFieldValue(patch.value),
1087
+ },
1088
+ observedTextContent: summarizeFieldValue(observedTextContent),
1089
+ previousSignature,
1090
+ });
979
1091
  bridge.sendCommand("applyFieldPatch", patch);
980
1092
  return true;
981
1093
  }, []);
@@ -1517,9 +1629,11 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1517
1629
  if (!editor)
1518
1630
  return;
1519
1631
  const selectFromBridgeInteraction = (ids) => {
1520
- if (!bridgeComponentSelectionsMatch(editor.selection, ids)) {
1632
+ const unchanged = bridgeComponentSelectionsMatch(editor.getSelection(), ids);
1633
+ if (!unchanged) {
1521
1634
  suppressNextSelectionScrollRef.current = true;
1522
1635
  }
1636
+ perfTraceStep("frame:select-dispatched", { ids, unchanged });
1523
1637
  editor.select(ids);
1524
1638
  };
1525
1639
  let effectiveInteraction = interaction;
@@ -1581,7 +1695,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1581
1695
  selectFromBridgeInteraction([]);
1582
1696
  if (isEditableFieldClick) {
1583
1697
  clearSelectedRangeForEditableClick();
1584
- await beginTrackedBridgeInlineEdit(effectiveInteraction);
1698
+ beginTrackedBridgeInlineEditAfterSelection(effectiveInteraction);
1585
1699
  }
1586
1700
  else if (interaction.kind === "click") {
1587
1701
  closeInlineAiIfOpen();
@@ -1589,7 +1703,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1589
1703
  }
1590
1704
  return;
1591
1705
  }
1592
- const currentSelection = editor.selection || [];
1706
+ const currentSelection = editor.getSelection();
1593
1707
  if (interaction.kind === "contextMenu") {
1594
1708
  if (pageViewContextRef.current) {
1595
1709
  pageViewContextRef.current.bridgeInteraction = effectiveInteraction;
@@ -1719,7 +1833,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1719
1833
  selectFromBridgeInteraction([componentId]);
1720
1834
  if (isEditableFieldClick) {
1721
1835
  clearSelectedRangeForEditableClick();
1722
- await beginTrackedBridgeInlineEdit(effectiveInteraction);
1836
+ beginTrackedBridgeInlineEditAfterSelection(effectiveInteraction);
1723
1837
  }
1724
1838
  else {
1725
1839
  closeInlineAiIfOpen();
@@ -1728,6 +1842,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1728
1842
  }, [
1729
1843
  applyBridgeTextCompletion,
1730
1844
  beginTrackedBridgeInlineEdit,
1845
+ beginTrackedBridgeInlineEditAfterSelection,
1731
1846
  clearBridgeTextCompletion,
1732
1847
  endTrackedBridgeFieldFocus,
1733
1848
  requestBridgeTextCompletion,
@@ -1849,9 +1964,12 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1849
1964
  suggestionsCursorActive: !activeBridgeInlineEditRef.current &&
1850
1965
  (editContextRef.current?.mode === "suggestions" ||
1851
1966
  !!editContextRef.current?.showSuggestedEdits),
1852
- selectedComponentIds: editContextRef.current?.selection ?? [],
1967
+ selectedComponentIds: editContextRef.current?.getSelection() ?? [],
1853
1968
  featureFlags: {
1854
1969
  crossOriginBridge: true,
1970
+ // Lets the host emit its own perf logs + interaction perf
1971
+ // timestamps without needing a flag in the iframe's origin.
1972
+ perfTrace: isPerfTraceEnabled(),
1855
1973
  },
1856
1974
  version: bridge.getDiagnostics().editorVersion,
1857
1975
  acknowledgedCapabilities: bridge.getAcknowledgedCapabilities(),
@@ -1864,12 +1982,31 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1864
1982
  }
1865
1983
  break;
1866
1984
  case "structureUpdated":
1985
+ if (isPerfTraceActive()) {
1986
+ perfTraceStep("frame:structure-updated-received", {
1987
+ components: event.payload.structure.components.length,
1988
+ fields: event.payload.structure.fields.length,
1989
+ transportMs: Date.now() - event.timestamp,
1990
+ });
1991
+ }
1867
1992
  pageViewContext.setBridgeStructure(event.payload.structure);
1993
+ if (awaitingHostAppRefreshRenderRef.current) {
1994
+ // First structure change after a host-app refresh: the page's
1995
+ // own re-render has reached the DOM — the refresh is now truly
1996
+ // complete.
1997
+ completeCanvasRefresh();
1998
+ }
1868
1999
  break;
1869
2000
  case "pageSkeletonUpdated":
1870
2001
  pageViewContext.setPageSkeleton(event.payload.pageSkeleton);
1871
2002
  break;
1872
2003
  case "geometryUpdated": {
2004
+ if (isPerfTraceActive()) {
2005
+ perfTraceStep("frame:geometry-updated-received", {
2006
+ targets: event.payload.geometry.targets.length,
2007
+ transportMs: Date.now() - event.timestamp,
2008
+ });
2009
+ }
1873
2010
  latestBridgeScrollRef.current = event.payload.geometry.scroll;
1874
2011
  pageViewContext.bridgeGeometry = event.payload.geometry;
1875
2012
  pageViewContext.setBridgeGeometry(event.payload.geometry);
@@ -1893,11 +2030,22 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1893
2030
  window.dispatchEvent(new CustomEvent(BRIDGE_DOM_UPDATED_EVENT));
1894
2031
  break;
1895
2032
  case "refreshStarted":
2033
+ logFieldValueDiagnostic("bridge-frame", "refresh started", {
2034
+ revision: event.payload.revision,
2035
+ activeInlineEdit: activeBridgeInlineEditRef.current,
2036
+ patchSignatureCount: bridgePatchSignatureRef.current.size,
2037
+ });
1896
2038
  clearBridgeTextCompletion();
1897
2039
  clearBridgePatchSignatures();
1898
2040
  clearActiveBridgeInlineEdit();
1899
2041
  break;
1900
2042
  case "refreshCompleted":
2043
+ logFieldValueDiagnostic("bridge-frame", "refresh completed", {
2044
+ activeInlineEdit: activeBridgeInlineEditRef.current,
2045
+ hasStructure: !!event.payload.structure,
2046
+ hasGeometry: !!event.payload.geometry,
2047
+ structureFieldCount: event.payload.structure?.fields.length,
2048
+ });
1901
2049
  // Runtime refresh replaces host DOM without a new bridge ready event.
1902
2050
  // Field patches sent during the refresh window may have been cached
1903
2051
  // against the previous DOM generation, so force the refreshed host to
@@ -1934,6 +2082,18 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1934
2082
  };
1935
2083
  restorePendingRefreshScroll(iframe, targetScroll, observedScroll);
1936
2084
  }
2085
+ if (event.payload.applied === "host-app") {
2086
+ // The page's own app (e.g. a Next.js rendering host) accepted
2087
+ // the refresh, but its re-render lands later as ordinary DOM
2088
+ // mutations. Completing now would reopen viewport reactions
2089
+ // (selection auto-scroll, scrollIntoView, the inserting
2090
+ // indicator) while geometry still describes the pre-edit DOM —
2091
+ // wait for the mutation-driven structureUpdated instead.
2092
+ awaitingHostAppRefreshRenderRef.current = true;
2093
+ }
2094
+ else {
2095
+ completeCanvasRefresh();
2096
+ }
1937
2097
  setShowSpinner(false);
1938
2098
  break;
1939
2099
  case "selectionChanged":
@@ -1944,11 +2104,32 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1944
2104
  if (event.payload.elementKey && event.payload.fieldId) {
1945
2105
  setIsBridgeInlineEditing(true);
1946
2106
  }
2107
+ logFieldValueDiagnostic("bridge-frame", "fieldValueChanged event", {
2108
+ elementKey: event.payload.elementKey,
2109
+ fieldId: event.payload.fieldId,
2110
+ item: event.payload.item,
2111
+ isRichText: event.payload.isRichText,
2112
+ value: summarizeFieldValue(event.payload.value),
2113
+ activeInlineEdit: activeBridgeInlineEditRef.current,
2114
+ });
1947
2115
  handleBridgeFieldValueChanged(event.payload);
1948
2116
  break;
1949
2117
  case "inlineEditEnded":
2118
+ logFieldValueDiagnostic("bridge-frame", "inlineEditEnded event", {
2119
+ reason: event.payload.reason,
2120
+ elementKey: event.payload.elementKey,
2121
+ fieldId: event.payload.fieldId,
2122
+ item: event.payload.item,
2123
+ isRichText: event.payload.isRichText,
2124
+ value: summarizeFieldValue(event.payload.value),
2125
+ activeInlineEdit: activeBridgeInlineEditRef.current,
2126
+ });
1950
2127
  handleBridgeInlineEditEnded(event.payload);
1951
2128
  clearActiveBridgeInlineEdit();
2129
+ logFieldValueDiagnostic("bridge-frame", "active inline edit cleared after end", {
2130
+ fieldId: event.payload.fieldId,
2131
+ reason: event.payload.reason,
2132
+ });
1952
2133
  break;
1953
2134
  case "interaction":
1954
2135
  if (event.payload.kind === "hover") {
@@ -1960,6 +2141,24 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1960
2141
  else {
1961
2142
  lastBridgeHoverInteractionRef.current = undefined;
1962
2143
  }
2144
+ if (isPerfTraceEnabled() &&
2145
+ (event.payload.kind === "click" ||
2146
+ event.payload.kind === "doubleClick" ||
2147
+ event.payload.kind === "contextMenu")) {
2148
+ // Backdate to the host's DOM-event timestamp when available so
2149
+ // host-side dispatch (hit-test + deferred-click debounce) is
2150
+ // part of the waterfall; the envelope timestamp is when the
2151
+ // host posted the message.
2152
+ const eventAt = event.payload.perf?.eventAt;
2153
+ beginPerfTrace(`select-${event.payload.kind}`, {
2154
+ componentId: event.payload.componentId,
2155
+ fieldId: event.payload.fieldId,
2156
+ hostDispatchMs: eventAt !== undefined
2157
+ ? event.timestamp - eventAt
2158
+ : "n/a (host without perf tracing)",
2159
+ transportMs: Date.now() - event.timestamp,
2160
+ }, eventAt ?? event.timestamp);
2161
+ }
1963
2162
  pageViewContext.setBridgeInteraction(event.payload);
1964
2163
  void handleBridgeInteraction(event.payload, iframe);
1965
2164
  break;
@@ -2032,6 +2231,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2032
2231
  scheduleBridgeGeometryRevision,
2033
2232
  shouldTrackMinimapScroll,
2034
2233
  restorePendingRefreshScroll,
2234
+ completeCanvasRefresh,
2035
2235
  ]);
2036
2236
  useEffect(() => {
2037
2237
  const repository = editContext.itemsRepository;
@@ -2421,6 +2621,8 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2421
2621
  pendingRefreshScrollRef.current ??=
2422
2622
  getIframeWindowScroll(iframeRef.current) ??
2423
2623
  latestBridgeScrollRef.current;
2624
+ canvasRefreshInFlightSinceRef.current = Date.now();
2625
+ awaitingHostAppRefreshRenderRef.current = false;
2424
2626
  setShowSpinner(true);
2425
2627
  console.log(initialLoad
2426
2628
  ? "Initial load - setting iframe src"
@@ -2443,6 +2645,8 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2443
2645
  }
2444
2646
  console.log("Bridge - requesting iframe refresh");
2445
2647
  pageViewContext.setIframeSupportsRefresh(true);
2648
+ canvasRefreshInFlightSinceRef.current = Date.now();
2649
+ awaitingHostAppRefreshRenderRef.current = false;
2446
2650
  const accepted = bridge.sendCommand("refresh", {
2447
2651
  url: renderUrl.toString(),
2448
2652
  revision: editContext.revision,
@@ -2450,6 +2654,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2450
2654
  });
2451
2655
  if (!accepted) {
2452
2656
  pendingRefreshScrollRef.current = undefined;
2657
+ canvasRefreshInFlightSinceRef.current = null;
2453
2658
  pageViewContext.setIframeSupportsRefresh(false);
2454
2659
  runFallbackRefresh();
2455
2660
  }
@@ -2486,8 +2691,18 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2486
2691
  ]);
2487
2692
  useEffect(() => {
2488
2693
  if (fieldsContext?.focusedField) {
2489
- if (editContext.selection.length > 0 &&
2490
- fieldsContext.focusedField.item.id !== editContext.selection[0])
2694
+ // A refresh is being applied: the field's rect still describes the
2695
+ // pre-edit DOM (after a move, its old position — where the drag
2696
+ // started), so scrolling now would yank the canvas to a stale spot.
2697
+ // This fires on the post-operation auto-focus of the moved/added
2698
+ // component; a genuine user focus during a refresh loses one
2699
+ // auto-scroll, which is the lesser evil.
2700
+ if (isCanvasRefreshInFlight())
2701
+ return;
2702
+ // Event-time read: this effect is keyed on focusedField, not selection.
2703
+ const currentSelection = editContext.getSelection();
2704
+ if (currentSelection.length > 0 &&
2705
+ fieldsContext.focusedField.item.id !== currentSelection[0])
2491
2706
  return;
2492
2707
  const bounds = findBridgeFieldDocumentBounds(pageViewContextRef.current?.bridgeGeometry, fieldsContext.focusedField);
2493
2708
  const activePageViewContext = pageViewContextRef.current;
@@ -2495,7 +2710,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2495
2710
  scrollBridgeBoundsIntoView(activePageViewContext, bounds, latestBridgeScrollRef.current);
2496
2711
  }
2497
2712
  }
2498
- }, [fieldsContext?.focusedField]);
2713
+ }, [fieldsContext?.focusedField, isCanvasRefreshInFlight]);
2499
2714
  // Drive the selection-scroll suppression window from the drag lifecycle.
2500
2715
  // Declared BEFORE the selection-scroll effect so it runs first in the same
2501
2716
  // commit (when a drop changes both dragObject and selection at once).
@@ -2512,7 +2727,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2512
2727
  }
2513
2728
  }, [editContext.dragObject]);
2514
2729
  useEffect(() => {
2515
- const selectionKey = editContext.selection.join("|");
2730
+ const selectionKey = canvasSelection.join("|");
2516
2731
  const selectionChanged = selectionKey !== lastScrolledSelectionKeyRef.current;
2517
2732
  lastScrolledSelectionKeyRef.current = selectionKey;
2518
2733
  // The effect also re-runs when focusedField changes (e.g. a field blurs
@@ -2525,24 +2740,43 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2525
2740
  suppressNextSelectionScrollRef.current = false;
2526
2741
  return;
2527
2742
  }
2743
+ // Programmatic selections (the drop target's parent, the post-operation
2744
+ // focus of a moved/added component) arm this suppression at their source:
2745
+ // their targets may still measure at pre-edit geometry, and unlike user
2746
+ // clicks they carry no intent to see the component. Deterministic — no
2747
+ // time window to lose against a slow operation or frozen host.
2748
+ if (editContext.consumeNextSelectionAutoScrollSuppression?.())
2749
+ return;
2528
2750
  // Suppressed during a drag and briefly after it ends. The drag owns
2529
2751
  // scrolling, and the drop's re-selection must not jump the page away from
2530
2752
  // where the user dropped (toward stale pre-move geometry).
2531
2753
  if (Date.now() < suppressSelectionScrollUntilRef.current)
2532
2754
  return;
2533
- if (!fieldsContext?.focusedField && editContext.selection.length > 0) {
2534
- const lastSelectedComponent = getComponentById(editContext.selection[editContext.selection.length - 1], pageViewContextRef.current.page);
2755
+ // Suppressed while a refresh is applying: geometry still measures the
2756
+ // pre-refresh DOM (a just-moved component still sits at its old rect), so
2757
+ // scrolling now would target a stale position. The refresh restore owns
2758
+ // the viewport until the refreshed geometry lands.
2759
+ if (isCanvasRefreshInFlight())
2760
+ return;
2761
+ if (!fieldsContext?.focusedField && canvasSelection.length > 0) {
2762
+ const lastSelectedComponent = getComponentById(canvasSelection[canvasSelection.length - 1], pageViewContextRef.current.page);
2535
2763
  if (lastSelectedComponent) {
2536
2764
  editContext.setScrollIntoView(lastSelectedComponent.id);
2537
2765
  }
2538
2766
  }
2539
- }, [editContext.selection, fieldsContext?.focusedField]);
2767
+ }, [canvasSelection, fieldsContext?.focusedField, isCanvasRefreshInFlight]);
2540
2768
  useEffect(() => {
2541
2769
  if (!editContext.scrollIntoView)
2542
2770
  return;
2543
2771
  const activePageViewContext = pageViewContextRef.current;
2544
2772
  if (!activePageViewContext)
2545
2773
  return;
2774
+ // Defer (without consuming the request) while a refresh is applying:
2775
+ // resolving now would measure the target against pre-refresh geometry and
2776
+ // scroll to a stale rect. The effect re-runs when the refreshed geometry
2777
+ // arrives (bridgeGeometry dependency) and resolves against fresh bounds.
2778
+ if (isCanvasRefreshInFlight())
2779
+ return;
2546
2780
  const scrollTarget = editContext.scrollIntoView;
2547
2781
  const requestState = scrollIntoViewRequestRef.current;
2548
2782
  const signature = getScrollIntoViewSignature(scrollTarget);
@@ -2577,6 +2811,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2577
2811
  editContext.scrollIntoView,
2578
2812
  pageViewContext.page,
2579
2813
  pageViewContext.bridgeGeometry,
2814
+ isCanvasRefreshInFlight,
2580
2815
  ]);
2581
2816
  useEffect(() => {
2582
2817
  const handleMessage = (message) => {
@@ -2651,10 +2886,16 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2651
2886
  };
2652
2887
  }, [iframeElement]);
2653
2888
  useEffect(() => {
2654
- bridgeClientRef.current?.sendCommand("setSelection", {
2655
- componentIds: editContext.selection,
2889
+ const sent = bridgeClientRef.current?.sendCommand("setSelection", {
2890
+ componentIds: canvasSelection,
2656
2891
  });
2657
- }, [editContext.selection]);
2892
+ if (isPerfTraceActive()) {
2893
+ perfTraceStep("frame:setSelection-command-sent", {
2894
+ ids: canvasSelection,
2895
+ sent: sent ?? false,
2896
+ });
2897
+ }
2898
+ }, [canvasSelection]);
2658
2899
  useEffect(() => {
2659
2900
  const bridge = bridgeClientRef.current;
2660
2901
  bridge?.sendCommand("setPreviewMode", {
@@ -2787,6 +3028,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2787
3028
  if (pendingRefreshScrollRef.current) {
2788
3029
  restorePendingRefreshScroll(iframeRef.current, pendingRefreshScrollRef.current);
2789
3030
  }
3031
+ completeCanvasRefresh();
2790
3032
  }
2791
3033
  } }), iframeElement && (_jsx(IframeOverlayProvider, { iframe: iframeElement, mode: "scrolling", scrollScale: 1, visualScale: 1, invalidationKey: `${zoom}:${bridgeGeometryRevision}`, deferredInvalidationMs: ZOOM_TRANSITION_MS + 50, bleed: { top: 2, bottom: 2, left: 2 }, children: _jsx(PageEditorChrome, { iframe: iframeElement, compareView: compareView, pageViewContext: pageViewContext, onBridgeRichTextCommand: sendApplyRichTextCommand }) })), pageViewContext.deviceHeight && pageViewContext.device && (_jsx("div", { className: "bg-neutral-grey-5 relative z-40 h-full w-full" }))] }), shouldRenderMiniMap && (_jsx(MiniMap, { compareView: compareView, scroll: scroll, mainViewIframeRef: iframeRef, pageViewContext: pageViewContext })), showSpinner && (_jsxs(_Fragment, { children: [_jsx("div", { className: "bg-neutral-grey-5/50 absolute top-0 left-0 h-full w-full" }), _jsx("div", { className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform", children: _jsx(Spinner, {}) })] }))] }), _jsx(FieldActionsOverlay, { ref: fieldActionsOverlay, generatorButtons: contextMenuFieldButtons, onActionClick: handleActionClick, onParameterizedActionExecute: handleParameterizedActionExecute, currentOverlay: editContext?.currentOverlay, fieldId: contextMenuField?.fieldId || "", setCurrentOverlay: editContext?.setCurrentOverlay || (() => { }), preSelectedAction: preSelectedAction })] }));
2792
3034
  }