@ai-matrx/context-menu 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -1
  3. package/dist/builder.cjs +1665 -0
  4. package/dist/builder.cjs.map +1 -0
  5. package/dist/builder.d.cts +22 -0
  6. package/dist/builder.d.ts +22 -0
  7. package/dist/builder.js +1650 -0
  8. package/dist/builder.js.map +1 -0
  9. package/dist/design-store.cjs +195 -0
  10. package/dist/design-store.cjs.map +1 -0
  11. package/dist/design-store.d.cts +42 -0
  12. package/dist/design-store.d.ts +42 -0
  13. package/dist/design-store.js +175 -0
  14. package/dist/design-store.js.map +1 -0
  15. package/dist/engine.cjs +3069 -0
  16. package/dist/engine.cjs.map +1 -0
  17. package/dist/engine.d.cts +731 -0
  18. package/dist/engine.d.ts +731 -0
  19. package/dist/engine.js +3250 -0
  20. package/dist/engine.js.map +1 -0
  21. package/dist/grouping-CB3zlD1T.d.ts +623 -0
  22. package/dist/grouping-CGcWsltu.d.cts +623 -0
  23. package/dist/host-types-AOiEYwR8.d.cts +9 -0
  24. package/dist/host-types-AOiEYwR8.d.ts +9 -0
  25. package/dist/index.cjs +486 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.d.cts +46 -0
  28. package/dist/index.d.ts +46 -0
  29. package/dist/index.js +464 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/menu-content.cjs +3146 -0
  32. package/dist/menu-content.cjs.map +1 -0
  33. package/dist/menu-content.d.cts +30 -0
  34. package/dist/menu-content.d.ts +30 -0
  35. package/dist/menu-content.js +3197 -0
  36. package/dist/menu-content.js.map +1 -0
  37. package/dist/model-BdF0TqZ3.d.cts +107 -0
  38. package/dist/model-BdF0TqZ3.d.ts +107 -0
  39. package/dist/react.cjs +3322 -0
  40. package/dist/react.cjs.map +1 -0
  41. package/dist/react.d.cts +133 -0
  42. package/dist/react.d.ts +133 -0
  43. package/dist/react.js +3345 -0
  44. package/dist/react.js.map +1 -0
  45. package/package.json +170 -1
