@ai-matrx/context-menu 0.0.0-stage → 0.1.0

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