@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,15 +1,16 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import React, { useState, useEffect, useRef, useCallback, useMemo, startTransition, } from "react";
3
+ import React, { useState, useEffect, useLayoutEffect, useRef, useCallback, useMemo, startTransition, } from "react";
4
4
  import { toast } from "sonner";
5
- import { EditContextProvider, FieldsEditContextProvider, OperationsContextProvider, useEditContext, } from "./editContext";
5
+ import { isPerfTraceActive, perfTracePaintStep, perfTraceStep, } from "../perfTrace";
6
+ import { EditContextProvider, FieldsEditContextProvider, OperationsContextProvider, SelectionContextProvider, useEditContext, } from "./editContext";
6
7
  import { aggregateModifiedFieldsFromStores, areModifiedFieldSnapshotsEqual, FieldModificationStore, } from "./fieldModificationStore";
7
8
  import { SplashScreenAgentProvider } from "../../splash-screen/SplashScreenAgentContext";
8
9
  import { useRouter, useSearchParams, usePathname } from "./navigation";
9
10
  import { findComponent, getComponentById } from "../componentTreeHelper";
10
11
  import { getOperationsContext } from "./operations";
11
12
  import { handleErrorResult } from "./helpers";
12
- import { showExpandableErrorToast, } from "./errorToast";
13
+ import { showExpandableErrorToast } from "./errorToast";
13
14
  import { isManualShowMeFocusTransfer, isManualShowMeTarget, } from "./manualShowMeFocus";
14
15
  import { executeFieldAction as executeFieldServerAction, connectSocket, getEditHistory, getRunningOperations, reconnectSession, releaseFieldLocks, validateItems, } from "../services/editService";
15
16
  import { emitAgentDocumentsChanged } from "../services/agentService";
@@ -37,6 +38,7 @@ import { post } from "../services/serviceHelper";
37
38
  import { CONTENT_TREE_SIDEBAR_ID } from "../sidebar/contentTreeConstants";
38
39
  import { PageViewerFrame } from "../page-viewer/PageViewerFrame";
39
40
  import { useItemsRepository } from "./itemsRepository";
41
+ import { logFieldValueDiagnostic, summarizeFieldDescriptor, } from "../fieldValueDiagnostics";
40
42
  import { Spinner } from "../ui/Spinner";
41
43
  import { createSettleRegistry } from "../ui/animationSettle";
42
44
  import { cleanId } from "../utils/id-helper";
@@ -47,7 +49,7 @@ import { Tour } from "../../tour/Tour";
47
49
  import { GuidanceOverlay } from "../ai/GuidanceOverlay";
48
50
  import { AgentDialogHandler } from "../ai/dialogs";
49
51
  import { setActiveAgentConversation } from "../ai/useActiveAgentConversation";
50
- import { usePageViewContext, } from "../page-viewer/pageViewContext";
52
+ import { CANVAS_REFRESH_COMPLETED_EVENT, usePageViewContext, } from "../page-viewer/pageViewContext";
51
53
  import { QuickItemSwitcher } from "../QuickItemSwitcher";
52
54
  import { useEditorSlotContext, } from "../views/editorSlotContext";
53
55
  import { getComments, getAvailableCommentTags, } from "../services/reviewsService";
@@ -176,6 +178,10 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
176
178
  const parheliaSettingsRefreshInFlightRef = useRef(null);
177
179
  const minimapEnabledInSettings = effectiveParheliaSettings?.showMinimap !== false;
178
180
  const [selection, setSelection] = useState([]);
181
+ // Current selection for event-time reads (select() early-bail, getSelection)
182
+ // without making callers depend on the reactive value.
183
+ const selectionRef = useRef(selection);
184
+ selectionRef.current = selection;
179
185
  const [contentTreeSelection, setContentTreeSelection] = useState([]);
180
186
  const [selectedForInsertion, setSelectedForInsertion] = useState("");
181
187
  const [refreshCompletedFlag, setRefreshCompletedFlag] = useState(false);
