@parhelia/core 0.1.12927 → 0.1.12931

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 (217) hide show
  1. package/dist/agents-view/AgentCard.js +4 -1
  2. package/dist/agents-view/AgentCard.js.map +1 -1
  3. package/dist/agents-view/AgentProfileIcon.js +8 -8
  4. package/dist/agents-view/AgentProfileIcon.js.map +1 -1
  5. package/dist/agents-view/AgentsView.js +1 -1
  6. package/dist/client-components/index.d.ts +1 -1
  7. package/dist/client-components/index.js +1 -1
  8. package/dist/client-components/index.js.map +1 -1
  9. package/dist/components/WorkspaceHomeView.d.ts +13 -0
  10. package/dist/components/WorkspaceHomeView.js +13 -0
  11. package/dist/components/WorkspaceHomeView.js.map +1 -0
  12. package/dist/components/ui/styled-dialog-title.d.ts +3 -1
  13. package/dist/components/ui/styled-dialog-title.js +2 -2
  14. package/dist/components/ui/styled-dialog-title.js.map +1 -1
  15. package/dist/config/config.js +7 -19
  16. package/dist/config/config.js.map +1 -1
  17. package/dist/config/notificationRoutes.js +1 -1
  18. package/dist/config/notificationRoutes.js.map +1 -1
  19. package/dist/editor/ConfirmationDialog.js +1 -1
  20. package/dist/editor/ConfirmationDialog.js.map +1 -1
  21. package/dist/editor/FieldListField.js +8 -0
  22. package/dist/editor/FieldListField.js.map +1 -1
  23. package/dist/editor/ImageEditButton.js +14 -2
  24. package/dist/editor/ImageEditButton.js.map +1 -1
  25. package/dist/editor/ImageEditor.js +1 -1
  26. package/dist/editor/ImageEditor.js.map +1 -1
  27. package/dist/editor/MainLayout.js +2 -1
  28. package/dist/editor/MainLayout.js.map +1 -1
  29. package/dist/editor/PerfProfiler.d.ts +11 -0
  30. package/dist/editor/PerfProfiler.js +29 -0
  31. package/dist/editor/PerfProfiler.js.map +1 -0
  32. package/dist/editor/PictureEditor.js +1 -1
  33. package/dist/editor/PictureEditor.js.map +1 -1
  34. package/dist/editor/ai/Agents.js +15 -79
  35. package/dist/editor/ai/Agents.js.map +1 -1
  36. package/dist/editor/ai/GuidanceOverlay.js +4 -9
  37. package/dist/editor/ai/GuidanceOverlay.js.map +1 -1
  38. package/dist/editor/ai/InlineAiDialog.js +2 -2
  39. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  40. package/dist/editor/ai/dialogs/capturePageScreenshot.js +1 -1
  41. package/dist/editor/ai/dialogs/capturePageScreenshot.js.map +1 -1
  42. package/dist/editor/ai/terminal/components/AgentMemoryPanel.d.ts +12 -0
  43. package/dist/editor/ai/terminal/components/AgentMemoryPanel.js +131 -0
  44. package/dist/editor/ai/terminal/components/AgentMemoryPanel.js.map +1 -0
  45. package/dist/editor/ai/terminal/components/AgentPromptTrayPopovers.js +10 -2
  46. package/dist/editor/ai/terminal/components/AgentPromptTrayPopovers.js.map +1 -1
  47. package/dist/editor/ai/terminal/components/AgentSettingsContent.d.ts +2 -1
  48. package/dist/editor/ai/terminal/components/AgentSettingsContent.js.map +1 -1
  49. package/dist/editor/ai/terminal/components/AgentSettingsSections.d.ts +2 -1
  50. package/dist/editor/ai/terminal/components/AgentSettingsSections.js +9 -2
  51. package/dist/editor/ai/terminal/components/AgentSettingsSections.js.map +1 -1
  52. package/dist/editor/ai/terminal/components/AgentTerminalContextPanels.js +11 -0
  53. package/dist/editor/ai/terminal/components/AgentTerminalContextPanels.js.map +1 -1
  54. package/dist/editor/ai/terminal/components/AgentTerminalStatusBar.js +27 -20
  55. package/dist/editor/ai/terminal/components/AgentTerminalStatusBar.js.map +1 -1
  56. package/dist/editor/ai/terminal/components/ContextInfoBar.js +4 -3
  57. package/dist/editor/ai/terminal/components/ContextInfoBar.js.map +1 -1
  58. package/dist/editor/ai/terminal/useAgentSettingsData.d.ts +8 -1
  59. package/dist/editor/ai/terminal/useAgentSettingsData.js +44 -5
  60. package/dist/editor/ai/terminal/useAgentSettingsData.js.map +1 -1
  61. package/dist/editor/ai/terminal/useAgentSettingsPanel.d.ts +1 -1
  62. package/dist/editor/bridge/BridgeClient.js +9 -1
  63. package/dist/editor/bridge/BridgeClient.js.map +1 -1
  64. package/dist/editor/bridge/protocol.d.ts +19 -0
  65. package/dist/editor/bridge/protocol.js +1 -1
  66. package/dist/editor/bridge/protocol.js.map +1 -1
  67. package/dist/editor/client/EditorShell.js +163 -67
  68. package/dist/editor/client/EditorShell.js.map +1 -1
  69. package/dist/editor/client/editContext.d.ts +35 -2
  70. package/dist/editor/client/editContext.js +8 -0
  71. package/dist/editor/client/editContext.js.map +1 -1
  72. package/dist/editor/client/fieldModificationStore.js +64 -0
  73. package/dist/editor/client/fieldModificationStore.js.map +1 -1
  74. package/dist/editor/client/itemsRepository.js +95 -3
  75. package/dist/editor/client/itemsRepository.js.map +1 -1
  76. package/dist/editor/client/operations.d.ts +1 -0
  77. package/dist/editor/client/operations.js +166 -13
  78. package/dist/editor/client/operations.js.map +1 -1
  79. package/dist/editor/client/ui/EditorChrome.js +6 -5
  80. package/dist/editor/client/ui/EditorChrome.js.map +1 -1
  81. package/dist/editor/commands/componentCommands.js +2 -1
  82. package/dist/editor/commands/componentCommands.js.map +1 -1
  83. package/dist/editor/commands/keyboardCommands.js +1 -1
  84. package/dist/editor/commands/keyboardCommands.js.map +1 -1
  85. package/dist/editor/componentTreeHelper.js +15 -5
  86. package/dist/editor/componentTreeHelper.js.map +1 -1
  87. package/dist/editor/context-menu/InsertMenu.js +1 -1
  88. package/dist/editor/context-menu/InsertMenu.js.map +1 -1
  89. package/dist/editor/field-types/SingleLineText.js +79 -9
  90. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  91. package/dist/editor/fieldValueDiagnostics.d.ts +79 -0
  92. package/dist/editor/fieldValueDiagnostics.js +133 -0
  93. package/dist/editor/fieldValueDiagnostics.js.map +1 -0
  94. package/dist/editor/menubar/PageSelector.js +1 -1
  95. package/dist/editor/menubar/PageSelector.js.map +1 -1
  96. package/dist/editor/menubar/toolbar-sections/EditControls.js +1 -2
  97. package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
  98. package/dist/editor/menubar/toolbar-sections/ManualBrowser.js +6 -0
  99. package/dist/editor/menubar/toolbar-sections/ManualBrowser.js.map +1 -1
  100. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +5 -5
  101. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  102. package/dist/editor/page-editor-chrome/DragInsertionOverlay.d.ts +8 -1
  103. package/dist/editor/page-editor-chrome/DragInsertionOverlay.js +203 -51
  104. package/dist/editor/page-editor-chrome/DragInsertionOverlay.js.map +1 -1
  105. package/dist/editor/page-editor-chrome/EmptyImageFieldIndicators.d.ts +10 -0
  106. package/dist/editor/page-editor-chrome/EmptyImageFieldIndicators.js +236 -0
  107. package/dist/editor/page-editor-chrome/EmptyImageFieldIndicators.js.map +1 -0
  108. package/dist/editor/page-editor-chrome/FrameMenu.d.ts +2 -0
  109. package/dist/editor/page-editor-chrome/FrameMenu.js +8 -6
  110. package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
  111. package/dist/editor/page-editor-chrome/FrameMenus.d.ts +3 -1
  112. package/dist/editor/page-editor-chrome/FrameMenus.js +4 -4
  113. package/dist/editor/page-editor-chrome/FrameMenus.js.map +1 -1
  114. package/dist/editor/page-editor-chrome/PageEditorChrome.js +18 -4
  115. package/dist/editor/page-editor-chrome/PageEditorChrome.js.map +1 -1
  116. package/dist/editor/page-editor-chrome/PictureEditorOverlay.d.ts +3 -0
  117. package/dist/editor/page-editor-chrome/PictureEditorOverlay.js +5 -8
  118. package/dist/editor/page-editor-chrome/PictureEditorOverlay.js.map +1 -1
  119. package/dist/editor/page-editor-chrome/PlaceholderDropZone.d.ts +8 -1
  120. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +20 -5
  121. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  122. package/dist/editor/page-editor-chrome/PlaceholderDropZones.d.ts +3 -1
  123. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js +2 -2
  124. package/dist/editor/page-editor-chrome/PlaceholderDropZones.js.map +1 -1
  125. package/dist/editor/page-editor-chrome/overlay/placeholderZones.d.ts +10 -0
  126. package/dist/editor/page-editor-chrome/overlay/placeholderZones.js +61 -1
  127. package/dist/editor/page-editor-chrome/overlay/placeholderZones.js.map +1 -1
  128. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js +121 -48
  129. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js.map +1 -1
  130. package/dist/editor/page-viewer/EditorForm.js +94 -7
  131. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  132. package/dist/editor/page-viewer/MiniMap.js +74 -9
  133. package/dist/editor/page-viewer/MiniMap.js.map +1 -1
  134. package/dist/editor/page-viewer/PageViewer.js +9 -7
  135. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  136. package/dist/editor/page-viewer/PageViewerFrame.js +390 -54
  137. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  138. package/dist/editor/page-viewer/bridgeFieldPatch.js +49 -2
  139. package/dist/editor/page-viewer/bridgeFieldPatch.js.map +1 -1
  140. package/dist/editor/page-viewer/minimapVisibility.d.ts +33 -0
  141. package/dist/editor/page-viewer/minimapVisibility.js +72 -0
  142. package/dist/editor/page-viewer/minimapVisibility.js.map +1 -0
  143. package/dist/editor/page-viewer/pageViewContext.d.ts +1 -0
  144. package/dist/editor/page-viewer/pageViewContext.js +7 -0
  145. package/dist/editor/page-viewer/pageViewContext.js.map +1 -1
  146. package/dist/editor/page-viewer/renderUrl.js +29 -7
  147. package/dist/editor/page-viewer/renderUrl.js.map +1 -1
  148. package/dist/editor/perfTrace.d.ts +46 -0
  149. package/dist/editor/perfTrace.js +213 -0
  150. package/dist/editor/perfTrace.js.map +1 -0
  151. package/dist/editor/reviews/CommentPopover.js +2 -3
  152. package/dist/editor/reviews/CommentPopover.js.map +1 -1
  153. package/dist/editor/reviews/CreateReviewConfirmStep.js +5 -4
  154. package/dist/editor/reviews/CreateReviewConfirmStep.js.map +1 -1
  155. package/dist/editor/reviews/CreateReviewDialog.js +120 -202
  156. package/dist/editor/reviews/CreateReviewDialog.js.map +1 -1
  157. package/dist/editor/reviews/CreateReviewItemsStep.d.ts +18 -0
  158. package/dist/editor/reviews/CreateReviewItemsStep.js +6 -0
  159. package/dist/editor/reviews/CreateReviewItemsStep.js.map +1 -0
  160. package/dist/editor/reviews/CreateReviewLanguagesStep.d.ts +9 -0
  161. package/dist/editor/reviews/CreateReviewLanguagesStep.js +36 -0
  162. package/dist/editor/reviews/CreateReviewLanguagesStep.js.map +1 -0
  163. package/dist/editor/reviews/CreateReviewReviewersStep.d.ts +17 -0
  164. package/dist/editor/reviews/CreateReviewReviewersStep.js +33 -0
  165. package/dist/editor/reviews/CreateReviewReviewersStep.js.map +1 -0
  166. package/dist/editor/reviews/CreateReviewSettingsStep.d.ts +21 -0
  167. package/dist/editor/reviews/CreateReviewSettingsStep.js +33 -0
  168. package/dist/editor/reviews/CreateReviewSettingsStep.js.map +1 -0
  169. package/dist/editor/reviews/MultiReviewManager.js +1 -4
  170. package/dist/editor/reviews/MultiReviewManager.js.map +1 -1
  171. package/dist/editor/reviews/commentAi.js +1 -1
  172. package/dist/editor/reviews/commentAi.js.map +1 -1
  173. package/dist/editor/reviews/openCommentsFeedback.d.ts +2 -6
  174. package/dist/editor/reviews/openCommentsFeedback.js +3 -11
  175. package/dist/editor/reviews/openCommentsFeedback.js.map +1 -1
  176. package/dist/editor/services/agentService.d.ts +38 -0
  177. package/dist/editor/services/agentService.js +42 -0
  178. package/dist/editor/services/agentService.js.map +1 -1
  179. package/dist/editor/services/aiService.d.ts +4 -1
  180. package/dist/editor/services/aiService.js.map +1 -1
  181. package/dist/editor/services/editService.d.ts +1 -1
  182. package/dist/editor/services/editService.js +2 -2
  183. package/dist/editor/services/editService.js.map +1 -1
  184. package/dist/editor/services/reviewsService.d.ts +6 -0
  185. package/dist/editor/services/reviewsService.js +15 -0
  186. package/dist/editor/services/reviewsService.js.map +1 -1
  187. package/dist/editor/settings/panels/AgentProfileConfigPanel.js +2 -1
  188. package/dist/editor/settings/panels/AgentProfileConfigPanel.js.map +1 -1
  189. package/dist/editor/sidebar/ComponentTree.js +31 -10
  190. package/dist/editor/sidebar/ComponentTree.js.map +1 -1
  191. package/dist/editor/sidebar/ContentTreePanel.d.ts +2 -2
  192. package/dist/editor/sidebar/ContentTreePanel.js +6 -2
  193. package/dist/editor/sidebar/ContentTreePanel.js.map +1 -1
  194. package/dist/editor/sidebar/MainContentTree.js +3 -2
  195. package/dist/editor/sidebar/MainContentTree.js.map +1 -1
  196. package/dist/editor/tree-indicators/GutterColumns.js +24 -6
  197. package/dist/editor/tree-indicators/GutterColumns.js.map +1 -1
  198. package/dist/editor/ui/ItemCollectionEditor.js +4 -4
  199. package/dist/editor/ui/ItemCollectionEditor.js.map +1 -1
  200. package/dist/editor/ui/PerfectTree.js +5 -5
  201. package/dist/editor/ui/PerfectTree.js.map +1 -1
  202. package/dist/editor/ui/PublishItemDialog.js +1 -1
  203. package/dist/editor/ui/PublishItemDialog.js.map +1 -1
  204. package/dist/editor/ui/PublishRestrictionsDialog.js +179 -148
  205. package/dist/editor/ui/PublishRestrictionsDialog.js.map +1 -1
  206. package/dist/editor/utils/keyboardNavigation.js +9 -2
  207. package/dist/editor/utils/keyboardNavigation.js.map +1 -1
  208. package/dist/editor/views/SingleEditView.js +32 -2
  209. package/dist/editor/views/SingleEditView.js.map +1 -1
  210. package/dist/index.d.ts +3 -1
  211. package/dist/index.js +2 -1
  212. package/dist/index.js.map +1 -1
  213. package/dist/revision.d.ts +2 -2
  214. package/dist/revision.js +2 -2
  215. package/dist/task-board/components/TaskBoardHomeView.js +3 -6
  216. package/dist/task-board/components/TaskBoardHomeView.js.map +1 -1
  217. package/package.json +1 -1
