@ai-matrx/context-menu 0.1.9 → 0.1.11
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 +12 -0
- package/dist/builder.cjs +17 -18
- package/dist/builder.cjs.map +1 -1
- package/dist/builder.js +2 -3
- package/dist/builder.js.map +1 -1
- package/dist/{chunk-ECKA474B.js → chunk-2B37SXQW.js} +21 -12
- package/dist/chunk-2B37SXQW.js.map +1 -0
- package/dist/{chunk-TSOUI723.cjs → chunk-7TIZAANU.cjs} +26 -11
- package/dist/chunk-7TIZAANU.cjs.map +1 -0
- package/dist/{chunk-A24SBKSA.js → chunk-EG23Y2KW.js} +14 -13
- package/dist/{chunk-A24SBKSA.js.map → chunk-EG23Y2KW.js.map} +1 -1
- package/dist/{chunk-7VDSMZWI.cjs → chunk-ERFH2RZC.cjs} +18 -17
- package/dist/chunk-ERFH2RZC.cjs.map +1 -0
- package/dist/{chunk-EIP5YS4Y.js → chunk-HJU3RU2B.js} +27 -12
- package/dist/chunk-HJU3RU2B.js.map +1 -0
- package/dist/{chunk-SWEWOUHK.cjs → chunk-IH5WN5JP.cjs} +31 -26
- package/dist/chunk-IH5WN5JP.cjs.map +1 -0
- package/dist/{chunk-QXHLM7SS.cjs → chunk-LDMYNYFC.cjs} +42 -33
- package/dist/chunk-LDMYNYFC.cjs.map +1 -0
- package/dist/{chunk-ANJYKQAA.cjs → chunk-MBU4Y22O.cjs} +3 -3
- package/dist/{chunk-ANJYKQAA.cjs.map → chunk-MBU4Y22O.cjs.map} +1 -1
- package/dist/{chunk-L6DPISFT.js → chunk-NBEXJ25Z.js} +24 -19
- package/dist/{chunk-L6DPISFT.js.map → chunk-NBEXJ25Z.js.map} +1 -1
- package/dist/{chunk-LE6ZUVCO.js → chunk-PB3PMDKV.js} +2 -2
- package/dist/{chunk-NOYMFNSU.js → chunk-SJWKMZEJ.js} +33 -12
- package/dist/chunk-SJWKMZEJ.js.map +1 -0
- package/dist/{chunk-XERD2YCJ.cjs → chunk-WROBL3DR.cjs} +32 -11
- package/dist/chunk-WROBL3DR.cjs.map +1 -0
- package/dist/design-store.cjs +0 -1
- package/dist/design-store.cjs.map +1 -1
- package/dist/design-store.js +0 -1
- package/dist/design-store.js.map +1 -1
- package/dist/engine.cjs +64 -96
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +7 -7
- package/dist/engine.d.ts +7 -7
- package/dist/engine.js +72 -104
- package/dist/engine.js.map +1 -1
- package/dist/index.cjs +3 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -3
- package/dist/menu-content.cjs +18 -19
- package/dist/menu-content.cjs.map +1 -1
- package/dist/menu-content.js +4 -5
- package/dist/menu-content.js.map +1 -1
- package/dist/menu-inspect.cjs +0 -1
- package/dist/menu-inspect.cjs.map +1 -1
- package/dist/menu-inspect.js +0 -1
- package/dist/menu-inspect.js.map +1 -1
- package/dist/model.cjs +4 -5
- package/dist/model.cjs.map +1 -1
- package/dist/model.js +3 -4
- package/dist/react.cjs +52 -44
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +30 -22
- package/dist/react.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-7VDSMZWI.cjs.map +0 -1
- package/dist/chunk-CUODNUAD.cjs +0 -24
- package/dist/chunk-CUODNUAD.cjs.map +0 -1
- package/dist/chunk-ECKA474B.js.map +0 -1
- package/dist/chunk-EIP5YS4Y.js.map +0 -1
- package/dist/chunk-NOYMFNSU.js.map +0 -1
- package/dist/chunk-PE3ZHHM5.js +0 -24
- package/dist/chunk-PE3ZHHM5.js.map +0 -1
- package/dist/chunk-QXHLM7SS.cjs.map +0 -1
- package/dist/chunk-SWEWOUHK.cjs.map +0 -1
- package/dist/chunk-TSOUI723.cjs.map +0 -1
- package/dist/chunk-XERD2YCJ.cjs.map +0 -1
- /package/dist/{chunk-LE6ZUVCO.js.map → chunk-PB3PMDKV.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/engine/table-row-item.ts","../src/engine/record-menu-registry.ts","../src/engine/utils/per-row-entity.ts","../src/engine/utils/join-extra-sections.ts","../src/engine/utils/widget-handle.ts","../src/engine/utils/table-at-target.ts","../src/engine/region-trigger-attrs.ts","../src/engine/item/itemMenuToV3.ts","../src/engine/selection-provider.ts"],"sourcesContent":["/**\n * THE TABLE ROW IS A DECLARED ITEM (ALC-18, LIST.md D4).\n *\n * A canonical table's row is alchemy's `table_row` item type (`TABLE_ROW_ITEM_TYPE`, a baseline\n * item type of every surface — `features/surfaces/manifests/registry.ts`). Each mounted table\n * registers ONE item source with `@ai-matrx/alchemy/surface` `itemSources` under its table id; a\n * right-click resolves the row through that one path into the declared item (identity\n * `table_id` + `row_id`, the level, its values) plus the row's menu, which rides as the item's\n * opaque host data. There is no private registry here: this file only builds the row's menu and\n * reads the words the row shows.\n */\nimport { compact } from \"./utils/compact\";\nimport { CONTEXT_MENU_HEADING_KEY, type ContextMenuExtraSection, type ResolvedContextMenuContext } from \"./types\";\nimport { TABLE_ROW_ITEM } from \"@ai-matrx/alchemy/declare\";\nimport type { ResolvedItem } from \"@ai-matrx/alchemy/declare\";\nimport { itemSources } from \"@ai-matrx/alchemy/surface\";\nimport type { MatrxDataTableRecordControls } from \"@ai-matrx/design-system/data-table/types\";\nimport type {\n MatrxTableMenuLevel,\n MatrxTableMenuPayload,\n MatrxTableMenuTarget,\n} from \"@ai-matrx/design-system/data-table/menu-targets\";\n\nexport interface TableRowMenuDescriptor {\n context: ResolvedContextMenuContext;\n extraSections: ContextMenuExtraSection[];\n}\n\n/**\n * The row's menu as the table hands it back (the table types it `unknown` and never reads it). A\n * class, so a resolver answering anything else is told apart without a private lookup table.\n */\nclass TableRowMenu {\n constructor(readonly descriptor: TableRowMenuDescriptor) {}\n}\n\nexport function createTableRowMenuDescriptor(descriptor: TableRowMenuDescriptor): object {\n return new TableRowMenu(descriptor);\n}\n\n/** Exactly the row commands the default menu renders. Nothing else is read. */\nexport type TableRowEditCommands = Pick<\n MatrxDataTableRecordControls,\n \"beginEdit\" | \"saveEdits\" | \"cancelEdits\"\n>;\n\nfunction isCommand(value: unknown): value is () => void {\n return typeof value === \"function\";\n}\n\n/**\n * The table hands its host the row's controls as `unknown` — it never\n * interprets what the host builds from them — so they are READ here, one\n * command at a time. A command the table did not send, or sent as something\n * that cannot be called, is simply not offered rather than rendered dead.\n */\nexport function tableRowEditCommands(controls: unknown): TableRowEditCommands {\n if (typeof controls !== \"object\" || controls === null) return {};\n const read = (key: string): (() => void) | undefined => {\n const value: unknown = Reflect.get(controls, key);\n return isCommand(value) ? value : undefined;\n };\n return compact({\n beginEdit: read(\"beginEdit\"),\n saveEdits: read(\"saveEdits\"),\n cancelEdits: read(\"cancelEdits\"),\n });\n}\n\n/**\n * The host's generic menu model: complete current row data plus the\n * table-owned edit commands.\n *\n * The table asks this for whichever of its five right-click LEVELS was aimed\n * at. Only the row has a generic menu here, so every other level answers\n * `null` and the table leaves that level to the surface that owns it.\n */\nexport function createDefaultTableRowMenuDescriptor(\n payload: MatrxTableMenuPayload,\n): object | null {\n if (payload.level !== \"row\") return null;\n return createTableRowMenuDescriptor(\n buildDefaultTableRowMenuDescriptor(\n payload.row,\n tableRowEditCommands(payload.controls),\n ),\n );\n}\n\n/** Reusable base for a domain menu that keeps the shared row edit controls. */\nexport function buildDefaultTableRowMenuDescriptor(\n row: unknown,\n controls: TableRowEditCommands,\n): TableRowMenuDescriptor {\n const items = [\n ...(controls.beginEdit\n ? [{ kind: \"item\" as const, id: \"table-edit-row\", label: \"Edit row\", onSelect: controls.beginEdit }]\n : []),\n ...(controls.saveEdits\n ? [{ kind: \"item\" as const, id: \"table-save-row\", label: \"Save changes\", onSelect: () => void controls.saveEdits?.() }]\n : []),\n ...(controls.cancelEdits\n ? [{ kind: \"item\" as const, id: \"table-discard-row\", label: \"Discard changes\", onSelect: controls.cancelEdits }]\n : []),\n ];\n return {\n context: {\n content: JSON.stringify(row, null, 2) ?? String(row),\n context: row,\n __entity: null,\n },\n extraSections: items.length === 0\n ? []\n : [{ id: \"table-row\", label: \"Row\", primary: true, anchor: \"after-clipboard\", items }],\n };\n}\n\n/** The mounted row element of a table, read when the item's values are read. */\nfunction rowElement(tableId: string, rowId: string): HTMLElement | null {\n if (typeof document === \"undefined\") return null;\n const table = Array.from(document.querySelectorAll<HTMLElement>(\"[data-matrx-table-id]\")).find((el) => el.dataset.matrxTableId === tableId);\n return Array.from(table?.querySelectorAll<HTMLElement>(\"[data-row-id]\") ?? []).find((el) => el.dataset.rowId === rowId) ?? null;\n}\n\n/** A row is writable when its table offers the row's edit command. */\nfunction rowIsReadOnly(descriptor: TableRowMenuDescriptor): boolean {\n return !descriptor.extraSections.some((section) => section.items.some((item) => \"id\" in item && item.id === \"table-edit-row\"));\n}\n\n/**\n * The canonical table registers ONE item source per mounted instance, under its table id. The table\n * answers which of its right-click LEVELS was aimed at (`MatrxTableMenuTarget`); the source turns\n * that answer into the declared `table_row` item, with the row's menu as its host data.\n */\nexport function registerTableRowContextResolver(\n tableId: string,\n resolve: (target: MatrxTableMenuTarget) => unknown,\n) {\n return itemSources.register<TableRowMenuDescriptor>(tableId, (target) => {\n const token = resolve({\n tableId,\n level: target.level as MatrxTableMenuLevel,\n ...(target.itemId ? { rowId: target.itemId } : {}),\n ...(target.partId ? { columnId: target.partId } : {}),\n });\n if (!(token instanceof TableRowMenu)) return null;\n const descriptor = token.descriptor;\n const rowId = target.itemId ?? \"\";\n return {\n item: {\n itemType: TABLE_ROW_ITEM,\n identity: { table_id: tableId, row_id: rowId },\n level: target.level,\n readOnly: rowIsReadOnly(descriptor),\n readValues: async () => ({\n table_id: tableId,\n row_id: rowId,\n shown: shownWords(rowElement(tableId, rowId)),\n row: descriptor.context.context,\n }),\n },\n host: descriptor,\n };\n });\n}\n\n/** The clicked row as its declared item (raw-free) and the menu its table built for it. */\nexport interface TableRowItemHit {\n item: ResolvedItem;\n menu: TableRowMenuDescriptor;\n}\n\nexport function resolveTableRowItem(target: HTMLElement | null): TableRowItemHit | null {\n const row = target?.closest<HTMLElement>(\"[data-row-id]\");\n const tableId = row?.closest<HTMLElement>(\"[data-matrx-table-id]\")?.dataset.matrxTableId;\n const rowId = row?.dataset.rowId;\n if (!tableId || !rowId) return null;\n // This menu acts on the ROW it found in the DOM, so it asks for that level by name.\n const hit = itemSources.resolve<TableRowMenuDescriptor>({ containerId: tableId, level: \"row\", itemId: rowId });\n const descriptor = hit?.host ?? null;\n if (!hit || !descriptor) return null;\n const { readValues: _raw, ...item } = hit.item;\n return { item, menu: headedMenu(descriptor, target, row) };\n}\n\n/** The row's menu, or null when the click was not on a canonical table's row. */\nexport function resolveTableRowMenuDescriptor(target: HTMLElement | null): TableRowMenuDescriptor | null {\n return resolveTableRowItem(target)?.menu ?? null;\n}\n\nfunction headedMenu(\n descriptor: TableRowMenuDescriptor,\n target: HTMLElement | null,\n row: HTMLElement,\n): TableRowMenuDescriptor {\n // 🚨 THE HEADING IS WHAT THE PERSON RIGHT-CLICKED, IN THE WORDS ON SCREEN (merged-grid review\n // 2026-09-26): the heading read `Content: { \"id\": …, \"_choices\": …, \"level\": \"admin\",\n // \"hidden\": {} }` — the row's raw document. The clicked cell's shown text (\"Content: Priya\n // Nair\") is the heading; the row's words when the click was between cells. The full row stays\n // in `context.context` for the actions that read it.\n const cell = target?.closest<HTMLElement>(\"td, [role='gridcell']\");\n const shown = shownWords(cell ?? row);\n // THE HEADER NAMES THE ROW'S RECORD (admin pass 2026-09-27: \"Content: partial\" — the clicked\n // status cell). The descriptor's own `__heading` wins (a list row names \"Quiz: …\"); otherwise\n // the row's record name — its first data cell's primary line. What the actions act on\n // (`content`) stays the clicked cell's words.\n const ownHeading = (descriptor.context as Record<string, unknown>)[CONTEXT_MENU_HEADING_KEY];\n const recordName = ownHeading ? \"\" : rowRecordName(row);\n const context = {\n ...descriptor.context,\n ...(shown ? { content: shown } : {}),\n ...(recordName ? { [CONTEXT_MENU_HEADING_KEY]: { label: \"Row\", text: recordName } } : {}),\n };\n // THE ROW IS NAMED ONCE (admin final judge 2026-09-27: \"Row: Basic Editor\" then a \"Basic\n // Editor\" section heading right under it). A primary section labelled with the same name the\n // header already shows is headed \"Row\" instead — its rows stay first (an unnamed section\n // would lose its place; the package never draws a coined or empty heading).\n const headed = (ownHeading as { text?: unknown } | undefined)?.text ?? recordName;\n const same = (label: string | undefined) =>\n typeof headed === \"string\" && !!label && label.trim().toLowerCase() === headed.trim().toLowerCase();\n const extraSections = descriptor.extraSections.map((section) =>\n section.primary && same(section.label) ? { ...section, label: \"Row\" } : section,\n );\n const changedSections = extraSections.some((section, i) => section !== descriptor.extraSections[i]);\n return shown || recordName || changedSections ? { ...descriptor, context, extraSections } : descriptor;\n}\n\n/** The row's record name: the first data cell (not a tick box, star or button cell) with words. */\nexport function rowRecordName(row: HTMLElement | null | undefined): string {\n if (!row) return \"\";\n for (const cell of Array.from(row.querySelectorAll<HTMLElement>(\"td[data-matrx-table-column-id], [role='gridcell'][data-matrx-table-column-id]\"))) {\n const words = primaryLine(cell);\n if (words) return words.length > 120 ? `${words.slice(0, 119)}…` : words;\n }\n return \"\";\n}\n\n/** Elements that start a new LINE of what a cell shows (name, then its subtitle). */\nconst LINE_BREAKING = \"div, p, li, br, tr, h1, h2, h3, h4, h5, h6, section, article, header, footer, dd, dt\";\n\n/**\n * The PRIMARY line an element shows — its first line, without its controls.\n * `textContent` glues a cell's stacked lines together with no space\n * (\"Data Destruction, Inc.Company re…\", \"Notesmatrx-user/notes\"; page-pass\n * 2026-09-27), so each line-starting element is marked with a newline first and\n * only the first non-empty line is kept. Inline markup inside a line\n * (<b>, <a>, <span>) is untouched, so words are never split.\n */\nfunction primaryLine(element: Element): string {\n const copy = element.cloneNode(true) as HTMLElement;\n copy.querySelectorAll(\"button, [aria-hidden='true'], input, textarea, select\").forEach((node) => node.remove());\n copy.querySelectorAll(LINE_BREAKING).forEach((node) => node.before(\"\\n\"));\n const lines = (copy.textContent ?? \"\")\n .split(\"\\n\")\n .map((line) => line.replace(/\\s+/g, \" \").trim())\n .filter((line) => line && line !== \"—\");\n return lines[0] ?? \"\";\n}\n\n/** The words an element shows: one cell's primary line, or each cell's, joined with \" · \". */\nfunction shownWords(element: HTMLElement | null | undefined): string {\n if (!element) return \"\";\n const cells = element.querySelectorAll(\"td, [role='gridcell']\");\n const text = cells.length > 1\n ? Array.from(cells).map(primaryLine).filter(Boolean).join(\" · \")\n : primaryLine(element);\n return text.length > 200 ? `${text.slice(0, 199)}…` : text;\n}\n","/**\n * A RECORD'S ROWS, IN THE ONE MENU OF THE CONTENT THAT SHOWS IT (R26; ALC-15 round 5).\n *\n * A record's toolbar can live apart from the content it belongs to: on /notes the tab strip holds the\n * note's ⋯ (Save, Duplicate, Move, Delete, Close tab…) while the note's content is drawn in the pane\n * below. The ⋯ and a right-click on that content target the SAME thing — the note — so they must be\n * one menu (measured 2026-09-27: 27 rows from the tab's ⋯, 25 from the content, each missing the\n * other's rows).\n *\n * · The owner of the rows registers them under a key: `registerRecordMenu(key, get)`.\n * · The host marks the content's root with `data-record-menu=\"<key>\"`.\n * · Every menu opened inside that root carries the registered sections (and the record's entity,\n * when the menu has none of its own) — the shell reads them at open (ContextMenuV3).\n * · The record's ⋯ calls `openRecordMenu(key, button)`: it opens the content's OWN menu at the\n * button. `false` = that content is not on screen; the caller opens its own menu instead.\n *\n * Plain data, no React.\n */\nimport type { ContextMenuEntityRef, ContextMenuExtraSection } from \"./types\";\n\nexport const RECORD_MENU_ATTR = \"data-record-menu\";\n\nexport interface RecordMenuRows {\n entity?: ContextMenuEntityRef | null | undefined;\n extraSections: ContextMenuExtraSection[];\n /**\n * The record's own name for the menu header (\"Note · Clinic intake\n * checklist\") — used when the clicked content names nothing itself, so the\n * header names the record instead of quoting its body. A selection still wins.\n */\n heading?: { label: string; text: string } | null;\n}\n\nconst registry = new Map<string, () => RecordMenuRows>();\n\n/**\n * A record's rows are pulled at open, but its NAME can change while a menu is\n * open or mounted (the note auto-labels from its first words 3 s after typing;\n * a person renames it). The owner calls `recordMenuChanged()` when what it\n * registers changes, and an open menu re-reads the heading (G6B review,\n * 2026-10-02: \"Note: New Note\" after the note was renamed).\n */\nlet revision = 0;\nconst listeners = new Set<() => void>();\n\n/** The owner of registered rows says they changed (its record was renamed). */\nexport function recordMenuChanged(): void {\n revision += 1;\n for (const listener of listeners) listener();\n}\n\n/** For `useSyncExternalStore`: re-render when any registered record changes. */\nexport function subscribeRecordMenus(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function recordMenusRevision(): number {\n return revision;\n}\n\n/** Register the rows a record contributes; returns the unregister. Read at every open, so always current. */\nexport function registerRecordMenu(key: string, get: () => RecordMenuRows): () => void {\n registry.set(key, get);\n return () => {\n if (registry.get(key) === get) registry.delete(key);\n };\n}\n\n/** The rows of the record whose content holds `target` (nearest marked root), or null. */\nexport function resolveRecordMenu(target: Element | null): RecordMenuRows | null {\n const key = target?.closest?.(`[${RECORD_MENU_ATTR}]`)?.getAttribute(RECORD_MENU_ATTR);\n if (!key) return null;\n return registry.get(key)?.() ?? null;\n}\n\nconst TRIGGER = '[data-alchemy-trigger=\"context\"]';\n\n/**\n * The right-click trigger a click on the record's content lands in: the first menu trigger inside\n * the marked root that carries content (`data-content-source`), else the root's own trigger.\n */\nfunction contentTrigger(root: HTMLElement): HTMLElement | null {\n const inner = root.querySelector<HTMLElement>(`${TRIGGER}[data-content-source]`);\n if (inner) return inner;\n if (root.matches(TRIGGER)) return root;\n return root.querySelector<HTMLElement>(TRIGGER) ?? root.closest<HTMLElement>(TRIGGER);\n}\n\n/**\n * Open the record content's own menu at `anchor` (a ⋯ button). Returns false when no content for\n * `key` is on screen, so the caller can open its own menu instead.\n */\nexport function openRecordMenu(key: string, anchor: HTMLElement): boolean {\n const doc = anchor.ownerDocument;\n const root = [...doc.querySelectorAll<HTMLElement>(`[${RECORD_MENU_ATTR}]`)].find(\n (el) => el.getAttribute(RECORD_MENU_ATTR) === key,\n );\n const trigger = root ? contentTrigger(root) : null;\n if (!trigger) return false;\n const rect = anchor.getBoundingClientRect();\n trigger.dispatchEvent(\n new MouseEvent(\"contextmenu\", {\n bubbles: true,\n cancelable: true,\n view: doc.defaultView ?? window,\n button: 2,\n buttons: 2,\n clientX: rect.left + rect.width / 2,\n clientY: rect.bottom,\n }),\n );\n return true;\n}\n","// features/context-menu-v3/utils/per-row-entity.ts\n//\n// THE PER-ROW ENTITY — the delegated-menu half of the entity contract.\n//\n// A table wires ONE menu for the whole pane (`resolveContextOnOpen`), so the\n// menu-level `entity` prop can never name the row that was actually\n// right-clicked: before 2026-08-24 Attach To / Share on a table menu silently\n// targeted the pane's entity, or rendered nothing at all. `resolveContextOnOpen`\n// may now return the reserved `CONTEXT_MENU_ENTITY_KEY` and the shell rebuilds\n// the entity-bound actions from it.\n//\n// Kept in its own module (not `value-resolution.ts`) on purpose: the INERT\n// SHELL imports this, and value-resolution pulls the surface-manifest registry.\n// Nothing here may import anything heavier than types.\n\nimport {\n CONTEXT_MENU_ENTITY_KEY,\n type ContextMenuEntityRef,\n type ResolvedContextMenuContext,\n} from \"../types\";\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * The effective entity for ONE menu open.\n *\n * key absent → the menu-level prop stands (single-entity surfaces unchanged)\n * key present → that row's entity wins\n * key `null` → this target has no entity, so the entity-bound actions hide\n * rather than target the wrong record\n *\n * A malformed value (no string `type` + `id`) is treated as absent and SCREAMS\n * in dev — a half-built entity would render an Attach that writes a broken edge.\n */\nexport function resolveEffectiveEntity(\n entityProp: ContextMenuEntityRef | undefined,\n resolved: ResolvedContextMenuContext | null,\n): ContextMenuEntityRef | undefined {\n if (!resolved || !(CONTEXT_MENU_ENTITY_KEY in resolved)) return entityProp;\n const perRow = resolved[CONTEXT_MENU_ENTITY_KEY];\n if (perRow === null || perRow === undefined) return undefined;\n const candidate = perRow as ContextMenuEntityRef;\n if (\n typeof perRow !== \"object\" ||\n typeof candidate.type !== \"string\" ||\n !candidate.type ||\n typeof candidate.id !== \"string\" ||\n !candidate.id\n ) {\n if (isDev) {\n console.error(\n `%c[ContextMenuV3] MALFORMED PER-ROW ENTITY%c — resolveContextOnOpen returned \"${CONTEXT_MENU_ENTITY_KEY}\" without a string type + id, so Attach To / Share would target nothing. Falling back to the menu-level entity.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { received: perRow },\n );\n }\n return entityProp;\n }\n return candidate;\n}\n\n/**\n * The effective `contextData` for one open: static payload + per-target merge,\n * with the reserved entity key stripped so it never lands in the\n * `ApplicationScope` as a value.\n */\nexport function mergeResolvedContextData(\n contextData: Record<string, unknown> | undefined,\n resolved: ResolvedContextMenuContext | null,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = {\n ...(contextData ?? {}),\n ...(resolved ?? {}),\n };\n delete merged[CONTEXT_MENU_ENTITY_KEY];\n return merged;\n}\n\n// ---------------------------------------------------------------------------\n// A TABLE ROW HAS TWO OWNERS — the table and the surface that mounted it\n// ---------------------------------------------------------------------------\n\n/**\n * The context for ONE open on a table row: the table's own row descriptor\n * (table-row-item.ts) JOINED with the surface's `resolveContextOnOpen` answer.\n *\n * 🚨 WHY (2026-10-03, CHAIR-REACH). The shell used to read `rowMenu?.context ?? surface(target)`:\n * once a canonical table registered its default row descriptor — which every MatrxDataTable does,\n * with `__entity: null` — the surface's resolver was never called on a row. So a list that names\n * its row's record there (CRM `useCrmRowMenu`, the keyword tables) lost Attach To, Share and\n * \"Link a record…\" on every row, and its own row doors with them (they are built from the state\n * that resolver sets).\n *\n * · The row descriptor still owns the row's VALUES (content, heading, the full row): on a key\n * both name, the descriptor stands (the 2026-09-26/27 heading rulings are unchanged).\n * · The ENTITY is the surface's to name when the descriptor names none: the table knows a row,\n * only the surface knows which record that row is. A descriptor that names its own entity\n * keeps it.\n * · Anything only the surface says is kept.\n */\nexport function joinRowAndSurfaceContext(\n row: ResolvedContextMenuContext | null | undefined,\n surface: ResolvedContextMenuContext | null | undefined,\n): ResolvedContextMenuContext | null {\n if (!row) return surface ?? null;\n if (!surface) return row;\n const rowNamesEntity = row[CONTEXT_MENU_ENTITY_KEY] !== null && row[CONTEXT_MENU_ENTITY_KEY] !== undefined;\n return {\n ...surface,\n ...row,\n ...(!rowNamesEntity && CONTEXT_MENU_ENTITY_KEY in surface\n ? { [CONTEXT_MENU_ENTITY_KEY]: surface[CONTEXT_MENU_ENTITY_KEY] }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// THE DOM SNIFFER — per-row identity with no resolver code\n// ---------------------------------------------------------------------------\n\n/**\n * A token SHAPE check, deliberately not a registry lookup.\n *\n * `isEntityTypeToken` would be the precise guard, but it lives in the 2,500-line\n * generated entity module and this file loads in the INERT SHELL (see the header\n * law). A shape check catches the realistic failure — a typo, a label, a raw\n * uuid in the wrong attribute — at zero weight; a well-formed but unregistered\n * token is caught downstream by Attach To's own write path.\n */\nconst TOKEN_SHAPE = /^[a-z][a-z0-9_]*$/;\n\n/**\n * Read the right-clicked row's entity straight off the DOM.\n *\n * 🚨 WHY THIS EXISTS (Phase 0, 2026-08-25). `resolveContextOnOpen` is the\n * precise path and remains it — but it made per-row identity cost a\n * hand-written resolver on EVERY table. With ~500 surfaces still to wire that\n * is ~500 bespoke `rowFor(target)` functions, each one a place to return the\n * wrong row. A row that already knows what it is can now simply SAY so:\n *\n * <tr data-entity-type=\"seo_keyword\" data-entity-id={k.id} data-entity-title={k.phrase}>\n *\n * and Attach To / Share target that record with no resolver at all.\n *\n * PRECEDENCE — THE SURFACE ALWAYS WINS. This runs ONLY when\n * `resolveContextOnOpen` did not speak about the entity at all (key absent).\n * An explicit `null` from the surface means \"this target has no entity\" and is\n * honoured; the DOM never overrides a deliberate answer.\n */\nexport function sniffEntityFromDom(\n target: HTMLElement | null,\n): ContextMenuEntityRef | null {\n const host = target?.closest?.(\"[data-entity-type][data-entity-id]\");\n if (!(host instanceof HTMLElement)) return null;\n\n const type = host.getAttribute(\"data-entity-type\")?.trim() ?? \"\";\n const id = host.getAttribute(\"data-entity-id\")?.trim() ?? \"\";\n if (!type || !id) return null;\n\n if (!TOKEN_SHAPE.test(type)) {\n if (isDev) {\n console.error(\n `%c[ContextMenuV3] MALFORMED data-entity-type%c — \"${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.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { element: host },\n );\n }\n return null;\n }\n\n // Title is what the user sees the row called; the row's own text is the\n // honest fallback so an Attach is never labelled with a raw uuid.\n const title =\n host.getAttribute(\"data-entity-title\")?.trim() ||\n (host.textContent ?? \"\").trim().slice(0, 120) ||\n id;\n\n const resourceType = host.getAttribute(\"data-entity-resource\")?.trim();\n\n return {\n type: type as ContextMenuEntityRef[\"type\"],\n id,\n title,\n ...(resourceType\n ? { resourceType: resourceType as NonNullable<ContextMenuEntityRef[\"resourceType\"]> }\n : {}),\n };\n}\n\n/**\n * The effective entity for one open, DOM sniff included. Surface answer wins;\n * the sniff only fills a silence.\n */\nexport function resolveEffectiveEntityWithDom(\n entityProp: ContextMenuEntityRef | undefined,\n resolved: ResolvedContextMenuContext | null,\n target: HTMLElement | null,\n): ContextMenuEntityRef | undefined {\n const surfaceSpoke = !!resolved && CONTEXT_MENU_ENTITY_KEY in resolved;\n if (surfaceSpoke) return resolveEffectiveEntity(entityProp, resolved);\n return sniffEntityFromDom(target) ?? entityProp;\n}\n","/**\n * ONE MENU, SEVERAL SOURCES OF SURFACE ROWS — joined so every row id is unique.\n *\n * A menu's surface sections can come from more than one owner for one open: a table row's\n * descriptor plus the grid's per-target sections, or the content's own sections plus the RECORD\n * it belongs to (record-menu-registry.ts — a note's tab rows join the menu opened on the note's\n * content). Each owner keeps its own ids unique, but not across owners, and the menu model\n * namespaces every surface row as `x:<id>` (menu-model.ts) — so the same id from two owners\n * reached the one Alchemy registry twice and it refused the second:\n * `DuplicateActionError: Action \"cm:x:save\" is yielded twice by provider \"context-menu:…\"`\n * (/notes, 2026-09-30: the editor's \"Save\" and the tab's \"Save\", both saving the same note).\n *\n * Sources are passed in precedence order. Two owners describing one target with the same id\n * mean the same action on it, so the FIRST source's row stands and a later source's row with\n * that id is not drawn a second time. A duplicate inside ONE source is that source's own defect\n * and is left untouched, so the registry still refuses it loudly.\n */\nimport type { ContextMenuExtraItem, ContextMenuExtraSection } from \"../types\";\n\nfunction isRow(item: ContextMenuExtraItem): boolean {\n return item.kind !== \"separator\";\n}\n\n/** Drop leading, trailing and doubled separators left behind when rows are removed. */\nfunction tidySeparators(items: ContextMenuExtraItem[]): ContextMenuExtraItem[] {\n const out: ContextMenuExtraItem[] = [];\n for (const item of items) {\n if (!isRow(item) && (out.length === 0 || !isRow(out[out.length - 1]!))) continue;\n out.push(item);\n }\n while (out.length > 0 && !isRow(out[out.length - 1]!)) out.pop();\n return out;\n}\n\nexport function joinExtraSections(\n ...sources: Array<ContextMenuExtraSection[] | null | undefined>\n): ContextMenuExtraSection[] | undefined {\n const present = sources.filter((s): s is ContextMenuExtraSection[] => Array.isArray(s));\n if (present.length === 0) return undefined;\n if (present.length === 1) return present[0];\n const taken = new Set<string>();\n const out: ContextMenuExtraSection[] = [];\n for (const source of present) {\n const ownIds = new Set<string>();\n for (const section of source) {\n const kept = section.items.filter((item) => !isRow(item) || !taken.has(item.id));\n for (const item of kept) if (isRow(item)) ownIds.add(item.id);\n if (kept.length === section.items.length) {\n out.push(section);\n continue;\n }\n const items = tidySeparators(kept);\n if (items.some(isRow)) out.push({ ...section, items });\n }\n for (const id of ownIds) taken.add(id);\n }\n return out;\n}\n","// features/context-menu-v3/utils/widget-handle.ts\n//\n// Inline agent editing — the v1 promise, paid via the platform channel.\n//\n// Builds a `WidgetHandle` (the canonical client-tool contract — see\n// `features/agents/types/widget-handle.types.ts` and CLIENT_SIDE_TOOLS.md)\n// from the SAME props an editable surface already passes to\n// `EditableContextMenu` (`onTextReplace` / `onTextInsertBefore|After` /\n// `getTextarea`). The shell registers it via `useOptionalWidgetHandle` and the\n// launch handlers pass the id as `runtime.widgetHandleId`, so any agent\n// launched from the menu on an editable surface can stream `widget_text_*`\n// tool calls that edit the surface's content in place — zero extra wiring per\n// surface, no bespoke edit protocol (the old XML-in-stream idea stays dead).\n//\n// Precedence: a surface callback (controlled React state) always wins; the\n// fallback writes through `setFieldValue` (native setter + input event), which\n// works on BOTH controlled and uncontrolled textareas. Only methods this\n// surface can actually service are present — the per-turn assembler\n// (`deriveClientToolsFromHandle`) advertises exactly that subset.\n\nimport type { WidgetHandle } from \"@ai-matrx/chat/agents/types/widget-handle.types\";\nimport type { ApplicationScope } from \"@ai-matrx/chat/agents/types/scope.types\";\n\n/**\n * Write a value into a textarea so BOTH controlled and uncontrolled fields\n * take it: the native value setter bypasses React's value tracker, and the\n * bubbled `input` event makes a controlled component's onChange fire (state\n * updates instead of React reverting the DOM on the next render). A bare\n * `element.value = x` write silently loses the edit on controlled fields —\n * while the tool result still reports ok:true to the agent.\n */\nfunction setFieldValue(\n el: HTMLTextAreaElement,\n next: string,\n caret: number,\n): void {\n const setter = Object.getOwnPropertyDescriptor(\n HTMLTextAreaElement.prototype,\n \"value\",\n )?.set;\n if (setter) setter.call(el, next);\n else el.value = next;\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n try {\n el.setSelectionRange(caret, caret);\n } catch {\n // non-focusable / detached — caret is best-effort\n }\n}\n\nexport interface BuildEditableWidgetHandleArgs {\n getTextarea?: (() => HTMLTextAreaElement | null) | undefined;\n onTextReplace?: ((newText: string) => void) | undefined;\n onTextInsertBefore?: ((text: string) => void) | undefined;\n onTextInsertAfter?: ((text: string) => void) | undefined;\n /** Live scope builder — read for `content` when there is no textarea. */\n getApplicationScope?: (() => ApplicationScope) | undefined;\n}\n\n/**\n * Build the widget handle for an editable surface, or `null` when the surface\n * exposes no way to write (nothing to register, no tools advertised).\n */\nexport function buildEditableWidgetHandle(\n args: BuildEditableWidgetHandleArgs,\n): WidgetHandle | null {\n const {\n getTextarea,\n onTextReplace,\n onTextInsertBefore,\n onTextInsertAfter,\n getApplicationScope,\n } = args;\n\n const field = (): HTMLTextAreaElement | null => getTextarea?.() ?? null;\n\n const readCurrent = (): string | null => {\n const el = field();\n if (el) return el.value;\n const content = getApplicationScope?.().content;\n return typeof content === \"string\" ? content : null;\n };\n\n // Full-content write. Surface callback wins; the field fallback goes\n // through setFieldValue so controlled AND uncontrolled textareas take it.\n const writeFull = (next: string): void => {\n if (onTextReplace) {\n onTextReplace(next);\n return;\n }\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n setFieldValue(el, next, next.length);\n };\n\n // Capability PRESENCE only — this function runs in the shell's render body,\n // so it must never invoke a surface callback (getTextarea reads the DOM,\n // getApplicationScope may serialize the whole document; per-keystroke cost\n // + a React render-purity violation). The method bodies re-check liveness\n // (readCurrent() null checks) at call time, which is the only time it matters.\n const canWrite = Boolean(onTextReplace) || Boolean(getTextarea);\n const canRead = Boolean(getTextarea) || Boolean(getApplicationScope);\n if (!canWrite) return null;\n\n // No `applyPolicy`: an agent's write into the person's own text is `ask` —\n // the approval card shows `readText()` as the \"before\".\n const handle: WidgetHandle = {\n onTextReplace: ({ text }) => writeFull(text),\n ...(canRead ? { readText: readCurrent } : {}),\n };\n\n // Insert relative to the cursor/selection. Surface callbacks carry their\n // own semantics; the fallback splices at the field's live cursor.\n if (onTextInsertBefore || getTextarea) {\n handle.onTextInsertBefore = ({ text }) => {\n if (onTextInsertBefore) return onTextInsertBefore(text);\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n const at = el.selectionStart ?? 0;\n setFieldValue(\n el,\n el.value.slice(0, at) + text + el.value.slice(at),\n at + text.length,\n );\n };\n }\n if (onTextInsertAfter || getTextarea) {\n handle.onTextInsertAfter = ({ text }) => {\n if (onTextInsertAfter) return onTextInsertAfter(text);\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n const at = el.selectionEnd ?? el.value.length;\n setFieldValue(\n el,\n el.value.slice(0, at) + text + el.value.slice(at),\n at + text.length,\n );\n };\n }\n\n // Whole-content operations need read + write.\n if (canRead) {\n handle.onTextPrepend = ({ text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n writeFull(text + current);\n };\n handle.onTextAppend = ({ text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n writeFull(current + text);\n };\n handle.onTextPatch = ({ search_text, replacement_text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n const idx = current.indexOf(search_text);\n if (idx === -1)\n // access-errors: ok — in-memory text-patch match against the surface's own content, no record read\n throw new Error(\"search_text not found in surface content\");\n writeFull(\n current.slice(0, idx) +\n replacement_text +\n current.slice(idx + search_text.length),\n );\n };\n }\n\n return handle;\n}\n","// features/context-menu-v3/utils/table-at-target.ts\n//\n// THE TABLE A RIGHT-CLICK LANDED ON, with its shape kept (header + rows). A chat answer's menu is\n// one menu over the whole answer, so \"Save to a table\" read the answer's text and opened on its\n// FIRST shape — a bullet list above the table won, and the table the person right-clicked was\n// never offered first. The shell hands the rich-document registry this reader (callbacks\n// `tableAtTarget`); the one shape reader (`shapeTextOfNode`, records-ui) turns the clicked\n// <table> back into table text, streamed or finished — it reads the rendered DOM.\n\n// The shape reader is imported only when a right-click lands on a <table> (the shell calls\n// `preloadTableShape` from its open path), never with the page: a menu is opened on ~1 in 149\n// page loads and most opens are not on a table.\ntype ShapeReader = (node: Node) => string;\nlet shapeReader: ShapeReader | null = null;\nlet shapeLoad: Promise<unknown> | null = null;\n\nexport function preloadTableShape(): Promise<unknown> {\n shapeLoad ??= import(\"@ai-matrx/records-ui/table-shape\")\n .then((m) => {\n shapeReader = m.shapeTextOfNode as ShapeReader;\n })\n .catch((error: unknown) => {\n shapeLoad = null;\n console.error(\"[ContextMenuV3] could not load the table shape reader\", error);\n });\n return shapeLoad;\n}\n\n/** Sync; null until the reader has loaded (a click on a table starts the load at open). */\nexport function tableTextAtTarget(target: Element | null): string | null {\n const table = target?.closest(\"table\");\n if (!table || !shapeReader) return null;\n const text = shapeReader(table.cloneNode(true));\n return text.trim() ? text : null;\n}\n","/**\n * The attributes ContextMenuV3 stamps onto every right-click REGION it wraps\n * (the chat composer textarea, every sidebar chat row, messages, table rows).\n *\n * ONE source: `ContextMenuV3.tsx` spreads this onto its trigger, and the real-\n * browser layout gate (`features/shell/layout-gate/shipped-css-region-triggers.spec.ts`)\n * renders its fixtures with the same object — so a stylesheet the app ships\n * that styles these attributes (the 2026-09-26 `[data-alchemy-trigger] {\n * width: 2rem }` collapse) is measured against exactly what production emits.\n *\n * Plain data, no React: the Playwright spec imports it directly.\n */\nexport const CONTEXT_REGION_TRIGGER_ATTRS = {\n \"data-alchemy-trigger\": \"context\",\n} as const;\n","/**\n * features/context-menu-v3/item/itemMenuToV3.ts\n *\n * ItemMenuConfig → v3 `extraSections` converter — the bridge that lets\n * `ItemContextMenu` render the ONE universal context menu instead of its own\n * Radix tree. Command/toggle execution reuses `run-entry.ts` (toast.promise\n * parity with the kebab dropdown), checkbox/link map to the dedicated v3 item\n * kinds, and section boundaries/labels carry over. Deliberate deltas from the\n * old bespoke render (documented in FEATURE.md): no in-menu single-key\n * shortcut execution (hints still display) and the header renders as a\n * leading section label instead of a Radix label block.\n */\n\nimport { compact } from \"../utils/compact\";\nimport type {\n ContextMenuExtraItem,\n ContextMenuExtraSection,\n} from \"../types\";\nimport {\n isCheckbox,\n isLink,\n isSubmenu,\n runCommand as runItemCommand,\n runToggle,\n type ItemMenuCommand,\n type ItemMenuConfig,\n type ItemMenuEntry,\n type ItemMenuSection,\n} from \"@ai-matrx/design-system/item\";\nimport { toast } from \"@ai-matrx/chat/host/notify\";\n\n// design-system's item icon is any component taking a className; the v3 menu's icon slot takes the same.\nconst iconOf = (entry: ItemMenuEntry) => entry.icon;\n\nconst toastPromise = (promise: Promise<unknown>, messages: { loading: string; success: string; error: (e: unknown) => string }) => {\n toast.promise(promise, messages);\n};\n\nfunction runCommand(entry: ItemMenuCommand) {\n runItemCommand(entry, toastPromise);\n}\n\nexport { toastPromise as itemToastPromise };\n\nfunction entryToExtraItem(entry: ItemMenuEntry): ContextMenuExtraItem {\n const description =\n entry.disabled && entry.disabledReason\n ? entry.disabledReason\n : entry.description;\n\n if (isSubmenu(entry)) {\n return compact({\n kind: \"submenu\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n disabled: entry.disabled,\n children: submenuChildren(entry.sections),\n }) as ContextMenuExtraItem;\n }\n if (isCheckbox(entry)) {\n return compact({\n kind: \"checkbox\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n checked: entry.checked,\n onCheckedChange: (next: boolean) => runToggle(entry, next),\n disabled: entry.disabled,\n hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),\n }) as ContextMenuExtraItem;\n }\n if (isLink(entry)) {\n return compact({\n kind: \"link\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n href: entry.href,\n target: entry.target,\n disabled: entry.disabled,\n }) as ContextMenuExtraItem;\n }\n // command\n return compact({\n kind: \"item\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n destructive: entry.tone === \"destructive\",\n disabled: entry.disabled,\n hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),\n onSelect: () => runCommand(entry),\n }) as ContextMenuExtraItem;\n}\n\n/** Flatten submenu sections into children with separators at boundaries. */\nfunction submenuChildren(sections: ItemMenuSection[]): ContextMenuExtraItem[] {\n const visible = sections\n .map((s) => ({ ...s, items: s.items.filter((e) => !e.hidden) }))\n .filter((s) => s.items.length > 0);\n const out: ContextMenuExtraItem[] = [];\n visible.forEach((section, i) => {\n if (i > 0)\n out.push({\n kind: \"separator\",\n id: `sep-${section.id ?? section.label ?? i}`,\n });\n out.push(...section.items.map(entryToExtraItem));\n });\n return out;\n}\n\nexport function itemMenuConfigToExtraSections(\n config: ItemMenuConfig,\n): ContextMenuExtraSection[] {\n const sections: ContextMenuExtraSection[] = config.sections\n .map((s, i) => compact({\n id: String(s.id ?? s.label ?? `im-${i}`),\n label: s.label,\n anchor: \"after-clipboard\" as const,\n items: s.items.filter((e) => !e.hidden).map(entryToExtraItem),\n }))\n .filter((s) => s.items.length > 0);\n\n // Header → leading label on the first section (or its own labeled block).\n const title = config.header?.title;\n const first = sections[0];\n if (title && first && !first.label) {\n sections[0] = { ...first, label: title };\n }\n return sections;\n}\n","// features/context-menu-v3/selection-provider.ts\n//\n// The context menu's one entry in the ONE selection toolbar\n// (components/selection-toolbar): \"Ask AI\" — it opens the same Alchemy menu a\n// right-click opens, over the selected text (the AI actions, the agent\n// libraries, copy / export / save), on desktop and as the sheet on a phone.\n// It replaces the old floating selection icon: the menu has one door on a\n// selection, and it sits in the toolbar with every other passage action.\n\nimport { AGENT_ICON } from \"@ai-matrx/icons/domain\";\nimport type { Action, ActionProvider, ClickTarget } from \"@ai-matrx/alchemy/actions\";\nimport { registerAlchemyIcon } from \"@ai-matrx/rich-content/utils/alchemy-icon-keys\";\nimport {\n declareSelectionProvider,\n placeSelectionActions,\n hostHalf,\n selectionToolbarHostOf,\n shownInSelectionMode,\n} from \"@ai-matrx/rich-content/selection-toolbar/selection-actions\";\n\nexport const CONTEXT_MENU_SELECTION_HOST_KEY = \"contextMenuSelection\";\n\nexport interface ContextMenuSelectionHost {\n kind: \"context-menu-selection\";\n /** Open this menu instance over the current selection. */\n open(): void;\n}\n\nfunction menuOf(target: ClickTarget): ContextMenuSelectionHost | null {\n const half = hostHalf<ContextMenuSelectionHost>(target, CONTEXT_MENU_SELECTION_HOST_KEY);\n return half?.kind === \"context-menu-selection\" ? half : null;\n}\n\nconst ASK_AI: Action = {\n id: \"selection:ai\",\n label: \"Ask AI\",\n description: \"AI actions, agents, copy and export for the selected text\",\n icon: registerAlchemyIcon(AGENT_ICON),\n category: \"ai\",\n order: 0,\n placement: \"primary\",\n preserveSelection: true,\n eligible: (t) => (menuOf(t) && shownInSelectionMode(ASK_AI.id, t) ? { status: \"available\" } : { status: \"absent\" }),\n run: (t) => {\n const menu = menuOf(t);\n if (!menu) return;\n // The toolbar steps aside; the selection stays for the menu to act on.\n selectionToolbarHostOf(t)?.ui.close();\n menu.open();\n },\n};\n\nexport const contextMenuSelectionProvider: ActionProvider = {\n id: \"context-menu-selection\",\n tier: \"T0\",\n declaredIds: () => [ASK_AI.id],\n actions: (target) => (menuOf(target) ? placeSelectionActions([ASK_AI], target) : []),\n};\n\ndeclareSelectionProvider(contextMenuSelectionProvider);\n"],"mappings":";;;;;;;AAaA,SAAS,sBAAsB;AAE/B,SAAS,mBAAmB;AAiB5B,IAAM,eAAN,MAAmB;AAAA,EACjB,YAAqB,YAAoC;AAApC;AAAA,EAAqC;AAAA,EAArC;AACvB;AAEO,SAAS,6BAA6B,YAA4C;AACvF,SAAO,IAAI,aAAa,UAAU;AACpC;AAQA,SAAS,UAAU,OAAqC;AACtD,SAAO,OAAO,UAAU;AAC1B;AAQO,SAAS,qBAAqB,UAAyC;AAC5E,MAAI,OAAO,aAAa,YAAY,aAAa,KAAM,QAAO,CAAC;AAC/D,QAAM,OAAO,CAAC,QAA0C;AACtD,UAAM,QAAiB,QAAQ,IAAI,UAAU,GAAG;AAChD,WAAO,UAAU,KAAK,IAAI,QAAQ;AAAA,EACpC;AACA,SAAO,QAAQ;AAAA,IACb,WAAW,KAAK,WAAW;AAAA,IAC3B,WAAW,KAAK,WAAW;AAAA,IAC3B,aAAa,KAAK,aAAa;AAAA,EACjC,CAAC;AACH;AAUO,SAAS,oCACd,SACe;AACf,MAAI,QAAQ,UAAU,MAAO,QAAO;AACpC,SAAO;AAAA,IACL;AAAA,MACE,QAAQ;AAAA,MACR,qBAAqB,QAAQ,QAAQ;AAAA,IACvC;AAAA,EACF;AACF;AAGO,SAAS,mCACd,KACA,UACwB;AACxB,QAAM,QAAQ;AAAA,IACZ,GAAI,SAAS,YACT,CAAC,EAAE,MAAM,QAAiB,IAAI,kBAAkB,OAAO,YAAY,UAAU,SAAS,UAAU,CAAC,IACjG,CAAC;AAAA,IACL,GAAI,SAAS,YACT,CAAC,EAAE,MAAM,QAAiB,IAAI,kBAAkB,OAAO,gBAAgB,UAAU,MAAM,KAAK,SAAS,YAAY,EAAE,CAAC,IACpH,CAAC;AAAA,IACL,GAAI,SAAS,cACT,CAAC,EAAE,MAAM,QAAiB,IAAI,qBAAqB,OAAO,mBAAmB,UAAU,SAAS,YAAY,CAAC,IAC7G,CAAC;AAAA,EACP;AACA,SAAO;AAAA,IACL,SAAS;AAAA,MACP,SAAS,KAAK,UAAU,KAAK,MAAM,CAAC,KAAK,OAAO,GAAG;AAAA,MACnD,SAAS;AAAA,MACT,UAAU;AAAA,IACZ;AAAA,IACA,eAAe,MAAM,WAAW,IAC5B,CAAC,IACD,CAAC,EAAE,IAAI,aAAa,OAAO,OAAO,SAAS,MAAM,QAAQ,mBAAmB,MAAM,CAAC;AAAA,EACzF;AACF;AAGA,SAAS,WAAW,SAAiB,OAAmC;AACtE,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,QAAQ,MAAM,KAAK,SAAS,iBAA8B,uBAAuB,CAAC,EAAE,KAAK,CAAC,OAAO,GAAG,QAAQ,iBAAiB,OAAO;AAC1I,SAAO,MAAM,KAAK,OAAO,iBAA8B,eAAe,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,GAAG,QAAQ,UAAU,KAAK,KAAK;AAC7H;AAGA,SAAS,cAAc,YAA6C;AAClE,SAAO,CAAC,WAAW,cAAc,KAAK,CAAC,YAAY,QAAQ,MAAM,KAAK,CAAC,SAAS,QAAQ,QAAQ,KAAK,OAAO,gBAAgB,CAAC;AAC/H;AAOO,SAAS,gCACd,SACA,SACA;AACA,SAAO,YAAY,SAAiC,SAAS,CAAC,WAAW;AACvE,UAAM,QAAQ,QAAQ;AAAA,MACpB;AAAA,MACA,OAAO,OAAO;AAAA,MACd,GAAI,OAAO,SAAS,EAAE,OAAO,OAAO,OAAO,IAAI,CAAC;AAAA,MAChD,GAAI,OAAO,SAAS,EAAE,UAAU,OAAO,OAAO,IAAI,CAAC;AAAA,IACrD,CAAC;AACD,QAAI,EAAE,iBAAiB,cAAe,QAAO;AAC7C,UAAM,aAAa,MAAM;AACzB,UAAM,QAAQ,OAAO,UAAU;AAC/B,WAAO;AAAA,MACL,MAAM;AAAA,QACJ,UAAU;AAAA,QACV,UAAU,EAAE,UAAU,SAAS,QAAQ,MAAM;AAAA,QAC7C,OAAO,OAAO;AAAA,QACd,UAAU,cAAc,UAAU;AAAA,QAClC,YAAY,aAAa;AAAA,UACvB,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,OAAO,WAAW,WAAW,SAAS,KAAK,CAAC;AAAA,UAC5C,KAAK,WAAW,QAAQ;AAAA,QAC1B;AAAA,MACF;AAAA,MACA,MAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;AAQO,SAAS,oBAAoB,QAAoD;AACtF,QAAM,MAAM,QAAQ,QAAqB,eAAe;AACxD,QAAM,UAAU,KAAK,QAAqB,uBAAuB,GAAG,QAAQ;AAC5E,QAAM,QAAQ,KAAK,QAAQ;AAC3B,MAAI,CAAC,WAAW,CAAC,MAAO,QAAO;AAE/B,QAAM,MAAM,YAAY,QAAgC,EAAE,aAAa,SAAS,OAAO,OAAO,QAAQ,MAAM,CAAC;AAC7G,QAAM,aAAa,KAAK,QAAQ;AAChC,MAAI,CAAC,OAAO,CAAC,WAAY,QAAO;AAChC,QAAM,EAAE,YAAY,MAAM,GAAG,KAAK,IAAI,IAAI;AAC1C,SAAO,EAAE,MAAM,MAAM,WAAW,YAAY,QAAQ,GAAG,EAAE;AAC3D;AAGO,SAAS,8BAA8B,QAA2D;AACvG,SAAO,oBAAoB,MAAM,GAAG,QAAQ;AAC9C;AAEA,SAAS,WACP,YACA,QACA,KACwB;AAMxB,QAAM,OAAO,QAAQ,QAAqB,uBAAuB;AACjE,QAAM,QAAQ,WAAW,QAAQ,GAAG;AAKpC,QAAM,aAAc,WAAW,QAAoC,sCAAwB;AAC3F,QAAM,aAAa,aAAa,KAAK,cAAc,GAAG;AACtD,QAAM,UAAU;AAAA,IACd,GAAG,WAAW;AAAA,IACd,GAAI,QAAQ,EAAE,SAAS,MAAM,IAAI,CAAC;AAAA,IAClC,GAAI,aAAa,EAAE,CAAC,sCAAwB,GAAG,EAAE,OAAO,OAAO,MAAM,WAAW,EAAE,IAAI,CAAC;AAAA,EACzF;AAKA,QAAM,SAAU,YAA+C,QAAQ;AACvE,QAAM,OAAO,CAAC,UACZ,OAAO,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,KAAK,EAAE,YAAY,MAAM,OAAO,KAAK,EAAE,YAAY;AACpG,QAAM,gBAAgB,WAAW,cAAc;AAAA,IAAI,CAAC,YAClD,QAAQ,WAAW,KAAK,QAAQ,KAAK,IAAI,EAAE,GAAG,SAAS,OAAO,MAAM,IAAI;AAAA,EAC1E;AACA,QAAM,kBAAkB,cAAc,KAAK,CAAC,SAAS,MAAM,YAAY,WAAW,cAAc,CAAC,CAAC;AAClG,SAAO,SAAS,cAAc,kBAAkB,EAAE,GAAG,YAAY,SAAS,cAAc,IAAI;AAC9F;AAGO,SAAS,cAAc,KAA6C;AACzE,MAAI,CAAC,IAAK,QAAO;AACjB,aAAW,QAAQ,MAAM,KAAK,IAAI,iBAA8B,+EAA+E,CAAC,GAAG;AACjJ,UAAM,QAAQ,YAAY,IAAI;AAC9B,QAAI,MAAO,QAAO,MAAM,SAAS,MAAM,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,WAAM;AAAA,EACrE;AACA,SAAO;AACT;AAGA,IAAM,gBAAgB;AAUtB,SAAS,YAAY,SAA0B;AAC7C,QAAM,OAAO,QAAQ,UAAU,IAAI;AACnC,OAAK,iBAAiB,uDAAuD,EAAE,QAAQ,CAAC,SAAS,KAAK,OAAO,CAAC;AAC9G,OAAK,iBAAiB,aAAa,EAAE,QAAQ,CAAC,SAAS,KAAK,OAAO,IAAI,CAAC;AACxE,QAAM,SAAS,KAAK,eAAe,IAChC,MAAM,IAAI,EACV,IAAI,CAAC,SAAS,KAAK,QAAQ,QAAQ,GAAG,EAAE,KAAK,CAAC,EAC9C,OAAO,CAAC,SAAS,QAAQ,SAAS,QAAG;AACxC,SAAO,MAAM,CAAC,KAAK;AACrB;AAGA,SAAS,WAAW,SAAiD;AACnE,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,QAAQ,iBAAiB,uBAAuB;AAC9D,QAAM,OAAO,MAAM,SAAS,IACxB,MAAM,KAAK,KAAK,EAAE,IAAI,WAAW,EAAE,OAAO,OAAO,EAAE,KAAK,QAAK,IAC7D,YAAY,OAAO;AACvB,SAAO,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,GAAG,CAAC,WAAM;AACxD;;;ACvPO,IAAM,mBAAmB;AAahC,IAAM,WAAW,oBAAI,IAAkC;AASvD,IAAI,WAAW;AACf,IAAM,YAAY,oBAAI,IAAgB;AAG/B,SAAS,oBAA0B;AACxC,cAAY;AACZ,aAAW,YAAY,UAAW,UAAS;AAC7C;AAGO,SAAS,qBAAqB,UAAkC;AACrE,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAEO,SAAS,sBAA8B;AAC5C,SAAO;AACT;AAGO,SAAS,mBAAmB,KAAa,KAAuC;AACrF,WAAS,IAAI,KAAK,GAAG;AACrB,SAAO,MAAM;AACX,QAAI,SAAS,IAAI,GAAG,MAAM,IAAK,UAAS,OAAO,GAAG;AAAA,EACpD;AACF;AAGO,SAAS,kBAAkB,QAA+C;AAC/E,QAAM,MAAM,QAAQ,UAAU,IAAI,gBAAgB,GAAG,GAAG,aAAa,gBAAgB;AACrF,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,SAAS,IAAI,GAAG,IAAI,KAAK;AAClC;AAEA,IAAM,UAAU;AAMhB,SAAS,eAAe,MAAuC;AAC7D,QAAM,QAAQ,KAAK,cAA2B,GAAG,OAAO,uBAAuB;AAC/E,MAAI,MAAO,QAAO;AAClB,MAAI,KAAK,QAAQ,OAAO,EAAG,QAAO;AAClC,SAAO,KAAK,cAA2B,OAAO,KAAK,KAAK,QAAqB,OAAO;AACtF;AAMO,SAAS,eAAe,KAAa,QAA8B;AACxE,QAAM,MAAM,OAAO;AACnB,QAAM,OAAO,CAAC,GAAG,IAAI,iBAA8B,IAAI,gBAAgB,GAAG,CAAC,EAAE;AAAA,IAC3E,CAAC,OAAO,GAAG,aAAa,gBAAgB,MAAM;AAAA,EAChD;AACA,QAAM,UAAU,OAAO,eAAe,IAAI,IAAI;AAC9C,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,OAAO,OAAO,sBAAsB;AAC1C,UAAQ;AAAA,IACN,IAAI,WAAW,eAAe;AAAA,MAC5B,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,MAAM,IAAI,eAAe;AAAA,MACzB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,SAAS,KAAK,OAAO,KAAK,QAAQ;AAAA,MAClC,SAAS,KAAK;AAAA,IAChB,CAAC;AAAA,EACH;AACA,SAAO;AACT;;;AC9FA,IAAM,QAAQ,yBAAyB;AAahC,SAAS,uBACd,YACA,UACkC;AAClC,MAAI,CAAC,YAAY,EAAE,yCAA2B,UAAW,QAAO;AAChE,QAAM,SAAS,SAAS,qCAAuB;AAC/C,MAAI,WAAW,QAAQ,WAAW,OAAW,QAAO;AACpD,QAAM,YAAY;AAClB,MACE,OAAO,WAAW,YAClB,OAAO,UAAU,SAAS,YAC1B,CAAC,UAAU,QACX,OAAO,UAAU,OAAO,YACxB,CAAC,UAAU,IACX;AACA,QAAI,OAAO;AACT,cAAQ;AAAA,QACN,sFAAiF,qCAAuB;AAAA,QACxG;AAAA,QACA;AAAA,QACA,EAAE,UAAU,OAAO;AAAA,MACrB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,yBACd,aACA,UACyB;AACzB,QAAM,SAAkC;AAAA,IACtC,GAAI,eAAe,CAAC;AAAA,IACpB,GAAI,YAAY,CAAC;AAAA,EACnB;AACA,SAAO,OAAO,qCAAuB;AACrC,SAAO;AACT;AAwBO,SAAS,yBACd,KACA,SACmC;AACnC,MAAI,CAAC,IAAK,QAAO,WAAW;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,iBAAiB,IAAI,qCAAuB,MAAM,QAAQ,IAAI,qCAAuB,MAAM;AACjG,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG;AAAA,IACH,GAAI,CAAC,kBAAkB,yCAA2B,UAC9C,EAAE,CAAC,qCAAuB,GAAG,QAAQ,qCAAuB,EAAE,IAC9D,CAAC;AAAA,EACP;AACF;AAeA,IAAM,cAAc;AAoBb,SAAS,mBACd,QAC6B;AAC7B,QAAM,OAAO,QAAQ,UAAU,oCAAoC;AACnE,MAAI,EAAE,gBAAgB,aAAc,QAAO;AAE3C,QAAM,OAAO,KAAK,aAAa,kBAAkB,GAAG,KAAK,KAAK;AAC9D,QAAM,KAAK,KAAK,aAAa,gBAAgB,GAAG,KAAK,KAAK;AAC1D,MAAI,CAAC,QAAQ,CAAC,GAAI,QAAO;AAEzB,MAAI,CAAC,YAAY,KAAK,IAAI,GAAG;AAC3B,QAAI,OAAO;AACT,cAAQ;AAAA,QACN,0DAAqD,IAAI;AAAA,QACzD;AAAA,QACA;AAAA,QACA,EAAE,SAAS,KAAK;AAAA,MAClB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAIA,QAAM,QACJ,KAAK,aAAa,mBAAmB,GAAG,KAAK,MAC5C,KAAK,eAAe,IAAI,KAAK,EAAE,MAAM,GAAG,GAAG,KAC5C;AAEF,QAAM,eAAe,KAAK,aAAa,sBAAsB,GAAG,KAAK;AAErE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,eACA,EAAE,aAAgF,IAClF,CAAC;AAAA,EACP;AACF;AAMO,SAAS,8BACd,YACA,UACA,QACkC;AAClC,QAAM,eAAe,CAAC,CAAC,YAAY,yCAA2B;AAC9D,MAAI,aAAc,QAAO,uBAAuB,YAAY,QAAQ;AACpE,SAAO,mBAAmB,MAAM,KAAK;AACvC;;;ACxLA,SAAS,MAAM,MAAqC;AAClD,SAAO,KAAK,SAAS;AACvB;AAGA,SAAS,eAAe,OAAuD;AAC7E,QAAM,MAA8B,CAAC;AACrC,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,MAAM,IAAI,MAAM,IAAI,WAAW,KAAK,CAAC,MAAM,IAAI,IAAI,SAAS,CAAC,CAAE,GAAI;AACxE,QAAI,KAAK,IAAI;AAAA,EACf;AACA,SAAO,IAAI,SAAS,KAAK,CAAC,MAAM,IAAI,IAAI,SAAS,CAAC,CAAE,EAAG,KAAI,IAAI;AAC/D,SAAO;AACT;AAEO,SAAS,qBACX,SACoC;AACvC,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAsC,MAAM,QAAQ,CAAC,CAAC;AACtF,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,MAAI,QAAQ,WAAW,EAAG,QAAO,QAAQ,CAAC;AAC1C,QAAM,QAAQ,oBAAI,IAAY;AAC9B,QAAM,MAAiC,CAAC;AACxC,aAAW,UAAU,SAAS;AAC5B,UAAM,SAAS,oBAAI,IAAY;AAC/B,eAAW,WAAW,QAAQ;AAC5B,YAAM,OAAO,QAAQ,MAAM,OAAO,CAAC,SAAS,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,EAAE,CAAC;AAC/E,iBAAW,QAAQ,KAAM,KAAI,MAAM,IAAI,EAAG,QAAO,IAAI,KAAK,EAAE;AAC5D,UAAI,KAAK,WAAW,QAAQ,MAAM,QAAQ;AACxC,YAAI,KAAK,OAAO;AAChB;AAAA,MACF;AACA,YAAM,QAAQ,eAAe,IAAI;AACjC,UAAI,MAAM,KAAK,KAAK,EAAG,KAAI,KAAK,EAAE,GAAG,SAAS,MAAM,CAAC;AAAA,IACvD;AACA,eAAW,MAAM,OAAQ,OAAM,IAAI,EAAE;AAAA,EACvC;AACA,SAAO;AACT;;;AC1BA,SAAS,cACP,IACA,MACA,OACM;AACN,QAAM,SAAS,OAAO;AAAA,IACpB,oBAAoB;AAAA,IACpB;AAAA,EACF,GAAG;AACH,MAAI,OAAQ,QAAO,KAAK,IAAI,IAAI;AAAA,MAC3B,IAAG,QAAQ;AAChB,KAAG,cAAc,IAAI,MAAM,SAAS,EAAE,SAAS,KAAK,CAAC,CAAC;AACtD,MAAI;AACF,OAAG,kBAAkB,OAAO,KAAK;AAAA,EACnC,QAAQ;AAAA,EAER;AACF;AAeO,SAAS,0BACd,MACqB;AACrB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,QAAQ,MAAkC,cAAc,KAAK;AAEnE,QAAM,cAAc,MAAqB;AACvC,UAAM,KAAK,MAAM;AACjB,QAAI,GAAI,QAAO,GAAG;AAClB,UAAM,UAAU,sBAAsB,EAAE;AACxC,WAAO,OAAO,YAAY,WAAW,UAAU;AAAA,EACjD;AAIA,QAAM,YAAY,CAAC,SAAuB;AACxC,QAAI,eAAe;AACjB,oBAAc,IAAI;AAClB;AAAA,IACF;AACA,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI,OAAM,IAAI,MAAM,gCAAgC;AACzD,kBAAc,IAAI,MAAM,KAAK,MAAM;AAAA,EACrC;AAOA,QAAM,WAAW,QAAQ,aAAa,KAAK,QAAQ,WAAW;AAC9D,QAAM,UAAU,QAAQ,WAAW,KAAK,QAAQ,mBAAmB;AACnE,MAAI,CAAC,SAAU,QAAO;AAItB,QAAM,SAAuB;AAAA,IAC3B,eAAe,CAAC,EAAE,KAAK,MAAM,UAAU,IAAI;AAAA,IAC3C,GAAI,UAAU,EAAE,UAAU,YAAY,IAAI,CAAC;AAAA,EAC7C;AAIA,MAAI,sBAAsB,aAAa;AACrC,WAAO,qBAAqB,CAAC,EAAE,KAAK,MAAM;AACxC,UAAI,mBAAoB,QAAO,mBAAmB,IAAI;AACtD,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,GAAI,OAAM,IAAI,MAAM,gCAAgC;AACzD,YAAM,KAAK,GAAG,kBAAkB;AAChC;AAAA,QACE;AAAA,QACA,GAAG,MAAM,MAAM,GAAG,EAAE,IAAI,OAAO,GAAG,MAAM,MAAM,EAAE;AAAA,QAChD,KAAK,KAAK;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACA,MAAI,qBAAqB,aAAa;AACpC,WAAO,oBAAoB,CAAC,EAAE,KAAK,MAAM;AACvC,UAAI,kBAAmB,QAAO,kBAAkB,IAAI;AACpD,YAAM,KAAK,MAAM;AACjB,UAAI,CAAC,GAAI,OAAM,IAAI,MAAM,gCAAgC;AACzD,YAAM,KAAK,GAAG,gBAAgB,GAAG,MAAM;AACvC;AAAA,QACE;AAAA,QACA,GAAG,MAAM,MAAM,GAAG,EAAE,IAAI,OAAO,GAAG,MAAM,MAAM,EAAE;AAAA,QAChD,KAAK,KAAK;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAGA,MAAI,SAAS;AACX,WAAO,gBAAgB,CAAC,EAAE,KAAK,MAAM;AACnC,YAAM,UAAU,YAAY;AAC5B,UAAI,YAAY,KAAM,OAAM,IAAI,MAAM,+BAA+B;AACrE,gBAAU,OAAO,OAAO;AAAA,IAC1B;AACA,WAAO,eAAe,CAAC,EAAE,KAAK,MAAM;AAClC,YAAM,UAAU,YAAY;AAC5B,UAAI,YAAY,KAAM,OAAM,IAAI,MAAM,+BAA+B;AACrE,gBAAU,UAAU,IAAI;AAAA,IAC1B;AACA,WAAO,cAAc,CAAC,EAAE,aAAa,iBAAiB,MAAM;AAC1D,YAAM,UAAU,YAAY;AAC5B,UAAI,YAAY,KAAM,OAAM,IAAI,MAAM,+BAA+B;AACrE,YAAM,MAAM,QAAQ,QAAQ,WAAW;AACvC,UAAI,QAAQ;AAEV,cAAM,IAAI,MAAM,0CAA0C;AAC5D;AAAA,QACE,QAAQ,MAAM,GAAG,GAAG,IAClB,mBACA,QAAQ,MAAM,MAAM,YAAY,MAAM;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;;;AC3JA,IAAI,cAAkC;AACtC,IAAI,YAAqC;AAElC,SAAS,oBAAsC;AACpD,gBAAc,OAAO,kCAAkC,EACpD,KAAK,CAAC,MAAM;AACX,kBAAc,EAAE;AAAA,EAClB,CAAC,EACA,MAAM,CAAC,UAAmB;AACzB,gBAAY;AACZ,YAAQ,MAAM,yDAAyD,KAAK;AAAA,EAC9E,CAAC;AACH,SAAO;AACT;AAGO,SAAS,kBAAkB,QAAuC;AACvE,QAAM,QAAQ,QAAQ,QAAQ,OAAO;AACrC,MAAI,CAAC,SAAS,CAAC,YAAa,QAAO;AACnC,QAAM,OAAO,YAAY,MAAM,UAAU,IAAI,CAAC;AAC9C,SAAO,KAAK,KAAK,IAAI,OAAO;AAC9B;;;ACtBO,IAAM,+BAA+B;AAAA,EAC1C,wBAAwB;AAC1B;;;ACIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,OAKK;AACP,SAAS,aAAa;AAGtB,IAAM,SAAS,CAAC,UAAyB,MAAM;AAE/C,IAAM,eAAe,CAAC,SAA2B,aAAkF;AACjI,QAAM,QAAQ,SAAS,QAAQ;AACjC;AAEA,SAAS,WAAW,OAAwB;AAC1C,iBAAe,OAAO,YAAY;AACpC;AAIA,SAAS,iBAAiB,OAA4C;AACpE,QAAM,cACJ,MAAM,YAAY,MAAM,iBACpB,MAAM,iBACN,MAAM;AAEZ,MAAI,UAAU,KAAK,GAAG;AACpB,WAAO,QAAQ;AAAA,MACb,MAAM;AAAA,MACN,IAAI,MAAM;AAAA,MACV,OAAO,MAAM;AAAA,MACb,MAAM,OAAO,KAAK;AAAA,MAClB,UAAU,MAAM;AAAA,MAChB,UAAU,gBAAgB,MAAM,QAAQ;AAAA,IAC1C,CAAC;AAAA,EACH;AACA,MAAI,WAAW,KAAK,GAAG;AACrB,WAAO,QAAQ;AAAA,MACb,MAAM;AAAA,MACN,IAAI,MAAM;AAAA,MACV,OAAO,MAAM;AAAA,MACb,MAAM,OAAO,KAAK;AAAA,MAClB;AAAA,MACA,SAAS,MAAM;AAAA,MACf,iBAAiB,CAAC,SAAkB,UAAU,OAAO,IAAI;AAAA,MACzD,UAAU,MAAM;AAAA,MAChB,MAAM,MAAM,YAAY,MAAM,aAAa,YAAY;AAAA,IACzD,CAAC;AAAA,EACH;AACA,MAAI,OAAO,KAAK,GAAG;AACjB,WAAO,QAAQ;AAAA,MACb,MAAM;AAAA,MACN,IAAI,MAAM;AAAA,MACV,OAAO,MAAM;AAAA,MACb,MAAM,OAAO,KAAK;AAAA,MAClB;AAAA,MACA,MAAM,MAAM;AAAA,MACZ,QAAQ,MAAM;AAAA,MACd,UAAU,MAAM;AAAA,IAClB,CAAC;AAAA,EACH;AAEA,SAAO,QAAQ;AAAA,IACb,MAAM;AAAA,IACN,IAAI,MAAM;AAAA,IACV,OAAO,MAAM;AAAA,IACb,MAAM,OAAO,KAAK;AAAA,IAClB;AAAA,IACA,aAAa,MAAM,SAAS;AAAA,IAC5B,UAAU,MAAM;AAAA,IAChB,MAAM,MAAM,YAAY,MAAM,aAAa,YAAY;AAAA,IACvD,UAAU,MAAM,WAAW,KAAK;AAAA,EAClC,CAAC;AACH;AAGA,SAAS,gBAAgB,UAAqD;AAC5E,QAAM,UAAU,SACb,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,EAC9D,OAAO,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC;AACnC,QAAM,MAA8B,CAAC;AACrC,UAAQ,QAAQ,CAAC,SAAS,MAAM;AAC9B,QAAI,IAAI;AACN,UAAI,KAAK;AAAA,QACP,MAAM;AAAA,QACN,IAAI,OAAO,QAAQ,MAAM,QAAQ,SAAS,CAAC;AAAA,MAC7C,CAAC;AACH,QAAI,KAAK,GAAG,QAAQ,MAAM,IAAI,gBAAgB,CAAC;AAAA,EACjD,CAAC;AACD,SAAO;AACT;AAEO,SAAS,8BACd,QAC2B;AAC3B,QAAM,WAAsC,OAAO,SAChD,IAAI,CAAC,GAAG,MAAM,QAAQ;AAAA,IACrB,IAAI,OAAO,EAAE,MAAM,EAAE,SAAS,MAAM,CAAC,EAAE;AAAA,IACvC,OAAO,EAAE;AAAA,IACT,QAAQ;AAAA,IACR,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,gBAAgB;AAAA,EAC9D,CAAC,CAAC,EACD,OAAO,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC;AAGnC,QAAM,QAAQ,OAAO,QAAQ;AAC7B,QAAM,QAAQ,SAAS,CAAC;AACxB,MAAI,SAAS,SAAS,CAAC,MAAM,OAAO;AAClC,aAAS,CAAC,IAAI,EAAE,GAAG,OAAO,OAAO,MAAM;AAAA,EACzC;AACA,SAAO;AACT;;;AC9HA,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,IAAM,kCAAkC;AAQ/C,SAAS,OAAO,QAAsD;AACpE,QAAM,OAAO,SAAmC,QAAQ,+BAA+B;AACvF,SAAO,MAAM,SAAS,2BAA2B,OAAO;AAC1D;AAEA,IAAM,SAAiB;AAAA,EACrB,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA,EACb,MAAM,oBAAoB,UAAU;AAAA,EACpC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,mBAAmB;AAAA,EACnB,UAAU,CAAC,MAAO,OAAO,CAAC,KAAK,qBAAqB,OAAO,IAAI,CAAC,IAAI,EAAE,QAAQ,YAAY,IAAI,EAAE,QAAQ,SAAS;AAAA,EACjH,KAAK,CAAC,MAAM;AACV,UAAM,OAAO,OAAO,CAAC;AACrB,QAAI,CAAC,KAAM;AAEX,2BAAuB,CAAC,GAAG,GAAG,MAAM;AACpC,SAAK,KAAK;AAAA,EACZ;AACF;AAEO,IAAM,+BAA+C;AAAA,EAC1D,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,aAAa,MAAM,CAAC,OAAO,EAAE;AAAA,EAC7B,SAAS,CAAC,WAAY,OAAO,MAAM,IAAI,sBAAsB,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC;AACpF;AAEA,yBAAyB,4BAA4B;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/engine/table-row-item.ts","../src/engine/record-menu-registry.ts","../src/engine/utils/per-row-entity.ts","../src/engine/utils/join-extra-sections.ts","../src/engine/utils/widget-handle.ts","../src/engine/utils/table-at-target.ts","../src/engine/region-trigger-attrs.ts","../src/engine/item/itemMenuToV3.ts","../src/engine/selection-provider.ts"],"sourcesContent":["/**\n * THE TABLE ROW IS A DECLARED ITEM (ALC-18, LIST.md D4).\n *\n * A canonical table's row is alchemy's `table_row` item type (`TABLE_ROW_ITEM_TYPE`, a baseline\n * item type of every surface — `features/surfaces/manifests/registry.ts`). Each mounted table\n * registers ONE item source with `@ai-matrx/alchemy/surface` `itemSources` under its table id; a\n * right-click resolves the row through that one path into the declared item (identity\n * `table_id` + `row_id`, the level, its values) plus the row's menu, which rides as the item's\n * opaque host data. There is no private registry here: this file only builds the row's menu and\n * reads the words the row shows.\n */\nimport { compact } from \"./utils/compact\";\nimport { CONTEXT_MENU_HEADING_KEY, type ContextMenuExtraSection, type ResolvedContextMenuContext } from \"./types\";\nimport { TABLE_ROW_ITEM } from \"@ai-matrx/alchemy/declare\";\nimport type { ResolvedItem } from \"@ai-matrx/alchemy/declare\";\nimport { itemSources } from \"@ai-matrx/alchemy/surface\";\nimport type { MatrxDataTableRecordControls } from \"@ai-matrx/design-system/data-table/types\";\nimport type {\n MatrxTableMenuLevel,\n MatrxTableMenuPayload,\n MatrxTableMenuTarget,\n} from \"@ai-matrx/design-system/data-table/menu-targets\";\n\nexport interface TableRowMenuDescriptor {\n context: ResolvedContextMenuContext;\n extraSections: ContextMenuExtraSection[];\n}\n\n/**\n * The row's menu as the table hands it back (the table types it `unknown` and never reads it). A\n * class, so a resolver answering anything else is told apart without a private lookup table.\n */\nclass TableRowMenu {\n constructor(readonly descriptor: TableRowMenuDescriptor) {}\n}\n\nexport function createTableRowMenuDescriptor(descriptor: TableRowMenuDescriptor): object {\n return new TableRowMenu(descriptor);\n}\n\n/** Exactly the row commands the default menu renders. Nothing else is read. */\nexport type TableRowEditCommands = Pick<\n MatrxDataTableRecordControls,\n \"beginEdit\" | \"saveEdits\" | \"cancelEdits\"\n>;\n\nfunction isCommand(value: unknown): value is () => void {\n return typeof value === \"function\";\n}\n\n/**\n * The table hands its host the row's controls as `unknown` — it never\n * interprets what the host builds from them — so they are READ here, one\n * command at a time. A command the table did not send, or sent as something\n * that cannot be called, is simply not offered rather than rendered dead.\n */\nexport function tableRowEditCommands(controls: unknown): TableRowEditCommands {\n if (typeof controls !== \"object\" || controls === null) return {};\n const read = (key: string): (() => void) | undefined => {\n const value: unknown = Reflect.get(controls, key);\n return isCommand(value) ? value : undefined;\n };\n return compact({\n beginEdit: read(\"beginEdit\"),\n saveEdits: read(\"saveEdits\"),\n cancelEdits: read(\"cancelEdits\"),\n });\n}\n\n/**\n * The host's generic menu model: complete current row data plus the\n * table-owned edit commands.\n *\n * The table asks this for whichever of its five right-click LEVELS was aimed\n * at. Only the row has a generic menu here, so every other level answers\n * `null` and the table leaves that level to the surface that owns it.\n */\nexport function createDefaultTableRowMenuDescriptor(\n payload: MatrxTableMenuPayload,\n): object | null {\n if (payload.level !== \"row\") return null;\n return createTableRowMenuDescriptor(\n buildDefaultTableRowMenuDescriptor(\n payload.row,\n tableRowEditCommands(payload.controls),\n ),\n );\n}\n\n/** Reusable base for a domain menu that keeps the shared row edit controls. */\nexport function buildDefaultTableRowMenuDescriptor(\n row: unknown,\n controls: TableRowEditCommands,\n): TableRowMenuDescriptor {\n const items = [\n ...(controls.beginEdit\n ? [{ kind: \"item\" as const, id: \"table-edit-row\", label: \"Edit row\", onSelect: controls.beginEdit }]\n : []),\n ...(controls.saveEdits\n ? [{ kind: \"item\" as const, id: \"table-save-row\", label: \"Save changes\", onSelect: () => void controls.saveEdits?.() }]\n : []),\n ...(controls.cancelEdits\n ? [{ kind: \"item\" as const, id: \"table-discard-row\", label: \"Discard changes\", onSelect: controls.cancelEdits }]\n : []),\n ];\n return {\n context: {\n content: JSON.stringify(row, null, 2) ?? String(row),\n context: row,\n __entity: null,\n },\n extraSections: items.length === 0\n ? []\n : [{ id: \"table-row\", label: \"Row\", primary: true, anchor: \"after-clipboard\", items }],\n };\n}\n\n/** The mounted row element of a table, read when the item's values are read. */\nfunction rowElement(tableId: string, rowId: string): HTMLElement | null {\n if (typeof document === \"undefined\") return null;\n const table = Array.from(document.querySelectorAll<HTMLElement>(\"[data-matrx-table-id]\")).find((el) => el.dataset.matrxTableId === tableId);\n return Array.from(table?.querySelectorAll<HTMLElement>(\"[data-row-id]\") ?? []).find((el) => el.dataset.rowId === rowId) ?? null;\n}\n\n/** A row is writable when its table offers the row's edit command. */\nfunction rowIsReadOnly(descriptor: TableRowMenuDescriptor): boolean {\n return !descriptor.extraSections.some((section) => section.items.some((item) => \"id\" in item && item.id === \"table-edit-row\"));\n}\n\n/**\n * The canonical table registers ONE item source per mounted instance, under its table id. The table\n * answers which of its right-click LEVELS was aimed at (`MatrxTableMenuTarget`); the source turns\n * that answer into the declared `table_row` item, with the row's menu as its host data.\n */\nexport function registerTableRowContextResolver(\n tableId: string,\n resolve: (target: MatrxTableMenuTarget) => unknown,\n) {\n return itemSources.register<TableRowMenuDescriptor>(tableId, (target) => {\n const token = resolve({\n tableId,\n level: target.level as MatrxTableMenuLevel,\n ...(target.itemId ? { rowId: target.itemId } : {}),\n ...(target.partId ? { columnId: target.partId } : {}),\n });\n if (!(token instanceof TableRowMenu)) return null;\n const descriptor = token.descriptor;\n const rowId = target.itemId ?? \"\";\n return {\n item: {\n itemType: TABLE_ROW_ITEM,\n identity: { table_id: tableId, row_id: rowId },\n level: target.level,\n readOnly: rowIsReadOnly(descriptor),\n readValues: async () => ({\n table_id: tableId,\n row_id: rowId,\n shown: shownWords(rowElement(tableId, rowId)),\n row: descriptor.context.context,\n }),\n },\n host: descriptor,\n };\n });\n}\n\n/** The clicked row as its declared item (raw-free) and the menu its table built for it. */\nexport interface TableRowItemHit {\n item: ResolvedItem;\n menu: TableRowMenuDescriptor;\n}\n\nexport function resolveTableRowItem(target: HTMLElement | null): TableRowItemHit | null {\n const row = target?.closest<HTMLElement>(\"[data-row-id]\");\n const tableId = row?.closest<HTMLElement>(\"[data-matrx-table-id]\")?.dataset.matrxTableId;\n const rowId = row?.dataset.rowId;\n if (!tableId || !rowId) return null;\n // This menu acts on the ROW it found in the DOM, so it asks for that level by name.\n const hit = itemSources.resolve<TableRowMenuDescriptor>({ containerId: tableId, level: \"row\", itemId: rowId });\n const descriptor = hit?.host ?? null;\n if (!hit || !descriptor) return null;\n const { readValues: _raw, ...item } = hit.item;\n return { item, menu: headedMenu(descriptor, target, row) };\n}\n\n/** The row's menu, or null when the click was not on a canonical table's row. */\nexport function resolveTableRowMenuDescriptor(target: HTMLElement | null): TableRowMenuDescriptor | null {\n return resolveTableRowItem(target)?.menu ?? null;\n}\n\nfunction headedMenu(\n descriptor: TableRowMenuDescriptor,\n target: HTMLElement | null,\n row: HTMLElement,\n): TableRowMenuDescriptor {\n // 🚨 THE HEADING IS WHAT THE PERSON RIGHT-CLICKED, IN THE WORDS ON SCREEN (merged-grid review\n // 2026-09-26): the heading read `Content: { \"id\": …, \"_choices\": …, \"level\": \"admin\",\n // \"hidden\": {} }` — the row's raw document. The clicked cell's shown text (\"Content: Priya\n // Nair\") is the heading; the row's words when the click was between cells. The full row stays\n // in `context.context` for the actions that read it.\n const cell = target?.closest<HTMLElement>(\"td, [role='gridcell']\");\n const shown = shownWords(cell ?? row);\n // THE HEADER NAMES THE ROW'S RECORD (admin pass 2026-09-27: \"Content: partial\" — the clicked\n // status cell). The descriptor's own `__heading` wins (a list row names \"Quiz: …\"); otherwise\n // the row's record name — its first data cell's primary line. What the actions act on\n // (`content`) stays the clicked cell's words.\n const ownHeading = (descriptor.context as Record<string, unknown>)[CONTEXT_MENU_HEADING_KEY];\n const recordName = ownHeading ? \"\" : rowRecordName(row);\n const context = {\n ...descriptor.context,\n ...(shown ? { content: shown } : {}),\n ...(recordName ? { [CONTEXT_MENU_HEADING_KEY]: { label: \"Row\", text: recordName } } : {}),\n };\n // THE ROW IS NAMED ONCE (admin final judge 2026-09-27: \"Row: Basic Editor\" then a \"Basic\n // Editor\" section heading right under it). A primary section labelled with the same name the\n // header already shows is headed \"Row\" instead — its rows stay first (an unnamed section\n // would lose its place; the package never draws a coined or empty heading).\n const headed = (ownHeading as { text?: unknown } | undefined)?.text ?? recordName;\n const same = (label: string | undefined) =>\n typeof headed === \"string\" && !!label && label.trim().toLowerCase() === headed.trim().toLowerCase();\n const extraSections = descriptor.extraSections.map((section) =>\n section.primary && same(section.label) ? { ...section, label: \"Row\" } : section,\n );\n const changedSections = extraSections.some((section, i) => section !== descriptor.extraSections[i]);\n return shown || recordName || changedSections ? { ...descriptor, context, extraSections } : descriptor;\n}\n\n/** The row's record name: the first data cell (not a tick box, star or button cell) with words. */\nexport function rowRecordName(row: HTMLElement | null | undefined): string {\n if (!row) return \"\";\n for (const cell of Array.from(row.querySelectorAll<HTMLElement>(\"td[data-matrx-table-column-id], [role='gridcell'][data-matrx-table-column-id]\"))) {\n const words = primaryLine(cell);\n if (words) return words.length > 120 ? `${words.slice(0, 119)}…` : words;\n }\n return \"\";\n}\n\n/** Elements that start a new LINE of what a cell shows (name, then its subtitle). */\nconst LINE_BREAKING = \"div, p, li, br, tr, h1, h2, h3, h4, h5, h6, section, article, header, footer, dd, dt\";\n\n/**\n * The PRIMARY line an element shows — its first line, without its controls.\n * `textContent` glues a cell's stacked lines together with no space\n * (\"Data Destruction, Inc.Company re…\", \"Notesmatrx-user/notes\"; page-pass\n * 2026-09-27), so each line-starting element is marked with a newline first and\n * only the first non-empty line is kept. Inline markup inside a line\n * (<b>, <a>, <span>) is untouched, so words are never split.\n */\nfunction primaryLine(element: Element): string {\n const copy = element.cloneNode(true) as HTMLElement;\n copy.querySelectorAll(\"button, [aria-hidden='true'], input, textarea, select\").forEach((node) => node.remove());\n copy.querySelectorAll(LINE_BREAKING).forEach((node) => node.before(\"\\n\"));\n const lines = (copy.textContent ?? \"\")\n .split(\"\\n\")\n .map((line) => line.replace(/\\s+/g, \" \").trim())\n .filter((line) => line && line !== \"—\");\n return lines[0] ?? \"\";\n}\n\n/** The words an element shows: one cell's primary line, or each cell's, joined with \" · \". */\nfunction shownWords(element: HTMLElement | null | undefined): string {\n if (!element) return \"\";\n const cells = element.querySelectorAll(\"td, [role='gridcell']\");\n const text = cells.length > 1\n ? Array.from(cells).map(primaryLine).filter(Boolean).join(\" · \")\n : primaryLine(element);\n return text.length > 200 ? `${text.slice(0, 199)}…` : text;\n}\n","/**\n * A RECORD'S ROWS, IN THE ONE MENU OF THE CONTENT THAT SHOWS IT (R26; ALC-15 round 5).\n *\n * A record's toolbar can live apart from the content it belongs to: on /notes the tab strip holds the\n * note's ⋯ (Save, Duplicate, Move, Delete, Close tab…) while the note's content is drawn in the pane\n * below. The ⋯ and a right-click on that content target the SAME thing — the note — so they must be\n * one menu (measured 2026-09-27: 27 rows from the tab's ⋯, 25 from the content, each missing the\n * other's rows).\n *\n * · The owner of the rows registers them under a key: `registerRecordMenu(key, get)`.\n * · The host marks the content's root with `data-record-menu=\"<key>\"`.\n * · Every menu opened inside that root carries the registered sections (and the record's entity,\n * when the menu has none of its own) — the shell reads them at open (ContextMenuV3).\n * · The record's ⋯ calls `openRecordMenu(key, button)`: it opens the content's OWN menu at the\n * button. `false` = that content is not on screen; the caller opens its own menu instead.\n *\n * Plain data, no React.\n */\nimport type { ContextMenuEntityRef, ContextMenuExtraSection } from \"./types\";\n\nexport const RECORD_MENU_ATTR = \"data-record-menu\";\n\nexport interface RecordMenuRows {\n entity?: ContextMenuEntityRef | null | undefined;\n extraSections: ContextMenuExtraSection[];\n /**\n * The record's own name for the menu header (\"Note · Clinic intake\n * checklist\") — used when the clicked content names nothing itself, so the\n * header names the record instead of quoting its body. A selection still wins.\n */\n heading?: { label: string; text: string } | null;\n}\n\nconst registry = new Map<string, () => RecordMenuRows>();\n\n/**\n * A record's rows are pulled at open, but its NAME can change while a menu is\n * open or mounted (the note auto-labels from its first words 3 s after typing;\n * a person renames it). The owner calls `recordMenuChanged()` when what it\n * registers changes, and an open menu re-reads the heading (G6B review,\n * 2026-10-02: \"Note: New Note\" after the note was renamed).\n */\nlet revision = 0;\nconst listeners = new Set<() => void>();\n\n/** The owner of registered rows says they changed (its record was renamed). */\nexport function recordMenuChanged(): void {\n revision += 1;\n for (const listener of listeners) listener();\n}\n\n/** For `useSyncExternalStore`: re-render when any registered record changes. */\nexport function subscribeRecordMenus(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function recordMenusRevision(): number {\n return revision;\n}\n\n/** Register the rows a record contributes; returns the unregister. Read at every open, so always current. */\nexport function registerRecordMenu(key: string, get: () => RecordMenuRows): () => void {\n registry.set(key, get);\n return () => {\n if (registry.get(key) === get) registry.delete(key);\n };\n}\n\n/** The rows of the record whose content holds `target` (nearest marked root), or null. */\nexport function resolveRecordMenu(target: Element | null): RecordMenuRows | null {\n const key = target?.closest?.(`[${RECORD_MENU_ATTR}]`)?.getAttribute(RECORD_MENU_ATTR);\n if (!key) return null;\n return registry.get(key)?.() ?? null;\n}\n\nconst TRIGGER = '[data-alchemy-trigger=\"context\"]';\n\n/**\n * The right-click trigger a click on the record's content lands in: the first menu trigger inside\n * the marked root that carries content (`data-content-source`), else the root's own trigger.\n */\nfunction contentTrigger(root: HTMLElement): HTMLElement | null {\n const inner = root.querySelector<HTMLElement>(`${TRIGGER}[data-content-source]`);\n if (inner) return inner;\n if (root.matches(TRIGGER)) return root;\n return root.querySelector<HTMLElement>(TRIGGER) ?? root.closest<HTMLElement>(TRIGGER);\n}\n\n/**\n * Open the record content's own menu at `anchor` (a ⋯ button). Returns false when no content for\n * `key` is on screen, so the caller can open its own menu instead.\n */\nexport function openRecordMenu(key: string, anchor: HTMLElement): boolean {\n const doc = anchor.ownerDocument;\n const root = [...doc.querySelectorAll<HTMLElement>(`[${RECORD_MENU_ATTR}]`)].find(\n (el) => el.getAttribute(RECORD_MENU_ATTR) === key,\n );\n const trigger = root ? contentTrigger(root) : null;\n if (!trigger) return false;\n const rect = anchor.getBoundingClientRect();\n trigger.dispatchEvent(\n new MouseEvent(\"contextmenu\", {\n bubbles: true,\n cancelable: true,\n view: doc.defaultView ?? window,\n button: 2,\n buttons: 2,\n clientX: rect.left + rect.width / 2,\n clientY: rect.bottom,\n }),\n );\n return true;\n}\n","// features/context-menu-v3/utils/per-row-entity.ts\n//\n// THE PER-ROW ENTITY — the delegated-menu half of the entity contract.\n//\n// A table wires ONE menu for the whole pane (`resolveContextOnOpen`), so the\n// menu-level `entity` prop can never name the row that was actually\n// right-clicked: before 2026-08-24 Attach To / Share on a table menu silently\n// targeted the pane's entity, or rendered nothing at all. `resolveContextOnOpen`\n// may now return the reserved `CONTEXT_MENU_ENTITY_KEY` and the shell rebuilds\n// the entity-bound actions from it.\n//\n// Kept in its own module (not `value-resolution.ts`) on purpose: the INERT\n// SHELL imports this, and value-resolution pulls the surface-manifest registry.\n// Nothing here may import anything heavier than types.\n\nimport {\n CONTEXT_MENU_ENTITY_KEY,\n type ContextMenuEntityRef,\n type ResolvedContextMenuContext,\n} from \"../types\";\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * The effective entity for ONE menu open.\n *\n * key absent → the menu-level prop stands (single-entity surfaces unchanged)\n * key present → that row's entity wins\n * key `null` → this target has no entity, so the entity-bound actions hide\n * rather than target the wrong record\n *\n * A malformed value (no string `type` + `id`) is treated as absent and SCREAMS\n * in dev — a half-built entity would render an Attach that writes a broken edge.\n */\nexport function resolveEffectiveEntity(\n entityProp: ContextMenuEntityRef | undefined,\n resolved: ResolvedContextMenuContext | null,\n): ContextMenuEntityRef | undefined {\n if (!resolved || !(CONTEXT_MENU_ENTITY_KEY in resolved)) return entityProp;\n const perRow = resolved[CONTEXT_MENU_ENTITY_KEY];\n if (perRow === null || perRow === undefined) return undefined;\n const candidate = perRow as ContextMenuEntityRef;\n if (\n typeof perRow !== \"object\" ||\n typeof candidate.type !== \"string\" ||\n !candidate.type ||\n typeof candidate.id !== \"string\" ||\n !candidate.id\n ) {\n if (isDev) {\n console.error(\n `%c[ContextMenuV3] MALFORMED PER-ROW ENTITY%c — resolveContextOnOpen returned \"${CONTEXT_MENU_ENTITY_KEY}\" without a string type + id, so Attach To / Share would target nothing. Falling back to the menu-level entity.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { received: perRow },\n );\n }\n return entityProp;\n }\n return candidate;\n}\n\n/**\n * The effective `contextData` for one open: static payload + per-target merge,\n * with the reserved entity key stripped so it never lands in the\n * `ApplicationScope` as a value.\n */\nexport function mergeResolvedContextData(\n contextData: Record<string, unknown> | undefined,\n resolved: ResolvedContextMenuContext | null,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = {\n ...(contextData ?? {}),\n ...(resolved ?? {}),\n };\n delete merged[CONTEXT_MENU_ENTITY_KEY];\n return merged;\n}\n\n// ---------------------------------------------------------------------------\n// A TABLE ROW HAS TWO OWNERS — the table and the surface that mounted it\n// ---------------------------------------------------------------------------\n\n/**\n * The context for ONE open on a table row: the table's own row descriptor\n * (table-row-item.ts) JOINED with the surface's `resolveContextOnOpen` answer.\n *\n * 🚨 WHY (2026-10-03, CHAIR-REACH). The shell used to read `rowMenu?.context ?? surface(target)`:\n * once a canonical table registered its default row descriptor — which every MatrxDataTable does,\n * with `__entity: null` — the surface's resolver was never called on a row. So a list that names\n * its row's record there (CRM `useCrmRowMenu`, the keyword tables) lost Attach To, Share and\n * \"Link a record…\" on every row, and its own row doors with them (they are built from the state\n * that resolver sets).\n *\n * · The row descriptor still owns the row's VALUES (content, heading, the full row): on a key\n * both name, the descriptor stands (the 2026-09-26/27 heading rulings are unchanged).\n * · The ENTITY is the surface's to name when the descriptor names none: the table knows a row,\n * only the surface knows which record that row is. A descriptor that names its own entity\n * keeps it.\n * · Anything only the surface says is kept.\n */\nexport function joinRowAndSurfaceContext(\n row: ResolvedContextMenuContext | null | undefined,\n surface: ResolvedContextMenuContext | null | undefined,\n): ResolvedContextMenuContext | null {\n if (!row) return surface ?? null;\n if (!surface) return row;\n const rowNamesEntity = row[CONTEXT_MENU_ENTITY_KEY] !== null && row[CONTEXT_MENU_ENTITY_KEY] !== undefined;\n return {\n ...surface,\n ...row,\n ...(!rowNamesEntity && CONTEXT_MENU_ENTITY_KEY in surface\n ? { [CONTEXT_MENU_ENTITY_KEY]: surface[CONTEXT_MENU_ENTITY_KEY] }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// THE DOM SNIFFER — per-row identity with no resolver code\n// ---------------------------------------------------------------------------\n\n/**\n * A token SHAPE check, deliberately not a registry lookup.\n *\n * `isEntityTypeToken` would be the precise guard, but it lives in the 2,500-line\n * generated entity module and this file loads in the INERT SHELL (see the header\n * law). A shape check catches the realistic failure — a typo, a label, a raw\n * uuid in the wrong attribute — at zero weight; a well-formed but unregistered\n * token is caught downstream by Attach To's own write path.\n */\nconst TOKEN_SHAPE = /^[a-z][a-z0-9_]*$/;\n\n/**\n * Read the right-clicked row's entity straight off the DOM.\n *\n * 🚨 WHY THIS EXISTS (Phase 0, 2026-08-25). `resolveContextOnOpen` is the\n * precise path and remains it — but it made per-row identity cost a\n * hand-written resolver on EVERY table. With ~500 surfaces still to wire that\n * is ~500 bespoke `rowFor(target)` functions, each one a place to return the\n * wrong row. A row that already knows what it is can now simply SAY so:\n *\n * <tr data-entity-type=\"seo_keyword\" data-entity-id={k.id} data-entity-title={k.phrase}>\n *\n * and Attach To / Share target that record with no resolver at all.\n *\n * PRECEDENCE — THE SURFACE ALWAYS WINS. This runs ONLY when\n * `resolveContextOnOpen` did not speak about the entity at all (key absent).\n * An explicit `null` from the surface means \"this target has no entity\" and is\n * honoured; the DOM never overrides a deliberate answer.\n */\nexport function sniffEntityFromDom(\n target: HTMLElement | null,\n): ContextMenuEntityRef | null {\n const host = target?.closest?.(\"[data-entity-type][data-entity-id]\");\n if (!(host instanceof HTMLElement)) return null;\n\n const type = host.getAttribute(\"data-entity-type\")?.trim() ?? \"\";\n const id = host.getAttribute(\"data-entity-id\")?.trim() ?? \"\";\n if (!type || !id) return null;\n\n if (!TOKEN_SHAPE.test(type)) {\n if (isDev) {\n console.error(\n `%c[ContextMenuV3] MALFORMED data-entity-type%c — \"${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.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { element: host },\n );\n }\n return null;\n }\n\n // Title is what the user sees the row called; the row's own text is the\n // honest fallback so an Attach is never labelled with a raw uuid.\n const title =\n host.getAttribute(\"data-entity-title\")?.trim() ||\n (host.textContent ?? \"\").trim().slice(0, 120) ||\n id;\n\n const resourceType = host.getAttribute(\"data-entity-resource\")?.trim();\n\n return {\n type: type as ContextMenuEntityRef[\"type\"],\n id,\n title,\n ...(resourceType\n ? { resourceType: resourceType as NonNullable<ContextMenuEntityRef[\"resourceType\"]> }\n : {}),\n };\n}\n\n/**\n * The effective entity for one open, DOM sniff included. Surface answer wins;\n * the sniff only fills a silence.\n */\nexport function resolveEffectiveEntityWithDom(\n entityProp: ContextMenuEntityRef | undefined,\n resolved: ResolvedContextMenuContext | null,\n target: HTMLElement | null,\n): ContextMenuEntityRef | undefined {\n const surfaceSpoke = !!resolved && CONTEXT_MENU_ENTITY_KEY in resolved;\n if (surfaceSpoke) return resolveEffectiveEntity(entityProp, resolved);\n return sniffEntityFromDom(target) ?? entityProp;\n}\n","/**\n * ONE MENU, SEVERAL SOURCES OF SURFACE ROWS — joined so every row id is unique.\n *\n * A menu's surface sections can come from more than one owner for one open: a table row's\n * descriptor plus the grid's per-target sections, or the content's own sections plus the RECORD\n * it belongs to (record-menu-registry.ts — a note's tab rows join the menu opened on the note's\n * content). Each owner keeps its own ids unique, but not across owners, and the menu model\n * namespaces every surface row as `x:<id>` (menu-model.ts) — so the same id from two owners\n * reached the one Alchemy registry twice and it refused the second:\n * `DuplicateActionError: Action \"cm:x:save\" is yielded twice by provider \"context-menu:…\"`\n * (/notes, 2026-09-30: the editor's \"Save\" and the tab's \"Save\", both saving the same note).\n *\n * Sources are passed in precedence order. Two owners describing one target with the same id\n * mean the same action on it, so the FIRST source's row stands and a later source's row with\n * that id is not drawn a second time. A duplicate inside ONE source is that source's own defect\n * and is left untouched, so the registry still refuses it loudly.\n */\nimport type { ContextMenuExtraItem, ContextMenuExtraSection } from \"../types\";\n\nfunction isRow(item: ContextMenuExtraItem): boolean {\n return item.kind !== \"separator\";\n}\n\n/** Drop leading, trailing and doubled separators left behind when rows are removed. */\nfunction tidySeparators(items: ContextMenuExtraItem[]): ContextMenuExtraItem[] {\n const out: ContextMenuExtraItem[] = [];\n for (const item of items) {\n if (!isRow(item) && (out.length === 0 || !isRow(out[out.length - 1]!))) continue;\n out.push(item);\n }\n while (out.length > 0 && !isRow(out[out.length - 1]!)) out.pop();\n return out;\n}\n\nexport function joinExtraSections(\n ...sources: Array<ContextMenuExtraSection[] | null | undefined>\n): ContextMenuExtraSection[] | undefined {\n const present = sources.filter((s): s is ContextMenuExtraSection[] => Array.isArray(s));\n if (present.length === 0) return undefined;\n if (present.length === 1) return present[0];\n const taken = new Set<string>();\n const out: ContextMenuExtraSection[] = [];\n for (const source of present) {\n const ownIds = new Set<string>();\n for (const section of source) {\n const kept = section.items.filter((item) => !isRow(item) || !taken.has(item.id));\n for (const item of kept) if (isRow(item)) ownIds.add(item.id);\n if (kept.length === section.items.length) {\n out.push(section);\n continue;\n }\n const items = tidySeparators(kept);\n if (items.some(isRow)) out.push({ ...section, items });\n }\n for (const id of ownIds) taken.add(id);\n }\n return out;\n}\n","// features/context-menu-v3/utils/widget-handle.ts\n//\n// Inline agent editing — the v1 promise, paid via the platform channel.\n//\n// Builds a `WidgetHandle` (the canonical client-tool contract — see\n// `features/agents/types/widget-handle.types.ts` and CLIENT_SIDE_TOOLS.md)\n// from the SAME props an editable surface already passes to\n// `EditableContextMenu` (`onTextReplace` / `onTextInsertBefore|After` /\n// `getTextarea`). The shell registers it via `useOptionalWidgetHandle` and the\n// launch handlers pass the id as `runtime.widgetHandleId`, so any agent\n// launched from the menu on an editable surface can stream `widget_text_*`\n// tool calls that edit the surface's content in place — zero extra wiring per\n// surface, no bespoke edit protocol (the old XML-in-stream idea stays dead).\n//\n// Precedence: a surface callback (controlled React state) always wins; the\n// fallback writes through `setFieldValue` (native setter + input event), which\n// works on BOTH controlled and uncontrolled textareas. Only methods this\n// surface can actually service are present — the per-turn assembler\n// (`deriveClientToolsFromHandle`) advertises exactly that subset.\n\nimport type { WidgetHandle } from \"@ai-matrx/chat/agents/types/widget-handle.types\";\nimport type { ApplicationScope } from \"@ai-matrx/chat/agents/types/scope.types\";\n\n/**\n * Write a value into a textarea so BOTH controlled and uncontrolled fields\n * take it: the native value setter bypasses React's value tracker, and the\n * bubbled `input` event makes a controlled component's onChange fire (state\n * updates instead of React reverting the DOM on the next render). A bare\n * `element.value = x` write silently loses the edit on controlled fields —\n * while the tool result still reports ok:true to the agent.\n */\nfunction setFieldValue(\n el: HTMLTextAreaElement,\n next: string,\n caret: number,\n): void {\n const setter = Object.getOwnPropertyDescriptor(\n HTMLTextAreaElement.prototype,\n \"value\",\n )?.set;\n if (setter) setter.call(el, next);\n else el.value = next;\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n try {\n el.setSelectionRange(caret, caret);\n } catch {\n // non-focusable / detached — caret is best-effort\n }\n}\n\nexport interface BuildEditableWidgetHandleArgs {\n getTextarea?: (() => HTMLTextAreaElement | null) | undefined;\n onTextReplace?: ((newText: string) => void) | undefined;\n onTextInsertBefore?: ((text: string) => void) | undefined;\n onTextInsertAfter?: ((text: string) => void) | undefined;\n /** Live scope builder — read for `content` when there is no textarea. */\n getApplicationScope?: (() => ApplicationScope) | undefined;\n}\n\n/**\n * Build the widget handle for an editable surface, or `null` when the surface\n * exposes no way to write (nothing to register, no tools advertised).\n */\nexport function buildEditableWidgetHandle(\n args: BuildEditableWidgetHandleArgs,\n): WidgetHandle | null {\n const {\n getTextarea,\n onTextReplace,\n onTextInsertBefore,\n onTextInsertAfter,\n getApplicationScope,\n } = args;\n\n const field = (): HTMLTextAreaElement | null => getTextarea?.() ?? null;\n\n const readCurrent = (): string | null => {\n const el = field();\n if (el) return el.value;\n const content = getApplicationScope?.().content;\n return typeof content === \"string\" ? content : null;\n };\n\n // Full-content write. Surface callback wins; the field fallback goes\n // through setFieldValue so controlled AND uncontrolled textareas take it.\n const writeFull = (next: string): void => {\n if (onTextReplace) {\n onTextReplace(next);\n return;\n }\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n setFieldValue(el, next, next.length);\n };\n\n // Capability PRESENCE only — this function runs in the shell's render body,\n // so it must never invoke a surface callback (getTextarea reads the DOM,\n // getApplicationScope may serialize the whole document; per-keystroke cost\n // + a React render-purity violation). The method bodies re-check liveness\n // (readCurrent() null checks) at call time, which is the only time it matters.\n const canWrite = Boolean(onTextReplace) || Boolean(getTextarea);\n const canRead = Boolean(getTextarea) || Boolean(getApplicationScope);\n if (!canWrite) return null;\n\n // No `applyPolicy`: an agent's write into the person's own text is `ask` —\n // the approval card shows `readText()` as the \"before\".\n const handle: WidgetHandle = {\n onTextReplace: ({ text }) => writeFull(text),\n ...(canRead ? { readText: readCurrent } : {}),\n };\n\n // Insert relative to the cursor/selection. Surface callbacks carry their\n // own semantics; the fallback splices at the field's live cursor.\n if (onTextInsertBefore || getTextarea) {\n handle.onTextInsertBefore = ({ text }) => {\n if (onTextInsertBefore) return onTextInsertBefore(text);\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n const at = el.selectionStart ?? 0;\n setFieldValue(\n el,\n el.value.slice(0, at) + text + el.value.slice(at),\n at + text.length,\n );\n };\n }\n if (onTextInsertAfter || getTextarea) {\n handle.onTextInsertAfter = ({ text }) => {\n if (onTextInsertAfter) return onTextInsertAfter(text);\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n const at = el.selectionEnd ?? el.value.length;\n setFieldValue(\n el,\n el.value.slice(0, at) + text + el.value.slice(at),\n at + text.length,\n );\n };\n }\n\n // Whole-content operations need read + write.\n if (canRead) {\n handle.onTextPrepend = ({ text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n writeFull(text + current);\n };\n handle.onTextAppend = ({ text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n writeFull(current + text);\n };\n handle.onTextPatch = ({ search_text, replacement_text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n const idx = current.indexOf(search_text);\n if (idx === -1)\n // access-errors: ok — in-memory text-patch match against the surface's own content, no record read\n throw new Error(\"search_text not found in surface content\");\n writeFull(\n current.slice(0, idx) +\n replacement_text +\n current.slice(idx + search_text.length),\n );\n };\n }\n\n return handle;\n}\n","// features/context-menu-v3/utils/table-at-target.ts\n//\n// THE TABLE A RIGHT-CLICK LANDED ON, with its shape kept (header + rows). A chat answer's menu is\n// one menu over the whole answer, so \"Save to a table\" read the answer's text and opened on its\n// FIRST shape — a bullet list above the table won, and the table the person right-clicked was\n// never offered first. The shell hands the rich-document registry this reader (callbacks\n// `tableAtTarget`); the one shape reader (`shapeTextOfNode`, records-ui) turns the clicked\n// <table> back into table text, streamed or finished — it reads the rendered DOM.\n\n// The shape reader is imported only when a right-click lands on a <table> (the shell calls\n// `preloadTableShape` from its open path), never with the page: a menu is opened on ~1 in 149\n// page loads and most opens are not on a table.\ntype ShapeReader = (node: Node) => string;\nlet shapeReader: ShapeReader | null = null;\nlet shapeLoad: Promise<unknown> | null = null;\n\nexport function preloadTableShape(): Promise<unknown> {\n shapeLoad ??= import(\"@ai-matrx/records-ui/table-shape\")\n .then((m) => {\n shapeReader = m.shapeTextOfNode as ShapeReader;\n })\n .catch((error: unknown) => {\n shapeLoad = null;\n console.error(\"[ContextMenuV3] could not load the table shape reader\", error);\n });\n return shapeLoad;\n}\n\n/** Sync; null until the reader has loaded (a click on a table starts the load at open). */\nexport function tableTextAtTarget(target: Element | null): string | null {\n const table = target?.closest(\"table\");\n if (!table || !shapeReader) return null;\n const text = shapeReader(table.cloneNode(true));\n return text.trim() ? text : null;\n}\n","/**\n * The attributes ContextMenuV3 stamps onto every right-click REGION it wraps\n * (the chat composer textarea, every sidebar chat row, messages, table rows).\n *\n * ONE source: `ContextMenuV3.tsx` spreads this onto its trigger, and the real-\n * browser layout gate (`features/shell/layout-gate/shipped-css-region-triggers.spec.ts`)\n * renders its fixtures with the same object — so a stylesheet the app ships\n * that styles these attributes (the 2026-09-26 `[data-alchemy-trigger] {\n * width: 2rem }` collapse) is measured against exactly what production emits.\n *\n * Plain data, no React: the Playwright spec imports it directly.\n */\nexport const CONTEXT_REGION_TRIGGER_ATTRS = {\n \"data-alchemy-trigger\": \"context\",\n} as const;\n","/**\n * features/context-menu-v3/item/itemMenuToV3.ts\n *\n * ItemMenuConfig → v3 `extraSections` converter — the bridge that lets\n * `ItemContextMenu` render the ONE universal context menu instead of its own\n * Radix tree. Command/toggle execution reuses `run-entry.ts` (toast.promise\n * parity with the kebab dropdown), checkbox/link map to the dedicated v3 item\n * kinds, and section boundaries/labels carry over. Deliberate deltas from the\n * old bespoke render (documented in FEATURE.md): no in-menu single-key\n * shortcut execution (hints still display) and the header renders as a\n * leading section label instead of a Radix label block.\n */\n\nimport { compact } from \"../utils/compact\";\nimport type {\n ContextMenuExtraItem,\n ContextMenuExtraSection,\n} from \"../types\";\nimport {\n isCheckbox,\n isLink,\n isSubmenu,\n runCommand as runItemCommand,\n runToggle,\n type ItemMenuCommand,\n type ItemMenuConfig,\n type ItemMenuEntry,\n type ItemMenuSection,\n} from \"@ai-matrx/design-system/item\";\nimport { toast } from \"@ai-matrx/chat/host/notify\";\n\n// design-system's item icon is any component taking a className; the v3 menu's icon slot takes the same.\nconst iconOf = (entry: ItemMenuEntry) => entry.icon;\n\nconst toastPromise = (promise: Promise<unknown>, messages: { loading: string; success: string; error: (e: unknown) => string }) => {\n toast.promise(promise, messages);\n};\n\nfunction runCommand(entry: ItemMenuCommand) {\n runItemCommand(entry, toastPromise);\n}\n\nexport { toastPromise as itemToastPromise };\n\nfunction entryToExtraItem(entry: ItemMenuEntry): ContextMenuExtraItem {\n const description =\n entry.disabled && entry.disabledReason\n ? entry.disabledReason\n : entry.description;\n\n if (isSubmenu(entry)) {\n return compact({\n kind: \"submenu\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n disabled: entry.disabled,\n children: submenuChildren(entry.sections),\n }) as ContextMenuExtraItem;\n }\n if (isCheckbox(entry)) {\n return compact({\n kind: \"checkbox\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n checked: entry.checked,\n onCheckedChange: (next: boolean) => runToggle(entry, next),\n disabled: entry.disabled,\n hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),\n }) as ContextMenuExtraItem;\n }\n if (isLink(entry)) {\n return compact({\n kind: \"link\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n href: entry.href,\n target: entry.target,\n disabled: entry.disabled,\n }) as ContextMenuExtraItem;\n }\n // command\n return compact({\n kind: \"item\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n destructive: entry.tone === \"destructive\",\n disabled: entry.disabled,\n hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),\n onSelect: () => runCommand(entry),\n }) as ContextMenuExtraItem;\n}\n\n/** Flatten submenu sections into children with separators at boundaries. */\nfunction submenuChildren(sections: ItemMenuSection[]): ContextMenuExtraItem[] {\n const visible = sections\n .map((s) => ({ ...s, items: s.items.filter((e) => !e.hidden) }))\n .filter((s) => s.items.length > 0);\n const out: ContextMenuExtraItem[] = [];\n visible.forEach((section, i) => {\n if (i > 0)\n out.push({\n kind: \"separator\",\n id: `sep-${section.id ?? section.label ?? i}`,\n });\n out.push(...section.items.map(entryToExtraItem));\n });\n return out;\n}\n\nexport function itemMenuConfigToExtraSections(\n config: ItemMenuConfig,\n): ContextMenuExtraSection[] {\n const sections: ContextMenuExtraSection[] = config.sections\n .map((s, i) => compact({\n id: String(s.id ?? s.label ?? `im-${i}`),\n label: s.label,\n anchor: \"after-clipboard\" as const,\n items: s.items.filter((e) => !e.hidden).map(entryToExtraItem),\n }))\n .filter((s) => s.items.length > 0);\n\n // Header → leading label on the first section (or its own labeled block).\n const title = config.header?.title;\n const first = sections[0];\n if (title && first && !first.label) {\n sections[0] = { ...first, label: title };\n }\n return sections;\n}\n","// features/context-menu-v3/selection-provider.ts\n//\n// The context menu's one entry in the ONE selection toolbar\n// (components/selection-toolbar): \"Ask AI\" — it opens the same Alchemy menu a\n// right-click opens, over the selected text (the AI actions, the agent\n// libraries, copy / export / save), on desktop and as the sheet on a phone.\n// It replaces the old floating selection icon: the menu has one door on a\n// selection, and it sits in the toolbar with every other passage action.\n\nimport { AGENT_ICON } from \"@ai-matrx/icons/domain\";\nimport type { Action, ActionProvider, ClickTarget } from \"@ai-matrx/alchemy/actions\";\nimport { registerAlchemyIcon } from \"@ai-matrx/rich-content/utils/alchemy-icon-keys\";\nimport {\n declareSelectionProvider,\n placeSelectionActions,\n hostHalf,\n selectionToolbarHostOf,\n shownInSelectionMode,\n} from \"@ai-matrx/rich-content/selection-toolbar/selection-actions\";\n\nexport const CONTEXT_MENU_SELECTION_HOST_KEY = \"contextMenuSelection\";\n\nexport interface ContextMenuSelectionHost {\n kind: \"context-menu-selection\";\n /** Open this menu instance over the current selection. */\n open(): void;\n}\n\nfunction menuOf(target: ClickTarget): ContextMenuSelectionHost | null {\n const half = hostHalf<ContextMenuSelectionHost>(target, CONTEXT_MENU_SELECTION_HOST_KEY);\n return half?.kind === \"context-menu-selection\" ? half : null;\n}\n\nconst ASK_AI: Action = {\n id: \"selection:ai\",\n label: \"Ask AI\",\n description: \"AI actions, agents, copy and export for the selected text\",\n icon: registerAlchemyIcon(AGENT_ICON),\n category: \"ai\",\n order: 0,\n placement: \"primary\",\n preserveSelection: true,\n eligible: (t) => (menuOf(t) && shownInSelectionMode(ASK_AI.id, t) ? { status: \"available\" } : { status: \"absent\" }),\n run: (t) => {\n const menu = menuOf(t);\n if (!menu) return;\n // The toolbar steps aside; the selection stays for the menu to act on.\n selectionToolbarHostOf(t)?.ui.close();\n menu.open();\n },\n};\n\nexport const contextMenuSelectionProvider: ActionProvider = {\n id: \"context-menu-selection\",\n tier: \"T0\",\n declaredIds: () => [ASK_AI.id],\n actions: (target) => (menuOf(target) ? placeSelectionActions([ASK_AI], target) : []),\n};\n\ndeclareSelectionProvider(contextMenuSelectionProvider);\n"],"mappings":"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","names":[]}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
var _chunkWROBL3DRcjs = require('./chunk-WROBL3DR.cjs');
|
|
6
7
|
|
|
7
8
|
// src/engine/table-row-item.ts
|
|
8
9
|
var _declare = require('@ai-matrx/alchemy/declare');
|
|
@@ -25,7 +26,7 @@ function tableRowEditCommands(controls) {
|
|
|
25
26
|
const value = Reflect.get(controls, key);
|
|
26
27
|
return isCommand(value) ? value : void 0;
|
|
27
28
|
};
|
|
28
|
-
return
|
|
29
|
+
return _chunkWROBL3DRcjs.compact.call(void 0, {
|
|
29
30
|
beginEdit: read("beginEdit"),
|
|
30
31
|
saveEdits: read("saveEdits"),
|
|
31
32
|
cancelEdits: read("cancelEdits")
|
|
@@ -108,12 +109,12 @@ function resolveTableRowMenuDescriptor(target) {
|
|
|
108
109
|
function headedMenu(descriptor, target, row) {
|
|
109
110
|
const cell = _optionalChain([target, 'optionalAccess', _16 => _16.closest, 'call', _17 => _17("td, [role='gridcell']")]);
|
|
110
111
|
const shown = shownWords(_nullishCoalesce(cell, () => ( row)));
|
|
111
|
-
const ownHeading = descriptor.context[
|
|
112
|
+
const ownHeading = descriptor.context[_chunkWROBL3DRcjs.CONTEXT_MENU_HEADING_KEY];
|
|
112
113
|
const recordName = ownHeading ? "" : rowRecordName(row);
|
|
113
114
|
const context = {
|
|
114
115
|
...descriptor.context,
|
|
115
116
|
...shown ? { content: shown } : {},
|
|
116
|
-
...recordName ? { [
|
|
117
|
+
...recordName ? { [_chunkWROBL3DRcjs.CONTEXT_MENU_HEADING_KEY]: { label: "Row", text: recordName } } : {}
|
|
117
118
|
};
|
|
118
119
|
const headed = _nullishCoalesce(_optionalChain([ownHeading, 'optionalAccess', _18 => _18.text]), () => ( recordName));
|
|
119
120
|
const same = (label) => typeof headed === "string" && !!label && label.trim().toLowerCase() === headed.trim().toLowerCase();
|
|
@@ -207,14 +208,14 @@ function openRecordMenu(key, anchor) {
|
|
|
207
208
|
// src/engine/utils/per-row-entity.ts
|
|
208
209
|
var isDev = process.env.NODE_ENV !== "production";
|
|
209
210
|
function resolveEffectiveEntity(entityProp, resolved) {
|
|
210
|
-
if (!resolved || !(
|
|
211
|
-
const perRow = resolved[
|
|
211
|
+
if (!resolved || !(_chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY in resolved)) return entityProp;
|
|
212
|
+
const perRow = resolved[_chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY];
|
|
212
213
|
if (perRow === null || perRow === void 0) return void 0;
|
|
213
214
|
const candidate = perRow;
|
|
214
215
|
if (typeof perRow !== "object" || typeof candidate.type !== "string" || !candidate.type || typeof candidate.id !== "string" || !candidate.id) {
|
|
215
216
|
if (isDev) {
|
|
216
217
|
console.error(
|
|
217
|
-
`%c[ContextMenuV3] MALFORMED PER-ROW ENTITY%c \u2014 resolveContextOnOpen returned "${
|
|
218
|
+
`%c[ContextMenuV3] MALFORMED PER-ROW ENTITY%c \u2014 resolveContextOnOpen returned "${_chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY}" without a string type + id, so Attach To / Share would target nothing. Falling back to the menu-level entity.`,
|
|
218
219
|
"color:#ef4444;font-weight:bold",
|
|
219
220
|
"color:inherit",
|
|
220
221
|
{ received: perRow }
|
|
@@ -229,17 +230,17 @@ function mergeResolvedContextData(contextData, resolved) {
|
|
|
229
230
|
..._nullishCoalesce(contextData, () => ( {})),
|
|
230
231
|
..._nullishCoalesce(resolved, () => ( {}))
|
|
231
232
|
};
|
|
232
|
-
delete merged[
|
|
233
|
+
delete merged[_chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY];
|
|
233
234
|
return merged;
|
|
234
235
|
}
|
|
235
236
|
function joinRowAndSurfaceContext(row, surface) {
|
|
236
237
|
if (!row) return _nullishCoalesce(surface, () => ( null));
|
|
237
238
|
if (!surface) return row;
|
|
238
|
-
const rowNamesEntity = row[
|
|
239
|
+
const rowNamesEntity = row[_chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY] !== null && row[_chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY] !== void 0;
|
|
239
240
|
return {
|
|
240
241
|
...surface,
|
|
241
242
|
...row,
|
|
242
|
-
...!rowNamesEntity &&
|
|
243
|
+
...!rowNamesEntity && _chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY in surface ? { [_chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY]: surface[_chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY] } : {}
|
|
243
244
|
};
|
|
244
245
|
}
|
|
245
246
|
var TOKEN_SHAPE = /^[a-z][a-z0-9_]*$/;
|
|
@@ -270,7 +271,7 @@ function sniffEntityFromDom(target) {
|
|
|
270
271
|
};
|
|
271
272
|
}
|
|
272
273
|
function resolveEffectiveEntityWithDom(entityProp, resolved, target) {
|
|
273
|
-
const surfaceSpoke = !!resolved &&
|
|
274
|
+
const surfaceSpoke = !!resolved && _chunkWROBL3DRcjs.CONTEXT_MENU_ENTITY_KEY in resolved;
|
|
274
275
|
if (surfaceSpoke) return resolveEffectiveEntity(entityProp, resolved);
|
|
275
276
|
return _nullishCoalesce(sniffEntityFromDom(target), () => ( entityProp));
|
|
276
277
|
}
|
|
@@ -450,7 +451,7 @@ function runCommand(entry) {
|
|
|
450
451
|
function entryToExtraItem(entry) {
|
|
451
452
|
const description = entry.disabled && entry.disabledReason ? entry.disabledReason : entry.description;
|
|
452
453
|
if (_item.isSubmenu.call(void 0, entry)) {
|
|
453
|
-
return
|
|
454
|
+
return _chunkWROBL3DRcjs.compact.call(void 0, {
|
|
454
455
|
kind: "submenu",
|
|
455
456
|
id: entry.id,
|
|
456
457
|
label: entry.label,
|
|
@@ -460,7 +461,7 @@ function entryToExtraItem(entry) {
|
|
|
460
461
|
});
|
|
461
462
|
}
|
|
462
463
|
if (_item.isCheckbox.call(void 0, entry)) {
|
|
463
|
-
return
|
|
464
|
+
return _chunkWROBL3DRcjs.compact.call(void 0, {
|
|
464
465
|
kind: "checkbox",
|
|
465
466
|
id: entry.id,
|
|
466
467
|
label: entry.label,
|
|
@@ -473,7 +474,7 @@ function entryToExtraItem(entry) {
|
|
|
473
474
|
});
|
|
474
475
|
}
|
|
475
476
|
if (_item.isLink.call(void 0, entry)) {
|
|
476
|
-
return
|
|
477
|
+
return _chunkWROBL3DRcjs.compact.call(void 0, {
|
|
477
478
|
kind: "link",
|
|
478
479
|
id: entry.id,
|
|
479
480
|
label: entry.label,
|
|
@@ -484,7 +485,7 @@ function entryToExtraItem(entry) {
|
|
|
484
485
|
disabled: entry.disabled
|
|
485
486
|
});
|
|
486
487
|
}
|
|
487
|
-
return
|
|
488
|
+
return _chunkWROBL3DRcjs.compact.call(void 0, {
|
|
488
489
|
kind: "item",
|
|
489
490
|
id: entry.id,
|
|
490
491
|
label: entry.label,
|
|
@@ -510,7 +511,7 @@ function submenuChildren(sections) {
|
|
|
510
511
|
return out;
|
|
511
512
|
}
|
|
512
513
|
function itemMenuConfigToExtraSections(config) {
|
|
513
|
-
const sections = config.sections.map((s, i) =>
|
|
514
|
+
const sections = config.sections.map((s, i) => _chunkWROBL3DRcjs.compact.call(void 0, {
|
|
514
515
|
id: String(_nullishCoalesce(_nullishCoalesce(s.id, () => ( s.label)), () => ( `im-${i}`))),
|
|
515
516
|
label: s.label,
|
|
516
517
|
anchor: "after-clipboard",
|
|
@@ -595,4 +596,4 @@ _selectionactions.declareSelectionProvider.call(void 0, contextMenuSelectionProv
|
|
|
595
596
|
|
|
596
597
|
|
|
597
598
|
exports.createTableRowMenuDescriptor = createTableRowMenuDescriptor; exports.tableRowEditCommands = tableRowEditCommands; exports.createDefaultTableRowMenuDescriptor = createDefaultTableRowMenuDescriptor; exports.buildDefaultTableRowMenuDescriptor = buildDefaultTableRowMenuDescriptor; exports.registerTableRowContextResolver = registerTableRowContextResolver; exports.resolveTableRowItem = resolveTableRowItem; exports.resolveTableRowMenuDescriptor = resolveTableRowMenuDescriptor; exports.rowRecordName = rowRecordName; exports.RECORD_MENU_ATTR = RECORD_MENU_ATTR; exports.recordMenuChanged = recordMenuChanged; exports.subscribeRecordMenus = subscribeRecordMenus; exports.recordMenusRevision = recordMenusRevision; exports.registerRecordMenu = registerRecordMenu; exports.resolveRecordMenu = resolveRecordMenu; exports.openRecordMenu = openRecordMenu; exports.resolveEffectiveEntity = resolveEffectiveEntity; exports.mergeResolvedContextData = mergeResolvedContextData; exports.joinRowAndSurfaceContext = joinRowAndSurfaceContext; exports.sniffEntityFromDom = sniffEntityFromDom; exports.resolveEffectiveEntityWithDom = resolveEffectiveEntityWithDom; exports.joinExtraSections = joinExtraSections; exports.buildEditableWidgetHandle = buildEditableWidgetHandle; exports.preloadTableShape = preloadTableShape; exports.tableTextAtTarget = tableTextAtTarget; exports.CONTEXT_REGION_TRIGGER_ATTRS = CONTEXT_REGION_TRIGGER_ATTRS; exports.toastPromise = toastPromise; exports.itemMenuConfigToExtraSections = itemMenuConfigToExtraSections; exports.CONTEXT_MENU_SELECTION_HOST_KEY = CONTEXT_MENU_SELECTION_HOST_KEY; exports.contextMenuSelectionProvider = contextMenuSelectionProvider;
|
|
598
|
-
//# sourceMappingURL=chunk-
|
|
599
|
+
//# sourceMappingURL=chunk-ERFH2RZC.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-ERFH2RZC.cjs","../src/engine/table-row-item.ts","../src/engine/record-menu-registry.ts","../src/engine/utils/per-row-entity.ts","../src/engine/utils/join-extra-sections.ts","../src/engine/utils/widget-handle.ts","../src/engine/utils/table-at-target.ts","../src/engine/region-trigger-attrs.ts","../src/engine/item/itemMenuToV3.ts","../src/engine/selection-provider.ts"],"names":[],"mappings":"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AGW,EAAA;AACgC,IAAA;AAC9B,IAAA;AACD,IAAA;AACgC,IAAA;AAET,EAAA;AAGN,EAAA;AACL,EAAA;AACY,EAAA;AACK,IAAA;AACzC,EAAA;AACO,EAAA;AACT;ARuY4C;AACA;AStgBjB;AAES;AACpC;AACE;AACA;AACA;AACA;AACA;AACK;AAEwC;AAQuB;AACZ,EAAA;AAClC,EAAA;AACxB;AAEuB;AACjB,EAAA;AACG,EAAA;AACM,EAAA;AACuB,EAAA;AAC1B,EAAA;AACH,EAAA;AACI,EAAA;AACQ,EAAA;AACY,EAAA;AACnB,EAAA;AACW,IAAA;AACV,IAAA;AAEyB,oBAAA;AAC1B,IAAA;AACZ,EAAA;AACF;AAE4D;AACtD,EAAA;AACE,EAAA;AACuB,EAAA;AACU,EAAA;AACzC;AAEyB;AT2fmB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-ERFH2RZC.cjs","sourcesContent":[null,"/**\n * THE TABLE ROW IS A DECLARED ITEM (ALC-18, LIST.md D4).\n *\n * A canonical table's row is alchemy's `table_row` item type (`TABLE_ROW_ITEM_TYPE`, a baseline\n * item type of every surface — `features/surfaces/manifests/registry.ts`). Each mounted table\n * registers ONE item source with `@ai-matrx/alchemy/surface` `itemSources` under its table id; a\n * right-click resolves the row through that one path into the declared item (identity\n * `table_id` + `row_id`, the level, its values) plus the row's menu, which rides as the item's\n * opaque host data. There is no private registry here: this file only builds the row's menu and\n * reads the words the row shows.\n */\nimport { compact } from \"./utils/compact\";\nimport { CONTEXT_MENU_HEADING_KEY, type ContextMenuExtraSection, type ResolvedContextMenuContext } from \"./types\";\nimport { TABLE_ROW_ITEM } from \"@ai-matrx/alchemy/declare\";\nimport type { ResolvedItem } from \"@ai-matrx/alchemy/declare\";\nimport { itemSources } from \"@ai-matrx/alchemy/surface\";\nimport type { MatrxDataTableRecordControls } from \"@ai-matrx/design-system/data-table/types\";\nimport type {\n MatrxTableMenuLevel,\n MatrxTableMenuPayload,\n MatrxTableMenuTarget,\n} from \"@ai-matrx/design-system/data-table/menu-targets\";\n\nexport interface TableRowMenuDescriptor {\n context: ResolvedContextMenuContext;\n extraSections: ContextMenuExtraSection[];\n}\n\n/**\n * The row's menu as the table hands it back (the table types it `unknown` and never reads it). A\n * class, so a resolver answering anything else is told apart without a private lookup table.\n */\nclass TableRowMenu {\n constructor(readonly descriptor: TableRowMenuDescriptor) {}\n}\n\nexport function createTableRowMenuDescriptor(descriptor: TableRowMenuDescriptor): object {\n return new TableRowMenu(descriptor);\n}\n\n/** Exactly the row commands the default menu renders. Nothing else is read. */\nexport type TableRowEditCommands = Pick<\n MatrxDataTableRecordControls,\n \"beginEdit\" | \"saveEdits\" | \"cancelEdits\"\n>;\n\nfunction isCommand(value: unknown): value is () => void {\n return typeof value === \"function\";\n}\n\n/**\n * The table hands its host the row's controls as `unknown` — it never\n * interprets what the host builds from them — so they are READ here, one\n * command at a time. A command the table did not send, or sent as something\n * that cannot be called, is simply not offered rather than rendered dead.\n */\nexport function tableRowEditCommands(controls: unknown): TableRowEditCommands {\n if (typeof controls !== \"object\" || controls === null) return {};\n const read = (key: string): (() => void) | undefined => {\n const value: unknown = Reflect.get(controls, key);\n return isCommand(value) ? value : undefined;\n };\n return compact({\n beginEdit: read(\"beginEdit\"),\n saveEdits: read(\"saveEdits\"),\n cancelEdits: read(\"cancelEdits\"),\n });\n}\n\n/**\n * The host's generic menu model: complete current row data plus the\n * table-owned edit commands.\n *\n * The table asks this for whichever of its five right-click LEVELS was aimed\n * at. Only the row has a generic menu here, so every other level answers\n * `null` and the table leaves that level to the surface that owns it.\n */\nexport function createDefaultTableRowMenuDescriptor(\n payload: MatrxTableMenuPayload,\n): object | null {\n if (payload.level !== \"row\") return null;\n return createTableRowMenuDescriptor(\n buildDefaultTableRowMenuDescriptor(\n payload.row,\n tableRowEditCommands(payload.controls),\n ),\n );\n}\n\n/** Reusable base for a domain menu that keeps the shared row edit controls. */\nexport function buildDefaultTableRowMenuDescriptor(\n row: unknown,\n controls: TableRowEditCommands,\n): TableRowMenuDescriptor {\n const items = [\n ...(controls.beginEdit\n ? [{ kind: \"item\" as const, id: \"table-edit-row\", label: \"Edit row\", onSelect: controls.beginEdit }]\n : []),\n ...(controls.saveEdits\n ? [{ kind: \"item\" as const, id: \"table-save-row\", label: \"Save changes\", onSelect: () => void controls.saveEdits?.() }]\n : []),\n ...(controls.cancelEdits\n ? [{ kind: \"item\" as const, id: \"table-discard-row\", label: \"Discard changes\", onSelect: controls.cancelEdits }]\n : []),\n ];\n return {\n context: {\n content: JSON.stringify(row, null, 2) ?? String(row),\n context: row,\n __entity: null,\n },\n extraSections: items.length === 0\n ? []\n : [{ id: \"table-row\", label: \"Row\", primary: true, anchor: \"after-clipboard\", items }],\n };\n}\n\n/** The mounted row element of a table, read when the item's values are read. */\nfunction rowElement(tableId: string, rowId: string): HTMLElement | null {\n if (typeof document === \"undefined\") return null;\n const table = Array.from(document.querySelectorAll<HTMLElement>(\"[data-matrx-table-id]\")).find((el) => el.dataset.matrxTableId === tableId);\n return Array.from(table?.querySelectorAll<HTMLElement>(\"[data-row-id]\") ?? []).find((el) => el.dataset.rowId === rowId) ?? null;\n}\n\n/** A row is writable when its table offers the row's edit command. */\nfunction rowIsReadOnly(descriptor: TableRowMenuDescriptor): boolean {\n return !descriptor.extraSections.some((section) => section.items.some((item) => \"id\" in item && item.id === \"table-edit-row\"));\n}\n\n/**\n * The canonical table registers ONE item source per mounted instance, under its table id. The table\n * answers which of its right-click LEVELS was aimed at (`MatrxTableMenuTarget`); the source turns\n * that answer into the declared `table_row` item, with the row's menu as its host data.\n */\nexport function registerTableRowContextResolver(\n tableId: string,\n resolve: (target: MatrxTableMenuTarget) => unknown,\n) {\n return itemSources.register<TableRowMenuDescriptor>(tableId, (target) => {\n const token = resolve({\n tableId,\n level: target.level as MatrxTableMenuLevel,\n ...(target.itemId ? { rowId: target.itemId } : {}),\n ...(target.partId ? { columnId: target.partId } : {}),\n });\n if (!(token instanceof TableRowMenu)) return null;\n const descriptor = token.descriptor;\n const rowId = target.itemId ?? \"\";\n return {\n item: {\n itemType: TABLE_ROW_ITEM,\n identity: { table_id: tableId, row_id: rowId },\n level: target.level,\n readOnly: rowIsReadOnly(descriptor),\n readValues: async () => ({\n table_id: tableId,\n row_id: rowId,\n shown: shownWords(rowElement(tableId, rowId)),\n row: descriptor.context.context,\n }),\n },\n host: descriptor,\n };\n });\n}\n\n/** The clicked row as its declared item (raw-free) and the menu its table built for it. */\nexport interface TableRowItemHit {\n item: ResolvedItem;\n menu: TableRowMenuDescriptor;\n}\n\nexport function resolveTableRowItem(target: HTMLElement | null): TableRowItemHit | null {\n const row = target?.closest<HTMLElement>(\"[data-row-id]\");\n const tableId = row?.closest<HTMLElement>(\"[data-matrx-table-id]\")?.dataset.matrxTableId;\n const rowId = row?.dataset.rowId;\n if (!tableId || !rowId) return null;\n // This menu acts on the ROW it found in the DOM, so it asks for that level by name.\n const hit = itemSources.resolve<TableRowMenuDescriptor>({ containerId: tableId, level: \"row\", itemId: rowId });\n const descriptor = hit?.host ?? null;\n if (!hit || !descriptor) return null;\n const { readValues: _raw, ...item } = hit.item;\n return { item, menu: headedMenu(descriptor, target, row) };\n}\n\n/** The row's menu, or null when the click was not on a canonical table's row. */\nexport function resolveTableRowMenuDescriptor(target: HTMLElement | null): TableRowMenuDescriptor | null {\n return resolveTableRowItem(target)?.menu ?? null;\n}\n\nfunction headedMenu(\n descriptor: TableRowMenuDescriptor,\n target: HTMLElement | null,\n row: HTMLElement,\n): TableRowMenuDescriptor {\n // 🚨 THE HEADING IS WHAT THE PERSON RIGHT-CLICKED, IN THE WORDS ON SCREEN (merged-grid review\n // 2026-09-26): the heading read `Content: { \"id\": …, \"_choices\": …, \"level\": \"admin\",\n // \"hidden\": {} }` — the row's raw document. The clicked cell's shown text (\"Content: Priya\n // Nair\") is the heading; the row's words when the click was between cells. The full row stays\n // in `context.context` for the actions that read it.\n const cell = target?.closest<HTMLElement>(\"td, [role='gridcell']\");\n const shown = shownWords(cell ?? row);\n // THE HEADER NAMES THE ROW'S RECORD (admin pass 2026-09-27: \"Content: partial\" — the clicked\n // status cell). The descriptor's own `__heading` wins (a list row names \"Quiz: …\"); otherwise\n // the row's record name — its first data cell's primary line. What the actions act on\n // (`content`) stays the clicked cell's words.\n const ownHeading = (descriptor.context as Record<string, unknown>)[CONTEXT_MENU_HEADING_KEY];\n const recordName = ownHeading ? \"\" : rowRecordName(row);\n const context = {\n ...descriptor.context,\n ...(shown ? { content: shown } : {}),\n ...(recordName ? { [CONTEXT_MENU_HEADING_KEY]: { label: \"Row\", text: recordName } } : {}),\n };\n // THE ROW IS NAMED ONCE (admin final judge 2026-09-27: \"Row: Basic Editor\" then a \"Basic\n // Editor\" section heading right under it). A primary section labelled with the same name the\n // header already shows is headed \"Row\" instead — its rows stay first (an unnamed section\n // would lose its place; the package never draws a coined or empty heading).\n const headed = (ownHeading as { text?: unknown } | undefined)?.text ?? recordName;\n const same = (label: string | undefined) =>\n typeof headed === \"string\" && !!label && label.trim().toLowerCase() === headed.trim().toLowerCase();\n const extraSections = descriptor.extraSections.map((section) =>\n section.primary && same(section.label) ? { ...section, label: \"Row\" } : section,\n );\n const changedSections = extraSections.some((section, i) => section !== descriptor.extraSections[i]);\n return shown || recordName || changedSections ? { ...descriptor, context, extraSections } : descriptor;\n}\n\n/** The row's record name: the first data cell (not a tick box, star or button cell) with words. */\nexport function rowRecordName(row: HTMLElement | null | undefined): string {\n if (!row) return \"\";\n for (const cell of Array.from(row.querySelectorAll<HTMLElement>(\"td[data-matrx-table-column-id], [role='gridcell'][data-matrx-table-column-id]\"))) {\n const words = primaryLine(cell);\n if (words) return words.length > 120 ? `${words.slice(0, 119)}…` : words;\n }\n return \"\";\n}\n\n/** Elements that start a new LINE of what a cell shows (name, then its subtitle). */\nconst LINE_BREAKING = \"div, p, li, br, tr, h1, h2, h3, h4, h5, h6, section, article, header, footer, dd, dt\";\n\n/**\n * The PRIMARY line an element shows — its first line, without its controls.\n * `textContent` glues a cell's stacked lines together with no space\n * (\"Data Destruction, Inc.Company re…\", \"Notesmatrx-user/notes\"; page-pass\n * 2026-09-27), so each line-starting element is marked with a newline first and\n * only the first non-empty line is kept. Inline markup inside a line\n * (<b>, <a>, <span>) is untouched, so words are never split.\n */\nfunction primaryLine(element: Element): string {\n const copy = element.cloneNode(true) as HTMLElement;\n copy.querySelectorAll(\"button, [aria-hidden='true'], input, textarea, select\").forEach((node) => node.remove());\n copy.querySelectorAll(LINE_BREAKING).forEach((node) => node.before(\"\\n\"));\n const lines = (copy.textContent ?? \"\")\n .split(\"\\n\")\n .map((line) => line.replace(/\\s+/g, \" \").trim())\n .filter((line) => line && line !== \"—\");\n return lines[0] ?? \"\";\n}\n\n/** The words an element shows: one cell's primary line, or each cell's, joined with \" · \". */\nfunction shownWords(element: HTMLElement | null | undefined): string {\n if (!element) return \"\";\n const cells = element.querySelectorAll(\"td, [role='gridcell']\");\n const text = cells.length > 1\n ? Array.from(cells).map(primaryLine).filter(Boolean).join(\" · \")\n : primaryLine(element);\n return text.length > 200 ? `${text.slice(0, 199)}…` : text;\n}\n","/**\n * A RECORD'S ROWS, IN THE ONE MENU OF THE CONTENT THAT SHOWS IT (R26; ALC-15 round 5).\n *\n * A record's toolbar can live apart from the content it belongs to: on /notes the tab strip holds the\n * note's ⋯ (Save, Duplicate, Move, Delete, Close tab…) while the note's content is drawn in the pane\n * below. The ⋯ and a right-click on that content target the SAME thing — the note — so they must be\n * one menu (measured 2026-09-27: 27 rows from the tab's ⋯, 25 from the content, each missing the\n * other's rows).\n *\n * · The owner of the rows registers them under a key: `registerRecordMenu(key, get)`.\n * · The host marks the content's root with `data-record-menu=\"<key>\"`.\n * · Every menu opened inside that root carries the registered sections (and the record's entity,\n * when the menu has none of its own) — the shell reads them at open (ContextMenuV3).\n * · The record's ⋯ calls `openRecordMenu(key, button)`: it opens the content's OWN menu at the\n * button. `false` = that content is not on screen; the caller opens its own menu instead.\n *\n * Plain data, no React.\n */\nimport type { ContextMenuEntityRef, ContextMenuExtraSection } from \"./types\";\n\nexport const RECORD_MENU_ATTR = \"data-record-menu\";\n\nexport interface RecordMenuRows {\n entity?: ContextMenuEntityRef | null | undefined;\n extraSections: ContextMenuExtraSection[];\n /**\n * The record's own name for the menu header (\"Note · Clinic intake\n * checklist\") — used when the clicked content names nothing itself, so the\n * header names the record instead of quoting its body. A selection still wins.\n */\n heading?: { label: string; text: string } | null;\n}\n\nconst registry = new Map<string, () => RecordMenuRows>();\n\n/**\n * A record's rows are pulled at open, but its NAME can change while a menu is\n * open or mounted (the note auto-labels from its first words 3 s after typing;\n * a person renames it). The owner calls `recordMenuChanged()` when what it\n * registers changes, and an open menu re-reads the heading (G6B review,\n * 2026-10-02: \"Note: New Note\" after the note was renamed).\n */\nlet revision = 0;\nconst listeners = new Set<() => void>();\n\n/** The owner of registered rows says they changed (its record was renamed). */\nexport function recordMenuChanged(): void {\n revision += 1;\n for (const listener of listeners) listener();\n}\n\n/** For `useSyncExternalStore`: re-render when any registered record changes. */\nexport function subscribeRecordMenus(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nexport function recordMenusRevision(): number {\n return revision;\n}\n\n/** Register the rows a record contributes; returns the unregister. Read at every open, so always current. */\nexport function registerRecordMenu(key: string, get: () => RecordMenuRows): () => void {\n registry.set(key, get);\n return () => {\n if (registry.get(key) === get) registry.delete(key);\n };\n}\n\n/** The rows of the record whose content holds `target` (nearest marked root), or null. */\nexport function resolveRecordMenu(target: Element | null): RecordMenuRows | null {\n const key = target?.closest?.(`[${RECORD_MENU_ATTR}]`)?.getAttribute(RECORD_MENU_ATTR);\n if (!key) return null;\n return registry.get(key)?.() ?? null;\n}\n\nconst TRIGGER = '[data-alchemy-trigger=\"context\"]';\n\n/**\n * The right-click trigger a click on the record's content lands in: the first menu trigger inside\n * the marked root that carries content (`data-content-source`), else the root's own trigger.\n */\nfunction contentTrigger(root: HTMLElement): HTMLElement | null {\n const inner = root.querySelector<HTMLElement>(`${TRIGGER}[data-content-source]`);\n if (inner) return inner;\n if (root.matches(TRIGGER)) return root;\n return root.querySelector<HTMLElement>(TRIGGER) ?? root.closest<HTMLElement>(TRIGGER);\n}\n\n/**\n * Open the record content's own menu at `anchor` (a ⋯ button). Returns false when no content for\n * `key` is on screen, so the caller can open its own menu instead.\n */\nexport function openRecordMenu(key: string, anchor: HTMLElement): boolean {\n const doc = anchor.ownerDocument;\n const root = [...doc.querySelectorAll<HTMLElement>(`[${RECORD_MENU_ATTR}]`)].find(\n (el) => el.getAttribute(RECORD_MENU_ATTR) === key,\n );\n const trigger = root ? contentTrigger(root) : null;\n if (!trigger) return false;\n const rect = anchor.getBoundingClientRect();\n trigger.dispatchEvent(\n new MouseEvent(\"contextmenu\", {\n bubbles: true,\n cancelable: true,\n view: doc.defaultView ?? window,\n button: 2,\n buttons: 2,\n clientX: rect.left + rect.width / 2,\n clientY: rect.bottom,\n }),\n );\n return true;\n}\n","// features/context-menu-v3/utils/per-row-entity.ts\n//\n// THE PER-ROW ENTITY — the delegated-menu half of the entity contract.\n//\n// A table wires ONE menu for the whole pane (`resolveContextOnOpen`), so the\n// menu-level `entity` prop can never name the row that was actually\n// right-clicked: before 2026-08-24 Attach To / Share on a table menu silently\n// targeted the pane's entity, or rendered nothing at all. `resolveContextOnOpen`\n// may now return the reserved `CONTEXT_MENU_ENTITY_KEY` and the shell rebuilds\n// the entity-bound actions from it.\n//\n// Kept in its own module (not `value-resolution.ts`) on purpose: the INERT\n// SHELL imports this, and value-resolution pulls the surface-manifest registry.\n// Nothing here may import anything heavier than types.\n\nimport {\n CONTEXT_MENU_ENTITY_KEY,\n type ContextMenuEntityRef,\n type ResolvedContextMenuContext,\n} from \"../types\";\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * The effective entity for ONE menu open.\n *\n * key absent → the menu-level prop stands (single-entity surfaces unchanged)\n * key present → that row's entity wins\n * key `null` → this target has no entity, so the entity-bound actions hide\n * rather than target the wrong record\n *\n * A malformed value (no string `type` + `id`) is treated as absent and SCREAMS\n * in dev — a half-built entity would render an Attach that writes a broken edge.\n */\nexport function resolveEffectiveEntity(\n entityProp: ContextMenuEntityRef | undefined,\n resolved: ResolvedContextMenuContext | null,\n): ContextMenuEntityRef | undefined {\n if (!resolved || !(CONTEXT_MENU_ENTITY_KEY in resolved)) return entityProp;\n const perRow = resolved[CONTEXT_MENU_ENTITY_KEY];\n if (perRow === null || perRow === undefined) return undefined;\n const candidate = perRow as ContextMenuEntityRef;\n if (\n typeof perRow !== \"object\" ||\n typeof candidate.type !== \"string\" ||\n !candidate.type ||\n typeof candidate.id !== \"string\" ||\n !candidate.id\n ) {\n if (isDev) {\n console.error(\n `%c[ContextMenuV3] MALFORMED PER-ROW ENTITY%c — resolveContextOnOpen returned \"${CONTEXT_MENU_ENTITY_KEY}\" without a string type + id, so Attach To / Share would target nothing. Falling back to the menu-level entity.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { received: perRow },\n );\n }\n return entityProp;\n }\n return candidate;\n}\n\n/**\n * The effective `contextData` for one open: static payload + per-target merge,\n * with the reserved entity key stripped so it never lands in the\n * `ApplicationScope` as a value.\n */\nexport function mergeResolvedContextData(\n contextData: Record<string, unknown> | undefined,\n resolved: ResolvedContextMenuContext | null,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = {\n ...(contextData ?? {}),\n ...(resolved ?? {}),\n };\n delete merged[CONTEXT_MENU_ENTITY_KEY];\n return merged;\n}\n\n// ---------------------------------------------------------------------------\n// A TABLE ROW HAS TWO OWNERS — the table and the surface that mounted it\n// ---------------------------------------------------------------------------\n\n/**\n * The context for ONE open on a table row: the table's own row descriptor\n * (table-row-item.ts) JOINED with the surface's `resolveContextOnOpen` answer.\n *\n * 🚨 WHY (2026-10-03, CHAIR-REACH). The shell used to read `rowMenu?.context ?? surface(target)`:\n * once a canonical table registered its default row descriptor — which every MatrxDataTable does,\n * with `__entity: null` — the surface's resolver was never called on a row. So a list that names\n * its row's record there (CRM `useCrmRowMenu`, the keyword tables) lost Attach To, Share and\n * \"Link a record…\" on every row, and its own row doors with them (they are built from the state\n * that resolver sets).\n *\n * · The row descriptor still owns the row's VALUES (content, heading, the full row): on a key\n * both name, the descriptor stands (the 2026-09-26/27 heading rulings are unchanged).\n * · The ENTITY is the surface's to name when the descriptor names none: the table knows a row,\n * only the surface knows which record that row is. A descriptor that names its own entity\n * keeps it.\n * · Anything only the surface says is kept.\n */\nexport function joinRowAndSurfaceContext(\n row: ResolvedContextMenuContext | null | undefined,\n surface: ResolvedContextMenuContext | null | undefined,\n): ResolvedContextMenuContext | null {\n if (!row) return surface ?? null;\n if (!surface) return row;\n const rowNamesEntity = row[CONTEXT_MENU_ENTITY_KEY] !== null && row[CONTEXT_MENU_ENTITY_KEY] !== undefined;\n return {\n ...surface,\n ...row,\n ...(!rowNamesEntity && CONTEXT_MENU_ENTITY_KEY in surface\n ? { [CONTEXT_MENU_ENTITY_KEY]: surface[CONTEXT_MENU_ENTITY_KEY] }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// THE DOM SNIFFER — per-row identity with no resolver code\n// ---------------------------------------------------------------------------\n\n/**\n * A token SHAPE check, deliberately not a registry lookup.\n *\n * `isEntityTypeToken` would be the precise guard, but it lives in the 2,500-line\n * generated entity module and this file loads in the INERT SHELL (see the header\n * law). A shape check catches the realistic failure — a typo, a label, a raw\n * uuid in the wrong attribute — at zero weight; a well-formed but unregistered\n * token is caught downstream by Attach To's own write path.\n */\nconst TOKEN_SHAPE = /^[a-z][a-z0-9_]*$/;\n\n/**\n * Read the right-clicked row's entity straight off the DOM.\n *\n * 🚨 WHY THIS EXISTS (Phase 0, 2026-08-25). `resolveContextOnOpen` is the\n * precise path and remains it — but it made per-row identity cost a\n * hand-written resolver on EVERY table. With ~500 surfaces still to wire that\n * is ~500 bespoke `rowFor(target)` functions, each one a place to return the\n * wrong row. A row that already knows what it is can now simply SAY so:\n *\n * <tr data-entity-type=\"seo_keyword\" data-entity-id={k.id} data-entity-title={k.phrase}>\n *\n * and Attach To / Share target that record with no resolver at all.\n *\n * PRECEDENCE — THE SURFACE ALWAYS WINS. This runs ONLY when\n * `resolveContextOnOpen` did not speak about the entity at all (key absent).\n * An explicit `null` from the surface means \"this target has no entity\" and is\n * honoured; the DOM never overrides a deliberate answer.\n */\nexport function sniffEntityFromDom(\n target: HTMLElement | null,\n): ContextMenuEntityRef | null {\n const host = target?.closest?.(\"[data-entity-type][data-entity-id]\");\n if (!(host instanceof HTMLElement)) return null;\n\n const type = host.getAttribute(\"data-entity-type\")?.trim() ?? \"\";\n const id = host.getAttribute(\"data-entity-id\")?.trim() ?? \"\";\n if (!type || !id) return null;\n\n if (!TOKEN_SHAPE.test(type)) {\n if (isDev) {\n console.error(\n `%c[ContextMenuV3] MALFORMED data-entity-type%c — \"${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.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { element: host },\n );\n }\n return null;\n }\n\n // Title is what the user sees the row called; the row's own text is the\n // honest fallback so an Attach is never labelled with a raw uuid.\n const title =\n host.getAttribute(\"data-entity-title\")?.trim() ||\n (host.textContent ?? \"\").trim().slice(0, 120) ||\n id;\n\n const resourceType = host.getAttribute(\"data-entity-resource\")?.trim();\n\n return {\n type: type as ContextMenuEntityRef[\"type\"],\n id,\n title,\n ...(resourceType\n ? { resourceType: resourceType as NonNullable<ContextMenuEntityRef[\"resourceType\"]> }\n : {}),\n };\n}\n\n/**\n * The effective entity for one open, DOM sniff included. Surface answer wins;\n * the sniff only fills a silence.\n */\nexport function resolveEffectiveEntityWithDom(\n entityProp: ContextMenuEntityRef | undefined,\n resolved: ResolvedContextMenuContext | null,\n target: HTMLElement | null,\n): ContextMenuEntityRef | undefined {\n const surfaceSpoke = !!resolved && CONTEXT_MENU_ENTITY_KEY in resolved;\n if (surfaceSpoke) return resolveEffectiveEntity(entityProp, resolved);\n return sniffEntityFromDom(target) ?? entityProp;\n}\n","/**\n * ONE MENU, SEVERAL SOURCES OF SURFACE ROWS — joined so every row id is unique.\n *\n * A menu's surface sections can come from more than one owner for one open: a table row's\n * descriptor plus the grid's per-target sections, or the content's own sections plus the RECORD\n * it belongs to (record-menu-registry.ts — a note's tab rows join the menu opened on the note's\n * content). Each owner keeps its own ids unique, but not across owners, and the menu model\n * namespaces every surface row as `x:<id>` (menu-model.ts) — so the same id from two owners\n * reached the one Alchemy registry twice and it refused the second:\n * `DuplicateActionError: Action \"cm:x:save\" is yielded twice by provider \"context-menu:…\"`\n * (/notes, 2026-09-30: the editor's \"Save\" and the tab's \"Save\", both saving the same note).\n *\n * Sources are passed in precedence order. Two owners describing one target with the same id\n * mean the same action on it, so the FIRST source's row stands and a later source's row with\n * that id is not drawn a second time. A duplicate inside ONE source is that source's own defect\n * and is left untouched, so the registry still refuses it loudly.\n */\nimport type { ContextMenuExtraItem, ContextMenuExtraSection } from \"../types\";\n\nfunction isRow(item: ContextMenuExtraItem): boolean {\n return item.kind !== \"separator\";\n}\n\n/** Drop leading, trailing and doubled separators left behind when rows are removed. */\nfunction tidySeparators(items: ContextMenuExtraItem[]): ContextMenuExtraItem[] {\n const out: ContextMenuExtraItem[] = [];\n for (const item of items) {\n if (!isRow(item) && (out.length === 0 || !isRow(out[out.length - 1]!))) continue;\n out.push(item);\n }\n while (out.length > 0 && !isRow(out[out.length - 1]!)) out.pop();\n return out;\n}\n\nexport function joinExtraSections(\n ...sources: Array<ContextMenuExtraSection[] | null | undefined>\n): ContextMenuExtraSection[] | undefined {\n const present = sources.filter((s): s is ContextMenuExtraSection[] => Array.isArray(s));\n if (present.length === 0) return undefined;\n if (present.length === 1) return present[0];\n const taken = new Set<string>();\n const out: ContextMenuExtraSection[] = [];\n for (const source of present) {\n const ownIds = new Set<string>();\n for (const section of source) {\n const kept = section.items.filter((item) => !isRow(item) || !taken.has(item.id));\n for (const item of kept) if (isRow(item)) ownIds.add(item.id);\n if (kept.length === section.items.length) {\n out.push(section);\n continue;\n }\n const items = tidySeparators(kept);\n if (items.some(isRow)) out.push({ ...section, items });\n }\n for (const id of ownIds) taken.add(id);\n }\n return out;\n}\n","// features/context-menu-v3/utils/widget-handle.ts\n//\n// Inline agent editing — the v1 promise, paid via the platform channel.\n//\n// Builds a `WidgetHandle` (the canonical client-tool contract — see\n// `features/agents/types/widget-handle.types.ts` and CLIENT_SIDE_TOOLS.md)\n// from the SAME props an editable surface already passes to\n// `EditableContextMenu` (`onTextReplace` / `onTextInsertBefore|After` /\n// `getTextarea`). The shell registers it via `useOptionalWidgetHandle` and the\n// launch handlers pass the id as `runtime.widgetHandleId`, so any agent\n// launched from the menu on an editable surface can stream `widget_text_*`\n// tool calls that edit the surface's content in place — zero extra wiring per\n// surface, no bespoke edit protocol (the old XML-in-stream idea stays dead).\n//\n// Precedence: a surface callback (controlled React state) always wins; the\n// fallback writes through `setFieldValue` (native setter + input event), which\n// works on BOTH controlled and uncontrolled textareas. Only methods this\n// surface can actually service are present — the per-turn assembler\n// (`deriveClientToolsFromHandle`) advertises exactly that subset.\n\nimport type { WidgetHandle } from \"@ai-matrx/chat/agents/types/widget-handle.types\";\nimport type { ApplicationScope } from \"@ai-matrx/chat/agents/types/scope.types\";\n\n/**\n * Write a value into a textarea so BOTH controlled and uncontrolled fields\n * take it: the native value setter bypasses React's value tracker, and the\n * bubbled `input` event makes a controlled component's onChange fire (state\n * updates instead of React reverting the DOM on the next render). A bare\n * `element.value = x` write silently loses the edit on controlled fields —\n * while the tool result still reports ok:true to the agent.\n */\nfunction setFieldValue(\n el: HTMLTextAreaElement,\n next: string,\n caret: number,\n): void {\n const setter = Object.getOwnPropertyDescriptor(\n HTMLTextAreaElement.prototype,\n \"value\",\n )?.set;\n if (setter) setter.call(el, next);\n else el.value = next;\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n try {\n el.setSelectionRange(caret, caret);\n } catch {\n // non-focusable / detached — caret is best-effort\n }\n}\n\nexport interface BuildEditableWidgetHandleArgs {\n getTextarea?: (() => HTMLTextAreaElement | null) | undefined;\n onTextReplace?: ((newText: string) => void) | undefined;\n onTextInsertBefore?: ((text: string) => void) | undefined;\n onTextInsertAfter?: ((text: string) => void) | undefined;\n /** Live scope builder — read for `content` when there is no textarea. */\n getApplicationScope?: (() => ApplicationScope) | undefined;\n}\n\n/**\n * Build the widget handle for an editable surface, or `null` when the surface\n * exposes no way to write (nothing to register, no tools advertised).\n */\nexport function buildEditableWidgetHandle(\n args: BuildEditableWidgetHandleArgs,\n): WidgetHandle | null {\n const {\n getTextarea,\n onTextReplace,\n onTextInsertBefore,\n onTextInsertAfter,\n getApplicationScope,\n } = args;\n\n const field = (): HTMLTextAreaElement | null => getTextarea?.() ?? null;\n\n const readCurrent = (): string | null => {\n const el = field();\n if (el) return el.value;\n const content = getApplicationScope?.().content;\n return typeof content === \"string\" ? content : null;\n };\n\n // Full-content write. Surface callback wins; the field fallback goes\n // through setFieldValue so controlled AND uncontrolled textareas take it.\n const writeFull = (next: string): void => {\n if (onTextReplace) {\n onTextReplace(next);\n return;\n }\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n setFieldValue(el, next, next.length);\n };\n\n // Capability PRESENCE only — this function runs in the shell's render body,\n // so it must never invoke a surface callback (getTextarea reads the DOM,\n // getApplicationScope may serialize the whole document; per-keystroke cost\n // + a React render-purity violation). The method bodies re-check liveness\n // (readCurrent() null checks) at call time, which is the only time it matters.\n const canWrite = Boolean(onTextReplace) || Boolean(getTextarea);\n const canRead = Boolean(getTextarea) || Boolean(getApplicationScope);\n if (!canWrite) return null;\n\n // No `applyPolicy`: an agent's write into the person's own text is `ask` —\n // the approval card shows `readText()` as the \"before\".\n const handle: WidgetHandle = {\n onTextReplace: ({ text }) => writeFull(text),\n ...(canRead ? { readText: readCurrent } : {}),\n };\n\n // Insert relative to the cursor/selection. Surface callbacks carry their\n // own semantics; the fallback splices at the field's live cursor.\n if (onTextInsertBefore || getTextarea) {\n handle.onTextInsertBefore = ({ text }) => {\n if (onTextInsertBefore) return onTextInsertBefore(text);\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n const at = el.selectionStart ?? 0;\n setFieldValue(\n el,\n el.value.slice(0, at) + text + el.value.slice(at),\n at + text.length,\n );\n };\n }\n if (onTextInsertAfter || getTextarea) {\n handle.onTextInsertAfter = ({ text }) => {\n if (onTextInsertAfter) return onTextInsertAfter(text);\n const el = field();\n if (!el) throw new Error(\"Surface has no writable target\");\n const at = el.selectionEnd ?? el.value.length;\n setFieldValue(\n el,\n el.value.slice(0, at) + text + el.value.slice(at),\n at + text.length,\n );\n };\n }\n\n // Whole-content operations need read + write.\n if (canRead) {\n handle.onTextPrepend = ({ text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n writeFull(text + current);\n };\n handle.onTextAppend = ({ text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n writeFull(current + text);\n };\n handle.onTextPatch = ({ search_text, replacement_text }) => {\n const current = readCurrent();\n if (current === null) throw new Error(\"Surface content is unreadable\");\n const idx = current.indexOf(search_text);\n if (idx === -1)\n // access-errors: ok — in-memory text-patch match against the surface's own content, no record read\n throw new Error(\"search_text not found in surface content\");\n writeFull(\n current.slice(0, idx) +\n replacement_text +\n current.slice(idx + search_text.length),\n );\n };\n }\n\n return handle;\n}\n","// features/context-menu-v3/utils/table-at-target.ts\n//\n// THE TABLE A RIGHT-CLICK LANDED ON, with its shape kept (header + rows). A chat answer's menu is\n// one menu over the whole answer, so \"Save to a table\" read the answer's text and opened on its\n// FIRST shape — a bullet list above the table won, and the table the person right-clicked was\n// never offered first. The shell hands the rich-document registry this reader (callbacks\n// `tableAtTarget`); the one shape reader (`shapeTextOfNode`, records-ui) turns the clicked\n// <table> back into table text, streamed or finished — it reads the rendered DOM.\n\n// The shape reader is imported only when a right-click lands on a <table> (the shell calls\n// `preloadTableShape` from its open path), never with the page: a menu is opened on ~1 in 149\n// page loads and most opens are not on a table.\ntype ShapeReader = (node: Node) => string;\nlet shapeReader: ShapeReader | null = null;\nlet shapeLoad: Promise<unknown> | null = null;\n\nexport function preloadTableShape(): Promise<unknown> {\n shapeLoad ??= import(\"@ai-matrx/records-ui/table-shape\")\n .then((m) => {\n shapeReader = m.shapeTextOfNode as ShapeReader;\n })\n .catch((error: unknown) => {\n shapeLoad = null;\n console.error(\"[ContextMenuV3] could not load the table shape reader\", error);\n });\n return shapeLoad;\n}\n\n/** Sync; null until the reader has loaded (a click on a table starts the load at open). */\nexport function tableTextAtTarget(target: Element | null): string | null {\n const table = target?.closest(\"table\");\n if (!table || !shapeReader) return null;\n const text = shapeReader(table.cloneNode(true));\n return text.trim() ? text : null;\n}\n","/**\n * The attributes ContextMenuV3 stamps onto every right-click REGION it wraps\n * (the chat composer textarea, every sidebar chat row, messages, table rows).\n *\n * ONE source: `ContextMenuV3.tsx` spreads this onto its trigger, and the real-\n * browser layout gate (`features/shell/layout-gate/shipped-css-region-triggers.spec.ts`)\n * renders its fixtures with the same object — so a stylesheet the app ships\n * that styles these attributes (the 2026-09-26 `[data-alchemy-trigger] {\n * width: 2rem }` collapse) is measured against exactly what production emits.\n *\n * Plain data, no React: the Playwright spec imports it directly.\n */\nexport const CONTEXT_REGION_TRIGGER_ATTRS = {\n \"data-alchemy-trigger\": \"context\",\n} as const;\n","/**\n * features/context-menu-v3/item/itemMenuToV3.ts\n *\n * ItemMenuConfig → v3 `extraSections` converter — the bridge that lets\n * `ItemContextMenu` render the ONE universal context menu instead of its own\n * Radix tree. Command/toggle execution reuses `run-entry.ts` (toast.promise\n * parity with the kebab dropdown), checkbox/link map to the dedicated v3 item\n * kinds, and section boundaries/labels carry over. Deliberate deltas from the\n * old bespoke render (documented in FEATURE.md): no in-menu single-key\n * shortcut execution (hints still display) and the header renders as a\n * leading section label instead of a Radix label block.\n */\n\nimport { compact } from \"../utils/compact\";\nimport type {\n ContextMenuExtraItem,\n ContextMenuExtraSection,\n} from \"../types\";\nimport {\n isCheckbox,\n isLink,\n isSubmenu,\n runCommand as runItemCommand,\n runToggle,\n type ItemMenuCommand,\n type ItemMenuConfig,\n type ItemMenuEntry,\n type ItemMenuSection,\n} from \"@ai-matrx/design-system/item\";\nimport { toast } from \"@ai-matrx/chat/host/notify\";\n\n// design-system's item icon is any component taking a className; the v3 menu's icon slot takes the same.\nconst iconOf = (entry: ItemMenuEntry) => entry.icon;\n\nconst toastPromise = (promise: Promise<unknown>, messages: { loading: string; success: string; error: (e: unknown) => string }) => {\n toast.promise(promise, messages);\n};\n\nfunction runCommand(entry: ItemMenuCommand) {\n runItemCommand(entry, toastPromise);\n}\n\nexport { toastPromise as itemToastPromise };\n\nfunction entryToExtraItem(entry: ItemMenuEntry): ContextMenuExtraItem {\n const description =\n entry.disabled && entry.disabledReason\n ? entry.disabledReason\n : entry.description;\n\n if (isSubmenu(entry)) {\n return compact({\n kind: \"submenu\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n disabled: entry.disabled,\n children: submenuChildren(entry.sections),\n }) as ContextMenuExtraItem;\n }\n if (isCheckbox(entry)) {\n return compact({\n kind: \"checkbox\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n checked: entry.checked,\n onCheckedChange: (next: boolean) => runToggle(entry, next),\n disabled: entry.disabled,\n hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),\n }) as ContextMenuExtraItem;\n }\n if (isLink(entry)) {\n return compact({\n kind: \"link\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n href: entry.href,\n target: entry.target,\n disabled: entry.disabled,\n }) as ContextMenuExtraItem;\n }\n // command\n return compact({\n kind: \"item\" as const,\n id: entry.id,\n label: entry.label,\n icon: iconOf(entry),\n description,\n destructive: entry.tone === \"destructive\",\n disabled: entry.disabled,\n hint: entry.shortcut ?? entry.shortcutKey?.toUpperCase(),\n onSelect: () => runCommand(entry),\n }) as ContextMenuExtraItem;\n}\n\n/** Flatten submenu sections into children with separators at boundaries. */\nfunction submenuChildren(sections: ItemMenuSection[]): ContextMenuExtraItem[] {\n const visible = sections\n .map((s) => ({ ...s, items: s.items.filter((e) => !e.hidden) }))\n .filter((s) => s.items.length > 0);\n const out: ContextMenuExtraItem[] = [];\n visible.forEach((section, i) => {\n if (i > 0)\n out.push({\n kind: \"separator\",\n id: `sep-${section.id ?? section.label ?? i}`,\n });\n out.push(...section.items.map(entryToExtraItem));\n });\n return out;\n}\n\nexport function itemMenuConfigToExtraSections(\n config: ItemMenuConfig,\n): ContextMenuExtraSection[] {\n const sections: ContextMenuExtraSection[] = config.sections\n .map((s, i) => compact({\n id: String(s.id ?? s.label ?? `im-${i}`),\n label: s.label,\n anchor: \"after-clipboard\" as const,\n items: s.items.filter((e) => !e.hidden).map(entryToExtraItem),\n }))\n .filter((s) => s.items.length > 0);\n\n // Header → leading label on the first section (or its own labeled block).\n const title = config.header?.title;\n const first = sections[0];\n if (title && first && !first.label) {\n sections[0] = { ...first, label: title };\n }\n return sections;\n}\n","// features/context-menu-v3/selection-provider.ts\n//\n// The context menu's one entry in the ONE selection toolbar\n// (components/selection-toolbar): \"Ask AI\" — it opens the same Alchemy menu a\n// right-click opens, over the selected text (the AI actions, the agent\n// libraries, copy / export / save), on desktop and as the sheet on a phone.\n// It replaces the old floating selection icon: the menu has one door on a\n// selection, and it sits in the toolbar with every other passage action.\n\nimport { AGENT_ICON } from \"@ai-matrx/icons/domain\";\nimport type { Action, ActionProvider, ClickTarget } from \"@ai-matrx/alchemy/actions\";\nimport { registerAlchemyIcon } from \"@ai-matrx/rich-content/utils/alchemy-icon-keys\";\nimport {\n declareSelectionProvider,\n placeSelectionActions,\n hostHalf,\n selectionToolbarHostOf,\n shownInSelectionMode,\n} from \"@ai-matrx/rich-content/selection-toolbar/selection-actions\";\n\nexport const CONTEXT_MENU_SELECTION_HOST_KEY = \"contextMenuSelection\";\n\nexport interface ContextMenuSelectionHost {\n kind: \"context-menu-selection\";\n /** Open this menu instance over the current selection. */\n open(): void;\n}\n\nfunction menuOf(target: ClickTarget): ContextMenuSelectionHost | null {\n const half = hostHalf<ContextMenuSelectionHost>(target, CONTEXT_MENU_SELECTION_HOST_KEY);\n return half?.kind === \"context-menu-selection\" ? half : null;\n}\n\nconst ASK_AI: Action = {\n id: \"selection:ai\",\n label: \"Ask AI\",\n description: \"AI actions, agents, copy and export for the selected text\",\n icon: registerAlchemyIcon(AGENT_ICON),\n category: \"ai\",\n order: 0,\n placement: \"primary\",\n preserveSelection: true,\n eligible: (t) => (menuOf(t) && shownInSelectionMode(ASK_AI.id, t) ? { status: \"available\" } : { status: \"absent\" }),\n run: (t) => {\n const menu = menuOf(t);\n if (!menu) return;\n // The toolbar steps aside; the selection stays for the menu to act on.\n selectionToolbarHostOf(t)?.ui.close();\n menu.open();\n },\n};\n\nexport const contextMenuSelectionProvider: ActionProvider = {\n id: \"context-menu-selection\",\n tier: \"T0\",\n declaredIds: () => [ASK_AI.id],\n actions: (target) => (menuOf(target) ? placeSelectionActions([ASK_AI], target) : []),\n};\n\ndeclareSelectionProvider(contextMenuSelectionProvider);\n"]}
|