@@ -230,6 +236,51 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
230
236
  const [inserting, setInserting] = useState();
231
237
  const [removingComponentIds, setRemovingComponentIds] = useState([]);
232
238
  const insertingSequenceRef = useRef(0);
239
+ // Armed by selectWithoutAutoScroll for programmatic selections (drop
240
+ // target's parent, post-operation focus): the canvas selection-scroll
241
+ // effect consumes it and skips exactly that change. Deterministic — unlike
242
+ // a time window, it cannot expire while a slow operation or a frozen host
243
+ // delays the effect flush.
244
+ const suppressNextSelectionAutoScrollRef = useRef(false);
245
+ const selectWithoutAutoScroll = useCallback((ids) => {
246
+ const current = selectionRef.current;
247
+ if (ids.length === current.length &&
248
+ ids.every((id, index) => id === current[index])) {
249
+ // No state change: nothing renders, so don't leave the suppression
250
+ // armed for an unrelated later selection.
251
+ return;
252
+ }
253
+ suppressNextSelectionAutoScrollRef.current = true;
254
+ setSelection(ids);
255
+ }, []);
256
+ const consumeNextSelectionAutoScrollSuppression = useCallback(() => {
257
+ const suppressed = suppressNextSelectionAutoScrollRef.current;
258
+ suppressNextSelectionAutoScrollRef.current = false;
259
+ return suppressed;
260
+ }, []);
261
+ const insertingHoldCleanupRef = useRef(null);
262
+ // The drop operation's HTTP resolve only means the server accepted it — the
263
+ // canvas re-render (fetch + DOM patch) lands seconds later. Keep the
264
+ // inserting indicator up until the first canvas refresh after the operation
265
+ // completes, so it visually hands off to the component actually appearing,
266
+ // with a timeout backstop for hosts that never report a refresh.
267
+ const holdInsertingUntilCanvasRefresh = useCallback((sequence) => {
268
+ insertingHoldCleanupRef.current?.();
269
+ const finish = () => {
270
+ insertingHoldCleanupRef.current = null;
271
+ window.removeEventListener(CANVAS_REFRESH_COMPLETED_EVENT, finish);
272
+ window.clearTimeout(timeoutId);
273
+ if (insertingSequenceRef.current === sequence) {
274
+ setInserting(undefined);
275
+ }
276
+ };
277
+ const timeoutId = window.setTimeout(finish, 15000);
278
+ window.addEventListener(CANVAS_REFRESH_COMPLETED_EVENT, finish);
279
+ insertingHoldCleanupRef.current = finish;
280
+ }, []);
281
+ useEffect(() => () => {
282
+ insertingHoldCleanupRef.current?.();
283
+ }, []);
233
284
  const [showFullscreenHint, setShowFullscreenHint] = useState(false);
234
285
  // Fullscreen is a shell-level UI mode. Keep it in EditorShell state so the shell can rerender
235
286
  // immediately when toggled, while still syncing the active slot's PageViewContext.fullscreen.
@@ -873,6 +924,15 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
873
924
  setSelectedHelpSectionId(helpParam === "contents" || helpParam === "true" ? null : helpParam);
874
925
  }
875
926
  }, [searchParams, isInitialLoad]);