@@ -1,14 +1,15 @@
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 { resolveMiniMapVisibility, } from "./minimapVisibility";
5
+ import { useCanvasSelection, useEditContext, useEditContextRef, useFieldsEditContext, useFieldsEditContextRef, } from "../client/editContext";
5
6
  import { useSlotContext } from "../views/editorSlotContext";
6
7
  import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
7
8
  import { PageEditorChrome } from "../page-editor-chrome/PageEditorChrome";
8
9
  import { useBridgeInlineEditing, } from "../page-editor-chrome/useBridgeInlineEditing";
9
10
  import { getInlineAiAnchorFromBridgeToolbar, getVisibleBridgeToolbarRect, waitForVisibleBridgeToolbarRect, } from "../page-editor-chrome/bridgeInlineFormatToolbarLayout";
10
11
  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";
12
+ import { CANVAS_REFRESH_COMPLETED_EVENT, DEVICE_CHANGE_EVENT, useViewportChangeSignal, } from "./pageViewContext";
12
13
  import uuid from "react-uuid";
13
14
  import { cn } from "../../lib/utils";
14
15
  import { normalizeMarkerId } from "../utils";
@@ -25,9 +26,12 @@ import { Spinner } from "../ui/Spinner";
25
26
  import { DeviceToolbar } from "./DeviceToolbar";
