@parhelia/core 0.1.12881 → 0.1.12883

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 (220) hide show
  1. package/dist/agents-view/AgentsSidebar.js +1 -1
  2. package/dist/agents-view/AgentsSidebar.js.map +1 -1
  3. package/dist/agents-view/AgentsTitlebar.d.ts +1 -1
  4. package/dist/agents-view/AgentsTitlebar.js +3 -6
  5. package/dist/agents-view/AgentsTitlebar.js.map +1 -1
  6. package/dist/agents-view/AgentsView.d.ts +2 -2
  7. package/dist/agents-view/AgentsView.js +2 -2
  8. package/dist/agents-view/AgentsView.js.map +1 -1
  9. package/dist/agents-view/AgentsWorkspaceView.js +1 -12
  10. package/dist/agents-view/AgentsWorkspaceView.js.map +1 -1
  11. package/dist/agents-view/CreateAgentView.d.ts +1 -1
  12. package/dist/agents-view/CreateAgentView.js +1 -1
  13. package/dist/agents-view/DateAgentsGroup.js +12 -1
  14. package/dist/agents-view/DateAgentsGroup.js.map +1 -1
  15. package/dist/agents-view/ProfileAgentsGroup.js +16 -4
  16. package/dist/agents-view/ProfileAgentsGroup.js.map +1 -1
  17. package/dist/components/ui/checkbox.js +1 -1
  18. package/dist/components/ui/checkbox.js.map +1 -1
  19. package/dist/components/ui/context-menu.d.ts +2 -1
  20. package/dist/components/ui/context-menu.js +4 -1
  21. package/dist/components/ui/context-menu.js.map +1 -1
  22. package/dist/components/ui/input.js +2 -2
  23. package/dist/components/ui/input.js.map +1 -1
  24. package/dist/components/ui/select.js +1 -1
  25. package/dist/components/ui/select.js.map +1 -1
  26. package/dist/components/ui/textarea.js +2 -2
  27. package/dist/components/ui/textarea.js.map +1 -1
  28. package/dist/config/config.js +92 -14
  29. package/dist/config/config.js.map +1 -1
  30. package/dist/config/types.d.ts +7 -0
  31. package/dist/config/types.js.map +1 -1
  32. package/dist/editor/ContextMenu.d.ts +1 -0
  33. package/dist/editor/ContextMenu.js +4 -4
  34. package/dist/editor/ContextMenu.js.map +1 -1
  35. package/dist/editor/FieldHistory.d.ts +2 -1
  36. package/dist/editor/FieldHistory.js +13 -12
  37. package/dist/editor/FieldHistory.js.map +1 -1
  38. package/dist/editor/FieldListField.js +4 -18
  39. package/dist/editor/FieldListField.js.map +1 -1
  40. package/dist/editor/LinkEditorDialog.d.ts +9 -2
  41. package/dist/editor/LinkEditorDialog.js +174 -70
  42. package/dist/editor/LinkEditorDialog.js.map +1 -1
  43. package/dist/editor/MainLayout.js +49 -6
  44. package/dist/editor/MainLayout.js.map +1 -1
  45. package/dist/editor/MobileLayout.js +33 -1
  46. package/dist/editor/MobileLayout.js.map +1 -1
  47. package/dist/editor/PictureCropper.js +45 -28
  48. package/dist/editor/PictureCropper.js.map +1 -1
  49. package/dist/editor/ai/AgentProfileSelector.js +7 -7
  50. package/dist/editor/ai/AgentProfileSelector.js.map +1 -1
  51. package/dist/editor/ai/Agents.js +19 -5
  52. package/dist/editor/ai/Agents.js.map +1 -1
  53. package/dist/editor/ai/InlineAiDialog.d.ts +1 -0
  54. package/dist/editor/ai/InlineAiDialog.js +239 -198
  55. package/dist/editor/ai/InlineAiDialog.js.map +1 -1
  56. package/dist/editor/ai/InlineAiTextEditTooltip.d.ts +8 -0
  57. package/dist/editor/ai/InlineAiTextEditTooltip.js +10 -0
  58. package/dist/editor/ai/InlineAiTextEditTooltip.js.map +1 -0
  59. package/dist/editor/ai/InlineAiTrigger.js +187 -20
  60. package/dist/editor/ai/InlineAiTrigger.js.map +1 -1
  61. package/dist/editor/ai/inlineAiTextEditLabels.d.ts +2 -0
  62. package/dist/editor/ai/inlineAiTextEditLabels.js +8 -0
  63. package/dist/editor/ai/inlineAiTextEditLabels.js.map +1 -0
  64. package/dist/editor/ai/prepareInlineAiTextSelection.d.ts +5 -0
  65. package/dist/editor/ai/prepareInlineAiTextSelection.js +86 -0
  66. package/dist/editor/ai/prepareInlineAiTextSelection.js.map +1 -0
  67. package/dist/editor/ai/terminal/components/AgentSettingsPopover.js +1 -1
  68. package/dist/editor/ai/terminal/components/AgentSettingsPopover.js.map +1 -1
  69. package/dist/editor/ai/terminal/components/AiResponseMessage.js +9 -9
  70. package/dist/editor/ai/terminal/components/AiResponseMessage.js.map +1 -1
  71. package/dist/editor/ai/terminal/components/ToolCallDisplay.js +3 -1
  72. package/dist/editor/ai/terminal/components/ToolCallDisplay.js.map +1 -1
  73. package/dist/editor/ai/useActiveAgentConversation.d.ts +3 -0
  74. package/dist/editor/ai/useActiveAgentConversation.js +32 -0
  75. package/dist/editor/ai/useActiveAgentConversation.js.map +1 -0
  76. package/dist/editor/ai/useInlineAiPosition.d.ts +9 -1
  77. package/dist/editor/ai/useInlineAiPosition.js +10 -19
  78. package/dist/editor/ai/useInlineAiPosition.js.map +1 -1
  79. package/dist/editor/client/EditorShell.js +23 -4
  80. package/dist/editor/client/EditorShell.js.map +1 -1
  81. package/dist/editor/client/editContext.d.ts +25 -0
  82. package/dist/editor/client/editContext.js.map +1 -1
  83. package/dist/editor/client/fieldModificationStore.d.ts +1 -0
  84. package/dist/editor/client/fieldModificationStore.js +7 -2
  85. package/dist/editor/client/fieldModificationStore.js.map +1 -1
  86. package/dist/editor/client/itemsRepository.js +3 -1
  87. package/dist/editor/client/itemsRepository.js.map +1 -1
  88. package/dist/editor/client/operations.js +26 -4
  89. package/dist/editor/client/operations.js.map +1 -1
  90. package/dist/editor/client/ui/EditorChrome.js +1 -1
  91. package/dist/editor/client/ui/EditorChrome.js.map +1 -1
  92. package/dist/editor/commands/componentCommands.d.ts +3 -1
  93. package/dist/editor/commands/componentCommands.js +8 -3
  94. package/dist/editor/commands/componentCommands.js.map +1 -1
  95. package/dist/editor/commands/handlers/uiActionHandlers.js +5 -1
  96. package/dist/editor/commands/handlers/uiActionHandlers.js.map +1 -1
  97. package/dist/editor/editor-warnings/FinalWorkflowStateReadOnly.js +5 -0
  98. package/dist/editor/editor-warnings/FinalWorkflowStateReadOnly.js.map +1 -1
  99. package/dist/editor/editor-warnings/ItemLocked.js +6 -3
  100. package/dist/editor/editor-warnings/ItemLocked.js.map +1 -1
  101. package/dist/editor/field-types/DateFieldEditor.js +1 -1
  102. package/dist/editor/field-types/DateFieldEditor.js.map +1 -1
  103. package/dist/editor/field-types/DateTimeFieldEditor.js +1 -1
  104. package/dist/editor/field-types/DateTimeFieldEditor.js.map +1 -1
  105. package/dist/editor/field-types/DropLinkEditor.js +1 -1
  106. package/dist/editor/field-types/DropLinkEditor.js.map +1 -1
  107. package/dist/editor/field-types/DropListEditor.js +1 -1
  108. package/dist/editor/field-types/DropListEditor.js.map +1 -1
  109. package/dist/editor/field-types/ImageFieldEditor.js +1 -1
  110. package/dist/editor/field-types/ImageFieldEditor.js.map +1 -1
  111. package/dist/editor/field-types/InternalLinkFieldEditor.js +1 -1
  112. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  113. package/dist/editor/field-types/LinkFieldEditor.js +15 -3
  114. package/dist/editor/field-types/LinkFieldEditor.js.map +1 -1
  115. package/dist/editor/field-types/MultiLineText.js +1 -1
  116. package/dist/editor/field-types/MultiLineText.js.map +1 -1
  117. package/dist/editor/field-types/NameValueListEditor.js +1 -1
  118. package/dist/editor/field-types/NameValueListEditor.js.map +1 -1
  119. package/dist/editor/field-types/PictureFieldEditor.js +2 -2
  120. package/dist/editor/field-types/PictureFieldEditor.js.map +1 -1
  121. package/dist/editor/field-types/RawEditor.js +1 -1
  122. package/dist/editor/field-types/RawEditor.js.map +1 -1
  123. package/dist/editor/field-types/RichTextEditorComponent.js +18 -36
  124. package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
  125. package/dist/editor/field-types/SingleLineText.js +1 -1
  126. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  127. package/dist/editor/field-types/TreeListEditor.js +1 -1
  128. package/dist/editor/field-types/TreeListEditor.js.map +1 -1
  129. package/dist/editor/field-types/richtext/bridgeRichTextProfile.d.ts +21 -0
  130. package/dist/editor/field-types/richtext/bridgeRichTextProfile.js +96 -0
  131. package/dist/editor/field-types/richtext/bridgeRichTextProfile.js.map +1 -0
  132. package/dist/editor/field-types/richtext/components/ReactSlate.css +44 -6
  133. package/dist/editor/field-types/richtext/components/ReactSlate.js +185 -36
  134. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
  135. package/dist/editor/field-types/richtext/components/SimpleRichTextEditor.css +5 -2
  136. package/dist/editor/field-types/richtext/components/SimpleToolbar.js +5 -4
  137. package/dist/editor/field-types/richtext/components/SimpleToolbar.js.map +1 -1
  138. package/dist/editor/field-types/richtext/contextMenuFactory.d.ts +2 -14
  139. package/dist/editor/field-types/richtext/contextMenuFactory.js +4 -232
  140. package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
  141. package/dist/editor/field-types/richtext/richTextToolbarIcons.d.ts +7 -0
  142. package/dist/editor/field-types/richtext/richTextToolbarIcons.js +49 -0
  143. package/dist/editor/field-types/richtext/richTextToolbarIcons.js.map +1 -0
  144. package/dist/editor/field-types/richtext/utils/conversion.js +23 -2
  145. package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -1
  146. package/dist/editor/fieldTypes.d.ts +2 -0
  147. package/dist/editor/media-selector/MediaFolderBrowser.d.ts +2 -1
  148. package/dist/editor/media-selector/MediaFolderBrowser.js +19 -9
  149. package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
  150. package/dist/editor/media-selector/TreeSelector.js +15 -9
  151. package/dist/editor/media-selector/TreeSelector.js.map +1 -1
  152. package/dist/editor/media-selector/UploadZone.d.ts +2 -1
  153. package/dist/editor/media-selector/UploadZone.js +21 -9
  154. package/dist/editor/media-selector/UploadZone.js.map +1 -1
  155. package/dist/editor/menubar/toolbar-sections/ManualBrowser.d.ts +10 -0
  156. package/dist/editor/menubar/toolbar-sections/ManualBrowser.js +607 -85
  157. package/dist/editor/menubar/toolbar-sections/ManualBrowser.js.map +1 -1
  158. package/dist/editor/menubar/toolbar-sections/UtilityControls.js +3 -1
  159. package/dist/editor/menubar/toolbar-sections/UtilityControls.js.map +1 -1
  160. package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.d.ts +3 -1
  161. package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js +357 -73
  162. package/dist/editor/page-editor-chrome/BridgeInlineFormatOverlay.js.map +1 -1
  163. package/dist/editor/page-editor-chrome/PageEditorChrome.js +1 -1
  164. package/dist/editor/page-editor-chrome/PageEditorChrome.js.map +1 -1
  165. package/dist/editor/page-editor-chrome/bridgeInlineFormatToolbarLayout.d.ts +24 -0
  166. package/dist/editor/page-editor-chrome/bridgeInlineFormatToolbarLayout.js +89 -0
  167. package/dist/editor/page-editor-chrome/bridgeInlineFormatToolbarLayout.js.map +1 -0
  168. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.d.ts +1 -1
  169. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js +7 -1
  170. package/dist/editor/page-editor-chrome/useBridgeInlineEditing.js.map +1 -1
  171. package/dist/editor/page-viewer/PageViewer.js +23 -4
  172. package/dist/editor/page-viewer/PageViewer.js.map +1 -1
  173. package/dist/editor/page-viewer/PageViewerFrame.js +157 -2
  174. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  175. package/dist/editor/reviews/Comment.js +2 -2
  176. package/dist/editor/reviews/Comment.js.map +1 -1
  177. package/dist/editor/reviews/CommentDisplayPopover.js +2 -1
  178. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  179. package/dist/editor/reviews/CommentEditor.d.ts +1 -0
  180. package/dist/editor/reviews/CommentEditor.js +3 -2
  181. package/dist/editor/reviews/CommentEditor.js.map +1 -1
  182. package/dist/editor/reviews/CommentPopover.js +2 -2
  183. package/dist/editor/reviews/CommentPopover.js.map +1 -1
  184. package/dist/editor/reviews/Comments.js +5 -4
  185. package/dist/editor/reviews/Comments.js.map +1 -1
  186. package/dist/editor/reviews/FeedbackCard.js +5 -7
  187. package/dist/editor/reviews/FeedbackCard.js.map +1 -1
  188. package/dist/editor/reviews/SuggestionCommentThread.js +3 -3
  189. package/dist/editor/reviews/SuggestionCommentThread.js.map +1 -1
  190. package/dist/editor/services/contentService.d.ts +1 -0
  191. package/dist/editor/services/contentService.js.map +1 -1
  192. package/dist/editor/sidebar/Validation.js +4 -1
  193. package/dist/editor/sidebar/Validation.js.map +1 -1
  194. package/dist/editor/template-wizard/TemplateStructureInlineEditor.js +1 -1
  195. package/dist/editor/template-wizard/TemplateStructureInlineEditor.js.map +1 -1
  196. package/dist/editor/ui/SimpleIconButton.js +8 -2
  197. package/dist/editor/ui/SimpleIconButton.js.map +1 -1
  198. package/dist/editor/ui/Splitter.d.ts +1 -0
  199. package/dist/editor/ui/Splitter.js +12 -2
  200. package/dist/editor/ui/Splitter.js.map +1 -1
  201. package/dist/editor/ui/animationSettle.d.ts +32 -0
  202. package/dist/editor/ui/animationSettle.js +85 -0
  203. package/dist/editor/ui/animationSettle.js.map +1 -0
  204. package/dist/editor/utils/expandSelectionAtCaret.d.ts +15 -0
  205. package/dist/editor/utils/expandSelectionAtCaret.js +183 -0
  206. package/dist/editor/utils/expandSelectionAtCaret.js.map +1 -0
  207. package/dist/editor/views/MediaFolderEditView.js +1 -1
  208. package/dist/editor/views/MediaFolderEditView.js.map +1 -1
  209. package/dist/revision.d.ts +2 -2
  210. package/dist/revision.js +2 -2
  211. package/dist/splash-screen/DialogWrappers.js +2 -2
  212. package/dist/splash-screen/DialogWrappers.js.map +1 -1
  213. package/dist/task-board/components/TaskDetailPanel.js +2 -1
  214. package/dist/task-board/components/TaskDetailPanel.js.map +1 -1
  215. package/dist/task-board/views/DependencyGraphView.d.ts +42 -1
  216. package/dist/task-board/views/DependencyGraphView.js +94 -0
  217. package/dist/task-board/views/DependencyGraphView.js.map +1 -1
  218. package/dist/types.d.ts +1 -0
  219. package/package.json +1 -1
  220. package/styles.css +59 -12
