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