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