927
+ // Perf probe: the gap between shell:setSelection-state-update and
928
+ // shell:selection-render-committed is the React render of the entire shell
929
+ // tree (every editContext consumer); the paint step adds compositor time.
930
+ useLayoutEffect(() => {
931
+ if (!isPerfTraceActive())
932
+ return;
933
+ perfTraceStep("shell:selection-render-committed", { ids: selection });
934
+ perfTracePaintStep("shell:selection-painted");
935
+ }, [selection]);
876
936
  useEffect(() => {
877
937
  if (focusedField &&
878
938
  selection.length > 0 &&
@@ -3054,6 +3114,7 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
3054
3114
  configuration,
3055
3115
  selection,
3056
3116
  setSelection,
3117
+ selectWithoutAutoScroll,
3057
3118
  loadItem,
3058
3119
  addToEditHistory,
3059
3120
  setLockedField,
@@ -4030,6 +4091,13 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4030
4091
  }, [operationsContext.cleanup]);
4031
4092
  // Shared focused field setter with lock handling (used in both contexts)
4032
4093
  const setFocusedFieldWithLock = useCallback(async (field, requestLock) => {
4094
+ logFieldValueDiagnostic("focused-field", "setFocusedField requested", {
4095
+ field: summarizeFieldDescriptor(field),
4096
+ requestLock,
4097
+ ignoreBlur,
4098
+ currentFocusedField: summarizeFieldDescriptor(focusedFieldRef.current),
4099
+ lockedField: summarizeFieldDescriptor(lockedField),
4100
+ });
4033
4101
  if (field) {
4034
4102
  setIgnoreBlur(true);
4035
4103
  setTimeout(() => {
@@ -4038,12 +4106,25 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4038
4106
  if (requestLock) {
4039
4107
  const lockResult = (await operations.ensureLock(field)) || false;
4040
4108
  if (!lockResult) {
4109
+ logFieldValueDiagnostic("focused-field", "lock failed", {
4110
+ field: summarizeFieldDescriptor(field),
4111
+ requestLock,
4112
+ });
4041
4113
  return false;
4042
4114
  }
4043
4115
  setFocusedField({ ...field });
4116
+ logFieldValueDiagnostic("focused-field", "focused field set", {
4117
+ field: summarizeFieldDescriptor(field),
4118
+ requestLock,
4119
+ lockResult,
4120
+ });
4044
4121
  return true;
4045
4122
  }
4046
4123
  setFocusedField({ ...field });
4124
+ logFieldValueDiagnostic("focused-field", "focused field set", {
4125
+ field: summarizeFieldDescriptor(field),
4126
+ requestLock,
4127
+ });
4047
4128
  }
4048
4129
  else {
4049
4130
  // Keep the field selected when the blur was caused by clicking a User
@@ -4060,13 +4141,25 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4060
4141
  now: Date.now(),
4061
4142
  });
4062
4143
  if (!ignoreBlur && !focusMovedToShowMe) {
4144
+ logFieldValueDiagnostic("focused-field", "clearing focused field", {
4145
+ previousFocusedField: summarizeFieldDescriptor(focusedFieldRef.current),
4146
+ lockedField: summarizeFieldDescriptor(lockedField),
4147
+ sessionId,
4148
+ });
4063
4149
  setFocusedField(undefined);
4064
4150
  setLockedField(undefined); // Clear client-side lock state
4065
4151
  releaseFieldLocks(sessionId);
4066
4152
  }
4153
+ else {
4154
+ logFieldValueDiagnostic("focused-field", "clear skipped", {
4155
+ reason: ignoreBlur ? "ignoreBlur" : "manual show-me focus transfer",
4156
+ previousFocusedField: summarizeFieldDescriptor(focusedFieldRef.current),
4157
+ focusMovedToShowMe,
4158
+ });
4159
+ }
4067
4160
  }
4068
4161
  return true;
4069
- }, [operations, ignoreBlur, sessionId]);
4162
+ }, [operations, ignoreBlur, sessionId, lockedField]);
4070
4163
  const quickSwitcherEntries = useMemo(() => {
4071
4164
  const entries = [];
4072
4165
  const seen = new Set();
@@ -4552,10 +4645,21 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4552
4645
  loadItemVersions,
4553
4646
  sessionId: sessionId,
4554
4647
  readonly: isReadOnly,
4555
- selection,
4648
+ getSelection: () => selectionRef.current,
4556
4649
  select: (ids) => {
4650
+ // Re-selecting the same components is a no-op: without this bail a
4651
+ // fresh array identity re-renders every selection consumer.
4652
+ const current = selectionRef.current;
4653
+ if (ids.length === current.length &&
4654
+ ids.every((id, index) => id === current[index])) {
4655
+ perfTraceStep("shell:setSelection-bailed (unchanged)", { ids });
4656
+ return;
4657
+ }
4658
+ perfTraceStep("shell:setSelection-state-update", { ids });
4557
4659
  setSelection(ids);
4558
4660
  },
4661
+ selectWithoutAutoScroll,
4662
+ consumeNextSelectionAutoScrollSuppression,
4559
4663
  contentTreeSelection,
4560
4664
  setContentTreeSelection,
4561
4665
  selectedForInsertion,
@@ -4597,6 +4701,10 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4597
4701
  return await getComponentCommands(components, editContext, providerContext);
4598
4702
  },
4599
4703
  dragStart: (dragObject) => {
4704
+ // A drag supersedes any unresolved scroll request (e.g. a comment
4705
+ // target still waiting for geometry). Left pending, it would replay
4706
+ // against the post-drop refresh and yank the canvas to a stale spot.
4707
+ setScrollIntoView(undefined);
4600
4708
  setDragObject(dragObject);
4601
4709
  },
4602
4710
  dragEnd: () => {
@@ -4611,8 +4719,11 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4611
4719
  const parentId = placeholderKeyComponents.length === 1
4612
4720
  ? undefined
4613
4721
  : placeholderKeyComponents[1];
4722
+ // Without auto-scroll: the parent is selected for context, not to be
4723
+ // shown — scrolling would move the page away from the drop point
4724
+ // (and, before the refresh lands, toward stale pre-move geometry).
4614
4725
  if (parentId)
4615
- setSelection([parentId]);
4726
+ selectWithoutAutoScroll([parentId]);
4616
4727
  let op;
4617
4728
  if (!currentItemDescriptorRef.current)
4618
4729
  return;
@@ -4689,16 +4800,29 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
4689
4800
  }
4690
4801
  return;
4691
4802
  }
