@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-IH5WN5JP.cjs","../src/engine/utils/editor-text-agent.ts","../src/engine/utils/editor-text-insertion.ts","../src/engine/utils/insert-into-editor.ts","../src/engine/hooks/useUnifiedAgentContextMenu.ts","../src/engine/value-resolution.ts","../src/engine/utils/selection-write-back.ts","../src/engine/hooks/useContextMenuActions.ts","../src/engine/utils/notify.ts","../src/engine/utils/ensure-block-template.ts","../src/engine/model/menu-model.ts","../src/engine/alchemy-provider.ts"],"names":["placementGroupKey","icon","AGENT_ICON","Braces","record"],"mappings":"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A;AACA;AACA;AACA;AACA;AACA;AACA;AACK;AAGE;AACA;AACA;AACA;AACA;AACA;AP+jBC;AACA;AQrmBD;AASO;AACN,EAAA;AACF,EAAA;AACA,EAAA;AACA,IAAA;AACA,IAAA;AACN,EAAA;AACI,EAAA;AAAwD,EAAA;AACG,EAAA;AACA,EAAA;AAE1D,EAAA;AACP;AR+lBU;AACA;AS1nBD;AACA;AAIT;AAIU,EAAA;AAMJ,EAAA;AACE,EAAA;AACN,EAAA;AACO,EAAA;AACT;ATinBU;AACA;AOllBD;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAMT;AACE;AACA;AACA;AACK;AASE;AACA;AAEA;AAaA;AAWA;AAMI;AAET,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEW;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAGa;AACV,EAAA;AACA,EAAA;AACH;AAEa;AACV,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACH;AAEgB;AACN,EAAA;AACD,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACF,IAAA;AACE,MAAA;AACJ,EAAA;AACF;AAEgB;AAEZ,EAAA;AAKJ;AAEgB;AAIP,EAAA;AACT;AAEgB;AAGR,EAAA;AACN,EAAA;AAGOA,IAAAA;AACP,EAAA;AACO,EAAA;AACT;AAEgB;AACV,EAAA;AACG,EAAA;AACT;AAGgB;AAIR,EAAA;AAEA,EAAA;AACA,EAAA;AAEC,EAAA;AACT;AAsFgB;AAGR,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,EAAA;AAEE,EAAA;AACA,EAAA;AACA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A,IAAA;AACA,IAAA;AACF,EAAA;AACF;APyMU;AACA;AU97CD;AAGT;AACE;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;AACA;AACA;AACA;AACK;AAEE;AAET;AACE;AACA;AACA;AACK;AAkBE;AAcA;AACF,EAAA;AACC,EAAA;AACE,IAAA;AACF,IAAA;AACJ,IAAA;AACM,IAAA;AACN,IAAA;AACA,IAAA;AACF,EAAA;AACQ,EAAA;AACV;AAiLgB;AACP,EAAA;AACC,IAAA;AACA,IAAA;AACN,IAAA;AACD,EAAA;AACH;AAES;AACA,EAAA;AACT;AAKS;AACD,EAAA;AACE,EAAA;AACD,IAAA;AACH,MAAA;AACG,IAAA;AACH,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACG,IAAA;AACH,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACG,IAAA;AACH,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACG,IAAA;AACL,IAAA;AACE,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACJ,EAAA;AACF;AAES;AAGD,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AACF,IAAA;AACE,MAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACH,EAAA;AACO,EAAA;AACT;AAMgB;AACR,EAAA;AACF,EAAA;AACI,EAAA;A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,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACF;AVgmCU;AACA;AW3sED;AACA;AACA;AAeO;AACR,EAAA;AACD,EAAA;AACD,EAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEM;AACI,EAAA;AACD,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACV;AASM;AAGA;AACJ,EAAA;AACO,EAAA;AACP,EAAA;AACO,EAAA;AACP,EAAA;AACA,EAAA;AACI,EAAA;AACG,EAAA;AACP,EAAA;AACO,EAAA;AACP,EAAA;AACF;AAGM;AACJ,EAAA;AACA,EAAA;AACF;AAmBM;AAEG;AACP,EAAA;AACQ,IAAA;AACA,IAAA;AACJ,MAAA;AACI,MAAA;AACN,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAQM;AACA;AAEG;AACA,EAAA;AACT;AAGS;AACA,EAAA;AACT;AAGS;AACH,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACN,EAAA;AACI,EAAA;AACI,IAAA;AACN,IAAA;AACK,MAAA;AAAA;AAEH,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AAEA,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACE,QAAA;AAGA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AAIA,QAAA;AACA,QAAA;AACA,QAAA;AACE,UAAA;AACE,YAAA;AACA,YAAA;AACF,UAAA;AAGA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACI,EAAA;AACF,IAAA;AACK,MAAA;AACH,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACM,EAAA;AACC,EAAA;AACF,IAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACJ,IAAA;AACM,MAAA;AACA,MAAA;AACA,MAAA;AAEA,MAAA;AAIJ,MAAA;AACA,MAAA;AACF,IAAA;AACK,IAAA;AACP,EAAA;AACF;AAGS;AACH,EAAA;AACA,EAAA;AAEE,EAAA;AACC,EAAA;AACT;AAES;AACA,EAAA;AACT;AAMgB;AACR,EAAA;AACA,EAAA;AACE,IAAA;AACF,IAAA;AAEF,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACE,UAAA;AAAU,YAAA;AACR,YAAA;AACA,cAAA;AACY,cAAA;AACc,YAAA;AAC1B,YAAA;AACA,YAAA;AAEF,UAAA;AACA,UAAA;AACD,QAAA;AACF,MAAA;AACD,MAAA;AACF,IAAA;AAYM,IAAA;AAKQ,MAAA;AACJ,MAAA;AACI,MAAA;AAAuC;AAEvC,MAAA;AAEN,IAAA;AAGJ,IAAA;AACJ,IAAA;AACM,MAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AAGA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACI,MAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACD,IAAA;AACF,EAAA;AACM,EAAA;AACT;AAUgB;AACP,EAAA;AACD,IAAA;AACE,IAAA;AACN,IAAA;AACF,EAAA;AACF;AAGgB;AACR,EAAA;AAEI,IAAA;AAKA,EAAA;AACH,EAAA;AACT;AAQgB;AACV,EAAA;AACG,EAAA;AACT;AA2BgB;AAMN,EAAA;AACF,EAAA;AAIF,EAAA;AACIC,IAAAA;AACA,IAAA;AACN,IAAA;AACF,EAAA;AACM,EAAA;AACC,EAAA;AACT;AAOgB;AAQV,EAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AAIN,IAAA;AACF,EAAA;AACI,EAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAUgB;AACR,EAAA;AAKC,EAAA;AACT;AAQgB;AAIT,EAAA;AACE,EAAA;AACT;AAWgB;AAIP,EAAA;AACT;AX4/DU;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/aidream/aidream/apps/shared/context-menu/dist/chunk-IH5WN5JP.cjs","sourcesContent":[null,"// features/context-menu-v3/utils/editor-text-agent.ts\n//\n// ONE AI SET FOR A RECORD IN EVERY VIEW (page-pass /notes, 2026-09-28). Clean\n// up / Help with this… / Custom agent are visible only when the action context\n// carries `callbacks.onRequestTextAgentAction` (a host that can review and\n// apply). RichDocument (a note's Read view) supplied it; the editor menus\n// (Write, Plain, Split) did not, so the same note lost its AI rows on switching\n// view. Any menu now offers the shell's review dialog — only where the\n// source can be saved back (a record source with an `edit` adapter, not read\n// only), and never over a host that already supplied its own.\n\nimport type { ContentSource, ContentSourceAdapter, RichDocumentActionContext } from \"@ai-matrx/rich-content/rich-document/types\";\n\ntype Callbacks = RichDocumentActionContext[\"callbacks\"];\ntype Request = NonNullable<NonNullable<Callbacks>[\"onRequestTextAgentAction\"]>;\n\nexport function editorTextAgentCallbacks(\n callbacks: Callbacks,\n source: ContentSource,\n adapter: Pick<ContentSourceAdapter, \"edit\">,\n request: Request | undefined,\n): Callbacks {\n if (callbacks?.onRequestTextAgentAction || !request) return callbacks;\n if (source.type === \"raw\" || source.readOnly || typeof adapter.edit !== \"function\") return callbacks;\n return { ...(callbacks ?? {}), onRequestTextAgentAction: request };\n}\n","/**\n * Inserts text at the current cursor position in a contenteditable editor\n * identified by `data-editor-id`.\n */\nexport function insertTextAtCursor(editorId: string, text: string): boolean {\n try {\n const editor = document.querySelector(\n `[data-editor-id=\"${editorId}\"]`,\n ) as HTMLDivElement | null;\n if (!editor) {\n console.error(\"[editor-text-insertion] Editor not found:\", editorId);\n return false;\n }\n\n editor.focus();\n\n const selection = window.getSelection();\n if (!selection) {\n console.error(\"[editor-text-insertion] Selection not available\");\n return false;\n }\n\n let range: Range;\n\n if (selection.rangeCount > 0) {\n range = selection.getRangeAt(0);\n } else {\n range = document.createRange();\n const lastNode = editor.lastChild;\n if (lastNode) {\n if (lastNode.nodeType === Node.TEXT_NODE) {\n range.setStart(lastNode, lastNode.textContent?.length ?? 0);\n } else {\n range.setStartAfter(lastNode);\n }\n } else {\n range.setStart(editor, 0);\n }\n range.collapse(true);\n selection.removeAllRanges();\n selection.addRange(range);\n }\n\n if (!range.collapsed) {\n range.deleteContents();\n }\n\n const textNode = document.createTextNode(text);\n range.insertNode(textNode);\n\n range.setStartAfter(textNode);\n range.setEndAfter(textNode);\n selection.removeAllRanges();\n selection.addRange(range);\n\n editor.focus();\n\n return true;\n } catch (error) {\n console.error(\"[editor-text-insertion] Error inserting text:\", error);\n return false;\n }\n}\n","// features/context-menu-v3/utils/insert-into-editor.ts\n//\n// THE one way the menu puts text INTO the editable surface it was opened on.\n//\n// A surface advertises up to three write targets: a contentEditable editor id,\n// a textarea getter, and an `insertAtCaret` callback (rich editors). Which one\n// is live is a CALL-TIME fact, never a prop-presence fact: a host that swaps\n// editors in place (Notes: Split/Plain mount a textarea, Write mounts the rich\n// editor) keeps passing `getTextarea` and simply has it return null while the\n// rich editor is up. Gating the caret path on `!getTextarea` (the 2026-10-02\n// \"Insert reference… → Copied instead, 3/3 in Write mode\" bug) let a dead\n// getter veto the live editor. So each target is tried in order and the first\n// one that is actually there and takes the text wins.\n\nimport { insertTextAtCursor } from \"./editor-text-insertion\";\nimport { blockBoundary, insertTextAtTextareaCursor } from \"@ai-matrx/rich-editor/core/text-insertion\";\n\n/**\n * `inline` goes exactly at the caret; `block` (a reference fence, a section)\n * goes on its own line, never inside a word (G5 review, 2026-10-02: \"of\" became\n * \"o\" + block + \"f\") — BEFORE the caret's line only from its very start,\n * AFTER it otherwise (G11B: a right-click at the start of a line goes above;\n * G15, 2026-10-07: a character \"first half\" rule surprised wrapped\n * paragraphs). The one rule is `blockBoundary(…, \"nearest\")`\n * (@ai-matrx/rich-editor ≥ 0.3.1).\n */\nexport type EditorInsertPlacement = \"inline\" | \"block\";\n\nexport interface EditorInsertTargets {\n /** A contentEditable editor addressed by `data-editor-id`. */\n editorId?: string | undefined;\n /** The surface's textarea — may return null when no textarea is mounted now. */\n getTextarea?: (() => HTMLTextAreaElement | null) | undefined;\n /**\n * A rich editor's insert-at-the-caret. Returns false when it cannot take it.\n * A `block` must land on its own line/paragraph, never inside a word.\n */\n insertAtCaret?: ((text: string, placement?: EditorInsertPlacement) => boolean) | undefined;\n /** Full-value write-back for a controlled textarea. */\n onTextReplace?: ((nextValue: string) => void) | undefined;\n}\n\n/** The text to insert, shaped per target (a textarea knows its neighbours). */\nexport interface EditorInsertText {\n editor: string;\n textarea: (field: HTMLTextAreaElement) => string;\n caret: string;\n /** Default `inline`. */\n placement?: EditorInsertPlacement | undefined;\n}\n\nexport type EditorInsertTarget = \"editor\" | \"textarea\" | \"caret\";\n\n/** True when the surface advertises any write target at all. */\nexport function hasEditorInsertTarget(targets: EditorInsertTargets): boolean {\n return Boolean(targets.editorId) || Boolean(targets.getTextarea) || Boolean(targets.insertAtCaret);\n}\n\n/**\n * Insert into whichever target is live right now. Returns the target that took\n * the text, or null when none could (the caller then copies — never silent).\n */\nexport function insertIntoEditor(\n targets: EditorInsertTargets,\n text: string | EditorInsertText,\n): EditorInsertTarget | null {\n const shaped: EditorInsertText =\n typeof text === \"string\" ? { editor: text, textarea: () => text, caret: text } : text;\n const { editorId, getTextarea, insertAtCaret, onTextReplace } = targets;\n const placement = shaped.placement ?? \"inline\";\n\n if (editorId) {\n const took = attempt(\"editor\", () => {\n if (placement === \"block\") moveEditorCaretToLineEnd(editorId);\n return insertTextAtCursor(editorId, shaped.editor);\n });\n if (took) return \"editor\";\n }\n\n const field = editorId ? null : (getTextarea?.() ?? null);\n if (field) {\n const took = attempt(\"textarea\", () => {\n if (placement === \"block\" && field.selectionStart === field.selectionEnd) {\n const at = blockBoundary(field.value, field.selectionStart, field.selectionEnd, \"nearest\");\n field.setSelectionRange(at, at);\n }\n return insertTextAtTextareaCursor(field, shaped.textarea(field), onTextReplace);\n });\n if (took) return \"textarea\";\n }\n\n if (insertAtCaret && attempt(\"caret\", () => insertAtCaret(shaped.caret, placement))) return \"caret\";\n return null;\n}\n\n/**\n * A target that THROWS is a target that did not take the text — never an\n * exception that escapes into the caller. A rich editor's insert once threw\n * `TransformError` up through the reference picker's pick handler: the dialog\n * stayed open and nothing happened (G8B review, 2026-10-02). Now the next\n * target is tried, and with none left the caller copies and says so.\n */\nfunction attempt(target: EditorInsertTarget, insert: () => boolean): boolean {\n try {\n return insert();\n } catch (error) {\n console.error(`[ContextMenuV3] ${target} insert failed`, error);\n return false;\n }\n}\n\n/**\n * A collapsed caret inside a contentEditable text node moves to an edge of its\n * line (the start only from the very start, else the end), so a block never\n * lands inside a word. A selection is left alone: it is replaced where it is.\n */\nfunction moveEditorCaretToLineEnd(editorId: string): void {\n const editor = document.querySelector(`[data-editor-id=\"${editorId}\"]`);\n const selection = window.getSelection();\n if (!editor || !selection || selection.rangeCount === 0) return;\n const range = selection.getRangeAt(0);\n if (!range.collapsed || !editor.contains(range.startContainer)) return;\n const node = range.startContainer;\n if (node.nodeType !== Node.TEXT_NODE) return;\n const text = node.textContent ?? \"\";\n const at = blockBoundary(text, range.startOffset, range.startOffset, \"nearest\");\n const next = document.createRange();\n next.setStart(node, at);\n next.collapse(true);\n selection.removeAllRanges();\n selection.addRange(next);\n}\n\n/**\n * A block that must sit on its own paragraph inside a textarea: blank lines\n * are added only where the neighbours do not already provide them.\n */\nexport function ownParagraph(block: string, field: HTMLTextAreaElement): string {\n const before = field.value.slice(0, field.selectionStart);\n const after = field.value.slice(field.selectionEnd);\n const lead = before.length === 0 || before.endsWith(\"\\n\\n\") ? \"\" : before.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n const tail = after.length === 0 || after.startsWith(\"\\n\\n\") ? \"\" : after.startsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n return `${lead}${block}${tail}`;\n}\n","\"use client\";\n\nimport { compact } from \"../utils/compact\";\nimport { useMemo, useState, useCallback } from \"react\";\nimport { useAppDispatch, useAppSelector } from \"@ai-matrx/chat/store/hooks\";\nimport { getContextMenuHost } from \"../host/port\";\nimport {\n fetchUnifiedMenu,\n unifiedMenuLoadedKey,\n} from \"@ai-matrx/chat/agents/redux/agent-shortcuts/thunks\";\nimport { withMenuDeadline } from \"../utils/menu-deadline\";\nimport { selectAllShortcutsArray } from \"@ai-matrx/chat/agents/redux/agent-shortcuts/selectors\";\nimport { selectAllCategoriesArray } from \"@ai-matrx/chat/agents/redux/agent-shortcut-categories/selectors\";\nimport type { AgentShortcutRecord } from \"@ai-matrx/chat/agents/redux/agent-shortcuts/types\";\nimport type { AgentShortcutCategoryRecord } from \"@ai-matrx/chat/agents/redux/agent-shortcut-categories/types\";\nimport type { SklRenderDefinition as AgentContentBlockRecord } from \"@ai-matrx/chat/agents/redux/agent-shortcuts/skl-types\";\nimport type { Scope } from \"@ai-matrx/chat/agents/redux/shared/scope\";\nimport { resolveRowScope } from \"@ai-matrx/chat/agents/redux/shared/scope\";\nimport { placementGroupKey } from \"@ai-matrx/chat/agents/shortcuts/constants\";\nimport {\n decideOffer,\n readMenuScope,\n requirementsOf,\n type MenuScopeLevel,\n} from \"../model/requirement-gate\";\n\nexport type AgentMenuEntry =\n | ({ entryType: \"agent_shortcut\"; scopeLevel: Scope } & AgentShortcutRecord)\n | ({ entryType: \"content_block\"; scopeLevel: Scope } & AgentContentBlockRecord);\n\nexport interface AgentMenuCategoryGroup {\n category: AgentShortcutCategoryRecord & { scopeLevel: Scope };\n items: AgentMenuEntry[];\n children: AgentMenuCategoryGroup[];\n}\n\n/**\n * Keys whose POPULATION (not just existence) is checked — the single\n * population-level gate the menu keeps, and the only exception to\n * \"availability is the key existing, not the value\".\n *\n * Kept because the menu is selection-triggered anyway: \"No text selected →\n * nothing to translate → no translate option in the menu\" (Arman, INTERVIEW\n * R8). Everything else gates on key-existence only.\n *\n * The value tested is the RESOLVED scope's `selection`, which already carries\n * the platform's `active_text` convention (`value-resolution.ts`): on a\n * surface that knows the acting text, an unhighlighted right-click still has\n * a selection, so these items stay offered. Only a genuinely empty one hides\n * them.\n */\nconst SELECTION_DEPENDENT_KEYS: ReadonlySet<string> = new Set([\"selection\"]);\n\nexport interface UseUnifiedAgentContextMenuArgs {\n /**\n * Which placement submenus this host renders. LAYOUT ONLY — see\n * `buildCategoryGroups`. It never decides whether an ITEM is available.\n */\n placementTypes: string[];\n /**\n * The Surface Registry `ui_surface.name` (`<client>/<surface>`) this menu is\n * mounted on, or null on a page the platform cannot name. It is the\n * page-level rung of the scope hierarchy and the key to the exclusion valve.\n */\n surfaceName?: string | null | undefined;\n /**\n * Every surface value name that has a READ PATH here: the manifest's\n * declared values + the baseline floor + whatever actually landed in the\n * resolved scope. Built by `buildAvailableKeys` in the engine.\n */\n availableKeys: ReadonlySet<string>;\n /** True when the resolved scope carries a non-empty `selection`. */\n hasSelection: boolean;\n /** The surface's exclusion valve (`menu` surface-config namespace). */\n excludedItemIds?: ReadonlySet<string> | undefined;\n enabled?: boolean | undefined;\n scope?: Scope | undefined;\n scopeId?: string | null | undefined;\n}\n\nexport interface UseUnifiedAgentContextMenuResult {\n categoryGroups: AgentMenuCategoryGroup[];\n loading: boolean;\n error: string | null;\n refresh: () => Promise<void>;\n}\n\nconst SCOPE_PRIORITY: Record<Scope, number> = {\n task: 5,\n project: 4,\n user: 3,\n organization: 2,\n global: 1,\n};\n\n/**\n * Screen specificity of an item's authored scope: a page-pinned version beats\n * a domain-wide one, which beats a global one. Rows without a `surfaceName`\n * (categories, content blocks) are all global, so this never separates them.\n */\nconst SPECIFICITY: Record<MenuScopeLevel, number> = {\n surface: 3,\n domain: 2,\n global: 1,\n};\n\nfunction specificityOf(item: { surfaceName?: string | null }): number {\n return SPECIFICITY[\n readMenuScope({ id: \"\", surfaceName: item.surfaceName ?? null }).level\n ];\n}\n\n/**\n * Does `a` beat `b` for the same dedupe key? Deterministic and independent of\n * row order (the view emits ties in arbitrary order — every sort_order is 0):\n * 1. owner level (task > project > user > organization > global);\n * 2. screen specificity (page > domain > global) — the screen's own version\n * of an action always beats a generic copy at the same owner level;\n * 3. id, ascending — a total order, so equal rows never depend on arrival.\n */\nfunction beats<T extends { scopeLevel: Scope; id: string; surfaceName?: string | null }>(\n a: T,\n b: T,\n): boolean {\n const byOwner = SCOPE_PRIORITY[a.scopeLevel] - SCOPE_PRIORITY[b.scopeLevel];\n if (byOwner !== 0) return byOwner > 0;\n const bySpecificity = specificityOf(a) - specificityOf(b);\n if (bySpecificity !== 0) return bySpecificity > 0;\n return a.id < b.id;\n}\n\nfunction dedupeByPrecedence<\n T extends { scopeLevel: Scope; id: string; surfaceName?: string | null },\n>(items: T[], keyFn: (item: T) => string | null): T[] {\n const winners = new Map<string, T>();\n const passthrough: T[] = [];\n for (const item of items) {\n const key = keyFn(item);\n if (!key) {\n passthrough.push(item);\n continue;\n }\n const existing = winners.get(key);\n if (!existing || beats(item, existing)) winners.set(key, item);\n }\n return [...winners.values(), ...passthrough];\n}\n\nexport interface BuildCategoryGroupsArgs {\n placementTypes: string[];\n surfaceName?: string | null | undefined;\n availableKeys: ReadonlySet<string>;\n hasSelection: boolean;\n excludedItemIds?: ReadonlySet<string> | undefined;\n shortcuts: AgentShortcutRecord[];\n categories: AgentShortcutCategoryRecord[];\n contentBlocks: AgentContentBlockRecord[];\n}\n\n/**\n * THE grouping function — pure and unit-tested\n * (`__tests__/build-category-groups.test.ts`,\n * `../model/__tests__/requirement-gate.test.ts`). Turns the fetched\n * shortcut/category/content-block rows into the nested category tree each\n * placement submenu renders.\n *\n * 🚨 PHASE 6.7 — AVAILABILITY IS DERIVED (THE-MODEL law 3). An item is\n * offered here iff every surface value it consumes has a read path on this\n * surface; then its authored scope (global / domain / page) must reach here;\n * then the surface's exclusion valve gets the last word. The 16 hardcoded\n * `enabled_features` slugs and the RED `legacyMatch` rows they produced are\n * GONE: an item qualifies or is absent, and nothing renders as broken.\n *\n * 🚨 CATEGORIES GROUP, THEY NEVER GATE. A category's `placementType` still\n * decides WHICH placement submenu it renders under — that is layout, and it\n * is the only thing `placementTypes` does here. A category is never dropped\n * for failing a context filter, and an item's availability never consults its\n * category.\n *\n * Guarantees the tests certify:\n * - the derived gate: missing key → absent; key declared-but-empty → present;\n * - the hierarchy: global reaches everywhere, a domain reaches its surfaces,\n * a page-pinned item reaches only its page;\n * - the valve: an excluded id is absent even though it qualified;\n * - placement fidelity: a row renders under exactly its category's\n * placementType;\n * - nested categories nest under their parent, sorted by sortOrder then label;\n * - an EMPTY category still returns (with `items: []`) so the UI can render it\n * greyed — never silently dropped;\n * - an entry with no agent is kept (the renderer disables it as \"Not configured\");\n * - scope precedence dedupe (task > project > user > organization > global),\n * ties broken by screen specificity (page > domain > global), then id —\n * never by row order.\n */\nexport function buildCategoryGroups(\n args: BuildCategoryGroupsArgs,\n): AgentMenuCategoryGroup[] {\n const {\n placementTypes,\n surfaceName = null,\n availableKeys,\n hasSelection,\n excludedItemIds,\n shortcuts,\n categories,\n contentBlocks,\n } = args;\n if (placementTypes.length === 0) return [];\n\n // LAYOUT: which placement submenus this host draws.\n const placementSet = new Set(placementTypes);\n\n const scopedCategories = categories\n .filter(\n (c) =>\n c.isActive !== false &&\n placementSet.has(placementGroupKey(c.placementType)),\n )\n .map((c) => ({ ...c, scopeLevel: resolveRowScope(c) }));\n const scopedShortcuts = shortcuts\n .filter((s) => s.isActive !== false)\n .map((s) => ({\n ...s,\n entryType: \"agent_shortcut\" as const,\n scopeLevel: resolveRowScope(s),\n }));\n const scopedBlocks = contentBlocks\n .filter((b) => b.isActive !== false)\n .map((b) => ({\n ...b,\n entryType: \"content_block\" as const,\n scopeLevel: resolveRowScope(b),\n }));\n\n const offerCtx = compact({\n surfaceName,\n availableKeys,\n excludedItemIds,\n });\n\n const filteredShortcuts = scopedShortcuts.filter((s) => {\n if (!decideOffer(s, offerCtx).offered) return false;\n // The ONE population check (see SELECTION_DEPENDENT_KEYS).\n if (hasSelection) return true;\n return !requirementsOf(s).some((key) => SELECTION_DEPENDENT_KEYS.has(key));\n });\n\n // Content blocks are static insertable text — they consume nothing, so the\n // gate offers them everywhere. They still pass through the valve.\n const filteredBlocks = scopedBlocks.filter(\n (b) => !excludedItemIds?.has(b.id),\n );\n\n const dedupedShortcuts = dedupeByPrecedence(filteredShortcuts, (s) => {\n if (s.keyboardShortcut) return `kbd:${s.keyboardShortcut}`;\n return `label:${s.categoryId}:${s.label}`;\n });\n\n const dedupedBlocks = dedupeByPrecedence(\n filteredBlocks,\n (b) => `block:${b.categoryId ?? \"_none\"}:${b.blockId}`,\n );\n\n const byCategory = new Map<string, AgentMenuEntry[]>();\n for (const s of dedupedShortcuts) {\n if (!byCategory.has(s.categoryId)) byCategory.set(s.categoryId, []);\n byCategory.get(s.categoryId)!.push(s as AgentMenuEntry);\n }\n for (const b of dedupedBlocks) {\n const cid = b.categoryId;\n if (!cid) continue;\n if (!byCategory.has(cid)) byCategory.set(cid, []);\n byCategory.get(cid)!.push(b as AgentMenuEntry);\n }\n\n // CATEGORIES GROUP, NEVER GATE: every active category whose placement this\n // host renders is kept, empty or not. (An empty category still renders\n // greyed — Arman must see where new items will land the moment he creates\n // one.) Nothing here inspects the category's own `enabledFeatures`; the\n // column no longer decides anything.\n const dedupedCategories = dedupeByPrecedence(\n scopedCategories,\n (c) => `${c.placementType}:${c.parentCategoryId ?? \"_root\"}:${c.label}`,\n );\n\n const nodeMap = new Map<string, AgentMenuCategoryGroup>();\n for (const cat of dedupedCategories) {\n nodeMap.set(cat.id, {\n category: cat,\n items: (byCategory.get(cat.id) ?? [])\n .slice()\n .sort((x, y) => (x.sortOrder ?? 0) - (y.sortOrder ?? 0)),\n children: [],\n });\n }\n\n const roots: AgentMenuCategoryGroup[] = [];\n for (const cat of dedupedCategories) {\n const node = nodeMap.get(cat.id)!;\n if (cat.parentCategoryId && nodeMap.has(cat.parentCategoryId)) {\n nodeMap.get(cat.parentCategoryId)!.children.push(node);\n } else {\n roots.push(node);\n }\n }\n\n const sortNodes = (nodes: AgentMenuCategoryGroup[]) => {\n nodes.sort(\n (a, b) =>\n (a.category.sortOrder ?? 0) - (b.category.sortOrder ?? 0) ||\n a.category.label.localeCompare(b.category.label),\n );\n for (const n of nodes) sortNodes(n.children);\n };\n sortNodes(roots);\n\n return roots;\n}\n\nexport function useUnifiedAgentContextMenu(\n args: UseUnifiedAgentContextMenuArgs,\n): UseUnifiedAgentContextMenuResult {\n const {\n placementTypes,\n surfaceName = null,\n availableKeys,\n hasSelection,\n excludedItemIds,\n enabled = true,\n scope = \"global\",\n scopeId = null,\n } = args;\n\n const dispatch = useAppDispatch();\n\n const shortcuts = useAppSelector(selectAllShortcutsArray);\n const categories = useAppSelector(selectAllCategoriesArray);\n const contentBlocks = useAppSelector((state) => getContextMenuHost().selectRenderDefinitions(state));\n\n // A load is finished only when its rows are in hand: the menu's own\n // rows-in-hand flag in the store, not this hook's promise. A local\n // \"loading\" boolean read false on the first render (before the effect\n // fires) and false again when a skipped dispatch rejected at once — both\n // read as \"loaded, nothing here\", so My Items / Org Items vanished from the\n // first open and appeared on the next (round 6, 2026-09-27).\n const rowsInHand = useAppSelector(\n (state) =>\n state.agentShortcut.contextLoaded?.[\n unifiedMenuLoadedKey({ scope, scopeId })\n ] ?? false,\n );\n const [error, setError] = useState<string | null>(null);\n\n const refresh = useCallback(async () => {\n if (!enabled || placementTypes.length === 0) return;\n try {\n setError(null);\n await withMenuDeadline(\n dispatch(fetchUnifiedMenu({ scope, scopeId })).unwrap(),\n \"Loading the menu's AI actions\",\n );\n } catch (err) {\n // Skipped because the rows are already in hand: not a failure.\n if ((err as { name?: string } | null)?.name === \"ConditionError\") return;\n setError(err instanceof Error ? err.message : \"Failed to load menu\");\n }\n }, [dispatch, enabled, placementTypes.length, scope, scopeId]);\n\n // Intentionally NO mount-time useEffect that fires refresh(). The menu is\n // one of the most expensive fetches in the system — it must only run when\n // the user actually engages. UnifiedAgentContextMenu calls `refresh()`\n // from its `onOpenChange` handler. The fetchUnifiedMenu thunk dedupes\n // internally (module-level inflight map + scope-loaded condition) so\n // rapid opens + multi-mounted menus all resolve to a single HTTP call.\n\n const categoryGroups = useMemo<AgentMenuCategoryGroup[]>(() => {\n if (!enabled) return [];\n return buildCategoryGroups({\n placementTypes,\n surfaceName,\n availableKeys,\n hasSelection,\n excludedItemIds,\n shortcuts,\n categories,\n contentBlocks: [...contentBlocks],\n });\n }, [\n enabled,\n placementTypes,\n categories,\n shortcuts,\n contentBlocks,\n surfaceName,\n availableKeys,\n hasSelection,\n excludedItemIds,\n ]);\n\n return {\n categoryGroups,\n loading:\n enabled && placementTypes.length > 0 && !rowsInHand && error === null,\n // Rows in hand end a failure too: a fetch that landed after the menu's\n // deadline must not leave a \"Couldn't load\" row beside the rows it brought.\n error: rowsInHand ? null : error,\n refresh,\n };\n}\n","// features/context-menu-v3/value-resolution.ts\n//\n// The single most important module in v3: it assembles the `ApplicationScope`\n// the menu acts on, and it makes the two failure classes the user keeps hitting\n// STRUCTURALLY VISIBLE:\n//\n// 1. \"Fake menu\" — a menu that opens but Copy does nothing and the selection\n// bar is empty, because the surface wired neither a selection nor content.\n// Fixed at the root: the DOM-subtree text fallback means read-only content\n// is copyable/actionable with zero surface wiring; `detectInertMenu` then\n// SCREAMS in dev if a menu still opens with nothing resolvable.\n//\n// 2. Broken value mapping — a surface declares a value its shortcuts/agents\n// depend on but forgets to emit it. `auditSurfaceScope` compares the\n// surface's declared `alwaysAvailable` values against what actually landed\n// in the scope and screams about any that are missing.\n//\n// The 5 generic baseline values (selection, text_before, text_after, content,\n// context) are guaranteed present via the platform primitive `withBaselineScope`\n// — we reuse it, never reimplement it.\n\nimport type { ApplicationScope } from \"@ai-matrx/chat/agents/types/scope.types\";\nimport { withBaselineScope } from \"@ai-matrx/chat/surfaces/utils/baseline-scope\";\nimport {\n BASELINE_VALUE_NAMES,\n type BaselineKey,\n} from \"@ai-matrx/chat/surfaces/manifests/_baseline.manifest\";\nimport { getContextMenuHost } from \"./host/port\";\nimport {\n getSurfaceRuntime,\n getSurfaceRuntimeForName,\n} from \"@ai-matrx/chat/surfaces/runtime/SurfaceRuntimeContext\";\nimport type { SelectionRange } from \"./utils/selection-tracking\";\nimport { CONTEXT_MENU_ENTITY_KEY, CONTEXT_MENU_HEADING_KEY } from \"./types\";\n\n/**\n * Keys the menu manages internally and must not leak into the scope as values.\n * `__entity` is the reserved per-row entity (`utils/per-row-entity.ts`) — the\n * shell already strips it; this is the belt for a surface that puts it in\n * static `contextData`.\n */\nconst SKIP_MERGE_KEYS = new Set<string>([CONTEXT_MENU_ENTITY_KEY, CONTEXT_MENU_HEADING_KEY]);\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction strOf(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\nexport interface ResolveScopeArgs {\n /** Live scope builder from the surface — wins when present. */\n getApplicationScope?: (() => ApplicationScope) | undefined;\n /** Static + per-target-merged value payload from the surface. */\n contextData: Record<string, unknown>;\n /** The text the user has selected (captured by the shell at open). */\n selectedText: string;\n /** The captured selection range (drives text_before / text_after / content). */\n selectionRange: SelectionRange | null;\n /**\n * Plain text of the right-clicked DOM subtree, captured by the shell. Used as\n * `content` ONLY when nothing else resolved it — the net that stops a\n * read-only surface from presenting a dead Copy.\n */\n fallbackContent?: string | undefined;\n /**\n * The surface this menu declared. Used to adopt the LIVE mounted\n * `SurfaceRuntime` for that same surface as an underlay (see\n * `adoptMountedRuntimeScope`).\n */\n surfaceName?: string | undefined;\n /**\n * AMBIENT STATE — what is true around the user right now (active org, active\n * scopes, surface, whether a run is streaming). Collected by the engine from\n * Redux and landed on ONE namespaced `ambient` key, never spread across the\n * scope. The weakest layer there is: a surface that emits its own `ambient`\n * keeps it untouched.\n */\n ambient?: Record<string, unknown> | undefined;\n}\n\n/**\n * Build the complete `ApplicationScope` for this menu invocation.\n *\n * Assembly: the static underlay (`contextData` + the shell-captured selection\n * triad) is built first; when the surface supplies a LIVE builder\n * (`getApplicationScope`) its result is overlaid PER KEY on top. A key the\n * builder emits is authoritative — including an explicit empty (the Vault\n * forces `selection: \"\"` so a highlight over revealed plaintext never enters\n * the scope). A key the builder never mentions falls back to the underlay, so\n * a live-scope surface no longer silently discards `contextData` or the\n * captured selection/text_before/text_after wholesale.\n *\n * Order of precedence for `content`:\n * 1. surface `getApplicationScope().content` / `contextData.content`\n * 2. the full value of an editable field (from the selection range)\n * 3. the DOM-subtree text fallback (`fallbackContent`)\n *\n * The result always carries the 5 baseline keys (empty-floored).\n */\n/**\n * Adopt the LIVE mounted `SurfaceRuntime` as the bottom layer of the scope.\n *\n * 🚨 THE RICH-PAGE / INERT-MENU CLASS (Phase 0, 2026-08-25). `getSurfaceRuntime`\n * holds what the page is actually showing — its filters, its date range, its\n * rows. The launch thunk adopts it. The shell-header Agents panel reads it. The\n * MENU never did: a page with a fully mounted provider still produced an inert\n * menu whenever whoever wired the wrapper did not also hand it values by hand.\n * That made \"make this surface's menu know what is on screen\" per-surface\n * labour, repeated once per surface, done inconsistently — the single biggest\n * driver of shell menus in the census.\n *\n * PRECEDENCE — this is an UNDERLAY, the weakest layer there is. It fills only\n * keys that nothing else provided; `contextData`, the per-row merge, a live\n * `getApplicationScope` and the DOM capture all beat it. A surface that says\n * something is never contradicted by the page it sits on.\n *\n * SYNC ONLY, deliberately. `getScope()` may return a promise, and menu scope is\n * assembled during the render that opens the menu. An async builder is skipped\n * here and still reaches agents at Run time through the launch thunk's own\n * adoption — so the async case degrades to exactly the previous behaviour\n * rather than to something wrong.\n */\nfunction adoptMountedRuntimeScope(\n surfaceName: string | undefined,\n): ApplicationScope | null {\n try {\n const mounted = surfaceName\n ? getSurfaceRuntimeForName(surfaceName)\n : getSurfaceRuntime();\n if (!mounted) return null;\n const payload = mounted.getScope();\n // A promise means this surface builds its scope asynchronously — see above.\n if (payload && typeof (payload as Promise<unknown>).then === \"function\")\n return null;\n return payload as ApplicationScope;\n } catch (err) {\n // Loud, non-fatal: the menu still opens with whatever the surface passed.\n if (isDev)\n console.error(\n \"[ContextMenuV3] mounted SurfaceRuntime threw in getScope() — menu scope falls back to the surface's own values\",\n err,\n );\n return null;\n }\n}\n\n/** True for a value that carries no information — an underlay may fill it. */\nfunction isEmptyValue(value: unknown): boolean {\n if (value === undefined || value === null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (typeof value === \"object\" && !Array.isArray(value))\n return Object.keys(value as object).length === 0;\n return false;\n}\n\nexport function resolveApplicationScope(\n args: ResolveScopeArgs,\n): ApplicationScope {\n const base: ApplicationScope = buildFromMenuContext(args);\n\n /**\n * Keys the SURFACE itself spoke about — static payload plus live builder.\n * 🚨 An explicit EMPTY is an answer, not a silence: the Vault forces\n * `selection: \"\"` as credential-leak hardening, and the runtime underlay\n * below must never refill it from the page the Vault happens to sit on.\n */\n const surfaceAnswered = new Set<string>(\n Object.keys(args.contextData ?? {}).filter((k) => !SKIP_MERGE_KEYS.has(k)),\n );\n if (args.getApplicationScope) {\n const live = args.getApplicationScope();\n for (const [k, v] of Object.entries(live)) {\n if (SKIP_MERGE_KEYS.has(k) || v === undefined) continue;\n base[k] = v;\n surfaceAnswered.add(k);\n }\n }\n\n // THE RUNTIME UNDERLAY — fills only genuine silences, beaten by everything.\n const runtime = adoptMountedRuntimeScope(args.surfaceName);\n if (runtime) {\n for (const [k, v] of Object.entries(runtime)) {\n if (SKIP_MERGE_KEYS.has(k) || v === undefined) continue;\n if (surfaceAnswered.has(k)) continue;\n if (isEmptyValue(base[k])) base[k] = v;\n }\n }\n\n // THE AMBIENT LAYER — weaker still than the runtime underlay, and confined\n // to ONE key. A surface that speaks about `ambient` is never contradicted.\n if (args.ambient && !surfaceAnswered.has(\"ambient\") && isEmptyValue(base.ambient))\n base.ambient = args.ambient;\n\n // No-fake-menu net: if no usable content resolved but the DOM gave us text,\n // adopt it so Copy / AI actions always have something to act on. A surface\n // with a LIVE builder opted out of this net (documented in FEATURE.md — the\n // Vault relies on it: DOM text there can be revealed plaintext, and an empty\n // live scope must stay empty rather than adopt it).\n const hasContent = strOf(base.content).trim().length > 0;\n if (\n !args.getApplicationScope &&\n !hasContent &&\n args.fallbackContent &&\n args.fallbackContent.trim()\n ) {\n base.content = args.fallbackContent;\n }\n\n return withBaselineScope(base);\n}\n\n/** Assemble scope from static contextData + the captured selection. */\nfunction buildFromMenuContext(args: ResolveScopeArgs): ApplicationScope {\n const { selectedText, selectionRange, contextData: cd } = args;\n\n let textBefore = \"\";\n let textAfter = \"\";\n let fullContent = \"\";\n if (\n selectionRange &&\n selectionRange.type === \"editable\" &&\n selectionRange.element\n ) {\n const value = selectionRange.element.value ?? \"\";\n fullContent = value;\n textBefore = value.substring(0, selectionRange.start ?? 0);\n textAfter = value.substring(selectionRange.end ?? 0);\n }\n\n const scope: ApplicationScope = {};\n\n // Surface payload first — skip undefined so live capture is not clobbered.\n for (const [k, v] of Object.entries(cd)) {\n if (SKIP_MERGE_KEYS.has(k) || v === undefined) continue;\n scope[k] = v;\n }\n\n // Normalize `context` into an object (the resolver flattens it into entries).\n if (typeof cd.context === \"string\") {\n scope.context = { raw: cd.context };\n } else if (cd.context !== undefined && typeof cd.context !== \"string\") {\n scope.context = cd.context as Record<string, unknown>;\n } else if (scope.context === undefined) {\n scope.context = {};\n }\n\n // Live DOM capture wins for the baseline text-editor triad.\n scope.selection = selectedText;\n scope.text_before = textBefore;\n scope.text_after = textAfter;\n\n if (typeof cd.content === \"string\") {\n scope.content = cd.content;\n } else if (fullContent) {\n scope.content = fullContent;\n } else if (scope.content === undefined) {\n scope.content = \"\";\n }\n\n // Convention: `active_text` is selection when highlighted, else the full body.\n // Bindings mapping agent `content` ← surface `selection` keep working when the\n // user didn't highlight but the surface knows the acting text.\n const activeText = scope.active_text;\n if (\n scope.selection === \"\" &&\n typeof activeText === \"string\" &&\n activeText.length > 0\n ) {\n scope.selection = activeText;\n }\n\n return scope;\n}\n\n// ---------------------------------------------------------------------------\n// Source gating — \"never show an action that can't work\".\n// ---------------------------------------------------------------------------\n\n/**\n * The text Copy / AI actions should operate on: the selection if there is one,\n * otherwise the resolved primary content. This is what makes Copy work on a\n * read-only surface where the user merely right-clicked.\n */\nexport interface ResolvedActionText {\n text: string;\n source: \"selection\" | \"content\" | \"none\";\n}\n\nexport function resolveActionText(scope: ApplicationScope): ResolvedActionText {\n const selection = strOf(scope.selection).trim();\n if (selection) return { text: strOf(scope.selection), source: \"selection\" };\n const content = strOf(scope.content).trim();\n if (content) return { text: strOf(scope.content), source: \"content\" };\n return { text: \"\", source: \"none\" };\n}\n\n/** True when there is anything for the content-acting half of the menu to do. */\nexport function hasActionableContent(scope: ApplicationScope): boolean {\n return resolveActionText(scope).source !== \"none\";\n}\n\n// ---------------------------------------------------------------------------\n// Loud guards (dev only) — the second layer that makes the failure visible.\n// ---------------------------------------------------------------------------\n\nexport interface InertMenuDiagnostic {\n /** True when the menu opened with nothing meaningful to do. */\n inert: boolean;\n reasons: string[];\n}\n\n/**\n * A menu is INERT when, on a read-only surface, there is no selection, no\n * resolvable content, and the surface injected no items of its own — i.e. the\n * user sees Copy/AI rows that do nothing. (Editable surfaces are never inert:\n * Paste is always meaningful.)\n */\nexport function detectInertMenu(args: {\n scope: ApplicationScope;\n isEditable: boolean;\n hasExtraSections: boolean;\n}): InertMenuDiagnostic {\n const reasons: string[] = [];\n const selection = strOf(args.scope.selection).trim();\n const content = strOf(args.scope.content).trim();\n // Structured surfaces such as galleries and charts legitimately have no\n // document text, but their live surface values still make Agents and other\n // scope-aware actions meaningful. Treat any informative non-baseline value\n // as an actionable scope instead of falsely reporting an inert menu.\n const baseline = new Set<string>(\n BASELINE_VALUE_NAMES as readonly BaselineKey[],\n );\n const hasApplicationScope = Object.entries(args.scope).some(\n ([key, value]) => !baseline.has(key) && !isEmptyValue(value),\n );\n if (!selection) reasons.push(\"no selection\");\n if (!content) reasons.push(\"no resolvable content (and no DOM-text fallback)\");\n const inert =\n !args.isEditable &&\n !selection &&\n !content &&\n !hasApplicationScope &&\n !args.hasExtraSections;\n return { inert, reasons };\n}\n\n/**\n * Compare a surface's DECLARED `alwaysAvailable` values against what actually\n * landed in the resolved scope. A declared-but-missing value silently breaks\n * the shortcuts/agents that depend on it — the exact \"values not passed\"\n * failure. Baseline values are skipped (they are empty-floored by design, so a\n * missing one is not an error). Returns human-readable warnings.\n */\nexport function auditSurfaceScope(\n surfaceName: string | undefined,\n scope: ApplicationScope,\n): string[] {\n if (!surfaceName) return [];\n const manifest = getContextMenuHost().getSurfaceManifest(surfaceName);\n if (!manifest) {\n return [\n `surface \"${surfaceName}\" has no registered manifest — its declared values cannot be verified`,\n ];\n }\n const baseline = new Set<string>(BASELINE_VALUE_NAMES as readonly BaselineKey[]);\n const warnings: string[] = [];\n for (const value of manifest.values) {\n if (baseline.has(value.name)) continue; // floored — never an error\n if (!value.alwaysAvailable) continue; // optional by declaration\n // EMITTED-vs-EMPTY. A surface that emits the key has wired the value; an\n // empty string is a legal value for a text control nobody has typed into\n // (`crm.manifest.ts` declares exactly that for `search_query`). Screaming\n // at it made a correctly-wired surface look broken every time a search box\n // was empty — a false alarm is how a real scream gets ignored.\n const present = scope[value.name];\n if (!(value.name in scope) || present === undefined || present === null) {\n warnings.push(\n `declared value \"${value.name}\" is alwaysAvailable but was not emitted into the scope`,\n );\n }\n }\n return warnings;\n}\n\n/**\n * Run both loud guards and SCREAM in dev. A recovery/diagnostic layer that fires\n * means a real defect got past the surface wiring — per doctrine it must be\n * impossible to miss, not a silent default. No-op in production.\n */\nexport function reportMenuDiagnostics(args: {\n surfaceName: string | undefined;\n scope: ApplicationScope;\n isEditable: boolean;\n hasExtraSections: boolean;\n}): InertMenuDiagnostic {\n const diag = detectInertMenu(args);\n if (!isDev) return diag;\n\n const label = args.surfaceName ?? \"(no surfaceName)\";\n if (diag.inert) {\n console.error(\n `%c[ContextMenuV3] INERT MENU on \"${label}\"%c — opened with nothing to act on (${diag.reasons.join(\n \", \",\n )}). This is the \"fake menu\" defect: the surface must provide a selection, contextData.content, getApplicationScope(), or render selectable content the DOM fallback can read.`,\n \"color:#ef4444;font-weight:bold\",\n \"color:inherit\",\n { scopeKeys: Object.keys(args.scope) },\n );\n }\n const valueWarnings = auditSurfaceScope(args.surfaceName, args.scope);\n if (valueWarnings.length > 0) {\n console.error(\n `%c[ContextMenuV3] VALUE MAPPING GAP on \"${label}\"%c — ${valueWarnings.join(\n \"; \",\n )}. Shortcuts/agents bound to these values will receive nothing.`,\n \"color:#f59e0b;font-weight:bold\",\n \"color:inherit\",\n );\n }\n return diag;\n}\n","// features/context-menu-v3/utils/selection-write-back.ts\n//\n// SELECTION WRITE-BACK — the Replace / Insert below doors for an AI result\n// that was launched from an editable surface's right-click menu.\n//\n// History: the legacy prompts menu (DynamicContextMenu, deleted 2026-06-28 in\n// 3eef0cab17) ran a text action from an editable selection and showed\n// TextActionResultModal with Replace / Insert before / Insert after. The\n// agents-era menus (v2 UnifiedAgentContextMenu, then v3) kept only\n// `runtime.widgetHandleId`, which lets the MODEL call widget_text_* tools.\n// A shortcut whose agent just answers with text (e.g. \"Clean up webpage\n// content\") therefore produced the right text with no way to put it back —\n// reported on /notes 2026-10-01. This restores the human door for EVERY\n// shortcut / bound agent launched from an editable menu, independent of the\n// shortcut's display mode: the per-launch widget handle carries a\n// `selection` write-back, and the result's action bar (rich-document\n// actions `replace-selection` / `insert-below`) reads it from the\n// conversation's widget handle.\n//\n// Contract: the surface's `onTextReplace` takes the WHOLE new value (see\n// buildEditableWidgetHandle). A textarea/input write therefore splices the\n// captured range into the live value and hands over the full text. Editors\n// without a field (rich editors) use their own caret primitives:\n// `insertAtCaret` replaces the live selection, `onTextInsertAfter` inserts a\n// block after it.\n\nimport type { SelectionWriteBack } from \"@ai-matrx/chat/agents/types/widget-handle.types\";\nimport { spliceInputValue, type SelectionRange } from \"./selection-tracking\";\n\nexport interface BuildSelectionWriteBackArgs {\n /** The text the agent was launched on (the selection, or the whole field). */\n originalText: string;\n /** \"selection\" when the person selected text; anything else = whole field. */\n textSource: string;\n selectionRange: SelectionRange | null | undefined;\n onTextReplace?: ((fullValue: string) => void) | undefined;\n onTextInsertAfter?: ((text: string) => void) | undefined;\n insertAtCaret?: ((text: string) => boolean) | undefined;\n}\n\ntype Field = HTMLTextAreaElement | HTMLInputElement;\n\nfunction isField(el: unknown): el is Field {\n return (\n (typeof HTMLTextAreaElement !== \"undefined\" &&\n el instanceof HTMLTextAreaElement) ||\n (typeof HTMLInputElement !== \"undefined\" && el instanceof HTMLInputElement)\n );\n}\n\n/**\n * Build the write-back for one launch, or null when the surface cannot be\n * written (read-only, or nothing captured to write over).\n */\nexport function buildSelectionWriteBack(\n args: BuildSelectionWriteBackArgs,\n): SelectionWriteBack | null {\n const {\n originalText,\n textSource,\n selectionRange,\n onTextReplace,\n onTextInsertAfter,\n insertAtCaret,\n } = args;\n if (!originalText) return null;\n\n const fieldEl =\n selectionRange?.type === \"editable\" && isField(selectionRange.element)\n ? selectionRange.element\n : null;\n\n if (fieldEl) {\n // The anchor moves with every successful write, so Replace twice is a\n // no-op re-apply and Insert below after Replace lands under the new text.\n let anchorText = originalText;\n let anchorStart =\n textSource === \"selection\" ? selectionRange!.start : 0;\n\n const locate = (): { start: number; end: number } | null => {\n if (!fieldEl.isConnected) return null;\n const value = fieldEl.value;\n if (value.slice(anchorStart, anchorStart + anchorText.length) === anchorText) {\n return { start: anchorStart, end: anchorStart + anchorText.length };\n }\n // The text moved (edits above it) — find it again, only if unambiguous.\n const first = value.indexOf(anchorText);\n if (first === -1 || value.indexOf(anchorText, first + 1) !== -1) {\n return null;\n }\n return { start: first, end: first + anchorText.length };\n };\n\n const write = (start: number, end: number, insert: string): void => {\n const value = fieldEl.value;\n const next = value.slice(0, start) + insert + value.slice(end);\n if (onTextReplace) onTextReplace(next);\n else spliceInputValue(fieldEl, start, end, insert);\n };\n\n return {\n originalText,\n replace: (text) => {\n const at = locate();\n if (!at) return false;\n write(at.start, at.end, text);\n anchorStart = at.start;\n anchorText = text;\n return true;\n },\n insertBelow: (text) => {\n const at = locate();\n if (!at) return false;\n const block = \"\\n\\n\" + text;\n write(at.end, at.end, block);\n anchorStart = at.end + 2;\n anchorText = text;\n return true;\n },\n };\n }\n\n // Editors without a field (rich editors) — only a real selection is a\n // target; their caret primitives act on the editor's live selection.\n if (textSource !== \"selection\") return null;\n if (!insertAtCaret && !onTextInsertAfter) return null;\n return {\n originalText,\n replace: (text) => (insertAtCaret ? insertAtCaret(text) : false),\n insertBelow: (text) => {\n if (!onTextInsertAfter) return false;\n onTextInsertAfter(text);\n return true;\n },\n };\n}\n","\"use client\";\n\n// features/context-menu-v3/hooks/useContextMenuActions.ts\n//\n// THE shared engine behind every menu layout. `components/AlchemyMenuContent`\n// runs it and hands its model to the Alchemy package, whose layouts (right-\n// click, phone sheet, palette, bar ⋯) are pure PRESENTATION (ALC-15 S3) —\n// while every piece of behavior lives here\n// exactly once: placement resolution, the single deduped data fetch, scope +\n// action-text resolution, the rich-document action lists, and every handler\n// (clipboard, history, compare, launch, insert, attach/share, admin).\n//\n// This extraction pays down the long-flagged \"handlers ported 1:1, keep in\n// lockstep\" debt: a launch-path or handler change now lands in ONE place and\n// both renderers inherit it. Do NOT add a handler to a renderer — add it here.\n//\n// Inline agent editing: both launch handlers pass `runtime.widgetHandleId`\n// (registered by the shell for editable surfaces), so any agent/shortcut\n// launched from the menu can stream `widget_text_*` edits into the surface.\n\nimport { compact } from \"../utils/compact\";\nimport { fieldLabelOf } from \"../utils/field-menu-header\";\nimport { actionsAlreadyHere } from \"../utils/already-here\";\nimport { editorTextAgentCallbacks } from \"../utils/editor-text-agent\";\nimport { useEffect, useMemo } from \"react\";\nimport { copyText as kitCopyText, readText as kitReadText } from \"@ai-matrx/kit/clipboard\";\nimport {\n AppWindow,\n Braces,\n Building,\n ClipboardCopy,\n FileText,\n Info,\n Layers,\n Plus,\n Rocket,\n ShieldCheck,\n User,\n} from \"lucide-react\";\nimport { CANONICAL_MENU_VERSION_V3 } from \"../types\";\nimport type { ContextMenuExtraSection, ContextMenuExtraItem } from \"../types\";\nimport { detectActiveSurface } from \"@ai-matrx/chat/surfaces/utils/route-to-surface\";\nimport { getSurfaceDisplayLabel } from \"@ai-matrx/chat/surfaces/utils/surface-display\";\nimport { getRelatedSurfaces } from \"@ai-matrx/chat/surfaces/runtime/fetchRelatedSurfaces\";\nimport { getIconComponent } from \"@ai-matrx/icons\";\nimport { useAppDispatch, useAppSelector, useAppStore } from \"@ai-matrx/chat/store/hooks\";\nimport { selectUserId } from \"@ai-matrx/chat/host/identity\";\nimport { toast } from \"../utils/notify\";\nimport { ensureBlockTemplate } from \"../utils/ensure-block-template\";\nimport {\n getContextMenuHost,\n useContextMenuHostBindings,\n type ContextMenuQuickActions,\n type ReferencePick,\n} from \"../host/port\";\nimport { selectOrganizationId } from \"@ai-matrx/chat/host/org\";\nimport { useAgentLauncher } from \"@ai-matrx/chat/agents/hooks/useAgentLauncher\";\nimport { useSpeech } from \"@ai-matrx/media/react\";\nimport { primeAudioOutput } from \"@ai-matrx/media/speech\";\nimport { useSurfaceAgentRoles } from \"@ai-matrx/chat/surfaces/hooks/useSurfaceConfig\";\nimport { hasEditorInsertTarget, insertIntoEditor, ownParagraph } from \"../utils/insert-into-editor\";\nimport { resolveActions } from \"@ai-matrx/rich-content/rich-document/actions/provider\";\nimport { registryMenuActions } from \"@ai-matrx/rich-content/rich-document/variants/shared/menuStructure\";\nimport { getSourceAdapter } from \"@ai-matrx/rich-content/rich-document/actions/sources/index\";\nimport { shortHash } from \"@ai-matrx/rich-content/rich-document/actions/sources/raw\";\nimport type {\n ContentSource,\n RichDocumentAction,\n RichDocumentActionContext,\n} from \"@ai-matrx/rich-content/rich-document/types\";\nimport {\n placementGroupKey,\n PLACEMENT_TYPES,\n PLACEMENT_TYPE_META,\n} from \"@ai-matrx/chat/agents/shortcuts/constants\";\nimport type { ResultDisplayMode } from \"@ai-matrx/chat/agents/types/instance.types\";\nimport type { ApplicationScope } from \"@ai-matrx/chat/agents/types/scope.types\";\nimport {\n useUnifiedAgentContextMenu,\n type AgentMenuEntry,\n type AgentMenuCategoryGroup,\n} from \"./useUnifiedAgentContextMenu\";\nimport { buildAvailableKeys } from \"../model/requirement-gate\";\nimport { BASELINE_VALUE_NAMES } from \"@ai-matrx/chat/surfaces/manifests/_baseline.manifest\";\nimport { useSurfaceConfig } from \"@ai-matrx/chat/surfaces/hooks/useSurfaceConfig\";\nimport type { MenuConfig } from \"@ai-matrx/chat/surfaces/config/namespace-registry\";\nimport { useSurfaceBoundAgents } from \"@ai-matrx/chat/surfaces/hooks/useSurfaceBoundAgents\";\nimport type {\n SurfaceBoundAgentEntry,\n SurfaceBoundAgentSection,\n} from \"@ai-matrx/chat/surfaces/services/surface-bound-agents.service\";\nimport {\n resolveApplicationScope,\n resolveActionText,\n reportMenuDiagnostics,\n type ResolvedActionText,\n} from \"../value-resolution\";\nimport { spliceInputValue } from \"../utils/selection-tracking\";\nimport { buildSelectionWriteBack } from \"../utils/selection-write-back\";\nimport { registerLaunchWidgetHandle } from \"@ai-matrx/chat/agents/utils/launch-widget-handles\";\nimport {\n buildJsonMenuSection,\n type JsonMenuSection,\n} from \"../utils/json-menu-actions\";\nimport type {\n MenuContentProps,\n PlacementKey,\n PlacementVisibility,\n} from \"../types\";\nimport { MANAGED_CONTEXT_MENU_AGENT_CONFIG } from \"../managed-agent-launch\";\nimport { AGENT_ICON } from \"@ai-matrx/icons/domain\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared menu-model constants + pure helpers (used by both renderers).\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const DEFAULT_PLACEMENT_MODE: Record<PlacementKey, PlacementVisibility> =\n {\n \"ai-action\": \"show\",\n \"bound-agent\": \"show\",\n \"content-block\": \"show\",\n \"organization-tool\": \"show\",\n \"user-tool\": \"show\",\n \"quick-action\": \"show\",\n };\n\nexport const ALL_DB_PLACEMENTS: PlacementKey[] = [\n \"ai-action\",\n \"content-block\",\n \"organization-tool\",\n \"user-tool\",\n];\n\n/** User-facing relabels matching the v3 taxonomy (My Items / Org Items). */\nexport const PLACEMENT_LABEL_OVERRIDE: Partial<Record<string, string>> = {\n [PLACEMENT_TYPES.USER_TOOL]: \"My Items\",\n [PLACEMENT_TYPES.ORGANIZATION_TOOL]: \"Org Items\",\n};\n\nexport const PLACEMENT_COLOR: Record<string, string> = {\n [PLACEMENT_TYPES.AI_ACTION]: \"#0ea5e9\",\n [PLACEMENT_TYPES.CONTENT_BLOCK]: \"#8b5cf6\",\n [PLACEMENT_TYPES.ORGANIZATION_TOOL]: \"#f59e0b\",\n [PLACEMENT_TYPES.USER_TOOL]: \"#10b981\",\n};\n\nexport function getPlacementIcon(placementType: string) {\n switch (placementType) {\n case PLACEMENT_TYPES.AI_ACTION:\n return Rocket;\n case PLACEMENT_TYPES.CONTENT_BLOCK:\n return FileText;\n case PLACEMENT_TYPES.ORGANIZATION_TOOL:\n return Building;\n case PLACEMENT_TYPES.USER_TOOL:\n return User;\n default:\n return FileText;\n }\n}\n\nexport function getPlacementLabel(placementType: string): string {\n return (\n PLACEMENT_LABEL_OVERRIDE[placementType] ??\n PLACEMENT_TYPE_META[placementType as keyof typeof PLACEMENT_TYPE_META]\n ?.label ??\n placementType\n );\n}\n\nexport function resolveIcon(\n iconName: string | null | undefined,\n fallback = \"FileText\",\n) {\n return getIconComponent(iconName ?? fallback, fallback);\n}\n\nexport function groupsByPlacement(\n groups: AgentMenuCategoryGroup[],\n): Record<string, AgentMenuCategoryGroup[]> {\n const map: Record<string, AgentMenuCategoryGroup[]> = {};\n for (const g of groups) {\n // Nullable in storage — key by the shared group key so an unplaced\n // category groups instead of throwing (agent-shortcuts/constants.ts).\n (map[placementGroupKey(g.category.placementType)] ??= []).push(g);\n }\n return map;\n}\n\nexport function hasItemsRecursive(group: AgentMenuCategoryGroup): boolean {\n if (group.items.length > 0) return true;\n return group.children.some(hasItemsRecursive);\n}\n\n/** Resolve a rich-document action's label + disabled state for rendering. */\nexport function resolveRichActionView(\n action: RichDocumentAction,\n ctx: RichDocumentActionContext,\n): { label: string; disabled: boolean } {\n const label =\n typeof action.label === \"function\" ? action.label(ctx) : action.label;\n const disabledResult = action.disabled?.(ctx);\n const disabled =\n typeof disabledResult === \"object\" ? true : Boolean(disabledResult);\n return { label, disabled };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The engine hook.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ContextMenuActions {\n // Resolved model\n scope: ApplicationScope;\n actionText: ResolvedActionText;\n /** The name of the field the menu opened in (its label), or null. */\n fieldLabel: string | null;\n /** JSON verbs for a JSON-shaped selection/content. `null` = not JSON. */\n jsonSection: JsonMenuSection | null;\n resolvedPlacementMode: Record<PlacementKey, PlacementVisibility>;\n categoryGroups: AgentMenuCategoryGroup[];\n grouped: Record<string, AgentMenuCategoryGroup[]>;\n loading: boolean;\n /** Why the agent libraries (AI Actions, My Items, …) did not load — e.g. past the menu's deadline. */\n librariesError: string | null;\n boundAgentSections: SurfaceBoundAgentSection[];\n boundAgentsLoading: boolean;\n boundAgentsError: string | null;\n /** Re-run the library fetches after a failure or a missed deadline. */\n retryLibraries: () => void;\n /** Rich-document action ids this menu must not draw (surface exclusions + already-here). */\n excludedRichActionIds: string[];\n richDocCtx: RichDocumentActionContext;\n /** The registry actions this menu shows — rendered as the ONE tree. */\n registryActions: RichDocumentAction[];\n copyVariantActions: RichDocumentAction[];\n exportActions: RichDocumentAction[];\n convertActions: RichDocumentAction[];\n // Flags\n hasCompareBase: boolean;\n isAdmin: boolean;\n isDebugMode: boolean;\n isAdminIndicatorOpen: boolean;\n canNativeUndo: boolean;\n // Handlers\n handleCopy: () => Promise<void>;\n handleSpeak: () => void;\n handleSpokenSummary: () => void;\n handleSpokenSummaryLive: () => void;\n spokenSummaryAvailable: boolean;\n handleCut: () => Promise<void>;\n handlePaste: () => Promise<void>;\n handleSelectAll: () => void;\n handleUndo: () => void;\n handleRedo: () => void;\n handleCompareClipboard: () => Promise<void>;\n handleSetCompareBase: () => void;\n handleCompareWithBase: () => Promise<void>;\n handleShortcutExecute: (\n entry: Extract<AgentMenuEntry, { entryType: \"agent_shortcut\" }>,\n ) => Promise<void>;\n handleBoundAgentExecute: (entry: SurfaceBoundAgentEntry) => Promise<void>;\n handleContentBlockInsert: (\n entry: Extract<AgentMenuEntry, { entryType: \"content_block\" }>,\n ) => void;\n handleEntrySelect: (entry: AgentMenuEntry) => void;\n handleDelete: () => Promise<void>;\n handleFind: () => void;\n /** \"Insert reference…\" (editable) / \"Copy reference…\" (read-only). */\n handleInsertReference: () => void;\n handleAttach: () => void;\n /** \"Link a record…\" — the ONE record picker pointed at this menu's entity. */\n handleLinkRecord: () => void;\n handleShare: () => void;\n handleInspectValues: () => void;\n handleInspectState: () => void;\n handleToggleDebugMode: () => void;\n handleToggleAdminIndicator: () => void;\n // Quick actions (pass-through so renderers need no extra hook)\n quickActions: ContextMenuQuickActions;\n /**\n * The page's surface, as ONE nested menu entry titled with the surface's\n * display label (e.g. \"Notes\", \"Marketing Site Workspace\") — the same data\n * and actions the shell-header Agents button offers: location, Surface\n * Context, Surface Context Admin, the bound agents + bind action, related\n * surfaces, and (admins) the menu revision. Shaped as an extra section so\n * BOTH renderers draw it with their existing extra-item code.\n */\n surfaceSection: ContextMenuExtraSection;\n}\n\nexport function useContextMenuActions(\n props: Omit<MenuContentProps, \"variant\">,\n): ContextMenuActions {\n const {\n sourceFeature,\n surfaceName,\n menuVersion,\n getApplicationScope,\n contextData,\n selectedText,\n selectionRange,\n fallbackContent,\n placementMode,\n scope: shortcutScope,\n scopeId,\n extraSections,\n isEditable,\n editorId,\n getTextarea,\n insertAtCaret,\n onContentInserted,\n onTextReplace,\n onTextInsertAfter,\n onDelete,\n onUndo,\n onRedo,\n widgetHandleId,\n } = props;\n\n const dispatch = useAppDispatch();\n const store = useAppStore();\n const host = useContextMenuHostBindings();\n const entity = props.entity;\n\n const resolvedPlacementMode: Record<PlacementKey, PlacementVisibility> = {\n ...DEFAULT_PLACEMENT_MODE,\n ...(placementMode ?? {}),\n };\n const dbPlacementTypes = ALL_DB_PLACEMENTS.filter(\n (p) => resolvedPlacementMode[p] !== \"hide\",\n );\n\n /**\n * THE AMBIENT READS (Phase 1, 2026-08-25). The menu launches agent runs but\n * never asked what was already true around the user: which organization is\n * active and which scopes/context they have selected. Collected here — the\n * engine is the one place with Redux — and landed on a single `ambient`\n * scope key by `resolveApplicationScope`, so an agent launched from a\n * right-click stops asking what the app already knows. ONE key, weakest\n * layer, never overrides a surface (see the baseline manifest's\n * `AMBIENT_VALUES`).\n *\n * NOT here: \"is a run currently streaming\". Every streaming selector in the\n * execution system is conversation-scoped (`selectIsStreaming(conversationId)`)\n * and the menu has no conversation. A global one is worth adding, but it is a\n * new memoized selector over `activeRequests` that runs on every menu open —\n * a deliberate change, not a thing to invent in passing.\n */\n const activeOrganizationId = useAppSelector(selectOrganizationId);\n const activeScopeIds = host.activeScopeIds;\n\n // Assemble the scope the menu acts on. Stable for this open (the shell\n // captured selection before mount), so computing it in render is cheap.\n //\n // 🚨 ORDER MATTERS (Phase 6.7). This runs BEFORE the menu hook because\n // availability is now DERIVED from it: the scope's KEYS are half of what\n // this surface can read, and an item is offered iff every key it consumes\n // is readable here.\n const scope = resolveApplicationScope({\n getApplicationScope,\n contextData,\n selectedText,\n selectionRange,\n fallbackContent,\n surfaceName,\n ambient: {\n active_organization_id: activeOrganizationId ?? null,\n active_scope_ids: activeScopeIds ?? [],\n surface_name: surfaceName ?? null,\n },\n });\n\n /**\n * WHAT THIS SURFACE CAN READ — the derived gate's input (THE-MODEL law 3).\n *\n * Three sources, unioned, all of them KEY-existence and never\n * value-population:\n * 1. the baseline floor — the 5 generic values every menu resolves;\n * 2. the manifest's DECLARED values (inheritance already merged by the\n * registry) — a surface that declares `raw_transcript_text` offers the\n * transcript items before a single word has been recorded;\n * 3. the keys that actually landed in the resolved scope — the honest\n * catch for surfaces emitting values they never declared (\"Undeclared\n * (runtime only)\" in the Surface Context window). A read path is a read\n * path whether or not anyone wrote it down.\n */\n const scopeKeySignature = Object.keys(scope).sort().join(\"|\");\n const availableKeys = useMemo(\n () =>\n buildAvailableKeys(compact({\n baselineValueNames: BASELINE_VALUE_NAMES,\n declaredValueNames: surfaceName\n ? getContextMenuHost().getSurfaceManifest(surfaceName)?.values.map((v) => v.name)\n : undefined,\n // `scope` is rebuilt every render but its KEY SET is stable for an\n // open, so the signature — not the object — is the dependency.\n runtimeScopeKeys: scopeKeySignature.split(\"|\").filter(Boolean),\n })),\n [surfaceName, scopeKeySignature],\n );\n\n /**\n * THE EXCLUSION VALVE (#43) — the surface's `menu` config namespace, the\n * one sanctioned override of derived availability. Resolved through the\n * existing layered surface-config cache (global → org → scope → user), so\n * it costs no extra fetch: the menu already loads this bundle for agent\n * roles.\n */\n const { getNamespace: getSurfaceNamespace } = useSurfaceConfig(\n surfaceName ?? \"matrx-unregistered/context-menu\",\n );\n const menuSurfaceConfig = getSurfaceNamespace<MenuConfig>(\"menu\");\n const excludedItemIds = useMemo(\n () => new Set(menuSurfaceConfig?.excludedItemIds ?? []),\n [menuSurfaceConfig],\n );\n\n const {\n categoryGroups,\n loading,\n error: librariesError,\n refresh,\n } = useUnifiedAgentContextMenu({\n placementTypes: dbPlacementTypes,\n surfaceName,\n availableKeys,\n hasSelection: String(scope.selection ?? \"\").trim().length > 0,\n excludedItemIds,\n enabled: dbPlacementTypes.length > 0,\n scope: shortcutScope,\n scopeId,\n });\n\n const {\n sections: boundAgentSections,\n loading: boundAgentsFetching,\n settled: boundAgentsSettled,\n error: boundAgentsError,\n refresh: refreshBoundAgents,\n } = useSurfaceBoundAgents(surfaceName, { isEditable });\n // Loading until the rows are in hand — an unsettled empty list is never\n // \"no agents\" (round 6: the first open dropped the library).\n const boundAgentsLoading = boundAgentsFetching || !boundAgentsSettled;\n\n const { launchShortcut, launchAgent } = useAgentLauncher();\n const { speak } = useSpeech({ processMarkdown: true, label: \"Selected text\" });\n const { roles: surfaceAgentRoles } = useSurfaceAgentRoles(\n surfaceName ?? \"matrx-unregistered/context-menu\",\n );\n const {\n openListenSummaryWindow,\n quickActions,\n openDiffWindow,\n openFindReplace,\n openReferencePicker,\n openContextAssignment,\n openLinkRecordSheet,\n openShareModalWindow,\n openStateViewer,\n openSurfaceInspector,\n hasCompareBase,\n } = host;\n const currentUserId = useAppSelector(selectUserId);\n // Admin DEBUGGING (every page): the surface key, the menu version and the\n // Admin Tools submenu (debug mode, context values, Redux state, admin\n // indicator) — this browser's own diagnostics, no data. Admin POWER (the\n // Surface Context Admin editor, admin rich-document actions) stays on the\n // lane-gated selector, false outside /administration.\n const isAdmin = host.isAdminDebugger;\n const hasAdminPower = host.hasAdminPower;\n const organizationId = useAppSelector(selectOrganizationId);\n const isDebugMode = host.isDebugMode;\n const isAdminIndicatorOpen = host.isAdminIndicatorOpen;\n\n // The single, deduped fetch — fires on the renderer's mount (= on open).\n // Both the unified-menu thunk and the bound-agents service dedupe, so reopen\n // never refetches. A double fetch is structurally impossible.\n useEffect(() => {\n void refresh();\n // Default-contract agents (matrx-default/*) apply even with no surfaceName,\n // so always fetch — a bare/undeclared surface still gets its default agents.\n void refreshBoundAgents();\n }, []);\n\n const actionText = resolveActionText(scope);\n\n // Rich-document action context — reuses the canonical copy / export / convert\n // handlers (NOT a fork). Only populated when there is content to act on, so\n // the submenus self-hide on an inert menu.\n const richDocSource: ContentSource = props.contentSource ?? { type: \"raw\" };\n const richDocAdapter = getSourceAdapter(richDocSource.type);\n // Raw sources fold a content hash into the instance prefix so two raw\n // documents on one page never share overlay instance IDs (parity with\n // useActionSurfaceProvider's ctx builder).\n const richPrefix =\n richDocSource.type === \"raw\"\n ? `${richDocAdapter.instanceKeyPrefix(richDocSource)}-${shortHash(actionText.text).slice(0, 8)}`\n : richDocAdapter.instanceKeyPrefix(richDocSource);\n const richDocCtx: RichDocumentActionContext = {\n content: actionText.text,\n source: richDocSource,\n metadata: null,\n dispatch,\n getState: store.getState as unknown as () => Record<string, unknown>,\n organizationId,\n isAuthenticated: Boolean(currentUserId),\n isAdmin: hasAdminPower,\n isCreator: false,\n surfaceKey: surfaceName ?? null,\n onClose: () => {},\n instanceKey: (prefix) => `${richPrefix}-${prefix}`,\n sourceAdapter: richDocAdapter,\n applicationScope: scope,\n ...(props.richDocCtxExtras ?? {}),\n };\n // ONE AI SET FOR A RECORD IN EVERY VIEW (page-pass /notes, 2026-09-28): Read\n // (RichDocument) supplied Clean up / Help with this…, the editor views did\n // not. An editor menu over a source that can be saved gets the shell's\n // review-and-apply dialog unless its host already supplied one.\n const textAgentCallbacks = editorTextAgentCallbacks(richDocCtx.callbacks, richDocSource, richDocAdapter, props.requestTextAgentReview);\n if (textAgentCallbacks) richDocCtx.callbacks = textAgentCallbacks;\n // Never an action that targets the place this menu already is (Save to\n // Notes inside a note, Edit inside the editor). The SAME list goes to the\n // click target (AlchemyMenuContent), because the rich-document provider\n // draws its rows from the target, not from this resolve — live on\n // /notes/<id> the preview still showed them while only this list had them.\n const excludedRichActionIds = [\n ...(props.excludedRichActions ?? []),\n ...actionsAlreadyHere({\n sourceType: richDocSource.type,\n surfaceName,\n isEditable: Boolean(isEditable),\n }),\n ];\n const richActions =\n actionText.source !== \"none\"\n ? resolveActions(richDocCtx, compact({\n exclude: excludedRichActionIds,\n extra: props.extraRichActions,\n }))\n : [];\n // The ONE registry tree's actions (the same selector every menu host uses).\n const registryActions = registryMenuActions(richActions);\n const copyVariantActions = richActions.filter((a) => a.category === \"copy\");\n const exportActions = richActions.filter(\n (a) => a.category === \"export\" || a.id === \"save-as-file\",\n );\n const convertActions = richActions.filter(\n (a) => a.category === \"save\" && a.id !== \"save-as-file\",\n );\n\n // Loud guards — dev-only scream for inert menus + value-mapping gaps.\n useEffect(() => {\n reportMenuDiagnostics({\n surfaceName,\n scope,\n isEditable,\n hasExtraSections: Boolean(extraSections && extraSections.length > 0),\n });\n }, []);\n\n // ── Clipboard ─────────────────────────────────────────────────────────────\n const handleCopy = async () => {\n if (!actionText.text) return;\n // Blocked clipboard (embedded browser, permission policy): never a\n // silent swallow — hand the text over for a manual Cmd/Ctrl+C.\n if (!(await kitCopyText(actionText.text))) {\n getContextMenuHost().showManualCopy({ text: actionText.text });\n }\n };\n\n const handleSpeak = () => {\n if (actionText.text.trim()) speak(actionText.text);\n };\n\n // Listening is universal: every menu has content or a selection. A surface\n // that declares its own `spoken_summary` role wins; every other surface\n // falls back to the platform home role (mandate-backed, so it resolves for\n // every user). The home fetch is cached once per session by\n // `ensureSurfaceConfig`'s single-flight.\n const { roles: listenHomeRoles } = useSurfaceAgentRoles(\n getContextMenuHost().listeningHomeSurface,\n );\n const spokenSummaryRole = surfaceAgentRoles.spoken_summary?.effectiveAgentId\n ? surfaceAgentRoles.spoken_summary\n : listenHomeRoles.spoken_summary;\n const spokenSummaryAgentId = spokenSummaryRole?.effectiveAgentId ?? null;\n // Both listening actions open the Listen panel (summary text + audio\n // transport in one place). The only difference is stream-to-stream autoplay.\n const openSpokenSummary = (autoPlay: boolean) => {\n if (!spokenSummaryAgentId || !actionText.text.trim()) return;\n // This click is the ONLY user gesture before speech starts (the audio\n // itself begins from a websocket callback) — unlock iOS/WebKit output now.\n primeAudioOutput();\n openListenSummaryWindow({\n agentId: spokenSummaryAgentId,\n agentName: spokenSummaryRole?.role.label ?? null,\n sourceText: actionText.text,\n style: \"Extremely Concise Summary\",\n autoPlay,\n });\n };\n const handleSpokenSummary = () => openSpokenSummary(false);\n const handleSpokenSummaryLive = () => openSpokenSummary(true);\n\n const handleCut = async () => {\n if (!selectionRange || selectionRange.type !== \"editable\") return;\n const element = selectionRange.element;\n if (\n !(element instanceof HTMLTextAreaElement) &&\n !(element instanceof HTMLInputElement)\n )\n return;\n const { start, end } = selectionRange;\n const cutText = element.value.substring(start, end);\n if (!(await kitCopyText(cutText))) {\n // The text was NOT cut (the splice below never runs) — offer it for a\n // manual copy instead of losing the gesture.\n getContextMenuHost().showManualCopy({ text: cutText, title: \"Copy manually (cut was blocked)\" });\n return;\n }\n if (onTextReplace) {\n onTextReplace(\n element.value.substring(0, start) + element.value.substring(end),\n );\n } else {\n spliceInputValue(element, start, end, \"\");\n }\n };\n\n const handlePaste = async () => {\n if (!isEditable || !selectionRange || selectionRange.type !== \"editable\")\n return;\n const element = selectionRange.element;\n if (\n !(element instanceof HTMLTextAreaElement) &&\n !(element instanceof HTMLInputElement)\n )\n return;\n const text = await kitReadText();\n if (text === null) {\n toast({ title: \"Couldn't read the clipboard\", variant: \"destructive\" });\n return;\n }\n const { start, end } = selectionRange;\n if (onTextReplace) {\n onTextReplace(\n element.value.substring(0, start) + text + element.value.substring(end),\n );\n } else {\n spliceInputValue(element, start, end, text);\n }\n };\n\n // ── JSON ──────────────────────────────────────────────────────────────────\n // Selection-aware: when the text the menu is acting on parses as JSON (with\n // or without a code fence), offer the JSON verbs. Detection + formatting are\n // the shared `lib/json-format` primitive, so this menu and the notes cleanup\n // pass produce byte-identical output. Read-only surfaces get the same verbs\n // as copies.\n const editableTarget =\n isEditable && selectionRange?.type === \"editable\"\n ? selectionRange.element\n : null;\n const canWriteJson =\n editableTarget instanceof HTMLTextAreaElement ||\n editableTarget instanceof HTMLInputElement;\n\n const jsonSection: JsonMenuSection | null = buildJsonMenuSection({\n text: actionText.text,\n canWrite: canWriteJson,\n onReplace: (next) => {\n if (!canWriteJson || !selectionRange || selectionRange.type !== \"editable\")\n return;\n const element = selectionRange.element;\n if (\n !(element instanceof HTMLTextAreaElement) &&\n !(element instanceof HTMLInputElement)\n )\n return;\n // A real selection is rewritten in place; with no selection the menu is\n // acting on the whole field, so the whole value is replaced.\n const { start, end } =\n actionText.source === \"selection\"\n ? selectionRange\n : { start: 0, end: element.value.length };\n if (onTextReplace) {\n onTextReplace(\n element.value.substring(0, start) + next + element.value.substring(end),\n );\n } else {\n spliceInputValue(element, start, end, next);\n }\n },\n onCopy: async (next) => {\n if (await kitCopyText(next)) {\n toast({ title: \"Copied\", description: \"JSON copied to clipboard.\" });\n } else {\n getContextMenuHost().showManualCopy({ text: next });\n }\n },\n });\n\n const handleSelectAll = () => {\n // No captured selection (the common right-click-without-selecting case):\n // an editable surface still knows its field — select all of it. A no-op\n // here is the \"fake menu\" class this feature exists to kill.\n if (!selectionRange) {\n const field = getTextarea?.();\n if (field) {\n requestAnimationFrame(() => {\n field.focus();\n field.select();\n });\n }\n return;\n }\n if (selectionRange.type === \"editable\") {\n const element = selectionRange.element;\n if (\n element instanceof HTMLTextAreaElement ||\n element instanceof HTMLInputElement\n ) {\n requestAnimationFrame(() => {\n element.focus();\n element.select();\n });\n }\n } else {\n const container = selectionRange.containerElement;\n if (!container) return;\n requestAnimationFrame(() => {\n try {\n const range = document.createRange();\n range.selectNodeContents(container);\n const selection = window.getSelection();\n if (selection) {\n selection.removeAllRanges();\n selection.addRange(range);\n }\n } catch {\n // best-effort\n }\n });\n }\n };\n\n // ── History ───────────────────────────────────────────────────────────────\n // Native per-field Undo/Redo. When the surface provides no richer history\n // (`onUndo`/`onRedo`), an editable field still gets the browser's built-in\n // undo stack — \"offer undo\" without standing up a history system. There is no\n // non-deprecated API to trigger a textarea's native undo, so `execCommand` is\n // the intentional (and only) mechanism here.\n const editableElement: HTMLTextAreaElement | HTMLInputElement | null =\n (() => {\n const fromRange =\n selectionRange?.type === \"editable\" ? selectionRange.element : null;\n const el = fromRange ?? getTextarea?.() ?? null;\n return el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement\n ? el\n : null;\n })();\n const canNativeUndo = Boolean(isEditable && editableElement);\n const runNativeEdit = (command: \"undo\" | \"redo\") => {\n if (!editableElement) return;\n editableElement.focus();\n try {\n document.execCommand(command);\n } catch (err) {\n console.error(`[ContextMenuV3] native ${command} failed`, err);\n }\n };\n const handleUndo = () => (onUndo ? onUndo() : runNativeEdit(\"undo\"));\n const handleRedo = () => (onRedo ? onRedo() : runNativeEdit(\"redo\"));\n\n // ── Compare — reuses the existing diff-viewer window + compare-base slice ──\n const compareContent = (): { content: string; label: string } =>\n actionText.source === \"selection\"\n ? { content: actionText.text, label: \"Selection\" }\n : { content: actionText.text, label: \"Current\" };\n\n const handleCompareClipboard = async () => {\n const { content, label } = compareContent();\n const clip = await kitReadText();\n if (clip === null) {\n toast({ title: \"Couldn't read the clipboard\", variant: \"destructive\" });\n return;\n }\n if (!clip) {\n toast({ title: \"Clipboard is empty\" });\n return;\n }\n // Current content is the baseline (old); the clipboard is the incoming\n // version the user is about to paste (new). Clipboard-only text => addition.\n openDiffWindow({\n original: content,\n modified: clip,\n originalLabel: label,\n modifiedLabel: \"Clipboard\",\n title: \"Compare with clipboard\",\n engine: \"light\",\n });\n };\n\n const handleSetCompareBase = () => {\n const { content, label } = compareContent();\n host.setCompareBase({ content, label, language: null });\n toast({\n title: \"Set as compare base\",\n description: \"Open another item and choose “Compare with base”.\",\n });\n };\n\n const handleCompareWithBase = async () => {\n const { content, label } = compareContent();\n const opened = await host.openCompareWithBase({ current: content, currentLabel: label });\n if (!opened) {\n toast({\n title: \"No compare base set\",\n description: \"Choose “Set as compare base” on another item first.\",\n });\n }\n };\n\n // ── Launch (AI actions / bound agents / content blocks) ──────────────────\n // Per-launch widget handle: the surface's widget_* methods plus a selection\n // write-back, so the result's action bar offers Replace / Insert below in\n // place of the text this run was launched on (any display mode).\n const launchWidgetHandleId = (): string | undefined =>\n registerLaunchWidgetHandle(\n widgetHandleId,\n isEditable\n ? buildSelectionWriteBack({\n originalText: actionText.text,\n textSource: actionText.source,\n selectionRange,\n onTextReplace,\n onTextInsertAfter,\n insertAtCaret,\n })\n : null,\n );\n\n const handleShortcutExecute = async (\n entry: Extract<AgentMenuEntry, { entryType: \"agent_shortcut\" }>,\n ) => {\n if (!entry.agentId) {\n toast({\n title: \"Agent Not Connected\",\n description: `\"${entry.label}\" has no connected agent. Configure it in the admin panel.`,\n variant: \"destructive\",\n });\n return;\n }\n const resultDisplay = (entry.displayMode ??\n \"modal-full\") as ResultDisplayMode;\n try {\n await launchShortcut(entry.id, scope, {\n surfaceKey: `${sourceFeature}:${entry.id}`,\n sourceFeature,\n config: { displayMode: resultDisplay },\n runtime: compact({\n originalText: actionText.text,\n surfaceName,\n // Editable surfaces: let the agent stream widget_text_* edits\n // straight into the surface, and give the person Replace / Insert\n // below on the result (undefined on read-only — no tools).\n widgetHandleId: launchWidgetHandleId(),\n }),\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"An unknown error occurred\";\n toast({\n title: \"Execution Failed\",\n description: `${entry.label}: ${message}`,\n variant: \"destructive\",\n });\n }\n };\n\n const handleBoundAgentExecute = async (entry: SurfaceBoundAgentEntry) => {\n try {\n await launchAgent(entry.agentId, {\n surfaceKey: `${sourceFeature}:bound-agent:${entry.agentId}`,\n sourceFeature,\n // Managed entries share the WindowPanel default. autoRun is deliberately\n // absent so the safe open-and-wait default remains authoritative.\n config: MANAGED_CONTEXT_MENU_AGENT_CONFIG,\n runtime: compact({\n applicationScope: scope,\n originalText: actionText.text,\n surfaceName,\n widgetHandleId: launchWidgetHandleId(),\n }),\n });\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"An unknown error occurred\";\n toast({\n title: \"Execution Failed\",\n description: `${entry.name}: ${message}`,\n variant: \"destructive\",\n });\n }\n };\n\n // Every insert the menu makes goes through ONE function; which target is\n // live (editor id, textarea, rich caret) is decided when the insert runs.\n const insertTargets = { editorId, getTextarea, insertAtCaret, onTextReplace };\n\n const handleContentBlockInsert = (\n entry: Extract<AgentMenuEntry, { entryType: \"content_block\" }>,\n ) => {\n void (async () => {\n // The menu payload carries no template; the first insert fetches it by id.\n let template = entry.template;\n if (!template) {\n try {\n template = await ensureBlockTemplate(entry.id, dispatch);\n } catch (error) {\n toast({\n title: \"Couldn't load that block\",\n description: error instanceof Error ? error.message : \"Try again.\",\n variant: \"destructive\",\n });\n return;\n }\n }\n // The template passes through VERBATIM — placeholders like {{variable}}\n // must reach the editor/clipboard unmangled.\n if (insertIntoEditor(insertTargets, template)) {\n onContentInserted?.();\n return;\n }\n // Read-only surface (or the insert target vanished): never a silent no-op —\n // copy the block so the gesture still yields the text.\n const copied = await kitCopyText(template);\n if (copied) {\n toast({\n title: \"Copied to clipboard\",\n description: `\"${entry.label}\" can't be inserted here — this surface isn't editable.`,\n });\n } else {\n getContextMenuHost().showManualCopy({ text: template });\n }\n })();\n };\n\n const handleEntrySelect = (entry: AgentMenuEntry) => {\n if (entry.entryType === \"agent_shortcut\") void handleShortcutExecute(entry);\n else handleContentBlockInsert(entry);\n };\n\n // ── Editable Save / Delete — Delete always via ConfirmDialog ─────────────\n const handleDelete = async () => {\n if (!onDelete) return;\n const ok = await getContextMenuHost().confirm({\n title: \"Delete this item?\",\n description: \"This action cannot be undone.\",\n confirmLabel: \"Delete\",\n variant: \"destructive\",\n });\n if (ok) onDelete();\n };\n\n // ── Overlay-opening actions ───────────────────────────────────────────────\n // Find & Replace carries the live target element + onReplace through the\n // callback registry (never Redux). Suppress the shell's selection-restore so\n // the modal keeps focus after the menu closes (applies on BOTH renderers —\n // the mobile sheet closes into the same overlay).\n const handleFind = () => {\n props.suppressSelectionRestore();\n openFindReplace({\n getTargetElement: () =>\n selectionRange?.type === \"editable\" ? selectionRange.element : null,\n onReplace: onTextReplace,\n });\n };\n\n // Add a reference: the picker returns the canonical minified fence; an\n // editable surface gets it inserted at the caret on its own paragraph, any\n // other surface (or a \"Copy\" choice inside the picker) gets it on the\n // clipboard — never a silent no-op.\n const canInsertReference = isEditable && hasEditorInsertTarget(insertTargets);\n const copyReference = (pick: ReferencePick) => {\n void kitCopyText(pick.fence).then((copied) => {\n if (copied) {\n toast({\n title: \"Reference copied\",\n description: pick.title\n ? `Paste it anywhere to link \"${pick.title}\".`\n : \"Paste it anywhere to render the link.\",\n });\n } else {\n getContextMenuHost().showManualCopy({ text: pick.fence, title: \"Copy the reference\" });\n }\n });\n };\n const insertReference = (pick: ReferencePick) => {\n // Own paragraph everywhere: a fence must start and end on its own line.\n // A block: it lands at the end of the caret's line, never inside a word.\n const inserted = insertIntoEditor(insertTargets, {\n editor: `\\n${pick.fence}\\n`,\n textarea: (field) => ownParagraph(pick.fence, field),\n caret: pick.fence,\n placement: \"block\",\n });\n if (inserted) {\n onContentInserted?.();\n return;\n }\n toast({\n title: \"Copied instead\",\n description: \"Couldn't insert here. Paste it where you want it.\",\n });\n copyReference(pick);\n };\n const handleInsertReference = () => {\n props.suppressSelectionRestore();\n openReferencePicker({\n mode: canInsertReference ? \"insert\" : \"copy\",\n onPicked: (pick) => {\n if (pick.delivery === \"insert\" && canInsertReference) insertReference(pick);\n else copyReference(pick);\n },\n });\n };\n\n const handleAttach = () => {\n if (!entity) return;\n openContextAssignment({\n subject: {\n entityType: entity.type,\n entityId: entity.id,\n title: entity.title,\n },\n });\n };\n\n const handleLinkRecord = () => {\n if (!entity) return;\n openLinkRecordSheet({ target: { token: entity.type, id: entity.id, title: entity.title } });\n };\n\n const handleShare = () => {\n if (!entity?.resourceType) return;\n openShareModalWindow({\n resourceType: entity.resourceType,\n resourceId: entity.id,\n resourceName: entity.title,\n });\n };\n\n // ── Admin ─────────────────────────────────────────────────────────────────\n // ── Surface submenu (mirrors the shell-header Agents panel) ────────────────\n const { openSurfaceContextWindow, openBindAgent } = host;\n const resolvedSurfaceName = surfaceName ?? detectActiveSurface();\n const surfaceLabel = resolvedSurfaceName\n ? getSurfaceDisplayLabel(resolvedSurfaceName)\n : \"This page\";\n const related = getRelatedSurfaces(resolvedSurfaceName);\n const copyText = (text: string) => {\n void kitCopyText(text).then((copied) => {\n if (copied) toast({ title: \"Copied\", description: text });\n else getContextMenuHost().showManualCopy({ text });\n });\n };\n const surfaceAgentItems: ContextMenuExtraItem[] = [];\n boundAgentSections\n .filter((section) => section.agents.length > 0)\n .forEach((section, idx) => {\n if (idx > 0)\n surfaceAgentItems.push({ kind: \"separator\", id: `sa:${section.key}:sep` });\n for (const agent of section.agents) {\n surfaceAgentItems.push({\n kind: \"item\",\n id: `sa:${section.key}:${agent.agentId}`,\n label: agent.name,\n description: agent.organizationName ? `${section.label} · ${agent.organizationName}` : section.label,\n icon: AGENT_ICON,\n onSelect: () => void handleBoundAgentExecute(agent),\n });\n }\n });\n if (surfaceAgentItems.length > 0)\n surfaceAgentItems.push({ kind: \"separator\", id: \"sa:bind-sep\" });\n surfaceAgentItems.push({\n kind: \"item\",\n id: \"sa:bind\",\n label: \"Bind an agent to this page…\",\n icon: Plus,\n disabled: !resolvedSurfaceName,\n onSelect: () => {\n if (resolvedSurfaceName) openBindAgent({ surfaceName: resolvedSurfaceName });\n },\n });\n const relatedItems: ContextMenuExtraItem[] = [\n ...related.ancestry,\n ...related.children,\n ].map((ref) => ({\n kind: \"item\",\n id: `sr:${ref.name}`,\n label: ref.label,\n description: `${ref.kind === \"ancestor\" ? \"Parent\" : \"Child\"} · ${ref.name}`,\n icon: AppWindow,\n onSelect: () =>\n openSurfaceContextWindow({ surfaceName: ref.name, isEditable: false }),\n }));\n const surfaceChildren: ContextMenuExtraItem[] = [\n // The surface key is an engineer's handle — admin-only (cold walk 20),\n // like the Agents panel's sub-line. Everyone else sees the label only.\n ...(isAdmin\n ? ([\n {\n kind: \"item\",\n id: \"surface:location\",\n label: resolvedSurfaceName ?? \"No surface registered for this page\",\n ...(resolvedSurfaceName ? { description: \"Surface location · click to copy\" } : {}),\n icon: ClipboardCopy,\n disabled: !resolvedSurfaceName,\n onSelect: () => resolvedSurfaceName && copyText(resolvedSurfaceName),\n },\n { kind: \"separator\", id: \"surface:sep1\" },\n ] satisfies ContextMenuExtraItem[])\n : []),\n {\n kind: \"item\",\n id: \"surface:context\",\n label: \"Surface Context\",\n description: \"Live page values\",\n icon: Braces,\n disabled: !resolvedSurfaceName,\n onSelect: () => {\n if (!resolvedSurfaceName) return;\n openSurfaceContextWindow({\n surfaceName: resolvedSurfaceName,\n isEditable: Boolean(isEditable),\n });\n },\n },\n ...(hasAdminPower\n ? ([\n {\n kind: \"item\",\n id: \"surface:context-admin\",\n label: \"Surface Context Admin\",\n description: \"Contract, provenance & settings\",\n icon: ShieldCheck,\n onSelect: () =>\n openSurfaceInspector({\n surfaceName: resolvedSurfaceName ?? null,\n scope,\n isEditable: Boolean(isEditable),\n preferRuntime: true,\n }),\n },\n ] satisfies ContextMenuExtraItem[])\n : []),\n { kind: \"separator\", id: \"surface:sep2\" },\n {\n kind: \"submenu\",\n id: \"surface:agents\",\n label: \"Agents on this page\",\n icon: AGENT_ICON,\n children: surfaceAgentItems,\n },\n ...(relatedItems.length > 0\n ? ([\n {\n kind: \"submenu\",\n id: \"surface:related\",\n label: \"Related surfaces\",\n icon: Layers,\n children: relatedItems,\n },\n ] satisfies ContextMenuExtraItem[])\n : []),\n ...(isAdmin\n ? ([\n { kind: \"separator\", id: \"surface:sep3\" },\n {\n kind: \"item\",\n id: \"surface:version\",\n label: `Menu v3.${CANONICAL_MENU_VERSION_V3} · V${menuVersion}`,\n description: \"Menu revision · surface wiring version\",\n icon: Info,\n onSelect: () =>\n copyText(\n `${resolvedSurfaceName ?? \"(no surface)\"} · v3.${CANONICAL_MENU_VERSION_V3} · V${menuVersion}`,\n ),\n },\n ] satisfies ContextMenuExtraItem[])\n : []),\n ];\n const surfaceSection: ContextMenuExtraSection = {\n id: \"surface-info\",\n items: [\n {\n kind: \"submenu\",\n id: \"surface\",\n label: surfaceLabel,\n icon: AppWindow,\n children: surfaceChildren,\n },\n ],\n };\n\n const handleInspectValues = () => {\n openSurfaceInspector({\n surfaceName: surfaceName ?? null,\n scope,\n isEditable: Boolean(isEditable),\n });\n };\n const handleInspectState = () => {\n openStateViewer();\n };\n const handleToggleDebugMode = () => {\n host.toggleDebugMode();\n };\n const handleToggleAdminIndicator = () => {\n host.toggleAdminIndicator();\n };\n\n return {\n scope,\n actionText,\n // A textarea/input, or a contenteditable field (chip / rich editor), names\n // itself in the header — never \"Content: {{reply.body}}\".\n fieldLabel: isEditable && selectionRange ? fieldLabelOf(selectionRange.element) : null,\n jsonSection,\n resolvedPlacementMode,\n categoryGroups,\n grouped: groupsByPlacement(categoryGroups),\n loading,\n librariesError,\n boundAgentSections,\n boundAgentsLoading,\n boundAgentsError,\n excludedRichActionIds,\n retryLibraries: () => {\n void refresh();\n void refreshBoundAgents();\n },\n richDocCtx,\n registryActions,\n copyVariantActions,\n exportActions,\n convertActions,\n hasCompareBase,\n isAdmin,\n isDebugMode,\n isAdminIndicatorOpen,\n canNativeUndo,\n handleCopy,\n handleSpeak,\n handleSpokenSummary,\n handleSpokenSummaryLive,\n spokenSummaryAvailable: Boolean(spokenSummaryAgentId),\n handleCut,\n handlePaste,\n handleSelectAll,\n handleUndo,\n handleRedo,\n handleCompareClipboard,\n handleSetCompareBase,\n handleCompareWithBase,\n handleShortcutExecute,\n handleBoundAgentExecute,\n handleContentBlockInsert,\n handleEntrySelect,\n handleDelete,\n handleFind,\n handleInsertReference,\n handleAttach,\n handleLinkRecord,\n handleShare,\n handleInspectValues,\n handleInspectState,\n handleToggleDebugMode,\n handleToggleAdminIndicator,\n quickActions,\n surfaceSection,\n };\n}\n","/**\n * The menu's `toast({ title, description, variant })` call shape, delivered\n * through the chat package's notify port (`@ai-matrx/chat/host/notify`) —\n * the one notice stack the host already wires for agent runs.\n *\n * `destructive` → error; `success` / `warning` / `info` → the same; anything\n * else → a plain notice. A toast with only a description reads that\n * description as its message.\n */\nimport { toast as notify } from \"@ai-matrx/chat/host/notify\";\n\nexport interface MenuToast {\n title?: string | undefined;\n description?: string | undefined;\n variant?: \"destructive\" | \"success\" | \"warning\" | \"info\" | \"default\" | undefined;\n duration?: number | undefined;\n}\n\nexport function toast(props: MenuToast): void {\n const { title, description, variant, duration } = props;\n const message = title ?? description ?? \"\";\n const options = {\n ...(title !== undefined && description !== undefined ? { description } : {}),\n ...(duration !== undefined ? { duration } : {}),\n };\n if (variant === \"destructive\") notify.error(message, options);\n else if (variant === \"success\") notify.success(message, options);\n else if (variant === \"warning\") notify.warning(message, options);\n else if (variant === \"info\") notify.info(message, options);\n else notify.message(message, options);\n}\n","// The context menu's AI payload no longer carries each content block's\n// template (the menu draws labels only). The text is fetched by id the first\n// time a block is inserted, then cached on the block's store row so every\n// later insert is instant.\nimport { supabase } from \"@ai-matrx/chat/host/db\";\nimport { sklActions } from \"@ai-matrx/chat/host/ui-slots\";\n\ntype Dispatch = (action: unknown) => unknown;\n\nexport async function ensureBlockTemplate(\n blockId: string,\n dispatch: Dispatch,\n): Promise<string> {\n const { data, error } = await supabase\n .schema(\"skill\")\n .from(\"render_definition\")\n .select(\"template\")\n .eq(\"id\", blockId)\n .single();\n if (error) throw new Error(error.message);\n const template = (data as { template: string | null } | null)?.template ?? \"\";\n dispatch(sklActions.renderDefinitionsMerged!([{ id: blockId, template }]));\n return template;\n}\n","// features/context-menu-v3/model/menu-model.ts\n//\n// The MENU MODEL — a declarative tree of everything the desktop menu can show,\n// built ONCE from the shared engine (`useContextMenuActions`) and the props.\n// The renderer never decides what exists; it only decides how the model is\n// ARRANGED (`layouts.ts`) and how dense it is drawn.\n//\n// Why a model: the menu had grown to ~30 top-level rows rendered by hand in\n// JSX. Every \"make it less overwhelming\" idea (fold the tail into submenus,\n// an icon strip for the universal verbs, a type-to-filter box) needs to walk\n// the same set of nodes. A model makes each of those a pure function over\n// one tree instead of a second renderer — and keeps the engine as the ONLY\n// place behavior lives.\n//\n// Nodes are closures over the engine's handlers, so a node is directly\n// actionable wherever a layout puts it (top level, inside a fold, in a\n// filtered flat list).\n\nimport { hasEditInPlaceTarget, openEditInPlaceFromLastInteraction } from \"@ai-matrx/rich-editor/in-place/EditInPlace\";\nimport { parseKeyCombo } from \"../utils/key-combo\";\nimport type React from \"react\";\nimport {\n StickyNote,\n CheckSquare,\n MessageSquare,\n Database,\n FolderOpen,\n Zap,\n Scissors,\n Pencil,\n Copy,\n Clipboard,\n Type,\n Undo2,\n Redo2,\n History,\n GitCompareArrows,\n Pin,\n Shield,\n Eye,\n EyeOff,\n Save,\n Trash2,\n Mic,\n Download,\n Replace,\n Search,\n Share2,\n Link,\n Link2,\n AtSign,\n Bug,\n Braces,\n Volume2,\n Headphones,\n AudioLines,\n RotateCcw,\n} from \"lucide-react\";\nimport type { IconComponentType } from \"@ai-matrx/icons\";\nimport { PLACEMENT_TYPES } from \"@ai-matrx/chat/agents/shortcuts/constants\";\nimport type { RichDocumentAction } from \"@ai-matrx/rich-content/rich-document/types\";\nimport {\n AI_SUBMENU_LABEL,\n buildMenuTree,\n withAiSlot,\n} from \"@ai-matrx/rich-content/rich-document/variants/shared/menuStructure\";\nimport type { AgentMenuCategoryGroup } from \"../hooks/useUnifiedAgentContextMenu\";\nimport { jsonSectionLabel } from \"../utils/json-menu-actions\";\nimport {\n type ContextMenuActions,\n getPlacementIcon,\n getPlacementLabel,\n resolveIcon,\n resolveRichActionView,\n PLACEMENT_COLOR,\n} from \"../hooks/useContextMenuActions\";\nimport type {\n MenuContentProps,\n PlacementKey,\n ExtraSectionAnchor,\n ContextMenuExtraItem,\n ContextMenuExtraSection,\n} from \"../types\";\nimport { AGENT_ICON } from \"@ai-matrx/icons/domain\";\n\n// ---------------------------------------------------------------------------\n// Node types (internal — the public surface contract stays `ContextMenuExtraItem`).\n// ---------------------------------------------------------------------------\n\ninterface MenuNodeBase {\n id: string;\n}\n\n/**\n * \"Edit\" for rendered content that edits in place (Arman, 2026-10-08: never a double-click — the\n * Edit icon or this right-click row opens it; the caret lands where he right-clicked).\n */\nfunction withEditInPlace(sections: MenuSection[]): MenuSection[] {\n if (!hasEditInPlaceTarget()) return sections;\n const edit: MenuItemNode = {\n kind: \"item\",\n id: \"edit-in-place\",\n label: \"Edit\",\n icon: Pencil,\n iconClassName: \"text-muted-foreground\",\n onSelect: () => void openEditInPlaceFromLastInteraction(),\n };\n return [{ id: \"edit-in-place\", group: \"editable\", nodes: [edit] }, ...sections];\n}\n\nexport interface MenuItemNode extends MenuNodeBase {\n kind: \"item\";\n label: string;\n icon?: IconComponentType | undefined;\n /** Tailwind classes for the icon (colour). */\n iconClassName?: string | undefined;\n /** Inline style for the icon (data-driven category colours). */\n iconStyle?: React.CSSProperties | undefined;\n /** Second muted line. */\n description?: string | undefined;\n /** Right-aligned muted hint (shortcut, count, …). */\n hint?: string | undefined;\n /** Native tooltip. */\n title?: string | undefined;\n disabled?: boolean | undefined;\n destructive?: boolean | undefined;\n /** Extra classes on the row (admin amber, …). */\n className?: string | undefined;\n /** Running it reloads this menu's own rows (Retry): the menu stays open (alchemy `keepsMenuOpen`). */\n keepsMenuOpen?: boolean | undefined;\n onSelect: () => void;\n}\n\nexport interface MenuSubmenuNode extends MenuNodeBase {\n kind: \"submenu\";\n label: string;\n icon?: IconComponentType | undefined;\n iconClassName?: string | undefined;\n iconStyle?: React.CSSProperties | undefined;\n disabled?: boolean | undefined;\n /** Spinner in the trigger while the data loads. */\n loading?: boolean | undefined;\n /** Rendered centred when `children` has no actionable node. */\n emptyLabel?: string | undefined;\n /** Tailwind width class for the panel. Default `w-60`. */\n width?: string | undefined;\n /** Layout hint: which placement this submenu is (for re-grouping). */\n placement?: string | undefined;\n children: MenuNode[];\n}\n\nexport interface MenuCheckboxNode extends MenuNodeBase {\n kind: \"checkbox\";\n label: string;\n icon?: IconComponentType | undefined;\n description?: string | undefined;\n hint?: string | undefined;\n checked: boolean;\n disabled?: boolean | undefined;\n onCheckedChange: (next: boolean) => void;\n}\n\nexport interface MenuLinkNode extends MenuNodeBase {\n kind: \"link\";\n label: string;\n icon?: IconComponentType | undefined;\n description?: string | undefined;\n hint?: string | undefined;\n href: string;\n target?: string | undefined;\n disabled?: boolean | undefined;\n}\n\nexport interface MenuLabelNode extends MenuNodeBase {\n kind: \"label\";\n label: string;\n}\n\nexport interface MenuSeparatorNode extends MenuNodeBase {\n kind: \"separator\";\n}\n\nexport type MenuNode =\n | MenuItemNode\n | MenuSubmenuNode\n | MenuCheckboxNode\n | MenuLinkNode\n | MenuLabelNode\n | MenuSeparatorNode;\n\n/** Actionable leaf kinds (what a filter can match and run). */\nexport type MenuLeafNode = MenuItemNode | MenuCheckboxNode | MenuLinkNode;\n\n/**\n * Coarse group a section belongs to. Layouts regroup by this, never by id\n * string-matching, so a surface's `extraSections` (\"surface\") and the\n * data-driven placements (\"ai\") stay distinguishable from the core verbs.\n */\nexport type MenuGroup =\n | \"clipboard\"\n | \"tools\"\n | \"history\"\n | \"share\"\n | \"document\"\n | \"surface\"\n | \"ai\"\n | \"quick\"\n | \"editable\"\n | \"admin\"\n | \"surface-info\";\n\nexport interface MenuSection {\n id: string;\n group: MenuGroup;\n /** Muted heading rendered above the section (surface sections). */\n label?: string | undefined;\n /** Icon for the fold a layout may collapse this section into (any className-drawn icon, like nodes). */\n icon?: IconComponentType | undefined;\n /** Classic rendering: no separator between this and the previous section. */\n joinPrevious?: boolean | undefined;\n /** The clicked target's section — see `ContextMenuExtraSection.primary`. */\n primary?: boolean | undefined;\n nodes: MenuNode[];\n}\n\nexport interface MenuHeader {\n label: \"Selected\" | \"Content\";\n text: string;\n}\n\nexport interface MenuModel {\n header: MenuHeader | null;\n /** Classic order — exactly the historical top-to-bottom arrangement. */\n sections: MenuSection[];\n /** Well-known nodes layouts pull out by role (never by label). */\n roles: MenuRoles;\n}\n\n/** The nodes a layout needs to address individually. */\nexport interface MenuRoles {\n copy: MenuItemNode;\n speak: MenuItemNode;\n /** ONE slot: \"Listen\" submenu — Summarize without playing / Summarize & listen. */\n listen: MenuSubmenuNode | null;\n copyAs: MenuSubmenuNode | null;\n json: MenuSubmenuNode | null;\n cut: MenuItemNode;\n paste: MenuItemNode;\n selectAll: MenuItemNode;\n find: MenuItemNode;\n /** \"Insert reference…\" (editable) / \"Copy reference…\" (read-only) — never absent. */\n insertReference: MenuItemNode;\n chat: MenuItemNode;\n undo: MenuItemNode;\n redo: MenuItemNode;\n viewHistory: MenuItemNode;\n compare: MenuSubmenuNode | null;\n exportMenu: MenuSubmenuNode | null;\n convert: MenuSubmenuNode | null;\n attach: MenuItemNode | null;\n /** \"Link a record…\" — present on EVERY menu that targets a record (guard: every-record-menu-links-a-record). */\n linkRecord: MenuItemNode | null;\n share: MenuItemNode | null;\n placements: MenuSubmenuNode[];\n /**\n * THE ONE REGISTRY TREE (RC-B6): the same tree, in the same order, as the\n * ⋯ menu, ProTextarea's \"…\" and the mobile sheet — with the agent-shortcut\n * libraries folded into its AI submenu. `null` when there is no content.\n */\n registry: MenuNode[] | null;\n quickActions: MenuSubmenuNode | null;\n save: MenuItemNode | null;\n del: MenuItemNode | null;\n admin: MenuSubmenuNode | null;\n /** The engine-built surface submenu section (location / context / agents / related). */\n surfaceInfo: MenuSection;\n /** Surface `extraSections`, by anchor, already converted to model nodes. */\n extras: Record<ExtraSectionAnchor, MenuSection[]>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Does the subtree contain anything the user can act on? */\nexport function hasActionable(nodes: MenuNode[]): boolean {\n return nodes.some((n) => {\n if (n.kind === \"separator\" || n.kind === \"label\") return false;\n if (n.kind === \"submenu\") return hasActionable(n.children);\n return true;\n });\n}\n\nfunction compactNodes(nodes: Array<MenuNode | null>): MenuNode[] {\n return nodes.filter((node) => node !== null);\n}\n\n// Surface ids are namespaced (`x:`) so they can never collide with a core\n// node id — notes' \"export\" item vs the core \"export\" submenu was a duplicate\n// React key the moment both rendered in one list.\nfunction fromExtraItem(item: ContextMenuExtraItem): MenuNode {\n const id = `x:${item.id}`;\n switch (item.kind) {\n case \"separator\":\n return { kind: \"separator\", id };\n case \"submenu\":\n return {\n kind: \"submenu\",\n id,\n label: item.label,\n icon: item.icon,\n disabled: item.disabled,\n children: item.children.map(fromExtraItem),\n };\n case \"checkbox\":\n return {\n kind: \"checkbox\",\n id,\n label: item.label,\n icon: item.icon,\n description: item.description,\n hint: item.hint,\n checked: item.checked,\n disabled: item.disabled,\n onCheckedChange: item.onCheckedChange,\n };\n case \"link\":\n return {\n kind: \"link\",\n id,\n label: item.label,\n icon: item.icon,\n description: item.description,\n hint: item.hint,\n href: item.href,\n target: item.target,\n disabled: item.disabled,\n };\n case \"item\":\n default:\n return {\n kind: \"item\",\n id,\n label: item.label,\n icon: item.icon,\n description: item.description,\n hint: item.hint,\n disabled: item.disabled,\n destructive: item.destructive,\n onSelect: item.onSelect,\n };\n }\n}\n\nfunction extrasByAnchor(\n extraSections: ContextMenuExtraSection[] | undefined,\n): Record<ExtraSectionAnchor, MenuSection[]> {\n const out: Record<ExtraSectionAnchor, MenuSection[]> = {\n \"after-clipboard\": [],\n \"after-compare\": [],\n \"after-placements\": [],\n };\n for (const s of extraSections ?? []) {\n const anchor = s.anchor ?? \"after-compare\";\n out[anchor].push({\n id: `extra:${s.id}`,\n group: \"surface\",\n label: s.label,\n icon: s.icon,\n primary: s.primary === true ? true : undefined,\n nodes: s.items.map(fromExtraItem),\n });\n }\n return out;\n}\n\n/**\n * Classic order, with the clicked target's section lifted to the very top.\n * Stable: everything else keeps the anchor order it was assembled in.\n */\nexport function liftPrimarySections(sections: MenuSection[]): MenuSection[] {\n const primary = sections.filter((s) => s.primary);\n if (primary.length === 0) return sections;\n return [...primary, ...sections.filter((s) => !s.primary)];\n}\n\nfunction richActionNode(\n action: RichDocumentAction,\n ctx: ContextMenuActions[\"richDocCtx\"],\n): MenuItemNode {\n const { label, disabled } = resolveRichActionView(action, ctx);\n return {\n kind: \"item\",\n id: `rich:${action.id}`,\n label,\n icon: action.icon,\n iconClassName: action.iconColor ?? undefined,\n disabled,\n onSelect: () => void action.run(ctx),\n };\n}\n\n/**\n * The registry tree as model nodes — `buildMenuTree` (the ONE structure every\n * host renders), with `aiExtras` (the agent-shortcut libraries) folded into\n * its AI submenu. Ids are the registry ids behind a `rich:` prefix, which is\n * what the one-tree guard compares against the ⋯ menu.\n */\nexport function registryTreeNodes(\n actions: RichDocumentAction[],\n ctx: ContextMenuActions[\"richDocCtx\"],\n aiExtras: MenuNode[],\n): MenuNode[] {\n const tree = buildMenuTree(actions, { aiSlot: aiExtras.length > 0 });\n const nodes: MenuNode[] = tree.topLevel.map((a) => richActionNode(a, ctx));\n for (const sub of withAiSlot(tree.submenus, aiExtras.length > 0)) {\n const children: MenuNode[] = sub.actions.map((a) => richActionNode(a, ctx));\n const isAi = sub.label === AI_SUBMENU_LABEL;\n if (isAi && aiExtras.length > 0) {\n if (children.length > 0) children.push({ kind: \"separator\", id: \"rich-ai:sep\" });\n children.push(...aiExtras);\n }\n nodes.push({\n kind: \"submenu\",\n id: `rich-sub:${sub.label.toLowerCase().replace(/[^a-z0-9]+/g, \"-\")}`,\n label: sub.label,\n icon: sub.icon,\n width: isAi ? \"w-64\" : undefined,\n children,\n });\n }\n nodes.push(...tree.extras.map((a) => richActionNode(a, ctx)));\n return nodes;\n}\n\nfunction categoryGroupNode(\n group: AgentMenuCategoryGroup,\n m: ContextMenuActions,\n): MenuSubmenuNode {\n const { category, items, children } = group;\n const children_: MenuNode[] = items.map((entry) => {\n const isDisabled = entry.entryType === \"agent_shortcut\" && !entry.agentId;\n const hint =\n // Advertised only when the shell's key listener can run it (key-combo.ts).\n entry.entryType === \"agent_shortcut\" &&\n entry.keyboardShortcut &&\n parseKeyCombo(entry.keyboardShortcut)\n ? entry.keyboardShortcut\n : isDisabled\n ? \"Not configured\"\n : undefined;\n return {\n kind: \"item\",\n id: `entry:${entry.id}`,\n label: entry.label,\n icon: resolveIcon(entry.iconName),\n // Phase 6.7: there is no \"legacy match\" row any more. Availability is\n // derived (requirement-gate.ts) — an item either QUALIFIES here or is\n // absent. Nothing renders red to advertise a backfill backlog.\n hint,\n disabled: isDisabled,\n onSelect: () => m.handleEntrySelect(entry),\n } satisfies MenuItemNode;\n });\n if (children.length > 0) {\n if (items.length > 0) children_.push({ kind: \"separator\", id: `${category.id}:sep` });\n for (const child of children) children_.push(categoryGroupNode(child, m));\n }\n return {\n kind: \"submenu\",\n id: `cat:${category.id}`,\n label: category.label,\n icon: resolveIcon(category.iconName),\n iconStyle: { color: category.color || \"currentColor\" },\n disabled: items.length === 0 && children.length === 0,\n emptyLabel: `No items in ${category.label}`,\n width: \"w-64\",\n children: children_,\n };\n}\n\nfunction placementNode(\n placementType: string,\n m: ContextMenuActions,\n): MenuSubmenuNode | null {\n const mode = m.resolvedPlacementMode[placementType as PlacementKey];\n if (mode === \"hide\") return null;\n const groups = m.grouped[placementType] || [];\n // A category with zero items still RENDERS (as a greyed submenu) — Arman\n // must see where new items will land the moment he creates the category.\n // The placement is only disabled when it has no categories at all.\n const hasCategories = groups.length > 0;\n const label = getPlacementLabel(placementType);\n const color = PLACEMENT_COLOR[placementType];\n if (!hasCategories && m.librariesError && mode !== \"disable\") {\n return failedLibraryNode(`placement:${placementType}`, label, getPlacementIcon(placementType), m, placementType);\n }\n return {\n kind: \"submenu\",\n id: `placement:${placementType}`,\n label,\n icon: getPlacementIcon(placementType),\n iconStyle: color ? { color } : undefined,\n disabled: mode === \"disable\" || !hasCategories,\n loading: m.loading && !hasCategories,\n emptyLabel: `No ${label}`,\n width: \"w-64\",\n placement: placementType,\n children: groups.map((g) => categoryGroupNode(g, m)),\n };\n}\n\n/**\n * A library whose fetch failed or missed the menu's deadline: the row stays\n * (never silently absent, never \"Loading…\" forever) and opens onto one\n * honest row that retries (page-pass 2026-09-27).\n */\nfunction failedLibraryNode(\n id: string,\n label: string,\n icon: MenuSubmenuNode[\"icon\"],\n m: ContextMenuActions,\n placement: string,\n): MenuSubmenuNode {\n return {\n kind: \"submenu\",\n id,\n label,\n icon,\n width: \"w-64\",\n placement,\n children: [\n {\n kind: \"item\",\n id: `${id}:retry`,\n label: \"Couldn't load. Retry\",\n icon: RotateCcw,\n keepsMenuOpen: true,\n onSelect: m.retryLibraries,\n },\n ],\n };\n}\n\nfunction boundAgentsNode(m: ContextMenuActions): MenuSubmenuNode | null {\n const mode = m.resolvedPlacementMode[\"bound-agent\"];\n if (mode === \"hide\") return null;\n const sections = m.boundAgentSections.filter((s) => s.agents.length > 0);\n const hasAgents = sections.length > 0;\n if (!hasAgents && m.boundAgentsError && !m.boundAgentsLoading && mode !== \"disable\") {\n return failedLibraryNode(\"placement:bound-agent\", \"Agents\", AGENT_ICON, m, \"bound-agent\");\n }\n const children: MenuNode[] = [];\n sections.forEach((section, idx) => {\n if (idx > 0) children.push({ kind: \"separator\", id: `agents:${section.key}:sep` });\n children.push({\n kind: \"label\",\n id: `agents:${section.key}:label`,\n label: section.label.toUpperCase(),\n });\n for (const agent of section.agents) {\n children.push({\n kind: \"item\",\n id: `agent:${section.key}:${agent.agentId}`,\n label: agent.name,\n icon: AGENT_ICON,\n iconClassName: \"text-indigo-500/80\",\n onSelect: () => void m.handleBoundAgentExecute(agent),\n });\n }\n });\n return {\n kind: \"submenu\",\n id: \"placement:bound-agent\",\n label: \"Agents\",\n icon: AGENT_ICON,\n iconClassName: \"text-indigo-500\",\n disabled: mode === \"disable\" || (!hasAgents && !m.boundAgentsLoading),\n loading: m.boundAgentsLoading,\n emptyLabel: m.boundAgentsLoading && !hasAgents ? \"Loading…\" : \"No agents available\",\n width: \"w-64\",\n placement: \"bound-agent\",\n children,\n };\n}\n\n// ---------------------------------------------------------------------------\n// The builder\n// ---------------------------------------------------------------------------\n\nexport function buildMenuModel(\n m: ContextMenuActions,\n props: Pick<\n MenuContentProps,\n | \"extraSections\"\n | \"isEditable\"\n | \"onSave\"\n | \"onDelete\"\n | \"onUndo\"\n | \"onRedo\"\n | \"canUndo\"\n | \"canRedo\"\n | \"undoHint\"\n | \"redoHint\"\n | \"onViewHistory\"\n | \"hasHistory\"\n | \"selectedText\"\n | \"entity\"\n | \"getTextarea\"\n > &\n Partial<Pick<MenuContentProps, \"selectionRange\" | \"recordActionsOnly\">>,\n): MenuModel {\n const {\n extraSections,\n isEditable,\n onSave,\n onDelete,\n onUndo,\n onRedo,\n canUndo,\n canRedo,\n undoHint,\n redoHint,\n onViewHistory,\n hasHistory,\n selectedText,\n entity,\n selectionRange,\n getTextarea,\n recordActionsOnly = false,\n } = props;\n const { actionText, quickActions } = m;\n\n const header: MenuHeader | null =\n actionText.source === \"selection\"\n ? { label: \"Selected\", text: actionText.text }\n : actionText.source === \"content\"\n ? { label: \"Content\", text: actionText.text }\n : null;\n\n // ── Clipboard ───────────────────────────────────────────────────────────\n const copy: MenuItemNode = {\n kind: \"item\",\n id: \"copy\",\n label: \"Copy\",\n icon: Copy,\n iconClassName: \"text-emerald-500\",\n disabled: actionText.source === \"none\",\n onSelect: () => void m.handleCopy(),\n };\n const speak: MenuItemNode = {\n kind: \"item\",\n id: \"speak\",\n label: \"Speak\",\n icon: Volume2,\n iconClassName: \"text-sky-500\",\n disabled: actionText.source === \"none\",\n onSelect: m.handleSpeak,\n };\n // The two listening actions live under ONE \"Listen\" slot — every menu has\n // either content or a selection, so this pair is universal; a submenu keeps\n // it to a single row. Disabled (never hidden) when no text or no agent.\n const listenDisabled = actionText.source === \"none\" || !m.spokenSummaryAvailable;\n // With content, the registry owns every content verb (copy formats, listen,\n // compare, save, share & export) — the engine draws its own only without.\n const hasRegistry = m.registryActions.length > 0;\n const spokenSummary: MenuItemNode = {\n kind: \"item\",\n id: \"spoken-summary\",\n label: \"Summarize without playing\",\n icon: Headphones,\n iconClassName: \"text-violet-500\",\n disabled: listenDisabled,\n onSelect: m.handleSpokenSummary,\n };\n const spokenSummaryLive: MenuItemNode = {\n kind: \"item\",\n id: \"spoken-summary-live\",\n label: \"Summarize & listen\",\n icon: AudioLines,\n iconClassName: \"text-violet-500\",\n disabled: listenDisabled,\n onSelect: m.handleSpokenSummaryLive,\n };\n const listen: MenuSubmenuNode | null = hasRegistry ? null : {\n kind: \"submenu\",\n id: \"listen\",\n label: \"Listen\",\n icon: Headphones,\n iconClassName: \"text-violet-500\",\n disabled: listenDisabled,\n children: [spokenSummary, spokenSummaryLive],\n };\n const copyAs: MenuSubmenuNode | null =\n !hasRegistry && m.copyVariantActions.length > 0\n ? {\n kind: \"submenu\",\n id: \"copy-as\",\n label: \"Copy as\",\n icon: Copy,\n iconClassName: \"text-emerald-500\",\n children: m.copyVariantActions.map((a) => richActionNode(a, m.richDocCtx)),\n }\n : null;\n const json: MenuSubmenuNode | null = m.jsonSection\n ? {\n kind: \"submenu\",\n id: \"json\",\n label: jsonSectionLabel(m.jsonSection),\n icon: Braces,\n iconClassName: \"text-amber-500\",\n width: \"w-72\",\n children: m.jsonSection.actions.map((action) => ({\n kind: \"item\",\n id: `json:${action.id}`,\n label: action.label,\n hint: action.hint,\n disabled: action.disabled,\n onSelect: () => void action.run(),\n })),\n }\n : null;\n const cut: MenuItemNode = {\n kind: \"item\",\n id: \"cut\",\n label: \"Cut\",\n icon: Scissors,\n iconClassName: \"text-emerald-500\",\n disabled: !isEditable || !selectedText,\n onSelect: () => void m.handleCut(),\n };\n const paste: MenuItemNode = {\n kind: \"item\",\n id: \"paste\",\n label: \"Paste\",\n icon: Clipboard,\n iconClassName: \"text-emerald-500\",\n disabled: !isEditable,\n onSelect: () => void m.handlePaste(),\n };\n const selectAll: MenuItemNode = {\n kind: \"item\",\n id: \"select-all\",\n label: \"Select All\",\n icon: Type,\n iconClassName: \"text-muted-foreground\",\n // Select All acts on a captured selection's container, or an editable\n // surface's own field. With neither (right-click on a table row or on\n // read-only content without selecting) it would do nothing — a dead row,\n // so it is absent (R1 b), not offered.\n disabled: !selectionRange && !(isEditable && getTextarea),\n onSelect: m.handleSelectAll,\n };\n const find: MenuItemNode = {\n kind: \"item\",\n id: \"find\",\n label: \"Find & Replace\",\n icon: Search,\n iconClassName: \"text-muted-foreground\",\n onSelect: m.handleFind,\n };\n const insertReference: MenuItemNode = {\n kind: \"item\",\n id: \"insert-reference\",\n label: isEditable ? \"Insert reference…\" : \"Copy reference…\",\n icon: AtSign,\n iconClassName: \"text-primary\",\n onSelect: m.handleInsertReference,\n };\n\n // ── Tools ───────────────────────────────────────────────────────────────\n const chat: MenuItemNode = {\n kind: \"item\",\n id: \"chat\",\n label: \"Chat\",\n icon: MessageSquare,\n iconClassName: \"text-primary\",\n onSelect: () => quickActions.openChatWindow(),\n };\n\n // ── History ─────────────────────────────────────────────────────────────\n const undo: MenuItemNode = {\n kind: \"item\",\n id: \"undo\",\n label: \"Undo\",\n icon: Undo2,\n iconClassName: \"text-sky-500\",\n hint: undoHint,\n disabled: onUndo ? !canUndo : !m.canNativeUndo,\n onSelect: m.handleUndo,\n };\n const redo: MenuItemNode = {\n kind: \"item\",\n id: \"redo\",\n label: \"Redo\",\n icon: Redo2,\n iconClassName: \"text-sky-500\",\n hint: redoHint,\n disabled: onRedo ? !canRedo : !m.canNativeUndo,\n onSelect: m.handleRedo,\n };\n const viewHistory: MenuItemNode = {\n kind: \"item\",\n id: \"view-history\",\n label: \"View History\",\n icon: History,\n iconClassName: \"text-violet-500\",\n disabled: !onViewHistory || !hasHistory,\n onSelect: () => onViewHistory?.(),\n };\n const compare: MenuSubmenuNode | null = hasRegistry ? null : {\n kind: \"submenu\",\n id: \"compare\",\n label: \"Compare\",\n icon: GitCompareArrows,\n iconClassName: \"text-amber-500\",\n children: [\n {\n kind: \"item\",\n id: \"compare:clipboard\",\n label: \"Compare with clipboard\",\n icon: Clipboard,\n onSelect: () => void m.handleCompareClipboard(),\n },\n {\n kind: \"item\",\n id: \"compare:set-base\",\n label: \"Set as compare base\",\n icon: Pin,\n description:\n actionText.source === \"selection\" ? \"Use selection\" : \"Use content\",\n onSelect: m.handleSetCompareBase,\n },\n {\n kind: \"item\",\n id: \"compare:with-base\",\n label: \"Compare with base\",\n icon: GitCompareArrows,\n description: m.hasCompareBase ? undefined : \"No base set yet\",\n disabled: !m.hasCompareBase,\n onSelect: () => void m.handleCompareWithBase(),\n },\n ],\n };\n\n // ── Document ────────────────────────────────────────────────────────────\n const exportMenu: MenuSubmenuNode | null =\n !hasRegistry && m.exportActions.length > 0\n ? {\n kind: \"submenu\",\n id: \"export\",\n label: \"Export\",\n icon: Download,\n iconClassName: \"text-amber-500\",\n children: m.exportActions.map((a) => richActionNode(a, m.richDocCtx)),\n }\n : null;\n const convert: MenuSubmenuNode | null =\n !hasRegistry && m.convertActions.length > 0\n ? {\n kind: \"submenu\",\n id: \"convert\",\n label: \"Convert\",\n icon: Replace,\n iconClassName: \"text-violet-500\",\n children: m.convertActions.map((a) => richActionNode(a, m.richDocCtx)),\n }\n : null;\n const attach: MenuItemNode | null = entity\n ? {\n kind: \"item\",\n id: \"attach\",\n label: \"Attach To\",\n icon: Link2,\n iconClassName: \"text-sky-500\",\n onSelect: m.handleAttach,\n }\n : null;\n // THE OBJECT VERB \"Link a record…\" — bound once here, so every menu that targets a record\n // (an `entity` prop, a per-row entity, a `data-entity-*` sniff, or a record-menu-registry row)\n // inherits it; no module adds its own. Opens the ONE picker (LinkRecordOverlay).\n const linkRecord: MenuItemNode | null = entity\n ? {\n kind: \"item\",\n id: \"link-record\",\n label: \"Link a record…\",\n icon: Link,\n iconClassName: \"text-sky-500\",\n onSelect: m.handleLinkRecord,\n }\n : null;\n const share: MenuItemNode | null = entity?.resourceType\n ? {\n kind: \"item\",\n id: \"share\",\n label: \"Share\",\n icon: Share2,\n iconClassName: \"text-emerald-500\",\n onSelect: m.handleShare,\n }\n : null;\n\n // ── Placements (data-driven) ────────────────────────────────────────────\n const libraryNodes = [\n placementNode(PLACEMENT_TYPES.AI_ACTION, m),\n boundAgentsNode(m),\n placementNode(PLACEMENT_TYPES.CONTENT_BLOCK, m),\n placementNode(PLACEMENT_TYPES.USER_TOOL, m),\n placementNode(PLACEMENT_TYPES.ORGANIZATION_TOOL, m),\n ].filter((n): n is MenuSubmenuNode => n !== null);\n // ONE AI family: with a registry, the libraries live INSIDE its AI submenu\n // (every shortcut, agent, block and item kept — only the parallel top-level\n // family is gone). Without content they stay where they always were.\n const registry: MenuNode[] | null = hasRegistry\n ? registryTreeNodes(m.registryActions, m.richDocCtx, libraryNodes)\n : null;\n const placements = hasRegistry ? [] : libraryNodes;\n\n // ── Quick actions ───────────────────────────────────────────────────────\n const quickMode = m.resolvedPlacementMode[\"quick-action\"];\n const quick: MenuSubmenuNode | null =\n quickMode === \"hide\"\n ? null\n : {\n kind: \"submenu\",\n id: \"quick-actions\",\n label: \"Quick Actions\",\n icon: Zap,\n iconClassName: \"text-pink-500\",\n disabled: quickMode === \"disable\",\n width: \"w-56\",\n children: [\n { kind: \"item\", id: \"qa:notes\", label: \"Notes\", icon: StickyNote, onSelect: () => quickActions.openQuickNotes() },\n { kind: \"item\", id: \"qa:tasks\", label: \"Tasks\", icon: CheckSquare, onSelect: () => quickActions.openQuickTasks() },\n { kind: \"item\", id: \"qa:chat\", label: \"Chat\", icon: MessageSquare, onSelect: () => quickActions.openQuickChat() },\n { kind: \"item\", id: \"qa:data\", label: \"Data\", icon: Database, onSelect: () => quickActions.openQuickData() },\n { kind: \"item\", id: \"qa:files\", label: \"Files\", icon: FolderOpen, onSelect: () => quickActions.openQuickFiles() },\n { kind: \"item\", id: \"qa:voice\", label: \"Voice Input\", icon: Mic, onSelect: () => quickActions.openVoicePad() },\n ],\n };\n\n // ── Editable ────────────────────────────────────────────────────────────\n const save: MenuItemNode | null =\n isEditable && onSave\n ? {\n kind: \"item\",\n id: \"save\",\n label: \"Save\",\n icon: Save,\n iconClassName: \"text-emerald-500\",\n onSelect: () => onSave(),\n }\n : null;\n const del: MenuItemNode | null =\n isEditable && onDelete\n ? {\n kind: \"item\",\n id: \"delete\",\n label: \"Delete\",\n icon: Trash2,\n destructive: true,\n onSelect: () => void m.handleDelete(),\n }\n : null;\n\n // ── Admin ───────────────────────────────────────────────────────────────\n const admin: MenuSubmenuNode | null = m.isAdmin\n ? {\n kind: \"submenu\",\n id: \"admin\",\n label: \"Admin Tools\",\n icon: Shield,\n iconClassName: \"text-rose-500\",\n width: \"w-56\",\n children: [\n {\n kind: \"item\",\n id: \"admin:debug\",\n label: `${m.isDebugMode ? \"Disable\" : \"Enable\"} Debug Mode`,\n icon: m.isDebugMode ? EyeOff : Eye,\n iconClassName: m.isDebugMode ? \"text-amber-600 dark:text-amber-400\" : undefined,\n onSelect: m.handleToggleDebugMode,\n },\n {\n kind: \"item\",\n id: \"admin:values\",\n label: \"Context Values\",\n icon: Bug,\n className: \"text-amber-600 dark:text-amber-400\",\n onSelect: m.handleInspectValues,\n },\n ...(m.isDebugMode\n ? ([\n {\n kind: \"item\",\n id: \"admin:state\",\n label: \"Redux State\",\n icon: Database,\n className: \"text-amber-600 dark:text-amber-400\",\n onSelect: m.handleInspectState,\n },\n ] satisfies MenuNode[])\n : []),\n { kind: \"separator\", id: \"admin:sep\" },\n {\n kind: \"item\",\n id: \"admin:indicator\",\n label: `${m.isAdminIndicatorOpen ? \"Hide\" : \"Show\"} Admin Indicator`,\n icon: m.isAdminIndicatorOpen ? Eye : EyeOff,\n iconClassName: m.isAdminIndicatorOpen ? \"text-green-600 dark:text-green-400\" : undefined,\n onSelect: m.handleToggleAdminIndicator,\n },\n ],\n }\n : null;\n\n const extras = extrasByAnchor(extraSections);\n // The surface submenu (engine-built, shared with mobile) — always LAST, where\n // the version footer used to sit.\n const surfaceInfo: MenuSection = {\n id: \"surface-info\",\n group: \"surface-info\",\n nodes: m.surfaceSection.items.map(fromExtraItem),\n };\n\n // ── Record actions only (a list row, a folder) ──────────────────────────\n // A row is a thing you act ON, not text you are in: the editor's items\n // (Select All, Find, Cut/Paste, Undo/Redo/History, Insert/Copy reference,\n // Speak, Listen, Compare, Chat, Quick Actions) are absent. The agent\n // libraries stay only where there is content for them to act on (they ride\n // the registry's AI submenu); a folder has none. (page-pass /notes, 2026-09-28)\n if (recordActionsOnly) {\n const recordSections: MenuSection[] = [];\n // A record with no content source (a folder: only its NAME is on hand) has\n // no document to copy, export or hand an agent — only its own rows.\n const recordHasDocument = hasRegistry && m.richDocCtx?.source?.type !== \"raw\";\n const clipboardRecord = compactNodes([recordHasDocument ? copy : null, recordHasDocument ? copyAs : null, recordHasDocument ? json : null]);\n if (clipboardRecord.length > 0) {\n recordSections.push({ id: \"clipboard\", group: \"clipboard\", nodes: clipboardRecord });\n }\n if (registry && recordHasDocument) recordSections.push({ id: \"registry\", group: \"document\", nodes: registry });\n recordSections.push(...extras[\"after-clipboard\"]);\n if (recordHasDocument && (exportMenu || convert)) {\n recordSections.push({ id: \"document-out\", group: \"document\", nodes: compactNodes([exportMenu, convert]) });\n }\n if (attach || linkRecord || share) {\n recordSections.push({ id: \"share\", group: \"share\", nodes: compactNodes([attach, linkRecord, share]) });\n }\n recordSections.push(...extras[\"after-compare\"]);\n recordSections.push(...extras[\"after-placements\"]);\n if (save || del) recordSections.push({ id: \"editable\", group: \"editable\", nodes: compactNodes([save, del]) });\n if (admin) recordSections.push({ id: \"admin\", group: \"admin\", nodes: [admin] });\n recordSections.push(surfaceInfo);\n return {\n header,\n sections: withEditInPlace(liftPrimarySections(recordSections)),\n roles: {\n copy, speak, listen: null, copyAs, json, cut, paste, selectAll, find, insertReference, chat,\n undo, redo, viewHistory, compare: null, exportMenu, convert, attach, linkRecord, share,\n placements: [], registry, quickActions: null, save, del, admin, extras, surfaceInfo,\n },\n };\n }\n\n // ── Classic order (the historical arrangement, separator for separator) ──\n const sections: MenuSection[] = [];\n sections.push({\n id: \"clipboard\",\n group: \"clipboard\",\n nodes: compactNodes([\n copy,\n speak,\n listen,\n copyAs,\n json,\n cut,\n paste,\n selectAll,\n find,\n insertReference,\n ]),\n });\n if (registry) sections.push({ id: \"registry\", group: \"document\", nodes: registry });\n sections.push(...extras[\"after-clipboard\"]);\n sections.push({ id: \"tools\", group: \"tools\", nodes: [chat] });\n // THE SECTION IS ITS TITLE (page-pass 2026-09-27, /applets/manage/[id]/run):\n // \"History\" held Compare, Export, Convert, Attach To and Share. History keeps\n // what is history; the rest sit in sections of their own kind.\n sections.push({\n id: \"history\",\n group: \"history\",\n nodes: compactNodes([undo, redo, viewHistory]),\n });\n if (compare) sections.push({ id: \"compare\", group: \"tools\", nodes: [compare] });\n if (exportMenu || convert) {\n sections.push({ id: \"document-out\", group: \"document\", nodes: compactNodes([exportMenu, convert]) });\n }\n if (attach || linkRecord || share) {\n sections.push({ id: \"share\", group: \"share\", nodes: compactNodes([attach, linkRecord, share]) });\n }\n sections.push(...extras[\"after-compare\"]);\n if (placements.length > 0) sections.push({ id: \"placements\", group: \"ai\", nodes: placements });\n sections.push(...extras[\"after-placements\"]);\n if (quick) {\n sections.push({\n id: \"quick\",\n group: \"quick\",\n joinPrevious: extras[\"after-placements\"].length === 0,\n nodes: [quick],\n });\n }\n if (save || del) {\n sections.push({\n id: \"editable\",\n group: \"editable\",\n nodes: compactNodes([save, del]),\n });\n }\n if (admin) sections.push({ id: \"admin\", group: \"admin\", nodes: [admin] });\n sections.push(surfaceInfo);\n\n return {\n header,\n sections: withEditInPlace(liftPrimarySections(sections)),\n roles: {\n copy,\n speak,\n listen,\n copyAs,\n json,\n cut,\n paste,\n selectAll,\n find,\n insertReference,\n chat,\n undo,\n redo,\n viewHistory,\n compare,\n exportMenu,\n convert,\n attach,\n linkRecord,\n share,\n placements,\n registry,\n quickActions: quick,\n save,\n del,\n admin,\n extras,\n surfaceInfo,\n },\n };\n}\n","// features/context-menu-v3/alchemy-provider.ts\n//\n// The context menu's actions as a provider of the ONE Alchemy action registry\n// (Matrx Alchemy ALC-15 S3, LIST A2). The engine hook (`useContextMenuActions`)\n// still decides WHAT exists — clipboard verbs, selection-aware listening, the\n// agent libraries, history, surface sections, the surface submenu — and\n// `buildMenuModel` still binds every handler. This file turns that model into\n// package `Action`s; the package's model, layouts and renderers draw it (bar,\n// ⋯, right-click, bottom sheet, palette) — the v3 renderers and layouts are\n// gone.\n//\n// • The rich-document registry tree inside the model is NOT re-emitted: the\n// rich-document provider contributes those actions from the same click\n// target (composite host). Only the agent libraries folded into its AI\n// submenu come from here — inline in the \"ai\" group, under no heading,\n// like the rich-document AI rows (the group has no approved name).\n// • A surface section's label (\"Cell\", \"Row · Widget A\") names the clicked\n// thing: it is declared `kind: \"target\"`, never a group heading.\n// • R1: a disabled universal verb (copy, cut, paste, undo, redo, find)\n// greys WITH its sentence; every other unavailable row is absent.\n// • Headings are only names the classic menu already shows (\"History\",\n// a surface section's own label, the surface's display label).\n\nimport { fieldPreview } from \"./utils/field-menu-header\";\nimport type { Action, ActionCategory, ActionProvider, ClickTarget, Eligibility } from \"@ai-matrx/alchemy/actions\";\nimport { registerAlchemyIcon } from \"@ai-matrx/rich-content/utils/alchemy-icon-keys\";\nimport { stripTurnTrust } from \"@ai-matrx/chat/agents/conversation-export/turn-trust-strip\";\nimport { markdownToPlainText } from \"@ai-matrx/content-ir/source\";\nimport type { MenuModel, MenuNode, MenuSection } from \"./model/menu-model\";\n\nexport interface ContextMenuTargetHost {\n kind: \"context-menu\";\n /** The menu instance (one open menu = one provider registration). */\n instanceId: string;\n}\n\n/** Composite host: a right-click target can carry both providers' halves. */\nexport interface CompositeTargetHost {\n contextMenu?: ContextMenuTargetHost | undefined;\n richDocument?: unknown | undefined;\n}\n\nexport function contextMenuHostOf(target: ClickTarget): ContextMenuTargetHost | null {\n const host = target.host as (CompositeTargetHost & Partial<ContextMenuTargetHost>) | undefined;\n if (!host) return null;\n if (host.kind === \"context-menu\" && typeof host.instanceId === \"string\") {\n return host as ContextMenuTargetHost;\n }\n return host.contextMenu ?? null;\n}\n\nconst VERBS: Record<string, { verb: \"copy\" | \"cut\" | \"paste\" | \"undo\" | \"redo\" | \"find\"; sentence: string }> = {\n copy: { verb: \"copy\", sentence: \"Select text, or right-click content, to copy it.\" },\n cut: { verb: \"cut\", sentence: \"Cut works on selected text in an editable field.\" },\n paste: { verb: \"paste\", sentence: \"Paste works in an editable field.\" },\n undo: { verb: \"undo\", sentence: \"Nothing to undo here.\" },\n redo: { verb: \"redo\", sentence: \"Nothing to redo here.\" },\n find: { verb: \"find\", sentence: \"Find works where there is text.\" },\n};\n\n/**\n * Verbs that only mean something where the person can edit: on a read-only\n * target (a table row, rendered content) an unavailable one is ABSENT rather\n * than greyed — Cut / Paste / Undo on a row is noise, and it pushes the row's\n * own section down (page-pass 2026-09-27). Where the target is editable they\n * grey with their sentence as before (R1 c).\n */\nconst EDIT_ONLY_VERBS = new Set([\"cut\", \"paste\", \"undo\", \"redo\"]);\n\n/** v3 section group → the action category that lands in the same package group. */\nconst CATEGORY_OF: Record<MenuSection[\"group\"], ActionCategory> = {\n clipboard: \"clipboard\",\n tools: \"app\",\n history: \"history\",\n share: \"share\",\n document: \"save\",\n surface: \"edit\",\n ai: \"ai\",\n quick: \"feedback\",\n editable: \"edit\",\n admin: \"admin\",\n \"surface-info\": \"surface-info\",\n};\n\n/** Clipboard-section rows that are not universal verbs sit with in-place tools. */\nconst NON_VERB_CLIPBOARD: Record<string, ActionCategory> = {\n \"select-all\": \"edit\",\n \"insert-reference\": \"edit\",\n};\n\nexport interface ProviderOptions {\n /**\n * Resolves with the NEXT model the engine hook builds. A library still\n * loading (the first agent fetch) waits on it instead of vanishing.\n */\n nextModel?: (() => Promise<MenuModel>) | undefined;\n /**\n * Whether THIS menu instance edits its surface (`isEditable`). A read-only\n * wrapper over rich content still gets a writable rich-document target\n * (actions may write back to the SOURCE), so `target.readOnly` alone cannot\n * say \"the person cannot type here\" — this can (live 2026-09-27: a table\n * row's menu still drew Cut / Paste / Undo / Redo).\n */\n editable?: boolean | undefined;\n}\n\n/** Rows that INSERT content into the surface: absent on a read-only source. */\nconst INSERTS_CONTENT = new Set([\"placement:content-block\"]);\n\nfunction findNodeById(nodes: readonly MenuNode[], id: string): MenuNode | null {\n for (const n of nodes) {\n if (n.id === id) return n;\n if (n.kind === \"submenu\") {\n const hit = findNodeById(n.children, id);\n if (hit) return hit;\n }\n }\n return null;\n}\n\ninterface Placement {\n category: ActionCategory;\n section?: Action[\"section\"] | undefined;\n order: number;\n}\n\nconst available: Eligibility = { status: \"available\" };\nconst absent: Eligibility = { status: \"absent\" };\n\nfunction icon(node: { icon?: unknown }): string | undefined {\n return node.icon ? registerAlchemyIcon(node.icon) : undefined;\n}\n\n/** The row starts a new group: the menu draws a divider above it (alchemy `Action.startsGroup`). */\nfunction startGroup(a: Action): void {\n Object.assign(a, { startsGroup: true });\n}\n\n/** One v3 node → one package action (submenus expand to their rows). */\nfunction toAction(node: MenuNode, place: Placement, instanceId: string, opts: ProviderOptions = {}): Action | null {\n if (node.kind === \"separator\" || node.kind === \"label\") return null;\n const id = `cm:${node.id}`;\n const own = (t: ClickTarget) => contextMenuHostOf(t)?.instanceId === instanceId;\n const base = {\n id,\n label: node.label,\n category: place.category,\n order: place.order,\n ...(place.section ? { section: place.section } : {}),\n ...((key) => (key ? { icon: key } : {}))(icon(node)),\n ...(\"iconClassName\" in node && node.iconClassName ? { iconTone: node.iconClassName } : {}),\n ...(\"description\" in node && node.description ? { description: node.description } : {}),\n ...(\"hint\" in node && node.hint ? { hint: node.hint } : {}),\n };\n if (node.kind === \"submenu\") {\n const inserts = INSERTS_CONTENT.has(node.id);\n return {\n ...base,\n // Still loading at the source: every layout shows \"Loading…\" on the row.\n pending: () => Boolean(node.loading),\n eligible: (t) => {\n if (!own(t)) return absent;\n if (inserts && (t.readOnly || opts.editable === false)) return absent;\n // Still loading: SHOWN (as loading), never silently absent (round 2).\n if (node.loading) return available;\n return !node.disabled && hasRows(node.children) ? available : absent;\n },\n expand: async (t) => {\n let current: MenuNode = node;\n // Wait for the library's first load (bounded), then read its rows\n // from the model that has them.\n for (let i = 0; current.kind === \"submenu\" && current.loading && opts.nextModel && i < 20; i++) {\n const next = await opts.nextModel();\n const found = next.sections.map((s) => findNodeById(s.nodes, node.id)).find(Boolean);\n if (!found) return [];\n current = found;\n }\n if (current.kind !== \"submenu\") return [];\n void t;\n // THE SUBMENU KEEPS ITS GROUPS (DATA-V2-BASICS-2): each run of rows between the source's\n // separators is one section, and the menu draws a divider where the section changes\n // (alchemy withGroupDividers) — \"Delete row…\" in its own group, never beside Highlight.\n const rows: Action[] = [];\n let startsNext = false;\n for (const child of current.children) {\n if (child.kind === \"separator\") {\n startsNext = rows.length > 0;\n continue;\n }\n // An empty category (or a row that cannot run) is ABSENT (R1) —\n // never a \"No items in …\" panel, never a dead row.\n if (unusable(child)) continue;\n const a = toAction(child, { ...place, section: undefined, order: rows.length }, instanceId, opts);\n if (!a) continue;\n if (startsNext) startGroup(a);\n startsNext = false;\n rows.push(a);\n }\n return rows;\n },\n run: () => undefined,\n };\n }\n if (node.kind === \"link\") {\n return { ...base, href: node.href, eligible: (t) => (own(t) && !node.disabled ? available : absent), run: () => undefined };\n }\n if (node.kind === \"checkbox\") {\n return {\n ...base,\n toggle: true,\n pressed: () => node.checked,\n eligible: (t) => (own(t) && !node.disabled ? available : absent),\n run: () => node.onCheckedChange(!node.checked),\n };\n }\n const verb = place.category === \"clipboard\" ? VERBS[node.id] : undefined;\n return {\n ...base,\n ...(verb ? { verb: verb.verb } : {}),\n ...(node.destructive ? { destructive: true } : {}),\n ...(node.keepsMenuOpen ? { keepsMenuOpen: true } : {}),\n eligible: (t) => {\n if (!own(t)) return absent;\n if (!node.disabled) return available;\n if (verb && (t.readOnly || opts.editable === false) && EDIT_ONLY_VERBS.has(verb.verb)) return absent;\n // R1 (c): a universal verb greys, and it says why.\n if (verb) return { status: \"unavailable-verb\", verb: verb.verb, sentence: verb.sentence };\n // ABSENT OR HONEST (admin pass 2026-09-27): a row that is off FOR A REASON it states\n // (\"Manifested surfaces are managed in code\") stays, greyed, with the reason. With no\n // reason it is absent.\n const reason = \"description\" in node && typeof node.description === \"string\" ? node.description.trim() : \"\";\n return reason ? { status: \"unavailable\", sentence: reason } : absent;\n },\n run: () => node.onSelect(),\n };\n}\n\n/** A row that can never run here: a disabled leaf, or a submenu with nothing to open. */\nfunction unusable(node: MenuNode): boolean {\n if (node.kind === \"separator\" || node.kind === \"label\") return false;\n if (node.kind === \"submenu\") return !node.loading && (Boolean(node.disabled) || !hasRows(node.children));\n // A disabled row that says why stays (greyed, with its reason); one with no reason cannot run.\n if (!(\"disabled\" in node) || !node.disabled) return false;\n return !(\"description\" in node && typeof node.description === \"string\" && node.description.trim());\n}\n\nfunction hasRows(nodes: MenuNode[]): boolean {\n return nodes.some((n) => (n.kind === \"submenu\" ? n.loading || hasRows(n.children) : n.kind !== \"separator\" && n.kind !== \"label\"));\n}\n\n/**\n * The model → the actions this menu instance contributes. Rich-document rows\n * (`rich:` leaves) are skipped: the rich-document provider supplies them.\n */\nexport function contextMenuActionsFromModel(model: MenuModel, instanceId: string, opts: ProviderOptions = {}): Action[] {\n const out: Action[] = [];\n model.sections.forEach((section, sectionIndex) => {\n const base = sectionIndex * 100;\n if (section.id === \"registry\") {\n // Only the agent libraries folded into the registry's AI submenu are ours.\n section.nodes.forEach((node, i) => {\n if (node.kind !== \"submenu\" || !node.id.startsWith(\"rich-sub:\")) return;\n const libraries = node.children.filter((c) => !c.id.startsWith(\"rich\") && c.kind !== \"separator\");\n libraries.forEach((lib, j) => {\n const a = toAction(\n lib,\n {\n category: \"ai\",\n order: 5_000 + i * 50 + j,\n },\n instanceId,\n opts,\n );\n if (a) out.push(a);\n });\n });\n return;\n }\n // A section with no declared label has no heading — never fabricate one\n // (not even \"\"): the package's own contract is \"no declared name → no\n // heading, rows show inline, never folded\" (`@ai-matrx/alchemy/menu`\n // `buildMenuModel` doc). The empty-string fallback that used to sit here\n // for a labelless `primary` section (e.g. notes'\n // `buildNoteContextSections`'s unlabeled \"note-actions\" section) declared\n // a REAL heading of `\"\"`, which is never an approved heading — every menu\n // carrying one threw `UnapprovedHeadingError` and refused to open\n // (2026-09-28). Ordering for a `primary` section is already handled\n // upstream by `liftPrimarySections` on the v3 `MenuSection[]`, so\n // dropping the heading here costs nothing but the (non-existent) label.\n const named =\n section.group === \"history\"\n ? { id: \"cm-history\", label: \"History\" }\n : section.label\n ? {\n id: `cm-${section.id}`,\n label: section.label,\n ...(section.primary ? { primary: true } : {}),\n // A surface section's label is the clicked thing's own name.\n ...(section.group === \"surface\" ? { kind: \"target\" as const } : {}),\n }\n : undefined;\n // A separator in the source starts a new group: the next row carries `startsGroup` and the\n // menu draws the divider (alchemy buildMenuModel) — \"Delete row…\" in its own group (DATA-V2-BASICS-2).\n let groupStarts = false;\n section.nodes.forEach((node, i) => {\n if (node.kind === \"separator\") {\n groupStarts = true;\n return;\n }\n const category =\n section.group === \"clipboard\" ? (NON_VERB_CLIPBOARD[node.id] ?? \"clipboard\") : CATEGORY_OF[section.group];\n // Undo / Redo are universal verbs: they ride the strip, not the History fold.\n const isHistoryVerb = section.group === \"history\" && (node.id === \"undo\" || node.id === \"redo\");\n const a = toAction(\n node,\n {\n category: isHistoryVerb ? \"clipboard\" : category,\n ...(named && !isHistoryVerb ? { section: named } : {}),\n order: base + i,\n },\n instanceId,\n opts,\n );\n if (a) {\n if (groupStarts) startGroup(a);\n groupStarts = false;\n out.push(a);\n }\n });\n });\n return out;\n}\n\n/**\n * THE provider one open menu registers (AlchemyMenuContent). Every menu\n * instance yields the same baseline ids (`cm:copy`, `cm:find`, …) and the\n * Alchemy registry refuses an id a second provider yields — so an instance\n * answers ONLY a target it opened. Two menus mounted at once (a message's menu\n * nested in the page's, PB-06 2026-10-01: 18 `DuplicateActionError`s per\n * open) each contribute to their own target and nothing to the other's.\n */\nexport function contextMenuProvider(instanceId: string, actions: () => readonly Action[]): ActionProvider {\n return {\n id: `context-menu:${instanceId}`,\n tier: \"T1\",\n actions: (target) => (contextMenuHostOf(target)?.instanceId === instanceId ? actions() : []),\n };\n}\n\n/** A cheap fingerprint of what the menu would draw (re-resolve when it moves). */\nexport function modelRevision(model: MenuModel): string {\n const walk = (nodes: MenuNode[]): string =>\n nodes\n .map((n) =>\n n.kind === \"submenu\"\n ? `${n.id}${n.disabled ? \"!\" : \"\"}${n.loading ? \"~\" : \"\"}[${walk(n.children)}]`\n : `${n.id}${\"disabled\" in n && n.disabled ? \"!\" : \"\"}${n.kind === \"checkbox\" && n.checked ? \"*\" : \"\"}${\"label\" in n ? `:${n.label}` : \"\"}`,\n )\n .join(\",\");\n return model.sections.map((s) => `${s.id}(${walk(s.nodes)})`).join(\"|\");\n}\n\n/**\n * The text the menu's \"Content: …\" header shows for what the menu acts on —\n * without storage plumbing: the tutor's trust comment goes through the ONE\n * strip copies and files use (live 2026-09-26: the header showed\n * `<!--MATRX_TRUST_V1 …-->` on every tutor answer).\n */\nexport function menuHeaderContent(actionText: { source: string; text: string }): string | null {\n if (actionText.source === \"none\") return null;\n return headerPreview(stripTurnTrust(actionText.text));\n}\n\n/** The transcript store slice `chatMessageSubject` reads (RootState[\"messages\"]). */\ninterface TranscriptStoreSlice {\n messages?: {\n byConversationId?: Record<\n string,\n { byId?: Record<string, { role?: unknown; createdAt?: string | null } | undefined> } | undefined\n >;\n };\n}\n\n/**\n * THE MESSAGE A MENU WAS OPENED ON — one answer for every menu over a chat\n * transcript, so the same message gets the same heading everywhere (\"AI answer\n * · Sep 27, 6:50 PM\", \"Your message · …\").\n *\n * Two doors name a message and both land here:\n * 1. the menu's own content source is that message (`chat-message` — the\n * per-answer registry menu);\n * 2. the menu is the transcript-level one and the right-click resolved the\n * message under the pointer (`messageId` in the per-open context —\n * `resolveMarkdownContext` reads the `data-message-id` every message root\n * carries). Run History's user turns and any answer area outside the\n * per-answer menu came through here and were headed \"Content: <the whole\n * text>\" (2026-09-28).\n */\nexport function chatMessageSubject(args: {\n state: TranscriptStoreSlice;\n source: { type: string; conversationId?: string; messageId?: string } | null | undefined;\n extensions?: { type?: string; role?: unknown } | null;\n contextData?: Record<string, unknown> | null | undefined;\n}): { role: string; createdAt: string | null } | null {\n const { state, source, extensions, contextData } = args;\n const read = (conversationId: unknown, messageId: unknown) =>\n typeof conversationId === \"string\" && typeof messageId === \"string\"\n ? state.messages?.byConversationId?.[conversationId]?.byId?.[messageId]\n : undefined;\n if (source?.type === \"chat-message\") {\n const record = read(source.conversationId, source.messageId);\n const role = record?.role ?? (extensions?.type === \"chat-message\" ? extensions.role : null);\n return role ? { role: String(role), createdAt: record?.createdAt ?? null } : null;\n }\n const record = read(contextData?.conversationId, contextData?.messageId);\n return record?.role ? { role: String(record.role), createdAt: record.createdAt ?? null } : null;\n}\n\n/**\n * The header for a menu opened in a FIELD with no selection: the field's name\n * and, when it helps, a short plain preview (`utils/field-menu-header.ts`).\n * Anything else keeps `menuHeaderContent`.\n */\nexport function menuHeader(\n actionText: { source: string; text: string },\n fieldLabel: string | null,\n /** The chat message the menu opened on, when it did (its role and when it was written). */\n message?: { role: string; createdAt?: string | null } | null,\n): { content: string | null; contentLabel: string | null } {\n // A MESSAGE is named, never dumped: \"AI answer\", not \"Content: <the whole answer>\"\n // (page-pass 2026-09-27, applet Run History). A selection still shows itself.\n if (message && actionText.source !== \"selection\" && actionText.source !== \"none\") {\n const who = message.role === \"user\" ? \"Your message\" : message.role === \"assistant\" ? \"AI answer\" : \"Message\";\n const at = message.createdAt ? new Date(message.createdAt) : null;\n const when =\n at && !Number.isNaN(at.getTime())\n ? ` · ${at.toLocaleString(undefined, { month: \"short\", day: \"numeric\", hour: \"numeric\", minute: \"2-digit\" })}`\n : \"\";\n return { content: \"\", contentLabel: `${who}${when}` };\n }\n if (fieldLabel && actionText.source !== \"selection\") {\n return { content: fieldPreview(headerPreview(stripTurnTrust(actionText.text))), contentLabel: fieldLabel };\n }\n return { content: menuHeaderContent(actionText), contentLabel: null };\n}\n\n/**\n * The header is a one-line PREVIEW, so it shows words, never markdown syntax:\n * \"Content: # Clinic intake…\" on /notes/<id> (page-pass 2026-09-27). Block\n * markers (headings, quotes, list bullets, task boxes, fences) are dropped per\n * line, inline emphasis goes through THE plain-text helper, and lines join\n * with a space. Only the header changes — the actions still act on the text\n * as written.\n */\nexport function headerPreview(text: string): string {\n const blocks = text\n .replace(/^\\s{0,3}(?:```|~~~).*$/gm, \"\")\n .replace(/^\\s{0,3}#{1,6}\\s+/gm, \"\")\n .replace(/^\\s{0,3}>\\s?/gm, \"\")\n .replace(/^\\s{0,3}(?:[-*+]|\\d+[.)])\\s+(?:\\[[ xX]\\]\\s+)?/gm, \"\");\n return markdownToPlainText(blocks).replace(/\\s*\\n+\\s*/g, \" \").trim();\n}\n\n/**\n * A target that NAMES itself (`CONTEXT_MENU_HEADING_KEY`: a list row's record)\n * heads the menu with that name — \"Quiz: Unit 2 review\" — instead of the\n * generic \"Content: <agent context>\" (page-pass 2026-09-27). A selection still\n * shows itself.\n */\nexport function namedHeader(\n heading: { label: string; text: string } | null | undefined,\n actionText: { source: string },\n): { content: string; contentLabel: string } | null {\n if (!heading || actionText.source === \"selection\") return null;\n return { content: heading.text, contentLabel: heading.label };\n}\n\n/**\n * The revision the alchemy menu engine re-resolves on. The engine\n * (`useMenuEngine` in @ai-matrx/alchemy/react/menu) rebuilds its model — header\n * included — only when its `revision` moves; `content` / `contentLabel` are not\n * in its dependency list. A menu that stays mounted between opens therefore\n * kept the FIRST header: \"Note: New Note\" after the note was renamed\n * (G6B review, 2026-10-02). The header text is part of what the menu draws, so\n * it is part of the revision.\n */\nexport function menuEngineRevision(\n drawn: string,\n header: { content: string | null; contentLabel: string | null },\n): string {\n return `${drawn}\\u0000${header.contentLabel ?? \"\"}\\u0000${header.content ?? \"\"}`;\n}\n"]}
|