@@ -11,12 +11,58 @@ import { useEditContext } from "../../client/editContext";
11
11
  import { cn } from "../../../lib/utils";
12
12
  import { sanitizeSvg } from "../../../lib/sanitize";
13
13
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../../components/ui/dropdown-menu";
14
+ import { waitForSelectorAnimations } from "../../ui/animationSettle";
14
15
  /** Render a sanitized SVG string at a specific pixel size. */
15
16
  function SvgIcon({ svg, size, className, }) {
16
17
  const sanitizedSvg = sanitizeSvg(svg).trim();
17
18
  return (_jsx("span", { className: cn("inline-flex items-center justify-center [&>svg]:block [&>svg]:h-full [&>svg]:w-full", className), style: { width: size, height: size }, dangerouslySetInnerHTML: { __html: sanitizedSvg } }));
18
19
  }
19
20
  const HIGHLIGHT_DURATION_MS = 2000;
21
+ const EDITOR_FORM_SLIDER_SELECTOR = '[data-testid="editor-form-slider"]';
22
+ const EDITOR_FORM_HIDDEN_ATTRIBUTE = "aria-hidden";
23
+ const CLOSE_PANE_LABEL = "Close pane";
24
+ function waitForDelay(delayMs) {
25
+ return new Promise((resolve) => {
26
+ window.setTimeout(resolve, delayMs);
27
+ });
28
+ }
29
+ function getEditorFormSlider(elements = []) {
30
+ for (const element of elements) {
31
+ const slider = element.closest(EDITOR_FORM_SLIDER_SELECTOR);
32
+ if (slider) {
33
+ return slider;
34
+ }
35
+ }
36
+ return document.querySelector(EDITOR_FORM_SLIDER_SELECTOR);
37
+ }
38
+ function isEditorFormSliderHidden(slider) {
39
+ return slider?.getAttribute(EDITOR_FORM_HIDDEN_ATTRIBUTE) === "true";
40
+ }
41
+ async function waitForResolvedElements(resolveElements, selector, timeoutMs = 1000) {
42
+ const startedAt = Date.now();
43
+ while (Date.now() - startedAt < timeoutMs) {
44
+ const elements = resolveElements(selector);
45
+ if (elements.length > 0) {
46
+ return elements;
47
+ }
48
+ await waitForDelay(50);
49
+ }
50
+ return resolveElements(selector);
51
+ }
52
+ // Wait until at least one element for the selector is actually VISIBLE (not just
53
+ // present). After a reveal/animation a target can be in the DOM but not yet laid
54
+ // out, which would otherwise cause highlightElement to skip it.
55
+ async function waitForVisibleElements(selector, timeoutMs = 1000) {
56
+ const startedAt = Date.now();
57
+ while (Date.now() - startedAt < timeoutMs) {
58
+ const elements = getVisibleElementsForSelector(selector);
59
+ if (elements.length > 0) {
60
+ return elements;
61
+ }
62
+ await waitForDelay(50);
63
+ }
64
+ return getVisibleElementsForSelector(selector);
65
+ }
20
66
  const manualMarkdownComponents = {
21
67
  h1: ({ children }) => (_jsx("h1", { className: "text-neutral-grey-100 mt-4 mb-0 text-xl font-bold", children: children })),
22
68
  h2: ({ children }) => (_jsx("h2", { className: "text-neutral-grey-100 mt-3 mb-0 text-lg font-semibold", children: children })),
@@ -52,7 +98,15 @@ export function parseUiSelectors(uiSelectors) {
52
98
  let beforeAction;
53
99
  let afterAction;
54
100
  let availabilitySelector;
101
+ let selectorOverride;
102
+ let tab;
103
+ let closeMode;
55
104
  for (const segment of segments) {
105
+ if (segment.startsWith("close=")) {
106
+ const value = segment.substring("close=".length).trim();
107
+ closeMode = value === "auto" ? "auto" : "click";
108
+ continue;
109
+ }
56
110
  if (segment.startsWith("beforeAction=")) {
57
111
  beforeAction = segment.substring("beforeAction=".length).trim();
58
112
  continue;
@@ -67,6 +121,14 @@ export function parseUiSelectors(uiSelectors) {
67
121
  .trim();
68
122
  continue;
69
123
  }
124
+ if (segment.startsWith("selector=")) {
125
+ selectorOverride = segment.substring("selector=".length).trim();
126
+ continue;
127
+ }
128
+ if (segment.startsWith("tab=")) {
129
+ tab = segment.substring("tab=".length).trim();
130
+ continue;
131
+ }
70
132
  if (!notFoundMessage) {
71
133
  notFoundMessage = segment;
72
134
  }
@@ -80,14 +142,16 @@ export function parseUiSelectors(uiSelectors) {
80
142
  : undefined;
81
143
  const parsed = {
82
144
  name,
83
- selector: isSidebarOnly ? "" : `@${name}`, // Empty selector for sidebar-only
145
+ selector: isSidebarOnly ? "" : selectorOverride || `@${name}`,
84
146
  availabilitySelector,
147
+ tab,
85
148
  description,
86
149
  notFoundMessage: notFoundMessage || undefined,
87
150
  location: isSidebarOnly ? sidebarId : undefined, // Use actual sidebar ID (without -sidebar suffix) for sidebar-only
88
151
  isSidebarOnly,
89
152
  beforeAction,
90
153
  afterAction,
154
+ closeMode,
91
155
  };
92
156
  // Primary key remains the selector name
93
157
  map.set(name, parsed);
@@ -148,6 +212,46 @@ function getElementsForSelector(selector) {
148
212
  }
149
213
  return Array.from(document.querySelectorAll(cssSelector));
150
214
  }
215
+ // Determine whether an element is genuinely visible to the user, not merely
216
+ // present in the DOM. A target sitting inside a collapsed panel, a hidden
217
+ // sidebar (aria-hidden), or with `display:none` / `visibility:hidden` / zero
218
+ // size counts as NOT visible.
219
+ function isElementVisible(element) {
220
+ // Treat anything inside an explicitly aria-hidden container (e.g. a collapsed
221
+ // editor form slider) as not visible.
222
+ if (element.closest('[aria-hidden="true"]')) {
223
+ return false;
224
+ }
225
+ const candidate = element;
226
+ if (typeof candidate.checkVisibility === "function") {
227
+ if (!candidate.checkVisibility({
228
+ checkOpacity: true,
229
+ checkVisibilityCSS: true,
230
+ })) {
231
+ return false;
232
+ }
233
+ }
234
+ else {
235
+ // Fallback for environments without Element.checkVisibility (e.g. jsdom).
236
+ const style = window.getComputedStyle(element);
237
+ if (style.display === "none" ||
238
+ style.visibility === "hidden" ||
239
+ style.visibility === "collapse") {
240
+ return false;
241
+ }
242
+ // position:fixed elements have a null offsetParent but can still be visible.
243
+ if (element.offsetParent === null && style.position !== "fixed") {
244
+ return false;
245
+ }
246
+ }
247
+ // Reject zero-area elements (e.g. a width:0 collapsed panel).
248
+ const rect = element.getBoundingClientRect();
249
+ return rect.width > 0 && rect.height > 0;
250
+ }
251
+ // Return only the elements matching a selector that are currently visible.
252
+ function getVisibleElementsForSelector(selector) {
253
+ return getElementsForSelector(selector).filter(isElementVisible);
254
+ }
151
255
  function shouldSkipHighlightScroll(element) {
152
256
  const rect = element.getBoundingClientRect();
153
257
  const computedStyle = window.getComputedStyle(element);
@@ -199,22 +303,60 @@ async function scrollElementForHighlight(element) {
199
303
  // Highlight an element temporarily
200
304
  export async function highlightElement(selector, duration = HIGHLIGHT_DURATION_MS) {
201
305
  const cssSelector = expandSelector(selector);
202
- const elements = getElementsForSelector(selector);
306
+ // Only ever highlight elements that are actually visible. A target inside a
307
+ // collapsed panel or hidden sidebar must not receive a stray overlay.
308
+ const elements = getElementsForSelector(selector).filter(isElementVisible);
203
309
  if (elements.length === 0)
204
310
  return;
311
+ const overlayLifetimes = [];
312
+ if (cssSelector.startsWith("iframe:")) {
313
+ const iframe = document.querySelector("iframe.page-iframe");
314
+ const iframeRect = iframe?.getBoundingClientRect();
315
+ if (elements[0]) {
316
+ await scrollElementForHighlight(elements[0]);
317
+ }
318
+ elements.forEach((element) => {
319
+ const elementRect = element.getBoundingClientRect();
320
+ overlayLifetimes.push(showHighlightOverlay(elementRect.left + (iframeRect?.left ?? 0), elementRect.top + (iframeRect?.top ?? 0), elementRect.width, elementRect.height, duration));
321
+ });
322
+ await Promise.all(overlayLifetimes);
323
+ return;
324
+ }
205
325
  if (elements[0]) {
206
326
  await scrollElementForHighlight(elements[0]);
207
327
  }
208
328
  elements.forEach((element) => {
209
329
  const rect = element.getBoundingClientRect();
210
- showHighlightOverlay(rect.left, rect.top, rect.width, rect.height, duration);
330
+ overlayLifetimes.push(showHighlightOverlay(rect.left, rect.top, rect.width, rect.height, duration));
211
331
  });
332
+ await Promise.all(overlayLifetimes);
212
333
  }
213
- // Check if an element is currently available in the DOM
214
- function isElementAvailable(selector) {
334
+ // Check if an element is currently present in the DOM (regardless of visibility)
335
+ function isElementPresent(selector) {
215
336
  return getElementsForSelector(selector).length > 0;
216
337
  }
217
- // Create a temporary highlight overlay
338
+ // Check if an element matching the selector is currently visible to the user
339
+ function isElementVisibleBySelector(selector) {
340
+ return getVisibleElementsForSelector(selector).length > 0;
341
+ }
342
+ export function deriveSelectorState(params) {
343
+ if (!params.isAnchorReachable) {
344
+ return "disabled";
345
+ }
346
+ // Sidebar-only selectors have no element to "see"; opening the sidebar is the action.
347
+ if (params.isSidebarOnly) {
348
+ return "show-action";
349
+ }
350
+ if (params.isTargetVisible) {
351
+ return "show";
352
+ }
353
+ if (params.hasRevealPath) {
354
+ return "show-action";
355
+ }
356
+ return "disabled";
357
+ }
358
+ // Create a temporary highlight overlay. Resolves when its finite "life"
359
+ // animation ends and the overlay has been removed (event-driven, not a timer).
218
360
  function showHighlightOverlay(x, y, width, height, duration) {
219
361
  const padding = 4;
220
362
  const viewportWidth = window.innerWidth;
@@ -231,9 +373,11 @@ function showHighlightOverlay(x, y, width, height, duration) {
231
373
  const clampedHeight = bottom - top;
232
374
  // Skip if target is fully outside the viewport after clamping.
233
375
  if (clampedWidth <= 0 || clampedHeight <= 0)
234
- return;
376
+ return Promise.resolve();
235
377
  const overlay = document.createElement("div");
236
378
  overlay.className = "selector-highlight-overlay";
379
+ // Two animations: the infinite pulse for the visual, and a finite "life"
380
+ // animation whose animationend drives removal (single source of duration).
237
381
  overlay.style.cssText = `
238
382
  position: fixed;
239
383
  left: ${left}px;
@@ -245,7 +389,7 @@ function showHighlightOverlay(x, y, width, height, duration) {
245
389
  background: color-mix(in srgb, var(--color-component-default) 10%, transparent);
246
390
  pointer-events: none;
247
391
  z-index: 999999;
248
- animation: pulse-highlight 1s ease-in-out infinite;
392
+ animation: pulse-highlight 1s ease-in-out infinite, selector-highlight-life ${duration}ms linear forwards;
249
393
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-component-default) 20%, transparent);
250
394
  `;
251
395
  // Add animation keyframes if not already added
@@ -257,114 +401,492 @@ function showHighlightOverlay(x, y, width, height, duration) {
257
401
  0%, 100% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-component-default) 20%, transparent); }
258
402
  50% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--color-component-default) 10%, transparent); }
259
403
  }
404
+ @keyframes selector-highlight-life {
405
+ from { opacity: 1; }
406
+ to { opacity: 1; }
407
+ }
260
408
  `;
261
409
  document.head.appendChild(style);
262
410
  }
263
411
  document.body.appendChild(overlay);
264
- setTimeout(() => {
265
- overlay.remove();
266
- }, duration);
412
+ return new Promise((resolve) => {
413
+ let finished = false;
414
+ const finish = () => {
415
+ if (finished)
416
+ return;
417
+ finished = true;
418
+ overlay.removeEventListener("animationend", onAnimationEnd);
419
+ overlay.remove();
420
+ resolve();
421
+ };
422
+ const onAnimationEnd = (event) => {
423
+ // Ignore the infinite pulse; only the finite life animation ends.
424
+ if (event.animationName === "selector-highlight-life") {
425
+ finish();
426
+ }
427
+ };
428
+ overlay.addEventListener("animationend", onAnimationEnd);
429
+ // Guardrail: if animationend is ever missed, remove shortly after duration.
430
+ window.setTimeout(finish, duration + 500);
431
+ });
432
+ }
433
+ let activeRevealController = null;
434
+ let revealControllerSeq = 0;
435
+ // Serialize Show-me reveal clicks across ALL buttons so overlapping rapid clicks
436
+ // can never interleave. Each click chains behind the previous handler and grabs
437
+ // a monotonic token. This closes the race where, during the
438
+ // `await superseded.close()` teardown window, no global controller is registered:
439
+ // without serialization a faster later click could start, become active, and
440
+ // then be overwritten by this (older, still-awaiting) handler — i.e. NOT
441
+ // "last click wins". With the chain, a newer click waits for this one to fully
442
+ // settle (and is then properly superseded), and an older handler that has been
443
+ // overtaken while waiting discards its work via the token check.
444
+ let revealClickChain = Promise.resolve();
445
+ let revealClickSeq = 0;
446
+ // Store the new active reveal. Superseding the previous one is done explicitly
447
+ // at click start (see supersedeActiveReveal), so this only records state.
448
+ function registerActiveReveal(controller) {
449
+ activeRevealController = controller;
450
+ }
451
+ // Detach and return the currently active reveal unless it belongs to exceptId.
452
+ // The caller is responsible for closing it (and awaiting its teardown).
453
+ function supersedeActiveReveal(exceptId) {
454
+ const current = activeRevealController;
455
+ if (current && current.id !== exceptId) {
456
+ activeRevealController = null;
457
+ return current;
458
+ }
459
+ return null;
460
+ }
461
+ function clearActiveReveal(id) {
462
+ if (activeRevealController?.id === id) {
463
+ activeRevealController = null;
464
+ }
465
+ }
466
+ // Close the More-panels flyout if it is currently open (inverse of open-more-sidebars).
467
+ function closeMorePanelsIfOpen() {
468
+ if (!isElementPresent("@more-sidebars-panel"))
469
+ return;
470
+ const trigger = document.querySelector('[data-testid="more-sidebars-button"]');
471
+ trigger?.click();
472
+ }
473
+ function isAgentsPanelOpen() {
474
+ const trigger = document.querySelector('[data-testid="agents-panel-button"]');
475
+ return (trigger?.getAttribute("aria-pressed") === "true" ||
476
+ isElementVisibleBySelector("@sidebar-panel-agents-panel"));
267
477
  }
268
478
  // Button component for "Show me" functionality with availability detection
269
479
  function SelectorButton({ selectorDef, keyProp, }) {
270
480
  const editContext = useEditContext();
271
481
  const manualActions = editContext?.configuration.editor.manualActions;
272
- const availabilitySelector = selectorDef.availabilitySelector || selectorDef.selector;
273
- // For sidebar-only selectors, we don't need to check for an element
274
- const isSidebarOnly = selectorDef.isSidebarOnly;
275
- // Initialize to true for sidebar-only, false otherwise to avoid hydration mismatch
276
- // (document.querySelector doesn't exist during SSR)
277
- const [isAvailable, setIsAvailable] = useState(isSidebarOnly ? true : false);
278
- // Can open sidebar if: sidebar-only selector
279
- const canAutoOpenSidebar = Boolean(isSidebarOnly && editContext?.openSidebar && selectorDef.location);
482
+ const isSidebarOnly = !!selectorDef.isSidebarOnly;
483
+ const targetSelector = selectorDef.selector;
484
+ // Anchor = explicit availabilitySelector only. It gates the disabled state.
485
+ const anchorSelector = selectorDef.availabilitySelector;
486
+ const hasAvailabilitySelector = Boolean(anchorSelector);
487
+ const closeMode = selectorDef.closeMode ?? "click";
488
+ // Keep a fresh reference to editContext for use inside reveal/restore closures.
489
+ const editContextRef = useRef(editContext);
490
+ editContextRef.current = editContext;
280
491
  const hasBeforeAction = Boolean(selectorDef.beforeAction &&
281
492
  manualActions?.[selectorDef.beforeAction] &&
282
493
  editContext);
283
- const isActionable = isSidebarOnly
284
- ? canAutoOpenSidebar || hasBeforeAction
285
- : isAvailable || canAutoOpenSidebar || hasBeforeAction;
286
- // Monitor for element availability changes (skip for sidebar-only selectors)
494
+ // A sidebar-only selector is reachable only when the current workspace
495
+ // supports sidebars and offers this specific sidebar (both are workspace-scoped).
496
+ const canAutoOpenSidebar = Boolean(isSidebarOnly &&
497
+ selectorDef.location &&
498
+ editContext?.openSidebar &&
499
+ editContext.workspace?.supportsSidebars &&
500
+ editContext.availableSidebars?.some((s) => s.id === selectorDef.location));
501
+ // Presence/visibility probes, kept up to date by the effect below.
502
+ // The anchor gates "is this relevant in the current context", so it checks
503
+ // presence (it may itself be a currently-collapsed container). The target
504
+ // checks visibility to distinguish "show" from "show-action".
505
+ const [isTargetPresent, setIsTargetPresent] = useState(false);
506
+ const [isTargetVisible, setIsTargetVisible] = useState(false);
507
+ const [isAnchorPresent, setIsAnchorPresent] = useState(false);
508
+ // True while THIS button has revealed a container (State 3 active -> green).
509
+ const [isActiveReveal, setIsActiveReveal] = useState(false);
510
+ const controllerIdRef = useRef(0);
511
+ const revealRef = useRef(null);
512
+ const isAnchorReachable = hasAvailabilitySelector
513
+ ? isAnchorPresent
514
+ : isSidebarOnly
515
+ ? canAutoOpenSidebar
516
+ : isTargetPresent || hasBeforeAction;
517
+ const hasRevealPath = isSidebarOnly || hasBeforeAction || canAutoOpenSidebar || isTargetPresent;
518
+ const state = deriveSelectorState({
519
+ isSidebarOnly,
520
+ isAnchorReachable,
521
+ isTargetVisible,
522
+ hasRevealPath,
523
+ });
524
+ const isDisabled = state === "disabled";
525
+ // Reset active (green) state WITHOUT running the undo steps. Used when the
526
+ // revealed container was closed by some other means.
527
+ const resetActiveState = useCallback(() => {
528
+ clearActiveReveal(controllerIdRef.current);
529
+ revealRef.current = null;
530
+ setIsActiveReveal(false);
531
+ }, []);
532
+ // Run the undo steps and reset. Used for explicit close (2nd click, auto
533
+ // timer, or being superseded by another reveal via mutual exclusion).
534
+ const runRestore = useCallback(async () => {
535
+ const reveal = revealRef.current;
536
+ // Reset button state immediately (button turns blue) while the surface
537
+ // teardown animation plays out; the returned promise resolves once settled.
538
+ clearActiveReveal(reveal?.id ?? controllerIdRef.current);
539
+ revealRef.current = null;
540
+ setIsActiveReveal(false);
541
+ if (reveal) {
542
+ try {
543
+ await reveal.restore();
544
+ }
545
+ catch {
546
+ // ignore restore failures
547
+ }
548
+ }
549
+ }, []);
550
+ const runRestoreRef = useRef(runRestore);
551
+ runRestoreRef.current = runRestore;
552
+ // Keep the presence/visibility probes fresh.
287
553
  useEffect(() => {
288
- // Sidebar-only selectors are always "available" if openSidebar exists
289
- if (isSidebarOnly) {
290
- setIsAvailable(true);
291
- return;
554
+ const check = () => {
555
+ setIsTargetPresent(targetSelector ? isElementPresent(targetSelector) : false);
556
+ setIsTargetVisible(targetSelector ? isElementVisibleBySelector(targetSelector) : false);
557
+ setIsAnchorPresent(anchorSelector ? isElementPresent(anchorSelector) : false);
558
+ };
559
+ check();
560
+ const attributeFilter = [
561
+ "data-testid",
562
+ "class",
563
+ "id",
564
+ "style",
565
+ "hidden",
566
+ "aria-hidden",
567
+ ];
568
+ const observer = new MutationObserver(check);
569
+ observer.observe(document.body, {
570
+ childList: true,
571
+ subtree: true,
572
+ attributes: true,
573
+ attributeFilter,
574
+ });
575
+ let iframeObserver = null;
576
+ const iframe = document.querySelector("iframe.page-iframe");
577
+ if (iframe?.contentDocument?.body) {
578
+ iframeObserver = new MutationObserver(check);
579
+ iframeObserver.observe(iframe.contentDocument.body, {
580
+ childList: true,
581
+ subtree: true,
582
+ attributes: true,
583
+ attributeFilter,
584
+ });
292
585
  }
293
- // Initial check
294
- const initialAvailable = isElementAvailable(availabilitySelector);
295
- setIsAvailable(initialAvailable);
296
- // Set up MutationObserver to watch for DOM changes
297
- const checkAvailability = () => {
298
- const available = isElementAvailable(availabilitySelector);
299
- setIsAvailable(available);
586
+ const intervalId = setInterval(check, 1000);
587
+ return () => {
588
+ observer.disconnect();
589
+ iframeObserver?.disconnect();
590
+ clearInterval(intervalId);
591
+ };
592
+ }, [targetSelector, anchorSelector]);
593
+ // While active, detect if the revealed container was closed externally and
594
+ // reset the button's active (green) state accordingly.
595
+ useEffect(() => {
596
+ if (!isActiveReveal)
597
+ return;
598
+ const check = () => {
599
+ const reveal = revealRef.current;
600
+ if (!reveal || !reveal.isStillOpen()) {
601
+ resetActiveState();
602
+ }
300
603
  };
301
- // Watch the main document for changes
302
- const observer = new MutationObserver(checkAvailability);
604
+ const observer = new MutationObserver(check);
303
605
  observer.observe(document.body, {
304
606
  childList: true,
305
607
  subtree: true,
306
608
  attributes: true,
307
- attributeFilter: ["data-testid", "class", "id"],
609
+ attributeFilter: [
610
+ EDITOR_FORM_HIDDEN_ATTRIBUTE,
611
+ "class",
612
+ "style",
613
+ "data-testid",
614
+ ],
308
615
  });
309
- // Also poll periodically as a fallback for async UI updates.
310
- const intervalId = setInterval(checkAvailability, 1000);
616
+ const intervalId = setInterval(check, 500);
311
617
  return () => {
312
618
  observer.disconnect();
313
619
  clearInterval(intervalId);
314
620
  };
315
- }, [availabilitySelector, isSidebarOnly]);
316
- const tooltipText = isActionable
317
- ? selectorDef.description
318
- : selectorDef.notFoundMessage || selectorDef.description;
319
- return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { "aria-label": "Show me", onClick: async () => {
320
- if (!isActionable)
621
+ }, [isActiveReveal, resetActiveState]);
622
+ // Clear the global active controller if this button unmounts while active.
623
+ useEffect(() => {
624
+ return () => {
625
+ clearActiveReveal(controllerIdRef.current);
626
+ };
627
+ }, []);
628
+ const tooltipText = isActiveReveal
629
+ ? CLOSE_PANE_LABEL
630
+ : isDisabled
631
+ ? selectorDef.notFoundMessage || selectorDef.description
632
+ : selectorDef.description;
633
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { "aria-label": isActiveReveal ? CLOSE_PANE_LABEL : "Show me", onClick: async () => {
634
+ // Second click while active -> restore (close=click behaviour).
635
+ if (isActiveReveal) {
636
+ runRestore();
321
637
  return;
322
- const runId = globalThis.crypto?.randomUUID?.() ??
323
- `manual-${Date.now()}-${Math.random().toString(36).slice(2)}`;
324
- const resolveElements = (selector = selectorDef.selector) => selector ? getElementsForSelector(selector) : [];
325
- const runManualAction = async (actionName, elements) => {
326
- if (!actionName || !editContext || !manualActions?.[actionName]) {
638
+ }
639
+ if (isDisabled)
640
+ return;
641
+ // Sequence overlapping Show-me clicks across ALL buttons so a rapid
642
+ // second click can't interleave with this one. We chain behind any
643
+ // in-flight reveal and take a monotonic token; if a newer click
644
+ // arrives while we wait our turn, this (now stale) handler bails out
645
+ // instead of overtaking it. This guarantees "last click wins" and
646
+ // closes the teardown-window race where, during
647
+ // `await superseded.close()`, no global controller is registered.
648
+ const clickToken = ++revealClickSeq;
649
+ const previousClick = revealClickChain;
650
+ let releaseClick = () => { };
651
+ revealClickChain = new Promise((resolve) => {
652
+ releaseClick = resolve;
653
+ });
654
+ try {
655
+ await previousClick;
656
+ // A newer click superseded this one while we waited our turn.
657
+ if (clickToken !== revealClickSeq) {
327
658
  return;
328
659
  }
329
- const action = manualActions[actionName];
330
- const actionProps = {
331
- editContext,
332
- selectorDef,
333
- duration: HIGHLIGHT_DURATION_MS,
334
- runId,
335
- elements,
336
- resolveElements,
660
+ // State-machine semantics: any new Show-me action supersedes the
661
+ // currently active State 3 reveal from another button, restoring
662
+ // its UI first. Await the teardown so this reveal samples fresh
663
+ // DOM and avoids a stale "was this surface open?" read. Because
664
+ // clicks are serialized above, the superseded controller is
665
+ // always the previous fully-settled reveal (no race here).
666
+ const superseded = supersedeActiveReveal(controllerIdRef.current);
667
+ if (superseded) {
668
+ try {
669
+ // Await the close so A's surface finishes its teardown
670
+ // animation before B starts revealing (event-driven, not timed).
671
+ await superseded.close();
672
+ }
673
+ catch {
674
+ // ignore supersede failures
675
+ }
676
+ }
677
+ const runId = globalThis.crypto?.randomUUID?.() ??
678
+ `manual-${Date.now()}-${Math.random().toString(36).slice(2)}`;
679
+ const resolveElements = (selector = selectorDef.selector) => selector ? getElementsForSelector(selector) : [];
680
+ const runManualAction = async (actionName, elements) => {
681
+ if (!actionName ||
682
+ !editContext ||
683
+ !manualActions?.[actionName]) {
684
+ return;
685
+ }
686
+ const action = manualActions[actionName];
687
+ const actionProps = {
688
+ editContext,
689
+ selectorDef,
690
+ duration: HIGHLIGHT_DURATION_MS,
691
+ runId,
692
+ elements,
693
+ resolveElements,
694
+ };
695
+ await action(actionProps);
337
696
  };
338
- await action(actionProps);
339
- };
340
- // If this selector is sidebar-only, open the sidebar before highlighting.
341
- // For sidebar-only selectors, location is the sidebar ID (without -sidebar suffix)
342
- if (selectorDef.isSidebarOnly &&
343
- editContext?.openSidebar &&
344
- selectorDef.location) {
345
- const sidebarId = selectorDef.location;
346
- const isSidebarAlreadyOpen = !!editContext.openSidebars?.includes(sidebarId);
347
- if (!isSidebarAlreadyOpen) {
348
- editContext.openSidebar(sidebarId);
349
- // Give the sidebar a moment to render before querying/highlighting.
350
- await new Promise((resolve) => setTimeout(resolve, 150));
697
+ // Undo steps captured as we reveal, plus predicates telling
698
+ // whether the revealed container is still open.
699
+ const undoSteps = [];
700
+ const openPredicates = [];
701
+ // 1. Sidebar-only: open the sidebar.
702
+ if (isSidebarOnly &&
703
+ editContext?.openSidebar &&
704
+ selectorDef.location) {
705
+ const sidebarId = selectorDef.location;
706
+ const wasOpen = !!editContext.openSidebars?.includes(sidebarId);
707
+ if (!wasOpen) {
708
+ editContext.openSidebar(sidebarId);
709
+ undoSteps.push(async () => {
710
+ editContextRef.current?.toggleSidebar?.(sidebarId, {
711
+ forceClose: true,
712
+ });
713
+ await editContextRef.current?.waitForSurfaceSettled?.(`sidebar:${sidebarId}`);
714
+ });
715
+ // Wait for the sidebar's open animation to finish before querying.
716
+ await editContext.waitForSurfaceSettled?.(`sidebar:${sidebarId}`);
717
+ }
718
+ openPredicates.push(() => !!editContextRef.current?.openSidebars?.includes(sidebarId));
719
+ }
720
+ const availabilityElements = anchorSelector
721
+ ? resolveElements(anchorSelector)
722
+ : [];
723
+ const relevantElements = !isSidebarOnly
724
+ ? [...resolveElements(), ...availabilityElements]
725
+ : availabilityElements;
726
+ const formSlider = getEditorFormSlider(relevantElements);
727
+ const shouldOpenEditorForm = !isSidebarOnly &&
728
+ !!formSlider &&
729
+ isEditorFormSliderHidden(formSlider);
730
+ if (selectorDef.tab) {
731
+ editContext?.setActiveEditorTab(selectorDef.tab);
732
+ }
733
+ // 2. Editor form slider.
734
+ if (shouldOpenEditorForm && editContext) {
735
+ const slotId = editContext.getActiveSlotId();
736
+ editContext.setEditorFormHiddenForSlot(slotId, false);
737
+ undoSteps.push(async () => {
738
+ editContextRef.current?.setEditorFormHiddenForSlot(slotId, true);
739
+ await editContextRef.current?.waitForSurfaceSettled?.("editor-form");
740
+ });
741
+ openPredicates.push(() => !isEditorFormSliderHidden(getEditorFormSlider()));
742
+ await editContext.waitForSurfaceSettled?.("editor-form");
743
+ }
744
+ else if (selectorDef.tab) {
745
+ await waitForNextFrame();
746
+ }
747
+ // 3. beforeAction (may open the More-panels flyout, reveal fields, etc.).
748
+ const resolvedElements = !isSidebarOnly
749
+ ? await waitForResolvedElements(resolveElements, selectorDef.selector)
750
+ : [];
751
+ const actionElements = resolvedElements.length > 0
752
+ ? resolvedElements
753
+ : availabilityElements;
754
+ const morePanelsWasOpen = isElementPresent("@more-sidebars-panel");
755
+ // Capture pre-reveal state for the AI Assistant reveals so we only
756
+ // restore (close) what THIS button actually opened.
757
+ const agentsPanelWasOpen = isAgentsPanelOpen();
758
+ const agentSettingsWasOpen = !!selectorDef.selector &&
759
+ isElementPresent(selectorDef.selector) &&
760
+ selectorDef.beforeAction === "open-agent-settings";
761
+ await runManualAction(selectorDef.beforeAction, actionElements);
762
+ if (selectorDef.beforeAction === "open-more-sidebars") {
763
+ const morePanelsNowOpen = isElementPresent("@more-sidebars-panel");
764
+ if (!morePanelsWasOpen && morePanelsNowOpen) {
765
+ undoSteps.push(async () => {
766
+ closeMorePanelsIfOpen();
767
+ await waitForSelectorAnimations('[data-testid="more-sidebars-panel"]');
768
+ });
769
+ openPredicates.push(() => isElementPresent("@more-sidebars-panel"));
770
+ }
771
+ }
772
+ if (selectorDef.beforeAction === "open-agents-panel" ||
773
+ selectorDef.beforeAction === "open-agent-settings") {
774
+ // The beforeAction already awaited the panel/popover open
775
+ // animation (see config openAgentsPanel/openAgentSettings), so
776
+ // no polling here.
777
+ const agentsPanelNowOpen = isAgentsPanelOpen();
778
+ // Push the panel-close undo first so that, since undo steps run
779
+ // in reverse, the settings popover is closed before the panel.
780
+ if (!agentsPanelWasOpen && agentsPanelNowOpen) {
781
+ undoSteps.push(async () => {
782
+ if (editContextRef.current?.isMobile) {
783
+ editContextRef.current?.toggleSidebar?.("agents-panel", {
784
+ forceClose: true,
785
+ });
786
+ await editContextRef.current?.waitForSurfaceSettled?.("sidebar:agents-panel");
787
+ }
788
+ else {
789
+ editContextRef.current?.setShowAgentsPanel?.(false);
790
+ await editContextRef.current?.waitForSurfaceSettled?.("agents-panel");
791
+ }
792
+ });
793
+ openPredicates.push(() => isAgentsPanelOpen());
794
+ }
795
+ if (selectorDef.beforeAction === "open-agent-settings") {
796
+ const settingsTargetNowOpen = !!selectorDef.selector &&
797
+ isElementPresent(selectorDef.selector);
798
+ if (!agentSettingsWasOpen && settingsTargetNowOpen) {
799
+ undoSteps.push(async () => {
800
+ const trigger = document.querySelector('[data-testid="agent-settings-popover-trigger"]');
801
+ if (trigger &&
802
+ selectorDef.selector &&
803
+ isElementPresent(selectorDef.selector)) {
804
+ trigger.click();
805
+ await waitForSelectorAnimations('[data-testid="agent-settings-popover-content"]');
806
+ }
807
+ });
808
+ openPredicates.push(() => selectorDef.selector
809
+ ? isElementPresent(selectorDef.selector)
810
+ : false);
811
+ }
812
+ }
813
+ }
814
+ // afterAction is the declared inverse for custom reveals (e.g. field actions).
815
+ if (selectorDef.afterAction) {
816
+ undoSteps.push(async () => {
817
+ await runManualAction(selectorDef.afterAction, !isSidebarOnly ? resolveElements() : availabilityElements);
818
+ });
819
+ }
820
+ // Highlight the target. For sidebar-only selectors there is no
821
+ // inner target, so we highlight the opened sidebar panel itself.
822
+ // Wait for it to become visible first, since a just-revealed
823
+ // element may be present but not yet laid out (highlightElement
824
+ // skips invisible ones).
825
+ const highlightSelector = isSidebarOnly
826
+ ? selectorDef.location
827
+ ? `@sidebar-panel-${selectorDef.location}`
828
+ : ""
829
+ : selectorDef.selector;
830
+ // The highlight runs a finite CSS animation; highlightElement
831
+ // resolves on its animationend. We do NOT await it before
832
+ // registering the reveal (the button must turn green immediately);
833
+ // close=auto instead chains its restore onto this lifetime.
834
+ let highlightLifetime = Promise.resolve();
835
+ if (highlightSelector) {
836
+ const highlightTargets = await waitForVisibleElements(highlightSelector, 1500);
837
+ if (highlightTargets.length > 0) {
838
+ highlightLifetime = highlightElement(highlightSelector, HIGHLIGHT_DURATION_MS);
839
+ }
840
+ }
841
+ // If we opened/undid anything, this was a State-3 reveal: keep the
842
+ // button active (green) and register it for mutual exclusion.
843
+ if (undoSteps.length > 0) {
844
+ const id = ++revealControllerSeq;
845
+ controllerIdRef.current = id;
846
+ revealRef.current = {
847
+ id,
848
+ restore: async () => {
849
+ for (let i = undoSteps.length - 1; i >= 0; i -= 1) {
850
+ try {
851
+ await undoSteps[i]?.();
852
+ }
853
+ catch {
854
+ // ignore individual undo failures
855
+ }
856
+ }
857
+ },
858
+ isStillOpen: () => openPredicates.length === 0
859
+ ? true
860
+ : openPredicates.some((predicate) => predicate()),
861
+ };
862
+ registerActiveReveal({
863
+ id,
864
+ close: () => runRestoreRef.current(),
865
+ isStillOpen: () => revealRef.current?.isStillOpen() ?? false,
866
+ });
867
+ setIsActiveReveal(true);
868
+ if (closeMode === "auto") {
869
+ // Restore right after the highlight animation completes,
870
+ // instead of racing a duration-matched timer.
871
+ void highlightLifetime.then(() => {
872
+ if (controllerIdRef.current === id) {
873
+ void runRestoreRef.current();
874
+ }
875
+ });
876
+ }
351
877
  }
352
878
  }
353
- const resolvedElements = !isSidebarOnly ? resolveElements() : [];
354
- await runManualAction(selectorDef.beforeAction, resolvedElements);
355
- // For sidebar-only selectors, we just open the sidebar (done above)
356
- // For regular selectors, also highlight the element
357
- if (!isSidebarOnly && selectorDef.selector) {
358
- await highlightElement(selectorDef.selector, HIGHLIGHT_DURATION_MS);
359
- }
360
- if (selectorDef.afterAction) {
361
- window.setTimeout(() => {
362
- void runManualAction(selectorDef.afterAction, !isSidebarOnly ? resolveElements() : []);
363
- }, HIGHLIGHT_DURATION_MS);
879
+ finally {
880
+ // Release our slot so the next queued click can proceed (and then
881
+ // properly supersede us). Runs even if the reveal threw, so the
882
+ // click chain can never deadlock.
883
+ releaseClick();
364
884
  }
365
- }, disabled: !isActionable, className: cn("inline-flex h-5 w-5 items-center justify-center rounded border transition-colors", isActionable
366
- ? "border-feedback-blue bg-feedback-blue-light text-feedback-blue hover:bg-feedback-blue-light cursor-pointer"
367
- : "border-border-default bg-neutral-grey-5 text-neutral-grey-50 cursor-not-allowed"), children: [_jsx(LocateFixed, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), _jsx("span", { className: "sr-only", children: "Show me" })] }, keyProp) }), _jsx(TooltipContent, { children: tooltipText })] }));
885
+ }, disabled: isDisabled && !isActiveReveal, className: cn("inline-flex h-5 w-5 items-center justify-center rounded border transition-colors", isActiveReveal
886
+ ? "border-feedback-green bg-feedback-green-light text-feedback-green hover:bg-feedback-green-light cursor-pointer"
887
+ : !isDisabled
888
+ ? "border-feedback-blue bg-feedback-blue-light text-feedback-blue hover:bg-feedback-blue-light cursor-pointer"
889
+ : "border-border-default bg-neutral-grey-5 text-neutral-grey-50 cursor-not-allowed"), children: [_jsx(LocateFixed, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }), _jsx("span", { className: "sr-only", children: "Show me" })] }, keyProp) }), _jsx(TooltipContent, { children: tooltipText })] }));
368
890
  }
369
891
  // Render markdown content with clickable selectors
370
892
  // Supports both {{selectorName}} syntax (looks up in uiSelectors) and legacy @selector syntax