4803
+ let operationFailed = false;
4692
4804
  try {
4693
- await operationPromise;
4805
+ // executeOp resolves (instead of throwing) with a failed status when
4806
+ // the server rejects the operation — no refresh follows then.
4807
+ const result = (await operationPromise);
4808
+ operationFailed = result?.executionStatus === "failed";
4694
4809
  }
4695
4810
  catch (err) {
4811
+ operationFailed = true;
4696
4812
  console.error("Failed to drop component in placeholder", err);
4697
4813
  }
4698
4814
  finally {
4699
4815
  if (insertionSequence !== undefined &&
4700
4816
  insertingSequenceRef.current === insertionSequence) {
4701
- setInserting(undefined);
4817
+ if (operationFailed) {
4818
+ setInserting(undefined);
4819
+ }
4820
+ else {
4821
+ // The server accepted the drop; the canvas has not repainted
4822
+ // yet. Hold the indicator until the refresh lands so it stops
4823
+ // when the component actually appears, not seconds earlier.
4824
+ holdInsertingUntilCanvasRefresh(insertionSequence);
4825
+ }
4702
4826
  }
4703
4827
  }
4704
4828
  },
@@ -5200,7 +5324,6 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
5200
5324
  loadItemVersions,
5201
5325
  sessionId,
5202
5326
  isReadOnly,
5203
- selection,
5204
5327
  contentTreeSelection,
5205
5328
  selectedForInsertion,
5206
5329
  dragObject,
@@ -5348,7 +5471,13 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
5348
5471
  // This ensures editContextRef.current always has the latest value when executeCommand is called
5349
5472
  // Using useEffect would create a timing window where the ref is stale
5350
5473
  editContextRef.current = editContext;