26
27
  import { FullscreenControls } from "../client/ui/FullscreenControls";
27
28
  import { toSitecoreDate } from "../utils/sitecoreDate";
29
+ import { pickVersionAtDate } from "../utils/versionAtDate";
28
30
  import { BridgeClient } from "../bridge/BridgeClient";
31
+ import { beginPerfTrace, isPerfTraceActive, isPerfTraceEnabled, perfTraceStep, } from "../perfTrace";
29
32
  import { getSuggestionDisplayValue } from "../reviews/suggestionDisplayValue";
30
33
  import { buildBridgeFieldPatchPayload, getBridgeFieldPatchValue, isBridgePatchableTextField, } from "./bridgeFieldPatch";
34
+ import { logFieldValueDiagnostic, summarizeField, summarizeFieldDescriptor, summarizeFieldValue, } from "../fieldValueDiagnostics";
31
35
  import { createKeyboardEventFromBridgeInteraction } from "./bridgeKeyboardShortcuts";
32
36
  import { formatParentBridgeInjectionDiagnostic, injectParheliaBridgeFromParent, } from "./parentBridgeInjection";
33
37
  import { getTextCompletion, normalizeTextCompletion, } from "../services/textCompletionService";
@@ -39,6 +43,9 @@ const BRIDGE_INLINE_EDIT_RELEASE_EVENT = "parhelia:bridge-inline-edit-release";
39
43
  const INLINE_AI_CLOSE_EVENT = "inline-ai-close";
40
44
  const BRIDGE_INLINE_LINK_OPEN_EVENT = "bridge-inline-link-open";
41
45
  const SCROLL_INTO_VIEW_BRIDGE_SOURCE = "scroll-into-view";
46
+ // How long a requested canvas refresh may suppress viewport reactions
47
+ // (selection auto-scroll, scrollIntoView) before it is presumed lost.
48
+ const CANVAS_REFRESH_IN_FLIGHT_MAX_MS = 15000;
42
49
  // Suggestions-mode cursor: a text I-beam (the click insertion point) badged with
43
50
  // the lucide PenLine icon from the "Suggestions mode" toolbar button. A white
44
51
  // halo keeps it visible on any page background; the hotspot is the I-beam centre.
