@ai-matrx/context-menu 0.0.0 → 0.1.1

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 (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +14 -1
  3. package/dist/builder.cjs +1810 -0
  4. package/dist/builder.cjs.map +1 -0
  5. package/dist/builder.d.cts +22 -0
  6. package/dist/builder.d.ts +22 -0
  7. package/dist/builder.js +1795 -0
  8. package/dist/builder.js.map +1 -0
  9. package/dist/design-store.cjs +195 -0
  10. package/dist/design-store.cjs.map +1 -0
  11. package/dist/design-store.d.cts +42 -0
  12. package/dist/design-store.d.ts +42 -0
  13. package/dist/design-store.js +175 -0
  14. package/dist/design-store.js.map +1 -0
  15. package/dist/engine.cjs +3069 -0
  16. package/dist/engine.cjs.map +1 -0
  17. package/dist/engine.d.cts +731 -0
  18. package/dist/engine.d.ts +731 -0
  19. package/dist/engine.js +3250 -0
  20. package/dist/engine.js.map +1 -0
  21. package/dist/grouping-CB3zlD1T.d.ts +623 -0
  22. package/dist/grouping-CGcWsltu.d.cts +623 -0
  23. package/dist/host-types-AOiEYwR8.d.cts +9 -0
  24. package/dist/host-types-AOiEYwR8.d.ts +9 -0
  25. package/dist/index.cjs +583 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.d.cts +60 -0
  28. package/dist/index.d.ts +60 -0
  29. package/dist/index.js +561 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/menu-content.cjs +3146 -0
  32. package/dist/menu-content.cjs.map +1 -0
  33. package/dist/menu-content.d.cts +30 -0
  34. package/dist/menu-content.d.ts +30 -0
  35. package/dist/menu-content.js +3197 -0
  36. package/dist/menu-content.js.map +1 -0
  37. package/dist/model-CkLCR6WY.d.cts +123 -0
  38. package/dist/model-CkLCR6WY.d.ts +123 -0
  39. package/dist/react.cjs +3322 -0
  40. package/dist/react.cjs.map +1 -0
  41. package/dist/react.d.cts +133 -0
  42. package/dist/react.d.ts +133 -0
  43. package/dist/react.js +3345 -0
  44. package/dist/react.js.map +1 -0
  45. package/package.json +170 -1
package/dist/react.js ADDED
@@ -0,0 +1,3345 @@
1
+ "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
15
+
16
+ // src/engine/utils/compact.ts
17
+ function compact(value) {
18
+ const out = {};
19
+ for (const [key, v] of Object.entries(value)) if (v !== void 0) out[key] = v;
20
+ return out;
21
+ }
22
+
23
+ // src/engine/ContextMenuV3.tsx
24
+ import React, { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
25
+ import { Slot } from "@radix-ui/react-slot";
26
+ import { useIsMobile } from "@ai-matrx/kit/media-query";
27
+ import { useSelectionZone } from "@ai-matrx/rich-content/selection-toolbar/selection-zones";
28
+
29
+ // src/engine/selection-provider.ts
30
+ import { AGENT_ICON } from "@ai-matrx/icons/domain";
31
+ import { registerAlchemyIcon } from "@ai-matrx/rich-content/utils/alchemy-icon-keys";
32
+ import {
33
+ declareSelectionProvider,
34
+ placeSelectionActions,
35
+ hostHalf,
36
+ selectionToolbarHostOf,
37
+ shownInSelectionMode
38
+ } from "@ai-matrx/rich-content/selection-toolbar/selection-actions";
39
+ var CONTEXT_MENU_SELECTION_HOST_KEY = "contextMenuSelection";
40
+ function menuOf(target) {
41
+ const half = hostHalf(target, CONTEXT_MENU_SELECTION_HOST_KEY);
42
+ return half?.kind === "context-menu-selection" ? half : null;
43
+ }
44
+ var ASK_AI = {
45
+ id: "selection:ai",
46
+ label: "Ask AI",
47
+ description: "AI actions, agents, copy and export for the selected text",
48
+ icon: registerAlchemyIcon(AGENT_ICON),
49
+ category: "ai",
50
+ order: 0,
51
+ placement: "primary",
52
+ preserveSelection: true,
53
+ eligible: (t) => menuOf(t) && shownInSelectionMode(ASK_AI.id, t) ? { status: "available" } : { status: "absent" },
54
+ run: (t) => {
55
+ const menu = menuOf(t);
56
+ if (!menu) return;
57
+ selectionToolbarHostOf(t)?.ui.close();
58
+ menu.open();
59
+ }
60
+ };
61
+ var contextMenuSelectionProvider = {
62
+ id: "context-menu-selection",
63
+ tier: "T0",
64
+ declaredIds: () => [ASK_AI.id],
65
+ actions: (target) => menuOf(target) ? placeSelectionActions([ASK_AI], target) : []
66
+ };
67
+ declareSelectionProvider(contextMenuSelectionProvider);
68
+
69
+ // src/engine/utils/selection-tracking.ts
70
+ var selection_tracking_exports = {};
71
+ __reExport(selection_tracking_exports, selection_tracking_star);
72
+ import * as selection_tracking_star from "@ai-matrx/chat/context-menu/utils/selection-tracking";
73
+
74
+ // src/engine/types.ts
75
+ var types_exports = {};
76
+ __reExport(types_exports, types_star);
77
+ import * as types_star from "@ai-matrx/chat/context-menu/types";
78
+
79
+ // src/engine/utils/per-row-entity.ts
80
+ var isDev = process.env.NODE_ENV !== "production";
81
+ function resolveEffectiveEntity(entityProp, resolved) {
82
+ if (!resolved || !(types_exports.CONTEXT_MENU_ENTITY_KEY in resolved)) return entityProp;
83
+ const perRow = resolved[types_exports.CONTEXT_MENU_ENTITY_KEY];
84
+ if (perRow === null || perRow === void 0) return void 0;
85
+ const candidate = perRow;
86
+ if (typeof perRow !== "object" || typeof candidate.type !== "string" || !candidate.type || typeof candidate.id !== "string" || !candidate.id) {
87
+ if (isDev) {
88
+ console.error(
89
+ `%c[ContextMenuV3] MALFORMED PER-ROW ENTITY%c \u2014 resolveContextOnOpen returned "${types_exports.CONTEXT_MENU_ENTITY_KEY}" without a string type + id, so Attach To / Share would target nothing. Falling back to the menu-level entity.`,
90
+ "color:#ef4444;font-weight:bold",
91
+ "color:inherit",
92
+ { received: perRow }
93
+ );
94
+ }
95
+ return entityProp;
96
+ }
97
+ return candidate;
98
+ }
99
+ function mergeResolvedContextData(contextData, resolved) {
100
+ const merged = {
101
+ ...contextData ?? {},
102
+ ...resolved ?? {}
103
+ };
104
+ delete merged[types_exports.CONTEXT_MENU_ENTITY_KEY];
105
+ return merged;
106
+ }
107
+ function joinRowAndSurfaceContext(row, surface) {
108
+ if (!row) return surface ?? null;
109
+ if (!surface) return row;
110
+ const rowNamesEntity = row[types_exports.CONTEXT_MENU_ENTITY_KEY] !== null && row[types_exports.CONTEXT_MENU_ENTITY_KEY] !== void 0;
111
+ return {
112
+ ...surface,
113
+ ...row,
114
+ ...!rowNamesEntity && types_exports.CONTEXT_MENU_ENTITY_KEY in surface ? { [types_exports.CONTEXT_MENU_ENTITY_KEY]: surface[types_exports.CONTEXT_MENU_ENTITY_KEY] } : {}
115
+ };
116
+ }
117
+ var TOKEN_SHAPE = /^[a-z][a-z0-9_]*$/;
118
+ function sniffEntityFromDom(target) {
119
+ const host = target?.closest?.("[data-entity-type][data-entity-id]");
120
+ if (!(host instanceof HTMLElement)) return null;
121
+ const type = host.getAttribute("data-entity-type")?.trim() ?? "";
122
+ const id = host.getAttribute("data-entity-id")?.trim() ?? "";
123
+ if (!type || !id) return null;
124
+ if (!TOKEN_SHAPE.test(type)) {
125
+ if (isDev) {
126
+ console.error(
127
+ `%c[ContextMenuV3] MALFORMED data-entity-type%c \u2014 "${type}" is not an entity token (expected snake_case, e.g. "seo_keyword"), so Attach To / Share would target nothing. Fix the attribute or drop it.`,
128
+ "color:#ef4444;font-weight:bold",
129
+ "color:inherit",
130
+ { element: host }
131
+ );
132
+ }
133
+ return null;
134
+ }
135
+ const title = host.getAttribute("data-entity-title")?.trim() || (host.textContent ?? "").trim().slice(0, 120) || id;
136
+ const resourceType = host.getAttribute("data-entity-resource")?.trim();
137
+ return {
138
+ type,
139
+ id,
140
+ title,
141
+ ...resourceType ? { resourceType } : {}
142
+ };
143
+ }
144
+
145
+ // src/engine/menu-presence.tsx
146
+ import { createContext, useContext } from "react";
147
+ var MenuPresenceContext = createContext(false);
148
+ var MenuPresenceProvider = MenuPresenceContext.Provider;
149
+ function useIsInsideContextMenu() {
150
+ return useContext(MenuPresenceContext);
151
+ }
152
+ var RegistryMenuSourceContext = createContext(null);
153
+ var RegistryMenuSourceProvider = RegistryMenuSourceContext.Provider;
154
+ function useRegistryMenuSource() {
155
+ return useContext(RegistryMenuSourceContext);
156
+ }
157
+ function sameContentSource(a, b) {
158
+ if (!a || !b || a.type !== b.type) return false;
159
+ return contentSourceKey(a) === contentSourceKey(b);
160
+ }
161
+ function contentSourceKey(s) {
162
+ return JSON.stringify(
163
+ Object.fromEntries(
164
+ Object.entries(s).filter(([k]) => k !== "readOnly").sort(([x], [y]) => x.localeCompare(y))
165
+ )
166
+ );
167
+ }
168
+
169
+ // src/engine/ContextMenuV3.tsx
170
+ import { useOptionalWidgetHandle } from "@ai-matrx/chat/agents/hooks/useWidgetHandle";
171
+
172
+ // src/engine/utils/widget-handle.ts
173
+ function setFieldValue(el, next, caret) {
174
+ const setter = Object.getOwnPropertyDescriptor(
175
+ HTMLTextAreaElement.prototype,
176
+ "value"
177
+ )?.set;
178
+ if (setter) setter.call(el, next);
179
+ else el.value = next;
180
+ el.dispatchEvent(new Event("input", { bubbles: true }));
181
+ try {
182
+ el.setSelectionRange(caret, caret);
183
+ } catch {
184
+ }
185
+ }
186
+ function buildEditableWidgetHandle(args) {
187
+ const {
188
+ getTextarea,
189
+ onTextReplace,
190
+ onTextInsertBefore,
191
+ onTextInsertAfter,
192
+ getApplicationScope
193
+ } = args;
194
+ const field = () => getTextarea?.() ?? null;
195
+ const readCurrent = () => {
196
+ const el = field();
197
+ if (el) return el.value;
198
+ const content = getApplicationScope?.().content;
199
+ return typeof content === "string" ? content : null;
200
+ };
201
+ const writeFull = (next) => {
202
+ if (onTextReplace) {
203
+ onTextReplace(next);
204
+ return;
205
+ }
206
+ const el = field();
207
+ if (!el) throw new Error("Surface has no writable target");
208
+ setFieldValue(el, next, next.length);
209
+ };
210
+ const canWrite = Boolean(onTextReplace) || Boolean(getTextarea);
211
+ const canRead = Boolean(getTextarea) || Boolean(getApplicationScope);
212
+ if (!canWrite) return null;
213
+ const handle = {
214
+ onTextReplace: ({ text }) => writeFull(text),
215
+ ...canRead ? { readText: readCurrent } : {}
216
+ };
217
+ if (onTextInsertBefore || getTextarea) {
218
+ handle.onTextInsertBefore = ({ text }) => {
219
+ if (onTextInsertBefore) return onTextInsertBefore(text);
220
+ const el = field();
221
+ if (!el) throw new Error("Surface has no writable target");
222
+ const at = el.selectionStart ?? 0;
223
+ setFieldValue(
224
+ el,
225
+ el.value.slice(0, at) + text + el.value.slice(at),
226
+ at + text.length
227
+ );
228
+ };
229
+ }
230
+ if (onTextInsertAfter || getTextarea) {
231
+ handle.onTextInsertAfter = ({ text }) => {
232
+ if (onTextInsertAfter) return onTextInsertAfter(text);
233
+ const el = field();
234
+ if (!el) throw new Error("Surface has no writable target");
235
+ const at = el.selectionEnd ?? el.value.length;
236
+ setFieldValue(
237
+ el,
238
+ el.value.slice(0, at) + text + el.value.slice(at),
239
+ at + text.length
240
+ );
241
+ };
242
+ }
243
+ if (canRead) {
244
+ handle.onTextPrepend = ({ text }) => {
245
+ const current = readCurrent();
246
+ if (current === null) throw new Error("Surface content is unreadable");
247
+ writeFull(text + current);
248
+ };
249
+ handle.onTextAppend = ({ text }) => {
250
+ const current = readCurrent();
251
+ if (current === null) throw new Error("Surface content is unreadable");
252
+ writeFull(current + text);
253
+ };
254
+ handle.onTextPatch = ({ search_text, replacement_text }) => {
255
+ const current = readCurrent();
256
+ if (current === null) throw new Error("Surface content is unreadable");
257
+ const idx = current.indexOf(search_text);
258
+ if (idx === -1)
259
+ throw new Error("search_text not found in surface content");
260
+ writeFull(
261
+ current.slice(0, idx) + replacement_text + current.slice(idx + search_text.length)
262
+ );
263
+ };
264
+ }
265
+ return handle;
266
+ }
267
+
268
+ // src/engine/table-row-item.ts
269
+ import { TABLE_ROW_ITEM } from "@ai-matrx/alchemy/declare";
270
+ import { itemSources } from "@ai-matrx/alchemy/surface";
271
+ function resolveTableRowItem(target) {
272
+ const row = target?.closest("[data-row-id]");
273
+ const tableId = row?.closest("[data-matrx-table-id]")?.dataset.matrxTableId;
274
+ const rowId = row?.dataset.rowId;
275
+ if (!tableId || !rowId) return null;
276
+ const hit = itemSources.resolve({ containerId: tableId, level: "row", itemId: rowId });
277
+ const descriptor = hit?.host ?? null;
278
+ if (!hit || !descriptor) return null;
279
+ const { readValues: _raw, ...item } = hit.item;
280
+ return { item, menu: headedMenu(descriptor, target, row) };
281
+ }
282
+ function headedMenu(descriptor, target, row) {
283
+ const cell = target?.closest("td, [role='gridcell']");
284
+ const shown = shownWords(cell ?? row);
285
+ const ownHeading = descriptor.context[types_exports.CONTEXT_MENU_HEADING_KEY];
286
+ const recordName = ownHeading ? "" : rowRecordName(row);
287
+ const context = {
288
+ ...descriptor.context,
289
+ ...shown ? { content: shown } : {},
290
+ ...recordName ? { [types_exports.CONTEXT_MENU_HEADING_KEY]: { label: "Row", text: recordName } } : {}
291
+ };
292
+ const headed = ownHeading?.text ?? recordName;
293
+ const same = (label) => typeof headed === "string" && !!label && label.trim().toLowerCase() === headed.trim().toLowerCase();
294
+ const extraSections = descriptor.extraSections.map(
295
+ (section) => section.primary && same(section.label) ? { ...section, label: "Row" } : section
296
+ );
297
+ const changedSections = extraSections.some((section, i) => section !== descriptor.extraSections[i]);
298
+ return shown || recordName || changedSections ? { ...descriptor, context, extraSections } : descriptor;
299
+ }
300
+ function rowRecordName(row) {
301
+ if (!row) return "";
302
+ for (const cell of Array.from(row.querySelectorAll("td[data-matrx-table-column-id], [role='gridcell'][data-matrx-table-column-id]"))) {
303
+ const words = primaryLine(cell);
304
+ if (words) return words.length > 120 ? `${words.slice(0, 119)}\u2026` : words;
305
+ }
306
+ return "";
307
+ }
308
+ var LINE_BREAKING = "div, p, li, br, tr, h1, h2, h3, h4, h5, h6, section, article, header, footer, dd, dt";
309
+ function primaryLine(element) {
310
+ const copy = element.cloneNode(true);
311
+ copy.querySelectorAll("button, [aria-hidden='true'], input, textarea, select").forEach((node) => node.remove());
312
+ copy.querySelectorAll(LINE_BREAKING).forEach((node) => node.before("\n"));
313
+ const lines = (copy.textContent ?? "").split("\n").map((line) => line.replace(/\s+/g, " ").trim()).filter((line) => line && line !== "\u2014");
314
+ return lines[0] ?? "";
315
+ }
316
+ function shownWords(element) {
317
+ if (!element) return "";
318
+ const cells = element.querySelectorAll("td, [role='gridcell']");
319
+ const text = cells.length > 1 ? Array.from(cells).map(primaryLine).filter(Boolean).join(" \xB7 ") : primaryLine(element);
320
+ return text.length > 200 ? `${text.slice(0, 199)}\u2026` : text;
321
+ }
322
+
323
+ // src/engine/record-menu-registry.ts
324
+ var RECORD_MENU_ATTR = "data-record-menu";
325
+ var registry = /* @__PURE__ */ new Map();
326
+ var revision = 0;
327
+ var listeners = /* @__PURE__ */ new Set();
328
+ function subscribeRecordMenus(listener) {
329
+ listeners.add(listener);
330
+ return () => {
331
+ listeners.delete(listener);
332
+ };
333
+ }
334
+ function recordMenusRevision() {
335
+ return revision;
336
+ }
337
+ function resolveRecordMenu(target) {
338
+ const key = target?.closest?.(`[${RECORD_MENU_ATTR}]`)?.getAttribute(RECORD_MENU_ATTR);
339
+ if (!key) return null;
340
+ return registry.get(key)?.() ?? null;
341
+ }
342
+
343
+ // src/engine/utils/join-extra-sections.ts
344
+ function isRow(item) {
345
+ return item.kind !== "separator";
346
+ }
347
+ function tidySeparators(items) {
348
+ const out = [];
349
+ for (const item of items) {
350
+ if (!isRow(item) && (out.length === 0 || !isRow(out[out.length - 1]))) continue;
351
+ out.push(item);
352
+ }
353
+ while (out.length > 0 && !isRow(out[out.length - 1])) out.pop();
354
+ return out;
355
+ }
356
+ function joinExtraSections(...sources) {
357
+ const present = sources.filter((s) => Array.isArray(s));
358
+ if (present.length === 0) return void 0;
359
+ if (present.length === 1) return present[0];
360
+ const taken = /* @__PURE__ */ new Set();
361
+ const out = [];
362
+ for (const source of present) {
363
+ const ownIds = /* @__PURE__ */ new Set();
364
+ for (const section of source) {
365
+ const kept = section.items.filter((item) => !isRow(item) || !taken.has(item.id));
366
+ for (const item of kept) if (isRow(item)) ownIds.add(item.id);
367
+ if (kept.length === section.items.length) {
368
+ out.push(section);
369
+ continue;
370
+ }
371
+ const items = tidySeparators(kept);
372
+ if (items.some(isRow)) out.push({ ...section, items });
373
+ }
374
+ for (const id of ownIds) taken.add(id);
375
+ }
376
+ return out;
377
+ }
378
+
379
+ // src/engine/region-trigger-attrs.ts
380
+ var CONTEXT_REGION_TRIGGER_ATTRS = {
381
+ "data-alchemy-trigger": "context"
382
+ };
383
+
384
+ // src/engine/utils/table-at-target.ts
385
+ import { shapeTextOfNode } from "@ai-matrx/records-ui/table-shape";
386
+ function tableTextAtTarget(target) {
387
+ const table = target?.closest("table");
388
+ if (!table) return null;
389
+ const text = shapeTextOfNode(table.cloneNode(true));
390
+ return text.trim() ? text : null;
391
+ }
392
+
393
+ // src/engine/utils/key-combo.ts
394
+ var MOD_ALIASES = {
395
+ alt: "alt",
396
+ option: "alt",
397
+ opt: "alt",
398
+ "\u2325": "alt",
399
+ shift: "shift",
400
+ "\u21E7": "shift",
401
+ ctrl: "ctrl",
402
+ control: "ctrl",
403
+ "\u2303": "ctrl",
404
+ cmd: "meta",
405
+ command: "meta",
406
+ meta: "meta",
407
+ "\u2318": "meta",
408
+ win: "meta",
409
+ mod: "mod"
410
+ };
411
+ function isApplePlatform() {
412
+ if (typeof navigator === "undefined") return false;
413
+ return /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || "");
414
+ }
415
+ function parseKeyCombo(raw) {
416
+ if (!raw) return null;
417
+ const parts = raw.split(/\s*\+\s*/).map((p) => p.trim()).filter(Boolean);
418
+ if (parts.length < 2) return null;
419
+ const combo = { alt: false, shift: false, ctrl: false, meta: false, code: null, key: null };
420
+ const last = parts[parts.length - 1];
421
+ for (const part of parts.slice(0, -1)) {
422
+ const mod = MOD_ALIASES[part.toLowerCase()];
423
+ if (!mod) return null;
424
+ if (mod === "mod") combo[isApplePlatform() ? "meta" : "ctrl"] = true;
425
+ else combo[mod] = true;
426
+ }
427
+ if (/^[a-z]$/i.test(last)) combo.code = `Key${last.toUpperCase()}`;
428
+ else if (/^[0-9]$/.test(last)) combo.code = `Digit${last}`;
429
+ else combo.key = last.toLowerCase();
430
+ if (!combo.alt && !combo.ctrl && !combo.meta) return null;
431
+ if ((combo.ctrl || combo.meta) && combo.shift && !combo.alt && combo.code === "KeyK") return null;
432
+ return combo;
433
+ }
434
+ function eventMatchesCombo(e, combo) {
435
+ if (!!e.altKey !== combo.alt || !!e.shiftKey !== combo.shift) return false;
436
+ if (!!e.ctrlKey !== combo.ctrl || !!e.metaKey !== combo.meta) return false;
437
+ if (combo.code) return e.code === combo.code;
438
+ return !!combo.key && (e.key ?? "").toLowerCase() === combo.key;
439
+ }
440
+ function findComboMatch(e, items, comboOf) {
441
+ if (!e.altKey && !e.ctrlKey && !e.metaKey) return null;
442
+ for (const item of items) {
443
+ const combo = parseKeyCombo(comboOf(item));
444
+ if (combo && eventMatchesCombo(e, combo)) return item;
445
+ }
446
+ return null;
447
+ }
448
+
449
+ // src/engine/ContextMenuV3.tsx
450
+ import { ReactReduxContext } from "react-redux";
451
+
452
+ // src/engine/host/port.ts
453
+ import { confirm as kitConfirm } from "@ai-matrx/kit/confirm-opener";
454
+ function unwired(name) {
455
+ const announced = slot().announced;
456
+ if (announced.has(name)) return;
457
+ announced.add(name);
458
+ console.warn(`[context-menu] host port "${name}" is not wired \u2014 configureContextMenuHost({ ${name} }).`);
459
+ }
460
+ function unwiredFn(name) {
461
+ return () => unwired(name);
462
+ }
463
+ var DEFAULT_QUICK_ACTIONS = {
464
+ openChatWindow: unwiredFn("quickActions.openChatWindow"),
465
+ openQuickChat: unwiredFn("quickActions.openQuickChat"),
466
+ openQuickData: unwiredFn("quickActions.openQuickData"),
467
+ openQuickFiles: unwiredFn("quickActions.openQuickFiles"),
468
+ openQuickNotes: unwiredFn("quickActions.openQuickNotes"),
469
+ openQuickTasks: unwiredFn("quickActions.openQuickTasks"),
470
+ openVoicePad: unwiredFn("quickActions.openVoicePad")
471
+ };
472
+ var DEFAULT_BINDINGS = {
473
+ isAdminDebugger: false,
474
+ hasAdminPower: false,
475
+ isDebugMode: false,
476
+ isAdminIndicatorOpen: false,
477
+ hasCompareBase: false,
478
+ activeScopeIds: null,
479
+ toggleDebugMode: unwiredFn("useBindings.toggleDebugMode"),
480
+ toggleAdminIndicator: unwiredFn("useBindings.toggleAdminIndicator"),
481
+ setCompareBase: unwiredFn("useBindings.setCompareBase"),
482
+ openCompareWithBase: async () => {
483
+ unwired("useBindings.openCompareWithBase");
484
+ return false;
485
+ },
486
+ openListenSummaryWindow: unwiredFn("useBindings.openListenSummaryWindow"),
487
+ openDiffWindow: unwiredFn("useBindings.openDiffWindow"),
488
+ openFindReplace: unwiredFn("useBindings.openFindReplace"),
489
+ openReferencePicker: unwiredFn("useBindings.openReferencePicker"),
490
+ openContextAssignment: unwiredFn("useBindings.openContextAssignment"),
491
+ openLinkRecordSheet: unwiredFn("useBindings.openLinkRecordSheet"),
492
+ openShareModalWindow: unwiredFn("useBindings.openShareModalWindow"),
493
+ openStateViewer: unwiredFn("useBindings.openStateViewer"),
494
+ openSurfaceInspector: unwiredFn("useBindings.openSurfaceInspector"),
495
+ openSurfaceContextWindow: unwiredFn("useBindings.openSurfaceContextWindow"),
496
+ openBindAgent: unwiredFn("useBindings.openBindAgent"),
497
+ quickActions: DEFAULT_QUICK_ACTIONS
498
+ };
499
+ var EMPTY_RENDER_DEFINITIONS = [];
500
+ var DEFAULT_CONTEXT_MENU_HOST = {
501
+ useBindings: () => DEFAULT_BINDINGS,
502
+ getSurfaceManifest: () => void 0,
503
+ selectRenderDefinitions: () => EMPTY_RENDER_DEFINITIONS,
504
+ confirm: kitConfirm,
505
+ showManualCopy: () => unwired("showManualCopy"),
506
+ listeningHomeSurface: "matrx-user/assistant-message",
507
+ DocumentAgentReview: null
508
+ };
509
+ var SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.context-menu.host");
510
+ function slot() {
511
+ const g = globalThis;
512
+ let s = g[SLOT];
513
+ if (!s) {
514
+ s = { host: DEFAULT_CONTEXT_MENU_HOST, announced: /* @__PURE__ */ new Set() };
515
+ g[SLOT] = s;
516
+ }
517
+ return s;
518
+ }
519
+ function getContextMenuHost() {
520
+ return slot().host;
521
+ }
522
+ function useContextMenuHostBindings() {
523
+ return getContextMenuHost().useBindings();
524
+ }
525
+
526
+ // src/engine/ContextMenuV3.tsx
527
+ import { selectAllShortcutsArray } from "@ai-matrx/chat/agents/redux/agent-shortcuts/selectors";
528
+ import { fetchUnifiedMenu } from "@ai-matrx/chat/agents/redux/agent-shortcuts/thunks";
529
+ import { jsx, jsxs } from "react/jsx-runtime";
530
+ var NON_TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
531
+ "button",
532
+ "checkbox",
533
+ "color",
534
+ "file",
535
+ "image",
536
+ "radio",
537
+ "range",
538
+ "reset",
539
+ "submit"
540
+ ]);
541
+ var NATIVE_TEXT_YIELD = /* @__PURE__ */ new WeakMap();
542
+ function yieldsToNativeTextMenu(target) {
543
+ if (!(target instanceof HTMLElement)) return false;
544
+ if (target.isContentEditable) return true;
545
+ if (target instanceof HTMLTextAreaElement)
546
+ return !target.readOnly && !target.disabled;
547
+ if (target instanceof HTMLInputElement)
548
+ return !NON_TEXT_INPUT_TYPES.has(target.type) && !target.readOnly && !target.disabled;
549
+ return false;
550
+ }
551
+ function reachedThroughPortal(e) {
552
+ const { target, currentTarget } = e;
553
+ return target instanceof Node && currentTarget instanceof Node && !currentTarget.contains(target);
554
+ }
555
+ function isInsideOpenMenu(target) {
556
+ return target instanceof Element && Boolean(target.closest('[data-alchemy-layout], [role="menu"]'));
557
+ }
558
+ var MENU_CONTENT_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.context-menu.menu-content");
559
+ function menuContentSlot() {
560
+ const g = globalThis;
561
+ return g[MENU_CONTENT_SLOT] ??= { module: null, warm: null };
562
+ }
563
+ function loadMenuContent() {
564
+ return import("@ai-matrx/context-menu/menu-content").then((m) => {
565
+ menuContentSlot().module = m;
566
+ return m;
567
+ });
568
+ }
569
+ function useMenuContentModule() {
570
+ const [mod, setMod] = useState(() => menuContentSlot().module);
571
+ useEffect(() => {
572
+ if (mod) return;
573
+ let live = true;
574
+ loadMenuContent().then(
575
+ (m) => {
576
+ if (live) setMod(m);
577
+ },
578
+ (error) => console.error("[ContextMenuV3] could not load the menu", error)
579
+ );
580
+ return () => {
581
+ live = false;
582
+ };
583
+ }, [mod]);
584
+ return mod;
585
+ }
586
+ function AlchemyMenuContent(props) {
587
+ const mod = useMenuContentModule();
588
+ return mod ? /* @__PURE__ */ jsx(mod.AlchemyMenuContent, { ...props }) : null;
589
+ }
590
+ function warmMenuContent() {
591
+ const slot2 = menuContentSlot();
592
+ if (!slot2.warm) {
593
+ slot2.warm = loadMenuContent().catch((error) => {
594
+ slot2.warm = null;
595
+ console.error("[ContextMenuV3] could not warm the menu; the first open will load it", error);
596
+ });
597
+ }
598
+ return menuContentSlot().warm ?? Promise.resolve();
599
+ }
600
+ function useWarmMenuContentWhenIdle() {
601
+ useEffect(() => {
602
+ if (menuContentSlot().warm) return;
603
+ if (typeof window.requestIdleCallback === "function") {
604
+ const id2 = window.requestIdleCallback(() => void warmMenuContent(), { timeout: 4e3 });
605
+ return () => window.cancelIdleCallback(id2);
606
+ }
607
+ const id = window.setTimeout(() => void warmMenuContent(), 1500);
608
+ return () => window.clearTimeout(id);
609
+ }, []);
610
+ }
611
+ function ShortcutKeyRunner(props) {
612
+ const mod = useMenuContentModule();
613
+ return mod ? /* @__PURE__ */ jsx(mod.ShortcutKeyRunner, { ...props }) : null;
614
+ }
615
+ var KEY_RUN_CLAIMED = "__alchemyShortcutKeyClaimed";
616
+ function DocumentAgentReviewHost(props) {
617
+ const Review = getContextMenuHost().DocumentAgentReview;
618
+ React.useEffect(() => {
619
+ if (!Review) {
620
+ console.warn('[context-menu] host port "DocumentAgentReview" is not wired \u2014 the review request was dropped.');
621
+ props.onClose();
622
+ }
623
+ }, [Review, props]);
624
+ return Review ? /* @__PURE__ */ jsx(Review, { ...props }) : null;
625
+ }
626
+ var PALETTE_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.context-menu.palette");
627
+ function paletteRegistry() {
628
+ const g = globalThis;
629
+ let reg = g[PALETTE_KEY];
630
+ if (!reg) {
631
+ reg = { openers: /* @__PURE__ */ new Map(), pointer: null, focus: null, pointerDown: null, seq: 0, installed: false };
632
+ g[PALETTE_KEY] = reg;
633
+ }
634
+ if (!reg.installed && typeof document !== "undefined") {
635
+ reg.installed = true;
636
+ const r = reg;
637
+ document.addEventListener("keydown", (e) => {
638
+ if (e.defaultPrevented) return;
639
+ if (!(e.metaKey || e.ctrlKey) || !e.shiftKey || e.key.toLowerCase() !== "k") return;
640
+ const target = paletteTarget(r);
641
+ const open = target ? r.openers.get(target.id) : void 0;
642
+ if (!target || !open) return;
643
+ e.preventDefault();
644
+ open(target.element);
645
+ });
646
+ }
647
+ return reg;
648
+ }
649
+ function paletteTarget(r) {
650
+ const live = (c) => c && c.element.isConnected && r.openers.has(c.id) ? c : null;
651
+ const pointer = live(r.pointer);
652
+ const focus = live(r.focus);
653
+ if (!focus) return pointer;
654
+ if (!pointer) return focus;
655
+ if (focus.seq < pointer.seq) return pointer;
656
+ const down = r.pointerDown;
657
+ const causedByPress = !!down && focus.element.contains(down.element) && focus.at - down.at < 1e3;
658
+ return causedByPress ? pointer : focus;
659
+ }
660
+ var CLAIMED = "__alchemyPaletteClaimed";
661
+ function ContextMenuV3({
662
+ children,
663
+ sourceFeature,
664
+ surfaceName,
665
+ menuVersion = 1,
666
+ getApplicationScope,
667
+ contextData = {},
668
+ resolveContextOnOpen,
669
+ contentSource,
670
+ entity,
671
+ excludedRichActions,
672
+ recordActionsOnly = false,
673
+ subjectFold,
674
+ extraRichActions,
675
+ richDocCtxExtras,
676
+ placementMode,
677
+ extraSections,
678
+ resolveExtraSectionsOnOpen,
679
+ onUndo,
680
+ onRedo,
681
+ canUndo = false,
682
+ canRedo = false,
683
+ undoHint,
684
+ redoHint,
685
+ onViewHistory,
686
+ hasHistory = false,
687
+ scope = "global",
688
+ scopeId = null,
689
+ className,
690
+ menuLayout = types_exports.DEFAULT_MENU_LAYOUT,
691
+ menuDensity = types_exports.DEFAULT_MENU_DENSITY,
692
+ suppressed = false,
693
+ onMenuOpenChange,
694
+ onCloseAutoFocus,
695
+ isEditable,
696
+ editorId,
697
+ getTextarea,
698
+ insertAtCaret,
699
+ onContentInserted,
700
+ onTextReplace,
701
+ onTextInsertBefore,
702
+ onTextInsertAfter,
703
+ onSave,
704
+ onDelete
705
+ }) {
706
+ const [selectedText, setSelectedText] = useState("");
707
+ const [selectionRange, setSelectionRange] = useState(
708
+ null
709
+ );
710
+ const [fallbackContent, setFallbackContent] = useState("");
711
+ const [menuOpen, setMenuOpen] = useState(false);
712
+ const [textAgentReview, setTextAgentReview] = useState(null);
713
+ const requestTextAgentReview = useCallback(
714
+ (actionId, ctx) => setTextAgentReview({ actionId, ctx }),
715
+ []
716
+ );
717
+ const [dropdownOpen, setDropdownOpen] = useState(false);
718
+ const isMobile = useIsMobile();
719
+ const [sheetOpen, setSheetOpen] = useState(false);
720
+ const [paletteOpen, setPaletteOpen] = useState(false);
721
+ const [keyRun, setKeyRun] = useState(null);
722
+ const store = React.useContext(ReactReduxContext)?.store;
723
+ const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 });
724
+ const [openSeq, setOpenSeq] = useState(0);
725
+ useWarmMenuContentWhenIdle();
726
+ const capturedSelection = useRef(null);
727
+ const selectionLocked = useRef(false);
728
+ const lastMousePos = useRef(null);
729
+ const selectionOwnerRef = useRef(null);
730
+ const [selectionOwner, setSelectionOwnerState] = useState(null);
731
+ const setSelectionOwner = useCallback((node) => {
732
+ selectionOwnerRef.current = node;
733
+ setSelectionOwnerState(node);
734
+ }, []);
735
+ const longPressTimer = useRef(null);
736
+ const touchStart = useRef(null);
737
+ const [openTarget, setOpenTarget] = useState(null);
738
+ const recordRevision = useSyncExternalStore(
739
+ subscribeRecordMenus,
740
+ recordMenusRevision,
741
+ recordMenusRevision
742
+ );
743
+ const [resolvedContext, setResolvedContext] = useState(null);
744
+ const [resolvedItem, setResolvedItem] = useState(null);
745
+ const [resolvedExtraSections, setResolvedExtraSections] = useState(void 0);
746
+ const [rowJoinsSurfaceSections, setRowJoinsSurfaceSections] = useState(false);
747
+ const [sniffedEntity, setSniffedEntity] = useState(null);
748
+ const skipSelectionRestoreRef = useRef(false);
749
+ const widgetHandle = isEditable ? buildEditableWidgetHandle({
750
+ getTextarea,
751
+ onTextReplace,
752
+ onTextInsertBefore,
753
+ onTextInsertAfter,
754
+ getApplicationScope
755
+ }) : null;
756
+ const widgetHandleId = useOptionalWidgetHandle(widgetHandle);
757
+ const getEffectiveContextData = () => mergeResolvedContextData(
758
+ contextData,
759
+ resolvedContext
760
+ );
761
+ const surfaceSpokeAboutEntity = !!resolvedContext && types_exports.CONTEXT_MENU_ENTITY_KEY in resolvedContext;
762
+ const effectiveEntity = surfaceSpokeAboutEntity ? resolveEffectiveEntity(entity, resolvedContext) : sniffedEntity ?? entity;
763
+ useEffect(() => {
764
+ const handleSelection = () => {
765
+ if (selectionLocked.current) return;
766
+ const owner = selectionOwnerRef.current;
767
+ const selection = window.getSelection();
768
+ const anchor = selection?.anchorNode ?? null;
769
+ const active = document.activeElement;
770
+ const ownsSelection = owner != null && (anchor != null && owner.contains(anchor) || (active instanceof HTMLTextAreaElement || active instanceof HTMLInputElement) && owner.contains(active));
771
+ if (!ownsSelection) {
772
+ setSelectedText("");
773
+ return;
774
+ }
775
+ const text = !selection || selection.isCollapsed ? "" : selection.toString().trim();
776
+ setSelectedText(text);
777
+ };
778
+ document.addEventListener("selectionchange", handleSelection);
779
+ return () => document.removeEventListener("selectionchange", handleSelection);
780
+ }, []);
781
+ useEffect(() => {
782
+ const handleMouseMove = (e) => {
783
+ lastMousePos.current = { x: e.clientX, y: e.clientY };
784
+ };
785
+ document.addEventListener("mousemove", handleMouseMove, { passive: true });
786
+ return () => document.removeEventListener("mousemove", handleMouseMove);
787
+ }, []);
788
+ const handleMouseDown = (e) => {
789
+ if (suppressed) return;
790
+ if (reachedThroughPortal(e)) return;
791
+ if (e.button !== 2) return;
792
+ if (!isEditable && yieldsToNativeTextMenu(e.target)) return;
793
+ const target = e.target;
794
+ resolvePerTargetContext(target);
795
+ selectionLocked.current = true;
796
+ if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) {
797
+ const captured = (0, selection_tracking_exports.captureTextareaSelection)(target);
798
+ capturedSelection.current = captured;
799
+ setSelectedText(captured.text);
800
+ } else {
801
+ const captured = (0, selection_tracking_exports.captureDomSelection)();
802
+ capturedSelection.current = captured;
803
+ setSelectedText(captured.text);
804
+ }
805
+ };
806
+ const resolvePerTargetContext = (target) => {
807
+ const rowHit = resolveTableRowItem(target);
808
+ const rowMenu = rowHit?.menu ?? null;
809
+ setResolvedItem(rowHit?.item ?? null);
810
+ const answered = joinRowAndSurfaceContext(
811
+ rowMenu?.context,
812
+ resolveContextOnOpen ? resolveContextOnOpen(target) : null
813
+ );
814
+ setOpenTarget(target);
815
+ setResolvedContext(answered);
816
+ const surfaceSections = resolveExtraSectionsOnOpen?.(target);
817
+ const ownSections = joinExtraSections(rowMenu?.extraSections, surfaceSections);
818
+ setRowJoinsSurfaceSections(!!rowMenu && !surfaceSections);
819
+ const record = resolveRecordMenu(target);
820
+ setResolvedExtraSections(
821
+ record?.extraSections.length ? joinExtraSections(ownSections ?? extraSections ?? [], record.extraSections) : ownSections
822
+ );
823
+ setSniffedEntity(sniffEntityFromDom(target) ?? (entity ? null : record?.entity ?? null));
824
+ };
825
+ const captureContext = (target, containerEl) => {
826
+ resolvePerTargetContext(target);
827
+ let captured = capturedSelection.current;
828
+ const scopeEl = target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement ? target : target.closest('[contenteditable="true"], [contenteditable=""]') ?? containerEl;
829
+ if (captured?.range && !scopeEl.contains(captured.range.commonAncestorContainer)) {
830
+ captured = null;
831
+ }
832
+ if (!captured || !captured.text) {
833
+ captured = target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement ? (0, selection_tracking_exports.captureTextareaSelection)(target) : (0, selection_tracking_exports.captureDomSelection)();
834
+ capturedSelection.current = captured;
835
+ selectionLocked.current = true;
836
+ }
837
+ if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) {
838
+ const remembered = captured?.editable && captured.editable.element === target && captured.editable.start !== captured.editable.end ? captured.editable : null;
839
+ const { start, end } = remembered ?? (0, selection_tracking_exports.getEditableSelectionOffsets)(target);
840
+ if (remembered) {
841
+ const live = (0, selection_tracking_exports.getEditableSelectionOffsets)(target);
842
+ if (live.start !== start || live.end !== end) {
843
+ try {
844
+ target.setSelectionRange(start, end);
845
+ } catch {
846
+ }
847
+ }
848
+ }
849
+ setSelectedText(captured?.text || "");
850
+ setSelectionRange({
851
+ type: "editable",
852
+ element: target,
853
+ start,
854
+ end,
855
+ range: null,
856
+ containerElement: null
857
+ });
858
+ setFallbackContent(target.value ?? "");
859
+ } else {
860
+ let containerElement = containerEl;
861
+ if (!containerElement.hasAttribute("data-alchemy-trigger")) {
862
+ const trigger = containerElement.querySelector(
863
+ "[data-alchemy-trigger]"
864
+ );
865
+ if (trigger instanceof HTMLElement) containerElement = trigger;
866
+ }
867
+ setSelectedText(captured?.text || "");
868
+ setSelectionRange({
869
+ type: "non-editable",
870
+ // A contenteditable FIELD the right-click landed in (a rich or chip
871
+ // editor): recorded so the menu header can name the field instead of
872
+ // dumping its raw text. Every reader of `.element` gates on
873
+ // `type === "editable"`, so this changes nothing else.
874
+ element: target.closest('[contenteditable="true"], [contenteditable=""]'),
875
+ start: 0,
876
+ end: 0,
877
+ range: captured?.range || null,
878
+ containerElement
879
+ });
880
+ setFallbackContent((0, selection_tracking_exports.extractElementText)(containerElement));
881
+ }
882
+ };
883
+ const handleContextMenu = (e) => {
884
+ if (suppressed) return;
885
+ if (e.defaultPrevented) return;
886
+ e.preventDefault();
887
+ captureContext(e.target, e.currentTarget);
888
+ setMenuPoint({ x: e.clientX, y: e.clientY });
889
+ setOpenSeq((n) => n + 1);
890
+ setMenuOpen(true);
891
+ onMenuOpenChange?.(true);
892
+ };
893
+ const handleMenuClose = () => {
894
+ setMenuOpen(false);
895
+ selectionLocked.current = false;
896
+ capturedSelection.current = null;
897
+ if (skipSelectionRestoreRef.current) {
898
+ skipSelectionRestoreRef.current = false;
899
+ return;
900
+ }
901
+ if (!selectionRange) return;
902
+ if (selectionRange.type === "editable") {
903
+ const { element, start, end } = selectionRange;
904
+ if (element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement) {
905
+ (0, selection_tracking_exports.restoreTextareaSelection)(element, start, end);
906
+ }
907
+ } else if (selectionRange.range) {
908
+ (0, selection_tracking_exports.restoreDomSelection)(selectionRange.range);
909
+ }
910
+ };
911
+ const handleDropdownClose = (open) => {
912
+ onMenuOpenChange?.(open);
913
+ setDropdownOpen(open);
914
+ if (!open) {
915
+ selectionLocked.current = false;
916
+ capturedSelection.current = null;
917
+ }
918
+ };
919
+ const openFromSelection = () => {
920
+ if (suppressed) return;
921
+ selectionLocked.current = true;
922
+ const sel = window.getSelection();
923
+ const container = sel && sel.rangeCount > 0 ? sel.getRangeAt(0).commonAncestorContainer.parentElement ?? null : null;
924
+ resolvePerTargetContext(container);
925
+ setFallbackContent((0, selection_tracking_exports.extractElementText)(container));
926
+ if (isMobile) {
927
+ setSelectedText(sel?.toString().trim() || selectedText);
928
+ setSelectionRange({
929
+ type: "non-editable",
930
+ element: null,
931
+ start: 0,
932
+ end: 0,
933
+ range: sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null,
934
+ containerElement: container
935
+ });
936
+ setOpenSeq((n) => n + 1);
937
+ setSheetOpen(true);
938
+ return;
939
+ }
940
+ const rect = (0, selection_tracking_exports.getSelectionRect)();
941
+ const at = rect ? { x: rect.left, y: rect.bottom + 4 } : lastMousePos.current ?? { x: 16, y: 16 };
942
+ setMenuPoint(at);
943
+ setOpenSeq((n) => n + 1);
944
+ setDropdownOpen(true);
945
+ };
946
+ const selectionHost = { kind: "context-menu-selection", open: openFromSelection };
947
+ useSelectionZone(selectionOwner, {
948
+ // A suppressed menu (the text is streaming) keeps the toolbar away until it settles.
949
+ suppress: suppressed,
950
+ host: { [CONTEXT_MENU_SELECTION_HOST_KEY]: selectionHost }
951
+ });
952
+ const clearLongPress = () => {
953
+ if (longPressTimer.current !== null) {
954
+ clearTimeout(longPressTimer.current);
955
+ longPressTimer.current = null;
956
+ }
957
+ };
958
+ const handleTouchStart = (e) => {
959
+ if (suppressed) return;
960
+ if (reachedThroughPortal(e)) return;
961
+ if (!isEditable && yieldsToNativeTextMenu(e.target)) return;
962
+ e.stopPropagation();
963
+ const t = e.touches[0];
964
+ if (!t) return;
965
+ touchStart.current = {
966
+ x: t.clientX,
967
+ y: t.clientY,
968
+ target: e.target,
969
+ container: e.currentTarget
970
+ };
971
+ clearLongPress();
972
+ longPressTimer.current = window.setTimeout(() => {
973
+ const info = touchStart.current;
974
+ if (!info) return;
975
+ captureContext(info.target, info.container);
976
+ setOpenSeq((n) => n + 1);
977
+ setSheetOpen(true);
978
+ }, 480);
979
+ };
980
+ const handleTouchMove = (e) => {
981
+ const info = touchStart.current;
982
+ const t = e.touches[0];
983
+ if (!info || !t) return;
984
+ if (Math.abs(t.clientX - info.x) > 10 || Math.abs(t.clientY - info.y) > 10)
985
+ clearLongPress();
986
+ };
987
+ const handleTouchEnd = () => clearLongPress();
988
+ const handleSheetOpenChange = (open) => {
989
+ onMenuOpenChange?.(open);
990
+ setSheetOpen(open);
991
+ if (!open) {
992
+ selectionLocked.current = false;
993
+ capturedSelection.current = null;
994
+ }
995
+ };
996
+ const menuContentProps = compact({
997
+ sourceFeature,
998
+ surfaceName,
999
+ menuVersion,
1000
+ // A row descriptor owns this invocation's values. Preserve page-level live
1001
+ // values for empty space, but never let them overwrite the clicked row.
1002
+ getApplicationScope: resolvedContext ? () => ({ ...getApplicationScope?.() ?? {}, ...getEffectiveContextData() }) : getApplicationScope,
1003
+ contextData: getEffectiveContextData(),
1004
+ heading: readContextMenuHeading(resolvedContext) ?? recordHeadingAt(openTarget, recordRevision),
1005
+ item: resolvedItem,
1006
+ contentSource,
1007
+ entity: effectiveEntity,
1008
+ excludedRichActions,
1009
+ recordActionsOnly,
1010
+ subjectFold,
1011
+ // The hook adds it only where the source can be saved and no host supplied one.
1012
+ requestTextAgentReview,
1013
+ extraRichActions,
1014
+ // The table the click landed on rides to "Save to a table" beside the host's own extras.
1015
+ richDocCtxExtras: {
1016
+ ...richDocCtxExtras,
1017
+ callbacks: {
1018
+ ...richDocCtxExtras?.callbacks,
1019
+ tableAtTarget: () => tableTextAtTarget(openTarget)
1020
+ }
1021
+ // Spreading an optional bag leaves its Pick'd keys optional; the hook spreads it over defaults.
1022
+ },
1023
+ selectedText,
1024
+ selectionRange,
1025
+ fallbackContent,
1026
+ placementMode,
1027
+ scope,
1028
+ scopeId,
1029
+ extraSections: resolvedExtraSections ? rowJoinsSurfaceSections ? joinExtraSections(resolvedExtraSections, extraSections) : resolvedExtraSections : extraSections,
1030
+ menuLayout,
1031
+ menuDensity,
1032
+ isEditable,
1033
+ editorId,
1034
+ getTextarea,
1035
+ insertAtCaret,
1036
+ onContentInserted,
1037
+ onTextReplace,
1038
+ onTextInsertBefore,
1039
+ onTextInsertAfter,
1040
+ onSave,
1041
+ onDelete,
1042
+ onUndo,
1043
+ onRedo,
1044
+ canUndo,
1045
+ canRedo,
1046
+ undoHint,
1047
+ redoHint,
1048
+ onViewHistory,
1049
+ hasHistory,
1050
+ widgetHandleId,
1051
+ suppressSelectionRestore: () => {
1052
+ skipSelectionRestoreRef.current = true;
1053
+ }
1054
+ });
1055
+ const canSlotChildren = React.Children.count(children) === 1 && React.isValidElement(children) && typeof children.type === "string";
1056
+ const paletteIdRef = useRef(null);
1057
+ paletteIdRef.current ??= /* @__PURE__ */ Symbol("context-menu-surface");
1058
+ const openPaletteAt = (element) => {
1059
+ if (suppressed) return;
1060
+ const container = selectionOwnerRef.current ?? element;
1061
+ captureContext(element, container);
1062
+ setOpenSeq((n) => n + 1);
1063
+ setPaletteOpen(true);
1064
+ onMenuOpenChange?.(true);
1065
+ };
1066
+ const openPaletteRef = useRef(openPaletteAt);
1067
+ useEffect(() => {
1068
+ openPaletteRef.current = openPaletteAt;
1069
+ });
1070
+ useEffect(() => {
1071
+ const reg = paletteRegistry();
1072
+ const id = paletteIdRef.current;
1073
+ reg.openers.set(id, (el) => openPaletteRef.current(el));
1074
+ return () => {
1075
+ reg.openers.delete(id);
1076
+ if (reg.pointer?.id === id) reg.pointer = null;
1077
+ if (reg.focus?.id === id) reg.focus = null;
1078
+ };
1079
+ }, []);
1080
+ const claim = (kind) => (e) => {
1081
+ const native = e.nativeEvent;
1082
+ if (native[CLAIMED]) return;
1083
+ native[CLAIMED] = true;
1084
+ const reg = paletteRegistry();
1085
+ const now = Date.now();
1086
+ if (e.type === "pointerdown") reg.pointerDown = { element: e.target, at: now };
1087
+ reg.seq += 1;
1088
+ reg[kind] = { id: paletteIdRef.current, element: e.target, seq: reg.seq, at: now };
1089
+ };
1090
+ const mode = sheetOpen ? "sheet" : paletteOpen ? "palette" : menuOpen || dropdownOpen ? "context" : null;
1091
+ const closeActive = () => {
1092
+ if (sheetOpen) handleSheetOpenChange(false);
1093
+ else if (paletteOpen) {
1094
+ setPaletteOpen(false);
1095
+ onMenuOpenChange?.(false);
1096
+ handleMenuClose();
1097
+ } else if (dropdownOpen) handleDropdownClose(false);
1098
+ else {
1099
+ onMenuOpenChange?.(false);
1100
+ handleMenuClose();
1101
+ }
1102
+ };
1103
+ const handleShortcutKeyDown = (e) => {
1104
+ const native = e.nativeEvent;
1105
+ if (native[KEY_RUN_CLAIMED] || e.defaultPrevented || suppressed || e.repeat) return;
1106
+ if (reachedThroughPortal(e)) return;
1107
+ if (!store || !e.altKey && !e.ctrlKey && !e.metaKey) return;
1108
+ const matchOf = (event) => findComboMatch(
1109
+ event,
1110
+ selectAllShortcutsArray(store.getState()).filter((s) => s.isActive !== false),
1111
+ (s) => s.keyboardShortcut
1112
+ );
1113
+ const target = e.target;
1114
+ const owner = selectionOwnerRef.current ?? e.currentTarget;
1115
+ const run = (match2) => {
1116
+ captureContext(target, owner);
1117
+ setOpenSeq((n) => n + 1);
1118
+ setKeyRun({ id: match2.id, label: match2.label });
1119
+ };
1120
+ const loading = loadShortcuts();
1121
+ const match = matchOf(e.nativeEvent);
1122
+ if (!match) {
1123
+ if (loading) {
1124
+ const event = e.nativeEvent;
1125
+ void loading.then(() => {
1126
+ const late = matchOf(event);
1127
+ if (late) run(late);
1128
+ });
1129
+ }
1130
+ return;
1131
+ }
1132
+ native[KEY_RUN_CLAIMED] = true;
1133
+ e.preventDefault();
1134
+ e.stopPropagation();
1135
+ run(match);
1136
+ };
1137
+ const shortcutsLoad = useRef(null);
1138
+ const shortcutsLoaded = useRef(false);
1139
+ const loadShortcuts = () => {
1140
+ if (!store || !isEditable || shortcutsLoaded.current) return null;
1141
+ if (!shortcutsLoad.current) {
1142
+ shortcutsLoad.current = store.dispatch(fetchUnifiedMenu({ scope, scopeId })).catch(() => void 0).finally(() => {
1143
+ shortcutsLoaded.current = true;
1144
+ });
1145
+ }
1146
+ return shortcutsLoad.current;
1147
+ };
1148
+ const finishKeyRun = () => {
1149
+ setKeyRun(null);
1150
+ selectionLocked.current = false;
1151
+ capturedSelection.current = null;
1152
+ };
1153
+ const triggerProps = {
1154
+ ...CONTEXT_REGION_TRIGGER_ATTRS,
1155
+ // Radix-compatible open state, for styles that key on it.
1156
+ "data-state": mode ? "open" : "closed",
1157
+ // Lets a ⋯ beside (not inside) this content open THIS menu.
1158
+ "data-content-source": contentSource ? contentSourceKey(contentSource) : void 0,
1159
+ onContextMenuCapture: (e) => {
1160
+ if (reachedThroughPortal(e)) return;
1161
+ if (!isEditable && yieldsToNativeTextMenu(e.target))
1162
+ NATIVE_TEXT_YIELD.set(e.nativeEvent, e.currentTarget);
1163
+ },
1164
+ onContextMenu: (e) => {
1165
+ if (reachedThroughPortal(e)) {
1166
+ if (isInsideOpenMenu(e.target)) e.preventDefault();
1167
+ return;
1168
+ }
1169
+ const yielder = NATIVE_TEXT_YIELD.get(e.nativeEvent);
1170
+ if (yielder && (!isEditable || !yielder.contains(e.currentTarget))) return;
1171
+ if (isMobile) {
1172
+ if (suppressed) return;
1173
+ if (!isEditable && yieldsToNativeTextMenu(e.target)) return;
1174
+ e.stopPropagation();
1175
+ e.preventDefault();
1176
+ captureContext(e.target, e.currentTarget);
1177
+ setOpenSeq((n) => n + 1);
1178
+ setSheetOpen(true);
1179
+ return;
1180
+ }
1181
+ handleContextMenu(e);
1182
+ },
1183
+ onMouseDown: isMobile ? void 0 : handleMouseDown,
1184
+ // The palette key is answered by the ONE page listener (paletteTarget), so
1185
+ // a click that focused an OUTER surface cannot steal it from the inner one.
1186
+ onPointerOver: claim("pointer"),
1187
+ onPointerDown: claim("pointer"),
1188
+ onFocus: claim("focus"),
1189
+ onKeyDown: handleShortcutKeyDown,
1190
+ ...isMobile ? {
1191
+ onTouchStart: handleTouchStart,
1192
+ onTouchMove: handleTouchMove,
1193
+ onTouchEnd: handleTouchEnd,
1194
+ onTouchCancel: handleTouchEnd
1195
+ } : {}
1196
+ };
1197
+ return /* @__PURE__ */ jsx(MenuPresenceProvider, { value: true, children: /* @__PURE__ */ jsxs(RegistryMenuSourceProvider, { value: contentSource ?? null, children: [
1198
+ canSlotChildren ? /* @__PURE__ */ jsx(Slot, { ref: setSelectionOwner, ...triggerProps, children }) : /* @__PURE__ */ jsx("div", { ref: setSelectionOwner, style: { display: "contents" }, ...triggerProps, children }),
1199
+ mode ? /* @__PURE__ */ jsx(
1200
+ AlchemyMenuContent,
1201
+ {
1202
+ ...menuContentProps,
1203
+ mode,
1204
+ point: menuPoint,
1205
+ open: true,
1206
+ onOpenChange: (open) => {
1207
+ if (!open) closeActive();
1208
+ }
1209
+ },
1210
+ openSeq
1211
+ ) : null,
1212
+ keyRun && !mode ? /* @__PURE__ */ jsx(
1213
+ ShortcutKeyRunner,
1214
+ {
1215
+ ...menuContentProps,
1216
+ shortcutId: keyRun.id,
1217
+ shortcutLabel: keyRun.label,
1218
+ onDone: finishKeyRun
1219
+ },
1220
+ `key-run-${openSeq}`
1221
+ ) : null,
1222
+ textAgentReview ? /* @__PURE__ */ jsx(
1223
+ DocumentAgentReviewHost,
1224
+ {
1225
+ actionId: textAgentReview.actionId,
1226
+ ctx: textAgentReview.ctx,
1227
+ onClose: () => setTextAgentReview(null)
1228
+ }
1229
+ ) : null
1230
+ ] }) });
1231
+ }
1232
+ function recordHeadingAt(target, _revision) {
1233
+ return readContextMenuHeading({
1234
+ [types_exports.CONTEXT_MENU_HEADING_KEY]: resolveRecordMenu(target)?.heading ?? null
1235
+ });
1236
+ }
1237
+ function readContextMenuHeading(resolved) {
1238
+ if (!resolved || typeof resolved !== "object") return null;
1239
+ const heading = resolved[types_exports.CONTEXT_MENU_HEADING_KEY];
1240
+ if (!heading || typeof heading !== "object") return null;
1241
+ const { label, text } = heading;
1242
+ return typeof label === "string" && typeof text === "string" && label.trim() ? { label: label.trim(), text } : null;
1243
+ }
1244
+
1245
+ // src/engine/EditableContextMenu.tsx
1246
+ import { jsx as jsx2 } from "react/jsx-runtime";
1247
+ function EditableContextMenu(props) {
1248
+ return /* @__PURE__ */ jsx2(ContextMenuV3, { ...props, isEditable: true });
1249
+ }
1250
+
1251
+ // src/engine/NonEditableContextMenu.tsx
1252
+ import { jsx as jsx3 } from "react/jsx-runtime";
1253
+ function NonEditableContextMenu(props) {
1254
+ return /* @__PURE__ */ jsx3(ContextMenuV3, { ...props, isEditable: false });
1255
+ }
1256
+
1257
+ // src/engine/PageSurfaceMenu.tsx
1258
+ import { useActivePageSurface } from "@ai-matrx/chat/surfaces/runtime/useActivePageSurface";
1259
+ import { jsx as jsx4 } from "react/jsx-runtime";
1260
+ function PageSurfaceMenu({
1261
+ sourceFeature,
1262
+ children
1263
+ }) {
1264
+ const active = useActivePageSurface();
1265
+ return /* @__PURE__ */ jsx4(
1266
+ NonEditableContextMenu,
1267
+ {
1268
+ sourceFeature,
1269
+ ...compact({ surfaceName: active.surfaceName ?? void 0 }),
1270
+ getApplicationScope: () => {
1271
+ const scope = active.runtime?.getScope();
1272
+ return scope && !(scope instanceof Promise) ? scope : {};
1273
+ },
1274
+ contentSource: { type: "raw" },
1275
+ contextData: { content: "" },
1276
+ children
1277
+ }
1278
+ );
1279
+ }
1280
+
1281
+ // src/engine/regroup/RegroupContext.tsx
1282
+ import * as React2 from "react";
1283
+ import { AlchemyActionsProvider, useAlchemyActions } from "@ai-matrx/alchemy/react/host";
1284
+
1285
+ // src/engine/regroup/grouping.ts
1286
+ import {
1287
+ buildMenuModel,
1288
+ context as arrangeContext
1289
+ } from "@ai-matrx/alchemy/menu";
1290
+ var REGROUP_ID_PREFIX = "regroup:";
1291
+ function actionLabel(action, target) {
1292
+ try {
1293
+ return typeof action.label === "function" ? action.label(target) : action.label;
1294
+ } catch {
1295
+ return action.id;
1296
+ }
1297
+ }
1298
+ function isPageOwn(action) {
1299
+ const section = action.section;
1300
+ return Boolean(section && (section.primary || section.kind === "target"));
1301
+ }
1302
+ function labelOf(action, target) {
1303
+ if (typeof action.label === "string") return action.label;
1304
+ return target ? actionLabel(action, target) : "";
1305
+ }
1306
+ function ruleMatches(rule, action, target) {
1307
+ const w = rule.when;
1308
+ if (w.pageOwnLabels && isPageOwn(action)) {
1309
+ const label = labelOf(action, target);
1310
+ if (label && w.pageOwnLabels.some((p) => new RegExp(p, "i").test(label))) return true;
1311
+ }
1312
+ if (w.ids?.includes(action.id)) return true;
1313
+ if (w.idPrefixes?.some((p) => action.id.startsWith(p))) return true;
1314
+ if (w.categories?.includes(action.category)) return true;
1315
+ if (w.pageOwn && isPageOwn(action)) return true;
1316
+ return false;
1317
+ }
1318
+ function matchRule(grouping, action, target) {
1319
+ return grouping.rules.find((r) => ruleMatches(r, action, target)) ?? null;
1320
+ }
1321
+ function destinationFor(grouping, action, target) {
1322
+ return matchRule(grouping, action, target)?.to ?? grouping.fallback;
1323
+ }
1324
+ var sameName = (label) => label.toLowerCase().replace(/[….]+$/g, "").replace(/\s+/g, " ").trim();
1325
+ function drawnOrder(model) {
1326
+ const order = /* @__PURE__ */ new Map();
1327
+ const walk = (nodes) => {
1328
+ for (const n of nodes) {
1329
+ if (!order.has(n.id)) order.set(n.id, order.size);
1330
+ if (n.kind === "submenu") walk(n.children);
1331
+ }
1332
+ };
1333
+ for (const s of model.sections) walk(s.nodes);
1334
+ return order;
1335
+ }
1336
+ function currentModel(target, resolved) {
1337
+ return buildMenuModel(target, resolved, { headingPolicy: "inline" });
1338
+ }
1339
+ function regroupResolved(target, resolved, grouping, options) {
1340
+ const order = drawnOrder(currentModel(target, resolved));
1341
+ const pos = (r) => order.get(r.action.id) ?? Number.MAX_SAFE_INTEGER;
1342
+ const groupDef = new Map(grouping.groups.map((g) => [g.key, g]));
1343
+ const top = [];
1344
+ const home = /* @__PURE__ */ new Map();
1345
+ const wanted = /* @__PURE__ */ new Map();
1346
+ const present = new Map(resolved.map((r) => [r.action.id, r]));
1347
+ const twinOf = (r) => {
1348
+ const rule = matchRule(grouping, r.action, target);
1349
+ for (const id of rule?.mergeWithIds ?? []) {
1350
+ const twin = present.get(id);
1351
+ if (twin && twin !== r) return twin;
1352
+ }
1353
+ return null;
1354
+ };
1355
+ const mergedInto = /* @__PURE__ */ new Map();
1356
+ const pageVersionOf = /* @__PURE__ */ new Map();
1357
+ for (const r of resolved) {
1358
+ if (r.action.expand) continue;
1359
+ const twin = twinOf(r);
1360
+ if (!twin || pageVersionOf.has(twin.action.id)) continue;
1361
+ mergedInto.set(r.action.id, twin);
1362
+ pageVersionOf.set(twin.action.id, r);
1363
+ }
1364
+ const withPageHandler = (r) => {
1365
+ const page = pageVersionOf.get(r.action.id);
1366
+ if (!page) return r;
1367
+ const { href: _generalHref, ...general } = r.action;
1368
+ void _generalHref;
1369
+ const action = {
1370
+ ...general,
1371
+ run: page.action.run,
1372
+ ...page.action.href !== void 0 ? { href: page.action.href } : {}
1373
+ };
1374
+ return { ...r, action };
1375
+ };
1376
+ for (const original of resolved) {
1377
+ if (mergedInto.has(original.action.id)) continue;
1378
+ const r = withPageHandler(original);
1379
+ const to = destinationFor(grouping, r.action, target);
1380
+ if (to.kind === "group" && groupDef.has(to.key)) {
1381
+ if (r.eligibility.status !== "available") {
1382
+ top.push(r);
1383
+ home.set(r.action.id, { kind: "top", note: "greyed here, so it stays visible with its reason" });
1384
+ continue;
1385
+ }
1386
+ const list = wanted.get(to.key) ?? [];
1387
+ list.push(r);
1388
+ wanted.set(to.key, list);
1389
+ continue;
1390
+ }
1391
+ if (to.kind === "page-first") {
1392
+ const section = r.action.section;
1393
+ top.push(
1394
+ section ? { ...r, action: { ...r.action, section: { ...section, primary: true } } } : r
1395
+ );
1396
+ home.set(r.action.id, { kind: "page-first" });
1397
+ continue;
1398
+ }
1399
+ top.push(r);
1400
+ home.set(r.action.id, { kind: "top" });
1401
+ }
1402
+ const members = /* @__PURE__ */ new Map();
1403
+ const groupActions = [];
1404
+ for (const def of grouping.groups) {
1405
+ const list = (wanted.get(def.key) ?? []).slice().sort((a, b) => pos(a) - pos(b));
1406
+ if (list.length === 0) continue;
1407
+ if (list.length < grouping.minMembers) {
1408
+ for (const r of list) {
1409
+ top.push(r);
1410
+ home.set(r.action.id, { kind: "top", note: `the only row for ${def.label}` });
1411
+ }
1412
+ continue;
1413
+ }
1414
+ const kept = [];
1415
+ const byName = /* @__PURE__ */ new Map();
1416
+ for (const r of list) {
1417
+ const name = sameName(actionLabel(r.action, target));
1418
+ const first = options.mergeSameName ? byName.get(name) : void 0;
1419
+ if (first) {
1420
+ home.set(r.action.id, {
1421
+ kind: "merged",
1422
+ intoId: first.action.id,
1423
+ intoLabel: actionLabel(first.action, target),
1424
+ groupLabel: def.label
1425
+ });
1426
+ continue;
1427
+ }
1428
+ byName.set(name, r);
1429
+ kept.push(r);
1430
+ home.set(r.action.id, { kind: "group", key: def.key, label: def.label });
1431
+ }
1432
+ members.set(def.key, kept);
1433
+ const children = kept.map((r) => r.action);
1434
+ const submenu = {
1435
+ id: `${REGROUP_ID_PREFIX}${def.key}`,
1436
+ label: def.label,
1437
+ category: def.category,
1438
+ order: def.order,
1439
+ ...def.icon ? { icon: def.icon } : {},
1440
+ // Still loading at the source when any member is.
1441
+ pending: (t) => children.some((c) => Boolean(c.expand && c.pending?.(t))),
1442
+ eligible: () => ({ status: "available" }),
1443
+ expand: async () => children,
1444
+ run: () => void 0
1445
+ };
1446
+ groupActions.push({ action: submenu, eligibility: { status: "available" } });
1447
+ }
1448
+ for (const [id, twin] of mergedInto) {
1449
+ const at = home.get(twin.action.id);
1450
+ home.set(id, {
1451
+ kind: "merged",
1452
+ intoId: twin.action.id,
1453
+ intoLabel: actionLabel(twin.action, target),
1454
+ groupLabel: at?.kind === "group" ? at.label : at?.kind === "merged" ? at.groupLabel : "",
1455
+ pageActionKept: true
1456
+ });
1457
+ }
1458
+ return { resolved: [...top, ...groupActions], members, home };
1459
+ }
1460
+
1461
+ // src/engine/regroup/RegroupContext.tsx
1462
+ import { jsx as jsx5 } from "react/jsx-runtime";
1463
+ var MenuRegroupContext = React2.createContext(null);
1464
+ function MenuRegroupProvider({ value, children }) {
1465
+ return /* @__PURE__ */ jsx5(MenuRegroupContext.Provider, { value, children });
1466
+ }
1467
+ function regroupingRegistry(inner, first) {
1468
+ let inputs = first;
1469
+ const read = () => inputs;
1470
+ return {
1471
+ update(next) {
1472
+ inputs = next;
1473
+ },
1474
+ register: (provider) => inner.register(provider),
1475
+ providers: () => inner.providers(),
1476
+ subscribe: (listener) => inner.subscribe(listener),
1477
+ // By-id lookup (alchemy 0.12): regrouping changes how a menu is arranged, never which Action an id names.
1478
+ get: (id, target) => inner.get(id, target),
1479
+ async resolve(target, restrict) {
1480
+ const list = await inner.resolve(target, restrict);
1481
+ const { value, arrangement, close } = read();
1482
+ if (!value) return list;
1483
+ value.onResolved?.({ target, resolved: list, arrangement }, close);
1484
+ if (value.transform) return value.transform(target, list);
1485
+ if (!value.grouping) return list;
1486
+ return regroupResolved(target, list, value.grouping, { mergeSameName: value.mergeSameName }).resolved;
1487
+ }
1488
+ };
1489
+ }
1490
+ function RegroupBoundary({
1491
+ arrangement,
1492
+ close,
1493
+ children
1494
+ }) {
1495
+ const value = React2.useContext(MenuRegroupContext);
1496
+ const { registry: registry2, ports } = useAlchemyActions();
1497
+ const [view] = React2.useState(
1498
+ () => value ? regroupingRegistry(registry2, { value, arrangement, close }) : null
1499
+ );
1500
+ React2.useLayoutEffect(() => {
1501
+ view?.update({ value, arrangement, close });
1502
+ });
1503
+ if (!view) return children;
1504
+ return /* @__PURE__ */ jsx5(AlchemyActionsProvider, { ports, registry: view, children });
1505
+ }
1506
+
1507
+ // src/engine/item/ItemMenuHostBinding.tsx
1508
+ import { ItemMenuHostProvider } from "@ai-matrx/design-system/item";
1509
+
1510
+ // src/engine/item/ItemContextMenuV3.tsx
1511
+ import { useState as useState3 } from "react";
1512
+ import { resolveItemMenuConfig } from "@ai-matrx/design-system/item";
1513
+
1514
+ // src/engine/item/itemMenuToV3.ts
1515
+ import {
1516
+ isCheckbox,
1517
+ isLink,
1518
+ isSubmenu,
1519
+ runCommand as runItemCommand,
1520
+ runToggle
1521
+ } from "@ai-matrx/design-system/item";
1522
+ import { toast } from "@ai-matrx/chat/host/notify";
1523
+ var iconOf = (entry) => entry.icon;
1524
+ var toastPromise = (promise, messages) => {
1525
+ toast.promise(promise, messages);
1526
+ };
1527
+ function runCommand(entry) {
1528
+ runItemCommand(entry, toastPromise);
1529
+ }
1530
+ function entryToExtraItem(entry) {
1531
+ const description = entry.disabled && entry.disabledReason ? entry.disabledReason : entry.description;
1532
+ if (isSubmenu(entry)) {
1533
+ return compact({
1534
+ kind: "submenu",
1535
+ id: entry.id,
1536
+ label: entry.label,
1537
+ icon: iconOf(entry),
1538
+ disabled: entry.disabled,
1539
+ children: submenuChildren(entry.sections)
1540
+ });
1541
+ }
1542
+ if (isCheckbox(entry)) {
1543
+ return compact({
1544
+ kind: "checkbox",
1545
+ id: entry.id,
1546
+ label: entry.label,
1547
+ icon: iconOf(entry),
1548
+ description,
1549
+ checked: entry.checked,
1550
+ onCheckedChange: (next) => runToggle(entry, next),
1551
+ disabled: entry.disabled,
1552
+ hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase()
1553
+ });
1554
+ }
1555
+ if (isLink(entry)) {
1556
+ return compact({
1557
+ kind: "link",
1558
+ id: entry.id,
1559
+ label: entry.label,
1560
+ icon: iconOf(entry),
1561
+ description,
1562
+ href: entry.href,
1563
+ target: entry.target,
1564
+ disabled: entry.disabled
1565
+ });
1566
+ }
1567
+ return compact({
1568
+ kind: "item",
1569
+ id: entry.id,
1570
+ label: entry.label,
1571
+ icon: iconOf(entry),
1572
+ description,
1573
+ destructive: entry.tone === "destructive",
1574
+ disabled: entry.disabled,
1575
+ hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),
1576
+ onSelect: () => runCommand(entry)
1577
+ });
1578
+ }
1579
+ function submenuChildren(sections) {
1580
+ const visible = sections.map((s) => ({ ...s, items: s.items.filter((e) => !e.hidden) })).filter((s) => s.items.length > 0);
1581
+ const out = [];
1582
+ visible.forEach((section, i) => {
1583
+ if (i > 0)
1584
+ out.push({
1585
+ kind: "separator",
1586
+ id: `sep-${section.id ?? section.label ?? i}`
1587
+ });
1588
+ out.push(...section.items.map(entryToExtraItem));
1589
+ });
1590
+ return out;
1591
+ }
1592
+ function itemMenuConfigToExtraSections(config) {
1593
+ const sections = config.sections.map((s, i) => compact({
1594
+ id: String(s.id ?? s.label ?? `im-${i}`),
1595
+ label: s.label,
1596
+ anchor: "after-clipboard",
1597
+ items: s.items.filter((e) => !e.hidden).map(entryToExtraItem)
1598
+ })).filter((s) => s.items.length > 0);
1599
+ const title = config.header?.title;
1600
+ const first = sections[0];
1601
+ if (title && first && !first.label) {
1602
+ sections[0] = { ...first, label: title };
1603
+ }
1604
+ return sections;
1605
+ }
1606
+
1607
+ // src/engine/item/ItemContextMenuV3.tsx
1608
+ import { jsx as jsx6 } from "react/jsx-runtime";
1609
+ function ItemContextMenuV3({
1610
+ config,
1611
+ children,
1612
+ sourceFeature,
1613
+ surfaceName,
1614
+ entity,
1615
+ getApplicationScope,
1616
+ resolveItemOnOpen,
1617
+ onOpenChange,
1618
+ onCloseAutoFocus
1619
+ }) {
1620
+ const [resolved, setResolved] = useState3(null);
1621
+ return /* @__PURE__ */ jsx6(
1622
+ NonEditableContextMenu,
1623
+ {
1624
+ sourceFeature,
1625
+ ...compact({ surfaceName, entity, getApplicationScope }),
1626
+ resolveContextOnOpen: (target) => {
1627
+ const item = resolveItemOnOpen?.(target);
1628
+ setResolved(resolveItemMenuConfig(item?.config ?? config));
1629
+ const context = item?.context ?? null;
1630
+ if (entity?.title && !(context && context[types_exports.CONTEXT_MENU_HEADING_KEY])) {
1631
+ const type = String(entity.type ?? "Item").replace(/[_-]+/g, " ");
1632
+ return {
1633
+ ...context ?? {},
1634
+ [types_exports.CONTEXT_MENU_HEADING_KEY]: {
1635
+ label: type.charAt(0).toUpperCase() + type.slice(1),
1636
+ text: entity.title
1637
+ }
1638
+ };
1639
+ }
1640
+ return context;
1641
+ },
1642
+ extraSections: resolved ? itemMenuConfigToExtraSections(resolved) : [],
1643
+ ...compact({ onMenuOpenChange: onOpenChange, onCloseAutoFocus }),
1644
+ children
1645
+ }
1646
+ );
1647
+ }
1648
+
1649
+ // src/engine/item/ItemMenuHostBinding.tsx
1650
+ import { jsx as jsx7 } from "react/jsx-runtime";
1651
+ var HOST = {
1652
+ // The package's surface fields are open strings; every website row passes chat's closed registries.
1653
+ ContextMenu: ItemContextMenuV3,
1654
+ toastPromise
1655
+ };
1656
+ function ItemMenuHostBinding({ children }) {
1657
+ return /* @__PURE__ */ jsx7(ItemMenuHostProvider, { host: HOST, children });
1658
+ }
1659
+
1660
+ // src/engine/components/OpenSurfaceMenuButton.tsx
1661
+ import * as React3 from "react";
1662
+ import { MoreHorizontal } from "lucide-react";
1663
+ import { Button } from "@ai-matrx/design-system";
1664
+ import { cn } from "@ai-matrx/design-system/cn";
1665
+ import { jsx as jsx8 } from "react/jsx-runtime";
1666
+ function OpenSurfaceMenuButton({
1667
+ getSurface,
1668
+ label = "More actions",
1669
+ className
1670
+ }) {
1671
+ const buttonRef = React3.useRef(null);
1672
+ return /* @__PURE__ */ jsx8(
1673
+ Button,
1674
+ {
1675
+ ref: buttonRef,
1676
+ type: "button",
1677
+ variant: "ghost",
1678
+ size: "icon",
1679
+ className: cn("h-8 w-8 p-0", className),
1680
+ "aria-label": label,
1681
+ "aria-haspopup": "menu",
1682
+ onClick: () => {
1683
+ const surface = getSurface();
1684
+ const button = buttonRef.current;
1685
+ if (!surface || !button) return;
1686
+ const rect = button.getBoundingClientRect();
1687
+ surface.dispatchEvent(
1688
+ new MouseEvent("contextmenu", {
1689
+ bubbles: true,
1690
+ cancelable: true,
1691
+ view: window,
1692
+ button: 2,
1693
+ buttons: 2,
1694
+ clientX: rect.left + rect.width / 2,
1695
+ clientY: rect.bottom
1696
+ })
1697
+ );
1698
+ },
1699
+ children: /* @__PURE__ */ jsx8(MoreHorizontal, { className: "h-4 w-4" })
1700
+ }
1701
+ );
1702
+ }
1703
+
1704
+ // src/engine/utils/field-menu-header.ts
1705
+ function fieldLabelOf(element) {
1706
+ if (!element) return null;
1707
+ const clean = (s) => (s ?? "").replace(/\s+/g, " ").replace(/\s*\*$/, "").trim() || null;
1708
+ const aria = clean(element.getAttribute("aria-label"));
1709
+ if (aria) return aria;
1710
+ const labelledBy = element.getAttribute("aria-labelledby");
1711
+ if (labelledBy) {
1712
+ const text = labelledBy.split(/\s+/).map((id2) => element.ownerDocument?.getElementById(id2)?.textContent ?? "").join(" ");
1713
+ if (clean(text)) return clean(text);
1714
+ }
1715
+ const labels = element.labels;
1716
+ if (labels && labels.length > 0) {
1717
+ const text = clean(labels[0]?.textContent);
1718
+ if (text) return text;
1719
+ }
1720
+ const id = element.getAttribute("id");
1721
+ if (id && element.ownerDocument) {
1722
+ const label = element.ownerDocument.querySelector(`label[for="${CSS.escape(id)}"]`);
1723
+ if (clean(label?.textContent)) return clean(label?.textContent);
1724
+ }
1725
+ return null;
1726
+ }
1727
+
1728
+ // src/engine/utils/already-here.ts
1729
+ var SAVE_INTO_WHERE_YOU_ARE = [
1730
+ { sourceType: "note", surface: "matrx-user/notes", actionId: "save-to-notes" },
1731
+ { sourceType: "task", surface: "matrx-user/tasks", actionId: "save-to-task" }
1732
+ ];
1733
+ var OPEN_WHERE_YOU_ARE = [
1734
+ // The Feedback window's menus offered "Submit feedback" — reopening itself.
1735
+ { surface: "matrx-user/feedback", actionId: "submit-feedback" }
1736
+ ];
1737
+ var EDITOR_DOORS = ["edit", "open-fullscreen-editor"];
1738
+ function actionsAlreadyHere(where) {
1739
+ const out = [];
1740
+ for (const rule of SAVE_INTO_WHERE_YOU_ARE) {
1741
+ if (where.sourceType === rule.sourceType || where.surfaceName === rule.surface) out.push(rule.actionId);
1742
+ }
1743
+ for (const rule of OPEN_WHERE_YOU_ARE) {
1744
+ if (where.surfaceName === rule.surface) out.push(rule.actionId);
1745
+ }
1746
+ const ownPage = SAVE_INTO_WHERE_YOU_ARE.some(
1747
+ (rule) => where.sourceType === rule.sourceType || where.surfaceName === rule.surface
1748
+ );
1749
+ if (where.isEditable || ownPage) out.push(...EDITOR_DOORS);
1750
+ return [...new Set(out)];
1751
+ }
1752
+
1753
+ // src/engine/utils/editor-text-agent.ts
1754
+ function editorTextAgentCallbacks(callbacks, source, adapter, request) {
1755
+ if (callbacks?.onRequestTextAgentAction || !request) return callbacks;
1756
+ if (source.type === "raw" || source.readOnly || typeof adapter.edit !== "function") return callbacks;
1757
+ return { ...callbacks ?? {}, onRequestTextAgentAction: request };
1758
+ }
1759
+
1760
+ // src/engine/hooks/useContextMenuActions.ts
1761
+ import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
1762
+ import { copyText as kitCopyText, readText as kitReadText } from "@ai-matrx/kit/clipboard";
1763
+ import {
1764
+ AppWindow,
1765
+ Braces,
1766
+ Building,
1767
+ ClipboardCopy,
1768
+ FileText,
1769
+ Info,
1770
+ Layers,
1771
+ Plus,
1772
+ Rocket,
1773
+ ShieldCheck,
1774
+ User
1775
+ } from "lucide-react";
1776
+ import { detectActiveSurface } from "@ai-matrx/chat/surfaces/utils/route-to-surface";
1777
+ import { getSurfaceDisplayLabel } from "@ai-matrx/chat/surfaces/utils/surface-display";
1778
+ import { getRelatedSurfaces } from "@ai-matrx/chat/surfaces/runtime/fetchRelatedSurfaces";
1779
+ import { getIconComponent } from "@ai-matrx/icons";
1780
+ import { useAppDispatch as useAppDispatch2, useAppSelector as useAppSelector2, useAppStore } from "@ai-matrx/chat/store/hooks";
1781
+ import { selectUserId } from "@ai-matrx/chat/host/identity";
1782
+
1783
+ // src/engine/utils/notify.ts
1784
+ import { toast as notify } from "@ai-matrx/chat/host/notify";
1785
+ function toast2(props) {
1786
+ const { title, description, variant, duration } = props;
1787
+ const message = title ?? description ?? "";
1788
+ const options = {
1789
+ ...title !== void 0 && description !== void 0 ? { description } : {},
1790
+ ...duration !== void 0 ? { duration } : {}
1791
+ };
1792
+ if (variant === "destructive") notify.error(message, options);
1793
+ else if (variant === "success") notify.success(message, options);
1794
+ else if (variant === "warning") notify.warning(message, options);
1795
+ else if (variant === "info") notify.info(message, options);
1796
+ else notify.message(message, options);
1797
+ }
1798
+
1799
+ // src/engine/hooks/useContextMenuActions.ts
1800
+ import { selectOrganizationId } from "@ai-matrx/chat/host/org";
1801
+ import { useAgentLauncher } from "@ai-matrx/chat/agents/hooks/useAgentLauncher";
1802
+ import { useSpeech } from "@ai-matrx/media/react";
1803
+ import { primeAudioOutput } from "@ai-matrx/media/speech";
1804
+ import { useSurfaceAgentRoles } from "@ai-matrx/chat/surfaces/hooks/useSurfaceConfig";
1805
+
1806
+ // src/engine/utils/editor-text-insertion.ts
1807
+ function insertTextAtCursor(editorId, text) {
1808
+ try {
1809
+ const editor = document.querySelector(
1810
+ `[data-editor-id="${editorId}"]`
1811
+ );
1812
+ if (!editor) {
1813
+ console.error("[editor-text-insertion] Editor not found:", editorId);
1814
+ return false;
1815
+ }
1816
+ editor.focus();
1817
+ const selection = window.getSelection();
1818
+ if (!selection) {
1819
+ console.error("[editor-text-insertion] Selection not available");
1820
+ return false;
1821
+ }
1822
+ let range;
1823
+ if (selection.rangeCount > 0) {
1824
+ range = selection.getRangeAt(0);
1825
+ } else {
1826
+ range = document.createRange();
1827
+ const lastNode = editor.lastChild;
1828
+ if (lastNode) {
1829
+ if (lastNode.nodeType === Node.TEXT_NODE) {
1830
+ range.setStart(lastNode, lastNode.textContent?.length ?? 0);
1831
+ } else {
1832
+ range.setStartAfter(lastNode);
1833
+ }
1834
+ } else {
1835
+ range.setStart(editor, 0);
1836
+ }
1837
+ range.collapse(true);
1838
+ selection.removeAllRanges();
1839
+ selection.addRange(range);
1840
+ }
1841
+ if (!range.collapsed) {
1842
+ range.deleteContents();
1843
+ }
1844
+ const textNode = document.createTextNode(text);
1845
+ range.insertNode(textNode);
1846
+ range.setStartAfter(textNode);
1847
+ range.setEndAfter(textNode);
1848
+ selection.removeAllRanges();
1849
+ selection.addRange(range);
1850
+ editor.focus();
1851
+ return true;
1852
+ } catch (error) {
1853
+ console.error("[editor-text-insertion] Error inserting text:", error);
1854
+ return false;
1855
+ }
1856
+ }
1857
+
1858
+ // src/engine/utils/insert-into-editor.ts
1859
+ import { blockBoundary, insertTextAtTextareaCursor } from "@ai-matrx/rich-editor/core/text-insertion";
1860
+ function hasEditorInsertTarget(targets) {
1861
+ return Boolean(targets.editorId) || Boolean(targets.getTextarea) || Boolean(targets.insertAtCaret);
1862
+ }
1863
+ function insertIntoEditor(targets, text) {
1864
+ const shaped = typeof text === "string" ? { editor: text, textarea: () => text, caret: text } : text;
1865
+ const { editorId, getTextarea, insertAtCaret, onTextReplace } = targets;
1866
+ const placement = shaped.placement ?? "inline";
1867
+ if (editorId) {
1868
+ const took = attempt("editor", () => {
1869
+ if (placement === "block") moveEditorCaretToLineEnd(editorId);
1870
+ return insertTextAtCursor(editorId, shaped.editor);
1871
+ });
1872
+ if (took) return "editor";
1873
+ }
1874
+ const field = editorId ? null : getTextarea?.() ?? null;
1875
+ if (field) {
1876
+ const took = attempt("textarea", () => {
1877
+ if (placement === "block" && field.selectionStart === field.selectionEnd) {
1878
+ const at = blockBoundary(field.value, field.selectionStart, field.selectionEnd, "nearest");
1879
+ field.setSelectionRange(at, at);
1880
+ }
1881
+ return insertTextAtTextareaCursor(field, shaped.textarea(field), onTextReplace);
1882
+ });
1883
+ if (took) return "textarea";
1884
+ }
1885
+ if (insertAtCaret && attempt("caret", () => insertAtCaret(shaped.caret, placement))) return "caret";
1886
+ return null;
1887
+ }
1888
+ function attempt(target, insert) {
1889
+ try {
1890
+ return insert();
1891
+ } catch (error) {
1892
+ console.error(`[ContextMenuV3] ${target} insert failed`, error);
1893
+ return false;
1894
+ }
1895
+ }
1896
+ function moveEditorCaretToLineEnd(editorId) {
1897
+ const editor = document.querySelector(`[data-editor-id="${editorId}"]`);
1898
+ const selection = window.getSelection();
1899
+ if (!editor || !selection || selection.rangeCount === 0) return;
1900
+ const range = selection.getRangeAt(0);
1901
+ if (!range.collapsed || !editor.contains(range.startContainer)) return;
1902
+ const node = range.startContainer;
1903
+ if (node.nodeType !== Node.TEXT_NODE) return;
1904
+ const text = node.textContent ?? "";
1905
+ const at = blockBoundary(text, range.startOffset, range.startOffset, "nearest");
1906
+ const next = document.createRange();
1907
+ next.setStart(node, at);
1908
+ next.collapse(true);
1909
+ selection.removeAllRanges();
1910
+ selection.addRange(next);
1911
+ }
1912
+ function ownParagraph(block, field) {
1913
+ const before = field.value.slice(0, field.selectionStart);
1914
+ const after = field.value.slice(field.selectionEnd);
1915
+ const lead = before.length === 0 || before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n";
1916
+ const tail = after.length === 0 || after.startsWith("\n\n") ? "" : after.startsWith("\n") ? "\n" : "\n\n";
1917
+ return `${lead}${block}${tail}`;
1918
+ }
1919
+
1920
+ // src/engine/hooks/useContextMenuActions.ts
1921
+ import { resolveActions } from "@ai-matrx/rich-content/rich-document/actions/provider";
1922
+ import { registryMenuActions } from "@ai-matrx/rich-content/rich-document/variants/shared/menuStructure";
1923
+ import { getSourceAdapter } from "@ai-matrx/rich-content/rich-document/actions/sources/index";
1924
+ import { shortHash } from "@ai-matrx/rich-content/rich-document/actions/sources/raw";
1925
+ import {
1926
+ placementGroupKey as placementGroupKey2,
1927
+ PLACEMENT_TYPES,
1928
+ PLACEMENT_TYPE_META
1929
+ } from "@ai-matrx/chat/agents/shortcuts/constants";
1930
+
1931
+ // src/engine/hooks/useUnifiedAgentContextMenu.ts
1932
+ import { useMemo, useState as useState4, useCallback as useCallback2 } from "react";
1933
+ import { useAppDispatch, useAppSelector } from "@ai-matrx/chat/store/hooks";
1934
+ import {
1935
+ fetchUnifiedMenu as fetchUnifiedMenu2,
1936
+ unifiedMenuLoadedKey
1937
+ } from "@ai-matrx/chat/agents/redux/agent-shortcuts/thunks";
1938
+
1939
+ // src/engine/utils/menu-deadline.ts
1940
+ var menu_deadline_exports = {};
1941
+ __reExport(menu_deadline_exports, menu_deadline_star);
1942
+ import * as menu_deadline_star from "@ai-matrx/chat/context-menu/utils/menu-deadline";
1943
+
1944
+ // src/engine/hooks/useUnifiedAgentContextMenu.ts
1945
+ import { selectAllShortcutsArray as selectAllShortcutsArray2 } from "@ai-matrx/chat/agents/redux/agent-shortcuts/selectors";
1946
+ import { selectAllCategoriesArray } from "@ai-matrx/chat/agents/redux/agent-shortcut-categories/selectors";
1947
+ import { resolveRowScope } from "@ai-matrx/chat/agents/redux/shared/scope";
1948
+ import { placementGroupKey } from "@ai-matrx/chat/agents/shortcuts/constants";
1949
+
1950
+ // src/engine/model/requirement-gate.ts
1951
+ var requirement_gate_exports = {};
1952
+ __reExport(requirement_gate_exports, requirement_gate_star);
1953
+ import * as requirement_gate_star from "@ai-matrx/chat/context-menu/model/requirement-gate";
1954
+
1955
+ // src/engine/hooks/useUnifiedAgentContextMenu.ts
1956
+ var SELECTION_DEPENDENT_KEYS = /* @__PURE__ */ new Set(["selection"]);
1957
+ var SCOPE_PRIORITY = {
1958
+ task: 5,
1959
+ project: 4,
1960
+ user: 3,
1961
+ organization: 2,
1962
+ global: 1
1963
+ };
1964
+ var SPECIFICITY = {
1965
+ surface: 3,
1966
+ domain: 2,
1967
+ global: 1
1968
+ };
1969
+ function specificityOf(item) {
1970
+ return SPECIFICITY[(0, requirement_gate_exports.readMenuScope)({ id: "", surfaceName: item.surfaceName ?? null }).level];
1971
+ }
1972
+ function beats(a, b) {
1973
+ const byOwner = SCOPE_PRIORITY[a.scopeLevel] - SCOPE_PRIORITY[b.scopeLevel];
1974
+ if (byOwner !== 0) return byOwner > 0;
1975
+ const bySpecificity = specificityOf(a) - specificityOf(b);
1976
+ if (bySpecificity !== 0) return bySpecificity > 0;
1977
+ return a.id < b.id;
1978
+ }
1979
+ function dedupeByPrecedence(items, keyFn) {
1980
+ const winners = /* @__PURE__ */ new Map();
1981
+ const passthrough = [];
1982
+ for (const item of items) {
1983
+ const key = keyFn(item);
1984
+ if (!key) {
1985
+ passthrough.push(item);
1986
+ continue;
1987
+ }
1988
+ const existing = winners.get(key);
1989
+ if (!existing || beats(item, existing)) winners.set(key, item);
1990
+ }
1991
+ return [...winners.values(), ...passthrough];
1992
+ }
1993
+ function buildCategoryGroups(args) {
1994
+ const {
1995
+ placementTypes,
1996
+ surfaceName = null,
1997
+ availableKeys,
1998
+ hasSelection,
1999
+ excludedItemIds,
2000
+ shortcuts,
2001
+ categories,
2002
+ contentBlocks
2003
+ } = args;
2004
+ if (placementTypes.length === 0) return [];
2005
+ const placementSet = new Set(placementTypes);
2006
+ const scopedCategories = categories.filter(
2007
+ (c) => c.isActive !== false && placementSet.has(placementGroupKey(c.placementType))
2008
+ ).map((c) => ({ ...c, scopeLevel: resolveRowScope(c) }));
2009
+ const scopedShortcuts = shortcuts.filter((s) => s.isActive !== false).map((s) => ({
2010
+ ...s,
2011
+ entryType: "agent_shortcut",
2012
+ scopeLevel: resolveRowScope(s)
2013
+ }));
2014
+ const scopedBlocks = contentBlocks.filter((b) => b.isActive !== false).map((b) => ({
2015
+ ...b,
2016
+ entryType: "content_block",
2017
+ scopeLevel: resolveRowScope(b)
2018
+ }));
2019
+ const offerCtx = compact({
2020
+ surfaceName,
2021
+ availableKeys,
2022
+ excludedItemIds
2023
+ });
2024
+ const filteredShortcuts = scopedShortcuts.filter((s) => {
2025
+ if (!(0, requirement_gate_exports.decideOffer)(s, offerCtx).offered) return false;
2026
+ if (hasSelection) return true;
2027
+ return !(0, requirement_gate_exports.requirementsOf)(s).some((key) => SELECTION_DEPENDENT_KEYS.has(key));
2028
+ });
2029
+ const filteredBlocks = scopedBlocks.filter(
2030
+ (b) => !excludedItemIds?.has(b.id)
2031
+ );
2032
+ const dedupedShortcuts = dedupeByPrecedence(filteredShortcuts, (s) => {
2033
+ if (s.keyboardShortcut) return `kbd:${s.keyboardShortcut}`;
2034
+ return `label:${s.categoryId}:${s.label}`;
2035
+ });
2036
+ const dedupedBlocks = dedupeByPrecedence(
2037
+ filteredBlocks,
2038
+ (b) => `block:${b.categoryId ?? "_none"}:${b.blockId}`
2039
+ );
2040
+ const byCategory = /* @__PURE__ */ new Map();
2041
+ for (const s of dedupedShortcuts) {
2042
+ if (!byCategory.has(s.categoryId)) byCategory.set(s.categoryId, []);
2043
+ byCategory.get(s.categoryId).push(s);
2044
+ }
2045
+ for (const b of dedupedBlocks) {
2046
+ const cid = b.categoryId;
2047
+ if (!cid) continue;
2048
+ if (!byCategory.has(cid)) byCategory.set(cid, []);
2049
+ byCategory.get(cid).push(b);
2050
+ }
2051
+ const dedupedCategories = dedupeByPrecedence(
2052
+ scopedCategories,
2053
+ (c) => `${c.placementType}:${c.parentCategoryId ?? "_root"}:${c.label}`
2054
+ );
2055
+ const nodeMap = /* @__PURE__ */ new Map();
2056
+ for (const cat of dedupedCategories) {
2057
+ nodeMap.set(cat.id, {
2058
+ category: cat,
2059
+ items: (byCategory.get(cat.id) ?? []).slice().sort((x, y) => (x.sortOrder ?? 0) - (y.sortOrder ?? 0)),
2060
+ children: []
2061
+ });
2062
+ }
2063
+ const roots = [];
2064
+ for (const cat of dedupedCategories) {
2065
+ const node = nodeMap.get(cat.id);
2066
+ if (cat.parentCategoryId && nodeMap.has(cat.parentCategoryId)) {
2067
+ nodeMap.get(cat.parentCategoryId).children.push(node);
2068
+ } else {
2069
+ roots.push(node);
2070
+ }
2071
+ }
2072
+ const sortNodes = (nodes) => {
2073
+ nodes.sort(
2074
+ (a, b) => (a.category.sortOrder ?? 0) - (b.category.sortOrder ?? 0) || a.category.label.localeCompare(b.category.label)
2075
+ );
2076
+ for (const n of nodes) sortNodes(n.children);
2077
+ };
2078
+ sortNodes(roots);
2079
+ return roots;
2080
+ }
2081
+ function useUnifiedAgentContextMenu(args) {
2082
+ const {
2083
+ placementTypes,
2084
+ surfaceName = null,
2085
+ availableKeys,
2086
+ hasSelection,
2087
+ excludedItemIds,
2088
+ enabled = true,
2089
+ scope = "global",
2090
+ scopeId = null
2091
+ } = args;
2092
+ const dispatch = useAppDispatch();
2093
+ const shortcuts = useAppSelector(selectAllShortcutsArray2);
2094
+ const categories = useAppSelector(selectAllCategoriesArray);
2095
+ const contentBlocks = useAppSelector((state) => getContextMenuHost().selectRenderDefinitions(state));
2096
+ const rowsInHand = useAppSelector(
2097
+ (state) => state.agentShortcut.contextLoaded?.[unifiedMenuLoadedKey({ scope, scopeId })] ?? false
2098
+ );
2099
+ const [error, setError] = useState4(null);
2100
+ const refresh = useCallback2(async () => {
2101
+ if (!enabled || placementTypes.length === 0) return;
2102
+ try {
2103
+ setError(null);
2104
+ await (0, menu_deadline_exports.withMenuDeadline)(
2105
+ dispatch(fetchUnifiedMenu2({ scope, scopeId })).unwrap(),
2106
+ "Loading the menu's AI actions"
2107
+ );
2108
+ } catch (err) {
2109
+ if (err?.name === "ConditionError") return;
2110
+ setError(err instanceof Error ? err.message : "Failed to load menu");
2111
+ }
2112
+ }, [dispatch, enabled, placementTypes.length, scope, scopeId]);
2113
+ const categoryGroups = useMemo(() => {
2114
+ if (!enabled) return [];
2115
+ return buildCategoryGroups({
2116
+ placementTypes,
2117
+ surfaceName,
2118
+ availableKeys,
2119
+ hasSelection,
2120
+ excludedItemIds,
2121
+ shortcuts,
2122
+ categories,
2123
+ contentBlocks: [...contentBlocks]
2124
+ });
2125
+ }, [
2126
+ enabled,
2127
+ placementTypes,
2128
+ categories,
2129
+ shortcuts,
2130
+ contentBlocks,
2131
+ surfaceName,
2132
+ availableKeys,
2133
+ hasSelection,
2134
+ excludedItemIds
2135
+ ]);
2136
+ return {
2137
+ categoryGroups,
2138
+ loading: enabled && placementTypes.length > 0 && !rowsInHand && error === null,
2139
+ // Rows in hand end a failure too: a fetch that landed after the menu's
2140
+ // deadline must not leave a "Couldn't load" row beside the rows it brought.
2141
+ error: rowsInHand ? null : error,
2142
+ refresh
2143
+ };
2144
+ }
2145
+
2146
+ // src/engine/hooks/useContextMenuActions.ts
2147
+ import { BASELINE_VALUE_NAMES as BASELINE_VALUE_NAMES2 } from "@ai-matrx/chat/surfaces/manifests/_baseline.manifest";
2148
+ import { useSurfaceConfig } from "@ai-matrx/chat/surfaces/hooks/useSurfaceConfig";
2149
+ import { useSurfaceBoundAgents } from "@ai-matrx/chat/surfaces/hooks/useSurfaceBoundAgents";
2150
+
2151
+ // src/engine/value-resolution.ts
2152
+ import { withBaselineScope } from "@ai-matrx/chat/surfaces/utils/baseline-scope";
2153
+ import {
2154
+ BASELINE_VALUE_NAMES
2155
+ } from "@ai-matrx/chat/surfaces/manifests/_baseline.manifest";
2156
+ import {
2157
+ getSurfaceRuntime,
2158
+ getSurfaceRuntimeForName
2159
+ } from "@ai-matrx/chat/surfaces/runtime/SurfaceRuntimeContext";
2160
+ var SKIP_MERGE_KEYS = /* @__PURE__ */ new Set([types_exports.CONTEXT_MENU_ENTITY_KEY, types_exports.CONTEXT_MENU_HEADING_KEY]);
2161
+ var isDev2 = process.env.NODE_ENV !== "production";
2162
+ function strOf(value) {
2163
+ return typeof value === "string" ? value : "";
2164
+ }
2165
+ function adoptMountedRuntimeScope(surfaceName) {
2166
+ try {
2167
+ const mounted = surfaceName ? getSurfaceRuntimeForName(surfaceName) : getSurfaceRuntime();
2168
+ if (!mounted) return null;
2169
+ const payload = mounted.getScope();
2170
+ if (payload && typeof payload.then === "function")
2171
+ return null;
2172
+ return payload;
2173
+ } catch (err) {
2174
+ if (isDev2)
2175
+ console.error(
2176
+ "[ContextMenuV3] mounted SurfaceRuntime threw in getScope() \u2014 menu scope falls back to the surface's own values",
2177
+ err
2178
+ );
2179
+ return null;
2180
+ }
2181
+ }
2182
+ function isEmptyValue(value) {
2183
+ if (value === void 0 || value === null) return true;
2184
+ if (typeof value === "string") return value.trim() === "";
2185
+ if (typeof value === "object" && !Array.isArray(value))
2186
+ return Object.keys(value).length === 0;
2187
+ return false;
2188
+ }
2189
+ function resolveApplicationScope(args) {
2190
+ const base = buildFromMenuContext(args);
2191
+ const surfaceAnswered = new Set(
2192
+ Object.keys(args.contextData ?? {}).filter((k) => !SKIP_MERGE_KEYS.has(k))
2193
+ );
2194
+ if (args.getApplicationScope) {
2195
+ const live = args.getApplicationScope();
2196
+ for (const [k, v] of Object.entries(live)) {
2197
+ if (SKIP_MERGE_KEYS.has(k) || v === void 0) continue;
2198
+ base[k] = v;
2199
+ surfaceAnswered.add(k);
2200
+ }
2201
+ }
2202
+ const runtime = adoptMountedRuntimeScope(args.surfaceName);
2203
+ if (runtime) {
2204
+ for (const [k, v] of Object.entries(runtime)) {
2205
+ if (SKIP_MERGE_KEYS.has(k) || v === void 0) continue;
2206
+ if (surfaceAnswered.has(k)) continue;
2207
+ if (isEmptyValue(base[k])) base[k] = v;
2208
+ }
2209
+ }
2210
+ if (args.ambient && !surfaceAnswered.has("ambient") && isEmptyValue(base.ambient))
2211
+ base.ambient = args.ambient;
2212
+ const hasContent = strOf(base.content).trim().length > 0;
2213
+ if (!args.getApplicationScope && !hasContent && args.fallbackContent && args.fallbackContent.trim()) {
2214
+ base.content = args.fallbackContent;
2215
+ }
2216
+ return withBaselineScope(base);
2217
+ }
2218
+ function buildFromMenuContext(args) {
2219
+ const { selectedText, selectionRange, contextData: cd } = args;
2220
+ let textBefore = "";
2221
+ let textAfter = "";
2222
+ let fullContent = "";
2223
+ if (selectionRange && selectionRange.type === "editable" && selectionRange.element) {
2224
+ const value = selectionRange.element.value ?? "";
2225
+ fullContent = value;
2226
+ textBefore = value.substring(0, selectionRange.start ?? 0);
2227
+ textAfter = value.substring(selectionRange.end ?? 0);
2228
+ }
2229
+ const scope = {};
2230
+ for (const [k, v] of Object.entries(cd)) {
2231
+ if (SKIP_MERGE_KEYS.has(k) || v === void 0) continue;
2232
+ scope[k] = v;
2233
+ }
2234
+ if (typeof cd.context === "string") {
2235
+ scope.context = { raw: cd.context };
2236
+ } else if (cd.context !== void 0 && typeof cd.context !== "string") {
2237
+ scope.context = cd.context;
2238
+ } else if (scope.context === void 0) {
2239
+ scope.context = {};
2240
+ }
2241
+ scope.selection = selectedText;
2242
+ scope.text_before = textBefore;
2243
+ scope.text_after = textAfter;
2244
+ if (typeof cd.content === "string") {
2245
+ scope.content = cd.content;
2246
+ } else if (fullContent) {
2247
+ scope.content = fullContent;
2248
+ } else if (scope.content === void 0) {
2249
+ scope.content = "";
2250
+ }
2251
+ const activeText = scope.active_text;
2252
+ if (scope.selection === "" && typeof activeText === "string" && activeText.length > 0) {
2253
+ scope.selection = activeText;
2254
+ }
2255
+ return scope;
2256
+ }
2257
+ function resolveActionText(scope) {
2258
+ const selection = strOf(scope.selection).trim();
2259
+ if (selection) return { text: strOf(scope.selection), source: "selection" };
2260
+ const content = strOf(scope.content).trim();
2261
+ if (content) return { text: strOf(scope.content), source: "content" };
2262
+ return { text: "", source: "none" };
2263
+ }
2264
+ function detectInertMenu(args) {
2265
+ const reasons = [];
2266
+ const selection = strOf(args.scope.selection).trim();
2267
+ const content = strOf(args.scope.content).trim();
2268
+ const baseline = new Set(
2269
+ BASELINE_VALUE_NAMES
2270
+ );
2271
+ const hasApplicationScope = Object.entries(args.scope).some(
2272
+ ([key, value]) => !baseline.has(key) && !isEmptyValue(value)
2273
+ );
2274
+ if (!selection) reasons.push("no selection");
2275
+ if (!content) reasons.push("no resolvable content (and no DOM-text fallback)");
2276
+ const inert = !args.isEditable && !selection && !content && !hasApplicationScope && !args.hasExtraSections;
2277
+ return { inert, reasons };
2278
+ }
2279
+ function auditSurfaceScope(surfaceName, scope) {
2280
+ if (!surfaceName) return [];
2281
+ const manifest = getContextMenuHost().getSurfaceManifest(surfaceName);
2282
+ if (!manifest) {
2283
+ return [
2284
+ `surface "${surfaceName}" has no registered manifest \u2014 its declared values cannot be verified`
2285
+ ];
2286
+ }
2287
+ const baseline = new Set(BASELINE_VALUE_NAMES);
2288
+ const warnings = [];
2289
+ for (const value of manifest.values) {
2290
+ if (baseline.has(value.name)) continue;
2291
+ if (!value.alwaysAvailable) continue;
2292
+ const present = scope[value.name];
2293
+ if (!(value.name in scope) || present === void 0 || present === null) {
2294
+ warnings.push(
2295
+ `declared value "${value.name}" is alwaysAvailable but was not emitted into the scope`
2296
+ );
2297
+ }
2298
+ }
2299
+ return warnings;
2300
+ }
2301
+ function reportMenuDiagnostics(args) {
2302
+ const diag = detectInertMenu(args);
2303
+ if (!isDev2) return diag;
2304
+ const label = args.surfaceName ?? "(no surfaceName)";
2305
+ if (diag.inert) {
2306
+ console.error(
2307
+ `%c[ContextMenuV3] INERT MENU on "${label}"%c \u2014 opened with nothing to act on (${diag.reasons.join(
2308
+ ", "
2309
+ )}). This is the "fake menu" defect: the surface must provide a selection, contextData.content, getApplicationScope(), or render selectable content the DOM fallback can read.`,
2310
+ "color:#ef4444;font-weight:bold",
2311
+ "color:inherit",
2312
+ { scopeKeys: Object.keys(args.scope) }
2313
+ );
2314
+ }
2315
+ const valueWarnings = auditSurfaceScope(args.surfaceName, args.scope);
2316
+ if (valueWarnings.length > 0) {
2317
+ console.error(
2318
+ `%c[ContextMenuV3] VALUE MAPPING GAP on "${label}"%c \u2014 ${valueWarnings.join(
2319
+ "; "
2320
+ )}. Shortcuts/agents bound to these values will receive nothing.`,
2321
+ "color:#f59e0b;font-weight:bold",
2322
+ "color:inherit"
2323
+ );
2324
+ }
2325
+ return diag;
2326
+ }
2327
+
2328
+ // src/engine/utils/selection-write-back.ts
2329
+ function isField(el) {
2330
+ return typeof HTMLTextAreaElement !== "undefined" && el instanceof HTMLTextAreaElement || typeof HTMLInputElement !== "undefined" && el instanceof HTMLInputElement;
2331
+ }
2332
+ function buildSelectionWriteBack(args) {
2333
+ const {
2334
+ originalText,
2335
+ textSource,
2336
+ selectionRange,
2337
+ onTextReplace,
2338
+ onTextInsertAfter,
2339
+ insertAtCaret
2340
+ } = args;
2341
+ if (!originalText) return null;
2342
+ const fieldEl = selectionRange?.type === "editable" && isField(selectionRange.element) ? selectionRange.element : null;
2343
+ if (fieldEl) {
2344
+ let anchorText = originalText;
2345
+ let anchorStart = textSource === "selection" ? selectionRange.start : 0;
2346
+ const locate = () => {
2347
+ if (!fieldEl.isConnected) return null;
2348
+ const value = fieldEl.value;
2349
+ if (value.slice(anchorStart, anchorStart + anchorText.length) === anchorText) {
2350
+ return { start: anchorStart, end: anchorStart + anchorText.length };
2351
+ }
2352
+ const first = value.indexOf(anchorText);
2353
+ if (first === -1 || value.indexOf(anchorText, first + 1) !== -1) {
2354
+ return null;
2355
+ }
2356
+ return { start: first, end: first + anchorText.length };
2357
+ };
2358
+ const write = (start, end, insert) => {
2359
+ const value = fieldEl.value;
2360
+ const next = value.slice(0, start) + insert + value.slice(end);
2361
+ if (onTextReplace) onTextReplace(next);
2362
+ else (0, selection_tracking_exports.spliceInputValue)(fieldEl, start, end, insert);
2363
+ };
2364
+ return {
2365
+ originalText,
2366
+ replace: (text) => {
2367
+ const at = locate();
2368
+ if (!at) return false;
2369
+ write(at.start, at.end, text);
2370
+ anchorStart = at.start;
2371
+ anchorText = text;
2372
+ return true;
2373
+ },
2374
+ insertBelow: (text) => {
2375
+ const at = locate();
2376
+ if (!at) return false;
2377
+ const block = "\n\n" + text;
2378
+ write(at.end, at.end, block);
2379
+ anchorStart = at.end + 2;
2380
+ anchorText = text;
2381
+ return true;
2382
+ }
2383
+ };
2384
+ }
2385
+ if (textSource !== "selection") return null;
2386
+ if (!insertAtCaret && !onTextInsertAfter) return null;
2387
+ return {
2388
+ originalText,
2389
+ replace: (text) => insertAtCaret ? insertAtCaret(text) : false,
2390
+ insertBelow: (text) => {
2391
+ if (!onTextInsertAfter) return false;
2392
+ onTextInsertAfter(text);
2393
+ return true;
2394
+ }
2395
+ };
2396
+ }
2397
+
2398
+ // src/engine/hooks/useContextMenuActions.ts
2399
+ import { registerLaunchWidgetHandle } from "@ai-matrx/chat/agents/utils/launch-widget-handles";
2400
+
2401
+ // src/engine/utils/json-menu-actions.ts
2402
+ import { detectJson } from "@ai-matrx/kit/json-format";
2403
+ import { formatJsonText } from "@ai-matrx/kit/json-format";
2404
+ function lineCount(text) {
2405
+ let n = text === "" ? 0 : 1;
2406
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") n++;
2407
+ return n;
2408
+ }
2409
+ function sizeHint(before, after) {
2410
+ const b = lineCount(before);
2411
+ const a = lineCount(after);
2412
+ if (b !== a) {
2413
+ return `${b} line${b !== 1 ? "s" : ""} -> ${a} line${a !== 1 ? "s" : ""}`;
2414
+ }
2415
+ return `${before.length} -> ${after.length} chars`;
2416
+ }
2417
+ function shapePreservingStyle(detection) {
2418
+ return detection.payload.includes("\n") ? "pretty" : "minify";
2419
+ }
2420
+ function buildJsonMenuSection(params) {
2421
+ const { text, canWrite, onReplace, onCopy } = params;
2422
+ if (!text.trim()) return null;
2423
+ const detection = detectJson(text);
2424
+ if (!detection.looksLikeJson) return null;
2425
+ if (!detection.ok) {
2426
+ return {
2427
+ detection,
2428
+ writes: false,
2429
+ actions: [
2430
+ {
2431
+ id: "json-invalid",
2432
+ label: "Invalid JSON",
2433
+ hint: detection.error,
2434
+ disabled: true,
2435
+ run: () => {
2436
+ }
2437
+ }
2438
+ ]
2439
+ };
2440
+ }
2441
+ const apply = (next) => {
2442
+ if (canWrite) onReplace(next);
2443
+ else void onCopy(next);
2444
+ };
2445
+ const verb = canWrite ? "" : "Copy ";
2446
+ const actions = [];
2447
+ const addFormat = (id, label, style, fence = "preserve", sortKeys = false) => {
2448
+ const result = formatJsonText(text, { style, fence, sortKeys });
2449
+ if (!result.ok) return;
2450
+ if (canWrite && !result.changed) return;
2451
+ actions.push({
2452
+ id,
2453
+ label,
2454
+ hint: result.changed ? sizeHint(text, result.text) : void 0,
2455
+ run: () => apply(result.text)
2456
+ });
2457
+ };
2458
+ addFormat("json-condense", `${verb}Condense`, "compact");
2459
+ addFormat("json-minify", `${verb}Minify (one line)`, "minify");
2460
+ addFormat("json-expand", `${verb}Expand`, "pretty");
2461
+ addFormat(
2462
+ "json-sort-keys",
2463
+ `${verb}Sort keys A-Z`,
2464
+ shapePreservingStyle(detection),
2465
+ "preserve",
2466
+ true
2467
+ );
2468
+ if (detection.fence) {
2469
+ addFormat(
2470
+ "json-strip-fence",
2471
+ `${verb}Remove code fence`,
2472
+ shapePreservingStyle(detection),
2473
+ "strip"
2474
+ );
2475
+ } else {
2476
+ addFormat(
2477
+ "json-add-fence",
2478
+ `${verb}Wrap in json code fence`,
2479
+ shapePreservingStyle(detection),
2480
+ "add"
2481
+ );
2482
+ }
2483
+ if (canWrite) {
2484
+ const minified = formatJsonText(text, { style: "minify", fence: "strip" });
2485
+ if (minified.ok) {
2486
+ actions.push({
2487
+ id: "json-copy-minified",
2488
+ label: "Copy minified",
2489
+ hint: sizeHint(text, minified.text),
2490
+ run: () => void onCopy(minified.text)
2491
+ });
2492
+ }
2493
+ }
2494
+ if (actions.length === 0) return null;
2495
+ return { detection, writes: canWrite, actions };
2496
+ }
2497
+
2498
+ // src/engine/managed-agent-launch.ts
2499
+ var MANAGED_CONTEXT_MENU_AGENT_CONFIG = {
2500
+ displayMode: "flexible-panel",
2501
+ allowChat: true,
2502
+ showVariablePanel: true
2503
+ };
2504
+
2505
+ // src/engine/hooks/useContextMenuActions.ts
2506
+ import { AGENT_ICON as AGENT_ICON2 } from "@ai-matrx/icons/domain";
2507
+ var DEFAULT_PLACEMENT_MODE = {
2508
+ "ai-action": "show",
2509
+ "bound-agent": "show",
2510
+ "content-block": "show",
2511
+ "organization-tool": "show",
2512
+ "user-tool": "show",
2513
+ "quick-action": "show"
2514
+ };
2515
+ var ALL_DB_PLACEMENTS = [
2516
+ "ai-action",
2517
+ "content-block",
2518
+ "organization-tool",
2519
+ "user-tool"
2520
+ ];
2521
+ var PLACEMENT_LABEL_OVERRIDE = {
2522
+ [PLACEMENT_TYPES.USER_TOOL]: "My Items",
2523
+ [PLACEMENT_TYPES.ORGANIZATION_TOOL]: "Org Items"
2524
+ };
2525
+ var PLACEMENT_COLOR = {
2526
+ [PLACEMENT_TYPES.AI_ACTION]: "#0ea5e9",
2527
+ [PLACEMENT_TYPES.CONTENT_BLOCK]: "#8b5cf6",
2528
+ [PLACEMENT_TYPES.ORGANIZATION_TOOL]: "#f59e0b",
2529
+ [PLACEMENT_TYPES.USER_TOOL]: "#10b981"
2530
+ };
2531
+ function getPlacementIcon(placementType) {
2532
+ switch (placementType) {
2533
+ case PLACEMENT_TYPES.AI_ACTION:
2534
+ return Rocket;
2535
+ case PLACEMENT_TYPES.CONTENT_BLOCK:
2536
+ return FileText;
2537
+ case PLACEMENT_TYPES.ORGANIZATION_TOOL:
2538
+ return Building;
2539
+ case PLACEMENT_TYPES.USER_TOOL:
2540
+ return User;
2541
+ default:
2542
+ return FileText;
2543
+ }
2544
+ }
2545
+ function getPlacementLabel(placementType) {
2546
+ return PLACEMENT_LABEL_OVERRIDE[placementType] ?? PLACEMENT_TYPE_META[placementType]?.label ?? placementType;
2547
+ }
2548
+ function resolveIcon(iconName, fallback = "FileText") {
2549
+ return getIconComponent(iconName ?? fallback, fallback);
2550
+ }
2551
+ function groupsByPlacement(groups) {
2552
+ const map = {};
2553
+ for (const g of groups) {
2554
+ (map[placementGroupKey2(g.category.placementType)] ??= []).push(g);
2555
+ }
2556
+ return map;
2557
+ }
2558
+ function hasItemsRecursive(group) {
2559
+ if (group.items.length > 0) return true;
2560
+ return group.children.some(hasItemsRecursive);
2561
+ }
2562
+ function resolveRichActionView(action, ctx) {
2563
+ const label = typeof action.label === "function" ? action.label(ctx) : action.label;
2564
+ const disabledResult = action.disabled?.(ctx);
2565
+ const disabled = typeof disabledResult === "object" ? true : Boolean(disabledResult);
2566
+ return { label, disabled };
2567
+ }
2568
+ function useContextMenuActions(props) {
2569
+ const {
2570
+ sourceFeature,
2571
+ surfaceName,
2572
+ menuVersion,
2573
+ getApplicationScope,
2574
+ contextData,
2575
+ selectedText,
2576
+ selectionRange,
2577
+ fallbackContent,
2578
+ placementMode,
2579
+ scope: shortcutScope,
2580
+ scopeId,
2581
+ extraSections,
2582
+ isEditable,
2583
+ editorId,
2584
+ getTextarea,
2585
+ insertAtCaret,
2586
+ onContentInserted,
2587
+ onTextReplace,
2588
+ onTextInsertAfter,
2589
+ onDelete,
2590
+ onUndo,
2591
+ onRedo,
2592
+ widgetHandleId
2593
+ } = props;
2594
+ const dispatch = useAppDispatch2();
2595
+ const store = useAppStore();
2596
+ const host = useContextMenuHostBindings();
2597
+ const entity = props.entity;
2598
+ const resolvedPlacementMode = {
2599
+ ...DEFAULT_PLACEMENT_MODE,
2600
+ ...placementMode ?? {}
2601
+ };
2602
+ const dbPlacementTypes = ALL_DB_PLACEMENTS.filter(
2603
+ (p) => resolvedPlacementMode[p] !== "hide"
2604
+ );
2605
+ const activeOrganizationId = useAppSelector2(selectOrganizationId);
2606
+ const activeScopeIds = host.activeScopeIds;
2607
+ const scope = resolveApplicationScope({
2608
+ getApplicationScope,
2609
+ contextData,
2610
+ selectedText,
2611
+ selectionRange,
2612
+ fallbackContent,
2613
+ surfaceName,
2614
+ ambient: {
2615
+ active_organization_id: activeOrganizationId ?? null,
2616
+ active_scope_ids: activeScopeIds ?? [],
2617
+ surface_name: surfaceName ?? null
2618
+ }
2619
+ });
2620
+ const scopeKeySignature = Object.keys(scope).sort().join("|");
2621
+ const availableKeys = useMemo2(
2622
+ () => (0, requirement_gate_exports.buildAvailableKeys)(compact({
2623
+ baselineValueNames: BASELINE_VALUE_NAMES2,
2624
+ declaredValueNames: surfaceName ? getContextMenuHost().getSurfaceManifest(surfaceName)?.values.map((v) => v.name) : void 0,
2625
+ // `scope` is rebuilt every render but its KEY SET is stable for an
2626
+ // open, so the signature — not the object — is the dependency.
2627
+ runtimeScopeKeys: scopeKeySignature.split("|").filter(Boolean)
2628
+ })),
2629
+ [surfaceName, scopeKeySignature]
2630
+ );
2631
+ const { getNamespace: getSurfaceNamespace } = useSurfaceConfig(
2632
+ surfaceName ?? "matrx-unregistered/context-menu"
2633
+ );
2634
+ const menuSurfaceConfig = getSurfaceNamespace("menu");
2635
+ const excludedItemIds = useMemo2(
2636
+ () => new Set(menuSurfaceConfig?.excludedItemIds ?? []),
2637
+ [menuSurfaceConfig]
2638
+ );
2639
+ const {
2640
+ categoryGroups,
2641
+ loading,
2642
+ error: librariesError,
2643
+ refresh
2644
+ } = useUnifiedAgentContextMenu({
2645
+ placementTypes: dbPlacementTypes,
2646
+ surfaceName,
2647
+ availableKeys,
2648
+ hasSelection: String(scope.selection ?? "").trim().length > 0,
2649
+ excludedItemIds,
2650
+ enabled: dbPlacementTypes.length > 0,
2651
+ scope: shortcutScope,
2652
+ scopeId
2653
+ });
2654
+ const {
2655
+ sections: boundAgentSections,
2656
+ loading: boundAgentsFetching,
2657
+ settled: boundAgentsSettled,
2658
+ error: boundAgentsError,
2659
+ refresh: refreshBoundAgents
2660
+ } = useSurfaceBoundAgents(surfaceName, { isEditable });
2661
+ const boundAgentsLoading = boundAgentsFetching || !boundAgentsSettled;
2662
+ const { launchShortcut, launchAgent } = useAgentLauncher();
2663
+ const { speak } = useSpeech({ processMarkdown: true, label: "Selected text" });
2664
+ const { roles: surfaceAgentRoles } = useSurfaceAgentRoles(
2665
+ surfaceName ?? "matrx-unregistered/context-menu"
2666
+ );
2667
+ const {
2668
+ openListenSummaryWindow,
2669
+ quickActions,
2670
+ openDiffWindow,
2671
+ openFindReplace,
2672
+ openReferencePicker,
2673
+ openContextAssignment,
2674
+ openLinkRecordSheet,
2675
+ openShareModalWindow,
2676
+ openStateViewer,
2677
+ openSurfaceInspector,
2678
+ hasCompareBase
2679
+ } = host;
2680
+ const currentUserId = useAppSelector2(selectUserId);
2681
+ const isAdmin = host.isAdminDebugger;
2682
+ const hasAdminPower = host.hasAdminPower;
2683
+ const organizationId = useAppSelector2(selectOrganizationId);
2684
+ const isDebugMode = host.isDebugMode;
2685
+ const isAdminIndicatorOpen = host.isAdminIndicatorOpen;
2686
+ useEffect2(() => {
2687
+ void refresh();
2688
+ void refreshBoundAgents();
2689
+ }, []);
2690
+ const actionText = resolveActionText(scope);
2691
+ const richDocSource = props.contentSource ?? { type: "raw" };
2692
+ const richDocAdapter = getSourceAdapter(richDocSource.type);
2693
+ const richPrefix = richDocSource.type === "raw" ? `${richDocAdapter.instanceKeyPrefix(richDocSource)}-${shortHash(actionText.text).slice(0, 8)}` : richDocAdapter.instanceKeyPrefix(richDocSource);
2694
+ const richDocCtx = {
2695
+ content: actionText.text,
2696
+ source: richDocSource,
2697
+ metadata: null,
2698
+ dispatch,
2699
+ getState: store.getState,
2700
+ organizationId,
2701
+ isAuthenticated: Boolean(currentUserId),
2702
+ isAdmin: hasAdminPower,
2703
+ isCreator: false,
2704
+ surfaceKey: surfaceName ?? null,
2705
+ onClose: () => {
2706
+ },
2707
+ instanceKey: (prefix) => `${richPrefix}-${prefix}`,
2708
+ sourceAdapter: richDocAdapter,
2709
+ applicationScope: scope,
2710
+ ...props.richDocCtxExtras ?? {}
2711
+ };
2712
+ const textAgentCallbacks = editorTextAgentCallbacks(richDocCtx.callbacks, richDocSource, richDocAdapter, props.requestTextAgentReview);
2713
+ if (textAgentCallbacks) richDocCtx.callbacks = textAgentCallbacks;
2714
+ const excludedRichActionIds = [
2715
+ ...props.excludedRichActions ?? [],
2716
+ ...actionsAlreadyHere({
2717
+ sourceType: richDocSource.type,
2718
+ surfaceName,
2719
+ isEditable: Boolean(isEditable)
2720
+ })
2721
+ ];
2722
+ const richActions = actionText.source !== "none" ? resolveActions(richDocCtx, compact({
2723
+ exclude: excludedRichActionIds,
2724
+ extra: props.extraRichActions
2725
+ })) : [];
2726
+ const registryActions = registryMenuActions(richActions);
2727
+ const copyVariantActions = richActions.filter((a) => a.category === "copy");
2728
+ const exportActions = richActions.filter(
2729
+ (a) => a.category === "export" || a.id === "save-as-file"
2730
+ );
2731
+ const convertActions = richActions.filter(
2732
+ (a) => a.category === "save" && a.id !== "save-as-file"
2733
+ );
2734
+ useEffect2(() => {
2735
+ reportMenuDiagnostics({
2736
+ surfaceName,
2737
+ scope,
2738
+ isEditable,
2739
+ hasExtraSections: Boolean(extraSections && extraSections.length > 0)
2740
+ });
2741
+ }, []);
2742
+ const handleCopy = async () => {
2743
+ if (!actionText.text) return;
2744
+ if (!await kitCopyText(actionText.text)) {
2745
+ getContextMenuHost().showManualCopy({ text: actionText.text });
2746
+ }
2747
+ };
2748
+ const handleSpeak = () => {
2749
+ if (actionText.text.trim()) speak(actionText.text);
2750
+ };
2751
+ const { roles: listenHomeRoles } = useSurfaceAgentRoles(
2752
+ getContextMenuHost().listeningHomeSurface
2753
+ );
2754
+ const spokenSummaryRole = surfaceAgentRoles.spoken_summary?.effectiveAgentId ? surfaceAgentRoles.spoken_summary : listenHomeRoles.spoken_summary;
2755
+ const spokenSummaryAgentId = spokenSummaryRole?.effectiveAgentId ?? null;
2756
+ const openSpokenSummary = (autoPlay) => {
2757
+ if (!spokenSummaryAgentId || !actionText.text.trim()) return;
2758
+ primeAudioOutput();
2759
+ openListenSummaryWindow({
2760
+ agentId: spokenSummaryAgentId,
2761
+ agentName: spokenSummaryRole?.role.label ?? null,
2762
+ sourceText: actionText.text,
2763
+ style: "Extremely Concise Summary",
2764
+ autoPlay
2765
+ });
2766
+ };
2767
+ const handleSpokenSummary = () => openSpokenSummary(false);
2768
+ const handleSpokenSummaryLive = () => openSpokenSummary(true);
2769
+ const handleCut = async () => {
2770
+ if (!selectionRange || selectionRange.type !== "editable") return;
2771
+ const element = selectionRange.element;
2772
+ if (!(element instanceof HTMLTextAreaElement) && !(element instanceof HTMLInputElement))
2773
+ return;
2774
+ const { start, end } = selectionRange;
2775
+ const cutText = element.value.substring(start, end);
2776
+ if (!await kitCopyText(cutText)) {
2777
+ getContextMenuHost().showManualCopy({ text: cutText, title: "Copy manually (cut was blocked)" });
2778
+ return;
2779
+ }
2780
+ if (onTextReplace) {
2781
+ onTextReplace(
2782
+ element.value.substring(0, start) + element.value.substring(end)
2783
+ );
2784
+ } else {
2785
+ (0, selection_tracking_exports.spliceInputValue)(element, start, end, "");
2786
+ }
2787
+ };
2788
+ const handlePaste = async () => {
2789
+ if (!isEditable || !selectionRange || selectionRange.type !== "editable")
2790
+ return;
2791
+ const element = selectionRange.element;
2792
+ if (!(element instanceof HTMLTextAreaElement) && !(element instanceof HTMLInputElement))
2793
+ return;
2794
+ const text = await kitReadText();
2795
+ if (text === null) {
2796
+ toast2({ title: "Couldn't read the clipboard", variant: "destructive" });
2797
+ return;
2798
+ }
2799
+ const { start, end } = selectionRange;
2800
+ if (onTextReplace) {
2801
+ onTextReplace(
2802
+ element.value.substring(0, start) + text + element.value.substring(end)
2803
+ );
2804
+ } else {
2805
+ (0, selection_tracking_exports.spliceInputValue)(element, start, end, text);
2806
+ }
2807
+ };
2808
+ const editableTarget = isEditable && selectionRange?.type === "editable" ? selectionRange.element : null;
2809
+ const canWriteJson = editableTarget instanceof HTMLTextAreaElement || editableTarget instanceof HTMLInputElement;
2810
+ const jsonSection = buildJsonMenuSection({
2811
+ text: actionText.text,
2812
+ canWrite: canWriteJson,
2813
+ onReplace: (next) => {
2814
+ if (!canWriteJson || !selectionRange || selectionRange.type !== "editable")
2815
+ return;
2816
+ const element = selectionRange.element;
2817
+ if (!(element instanceof HTMLTextAreaElement) && !(element instanceof HTMLInputElement))
2818
+ return;
2819
+ const { start, end } = actionText.source === "selection" ? selectionRange : { start: 0, end: element.value.length };
2820
+ if (onTextReplace) {
2821
+ onTextReplace(
2822
+ element.value.substring(0, start) + next + element.value.substring(end)
2823
+ );
2824
+ } else {
2825
+ (0, selection_tracking_exports.spliceInputValue)(element, start, end, next);
2826
+ }
2827
+ },
2828
+ onCopy: async (next) => {
2829
+ if (await kitCopyText(next)) {
2830
+ toast2({ title: "Copied", description: "JSON copied to clipboard." });
2831
+ } else {
2832
+ getContextMenuHost().showManualCopy({ text: next });
2833
+ }
2834
+ }
2835
+ });
2836
+ const handleSelectAll = () => {
2837
+ if (!selectionRange) {
2838
+ const field = getTextarea?.();
2839
+ if (field) {
2840
+ requestAnimationFrame(() => {
2841
+ field.focus();
2842
+ field.select();
2843
+ });
2844
+ }
2845
+ return;
2846
+ }
2847
+ if (selectionRange.type === "editable") {
2848
+ const element = selectionRange.element;
2849
+ if (element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement) {
2850
+ requestAnimationFrame(() => {
2851
+ element.focus();
2852
+ element.select();
2853
+ });
2854
+ }
2855
+ } else {
2856
+ const container = selectionRange.containerElement;
2857
+ if (!container) return;
2858
+ requestAnimationFrame(() => {
2859
+ try {
2860
+ const range = document.createRange();
2861
+ range.selectNodeContents(container);
2862
+ const selection = window.getSelection();
2863
+ if (selection) {
2864
+ selection.removeAllRanges();
2865
+ selection.addRange(range);
2866
+ }
2867
+ } catch {
2868
+ }
2869
+ });
2870
+ }
2871
+ };
2872
+ const editableElement = (() => {
2873
+ const fromRange = selectionRange?.type === "editable" ? selectionRange.element : null;
2874
+ const el = fromRange ?? getTextarea?.() ?? null;
2875
+ return el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement ? el : null;
2876
+ })();
2877
+ const canNativeUndo = Boolean(isEditable && editableElement);
2878
+ const runNativeEdit = (command) => {
2879
+ if (!editableElement) return;
2880
+ editableElement.focus();
2881
+ try {
2882
+ document.execCommand(command);
2883
+ } catch (err) {
2884
+ console.error(`[ContextMenuV3] native ${command} failed`, err);
2885
+ }
2886
+ };
2887
+ const handleUndo = () => onUndo ? onUndo() : runNativeEdit("undo");
2888
+ const handleRedo = () => onRedo ? onRedo() : runNativeEdit("redo");
2889
+ const compareContent = () => actionText.source === "selection" ? { content: actionText.text, label: "Selection" } : { content: actionText.text, label: "Current" };
2890
+ const handleCompareClipboard = async () => {
2891
+ const { content, label } = compareContent();
2892
+ const clip = await kitReadText();
2893
+ if (clip === null) {
2894
+ toast2({ title: "Couldn't read the clipboard", variant: "destructive" });
2895
+ return;
2896
+ }
2897
+ if (!clip) {
2898
+ toast2({ title: "Clipboard is empty" });
2899
+ return;
2900
+ }
2901
+ openDiffWindow({
2902
+ original: content,
2903
+ modified: clip,
2904
+ originalLabel: label,
2905
+ modifiedLabel: "Clipboard",
2906
+ title: "Compare with clipboard",
2907
+ engine: "light"
2908
+ });
2909
+ };
2910
+ const handleSetCompareBase = () => {
2911
+ const { content, label } = compareContent();
2912
+ host.setCompareBase({ content, label, language: null });
2913
+ toast2({
2914
+ title: "Set as compare base",
2915
+ description: "Open another item and choose \u201CCompare with base\u201D."
2916
+ });
2917
+ };
2918
+ const handleCompareWithBase = async () => {
2919
+ const { content, label } = compareContent();
2920
+ const opened = await host.openCompareWithBase({ current: content, currentLabel: label });
2921
+ if (!opened) {
2922
+ toast2({
2923
+ title: "No compare base set",
2924
+ description: "Choose \u201CSet as compare base\u201D on another item first."
2925
+ });
2926
+ }
2927
+ };
2928
+ const launchWidgetHandleId = () => registerLaunchWidgetHandle(
2929
+ widgetHandleId,
2930
+ isEditable ? buildSelectionWriteBack({
2931
+ originalText: actionText.text,
2932
+ textSource: actionText.source,
2933
+ selectionRange,
2934
+ onTextReplace,
2935
+ onTextInsertAfter,
2936
+ insertAtCaret
2937
+ }) : null
2938
+ );
2939
+ const handleShortcutExecute = async (entry) => {
2940
+ if (!entry.agentId) {
2941
+ toast2({
2942
+ title: "Agent Not Connected",
2943
+ description: `"${entry.label}" has no connected agent. Configure it in the admin panel.`,
2944
+ variant: "destructive"
2945
+ });
2946
+ return;
2947
+ }
2948
+ const resultDisplay = entry.displayMode ?? "modal-full";
2949
+ try {
2950
+ await launchShortcut(entry.id, scope, {
2951
+ surfaceKey: `${sourceFeature}:${entry.id}`,
2952
+ sourceFeature,
2953
+ config: { displayMode: resultDisplay },
2954
+ runtime: compact({
2955
+ originalText: actionText.text,
2956
+ surfaceName,
2957
+ // Editable surfaces: let the agent stream widget_text_* edits
2958
+ // straight into the surface, and give the person Replace / Insert
2959
+ // below on the result (undefined on read-only — no tools).
2960
+ widgetHandleId: launchWidgetHandleId()
2961
+ })
2962
+ });
2963
+ } catch (error) {
2964
+ const message = error instanceof Error ? error.message : "An unknown error occurred";
2965
+ toast2({
2966
+ title: "Execution Failed",
2967
+ description: `${entry.label}: ${message}`,
2968
+ variant: "destructive"
2969
+ });
2970
+ }
2971
+ };
2972
+ const handleBoundAgentExecute = async (entry) => {
2973
+ try {
2974
+ await launchAgent(entry.agentId, {
2975
+ surfaceKey: `${sourceFeature}:bound-agent:${entry.agentId}`,
2976
+ sourceFeature,
2977
+ // Managed entries share the WindowPanel default. autoRun is deliberately
2978
+ // absent so the safe open-and-wait default remains authoritative.
2979
+ config: MANAGED_CONTEXT_MENU_AGENT_CONFIG,
2980
+ runtime: compact({
2981
+ applicationScope: scope,
2982
+ originalText: actionText.text,
2983
+ surfaceName,
2984
+ widgetHandleId: launchWidgetHandleId()
2985
+ })
2986
+ });
2987
+ } catch (error) {
2988
+ const message = error instanceof Error ? error.message : "An unknown error occurred";
2989
+ toast2({
2990
+ title: "Execution Failed",
2991
+ description: `${entry.name}: ${message}`,
2992
+ variant: "destructive"
2993
+ });
2994
+ }
2995
+ };
2996
+ const insertTargets = { editorId, getTextarea, insertAtCaret, onTextReplace };
2997
+ const handleContentBlockInsert = (entry) => {
2998
+ const template = entry.template;
2999
+ if (insertIntoEditor(insertTargets, template)) {
3000
+ onContentInserted?.();
3001
+ return;
3002
+ }
3003
+ void kitCopyText(template).then((copied) => {
3004
+ if (copied) {
3005
+ toast2({
3006
+ title: "Copied to clipboard",
3007
+ description: `"${entry.label}" can't be inserted here \u2014 this surface isn't editable.`
3008
+ });
3009
+ } else {
3010
+ getContextMenuHost().showManualCopy({ text: template });
3011
+ }
3012
+ });
3013
+ };
3014
+ const handleEntrySelect = (entry) => {
3015
+ if (entry.entryType === "agent_shortcut") void handleShortcutExecute(entry);
3016
+ else handleContentBlockInsert(entry);
3017
+ };
3018
+ const handleDelete = async () => {
3019
+ if (!onDelete) return;
3020
+ const ok = await getContextMenuHost().confirm({
3021
+ title: "Delete this item?",
3022
+ description: "This action cannot be undone.",
3023
+ confirmLabel: "Delete",
3024
+ variant: "destructive"
3025
+ });
3026
+ if (ok) onDelete();
3027
+ };
3028
+ const handleFind = () => {
3029
+ props.suppressSelectionRestore();
3030
+ openFindReplace({
3031
+ getTargetElement: () => selectionRange?.type === "editable" ? selectionRange.element : null,
3032
+ onReplace: onTextReplace
3033
+ });
3034
+ };
3035
+ const canInsertReference = isEditable && hasEditorInsertTarget(insertTargets);
3036
+ const copyReference = (pick) => {
3037
+ void kitCopyText(pick.fence).then((copied) => {
3038
+ if (copied) {
3039
+ toast2({
3040
+ title: "Reference copied",
3041
+ description: pick.title ? `Paste it anywhere to link "${pick.title}".` : "Paste it anywhere to render the link."
3042
+ });
3043
+ } else {
3044
+ getContextMenuHost().showManualCopy({ text: pick.fence, title: "Copy the reference" });
3045
+ }
3046
+ });
3047
+ };
3048
+ const insertReference = (pick) => {
3049
+ const inserted = insertIntoEditor(insertTargets, {
3050
+ editor: `
3051
+ ${pick.fence}
3052
+ `,
3053
+ textarea: (field) => ownParagraph(pick.fence, field),
3054
+ caret: pick.fence,
3055
+ placement: "block"
3056
+ });
3057
+ if (inserted) {
3058
+ onContentInserted?.();
3059
+ return;
3060
+ }
3061
+ toast2({
3062
+ title: "Copied instead",
3063
+ description: "Couldn't insert here. Paste it where you want it."
3064
+ });
3065
+ copyReference(pick);
3066
+ };
3067
+ const handleInsertReference = () => {
3068
+ props.suppressSelectionRestore();
3069
+ openReferencePicker({
3070
+ mode: canInsertReference ? "insert" : "copy",
3071
+ onPicked: (pick) => {
3072
+ if (pick.delivery === "insert" && canInsertReference) insertReference(pick);
3073
+ else copyReference(pick);
3074
+ }
3075
+ });
3076
+ };
3077
+ const handleAttach = () => {
3078
+ if (!entity) return;
3079
+ openContextAssignment({
3080
+ subject: {
3081
+ entityType: entity.type,
3082
+ entityId: entity.id,
3083
+ title: entity.title
3084
+ }
3085
+ });
3086
+ };
3087
+ const handleLinkRecord = () => {
3088
+ if (!entity) return;
3089
+ openLinkRecordSheet({ target: { token: entity.type, id: entity.id, title: entity.title } });
3090
+ };
3091
+ const handleShare = () => {
3092
+ if (!entity?.resourceType) return;
3093
+ openShareModalWindow({
3094
+ resourceType: entity.resourceType,
3095
+ resourceId: entity.id,
3096
+ resourceName: entity.title
3097
+ });
3098
+ };
3099
+ const { openSurfaceContextWindow, openBindAgent } = host;
3100
+ const resolvedSurfaceName = surfaceName ?? detectActiveSurface();
3101
+ const surfaceLabel = resolvedSurfaceName ? getSurfaceDisplayLabel(resolvedSurfaceName) : "This page";
3102
+ const related = getRelatedSurfaces(resolvedSurfaceName);
3103
+ const copyText = (text) => {
3104
+ void kitCopyText(text).then((copied) => {
3105
+ if (copied) toast2({ title: "Copied", description: text });
3106
+ else getContextMenuHost().showManualCopy({ text });
3107
+ });
3108
+ };
3109
+ const surfaceAgentItems = [];
3110
+ boundAgentSections.filter((section) => section.agents.length > 0).forEach((section, idx) => {
3111
+ if (idx > 0)
3112
+ surfaceAgentItems.push({ kind: "separator", id: `sa:${section.key}:sep` });
3113
+ for (const agent of section.agents) {
3114
+ surfaceAgentItems.push({
3115
+ kind: "item",
3116
+ id: `sa:${section.key}:${agent.agentId}`,
3117
+ label: agent.name,
3118
+ description: agent.organizationName ? `${section.label} \xB7 ${agent.organizationName}` : section.label,
3119
+ icon: AGENT_ICON2,
3120
+ onSelect: () => void handleBoundAgentExecute(agent)
3121
+ });
3122
+ }
3123
+ });
3124
+ if (surfaceAgentItems.length > 0)
3125
+ surfaceAgentItems.push({ kind: "separator", id: "sa:bind-sep" });
3126
+ surfaceAgentItems.push({
3127
+ kind: "item",
3128
+ id: "sa:bind",
3129
+ label: "Bind an agent to this page\u2026",
3130
+ icon: Plus,
3131
+ disabled: !resolvedSurfaceName,
3132
+ onSelect: () => {
3133
+ if (resolvedSurfaceName) openBindAgent({ surfaceName: resolvedSurfaceName });
3134
+ }
3135
+ });
3136
+ const relatedItems = [
3137
+ ...related.ancestry,
3138
+ ...related.children
3139
+ ].map((ref) => ({
3140
+ kind: "item",
3141
+ id: `sr:${ref.name}`,
3142
+ label: ref.label,
3143
+ description: `${ref.kind === "ancestor" ? "Parent" : "Child"} \xB7 ${ref.name}`,
3144
+ icon: AppWindow,
3145
+ onSelect: () => openSurfaceContextWindow({ surfaceName: ref.name, isEditable: false })
3146
+ }));
3147
+ const surfaceChildren = [
3148
+ // The surface key is an engineer's handle — admin-only (cold walk 20),
3149
+ // like the Agents panel's sub-line. Everyone else sees the label only.
3150
+ ...isAdmin ? [
3151
+ {
3152
+ kind: "item",
3153
+ id: "surface:location",
3154
+ label: resolvedSurfaceName ?? "No surface registered for this page",
3155
+ ...resolvedSurfaceName ? { description: "Surface location \xB7 click to copy" } : {},
3156
+ icon: ClipboardCopy,
3157
+ disabled: !resolvedSurfaceName,
3158
+ onSelect: () => resolvedSurfaceName && copyText(resolvedSurfaceName)
3159
+ },
3160
+ { kind: "separator", id: "surface:sep1" }
3161
+ ] : [],
3162
+ {
3163
+ kind: "item",
3164
+ id: "surface:context",
3165
+ label: "Surface Context",
3166
+ description: "Live page values",
3167
+ icon: Braces,
3168
+ disabled: !resolvedSurfaceName,
3169
+ onSelect: () => {
3170
+ if (!resolvedSurfaceName) return;
3171
+ openSurfaceContextWindow({
3172
+ surfaceName: resolvedSurfaceName,
3173
+ isEditable: Boolean(isEditable)
3174
+ });
3175
+ }
3176
+ },
3177
+ ...hasAdminPower ? [
3178
+ {
3179
+ kind: "item",
3180
+ id: "surface:context-admin",
3181
+ label: "Surface Context Admin",
3182
+ description: "Contract, provenance & settings",
3183
+ icon: ShieldCheck,
3184
+ onSelect: () => openSurfaceInspector({
3185
+ surfaceName: resolvedSurfaceName ?? null,
3186
+ scope,
3187
+ isEditable: Boolean(isEditable),
3188
+ preferRuntime: true
3189
+ })
3190
+ }
3191
+ ] : [],
3192
+ { kind: "separator", id: "surface:sep2" },
3193
+ {
3194
+ kind: "submenu",
3195
+ id: "surface:agents",
3196
+ label: "Agents on this page",
3197
+ icon: AGENT_ICON2,
3198
+ children: surfaceAgentItems
3199
+ },
3200
+ ...relatedItems.length > 0 ? [
3201
+ {
3202
+ kind: "submenu",
3203
+ id: "surface:related",
3204
+ label: "Related surfaces",
3205
+ icon: Layers,
3206
+ children: relatedItems
3207
+ }
3208
+ ] : [],
3209
+ ...isAdmin ? [
3210
+ { kind: "separator", id: "surface:sep3" },
3211
+ {
3212
+ kind: "item",
3213
+ id: "surface:version",
3214
+ label: `Menu v3.${types_exports.CANONICAL_MENU_VERSION_V3} \xB7 V${menuVersion}`,
3215
+ description: "Menu revision \xB7 surface wiring version",
3216
+ icon: Info,
3217
+ onSelect: () => copyText(
3218
+ `${resolvedSurfaceName ?? "(no surface)"} \xB7 v3.${types_exports.CANONICAL_MENU_VERSION_V3} \xB7 V${menuVersion}`
3219
+ )
3220
+ }
3221
+ ] : []
3222
+ ];
3223
+ const surfaceSection = {
3224
+ id: "surface-info",
3225
+ items: [
3226
+ {
3227
+ kind: "submenu",
3228
+ id: "surface",
3229
+ label: surfaceLabel,
3230
+ icon: AppWindow,
3231
+ children: surfaceChildren
3232
+ }
3233
+ ]
3234
+ };
3235
+ const handleInspectValues = () => {
3236
+ openSurfaceInspector({
3237
+ surfaceName: surfaceName ?? null,
3238
+ scope,
3239
+ isEditable: Boolean(isEditable)
3240
+ });
3241
+ };
3242
+ const handleInspectState = () => {
3243
+ openStateViewer();
3244
+ };
3245
+ const handleToggleDebugMode = () => {
3246
+ host.toggleDebugMode();
3247
+ };
3248
+ const handleToggleAdminIndicator = () => {
3249
+ host.toggleAdminIndicator();
3250
+ };
3251
+ return {
3252
+ scope,
3253
+ actionText,
3254
+ // A textarea/input, or a contenteditable field (chip / rich editor), names
3255
+ // itself in the header — never "Content: {{reply.body}}".
3256
+ fieldLabel: isEditable && selectionRange ? fieldLabelOf(selectionRange.element) : null,
3257
+ jsonSection,
3258
+ resolvedPlacementMode,
3259
+ categoryGroups,
3260
+ grouped: groupsByPlacement(categoryGroups),
3261
+ loading,
3262
+ librariesError,
3263
+ boundAgentSections,
3264
+ boundAgentsLoading,
3265
+ boundAgentsError,
3266
+ excludedRichActionIds,
3267
+ retryLibraries: () => {
3268
+ void refresh();
3269
+ void refreshBoundAgents();
3270
+ },
3271
+ richDocCtx,
3272
+ registryActions,
3273
+ copyVariantActions,
3274
+ exportActions,
3275
+ convertActions,
3276
+ hasCompareBase,
3277
+ isAdmin,
3278
+ isDebugMode,
3279
+ isAdminIndicatorOpen,
3280
+ canNativeUndo,
3281
+ handleCopy,
3282
+ handleSpeak,
3283
+ handleSpokenSummary,
3284
+ handleSpokenSummaryLive,
3285
+ spokenSummaryAvailable: Boolean(spokenSummaryAgentId),
3286
+ handleCut,
3287
+ handlePaste,
3288
+ handleSelectAll,
3289
+ handleUndo,
3290
+ handleRedo,
3291
+ handleCompareClipboard,
3292
+ handleSetCompareBase,
3293
+ handleCompareWithBase,
3294
+ handleShortcutExecute,
3295
+ handleBoundAgentExecute,
3296
+ handleContentBlockInsert,
3297
+ handleEntrySelect,
3298
+ handleDelete,
3299
+ handleFind,
3300
+ handleInsertReference,
3301
+ handleAttach,
3302
+ handleLinkRecord,
3303
+ handleShare,
3304
+ handleInspectValues,
3305
+ handleInspectState,
3306
+ handleToggleDebugMode,
3307
+ handleToggleAdminIndicator,
3308
+ quickActions,
3309
+ surfaceSection
3310
+ };
3311
+ }
3312
+ export {
3313
+ ALL_DB_PLACEMENTS,
3314
+ ContextMenuV3,
3315
+ DEFAULT_PLACEMENT_MODE,
3316
+ EditableContextMenu,
3317
+ ItemContextMenuV3,
3318
+ ItemMenuHostBinding,
3319
+ MenuPresenceProvider,
3320
+ MenuRegroupContext,
3321
+ MenuRegroupProvider,
3322
+ NonEditableContextMenu,
3323
+ OpenSurfaceMenuButton,
3324
+ PLACEMENT_COLOR,
3325
+ PLACEMENT_LABEL_OVERRIDE,
3326
+ PageSurfaceMenu,
3327
+ RegistryMenuSourceProvider,
3328
+ RegroupBoundary,
3329
+ buildCategoryGroups,
3330
+ contentSourceKey,
3331
+ getPlacementIcon,
3332
+ getPlacementLabel,
3333
+ groupsByPlacement,
3334
+ hasItemsRecursive,
3335
+ reachedThroughPortal,
3336
+ resolveIcon,
3337
+ resolveRichActionView,
3338
+ sameContentSource,
3339
+ useContextMenuActions,
3340
+ useIsInsideContextMenu,
3341
+ useRegistryMenuSource,
3342
+ useUnifiedAgentContextMenu,
3343
+ warmMenuContent
3344
+ };
3345
+ //# sourceMappingURL=react.js.map