5351
- const fieldsEditContext = {
5474
+ // Selection is deliberately NOT part of editContext (see SelectionContext):
5475
+ // a canvas click would otherwise rebuild the context and re-render every
5476
+ // consumer. Only SelectionContext subscribers re-render on selection change.
5477
+ const selectionContextValue = useMemo(() => ({ selection }), [selection]);
5478
+ // Memoized: a fresh identity here re-renders every FieldsEditContext
5479
+ // consumer on every shell render (same churn class as SelectionContext).
5480
+ const fieldsEditContext = useMemo(() => ({
5352
5481
  modifiedFields,
5353
5482
  recentEdits,
5354
5483
  clearRecentEdits: () => {
@@ -5361,7 +5490,14 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
5361
5490
  setLockedField(undefined); // Clear client-side lock state
5362
5491
  await releaseFieldLocks(sessionId);
5363
5492
  },
5364
- };
5493
+ }), [
5494
+ modifiedFields,
5495
+ recentEdits,
5496
+ addRecentEdit,
5497
+ focusedField,
5498
+ setFocusedFieldWithLock,
5499
+ sessionId,
5500
+ ]);
5365
5501
  // Debug: Track editContext changes
5366
5502
  // const prevDependencies = useRef<any[]>([]);
5367
5503
  // useEffect(() => {
@@ -5615,13 +5751,13 @@ export function EditorShell({ configuration, className, item: loadItemDescriptor
5615
5751
  window.history.replaceState(getCurrentHistoryState(), "", newUrl);
5616
5752
  }, configuration: configuration, restoredFromUrl: tourRestoredRef.current })), _jsx(FeatureGate, { feature: LicenseFeatures.AI, children: _jsx(GuidanceOverlay, {}) }), _jsx(FeatureGate, { feature: LicenseFeatures.AI, children: _jsx(AgentDialogHandler, {}) })] }));
5617
5753
  const showRefreshSpinner = editContext.isRefreshing && !activePageViewContext.iframeSupportsRefresh;