@@ -0,0 +1,731 @@
1
+ import './host-types-AOiEYwR8.cjs';
2
+ import { ExtraSectionAnchor, MenuContentProps, ResolvedContextMenuContext, ContextMenuExtraSection, ContextMenuEntityRef } from '@ai-matrx/chat/context-menu/types';
3
+ export * from '@ai-matrx/chat/context-menu/types';
4
+ import { C as ContextMenuActions, M as MenuGrouping } from './grouping-CGcWsltu.cjs';
5
+ export { A as AuditRow, B as BuildJsonMenuActionsParams, a as CompareBaseInput, b as ContextAssignmentOptions, c as ContextMenuHost, d as ContextMenuHostBindings, e as ContextMenuQuickActions, D as DEFAULT_CONTEXT_MENU_HOST, f as DiffWindowOptions, g as DocumentAgentReviewProps, F as FindReplaceOptions, G as GroupingDestination, h as GroupingRule, I as InertMenuDiagnostic, J as JsonMenuAction, i as JsonMenuSection, L as LinkRecordSheetOptions, j as ListenSummaryWindowOptions, k as ManualCopyOptions, l as MenuGroupDef, P as ProposedHome, R as REGROUP_ID_PREFIX, m as ReferencePick, n as ReferencePickerOptions, o as RegroupAudit, p as RegroupOptions, q as RegroupResult, r as ResolveScopeArgs, s as ResolvedActionText, S as ShareModalOptions, t as SurfaceAgentBindOptions, u as SurfaceContextWindowOptions, v as SurfaceInspectorOptions, w as actionLabel, x as auditRegroup, y as auditSurfaceScope, z as buildJsonMenuSection, E as configureContextMenuHost, H as currentModel, K as destinationFor, N as detectInertMenu, O as drawnPlaces, Q as getContextMenuHost, T as hasActionableContent, U as jsonSectionLabel, V as matchRule, W as regroupResolved, X as reportMenuDiagnostics, Y as resetContextMenuHost, Z as resolveActionText, _ as resolveApplicationScope, $ as topLevelRowCount, a0 as useContextMenuHostBindings } from './grouping-CGcWsltu.cjs';
6
+ export * from '@ai-matrx/chat/context-menu/utils/build-application-scope';
7
+ export * from '@ai-matrx/chat/context-menu/utils/availability';
8
+ import { SelectionRange } from '@ai-matrx/chat/context-menu/utils/selection-tracking';
9
+ export * from '@ai-matrx/chat/context-menu/utils/selection-tracking';
10
+ export * from '@ai-matrx/chat/context-menu/utils/menu-deadline';
11
+ export * from '@ai-matrx/chat/context-menu/utils/resolveMarkdownContext';
12
+ export * from '@ai-matrx/chat/context-menu/model/requirement-gate';
13
+ import React__default from 'react';
14
+ import { IconComponentType } from '@ai-matrx/icons';
15
+ import { RichDocumentAction, RichDocumentActionContext, ContentSource, ContentSourceAdapter } from '@ai-matrx/rich-content/rich-document/types';
16
+ import { ResolvedItem } from '@ai-matrx/alchemy/declare';
17
+ import { MatrxDataTableRecordControls } from '@ai-matrx/design-system/data-table/types';
18
+ import { MatrxTableMenuPayload, MatrxTableMenuTarget } from '@ai-matrx/design-system/data-table/menu-targets';
19
+ import { ActionCategory, ClickTarget, ResolvedAction, Action, ActionProvider } from '@ai-matrx/alchemy/actions';
20
+ import { WidgetHandle, SelectionWriteBack } from '@ai-matrx/chat/agents/types/widget-handle.types';
21
+ import { ApplicationScope } from '@ai-matrx/chat/agents/types/scope.types';
22
+ import { ItemMenuConfig } from '@ai-matrx/design-system/item';
23
+ import 'lucide-react';
24
+ import '@ai-matrx/chat/surfaces/types';
25
+ import '@ai-matrx/chat/agents/redux/agent-shortcuts/skl-types';
26
+ import '@ai-matrx/kit/confirm-opener';
27
+ import '@ai-matrx/chat/agents/redux/agent-shortcuts/types';
28
+ import '@ai-matrx/chat/agents/redux/agent-shortcut-categories/types';
29
+ import '@ai-matrx/chat/agents/redux/shared/scope';
30
+ import '@ai-matrx/chat/surfaces/services/surface-bound-agents.service';
31
+ import '@ai-matrx/kit/json-format';
32
+ import '@ai-matrx/alchemy/menu';
33
+
34
+ /**
35
+ * Opens the canonical context menu from an explicit overflow control.
36
+ *
37
+ * Touch surfaces cannot rely on a discoverable right-click. Dispatching the
38
+ * same bubbling event keeps the overflow button on the exact desktop/mobile
39
+ * ContextMenuV3 path instead of creating a second action menu.
40
+ */
41
+ declare function openContextMenuForElement(element: HTMLElement | null): void;
42
+
43
+ interface MenuNodeBase {
44
+ id: string;
45
+ }
46
+ interface MenuItemNode extends MenuNodeBase {
47
+ kind: "item";
48
+ label: string;
49
+ icon?: IconComponentType | undefined;
50
+ /** Tailwind classes for the icon (colour). */
51
+ iconClassName?: string | undefined;
52
+ /** Inline style for the icon (data-driven category colours). */
53
+ iconStyle?: React__default.CSSProperties | undefined;
54
+ /** Second muted line. */
55
+ description?: string | undefined;
56
+ /** Right-aligned muted hint (shortcut, count, …). */
57
+ hint?: string | undefined;
58
+ /** Native tooltip. */
59
+ title?: string | undefined;
60
+ disabled?: boolean | undefined;
61
+ destructive?: boolean | undefined;
62
+ /** Extra classes on the row (admin amber, …). */
63
+ className?: string | undefined;
64
+ /** Running it reloads this menu's own rows (Retry): the menu stays open (alchemy `keepsMenuOpen`). */
65
+ keepsMenuOpen?: boolean | undefined;
66
+ onSelect: () => void;
67
+ }
68
+ interface MenuSubmenuNode extends MenuNodeBase {
69
+ kind: "submenu";
70
+ label: string;
71
+ icon?: IconComponentType | undefined;
72
+ iconClassName?: string | undefined;
73
+ iconStyle?: React__default.CSSProperties | undefined;
74
+ disabled?: boolean | undefined;
75
+ /** Spinner in the trigger while the data loads. */
76
+ loading?: boolean | undefined;
77
+ /** Rendered centred when `children` has no actionable node. */
78
+ emptyLabel?: string | undefined;
79
+ /** Tailwind width class for the panel. Default `w-60`. */
80
+ width?: string | undefined;
81
+ /** Layout hint: which placement this submenu is (for re-grouping). */
82
+ placement?: string | undefined;
83
+ children: MenuNode[];
84
+ }
85
+ interface MenuCheckboxNode extends MenuNodeBase {
86
+ kind: "checkbox";
87
+ label: string;
88
+ icon?: IconComponentType | undefined;
89
+ description?: string | undefined;
90
+ hint?: string | undefined;
91
+ checked: boolean;
92
+ disabled?: boolean | undefined;
93
+ onCheckedChange: (next: boolean) => void;
94
+ }
95
+ interface MenuLinkNode extends MenuNodeBase {
96
+ kind: "link";
97
+ label: string;
98
+ icon?: IconComponentType | undefined;
99
+ description?: string | undefined;
100
+ hint?: string | undefined;
101
+ href: string;
102
+ target?: string | undefined;
103
+ disabled?: boolean | undefined;
104
+ }
105
+ interface MenuLabelNode extends MenuNodeBase {
106
+ kind: "label";
107
+ label: string;
108
+ }
109
+ interface MenuSeparatorNode extends MenuNodeBase {
110
+ kind: "separator";
111
+ }
112
+ type MenuNode = MenuItemNode | MenuSubmenuNode | MenuCheckboxNode | MenuLinkNode | MenuLabelNode | MenuSeparatorNode;
113
+ /** Actionable leaf kinds (what a filter can match and run). */
114
+ type MenuLeafNode = MenuItemNode | MenuCheckboxNode | MenuLinkNode;
115
+ /**
116
+ * Coarse group a section belongs to. Layouts regroup by this, never by id
117
+ * string-matching, so a surface's `extraSections` ("surface") and the
118
+ * data-driven placements ("ai") stay distinguishable from the core verbs.
119
+ */
120
+ type MenuGroup = "clipboard" | "tools" | "history" | "share" | "document" | "surface" | "ai" | "quick" | "editable" | "admin" | "surface-info";
121
+ interface MenuSection {
122
+ id: string;
123
+ group: MenuGroup;
124
+ /** Muted heading rendered above the section (surface sections). */
125
+ label?: string | undefined;
126
+ /** Icon for the fold a layout may collapse this section into (any className-drawn icon, like nodes). */
127
+ icon?: IconComponentType | undefined;
128
+ /** Classic rendering: no separator between this and the previous section. */
129
+ joinPrevious?: boolean | undefined;
130
+ /** The clicked target's section — see `ContextMenuExtraSection.primary`. */
131
+ primary?: boolean | undefined;
132
+ nodes: MenuNode[];
133
+ }
134
+ interface MenuHeader {
135
+ label: "Selected" | "Content";
136
+ text: string;
137
+ }
138
+ interface MenuModel {
139
+ header: MenuHeader | null;
140
+ /** Classic order — exactly the historical top-to-bottom arrangement. */
141
+ sections: MenuSection[];
142
+ /** Well-known nodes layouts pull out by role (never by label). */
143
+ roles: MenuRoles;
144
+ }
145
+ /** The nodes a layout needs to address individually. */
146
+ interface MenuRoles {
147
+ copy: MenuItemNode;
148
+ speak: MenuItemNode;
149
+ /** ONE slot: "Listen" submenu — Summarize without playing / Summarize & listen. */
150
+ listen: MenuSubmenuNode | null;
151
+ copyAs: MenuSubmenuNode | null;
152
+ json: MenuSubmenuNode | null;
153
+ cut: MenuItemNode;
154
+ paste: MenuItemNode;
155
+ selectAll: MenuItemNode;
156
+ find: MenuItemNode;
157
+ /** "Insert reference…" (editable) / "Copy reference…" (read-only) — never absent. */
158
+ insertReference: MenuItemNode;
159
+ chat: MenuItemNode;
160
+ undo: MenuItemNode;
161
+ redo: MenuItemNode;
162
+ viewHistory: MenuItemNode;
163
+ compare: MenuSubmenuNode | null;
164
+ exportMenu: MenuSubmenuNode | null;
165
+ convert: MenuSubmenuNode | null;
166
+ attach: MenuItemNode | null;
167
+ /** "Link a record…" — present on EVERY menu that targets a record (guard: every-record-menu-links-a-record). */
168
+ linkRecord: MenuItemNode | null;
169
+ share: MenuItemNode | null;
170
+ placements: MenuSubmenuNode[];
171
+ /**
172
+ * THE ONE REGISTRY TREE (RC-B6): the same tree, in the same order, as the
173
+ * ⋯ menu, ProTextarea's "…" and the mobile sheet — with the agent-shortcut
174
+ * libraries folded into its AI submenu. `null` when there is no content.
175
+ */
176
+ registry: MenuNode[] | null;
177
+ quickActions: MenuSubmenuNode | null;
178
+ save: MenuItemNode | null;
179
+ del: MenuItemNode | null;
180
+ admin: MenuSubmenuNode | null;
181
+ /** The engine-built surface submenu section (location / context / agents / related). */
182
+ surfaceInfo: MenuSection;
183
+ /** Surface `extraSections`, by anchor, already converted to model nodes. */
184
+ extras: Record<ExtraSectionAnchor, MenuSection[]>;
185
+ }
186
+ /** Does the subtree contain anything the user can act on? */
187
+ declare function hasActionable(nodes: MenuNode[]): boolean;
188
+ /**
189
+ * Classic order, with the clicked target's section lifted to the very top.
190
+ * Stable: everything else keeps the anchor order it was assembled in.
191
+ */
192
+ declare function liftPrimarySections(sections: MenuSection[]): MenuSection[];
193
+ /**
194
+ * The registry tree as model nodes — `buildMenuTree` (the ONE structure every
195
+ * host renders), with `aiExtras` (the agent-shortcut libraries) folded into
196
+ * its AI submenu. Ids are the registry ids behind a `rich:` prefix, which is
197
+ * what the one-tree guard compares against the ⋯ menu.
198
+ */
199
+ declare function registryTreeNodes(actions: RichDocumentAction[], ctx: ContextMenuActions["richDocCtx"], aiExtras: MenuNode[]): MenuNode[];
200
+ declare function buildMenuModel(m: ContextMenuActions, props: Pick<MenuContentProps, "extraSections" | "isEditable" | "onSave" | "onDelete" | "onUndo" | "onRedo" | "canUndo" | "canRedo" | "undoHint" | "redoHint" | "onViewHistory" | "hasHistory" | "selectedText" | "entity" | "getTextarea"> & Partial<Pick<MenuContentProps, "selectionRange" | "recordActionsOnly">>): MenuModel;
201
+
202
+ interface TableRowMenuDescriptor {
203
+ context: ResolvedContextMenuContext;
204
+ extraSections: ContextMenuExtraSection[];
205
+ }
206
+ declare function createTableRowMenuDescriptor(descriptor: TableRowMenuDescriptor): object;
207
+ /** Exactly the row commands the default menu renders. Nothing else is read. */
208
+ type TableRowEditCommands = Pick<MatrxDataTableRecordControls, "beginEdit" | "saveEdits" | "cancelEdits">;
209
+ /**
210
+ * The table hands its host the row's controls as `unknown` — it never
211
+ * interprets what the host builds from them — so they are READ here, one
212
+ * command at a time. A command the table did not send, or sent as something
213
+ * that cannot be called, is simply not offered rather than rendered dead.
214
+ */
215
+ declare function tableRowEditCommands(controls: unknown): TableRowEditCommands;
216
+ /**
217
+ * The host's generic menu model: complete current row data plus the
218
+ * table-owned edit commands.
219
+ *
220
+ * The table asks this for whichever of its five right-click LEVELS was aimed
221
+ * at. Only the row has a generic menu here, so every other level answers
222
+ * `null` and the table leaves that level to the surface that owns it.
223
+ */
224
+ declare function createDefaultTableRowMenuDescriptor(payload: MatrxTableMenuPayload): object | null;
225
+ /** Reusable base for a domain menu that keeps the shared row edit controls. */
226
+ declare function buildDefaultTableRowMenuDescriptor(row: unknown, controls: TableRowEditCommands): TableRowMenuDescriptor;
227
+ /**
228
+ * The canonical table registers ONE item source per mounted instance, under its table id. The table
229
+ * answers which of its right-click LEVELS was aimed at (`MatrxTableMenuTarget`); the source turns
230
+ * that answer into the declared `table_row` item, with the row's menu as its host data.
231
+ */
232
+ declare function registerTableRowContextResolver(tableId: string, resolve: (target: MatrxTableMenuTarget) => unknown): () => void;
233
+ /** The clicked row as its declared item (raw-free) and the menu its table built for it. */
234
+ interface TableRowItemHit {
235
+ item: ResolvedItem;
236
+ menu: TableRowMenuDescriptor;
237
+ }
238
+ declare function resolveTableRowItem(target: HTMLElement | null): TableRowItemHit | null;
239
+ /** The row's menu, or null when the click was not on a canonical table's row. */
240
+ declare function resolveTableRowMenuDescriptor(target: HTMLElement | null): TableRowMenuDescriptor | null;
241
+ /** The row's record name: the first data cell (not a tick box, star or button cell) with words. */
242
+ declare function rowRecordName(row: HTMLElement | null | undefined): string;
243
+
244
+ interface InventoryRow {
245
+ id: string;
246
+ label: string;
247
+ category: ActionCategory;
248
+ }
249
+ declare const CONTEXT_MENU_ENGINE_ROWS: readonly InventoryRow[];
250
+ /** Engine rows whose ids are generated per library (one per agent category or placement). */
251
+ declare const CONTEXT_MENU_ENGINE_ID_PREFIXES: readonly string[];
252
+
253
+ declare const PROPOSED_MENU_GROUPING: MenuGrouping;
254
+
255
+ /**
256
+ * A RECORD'S ROWS, IN THE ONE MENU OF THE CONTENT THAT SHOWS IT (R26; ALC-15 round 5).
257
+ *
258
+ * A record's toolbar can live apart from the content it belongs to: on /notes the tab strip holds the
259
+ * note's ⋯ (Save, Duplicate, Move, Delete, Close tab…) while the note's content is drawn in the pane
260
+ * below. The ⋯ and a right-click on that content target the SAME thing — the note — so they must be
261
+ * one menu (measured 2026-09-27: 27 rows from the tab's ⋯, 25 from the content, each missing the
262
+ * other's rows).
263
+ *
264
+ * · The owner of the rows registers them under a key: `registerRecordMenu(key, get)`.
265
+ * · The host marks the content's root with `data-record-menu="<key>"`.
266
+ * · Every menu opened inside that root carries the registered sections (and the record's entity,
267
+ * when the menu has none of its own) — the shell reads them at open (ContextMenuV3).
268
+ * · The record's ⋯ calls `openRecordMenu(key, button)`: it opens the content's OWN menu at the
269
+ * button. `false` = that content is not on screen; the caller opens its own menu instead.
270
+ *
271
+ * Plain data, no React.
272
+ */
273
+
274
+ declare const RECORD_MENU_ATTR = "data-record-menu";
275
+ interface RecordMenuRows {
276
+ entity?: ContextMenuEntityRef | null | undefined;
277
+ extraSections: ContextMenuExtraSection[];
278
+ /**
279
+ * The record's own name for the menu header ("Note · Clinic intake
280
+ * checklist") — used when the clicked content names nothing itself, so the
281
+ * header names the record instead of quoting its body. A selection still wins.
282
+ */
283
+ heading?: {
284
+ label: string;
285
+ text: string;
286
+ } | null;
287
+ }
288
+ /** The owner of registered rows says they changed (its record was renamed). */
289
+ declare function recordMenuChanged(): void;
290
+ /** For `useSyncExternalStore`: re-render when any registered record changes. */
291
+ declare function subscribeRecordMenus(listener: () => void): () => void;
292
+ declare function recordMenusRevision(): number;
293
+ /** Register the rows a record contributes; returns the unregister. Read at every open, so always current. */
294
+ declare function registerRecordMenu(key: string, get: () => RecordMenuRows): () => void;
295
+ /** The rows of the record whose content holds `target` (nearest marked root), or null. */
296
+ declare function resolveRecordMenu(target: Element | null): RecordMenuRows | null;
297
+ /**
298
+ * Open the record content's own menu at `anchor` (a ⋯ button). Returns false when no content for
299
+ * `key` is on screen, so the caller can open its own menu instead.
300
+ */
301
+ declare function openRecordMenu(key: string, anchor: HTMLElement): boolean;
302
+
303
+ declare const STRIP_MAX = 6;
304
+ declare const OWN_ROWS_MAX = 4;
305
+ type ClickedKind = "thing" | "editable" | "text";
306
+ declare function clickedKind(target: ClickTarget, resolved: readonly ResolvedAction[]): ClickedKind;
307
+ interface ProposedOptions {
308
+ /** The clicked thing's name for "More <noun> options" ("table", "quiz", "note"). */
309
+ noun: string;
310
+ }
311
+ declare function proposedArrangement(target: ClickTarget, resolved: readonly ResolvedAction[], { noun }: ProposedOptions): ResolvedAction[];
312
+
313
+ interface WhereTheMenuIs {
314
+ /** The rich-document source type (`note`, `task`, `chat-message`, `raw`, …). */
315
+ sourceType: string;
316
+ /** The registry surface the menu belongs to. */
317
+ surfaceName?: string | null | undefined;
318
+ /** The menu wraps an editor (EditableContextMenu). */
319
+ isEditable: boolean;
320
+ }
321
+ declare function actionsAlreadyHere(where: WhereTheMenuIs): string[];
322
+
323
+ /**
324
+ * The effective entity for ONE menu open.
325
+ *
326
+ * key absent → the menu-level prop stands (single-entity surfaces unchanged)
327
+ * key present → that row's entity wins
328
+ * key `null` → this target has no entity, so the entity-bound actions hide
329
+ * rather than target the wrong record
330
+ *
331
+ * A malformed value (no string `type` + `id`) is treated as absent and SCREAMS
332
+ * in dev — a half-built entity would render an Attach that writes a broken edge.
333
+ */
334
+ declare function resolveEffectiveEntity(entityProp: ContextMenuEntityRef | undefined, resolved: ResolvedContextMenuContext | null): ContextMenuEntityRef | undefined;
335
+ /**
336
+ * The effective `contextData` for one open: static payload + per-target merge,
337
+ * with the reserved entity key stripped so it never lands in the
338
+ * `ApplicationScope` as a value.
339
+ */
340
+ declare function mergeResolvedContextData(contextData: Record<string, unknown> | undefined, resolved: ResolvedContextMenuContext | null): Record<string, unknown>;
341
+ /**
342
+ * The context for ONE open on a table row: the table's own row descriptor
343
+ * (table-row-item.ts) JOINED with the surface's `resolveContextOnOpen` answer.
344
+ *
345
+ * 🚨 WHY (2026-10-03, CHAIR-REACH). The shell used to read `rowMenu?.context ?? surface(target)`:
346
+ * once a canonical table registered its default row descriptor — which every MatrxDataTable does,
347
+ * with `__entity: null` — the surface's resolver was never called on a row. So a list that names
348
+ * its row's record there (CRM `useCrmRowMenu`, the keyword tables) lost Attach To, Share and
349
+ * "Link a record…" on every row, and its own row doors with them (they are built from the state
350
+ * that resolver sets).
351
+ *
352
+ * · The row descriptor still owns the row's VALUES (content, heading, the full row): on a key
353
+ * both name, the descriptor stands (the 2026-09-26/27 heading rulings are unchanged).
354
+ * · The ENTITY is the surface's to name when the descriptor names none: the table knows a row,
355
+ * only the surface knows which record that row is. A descriptor that names its own entity
356
+ * keeps it.
357
+ * · Anything only the surface says is kept.
358
+ */
359
+ declare function joinRowAndSurfaceContext(row: ResolvedContextMenuContext | null | undefined, surface: ResolvedContextMenuContext | null | undefined): ResolvedContextMenuContext | null;
360
+ /**
361
+ * Read the right-clicked row's entity straight off the DOM.
362
+ *
363
+ * 🚨 WHY THIS EXISTS (Phase 0, 2026-08-25). `resolveContextOnOpen` is the
364
+ * precise path and remains it — but it made per-row identity cost a
365
+ * hand-written resolver on EVERY table. With ~500 surfaces still to wire that
366
+ * is ~500 bespoke `rowFor(target)` functions, each one a place to return the
367
+ * wrong row. A row that already knows what it is can now simply SAY so:
368
+ *
369
+ * <tr data-entity-type="seo_keyword" data-entity-id={k.id} data-entity-title={k.phrase}>
370
+ *
371
+ * and Attach To / Share target that record with no resolver at all.
372
+ *
373
+ * PRECEDENCE — THE SURFACE ALWAYS WINS. This runs ONLY when
374
+ * `resolveContextOnOpen` did not speak about the entity at all (key absent).
375
+ * An explicit `null` from the surface means "this target has no entity" and is
376
+ * honoured; the DOM never overrides a deliberate answer.
377
+ */
378
+ declare function sniffEntityFromDom(target: HTMLElement | null): ContextMenuEntityRef | null;
379
+ /**
380
+ * The effective entity for one open, DOM sniff included. Surface answer wins;
381
+ * the sniff only fills a silence.
382
+ */
383
+ declare function resolveEffectiveEntityWithDom(entityProp: ContextMenuEntityRef | undefined, resolved: ResolvedContextMenuContext | null, target: HTMLElement | null): ContextMenuEntityRef | undefined;
384
+
385
+ /**
386
+ * ONE MENU, SEVERAL SOURCES OF SURFACE ROWS — joined so every row id is unique.
387
+ *
388
+ * A menu's surface sections can come from more than one owner for one open: a table row's
389
+ * descriptor plus the grid's per-target sections, or the content's own sections plus the RECORD
390
+ * it belongs to (record-menu-registry.ts — a note's tab rows join the menu opened on the note's
391
+ * content). Each owner keeps its own ids unique, but not across owners, and the menu model
392
+ * namespaces every surface row as `x:<id>` (menu-model.ts) — so the same id from two owners
393
+ * reached the one Alchemy registry twice and it refused the second:
394
+ * `DuplicateActionError: Action "cm:x:save" is yielded twice by provider "context-menu:…"`
395
+ * (/notes, 2026-09-30: the editor's "Save" and the tab's "Save", both saving the same note).
396
+ *
397
+ * Sources are passed in precedence order. Two owners describing one target with the same id
398
+ * mean the same action on it, so the FIRST source's row stands and a later source's row with
399
+ * that id is not drawn a second time. A duplicate inside ONE source is that source's own defect
400
+ * and is left untouched, so the registry still refuses it loudly.
401
+ */
402
+
403
+ declare function joinExtraSections(...sources: Array<ContextMenuExtraSection[] | null | undefined>): ContextMenuExtraSection[] | undefined;
404
+
405
+ /**
406
+ * THE MENU OPENED IN A FIELD NAMES THE FIELD (page-pass 2026-09-27,
407
+ * /chat/message-templates/<id>). Right-clicking the Body box headed the menu
408
+ * "Content: {{reply.body}}" — the field's raw text. The header is now the
409
+ * field's own name ("Body"), with a short plain preview only when it helps:
410
+ * merge fields read as their names ("Reply body"), and text that is ONLY merge
411
+ * fields gets no preview (the name already says it). Pure, so it is tested
412
+ * without a DOM menu.
413
+ */
414
+ /** A field's name, the way a person reads it on screen. `null` when it has none. */
415
+ declare function fieldLabelOf(element: Element | null | undefined): string | null;
416
+ /** "reply.body" / "reply_body" → "Reply body". */
417
+ declare function mergeFieldName(token: string): string;
418
+ declare const FIELD_PREVIEW_MAX = 60;
419
+ /**
420
+ * The preview beside the field's name: plain words with merge fields named,
421
+ * clipped; `""` when the text holds nothing but merge fields (or nothing).
422
+ */
423
+ declare function fieldPreview(text: string): string;
424
+
425
+ interface KeyCombo {
426
+ alt: boolean;
427
+ shift: boolean;
428
+ ctrl: boolean;
429
+ meta: boolean;
430
+ /** `KeyboardEvent.code` for letters/digits ("KeyS", "Digit1"), else null. */
431
+ code: string | null;
432
+ /** Lowercased `KeyboardEvent.key` for named keys ("enter", "/"), else null. */
433
+ key: string | null;
434
+ }
435
+ /**
436
+ * Parse an advertised combo. Null when it cannot be a combo this listener can
437
+ * honour: no non-Shift modifier (plain typing), or no key.
438
+ */
439
+ declare function parseKeyCombo(raw: string | null | undefined): KeyCombo | null;
440
+ type KeyEventLike = Pick<KeyboardEvent, "altKey" | "shiftKey" | "ctrlKey" | "metaKey" | "code" | "key">;
441
+ declare function eventMatchesCombo(e: KeyEventLike, combo: KeyCombo): boolean;
442
+ /** First item whose advertised combo this key event presses. */
443
+ declare function findComboMatch<T>(e: KeyEventLike, items: readonly T[], comboOf: (item: T) => string | null | undefined): T | null;
444
+
445
+ interface BuildEditableWidgetHandleArgs {
446
+ getTextarea?: (() => HTMLTextAreaElement | null) | undefined;
447
+ onTextReplace?: ((newText: string) => void) | undefined;
448
+ onTextInsertBefore?: ((text: string) => void) | undefined;
449
+ onTextInsertAfter?: ((text: string) => void) | undefined;
450
+ /** Live scope builder — read for `content` when there is no textarea. */
451
+ getApplicationScope?: (() => ApplicationScope) | undefined;
452
+ }
453
+ /**
454
+ * Build the widget handle for an editable surface, or `null` when the surface
455
+ * exposes no way to write (nothing to register, no tools advertised).
456
+ */
457
+ declare function buildEditableWidgetHandle(args: BuildEditableWidgetHandleArgs): WidgetHandle | null;
458
+
459
+ interface BuildSelectionWriteBackArgs {
460
+ /** The text the agent was launched on (the selection, or the whole field). */
461
+ originalText: string;
462
+ /** "selection" when the person selected text; anything else = whole field. */
463
+ textSource: string;
464
+ selectionRange: SelectionRange | null | undefined;
465
+ onTextReplace?: ((fullValue: string) => void) | undefined;
466
+ onTextInsertAfter?: ((text: string) => void) | undefined;
467
+ insertAtCaret?: ((text: string) => boolean) | undefined;
468
+ }
469
+ /**
470
+ * Build the write-back for one launch, or null when the surface cannot be
471
+ * written (read-only, or nothing captured to write over).
472
+ */
473
+ declare function buildSelectionWriteBack(args: BuildSelectionWriteBackArgs): SelectionWriteBack | null;
474
+
475
+ /**
476
+ * `inline` goes exactly at the caret; `block` (a reference fence, a section)
477
+ * goes on its own line, never inside a word (G5 review, 2026-10-02: "of" became
478
+ * "o" + block + "f") — BEFORE the caret's line only from its very start,
479
+ * AFTER it otherwise (G11B: a right-click at the start of a line goes above;
480
+ * G15, 2026-10-07: a character "first half" rule surprised wrapped
481
+ * paragraphs). The one rule is `blockBoundary(…, "nearest")`
482
+ * (@ai-matrx/rich-editor ≥ 0.3.1).
483
+ */
484
+ type EditorInsertPlacement = "inline" | "block";
485
+ interface EditorInsertTargets {
486
+ /** A contentEditable editor addressed by `data-editor-id`. */
487
+ editorId?: string | undefined;
488
+ /** The surface's textarea — may return null when no textarea is mounted now. */
489
+ getTextarea?: (() => HTMLTextAreaElement | null) | undefined;
490
+ /**
491
+ * A rich editor's insert-at-the-caret. Returns false when it cannot take it.
492
+ * A `block` must land on its own line/paragraph, never inside a word.
493
+ */
494
+ insertAtCaret?: ((text: string, placement?: EditorInsertPlacement) => boolean) | undefined;
495
+ /** Full-value write-back for a controlled textarea. */
496
+ onTextReplace?: ((nextValue: string) => void) | undefined;
497
+ }
498
+ /** The text to insert, shaped per target (a textarea knows its neighbours). */
499
+ interface EditorInsertText {
500
+ editor: string;
501
+ textarea: (field: HTMLTextAreaElement) => string;
502
+ caret: string;
503
+ /** Default `inline`. */
504
+ placement?: EditorInsertPlacement | undefined;
505
+ }
506
+ type EditorInsertTarget = "editor" | "textarea" | "caret";
507
+ /** True when the surface advertises any write target at all. */
508
+ declare function hasEditorInsertTarget(targets: EditorInsertTargets): boolean;
509
+ /**
510
+ * Insert into whichever target is live right now. Returns the target that took
511
+ * the text, or null when none could (the caller then copies — never silent).
512
+ */
513
+ declare function insertIntoEditor(targets: EditorInsertTargets, text: string | EditorInsertText): EditorInsertTarget | null;
514
+ /**
515
+ * A block that must sit on its own paragraph inside a textarea: blank lines
516
+ * are added only where the neighbours do not already provide them.
517
+ */
518
+ declare function ownParagraph(block: string, field: HTMLTextAreaElement): string;
519
+
520
+ type Callbacks = RichDocumentActionContext["callbacks"];
521
+ type Request = NonNullable<NonNullable<Callbacks>["onRequestTextAgentAction"]>;
522
+ declare function editorTextAgentCallbacks(callbacks: Callbacks, source: ContentSource, adapter: Pick<ContentSourceAdapter, "edit">, request: Request | undefined): Callbacks;
523
+
524
+ declare function tableTextAtTarget(target: Element | null): string | null;
525
+
526
+ /**
527
+ * The attributes ContextMenuV3 stamps onto every right-click REGION it wraps
528
+ * (the chat composer textarea, every sidebar chat row, messages, table rows).
529
+ *
530
+ * ONE source: `ContextMenuV3.tsx` spreads this onto its trigger, and the real-
531
+ * browser layout gate (`features/shell/layout-gate/shipped-css-region-triggers.spec.ts`)
532
+ * renders its fixtures with the same object — so a stylesheet the app ships
533
+ * that styles these attributes (the 2026-09-26 `[data-alchemy-trigger] {
534
+ * width: 2rem }` collapse) is measured against exactly what production emits.
535
+ *
536
+ * Plain data, no React: the Playwright spec imports it directly.
537
+ */
538
+ declare const CONTEXT_REGION_TRIGGER_ATTRS: {
539
+ readonly "data-alchemy-trigger": "context";
540
+ };
541
+
542
+ /**
543
+ * Framework-owned launch defaults for agents listed in the managed "Agents"
544
+ * context-menu section. Shortcuts do not use this config: their persisted
545
+ * execution definitions remain authoritative.
546
+ */
547
+ declare const MANAGED_CONTEXT_MENU_AGENT_CONFIG: {
548
+ readonly displayMode: "flexible-panel";
549
+ readonly allowChat: true;
550
+ readonly showVariablePanel: true;
551
+ };
552
+
553
+ /**
554
+ * features/context-menu-v3/item/itemMenuToV3.ts
555
+ *
556
+ * ItemMenuConfig → v3 `extraSections` converter — the bridge that lets
557
+ * `ItemContextMenu` render the ONE universal context menu instead of its own
558
+ * Radix tree. Command/toggle execution reuses `run-entry.ts` (toast.promise
559
+ * parity with the kebab dropdown), checkbox/link map to the dedicated v3 item
560
+ * kinds, and section boundaries/labels carry over. Deliberate deltas from the
561
+ * old bespoke render (documented in FEATURE.md): no in-menu single-key
562
+ * shortcut execution (hints still display) and the header renders as a
563
+ * leading section label instead of a Radix label block.
564
+ */
565
+
566
+ declare const toastPromise: (promise: Promise<unknown>, messages: {
567
+ loading: string;
568
+ success: string;
569
+ error: (e: unknown) => string;
570
+ }) => void;
571
+
572
+ declare function itemMenuConfigToExtraSections(config: ItemMenuConfig): ContextMenuExtraSection[];
573
+
574
+ interface ContextMenuTargetHost {
575
+ kind: "context-menu";
576
+ /** The menu instance (one open menu = one provider registration). */
577
+ instanceId: string;
578
+ }
579
+ /** Composite host: a right-click target can carry both providers' halves. */
580
+ interface CompositeTargetHost {
581
+ contextMenu?: ContextMenuTargetHost | undefined;
582
+ richDocument?: unknown | undefined;
583
+ }
584
+ declare function contextMenuHostOf(target: ClickTarget): ContextMenuTargetHost | null;
585
+ interface ProviderOptions {
586
+ /**
587
+ * Resolves with the NEXT model the engine hook builds. A library still
588
+ * loading (the first agent fetch) waits on it instead of vanishing.
589
+ */
590
+ nextModel?: (() => Promise<MenuModel>) | undefined;
591
+ /**
592
+ * Whether THIS menu instance edits its surface (`isEditable`). A read-only
593
+ * wrapper over rich content still gets a writable rich-document target
594
+ * (actions may write back to the SOURCE), so `target.readOnly` alone cannot
595
+ * say "the person cannot type here" — this can (live 2026-09-27: a table
596
+ * row's menu still drew Cut / Paste / Undo / Redo).
597
+ */
598
+ editable?: boolean | undefined;
599
+ }
600
+ /**
601
+ * The model → the actions this menu instance contributes. Rich-document rows
602
+ * (`rich:` leaves) are skipped: the rich-document provider supplies them.
603
+ */
604
+ declare function contextMenuActionsFromModel(model: MenuModel, instanceId: string, opts?: ProviderOptions): Action[];
605
+ /**
606
+ * THE provider one open menu registers (AlchemyMenuContent). Every menu
607
+ * instance yields the same baseline ids (`cm:copy`, `cm:find`, …) and the
608
+ * Alchemy registry refuses an id a second provider yields — so an instance
609
+ * answers ONLY a target it opened. Two menus mounted at once (a message's menu
610
+ * nested in the page's, PB-06 2026-10-01: 18 `DuplicateActionError`s per
611
+ * open) each contribute to their own target and nothing to the other's.
612
+ */
613
+ declare function contextMenuProvider(instanceId: string, actions: () => readonly Action[]): ActionProvider;
614
+ /** A cheap fingerprint of what the menu would draw (re-resolve when it moves). */
615
+ declare function modelRevision(model: MenuModel): string;
616
+ /**
617
+ * The text the menu's "Content: …" header shows for what the menu acts on —
618
+ * without storage plumbing: the tutor's trust comment goes through the ONE
619
+ * strip copies and files use (live 2026-09-26: the header showed
620
+ * `<!--MATRX_TRUST_V1 …-->` on every tutor answer).
621
+ */
622
+ declare function menuHeaderContent(actionText: {
623
+ source: string;
624
+ text: string;
625
+ }): string | null;
626
+ /** The transcript store slice `chatMessageSubject` reads (RootState["messages"]). */
627
+ interface TranscriptStoreSlice {
628
+ messages?: {
629
+ byConversationId?: Record<string, {
630
+ byId?: Record<string, {
631
+ role?: unknown;
632
+ createdAt?: string | null;
633
+ } | undefined>;
634
+ } | undefined>;
635
+ };
636
+ }
637
+ /**
638
+ * THE MESSAGE A MENU WAS OPENED ON — one answer for every menu over a chat
639
+ * transcript, so the same message gets the same heading everywhere ("AI answer
640
+ * · Sep 27, 6:50 PM", "Your message · …").
641
+ *
642
+ * Two doors name a message and both land here:
643
+ * 1. the menu's own content source is that message (`chat-message` — the
644
+ * per-answer registry menu);
645
+ * 2. the menu is the transcript-level one and the right-click resolved the
646
+ * message under the pointer (`messageId` in the per-open context —
647
+ * `resolveMarkdownContext` reads the `data-message-id` every message root
648
+ * carries). Run History's user turns and any answer area outside the
649
+ * per-answer menu came through here and were headed "Content: <the whole
650
+ * text>" (2026-09-28).
651
+ */
652
+ declare function chatMessageSubject(args: {
653
+ state: TranscriptStoreSlice;
654
+ source: {
655
+ type: string;
656
+ conversationId?: string;
657
+ messageId?: string;
658
+ } | null | undefined;
659
+ extensions?: {
660
+ type?: string;
661
+ role?: unknown;
662
+ } | null;
663
+ contextData?: Record<string, unknown> | null | undefined;
664
+ }): {
665
+ role: string;
666
+ createdAt: string | null;
667
+ } | null;
668
+ /**
669
+ * The header for a menu opened in a FIELD with no selection: the field's name
670
+ * and, when it helps, a short plain preview (`utils/field-menu-header.ts`).
671
+ * Anything else keeps `menuHeaderContent`.
672
+ */
673
+ declare function menuHeader(actionText: {
674
+ source: string;
675
+ text: string;
676
+ }, fieldLabel: string | null,
677
+ /** The chat message the menu opened on, when it did (its role and when it was written). */
678
+ message?: {
679
+ role: string;
680
+ createdAt?: string | null;
681
+ } | null): {
682
+ content: string | null;
683
+ contentLabel: string | null;
684
+ };
685
+ /**
686
+ * The header is a one-line PREVIEW, so it shows words, never markdown syntax:
687
+ * "Content: # Clinic intake…" on /notes/<id> (page-pass 2026-09-27). Block
688
+ * markers (headings, quotes, list bullets, task boxes, fences) are dropped per
689
+ * line, inline emphasis goes through THE plain-text helper, and lines join
690
+ * with a space. Only the header changes — the actions still act on the text
691
+ * as written.
692
+ */
693
+ declare function headerPreview(text: string): string;
694
+ /**
695
+ * A target that NAMES itself (`CONTEXT_MENU_HEADING_KEY`: a list row's record)
696
+ * heads the menu with that name — "Quiz: Unit 2 review" — instead of the
697
+ * generic "Content: <agent context>" (page-pass 2026-09-27). A selection still
698
+ * shows itself.
699
+ */
700
+ declare function namedHeader(heading: {
701
+ label: string;
702
+ text: string;
703
+ } | null | undefined, actionText: {
704
+ source: string;
705
+ }): {
706
+ content: string;
707
+ contentLabel: string;
708
+ } | null;
709
+ /**
710
+ * The revision the alchemy menu engine re-resolves on. The engine
711
+ * (`useMenuEngine` in @ai-matrx/alchemy/react/menu) rebuilds its model — header
712
+ * included — only when its `revision` moves; `content` / `contentLabel` are not
713
+ * in its dependency list. A menu that stays mounted between opens therefore
714
+ * kept the FIRST header: "Note: New Note" after the note was renamed
715
+ * (G6B review, 2026-10-02). The header text is part of what the menu draws, so
716
+ * it is part of the revision.
717
+ */
718
+ declare function menuEngineRevision(drawn: string, header: {
719
+ content: string | null;
720
+ contentLabel: string | null;
721
+ }): string;
722
+
723
+ declare const CONTEXT_MENU_SELECTION_HOST_KEY = "contextMenuSelection";
724
+ interface ContextMenuSelectionHost {
725
+ kind: "context-menu-selection";
726
+ /** Open this menu instance over the current selection. */
727
+ open(): void;
728
+ }
729
+ declare const contextMenuSelectionProvider: ActionProvider;
730
+
731
+ export { type BuildEditableWidgetHandleArgs, type BuildSelectionWriteBackArgs, CONTEXT_MENU_ENGINE_ID_PREFIXES, CONTEXT_MENU_ENGINE_ROWS, CONTEXT_MENU_SELECTION_HOST_KEY, CONTEXT_REGION_TRIGGER_ATTRS, type ClickedKind, type CompositeTargetHost, type ContextMenuSelectionHost, type ContextMenuTargetHost, type EditorInsertPlacement, type EditorInsertTarget, type EditorInsertTargets, type EditorInsertText, FIELD_PREVIEW_MAX, type InventoryRow, type KeyCombo, MANAGED_CONTEXT_MENU_AGENT_CONFIG, type MenuCheckboxNode, type MenuGroup, MenuGrouping, type MenuHeader, type MenuItemNode, type MenuLabelNode, type MenuLeafNode, type MenuLinkNode, type MenuModel, type MenuNode, type MenuRoles, type MenuSection, type MenuSeparatorNode, type MenuSubmenuNode, OWN_ROWS_MAX, PROPOSED_MENU_GROUPING, type ProposedOptions, type ProviderOptions, RECORD_MENU_ATTR, type RecordMenuRows, STRIP_MAX, type TableRowEditCommands, type TableRowItemHit, type TableRowMenuDescriptor, type WhereTheMenuIs, actionsAlreadyHere, buildDefaultTableRowMenuDescriptor, buildEditableWidgetHandle, buildMenuModel, buildSelectionWriteBack, chatMessageSubject, clickedKind, contextMenuActionsFromModel, contextMenuHostOf, contextMenuProvider, contextMenuSelectionProvider, createDefaultTableRowMenuDescriptor, createTableRowMenuDescriptor, editorTextAgentCallbacks, eventMatchesCombo, fieldLabelOf, fieldPreview, findComboMatch, hasActionable, hasEditorInsertTarget, headerPreview, insertIntoEditor, itemMenuConfigToExtraSections, toastPromise as itemToastPromise, joinExtraSections, joinRowAndSurfaceContext, liftPrimarySections, menuEngineRevision, menuHeader, menuHeaderContent, mergeFieldName, mergeResolvedContextData, modelRevision, namedHeader, openContextMenuForElement, openRecordMenu, ownParagraph, parseKeyCombo, proposedArrangement, recordMenuChanged, recordMenusRevision, registerRecordMenu, registerTableRowContextResolver, registryTreeNodes, resolveEffectiveEntity, resolveEffectiveEntityWithDom, resolveRecordMenu, resolveTableRowItem, resolveTableRowMenuDescriptor, rowRecordName, sniffEntityFromDom, subscribeRecordMenus, tableRowEditCommands, tableTextAtTarget };