@@ -602,6 +609,7 @@ export function PageViewerFrame(props) {
602
609
  function PageViewerFrameContent({ compareView, pageViewContext, editContext, className, slotCloseButton, }) {
603
610
  const fieldsContext = useFieldsEditContext();
604
611
  const slotContext = useSlotContext();
612
+ const canvasSelection = useCanvasSelection();
605
613
  const pathname = usePathname();
606
614
  const pageViewContextRef = useRef(undefined);
607
615
  useEffect(() => {
@@ -636,6 +644,42 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
636
644
  const directOverlayScrollSyncRef = useRef(false);
637
645
  const lastBridgeHoverInteractionRef = useRef(undefined);
638
646
  const pendingRefreshScrollRef = useRef(undefined);
647
+ // When the pending refresh scroll was captured, and when the last explicit
648
+ // scrollBy command (minimap click/wheel) was issued. A capture is only safe
649
+ // to restore while no explicit command is newer than it: the capture
650
+ // describes where the canvas happened to be at refresh-request time, and
651
+ // blindly re-asserting it after the user commanded a scroll yanks the
652
+ // canvas away from where they sent it.
653
+ const pendingRefreshScrollCapturedAtRef = useRef(0);
654
+ const lastExplicitScrollCommandAtRef = useRef(0);
655
+ // Signature of the last canvas render actually requested (URL sans the
656
+ // per-request edit_rev nonce, plus the edit revision). When a re-run of the
657
+ // render effect produces the same signature — e.g. an edit<->suggestions
658
+ // mode toggle, which changes no host-rendered output — the refresh is
659
+ // skipped entirely: the bridge learns the mode via setEditorMode, and a
660
+ // needless refresh would drop scroll position and inline-edit state.
661
+ const lastRenderSignatureRef = useRef(undefined);
662
+ // Set when a canvas refresh (bridge patch or full reload) has been requested
663
+ // and its result has not landed yet. While in flight, bridgeGeometry still
664
+ // describes the PRE-refresh DOM — a moved component is still measured at its
665
+ // old position — so viewport-affecting reactions (selection auto-scroll,
666
+ // scrollIntoView) must wait for the refreshed geometry instead of scrolling
667
+ // toward stale rects. Timestamped so a refresh that never completes cannot
668
+ // suppress scrolling forever.
669
+ const canvasRefreshInFlightSinceRef = useRef(null);
670
+ // Set when refreshCompleted arrived with applied:"host-app": the page's own
671
+ // app accepted the refresh but its re-render lands later as DOM mutations.
672
+ // The in-flight window then stays open until the next structureUpdated.
673
+ const awaitingHostAppRefreshRenderRef = useRef(false);
674
+ const isCanvasRefreshInFlight = useCallback(() => {
675
+ const since = canvasRefreshInFlightSinceRef.current;
676
+ return (since !== null && Date.now() - since < CANVAS_REFRESH_IN_FLIGHT_MAX_MS);
677
+ }, []);
678
+ const completeCanvasRefresh = useCallback(() => {
679
+ awaitingHostAppRefreshRenderRef.current = false;
680
+ canvasRefreshInFlightSinceRef.current = null;
681
+ window.dispatchEvent(new CustomEvent(CANVAS_REFRESH_COMPLETED_EVENT));
682
+ }, []);
639
683
  const restorePendingRefreshScroll = useCallback((iframe, targetScroll, observedScroll) => {
640
684
  if (!targetScroll)
641
685
  return;
@@ -644,6 +688,15 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
644
688
  pendingRefreshScrollRef.current = undefined;
645
689
  }
646
690
  };
691
+ if (lastExplicitScrollCommandAtRef.current >
692
+ pendingRefreshScrollCapturedAtRef.current) {
693
+ // The user commanded a scroll after this position was captured; the
694
+ // bridge is delivering that newer target. Restoring the stale capture
695
+ // would fight it (restoreIframeWindowScroll re-asserts for ~150
696
+ // frames and cancels in-flight smooth scrolls).
697
+ clearPendingTarget();
698
+ return;
699
+ }
647
700
  const currentScroll = getIframeWindowScroll(iframe) ??
648
701
  observedScroll ?? { x: 0, y: 0 };
649
702
  const deltaX = targetScroll.x - currentScroll.x;
@@ -689,6 +742,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
689
742
  const [loadedIframeSrc, setLoadedIframeSrc] = useState();
690
743
  const [scroll, setScroll] = useState(0);
691
744
  const [showMiniMap, setShowMiniMap] = useState(false);
745
+ const miniMapVisibilityMemoryRef = useRef({});
692
746
  const [bridgeGeometryRevision, setBridgeGeometryRevision] = useState(0);
693
747
  const [bridgeDomRevision, setBridgeDomRevision] = useState(0);
694
748
  const bridgeGeometryRevisionRafRef = useRef(null);
@@ -775,16 +829,34 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
775
829
  // Iframe blur is handled by the bridge inline-edit lifecycle now.
776
830
  }, []);
777
831
  const sendBeginInlineEdit = useCallback((payload) => {
778
- return (bridgeClientRef.current?.sendCommand("beginInlineEdit", payload) ??
779
- false);
832
+ const sent = bridgeClientRef.current?.sendCommand("beginInlineEdit", payload) ??
833
+ false;
834
+ logFieldValueDiagnostic("bridge-frame", "beginInlineEdit command", {
835
+ sent,
836
+ payload,
837
+ });
838
+ return sent;
780
839
  }, []);
781
840
  const sendCancelInlineEdit = useCallback((payload) => {
782
- return (bridgeClientRef.current?.sendCommand("cancelInlineEdit", payload) ??
783
- false);
841
+ const sent = bridgeClientRef.current?.sendCommand("cancelInlineEdit", payload) ??
842
+ false;
843
+ logFieldValueDiagnostic("bridge-frame", "cancelInlineEdit command", {
844
+ sent,
845
+ payload,
846
+ });
847
+ return sent;
784
848
  }, []);
785
849
  const sendApplyFieldPatchCommand = useCallback((payload) => {
786
- return (bridgeClientRef.current?.sendCommand("applyFieldPatch", payload) ??
787
- false);
850
+ const sent = bridgeClientRef.current?.sendCommand("applyFieldPatch", payload) ??
851
+ false;
852
+ logFieldValueDiagnostic("bridge-frame", "applyFieldPatch command", {
853
+ sent,
854
+ payload: {
855
+ ...payload,
856
+ value: summarizeFieldValue(payload.value),
857
+ },
858
+ });
859
+ return sent;
788
860
  }, []);
789
861
  const sendApplyRichTextCommand = useCallback((payload) => {
790
862
  return (bridgeClientRef.current?.sendCommand("applyRichTextCommand", payload) ??
@@ -833,6 +905,18 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
833
905
  sendApplyFieldPatch: sendApplyFieldPatchCommand,
834
906
  });
835
907
  const beginTrackedBridgeInlineEdit = useCallback(async (interaction) => {
908
+ logFieldValueDiagnostic("bridge-frame", "begin tracked inline edit", {
909
+ previousActiveInlineEdit: activeBridgeInlineEditRef.current,
910
+ interaction: {
911
+ kind: interaction.kind,
912
+ elementKey: interaction.elementKey,
913
+ fieldId: interaction.fieldId,
914
+ item: interaction.item,
915
+ selectedText: summarizeFieldValue(interaction.selectedText),
916
+ selectionStartOffset: interaction.selectionStartOffset,
917
+ selectionEndOffset: interaction.selectionEndOffset,
918
+ },
919
+ });
836
920
  clearActiveBridgeInlineEdit();
837
921
  const started = await beginBridgeInlineEdit(interaction);
838
922
  if (started && interaction.elementKey && interaction.fieldId) {
@@ -857,6 +941,10 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
857
941
  };
858
942
  setIsBridgeInlineEditing(true);
859
943
  }
944
+ logFieldValueDiagnostic("bridge-frame", "tracked inline edit result", {
945
+ started,
946
+ nextActiveInlineEdit: activeBridgeInlineEditRef.current,
947
+ });
860
948
  return started;
861
949
  }, [
862
950
  beginBridgeInlineEdit,
@@ -865,9 +953,17 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
865
953
  editContextRef,
866
954
  ]);
867
955
  const endTrackedBridgeFieldFocus = useCallback(() => {
956
+ logFieldValueDiagnostic("bridge-frame", "ending tracked field focus", {
957
+ activeInlineEdit: activeBridgeInlineEditRef.current,
958
+ });
868
959
  clearActiveBridgeInlineEdit();
869
960
  endBridgeFieldFocus();
870
961
  }, [clearActiveBridgeInlineEdit, endBridgeFieldFocus]);
962
+ const beginTrackedBridgeInlineEditAfterSelection = useCallback((interaction) => {
963
+ window.setTimeout(() => {
964
+ void beginTrackedBridgeInlineEdit(interaction);
965
+ }, 0);
966
+ }, [beginTrackedBridgeInlineEdit]);
871
967
  useEffect(() => {
872
968
  const handleBridgeInlineEditRelease = (event) => {
873
969
  const detail = event
@@ -881,6 +977,10 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
881
977
  if (activeEdit.item && !bridgeItemMatches(activeEdit.item, field.item)) {
882
978
  return;
883
979
  }
980
+ logFieldValueDiagnostic("bridge-frame", "inline edit released by form", {
981
+ activeInlineEdit: activeEdit,
982
+ field: summarizeFieldDescriptor(field),
983
+ });
884
984
  clearActiveBridgeInlineEdit();
885
985
  bridgePatchSignatureRef.current.delete(`${activeEdit.elementKey}:${activeEdit.fieldId}`);
886
986
  };
@@ -932,12 +1032,33 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
932
1032
  },
933
1033
  mode: showSuggestions ? "suggestions" : "baseline",
934
1034
  });
935
- return {
1035
+ const result = {
936
1036
  value: showSuggestions ? display.mergedValue : display.baselineValue,
937
1037
  source: showSuggestions && display.hasSuggestions
938
1038
  ? "suggestion-preview"
939
1039
  : "real",
940
1040
  };
1041
+ logFieldValueDiagnostic("bridge-patch", "display value resolved", {
1042
+ showSuggestions,
1043
+ preferRepositoryValue,
1044
+ field: summarizeField(field),
1045
+ structureField: {
1046
+ elementKey: structureField.elementKey,
1047
+ fieldId: structureField.fieldId,
1048
+ item: structureField.item,
1049
+ textContent: summarizeFieldValue(structureField.textContent),
1050
+ isRichText: structureField.isRichText,
1051
+ },
1052
+ itemDescriptor,
1053
+ repositoryValue: summarizeFieldValue(repositoryValue),
1054
+ result: {
1055
+ ...result,
1056
+ value: summarizeFieldValue(result.value),
1057
+ },
1058
+ modifiedFieldCount: fieldsContext?.modifiedFields?.length,
1059
+ suggestedEditCount: editContext.suggestedEdits?.length,
1060
+ });
1061
+ return result;
941
1062
  }, [
942
1063
  editContext.mode,
943
1064
  editContext.showSuggestedEdits,
@@ -972,10 +1093,28 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
972
1093
  source: patch.source ?? "",
973
1094
  observedTextContent: observedTextContent ?? "",
974
1095
  });
975
- if (bridgePatchSignatureRef.current.get(cacheKey) === signature) {
1096
+ const previousSignature = bridgePatchSignatureRef.current.get(cacheKey);
1097
+ if (previousSignature === signature) {
1098
+ logFieldValueDiagnostic("bridge-patch", "skipped duplicate send", {
1099
+ cacheKey,
1100
+ patch: {
1101
+ ...patch,
1102
+ value: summarizeFieldValue(patch.value),
1103
+ },
1104
+ observedTextContent: summarizeFieldValue(observedTextContent),
1105
+ });
976
1106
  return false;
977
1107
  }
978
1108
  bridgePatchSignatureRef.current.set(cacheKey, signature);
1109
+ logFieldValueDiagnostic("bridge-patch", "sending patch", {
1110
+ cacheKey,
1111
+ patch: {
1112
+ ...patch,
1113
+ value: summarizeFieldValue(patch.value),
1114
+ },
1115
+ observedTextContent: summarizeFieldValue(observedTextContent),
1116
+ previousSignature,
1117
+ });
979
1118
  bridge.sendCommand("applyFieldPatch", patch);
980
1119
  return true;
981
1120
  }, []);