5618
- return (_jsx(LicenseProvider, { initialLicenseStatus: initialLicenseStatus, initialStatusLoaded: initialLicenseStatusLoaded, children: _jsx("div", { className: `editor h-full w-full`, children: _jsx(OperationsContextProvider, { value: operationsContext.context, children: _jsx(FieldsEditContextProvider, { value: fieldsEditContext, children: _jsx(EditContextProvider, { value: editContext, children: _jsxs(SplashScreenAgentProvider, { children: [_jsx(DevModeIndicator, {}), startupChecks.state === "loading" && _jsx(EditorLoadingOverlay, {}), showRefreshSpinner && (_jsx("div", { className: "text-neutral-grey-50 pointer-events-none fixed right-0 bottom-0 flex h-24 w-24 items-center justify-center opacity-50 select-none", children: _jsx(Spinner, {}) })), (currentWorkspace.id === "agents" ||
5619
- currentWorkspace.id === "taskboard" ||
5620
- currentWorkspace.id === "translation-management") &&
5621
- showAgentsWorkspaceEditor && (_jsx(AgentsSlotContextBridgeHost, { slots: editorSlots })), startupChecks.state !== "loading" && (children || editorUi), startupChecks.state !== "loading" && dialog, _jsx(Toaster, { position: "top-center" }), " ", _jsx(ConfirmationDialog, { ref: confirmationDialogRef }), _jsx(ConcurrentUserLimitDialog, { open: concurrentUserLimitError !== null, onOpenChange: (open) => {
5622
- if (!open) {
5623
- setConcurrentUserLimitError(null);
5624
- }
5625
- }, sessionId: sessionId, currentUsers: concurrentUserLimitError?.currentUsers ?? 0, maxUsers: concurrentUserLimitError?.maxUsers ?? 0, message: concurrentUserLimitError?.message ?? "", onRetry: handleRetryConnection, isAdministrator: userInfo.user.isAdministrator === true }), _jsx(QuickItemSwitcher, { visible: quickSwitcherVisible, entries: quickSwitcherEntries.slice(0, 5), selectedIndex: quickSwitcherSelectedIndex, onSelect: handleQuickSwitcherSelect, onClose: () => setQuickSwitcherVisible(false) }), _jsx(EditContextMenu, { ref: contextMenuRef, onOpenChange: handleContextMenuOpenChange }), _jsx(EditorFormHintPopover, {}), _jsx(FeatureGate, { feature: LicenseFeatures.AI, children: _jsx(InlineAiTrigger, {}) }), media.mediaSelectorVisible && (_jsx(MediaSelector, { language: editContext.currentItemDescriptor.language, visible: media.mediaSelectorVisible, onHide: media.handleHide, onMediaSelected: media.onMediaSelect, selectedIdPath: media.selectedMediaIdPath, mode: media.mediaSelectorMode, initialSearchTerm: media.initialSearchTerm })), _jsx(FieldEditorPopup, { ref: fieldEditorPopupRef }), _jsx(LicenseOverlay, {}), _jsx(SessionExpiredOverlay, {})] }) }) }) }) }) }));
5754
+ return (_jsx(LicenseProvider, { initialLicenseStatus: initialLicenseStatus, initialStatusLoaded: initialLicenseStatusLoaded, children: _jsx("div", { className: `editor h-full w-full`, children: _jsx(OperationsContextProvider, { value: operationsContext.context, children: _jsx(FieldsEditContextProvider, { value: fieldsEditContext, children: _jsx(EditContextProvider, { value: editContext, children: _jsx(SelectionContextProvider, { value: selectionContextValue, children: _jsxs(SplashScreenAgentProvider, { children: [_jsx(DevModeIndicator, {}), startupChecks.state === "loading" && (_jsx(EditorLoadingOverlay, {})), showRefreshSpinner && (_jsx("div", { className: "text-neutral-grey-50 pointer-events-none fixed right-0 bottom-0 flex h-24 w-24 items-center justify-center opacity-50 select-none", children: _jsx(Spinner, {}) })), (currentWorkspace.id === "agents" ||
5755
+ currentWorkspace.id === "taskboard" ||
5756
+ currentWorkspace.id === "translation-management") &&
5757
+ showAgentsWorkspaceEditor && (_jsx(AgentsSlotContextBridgeHost, { slots: editorSlots })), startupChecks.state !== "loading" && (children || editorUi), startupChecks.state !== "loading" && dialog, _jsx(Toaster, { position: "top-center" }), " ", _jsx(ConfirmationDialog, { ref: confirmationDialogRef }), _jsx(ConcurrentUserLimitDialog, { open: concurrentUserLimitError !== null, onOpenChange: (open) => {
5758
+ if (!open) {
5759
+ setConcurrentUserLimitError(null);
5760
+ }
5761
+ }, sessionId: sessionId, currentUsers: concurrentUserLimitError?.currentUsers ?? 0, maxUsers: concurrentUserLimitError?.maxUsers ?? 0, message: concurrentUserLimitError?.message ?? "", onRetry: handleRetryConnection, isAdministrator: userInfo.user.isAdministrator === true }), _jsx(QuickItemSwitcher, { visible: quickSwitcherVisible, entries: quickSwitcherEntries.slice(0, 5), selectedIndex: quickSwitcherSelectedIndex, onSelect: handleQuickSwitcherSelect, onClose: () => setQuickSwitcherVisible(false) }), _jsx(EditContextMenu, { ref: contextMenuRef, onOpenChange: handleContextMenuOpenChange }), _jsx(EditorFormHintPopover, {}), _jsx(FeatureGate, { feature: LicenseFeatures.AI, children: _jsx(InlineAiTrigger, {}) }), media.mediaSelectorVisible && (_jsx(MediaSelector, { language: editContext.currentItemDescriptor.language, visible: media.mediaSelectorVisible, onHide: media.handleHide, onMediaSelected: media.onMediaSelect, selectedIdPath: media.selectedMediaIdPath, mode: media.mediaSelectorMode, initialSearchTerm: media.initialSearchTerm })), _jsx(FieldEditorPopup, { ref: fieldEditorPopupRef }), _jsx(LicenseOverlay, {}), _jsx(SessionExpiredOverlay, {})] }) }) }) }) }) }) }));
5626
5762
  }
5627
5763
  //# sourceMappingURL=EditorShell.js.map