@flamingo-stack/openframe-frontend-core 0.0.510 → 0.0.511

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/dist/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
  2. package/dist/chunk-27OKD7ZV.js.map +1 -0
  3. package/dist/{chunk-2RLI5XVI.cjs → chunk-2CE725NN.cjs} +16 -16
  4. package/dist/{chunk-2RLI5XVI.cjs.map → chunk-2CE725NN.cjs.map} +1 -1
  5. package/dist/{chunk-Q4K3TGNP.cjs → chunk-3NECEXDF.cjs} +74 -44
  6. package/dist/chunk-3NECEXDF.cjs.map +1 -0
  7. package/dist/{chunk-RJ3D3PE2.cjs → chunk-4SG55XVA.cjs} +9 -9
  8. package/dist/{chunk-RJ3D3PE2.cjs.map → chunk-4SG55XVA.cjs.map} +1 -1
  9. package/dist/{chunk-SYZFFRVD.cjs → chunk-5BAGE7AI.cjs} +13 -13
  10. package/dist/{chunk-SYZFFRVD.cjs.map → chunk-5BAGE7AI.cjs.map} +1 -1
  11. package/dist/{chunk-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
  12. package/dist/{chunk-6TL5XIAW.cjs → chunk-7YV67K3I.cjs} +119 -119
  13. package/dist/{chunk-6TL5XIAW.cjs.map → chunk-7YV67K3I.cjs.map} +1 -1
  14. package/dist/{chunk-5CDPRCPS.cjs → chunk-A5YZUV6B.cjs} +23 -23
  15. package/dist/{chunk-5CDPRCPS.cjs.map → chunk-A5YZUV6B.cjs.map} +1 -1
  16. package/dist/{chunk-WSAPKKDU.cjs → chunk-C4EUXZSA.cjs} +9 -9
  17. package/dist/{chunk-WSAPKKDU.cjs.map → chunk-C4EUXZSA.cjs.map} +1 -1
  18. package/dist/{chunk-5AZZQQYS.cjs → chunk-DPLXHG7H.cjs} +40 -40
  19. package/dist/{chunk-5AZZQQYS.cjs.map → chunk-DPLXHG7H.cjs.map} +1 -1
  20. package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
  21. package/dist/chunk-FS3YSNSU.js.map +1 -0
  22. package/dist/{chunk-RB2EW6IZ.cjs → chunk-GHXTF35X.cjs} +3231 -3165
  23. package/dist/chunk-GHXTF35X.cjs.map +1 -0
  24. package/dist/{chunk-7VUKIWF6.cjs → chunk-HIV2GIFP.cjs} +3 -3
  25. package/dist/{chunk-7VUKIWF6.cjs.map → chunk-HIV2GIFP.cjs.map} +1 -1
  26. package/dist/{chunk-R4HKB3YD.cjs → chunk-IIPBTWQR.cjs} +22 -22
  27. package/dist/{chunk-R4HKB3YD.cjs.map → chunk-IIPBTWQR.cjs.map} +1 -1
  28. package/dist/{chunk-JMBIGH4H.cjs → chunk-IUZMWMVX.cjs} +83 -83
  29. package/dist/{chunk-JMBIGH4H.cjs.map → chunk-IUZMWMVX.cjs.map} +1 -1
  30. package/dist/{chunk-XJVRAKEX.cjs → chunk-IXCBVJRU.cjs} +56 -33
  31. package/dist/chunk-IXCBVJRU.cjs.map +1 -0
  32. package/dist/{chunk-TBGSQTJK.js → chunk-J4VB4EHW.js} +6 -6
  33. package/dist/{chunk-UWPT23SA.js → chunk-JXMNLH7F.js} +3 -3
  34. package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
  35. package/dist/chunk-KI4EGKNV.cjs.map +1 -0
  36. package/dist/{chunk-RHQ7BJKK.cjs → chunk-LEWQRAZ4.cjs} +73 -73
  37. package/dist/{chunk-RHQ7BJKK.cjs.map → chunk-LEWQRAZ4.cjs.map} +1 -1
  38. package/dist/{chunk-DBR5JLX3.js → chunk-LVAJK2U2.js} +46 -23
  39. package/dist/chunk-LVAJK2U2.js.map +1 -0
  40. package/dist/{chunk-OB7YDOTK.cjs → chunk-NJADHL2G.cjs} +8 -8
  41. package/dist/{chunk-OB7YDOTK.cjs.map → chunk-NJADHL2G.cjs.map} +1 -1
  42. package/dist/{chunk-HCZJZMU7.js → chunk-NKA3O4L6.js} +3 -3
  43. package/dist/{chunk-SRYV4H3M.js → chunk-NNWSGOUP.js} +3 -3
  44. package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
  45. package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
  46. package/dist/{chunk-EJ3EBYNV.cjs → chunk-OMN7A6RP.cjs} +32 -32
  47. package/dist/{chunk-EJ3EBYNV.cjs.map → chunk-OMN7A6RP.cjs.map} +1 -1
  48. package/dist/{chunk-WNOGR5AA.js → chunk-PRMR6WKC.js} +3 -3
  49. package/dist/{chunk-J2JUAS4C.cjs → chunk-PXQQFRUD.cjs} +11 -11
  50. package/dist/{chunk-J2JUAS4C.cjs.map → chunk-PXQQFRUD.cjs.map} +1 -1
  51. package/dist/{chunk-VDJF4ZAU.js → chunk-RU3SCECX.js} +3 -3
  52. package/dist/{chunk-53JKCFFE.cjs → chunk-RXFBW2EJ.cjs} +12 -12
  53. package/dist/{chunk-53JKCFFE.cjs.map → chunk-RXFBW2EJ.cjs.map} +1 -1
  54. package/dist/{chunk-JOCFPAS6.js → chunk-TNZOUHHN.js} +6 -6
  55. package/dist/{chunk-HX4CJYXL.js → chunk-TR7AMHGB.js} +10 -10
  56. package/dist/{chunk-MLGGF24T.js → chunk-TYVHM67X.js} +8 -8
  57. package/dist/{chunk-Z35QWWJH.cjs → chunk-U4WPFX5K.cjs} +34 -34
  58. package/dist/{chunk-Z35QWWJH.cjs.map → chunk-U4WPFX5K.cjs.map} +1 -1
  59. package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
  60. package/dist/{chunk-FEGEC6FY.js → chunk-X7AJ3J42.js} +5170 -5104
  61. package/dist/chunk-X7AJ3J42.js.map +1 -0
  62. package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
  63. package/dist/{chunk-CTMLY2GN.js → chunk-YGS7BLSN.js} +5 -5
  64. package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
  65. package/dist/{chunk-2AASLY6D.js → chunk-ZBQV7WH7.js} +3 -3
  66. package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
  67. package/dist/{chunk-LBOWSKGM.js → chunk-ZSIEM7CL.js} +5 -5
  68. package/dist/components/calendar.cjs +4 -4
  69. package/dist/components/calendar.js +3 -3
  70. package/dist/components/case-studies/index.cjs +17 -17
  71. package/dist/components/case-studies/index.js +7 -7
  72. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  73. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  74. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  75. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  76. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  77. package/dist/components/chat/index.cjs +13 -7
  78. package/dist/components/chat/index.cjs.map +1 -1
  79. package/dist/components/chat/index.js +12 -6
  80. package/dist/components/chat/remark-mention-chips.d.ts +5 -2
  81. package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
  82. package/dist/components/chat/utils/discuss-ref-prompt.d.ts +48 -0
  83. package/dist/components/chat/utils/discuss-ref-prompt.d.ts.map +1 -0
  84. package/dist/components/chat/utils/index.d.ts +2 -0
  85. package/dist/components/chat/utils/index.d.ts.map +1 -1
  86. package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +63 -0
  87. package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -0
  88. package/dist/components/chat/utils/source-row-cta.d.ts.map +1 -1
  89. package/dist/components/contact/index.cjs +8 -8
  90. package/dist/components/contact/index.js +7 -7
  91. package/dist/components/docs/index.cjs +10 -10
  92. package/dist/components/docs/index.js +9 -9
  93. package/dist/components/embeds/index.cjs +8 -8
  94. package/dist/components/embeds/index.js +7 -7
  95. package/dist/components/faq/index.cjs +9 -9
  96. package/dist/components/faq/index.js +8 -8
  97. package/dist/components/features/index.cjs +7 -7
  98. package/dist/components/features/index.js +6 -6
  99. package/dist/components/help-center-pages/index.cjs +28 -28
  100. package/dist/components/help-center-pages/index.js +19 -19
  101. package/dist/components/icons/index.cjs +3 -3
  102. package/dist/components/icons/index.js +2 -2
  103. package/dist/components/index.cjs +253 -247
  104. package/dist/components/index.cjs.map +1 -1
  105. package/dist/components/index.js +23 -17
  106. package/dist/components/index.js.map +1 -1
  107. package/dist/components/meeting-scheduler/index.cjs +92 -92
  108. package/dist/components/meeting-scheduler/index.js +9 -9
  109. package/dist/components/navigation/index.cjs +7 -7
  110. package/dist/components/navigation/index.js +6 -6
  111. package/dist/components/onboarding-guides/index.cjs +11 -11
  112. package/dist/components/onboarding-guides/index.js +10 -10
  113. package/dist/components/related-content/index.cjs +9 -9
  114. package/dist/components/related-content/index.js +8 -8
  115. package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +11 -3
  116. package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -1
  117. package/dist/components/shared/doc-search/use-doc-search.d.ts +7 -0
  118. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
  119. package/dist/components/tickets/index.cjs +11 -11
  120. package/dist/components/tickets/index.js +10 -10
  121. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  122. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  123. package/dist/components/ui/file-manager/index.cjs +70 -70
  124. package/dist/components/ui/file-manager/index.js +3 -3
  125. package/dist/components/ui/index.cjs +13 -7
  126. package/dist/components/ui/index.cjs.map +1 -1
  127. package/dist/components/ui/index.js +12 -6
  128. package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
  129. package/dist/components/ui/overlay-open-registry.d.ts +50 -0
  130. package/dist/components/ui/overlay-open-registry.d.ts.map +1 -0
  131. package/dist/components/ui/portal-container.d.ts +24 -0
  132. package/dist/components/ui/portal-container.d.ts.map +1 -1
  133. package/dist/hooks/index.cjs +4 -4
  134. package/dist/hooks/index.js +3 -3
  135. package/dist/index.cjs +20 -10
  136. package/dist/index.cjs.map +1 -1
  137. package/dist/index.js +24 -14
  138. package/dist/utils/anchor-id.d.ts +32 -0
  139. package/dist/utils/anchor-id.d.ts.map +1 -0
  140. package/dist/utils/content-href.d.ts +27 -6
  141. package/dist/utils/content-href.d.ts.map +1 -1
  142. package/dist/utils/index.cjs +213 -152
  143. package/dist/utils/index.cjs.map +1 -1
  144. package/dist/utils/index.d.ts +1 -0
  145. package/dist/utils/index.d.ts.map +1 -1
  146. package/dist/utils/index.js +212 -153
  147. package/dist/utils/index.js.map +1 -1
  148. package/dist/utils/list-url.d.ts.map +1 -1
  149. package/dist/utils/same-page-hash-nav.d.ts +17 -6
  150. package/dist/utils/same-page-hash-nav.d.ts.map +1 -1
  151. package/package.json +1 -1
  152. package/src/components/chat/__tests__/remark-mention-chips.test.ts +67 -0
  153. package/src/components/chat/chat-message-enhanced.tsx +3 -2
  154. package/src/components/chat/chat-message-list.tsx +49 -5
  155. package/src/components/chat/embeddable-chat.tsx +19 -2
  156. package/src/components/chat/entity-cards/dispatch.tsx +65 -4
  157. package/src/components/chat/hooks/use-nats-chat-adapter.ts +28 -4
  158. package/src/components/chat/hooks/use-sse-chat-adapter.ts +5 -3
  159. package/src/components/chat/remark-mention-chips.ts +6 -3
  160. package/src/components/chat/utils/__tests__/discuss-ref-prompt.test.ts +67 -0
  161. package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +156 -0
  162. package/src/components/chat/utils/discuss-ref-prompt.ts +63 -0
  163. package/src/components/chat/utils/index.ts +16 -0
  164. package/src/components/chat/utils/resolve-fetched-card-href.ts +104 -0
  165. package/src/components/chat/utils/source-row-cta.ts +5 -1
  166. package/src/components/shared/doc-search/__tests__/resolve-search-result-action.test.ts +145 -0
  167. package/src/components/shared/doc-search/resolve-search-result-action.ts +46 -10
  168. package/src/components/shared/doc-search/use-doc-search.ts +50 -14
  169. package/src/components/ui/__tests__/overlay-open-registry.test.tsx +87 -0
  170. package/src/components/ui/actions-menu.tsx +65 -2
  171. package/src/components/ui/dropdown-menu.tsx +62 -12
  172. package/src/components/ui/markdown/base-components.tsx +26 -0
  173. package/src/components/ui/overlay-open-registry.tsx +99 -0
  174. package/src/components/ui/portal-container.tsx +31 -0
  175. package/src/utils/__tests__/anchor-id.test.ts +139 -0
  176. package/src/utils/__tests__/content-href.test.ts +74 -0
  177. package/src/utils/__tests__/list-url.test.ts +11 -1
  178. package/src/utils/anchor-id.ts +106 -0
  179. package/src/utils/content-href.ts +113 -13
  180. package/src/utils/index.ts +8 -0
  181. package/src/utils/list-url.ts +6 -1
  182. package/src/utils/same-page-hash-nav.ts +28 -9
  183. package/dist/chunk-A62C4C4V.cjs.map +0 -1
  184. package/dist/chunk-BTUHMCYW.js.map +0 -1
  185. package/dist/chunk-DBR5JLX3.js.map +0 -1
  186. package/dist/chunk-EYTXD62B.cjs.map +0 -1
  187. package/dist/chunk-FEGEC6FY.js.map +0 -1
  188. package/dist/chunk-GE3JUTW2.js.map +0 -1
  189. package/dist/chunk-Q4K3TGNP.cjs.map +0 -1
  190. package/dist/chunk-RB2EW6IZ.cjs.map +0 -1
  191. package/dist/chunk-XJVRAKEX.cjs.map +0 -1
  192. /package/dist/{chunk-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
  193. /package/dist/{chunk-TBGSQTJK.js.map → chunk-J4VB4EHW.js.map} +0 -0
  194. /package/dist/{chunk-UWPT23SA.js.map → chunk-JXMNLH7F.js.map} +0 -0
  195. /package/dist/{chunk-HCZJZMU7.js.map → chunk-NKA3O4L6.js.map} +0 -0
  196. /package/dist/{chunk-SRYV4H3M.js.map → chunk-NNWSGOUP.js.map} +0 -0
  197. /package/dist/{chunk-WNOGR5AA.js.map → chunk-PRMR6WKC.js.map} +0 -0
  198. /package/dist/{chunk-VDJF4ZAU.js.map → chunk-RU3SCECX.js.map} +0 -0
  199. /package/dist/{chunk-JOCFPAS6.js.map → chunk-TNZOUHHN.js.map} +0 -0
  200. /package/dist/{chunk-HX4CJYXL.js.map → chunk-TR7AMHGB.js.map} +0 -0
  201. /package/dist/{chunk-MLGGF24T.js.map → chunk-TYVHM67X.js.map} +0 -0
  202. /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
  203. /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
  204. /package/dist/{chunk-CTMLY2GN.js.map → chunk-YGS7BLSN.js.map} +0 -0
  205. /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
  206. /package/dist/{chunk-2AASLY6D.js.map → chunk-ZBQV7WH7.js.map} +0 -0
  207. /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
  208. /package/dist/{chunk-LBOWSKGM.js.map → chunk-ZSIEM7CL.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"list-url.d.ts","sourceRoot":"","sources":["../../src/utils/list-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAWH;;;;iEAIiE;AACjE,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEtE;AA6BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,MAAM,GAAG,IAAI,CAa5F"}
1
+ {"version":3,"file":"list-url.d.ts","sourceRoot":"","sources":["../../src/utils/list-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAWH;;;;iEAIiE;AACjE,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAOtE;AA6BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,MAAM,GAAG,IAAI,CAa5F"}
@@ -15,12 +15,23 @@ export declare const HUB_HEADER_OFFSET_PX = 80;
15
15
  */
16
16
  export declare function normalizeHashFragment(hash: string): string;
17
17
  /**
18
- * Resolve the DOM element a hash fragment id points at. Tries the raw
19
- * id first (ordinary anchors), then the percent-decoded form — href
20
- * composers like `buildTicketOpenHref` encode the id into the fragment
21
- * while rows render the raw id, so `#ticket-a%2Fb` must still find
22
- * `id="ticket-a/b"`. A malformed escape sequence falls back to the
23
- * raw lookup result.
18
+ * Resolve the DOM element a hash fragment id points at THE hash resolver;
19
+ * `navigateSamePageHash`, `useScrollToHash` and the markdown link renderer
20
+ * all go through this one, so a fragment that resolves for a deep link
21
+ * resolves for an in-body click too.
22
+ *
23
+ * Three passes, cheapest first:
24
+ * 1. the raw id (ordinary anchors — the overwhelming common case);
25
+ * 2. the percent-decoded form — href composers like `buildTicketOpenHref`
26
+ * encode the id into the fragment while rows render the raw id, so
27
+ * `#ticket-a%2Fb` must still find `id="ticket-a/b"`. A malformed escape
28
+ * sequence just falls through;
29
+ * 3. a NORMALIZED match against the document's headings — doc bodies are
30
+ * authored on GitHub, whose slugger keeps the hyphen left behind by a
31
+ * stripped emoji (`## 📚 Table of Contents` → `#-table-of-contents`)
32
+ * while our ids trim it. Without this every in-body TOC link in every
33
+ * GitHub-authored doc resolves to nothing and clicking one does nothing.
34
+ * See `utils/anchor-id`.
24
35
  */
25
36
  export declare function getHashTargetElement(id: string): HTMLElement | null;
26
37
  export interface NavigateSamePageHashOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"same-page-hash-nav.d.ts","sourceRoot":"","sources":["../../src/utils/same-page-hash-nav.ts"],"names":[],"mappings":"AAEA;;sCAEsC;AACtC,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC;yCACyC;AACzC,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAWnE;AAED,MAAM,WAAW,2BAA2B;IAC1C,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;gEAC4D;IAC5D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAkET"}
1
+ {"version":3,"file":"same-page-hash-nav.d.ts","sourceRoot":"","sources":["../../src/utils/same-page-hash-nav.ts"],"names":[],"mappings":"AAGA;;sCAEsC;AACtC,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC;yCACyC;AACzC,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAkBnE;AAED,MAAM,WAAW,2BAA2B;IAC1C,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;gEAC4D;IAC5D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAkET"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.510",
3
+ "version": "0.0.511",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { Root, Text, Link, Paragraph } from 'mdast'
3
+ import { remarkMentionChips } from '../remark-mention-chips'
4
+
5
+ /** One-paragraph tree holding a single text leaf — what the plugin walks. */
6
+ function treeOf(text: string): Root {
7
+ return { type: 'root', children: [{ type: 'paragraph', children: [{ type: 'text', value: text }] }] }
8
+ }
9
+
10
+ /**
11
+ * unified's `Plugin` type declares a `this: Processor` parameter, so calling the
12
+ * attacher bare (outside a pipeline) is a TS2684 — cast the attacher itself, not
13
+ * just its return value, to drop that `this`.
14
+ */
15
+ const attach = remarkMentionChips as unknown as () => (t: Root) => void
16
+
17
+ function run(text: string): Array<Text | Link> {
18
+ const tree = treeOf(text)
19
+ attach()(tree)
20
+ return (tree.children[0] as Paragraph).children as Array<Text | Link>
21
+ }
22
+
23
+ /** `mention://marker:id` urls the plugin emitted, in order. */
24
+ function mentions(text: string): string[] {
25
+ return run(text).flatMap((n) => (n.type === 'link' ? [n.url] : []))
26
+ }
27
+
28
+ describe('remarkMentionChips markers', () => {
29
+ it('parses the lowercase markers', () => {
30
+ expect(mentions('see @device:64f0a1 and @kb:5')).toEqual([
31
+ 'mention://device:64f0a1',
32
+ 'mention://kb:5',
33
+ ])
34
+ })
35
+
36
+ it('parses a camelCase marker', () => {
37
+ // Regression: `ContextItemType.SCHEDULED_SCRIPT` ships as `scheduledScript`,
38
+ // and a lowercase-only grammar matched NOTHING in the token — the mention
39
+ // silently stayed raw text in the bubble instead of becoming a chip.
40
+ expect(mentions('run @scheduledScript:6a6c9342f8e10b2477900c08 now')).toEqual([
41
+ 'mention://scheduledScript:6a6c9342f8e10b2477900c08',
42
+ ])
43
+ })
44
+
45
+ it('keeps the surrounding text intact around a camelCase mention', () => {
46
+ const parts = run('run @scheduledScript:6a6c9342f8e10b2477900c08 now')
47
+ expect(parts.map((n) => (n.type === 'text' ? n.value : `[${(n as Link).url}]`))).toEqual([
48
+ 'run ',
49
+ '[mention://scheduledScript:6a6c9342f8e10b2477900c08]',
50
+ ' now',
51
+ ])
52
+ })
53
+
54
+ it('fires inside punctuation but never mid-word', () => {
55
+ expect(mentions('(@scheduledScript:6a6c9342f8e10b2477900c08)')).toEqual([
56
+ 'mention://scheduledScript:6a6c9342f8e10b2477900c08',
57
+ ])
58
+ // An email-shaped host:port is not a mention — the `@` is preceded by a word char.
59
+ expect(mentions('mail user@Host:1234 stays text')).toEqual([])
60
+ })
61
+
62
+ it('leaves a trailing sentence period out of the id', () => {
63
+ expect(mentions('ran @scheduledScript:6a6c9342f8e10b2477900c08.')).toEqual([
64
+ 'mention://scheduledScript:6a6c9342f8e10b2477900c08',
65
+ ])
66
+ })
67
+ })
@@ -27,8 +27,9 @@ import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from ".
27
27
  * (e.g. it matches `x@device:1` mid-word, which the plugin skips), so a context
28
28
  * item would be stripped from the chip strip yet never rendered inline — lost
29
29
  * from display entirely. The id is capture group 2 (group 1 is the boundary).
30
- * Marker lowercase; id is the mention-token charset. */
31
- const MENTION_MARKER_REGEX = /(^|[^\w@])@[a-z]+:([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
30
+ * Marker `[a-zA-Z]+` (markers are mostly lowercase but not all e.g.
31
+ * `scheduledScript`); id is the mention-token charset. */
32
+ const MENTION_MARKER_REGEX = /(^|[^\w@])@[a-zA-Z]+:([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
32
33
 
33
34
  /**
34
35
  * Same regex shape as `remarkCardLinks` — kept in lockstep so the
@@ -6,6 +6,7 @@ import { cn } from "../../utils/cn"
6
6
  import { ChatMessageEnhanced } from "./chat-message-enhanced"
7
7
  import { ChatMessageListSkeleton } from "./chat-message-skeleton"
8
8
  import { DotsLoaderIcon, Arrow02DownIcon } from "../icons-v2-generated"
9
+ import { OverlayOpenRegistryProvider } from "../ui/overlay-open-registry"
9
10
  import { CyclingPhrase } from "./cycling-phrase"
10
11
  import type { ChatMessageListProps } from "./types"
11
12
  import { SCROLL_ANCHOR } from "./types/message.types"
@@ -141,10 +142,44 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
141
142
  // effect below owns first-paint positioning so re-opening a chat
142
143
  // with prior history lands at the bottom without a smooth-scroll
143
144
  // animation playing over the cold-paint.
144
- const { scrollRef, contentRef, scrollToBottom, stopScroll } = useStickToBottom({
145
- resize: 'smooth',
146
- initial: false,
147
- })
145
+ const { scrollRef, contentRef, scrollToBottom, stopScroll, isAtBottom } =
146
+ useStickToBottom({
147
+ resize: 'smooth',
148
+ initial: false,
149
+ })
150
+
151
+ // ---- Overlay guard: don't move the ground under an open menu -------
152
+ // A ⋯ menu inside a bubble stays anchored to its trigger. While a reply
153
+ // streams in, the follow-the-bottom scroll carries that trigger upward and
154
+ // drags the menu with it, until the menu sits at the panel edge detached
155
+ // from its card. So: while any overlay in this thread is open, stop
156
+ // chasing the bottom (the same thing ChatGPT/Claude do once you interact
157
+ // with the transcript), then catch up on close if we were at the bottom
158
+ // when it opened. New content still renders — we simply stop scrolling to
159
+ // it, and the existing "scroll to bottom" affordance stays available.
160
+ const overlayOpenRef = useRef(false)
161
+ const wasAtBottomOnOpenRef = useRef(false)
162
+ const isAtBottomRef = useRef(isAtBottom)
163
+ isAtBottomRef.current = isAtBottom
164
+ const handleOverlayOpenChange = useCallback(
165
+ (hasOpenOverlay: boolean) => {
166
+ overlayOpenRef.current = hasOpenOverlay
167
+ if (hasOpenOverlay) {
168
+ wasAtBottomOnOpenRef.current = isAtBottomRef.current
169
+ // Cancel any in-flight spring so the thread settles immediately
170
+ // instead of drifting for another few hundred ms under the menu.
171
+ stopScroll()
172
+ return
173
+ }
174
+ // Closed again: resume only if the reader was pinned to the bottom
175
+ // before opening it. Someone who had scrolled up to read history keeps
176
+ // their position.
177
+ if (wasAtBottomOnOpenRef.current) {
178
+ void scrollToBottom({ animation: 'smooth' })
179
+ }
180
+ },
181
+ [scrollToBottom, stopScroll],
182
+ )
148
183
 
149
184
  // ---- Prepend / load-more state (NOT owned by the library) --------
150
185
  const sentinelRef = useRef<HTMLDivElement>(null)
@@ -263,7 +298,12 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
263
298
  const prevLastIdx = prevLastId ? messages.findIndex((m) => m.id === prevLastId) : -1
264
299
  const newSlice = prevLastIdx >= 0 ? messages.slice(prevLastIdx + 1) : messages.slice(prevCount)
265
300
  const hasNewUser = newSlice.some((m) => m.role === 'user')
266
- if (hasNewUser) {
301
+ // An open overlay suppresses even this one: `ignoreEscapes` would yank
302
+ // the thread out from under it — and with it the follow intent, which
303
+ // would re-assert the jump on every later resize. A user who just sent
304
+ // a message with a menu open is rare; a menu jumping away mid-click is
305
+ // not worth it.
306
+ if (hasNewUser && !overlayOpenRef.current) {
267
307
  // Arms the follow intent for the WHOLE turn: the answer's
268
308
  // async growth (attachment images, streamed tokens, entity
269
309
  // cards resolving out of their skeletons long after the
@@ -657,6 +697,8 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
657
697
 
658
698
  useLayoutEffect(() => {
659
699
  if (!autoScroll) return
700
+ // Top-anchored display turns also move the ground — same guard.
701
+ if (overlayOpenRef.current) return
660
702
  const last = messages[messages.length - 1]
661
703
  if (!last || last.role !== 'assistant' || last.scrollAnchor !== SCROLL_ANCHOR.TOP) return
662
704
  // Metadata frame arrives BEFORE body. Wait for content so we don't
@@ -847,6 +889,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
847
889
  !(pendingApprovals && pendingApprovals.length > 0)
848
890
 
849
891
  return (
892
+ <OverlayOpenRegistryProvider onOpenChange={handleOverlayOpenChange}>
850
893
  <div className="flex-1 min-h-0 flex flex-col">
851
894
  {/* Positioning box for the jump-to-bottom button: it wraps ONLY the
852
895
  scroller, so `absolute bottom-…` really is the scroller's lower
@@ -1029,6 +1072,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
1029
1072
  </div>
1030
1073
  )}
1031
1074
  </div>
1075
+ </OverlayOpenRegistryProvider>
1032
1076
  )
1033
1077
  },
1034
1078
  )
@@ -52,7 +52,7 @@ import { MingoHistoryRail } from './mingo-history-rail'
52
52
  import { GuideWelcome, type GuideWelcomeProps } from './guide-welcome'
53
53
  import { accentFromIdentityIcon, getAgentAccent } from './quick-action-chip'
54
54
  import { GuideModeBanner } from './guide-mode-banner'
55
- import { PortalContainerContext } from '../ui/portal-container'
55
+ import { CollisionBoundaryContext, PortalContainerContext } from '../ui/portal-container'
56
56
  import { ChatPanelHeader, type ChatPanelHeaderProps } from './chat-panel-header'
57
57
  import { SquareAvatar } from '../ui/square-avatar'
58
58
  import { ChatHeaderIconButton } from './chat-header-icon-button'
@@ -1795,6 +1795,13 @@ function EmbeddableChatInner({
1795
1795
 
1796
1796
  // Host node for in-panel Radix portals (see the body wrapper below).
1797
1797
  const [portalHost, setPortalHost] = useState<HTMLDivElement | null>(null)
1798
+ // The panel element itself, published as the COLLISION boundary for overlays
1799
+ // opened inside it. Radix otherwise collides against the viewport, so a ⋯ menu
1800
+ // near the top of the thread flips upward past the panel and renders over the
1801
+ // app header (reported: an entity card's menu landing in the top-right corner,
1802
+ // detached from its card). `portalHost` can't serve here — it is
1803
+ // `display: contents` and has no box to measure.
1804
+ const [panelBoundary, setPanelBoundary] = useState<HTMLDivElement | null>(null)
1798
1805
 
1799
1806
  // ── Split (wide) Mingo layout ─────────────────────────────────────────────
1800
1807
  // The panel measures its own width and, in Mingo mode, promotes the inline
@@ -1817,6 +1824,14 @@ function EmbeddableChatInner({
1817
1824
  return () => ro.disconnect()
1818
1825
  }, [])
1819
1826
 
1827
+ // One ref for two consumers: the width measurement above (a plain ref, read
1828
+ // once on mount) and the collision-boundary context (state, so provider
1829
+ // consumers re-render once the node exists).
1830
+ const setPanelNode = useCallback((node: HTMLDivElement | null) => {
1831
+ panelMeasureRef.current = node
1832
+ setPanelBoundary(node)
1833
+ }, [])
1834
+
1820
1835
  // Rail collapse toggle (Figma ⟶| control), persisted so it survives the
1821
1836
  // drawer remounting on close.
1822
1837
  const [railCollapsed, setRailCollapsed] = useState(false)
@@ -2003,6 +2018,7 @@ function EmbeddableChatInner({
2003
2018
  // document root. See `PortalContainerContext`.
2004
2019
  const body = (
2005
2020
  <PortalContainerContext.Provider value={portalHost}>
2021
+ <CollisionBoundaryContext.Provider value={panelBoundary}>
2006
2022
  {/* Panel surface depends on state (Figma):
2007
2023
  • Narrow "Current Chats" LIST + FULL-PANEL archive page → grey
2008
2024
  `ods-card` (#212121) — matching the grey rail those lists live
@@ -2017,7 +2033,7 @@ function EmbeddableChatInner({
2017
2033
  grey), so the chat block on the right must keep the conversation's
2018
2034
  dark surface instead of following the archive. */}
2019
2035
  <div
2020
- ref={panelMeasureRef}
2036
+ ref={setPanelNode}
2021
2037
  className={`flex h-full flex-col overflow-hidden transition-colors duration-200 ${
2022
2038
  (archiveOpen && !splitActive) || stackedListView
2023
2039
  ? 'bg-ods-card'
@@ -2587,6 +2603,7 @@ function EmbeddableChatInner({
2587
2603
  {/* Portal target for in-panel Radix overlays — `display: contents`
2588
2604
  so it adds no box; content is positioned `fixed` by Radix. */}
2589
2605
  <div ref={setPortalHost} style={{ display: 'contents' }} />
2606
+ </CollisionBoundaryContext.Provider>
2590
2607
  </PortalContainerContext.Provider>
2591
2608
  )
2592
2609
 
@@ -36,6 +36,7 @@ import type { ChatRef } from '../chat-ref.types'
36
36
  import { useChatCardItem } from '../hooks/use-chat-card-item'
37
37
  import { handleChatNavClick } from '../utils/nav-click-handler'
38
38
  import { resolveSourceRowCTA, resolveSourceIcon, sourceRowCtxFromRuntime } from '../utils/source-row-cta'
39
+ import { resolveFetchedCardHref, readFetchedCardTitle } from '../utils/resolve-fetched-card-href'
39
40
  import { resolveHrefForRuntime } from '../utils/chat-nav-resolution'
40
41
  import {
41
42
  computeIsNewTab,
@@ -1000,8 +1001,15 @@ type ChatCardRegistryEntry =
1000
1001
  * resolved ref — so the wrapper sees a non-null `href`, the
1001
1002
  * pre-computed `isNewTab` reflects the actual destination, and
1002
1003
  * the click goes through `handleChatNavClick` (no silent
1003
- * bypass of embed-mode / close-on-nav). */
1004
+ * bypass of embed-mode / close-on-nav). Takes precedence over the
1005
+ * `composeContentUrl` fallback below. */
1004
1006
  fallbackHref?: (item: any) => string | null
1007
+ /** Opt OUT of the post-fetch `composeContentUrl` fallback (see
1008
+ * `resolveFetchedCardHref`). Set for types with NO public destination
1009
+ * — the seam would otherwise synthesize `<contentOrigin>/<type>/<id>`
1010
+ * from its default branch and hand the user a 404. Types with a real
1011
+ * route (hosted, or covered by a host `override`) leave it unset. */
1012
+ noComposedHref?: boolean
1005
1013
  }
1006
1014
 
1007
1015
  interface FinancialCardConfig {
@@ -1106,6 +1114,8 @@ interface RoadmapEntryConfig {
1106
1114
  label: string
1107
1115
  cardType: RoadmapCardType
1108
1116
  contentRefType: string
1117
+ /** See `ChatCardRegistryEntry.noComposedHref`. */
1118
+ noComposedHref?: boolean
1109
1119
  }
1110
1120
  const ROADMAP_CARD_CONFIGS: Record<string, RoadmapEntryConfig> = {
1111
1121
  roadmap_item: { label: 'Roadmap item', cardType: 'roadmap_item', contentRefType: 'roadmap_item' },
@@ -1118,6 +1128,9 @@ const ROADMAP_CARD_CONFIGS: Record<string, RoadmapEntryConfig> = {
1118
1128
  label: 'Internal task',
1119
1129
  cardType: 'internal_task',
1120
1130
  contentRefType: 'internal_task',
1131
+ // Internal ClickUp work has no public destination on any platform — the
1132
+ // seam's default branch would synthesize `<hub>/internal_task/<id>`.
1133
+ noComposedHref: true,
1121
1134
  },
1122
1135
  }
1123
1136
  function roadmapRegistryEntries(): Record<string, ChatCardRegistryEntry> {
@@ -1128,6 +1141,7 @@ function roadmapRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1128
1141
  label: cfg.label,
1129
1142
  contentRefType: cfg.contentRefType,
1130
1143
  bareInline: true,
1144
+ ...(cfg.noComposedHref ? { noComposedHref: true } : {}),
1131
1145
  skeleton: () => <RoadmapCardSkeleton size="sm" />,
1132
1146
  render: (item, chatRef, opts) => (
1133
1147
  <RoadmapChatCard
@@ -1420,6 +1434,17 @@ function ChatCardNavWrap({
1420
1434
  const onClickCapture = (e: React.MouseEvent<HTMLElement>) => {
1421
1435
  if (!href) return
1422
1436
  const targetEl = e.target as HTMLElement
1437
+ // Only claim clicks that physically happened INSIDE this card.
1438
+ //
1439
+ // The card's "⋯" menu renders through a React PORTAL: its DOM lives in the
1440
+ // panel's portal host, but React still bubbles its events through this
1441
+ // subtree — so this capture handler sees them first and, being a capture
1442
+ // phase, runs BEFORE the menu row's own `stopPropagation`. That swallowed
1443
+ // the menu's trailing "↗ Open in new tab" button: `executeNavigation`
1444
+ // called `preventDefault()` and routed the card's href in the SAME tab,
1445
+ // so the anchor's `target="_blank"` never applied.
1446
+ const host = e.currentTarget as HTMLElement | null
1447
+ if (!host?.contains(targetEl)) return
1423
1448
  // Buttons rendered INSIDE the card's outer `<a>` (e.g. RoadmapCard
1424
1449
  // vote buttons, ImageGallery thumbnails) bubble up with
1425
1450
  // `closest('a')` truthy — without this guard, clicking them would
@@ -1522,18 +1547,54 @@ export function ChatCardLoader({
1522
1547
  )
1523
1548
  if (!entry) return null
1524
1549
 
1525
- // Apply per-type fallback URL AFTER fetch (e.g. campaign /admin/...).
1550
+ // Apply the post-fetch URL fallback (the ref carried no `externalUrl`).
1526
1551
  // We mutate `resolvedChatRef.url` BEFORE computing isNewTab so the
1527
1552
  // wrapper's interceptor sees the destination the user will actually
1528
1553
  // visit. `safeHref` blocks `javascript:` / `data:` payloads even
1529
1554
  // though the registry callers compose hub-internal strings today.
1530
- const finalChatRef: ChatRef =
1555
+ //
1556
+ // Two sources, in order:
1557
+ // 1. the registry's per-type `fallbackHref` — an explicit non-content
1558
+ // destination (marketing campaign → `/admin/...`);
1559
+ // 2. the host's `composeContentUrl` seam via `resolveFetchedCardHref` —
1560
+ // the SAME resolver page cards and SSE chat cards go through. This is
1561
+ // what makes Mingo/NATS cards clickable: that transport ships bare
1562
+ // `[card://type:id]` markers with no refs metadata, so the ref reaches
1563
+ // us with `url: null` and `resolveSourceRowCTA` has nothing to route.
1564
+ const composedHref =
1565
+ fetchEntry && !resolvedChatRef.url && item && !fetchEntry.fallbackHref && !fetchEntry.noComposedHref
1566
+ ? resolveFetchedCardHref({
1567
+ contentRefType: fetchEntry.contentRefType,
1568
+ id: resolvedChatRef.id,
1569
+ item,
1570
+ composeContentUrl: runtime.composeContentUrl,
1571
+ })
1572
+ : null
1573
+ const hrefResolvedChatRef: ChatRef =
1531
1574
  fetchEntry && !resolvedChatRef.url && item && fetchEntry.fallbackHref
1532
1575
  ? {
1533
1576
  ...resolvedChatRef,
1534
1577
  url: safeHref(fetchEntry.fallbackHref(item)),
1535
1578
  }
1536
- : resolvedChatRef
1579
+ : composedHref
1580
+ ? {
1581
+ ...resolvedChatRef,
1582
+ url: safeHref(composedHref.href),
1583
+ targetPlatform: composedHref.targetPlatform ?? resolvedChatRef.targetPlatform ?? null,
1584
+ }
1585
+ : resolvedChatRef
1586
+
1587
+ // Title enrichment, same synthetic-ref gap as the href above: a Mingo
1588
+ // `[card://type:id]` marker produces `title: <id>` because the transport
1589
+ // ships no ref metadata. Downstream consumers that read `ref.title` — most
1590
+ // visibly the "Ask Mingo" prompt, which would otherwise send
1591
+ // "Tell me more about 86ad3qvv5" — get the row's real title once it loads.
1592
+ // A ref that already carries a distinct title (the SSE path) is untouched.
1593
+ const fetchedTitle = fetchEntry && item ? readFetchedCardTitle(item) : null
1594
+ const finalChatRef: ChatRef =
1595
+ fetchedTitle && (!hrefResolvedChatRef.title || hrefResolvedChatRef.title === hrefResolvedChatRef.id)
1596
+ ? { ...hrefResolvedChatRef, title: fetchedTitle }
1597
+ : hrefResolvedChatRef
1537
1598
 
1538
1599
  // Pre-compute new-tab decision ONCE here (the same rule chips use).
1539
1600
  // Render branches that pass `target` / `rel` to their card pull this
@@ -56,6 +56,7 @@ import { createChatDialogStore, DEFAULT_DIALOG_SIDE } from '../stream/chat-dialo
56
56
  import { useChatStreamReducer } from '../stream/use-chat-stream-reducer'
57
57
  import { processHistoricalMessagesWithErrors } from '../utils/process-historical-messages'
58
58
  import { extractIncompleteTailState } from '../utils/extract-incomplete-message-state'
59
+ import { buildDiscussPrompt } from '../utils/discuss-ref-prompt'
59
60
  import type {
60
61
  ChunkData,
61
62
  FetchChunksFunction,
@@ -1076,10 +1077,33 @@ export function useNatsChatAdapter(
1076
1077
  [],
1077
1078
  )
1078
1079
 
1079
- // No-op refs — Mingo agent has no RAG entity-card affordances.
1080
- const discussRef = useCallback((_ref: ChatRef) => {
1081
- /* no-op in Mingo mode */
1082
- }, [])
1080
+ // "Ask Mingo" on an inline entity card. The agent transport has NO
1081
+ // entity-id-filtered retrieval (its `ContextItemType` enum covers DEVICE /
1082
+ // SCRIPT / TICKET / ORGANIZATION / USER / KB_ARTICLE / POLICY / QUERY /
1083
+ // SCHEDULED_SCRIPT — no content types), so unlike the SSE path there is no
1084
+ // `commandOverride.entityIdFilter` to send: the prompt carries the type + id
1085
+ // as text and the agent resolves the row itself. Same sentence as guide mode
1086
+ // (shared builder) so the affordance reads identically in both.
1087
+ //
1088
+ // This used to be a no-op, which made the card's "Ask Mingo" menu item dead
1089
+ // in Mingo mode. Swap `includeReference` for a structured filter once the
1090
+ // agent backend accepts one.
1091
+ const discussRef = useCallback(
1092
+ (reference: ChatRef) => {
1093
+ const prompt = buildDiscussPrompt(reference, { includeReference: true })
1094
+ // Menu-item click, so there is no composer promise for the rejection to
1095
+ // surface through — a bare `void` would leave it unhandled. Logged in the
1096
+ // same shape as `stopMessage`'s. NOTE: recovering the optimistic rows and
1097
+ // the stuck `thinking` phase after a failed publish is a `sendMessage`
1098
+ // concern (it affects composer sends identically) and is not done here.
1099
+ void sendMessage(prompt).catch((err) => {
1100
+ console.error('[useNatsChatAdapter] discussRef send failed:', err)
1101
+ })
1102
+ },
1103
+ [sendMessage],
1104
+ )
1105
+ // Display stays a no-op: it dispatches a `/… display "<x>"` slash command,
1106
+ // and the agent transport has no slash-command registry.
1083
1107
  const displayRef = useCallback((_ref: ChatRef) => {
1084
1108
  /* no-op in Mingo mode */
1085
1109
  }, [])
@@ -67,6 +67,7 @@ import {
67
67
  } from '../utils/chat-conversation-storage'
68
68
  import { useChatHistoryHydration } from './use-chat-history-hydration'
69
69
  import { sanitizeTitleForChat } from '../utils/slash-dispatch-utils'
70
+ import { buildDiscussPrompt } from '../utils/discuss-ref-prompt'
70
71
  import { defaultTableIdForDocumentType } from '../utils/source-icons'
71
72
  import type { WireCommandOverride } from '../utils/slash-dispatch-utils'
72
73
  import type { ChatAttachment } from '../utils/chat-attachment-markdown'
@@ -657,9 +658,10 @@ export function useSseChatAdapter(
657
658
  }
658
659
  // RETRIEVAL IS STRICTLY PRIMARY-KEY-DRIVEN. The visible prose
659
660
  // ("Tell me more about <title>") is UX-only; retrieval narrows
660
- // via `entityIdFilter` before the LLM is invoked.
661
- const sanitizedTitle = sanitizeTitleForChat(reference.title)
662
- const prompt = `Tell me more about ${sanitizedTitle || 'this item'}`
661
+ // via `entityIdFilter` before the LLM is invoked. The sentence itself
662
+ // comes from the shared builder so the Mingo transport (which has no
663
+ // id filter and leans on the prose) phrases it identically.
664
+ const prompt = buildDiscussPrompt(reference)
663
665
  sendMessage(prompt, {
664
666
  commandOverride: { entityIdFilter: { tableId, id: refId } },
665
667
  })
@@ -15,9 +15,12 @@
15
15
  * intact, and the marker disappears before any rehype-raw HTML pass.
16
16
  *
17
17
  * Grammar:
18
- * - marker: `[a-z]+` — the backend `ContextItemType.marker()` short form
18
+ * - marker: `[a-zA-Z]+` — the backend `ContextItemType.marker()` short form
19
19
  * (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,
20
- * `query`, …). Always lowercase.
20
+ * `query`, `scheduledScript`, …). Mostly lowercase, but NOT guaranteed to be:
21
+ * the marker is matched verbatim by the backend's `MentionParser`, and
22
+ * `SCHEDULED_SCRIPT` ships as camelCase `scheduledScript` — a lowercase-only
23
+ * grammar silently dropped that whole token (no chip, raw text in the bubble).
21
24
  * - id: charset `[A-Za-z0-9_.+/=-]` (UUIDs with hyphens, fleet numeric ids,
22
25
  * base64 global ids, etc.; matches the composer's `MENTION_TOKEN`) — but the
23
26
  * id may NOT END in `.` or `-`, so a trailing SENTENCE period/dash isn't
@@ -35,7 +38,7 @@ import type { Plugin } from 'unified'
35
38
  import type { Root, Text, Link } from 'mdast'
36
39
  import { visit, SKIP } from 'unist-util-visit'
37
40
 
38
- const MENTION_REGEX = /(^|[^\w@])@([a-z]+):([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
41
+ const MENTION_REGEX = /(^|[^\w@])@([a-zA-Z]+):([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
39
42
 
40
43
  export const remarkMentionChips: Plugin<[], Root> = () => {
41
44
  return (tree: Root) => {
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildDiscussPrompt } from '../discuss-ref-prompt'
3
+ import { readFetchedCardTitle } from '../resolve-fetched-card-href'
4
+
5
+ const ROADMAP_REF = {
6
+ type: 'roadmap_item',
7
+ id: '86ad3qvv5',
8
+ title: 'Deep Google Workspace tenant management',
9
+ }
10
+
11
+ describe('buildDiscussPrompt', () => {
12
+ it('guide/SSE shape — prose only, retrieval rides entityIdFilter', () => {
13
+ expect(buildDiscussPrompt(ROADMAP_REF)).toBe(
14
+ 'Tell me more about Deep Google Workspace tenant management',
15
+ )
16
+ })
17
+
18
+ it('Mingo shape — appends the reference the agent has no filter for', () => {
19
+ expect(buildDiscussPrompt(ROADMAP_REF, { includeReference: true })).toBe(
20
+ 'Tell me more about Deep Google Workspace tenant management (roadmap_item 86ad3qvv5)',
21
+ )
22
+ })
23
+
24
+ it('a synthetic ref (title === id) never leaks the id as the subject', () => {
25
+ const synthetic = { type: 'roadmap_item', id: '86ad3qvv5', title: '86ad3qvv5' }
26
+ expect(buildDiscussPrompt(synthetic, { includeReference: true })).toBe(
27
+ 'Tell me more about this item (roadmap_item 86ad3qvv5)',
28
+ )
29
+ })
30
+
31
+ it('missing / blank title falls back to "this item"', () => {
32
+ expect(buildDiscussPrompt({ type: 'blog_post', id: 'b1' })).toBe('Tell me more about this item')
33
+ expect(buildDiscussPrompt({ type: 'blog_post', id: 'b1', title: ' ' })).toBe(
34
+ 'Tell me more about this item',
35
+ )
36
+ })
37
+
38
+ it('no id → no reference suffix', () => {
39
+ expect(buildDiscussPrompt({ type: 'blog_post', id: '', title: 'Hello' }, { includeReference: true })).toBe(
40
+ 'Tell me more about Hello',
41
+ )
42
+ })
43
+ })
44
+
45
+ describe('readFetchedCardTitle — what upgrades a synthetic ref before the prompt', () => {
46
+ it('prefers title, then name', () => {
47
+ expect(readFetchedCardTitle({ title: 'Roadmap row' })).toBe('Roadmap row')
48
+ expect(readFetchedCardTitle({ name: 'Named row' })).toBe('Named row')
49
+ expect(readFetchedCardTitle({ title: ' ', name: 'Named row' })).toBe('Named row')
50
+ })
51
+
52
+ it('null for rows with neither (and for non-objects)', () => {
53
+ expect(readFetchedCardTitle({ headline: 'nope' })).toBeNull()
54
+ expect(readFetchedCardTitle(null)).toBeNull()
55
+ expect(readFetchedCardTitle('string')).toBeNull()
56
+ expect(readFetchedCardTitle({ title: 42 })).toBeNull()
57
+ })
58
+
59
+ it('end to end: fetched row title reaches the prompt', () => {
60
+ const synthetic = { type: 'roadmap_item', id: '86ad3qvv5', title: '86ad3qvv5' }
61
+ const row = { id: 'db-9', title: 'Deep Google Workspace tenant management' }
62
+ const enriched = { ...synthetic, title: readFetchedCardTitle(row) ?? synthetic.title }
63
+ expect(buildDiscussPrompt(enriched, { includeReference: true })).toBe(
64
+ 'Tell me more about Deep Google Workspace tenant management (roadmap_item 86ad3qvv5)',
65
+ )
66
+ })
67
+ })