@@ -1188,16 +1327,22 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1188
1327
  const requestBridgeTextCompletion = useCallback(async (interaction, force = false) => {
1189
1328
  const editor = editContextRef.current;
1190
1329
  if (!editor?.enableCompletions || editor.mode === "preview") {
1330
+ console.debug("[Parhelia] text completion skipped", JSON.stringify({
1331
+ enableCompletions: editor?.enableCompletions,
1332
+ mode: editor?.mode,
1333
+ }));
1191
1334
  clearBridgeTextCompletion();
1192
1335
  return;
1193
1336
  }
1194
1337
  const fieldDescriptor = await resolveBridgeFieldDescriptor(interaction);
1195
1338
  if (!fieldDescriptor) {
1339
+ console.debug("[Parhelia] text completion skipped: no field descriptor", JSON.stringify({ elementKey: interaction.elementKey }));
1196
1340
  clearBridgeTextCompletion();
1197
1341
  return;
1198
1342
  }
1199
1343
  const field = await editor.itemsRepository.getField(fieldDescriptor);
1200
1344
  if (!field) {
1345
+ console.debug("[Parhelia] text completion skipped: field not in repository", JSON.stringify(fieldDescriptor));
1201
1346
  clearBridgeTextCompletion();
1202
1347
  return;
1203
1348
  }
@@ -1212,24 +1357,15 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1212
1357
  if (!elementKey ||
1213
1358
  !hostFieldId ||
1214
1359
  !isBridgePatchableTextField(field, isRichText)) {
1360
+ console.debug("[Parhelia] text completion skipped: not patchable", JSON.stringify({
1361
+ elementKey,
1362
+ hostFieldId,
1363
+ fieldType: field.type,
1364
+ isRichText,
1365
+ }));
1215
1366
  clearBridgeTextCompletion();
1216
1367
  return;
1217
1368
  }
1218
- const selectedText = selectionMatches && typeof bridgeSelection?.text === "string"
1219
- ? bridgeSelection.text
1220
- : typeof interaction.selectedText === "string"
1221
- ? interaction.selectedText
1222
- : "";
1223
- const selectionStartOffset = selectionMatches && typeof bridgeSelection?.startOffset === "number"
1224
- ? bridgeSelection.startOffset
1225
- : typeof interaction.selectionStartOffset === "number"
1226
- ? interaction.selectionStartOffset
1227
- : undefined;
1228
- const selectionEndOffset = selectionMatches && typeof bridgeSelection?.endOffset === "number"
1229
- ? bridgeSelection.endOffset
1230
- : typeof interaction.selectionEndOffset === "number"
1231
- ? interaction.selectionEndOffset
1232
- : undefined;
1233
1369
  const liveSelectionStartOffset = typeof interaction.selectionStartOffset === "number"
1234
1370
  ? interaction.selectionStartOffset
1235
1371
  : undefined;
@@ -1242,6 +1378,19 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1242
1378
  const bridgeSelectionEndOffset = selectionMatches && typeof bridgeSelection?.endOffset === "number"
1243
1379
  ? bridgeSelection?.endOffset
1244
1380
  : undefined;
1381
+ const selectedText = typeof interaction.selectedText === "string"
1382
+ ? interaction.selectedText
1383
+ : selectionMatches && typeof bridgeSelection?.text === "string"
1384
+ ? bridgeSelection.text
1385
+ : "";
1386
+ const selectionStartOffset = liveSelectionStartOffset ??
1387
+ (selectionMatches && typeof bridgeSelection?.startOffset === "number"
1388
+ ? bridgeSelection.startOffset
1389
+ : undefined);
1390
+ const selectionEndOffset = liveSelectionEndOffset ??
1391
+ (selectionMatches && typeof bridgeSelection?.endOffset === "number"
1392
+ ? bridgeSelection.endOffset
1393
+ : undefined);
1245
1394
  const activeInlineEdit = activeBridgeInlineEditRef.current;
1246
1395
  const activeInlineEditMatches = !!(activeInlineEdit &&
1247
1396
  activeInlineEdit.elementKey === elementKey &&
@@ -1267,7 +1416,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1267
1416
  !hasLiveInteractionOffsets ||
1268
1417
  (bridgeSelectionStartOffset === liveSelectionStartOffset &&
1269
1418
  bridgeSelectionEndOffset === liveSelectionEndOffset);
1270
- const metadataMatchesLiveOffsets = !selectionSnapshotMatchesLiveOffsets ||
1419
+ const metadataMatchesLiveOffsets = selectionSnapshotMatchesLiveOffsets ||
1271
1420
  ((contextBefore?.length ?? 0) === interaction.selectionStartOffset &&
1272
1421
  (contextBefore?.length ?? 0) + selectedText.length ===
1273
1422
  interaction.selectionEndOffset);
@@ -1378,6 +1527,12 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1378
1527
  return;
1379
1528
  }
1380
1529
  }
1530
+ // Escape leaves insert mode, same as in the editor window itself
1531
+ if (interaction.key === "Escape" &&
1532
+ editContextRef.current?.insertMode) {
1533
+ editContextRef.current.setInsertMode(false);
1534
+ return;
1535
+ }
1381
1536
  if (isBridgeTextCompletionApplyShortcut(interaction)) {
1382
1537
  if (bridgeTextCompletionRef.current) {
1383
1538
  applyBridgeTextCompletion();
@@ -1517,9 +1672,11 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1517
1672
  if (!editor)
1518
1673
  return;
1519
1674
  const selectFromBridgeInteraction = (ids) => {
1520
- if (!bridgeComponentSelectionsMatch(editor.selection, ids)) {
1675
+ const unchanged = bridgeComponentSelectionsMatch(editor.getSelection(), ids);
1676
+ if (!unchanged) {
1521
1677
  suppressNextSelectionScrollRef.current = true;
1522
1678
  }
1679
+ perfTraceStep("frame:select-dispatched", { ids, unchanged });
1523
1680
  editor.select(ids);
1524
1681
  };
1525
1682
  let effectiveInteraction = interaction;
@@ -1581,7 +1738,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1581
1738
  selectFromBridgeInteraction([]);
1582
1739
  if (isEditableFieldClick) {
1583
1740
  clearSelectedRangeForEditableClick();
1584
- await beginTrackedBridgeInlineEdit(effectiveInteraction);
1741
+ beginTrackedBridgeInlineEditAfterSelection(effectiveInteraction);
1585
1742
  }
1586
1743
  else if (interaction.kind === "click") {
1587
1744
  closeInlineAiIfOpen();
@@ -1589,7 +1746,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1589
1746
  }
1590
1747
  return;
1591
1748
  }
1592
- const currentSelection = editor.selection || [];
1749
+ const currentSelection = editor.getSelection();
1593
1750
  if (interaction.kind === "contextMenu") {
1594
1751
  if (pageViewContextRef.current) {
1595
1752
  pageViewContextRef.current.bridgeInteraction = effectiveInteraction;
@@ -1719,7 +1876,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1719
1876
  selectFromBridgeInteraction([componentId]);
1720
1877
  if (isEditableFieldClick) {
1721
1878
  clearSelectedRangeForEditableClick();
1722
- await beginTrackedBridgeInlineEdit(effectiveInteraction);
1879
+ beginTrackedBridgeInlineEditAfterSelection(effectiveInteraction);
1723
1880
  }
1724
1881
  else {
1725
1882
  closeInlineAiIfOpen();
@@ -1728,6 +1885,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1728
1885
  }, [
1729
1886
  applyBridgeTextCompletion,
1730
1887
  beginTrackedBridgeInlineEdit,
1888
+ beginTrackedBridgeInlineEditAfterSelection,
1731
1889
  clearBridgeTextCompletion,
1732
1890
  endTrackedBridgeFieldFocus,
1733
1891
  requestBridgeTextCompletion,
@@ -1849,9 +2007,12 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1849
2007
  suggestionsCursorActive: !activeBridgeInlineEditRef.current &&
1850
2008
  (editContextRef.current?.mode === "suggestions" ||
1851
2009
  !!editContextRef.current?.showSuggestedEdits),
1852
- selectedComponentIds: editContextRef.current?.selection ?? [],
2010
+ selectedComponentIds: editContextRef.current?.getSelection() ?? [],
1853
2011
  featureFlags: {
1854
2012
  crossOriginBridge: true,
2013
+ // Lets the host emit its own perf logs + interaction perf
2014
+ // timestamps without needing a flag in the iframe's origin.
2015
+ perfTrace: isPerfTraceEnabled(),
1855
2016
  },
1856
2017
  version: bridge.getDiagnostics().editorVersion,
1857
2018
  acknowledgedCapabilities: bridge.getAcknowledgedCapabilities(),
@@ -1864,12 +2025,31 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1864
2025
  }
1865
2026
  break;
1866
2027
  case "structureUpdated":
2028
+ if (isPerfTraceActive()) {
2029
+ perfTraceStep("frame:structure-updated-received", {
2030
+ components: event.payload.structure.components.length,
2031
+ fields: event.payload.structure.fields.length,
2032
+ transportMs: Date.now() - event.timestamp,
2033
+ });
2034
+ }
1867
2035
  pageViewContext.setBridgeStructure(event.payload.structure);
2036
+ if (awaitingHostAppRefreshRenderRef.current) {
2037
+ // First structure change after a host-app refresh: the page's
2038
+ // own re-render has reached the DOM — the refresh is now truly
2039
+ // complete.
2040
+ completeCanvasRefresh();
2041
+ }
1868
2042
  break;
1869
2043
  case "pageSkeletonUpdated":
1870
2044
  pageViewContext.setPageSkeleton(event.payload.pageSkeleton);
1871
2045
  break;
1872
2046
  case "geometryUpdated": {
2047
+ if (isPerfTraceActive()) {
2048
+ perfTraceStep("frame:geometry-updated-received", {
2049
+ targets: event.payload.geometry.targets.length,
2050
+ transportMs: Date.now() - event.timestamp,
2051
+ });
2052
+ }
1873
2053
  latestBridgeScrollRef.current = event.payload.geometry.scroll;
1874
2054
  pageViewContext.bridgeGeometry = event.payload.geometry;
1875
2055
  pageViewContext.setBridgeGeometry(event.payload.geometry);
@@ -1893,11 +2073,22 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1893
2073
  window.dispatchEvent(new CustomEvent(BRIDGE_DOM_UPDATED_EVENT));
1894
2074
  break;
1895
2075
  case "refreshStarted":
2076
+ logFieldValueDiagnostic("bridge-frame", "refresh started", {
2077
+ revision: event.payload.revision,
2078
+ activeInlineEdit: activeBridgeInlineEditRef.current,
2079
+ patchSignatureCount: bridgePatchSignatureRef.current.size,
2080
+ });
1896
2081
  clearBridgeTextCompletion();
1897
2082
  clearBridgePatchSignatures();
1898
2083
  clearActiveBridgeInlineEdit();
1899
2084
  break;
1900
2085
  case "refreshCompleted":
2086
+ logFieldValueDiagnostic("bridge-frame", "refresh completed", {
2087
+ activeInlineEdit: activeBridgeInlineEditRef.current,
2088
+ hasStructure: !!event.payload.structure,
2089
+ hasGeometry: !!event.payload.geometry,
2090
+ structureFieldCount: event.payload.structure?.fields.length,
2091
+ });
1901
2092
  // Runtime refresh replaces host DOM without a new bridge ready event.
1902
2093
  // Field patches sent during the refresh window may have been cached
1903
2094
  // against the previous DOM generation, so force the refreshed host to
@@ -1934,6 +2125,18 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1934
2125
  };
1935
2126
  restorePendingRefreshScroll(iframe, targetScroll, observedScroll);
1936
2127
  }
2128
+ if (event.payload.applied === "host-app") {
2129
+ // The page's own app (e.g. a Next.js rendering host) accepted
2130
+ // the refresh, but its re-render lands later as ordinary DOM
2131
+ // mutations. Completing now would reopen viewport reactions
2132
+ // (selection auto-scroll, scrollIntoView, the inserting
2133
+ // indicator) while geometry still describes the pre-edit DOM —
2134
+ // wait for the mutation-driven structureUpdated instead.
2135
+ awaitingHostAppRefreshRenderRef.current = true;
2136
+ }
2137
+ else {
2138
+ completeCanvasRefresh();
2139
+ }
1937
2140
  setShowSpinner(false);
1938
2141
  break;
1939
2142
  case "selectionChanged":
@@ -1944,11 +2147,32 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1944
2147
  if (event.payload.elementKey && event.payload.fieldId) {
1945
2148
  setIsBridgeInlineEditing(true);
1946
2149
  }
2150
+ logFieldValueDiagnostic("bridge-frame", "fieldValueChanged event", {
2151
+ elementKey: event.payload.elementKey,
2152
+ fieldId: event.payload.fieldId,
2153
+ item: event.payload.item,
2154
+ isRichText: event.payload.isRichText,
2155
+ value: summarizeFieldValue(event.payload.value),
2156
+ activeInlineEdit: activeBridgeInlineEditRef.current,
2157
+ });
1947
2158
  handleBridgeFieldValueChanged(event.payload);
1948
2159
  break;
1949
2160
  case "inlineEditEnded":
2161
+ logFieldValueDiagnostic("bridge-frame", "inlineEditEnded event", {
2162
+ reason: event.payload.reason,
2163
+ elementKey: event.payload.elementKey,
2164
+ fieldId: event.payload.fieldId,
2165
+ item: event.payload.item,
2166
+ isRichText: event.payload.isRichText,
2167
+ value: summarizeFieldValue(event.payload.value),
2168
+ activeInlineEdit: activeBridgeInlineEditRef.current,
2169
+ });
1950
2170
  handleBridgeInlineEditEnded(event.payload);
1951
2171
  clearActiveBridgeInlineEdit();
2172
+ logFieldValueDiagnostic("bridge-frame", "active inline edit cleared after end", {
2173
+ fieldId: event.payload.fieldId,
2174
+ reason: event.payload.reason,
2175
+ });
1952
2176
  break;
1953
2177
  case "interaction":
1954
2178
  if (event.payload.kind === "hover") {
@@ -1960,6 +2184,24 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
1960
2184
  else {
1961
2185
  lastBridgeHoverInteractionRef.current = undefined;
1962
2186
  }
2187
+ if (isPerfTraceEnabled() &&
2188
+ (event.payload.kind === "click" ||
2189
+ event.payload.kind === "doubleClick" ||
2190
+ event.payload.kind === "contextMenu")) {
2191
+ // Backdate to the host's DOM-event timestamp when available so
2192
+ // host-side dispatch (hit-test + deferred-click debounce) is
2193
+ // part of the waterfall; the envelope timestamp is when the
2194
+ // host posted the message.
2195
+ const eventAt = event.payload.perf?.eventAt;
2196
+ beginPerfTrace(`select-${event.payload.kind}`, {
2197
+ componentId: event.payload.componentId,
2198
+ fieldId: event.payload.fieldId,
2199
+ hostDispatchMs: eventAt !== undefined
2200
+ ? event.timestamp - eventAt
2201
+ : "n/a (host without perf tracing)",
2202
+ transportMs: Date.now() - event.timestamp,
2203
+ }, eventAt ?? event.timestamp);
2204
+ }
1963
2205
  pageViewContext.setBridgeInteraction(event.payload);
1964
2206
  void handleBridgeInteraction(event.payload, iframe);
1965
2207
  break;
@@ -2032,6 +2274,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2032
2274
  scheduleBridgeGeometryRevision,
2033
2275
  shouldTrackMinimapScroll,
2034
2276
  restorePendingRefreshScroll,
2277
+ completeCanvasRefresh,
2035
2278
  ]);
2036
2279
  useEffect(() => {
2037
2280
  const repository = editContext.itemsRepository;
@@ -2245,7 +2488,11 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2245
2488
  };
2246
2489
  pageViewContext.requestBridgeGeometry = requestBridgeGeometry;
2247
2490
  const requestBridgeScrollBy = (payload) => {
2248
- return bridgeClientRef.current?.sendCommand("scrollBy", payload) ?? false;
2491
+ const accepted = bridgeClientRef.current?.sendCommand("scrollBy", payload) ?? false;
2492
+ if (accepted) {
2493
+ lastExplicitScrollCommandAtRef.current = Date.now();
2494
+ }
2495
+ return accepted;
2249
2496
  };
2250
2497
  const requestBridgeRichTextCommand = (payload) => {
2251
2498
  return (bridgeClientRef.current?.sendCommand("applyRichTextCommand", payload) ??
@@ -2331,24 +2578,24 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2331
2578
  const bridgeDom = pageViewContextRef.current?.bridgeDom;
2332
2579
  const contentHeight = bridgeDom?.scrollHeight ??
2333
2580
  getBridgeGeometryDocumentSize(bridgeGeometry)?.height;
2581
+ const contentWidth = bridgeDom?.viewport.width ?? bridgeGeometry?.viewport.width;
2334
2582
  const clientHeight = iframe.clientHeight;
2335
2583
  if (!contentHeight || !clientHeight)
2336
2584
  return;
2337
- const upperThreshold = clientHeight + 100; // show minimap if content exceeds this height
2338
2585
  // Check if minimap is enabled in settings and user controls
2339
2586
  const minimapEnabled = editContext.parheliaSettings?.showMinimap !== false &&
2340
2587
  editContext.showMinimap;
2341
2588
  if (!minimapEnabled) {
2589
+ miniMapVisibilityMemoryRef.current = {};
2342
2590
  if (showMiniMap) {
2343
2591
  setShowMiniMap(false);
2344
2592
  }
2345
2593
  return;
2346
2594
  }
2347
- if (showMiniMap) {
2348
- return;
2349
- }
2350
- if (contentHeight > upperThreshold) {
2351
- setShowMiniMap(true);
2595
+ const resolution = resolveMiniMapVisibility(showMiniMap, { contentHeight, contentWidth, clientHeight }, miniMapVisibilityMemoryRef.current);
2596
+ miniMapVisibilityMemoryRef.current = resolution.memory;
2597
+ if (resolution.visible !== showMiniMap) {
2598
+ setShowMiniMap(resolution.visible);
2352
2599
  }
2353
2600
  }, 100);
2354
2601
  useEffect(() => {
@@ -2369,6 +2616,13 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2369
2616
  editContextRef.current?.setSelectedRange(undefined);
2370
2617
  }
2371
2618
  }, [editContext.mode]);
2619
+ // Version that is valid at the active preview date (based on the versions'
2620
+ // publishing restrictions). Derived outside the refresh effect so the effect
2621
+ // re-runs on the resolved version number, not on itemVersions' identity.
2622
+ const previewDateVersion = editContext.mode === "preview" && editContext.previewDate
2623
+ ? pickVersionAtDate(editContext.itemVersions, editContext.previewDate)
2624
+ ?.version
2625
+ : undefined;
2372
2626
  useEffect(() => {
2373
2627
  if (!pageItemDescriptor ||
2374
2628
  !pageViewContext.editUrl ||
@@ -2387,7 +2641,23 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2387
2641
  renderUrl.searchParams.set("parhelia_session", editContext.sessionId);
2388
2642
  }
2389
2643
  if (editContext.mode === "preview" && editContext.previewDate) {
2390
- renderUrl.searchParams.delete("sc_version");
2644
+ // Pin the version that is valid at the preview date. The render URL can
2645
+ // carry the version pin as `sc_version`, `version`, or both; leaving
2646
+ // either behind keeps the page pinned to the previously selected
2647
+ // version, so every pin present must be rewritten. Only update params
2648
+ // that exist: proxied host URLs must not gain sc_* params (they break
2649
+ // Sitecore's httpRequestBegin pipeline — see applyParheliaRenderContext).
2650
+ // sc_date stays on the URL for hosts that resolve datasources by date.
2651
+ if (previewDateVersion != null) {
2652
+ if (renderUrl.searchParams.has("sc_version"))
2653
+ renderUrl.searchParams.set("sc_version", String(previewDateVersion));
2654
+ if (renderUrl.searchParams.has("version"))
2655
+ renderUrl.searchParams.set("version", String(previewDateVersion));
2656
+ }
2657
+ else {
2658
+ renderUrl.searchParams.delete("sc_version");
2659
+ renderUrl.searchParams.delete("version");
2660
+ }
2391
2661
  renderUrl.searchParams.set("sc_date", toSitecoreDate(editContext.previewDate));
2392
2662
  }
2393
2663
  // Layout-mode marker. Only set when editing shared layout, paired with the
@@ -2406,21 +2676,44 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2406
2676
  const currentIframeOrigin = getUrlOrigin(currentIframeUrl);
2407
2677
  const renderUrlIsCrossOrigin = renderUrl.origin !== window.location.origin;
2408
2678
  const iframeOriginChanged = !!(currentIframeOrigin && currentIframeOrigin !== renderUrl.origin);
2679
+ // Proxied host URLs carry the version pin as `version` (sc_* params are
2680
+ // stripped), so consider both param names when detecting a change.
2681
+ const getUrlVersionPin = (url) => url.searchParams.get("sc_version") ?? url.searchParams.get("version");
2409
2682
  const currentVersion = currentIframeUrl
2410
- ? new URL(currentIframeUrl).searchParams.get("sc_version")
2683
+ ? getUrlVersionPin(new URL(currentIframeUrl))
2411
2684
  : null;
2412
- const newVersion = renderUrl.searchParams.get("sc_version");
2685
+ const newVersion = getUrlVersionPin(renderUrl);
2413
2686
  const versionChanged = (currentVersion !== null || newVersion !== null) &&
2414
2687
  currentVersion !== newVersion;
2415
2688
  const initialLoad = currentItemDescriptor?.id !== pageItemDescriptor.id ||
2416
2689
  currentItemDescriptor?.language !== pageItemDescriptor.language ||
2417
2690
  currentItemDescriptor?.version !== pageItemDescriptor.version;
2418
2691
  const shouldUseIframeSrcReload = initialLoad || pageViewContext.fullscreen || iframeOriginChanged;
2692
+ const signatureUrl = new URL(renderUrl.toString());
2693
+ signatureUrl.searchParams.delete("edit_rev");
2694
+ const renderSignature = `${signatureUrl.toString()}|rev:${editContext.revision}`;
2695
+ // Only skip when a live bridge confirms the last refresh actually reached
2696
+ // the host. Before the bridge is connected, re-runs must keep going: the
2697
+ // retry/fallback-reload path below is what recovers a canvas whose first
2698
+ // load raced the bridge injection.
2699
+ if (!shouldUseIframeSrcReload &&
2700
+ !versionChanged &&
2701
+ lastRenderSignatureRef.current === renderSignature &&
2702
+ bridgeClientRef.current?.supportsCapability("refresh")) {
2703
+ setCurrentItemDescriptor(pageItemDescriptor);
2704
+ return;
2705
+ }
2419
2706
  function runFallbackRefresh() {
2707
+ lastRenderSignatureRef.current = renderSignature;
2420
2708
  pageViewContext.setIframeSupportsRefresh(false);
2421
- pendingRefreshScrollRef.current ??=
2422
- getIframeWindowScroll(iframeRef.current) ??
2423
- latestBridgeScrollRef.current;
2709
+ if (!pendingRefreshScrollRef.current) {
2710
+ pendingRefreshScrollRef.current =
2711
+ getIframeWindowScroll(iframeRef.current) ??
2712
+ latestBridgeScrollRef.current;
2713
+ pendingRefreshScrollCapturedAtRef.current = Date.now();
2714
+ }
2715
+ canvasRefreshInFlightSinceRef.current = Date.now();
2716
+ awaitingHostAppRefreshRenderRef.current = false;
2424
2717
  setShowSpinner(true);
2425
2718
  console.log(initialLoad
2426
2719
  ? "Initial load - setting iframe src"
@@ -2433,9 +2726,11 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2433
2726
  setIframeSrc(renderUrl.toString());
2434
2727
  }
2435
2728
  function runBridgeRefresh() {
2729
+ lastRenderSignatureRef.current = renderSignature;
2436
2730
  pendingRefreshScrollRef.current =
2437
2731
  getIframeWindowScroll(iframeRef.current) ??
2438
2732
  latestBridgeScrollRef.current;
2733
+ pendingRefreshScrollCapturedAtRef.current = Date.now();
2439
2734
  const bridge = bridgeClientRef.current;
2440
2735
  if (!bridge?.supportsCapability("refresh")) {
2441
2736
  runFallbackRefresh();
@@ -2443,6 +2738,8 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2443
2738
  }
2444
2739
  console.log("Bridge - requesting iframe refresh");
2445
2740
  pageViewContext.setIframeSupportsRefresh(true);
2741
+ canvasRefreshInFlightSinceRef.current = Date.now();
2742
+ awaitingHostAppRefreshRenderRef.current = false;
2446
2743
  const accepted = bridge.sendCommand("refresh", {
2447
2744
  url: renderUrl.toString(),
2448
2745
  revision: editContext.revision,
@@ -2450,6 +2747,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2450
2747
  });
2451
2748
  if (!accepted) {
2452
2749
  pendingRefreshScrollRef.current = undefined;
2750
+ canvasRefreshInFlightSinceRef.current = null;
2453
2751
  pageViewContext.setIframeSupportsRefresh(false);
2454
2752
  runFallbackRefresh();
2455
2753
  }
@@ -2481,13 +2779,24 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2481
2779
  pageViewContext.fullscreen,
2482
2780
  editContext.mode,
2483
2781
  editContext.previewDate,
2782
+ previewDateVersion,
2484
2783
  editContext.sessionId,
2485
2784
  editContext.layoutMode,
2486
2785
  ]);
2487
2786
  useEffect(() => {
2488
2787
  if (fieldsContext?.focusedField) {
2489
- if (editContext.selection.length > 0 &&
2490
- fieldsContext.focusedField.item.id !== editContext.selection[0])
2788
+ // A refresh is being applied: the field's rect still describes the
2789
+ // pre-edit DOM (after a move, its old position — where the drag
2790
+ // started), so scrolling now would yank the canvas to a stale spot.
2791
+ // This fires on the post-operation auto-focus of the moved/added
2792
+ // component; a genuine user focus during a refresh loses one
2793
+ // auto-scroll, which is the lesser evil.
2794
+ if (isCanvasRefreshInFlight())
2795
+ return;
2796
+ // Event-time read: this effect is keyed on focusedField, not selection.
2797
+ const currentSelection = editContext.getSelection();
2798
+ if (currentSelection.length > 0 &&
2799
+ fieldsContext.focusedField.item.id !== currentSelection[0])
2491
2800
  return;
2492
2801
  const bounds = findBridgeFieldDocumentBounds(pageViewContextRef.current?.bridgeGeometry, fieldsContext.focusedField);
2493
2802
  const activePageViewContext = pageViewContextRef.current;
@@ -2495,7 +2804,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2495
2804
  scrollBridgeBoundsIntoView(activePageViewContext, bounds, latestBridgeScrollRef.current);
2496
2805
  }
2497
2806
  }
2498
- }, [fieldsContext?.focusedField]);
2807
+ }, [fieldsContext?.focusedField, isCanvasRefreshInFlight]);
2499
2808
  // Drive the selection-scroll suppression window from the drag lifecycle.
2500
2809
  // Declared BEFORE the selection-scroll effect so it runs first in the same
2501
2810
  // commit (when a drop changes both dragObject and selection at once).
@@ -2512,7 +2821,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2512
2821
  }
2513
2822
  }, [editContext.dragObject]);
2514
2823
  useEffect(() => {
2515
- const selectionKey = editContext.selection.join("|");
2824
+ const selectionKey = canvasSelection.join("|");
2516
2825
  const selectionChanged = selectionKey !== lastScrolledSelectionKeyRef.current;
2517
2826
  lastScrolledSelectionKeyRef.current = selectionKey;
2518
2827
  // The effect also re-runs when focusedField changes (e.g. a field blurs
@@ -2525,24 +2834,43 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2525
2834
  suppressNextSelectionScrollRef.current = false;
2526
2835
  return;
2527
2836
  }
2837
+ // Programmatic selections (the drop target's parent, the post-operation
2838
+ // focus of a moved/added component) arm this suppression at their source:
2839
+ // their targets may still measure at pre-edit geometry, and unlike user
2840
+ // clicks they carry no intent to see the component. Deterministic — no
2841
+ // time window to lose against a slow operation or frozen host.
2842
+ if (editContext.consumeNextSelectionAutoScrollSuppression?.())
2843
+ return;
2528
2844
  // Suppressed during a drag and briefly after it ends. The drag owns
2529
2845
  // scrolling, and the drop's re-selection must not jump the page away from
2530
2846
  // where the user dropped (toward stale pre-move geometry).
2531
2847
  if (Date.now() < suppressSelectionScrollUntilRef.current)
2532
2848
  return;
2533
- if (!fieldsContext?.focusedField && editContext.selection.length > 0) {
2534
- const lastSelectedComponent = getComponentById(editContext.selection[editContext.selection.length - 1], pageViewContextRef.current.page);
2849
+ // Suppressed while a refresh is applying: geometry still measures the
2850
+ // pre-refresh DOM (a just-moved component still sits at its old rect), so
2851
+ // scrolling now would target a stale position. The refresh restore owns
2852
+ // the viewport until the refreshed geometry lands.
2853
+ if (isCanvasRefreshInFlight())
2854
+ return;
2855
+ if (!fieldsContext?.focusedField && canvasSelection.length > 0) {
2856
+ const lastSelectedComponent = getComponentById(canvasSelection[canvasSelection.length - 1], pageViewContextRef.current.page);
2535
2857
  if (lastSelectedComponent) {
2536
2858
  editContext.setScrollIntoView(lastSelectedComponent.id);
2537
2859
  }
2538
2860
  }
2539
- }, [editContext.selection, fieldsContext?.focusedField]);
2861
+ }, [canvasSelection, fieldsContext?.focusedField, isCanvasRefreshInFlight]);
2540
2862
  useEffect(() => {
2541
2863
  if (!editContext.scrollIntoView)
2542
2864
  return;
2543
2865
  const activePageViewContext = pageViewContextRef.current;
2544
2866
  if (!activePageViewContext)
2545
2867
  return;
2868
+ // Defer (without consuming the request) while a refresh is applying:
2869
+ // resolving now would measure the target against pre-refresh geometry and
2870
+ // scroll to a stale rect. The effect re-runs when the refreshed geometry
2871
+ // arrives (bridgeGeometry dependency) and resolves against fresh bounds.
2872
+ if (isCanvasRefreshInFlight())
2873
+ return;
2546
2874
  const scrollTarget = editContext.scrollIntoView;
2547
2875
  const requestState = scrollIntoViewRequestRef.current;
2548
2876
  const signature = getScrollIntoViewSignature(scrollTarget);
@@ -2577,6 +2905,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2577
2905
  editContext.scrollIntoView,
2578
2906
  pageViewContext.page,
2579
2907
  pageViewContext.bridgeGeometry,
2908
+ isCanvasRefreshInFlight,
2580
2909
  ]);
2581
2910
  useEffect(() => {
2582
2911
  const handleMessage = (message) => {
@@ -2651,10 +2980,16 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2651
2980
  };
2652
2981
  }, [iframeElement]);
2653
2982
  useEffect(() => {
2654
- bridgeClientRef.current?.sendCommand("setSelection", {
2655
- componentIds: editContext.selection,
2983
+ const sent = bridgeClientRef.current?.sendCommand("setSelection", {
2984
+ componentIds: canvasSelection,
2656
2985
  });
2657
- }, [editContext.selection]);
2986
+ if (isPerfTraceActive()) {
2987
+ perfTraceStep("frame:setSelection-command-sent", {
2988
+ ids: canvasSelection,
2989
+ sent: sent ?? false,
2990
+ });
2991
+ }
2992
+ }, [canvasSelection]);
2658
2993
  useEffect(() => {
2659
2994
  const bridge = bridgeClientRef.current;
2660
2995
  bridge?.sendCommand("setPreviewMode", {
@@ -2787,6 +3122,7 @@ function PageViewerFrameContent({ compareView, pageViewContext, editContext, cla
2787
3122
  if (pendingRefreshScrollRef.current) {
2788
3123
  restorePendingRefreshScroll(iframeRef.current, pendingRefreshScrollRef.current);
2789
3124
  }
3125
+ completeCanvasRefresh();
2790
3126
  }
2791
3127
  } }), 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
3128
  }