@flamingo-stack/openframe-frontend-core 0.0.510 → 0.0.511-snapshot.20260804140004

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 (248) hide show
  1. package/dist/chat-protocol/decode.d.ts.map +1 -1
  2. package/dist/chat-protocol/frames.d.ts +26 -1
  3. package/dist/chat-protocol/frames.d.ts.map +1 -1
  4. package/dist/chat-protocol/index.cjs +24 -0
  5. package/dist/chat-protocol/index.cjs.map +1 -1
  6. package/dist/chat-protocol/index.js +24 -0
  7. package/dist/chat-protocol/index.js.map +1 -1
  8. package/dist/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
  9. package/dist/chunk-27OKD7ZV.js.map +1 -0
  10. package/dist/{chunk-RHQ7BJKK.cjs → chunk-2WWDZR4D.cjs} +73 -73
  11. package/dist/{chunk-RHQ7BJKK.cjs.map → chunk-2WWDZR4D.cjs.map} +1 -1
  12. package/dist/{chunk-J2JUAS4C.cjs → chunk-2XL7UK5Y.cjs} +11 -11
  13. package/dist/{chunk-J2JUAS4C.cjs.map → chunk-2XL7UK5Y.cjs.map} +1 -1
  14. package/dist/{chunk-Q4K3TGNP.cjs → chunk-3NECEXDF.cjs} +74 -44
  15. package/dist/chunk-3NECEXDF.cjs.map +1 -0
  16. package/dist/{chunk-JOCFPAS6.js → chunk-4NFDSMB5.js} +6 -6
  17. package/dist/{chunk-SYZFFRVD.cjs → chunk-5BAGE7AI.cjs} +13 -13
  18. package/dist/{chunk-SYZFFRVD.cjs.map → chunk-5BAGE7AI.cjs.map} +1 -1
  19. package/dist/{chunk-53JKCFFE.cjs → chunk-5JBDT2MQ.cjs} +12 -12
  20. package/dist/{chunk-53JKCFFE.cjs.map → chunk-5JBDT2MQ.cjs.map} +1 -1
  21. package/dist/{chunk-XJVRAKEX.cjs → chunk-67YDSTH7.cjs} +56 -33
  22. package/dist/chunk-67YDSTH7.cjs.map +1 -0
  23. package/dist/{chunk-WNOGR5AA.js → chunk-6LMTBNLN.js} +3 -3
  24. package/dist/{chunk-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
  25. package/dist/{chunk-JMBIGH4H.cjs → chunk-7VBEOGCR.cjs} +83 -83
  26. package/dist/{chunk-JMBIGH4H.cjs.map → chunk-7VBEOGCR.cjs.map} +1 -1
  27. package/dist/{chunk-5AZZQQYS.cjs → chunk-AWLFJMOJ.cjs} +40 -40
  28. package/dist/{chunk-5AZZQQYS.cjs.map → chunk-AWLFJMOJ.cjs.map} +1 -1
  29. package/dist/{chunk-RJ3D3PE2.cjs → chunk-DQDPDUI3.cjs} +9 -9
  30. package/dist/{chunk-RJ3D3PE2.cjs.map → chunk-DQDPDUI3.cjs.map} +1 -1
  31. package/dist/{chunk-2RLI5XVI.cjs → chunk-F2YI5N7E.cjs} +16 -16
  32. package/dist/{chunk-2RLI5XVI.cjs.map → chunk-F2YI5N7E.cjs.map} +1 -1
  33. package/dist/{chunk-LBOWSKGM.js → chunk-FI35LJ3L.js} +5 -5
  34. package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
  35. package/dist/chunk-FS3YSNSU.js.map +1 -0
  36. package/dist/{chunk-RB2EW6IZ.cjs → chunk-GOFTC2HV.cjs} +3690 -3472
  37. package/dist/chunk-GOFTC2HV.cjs.map +1 -0
  38. package/dist/{chunk-CTMLY2GN.js → chunk-HGZPH7S5.js} +5 -5
  39. package/dist/{chunk-7VUKIWF6.cjs → chunk-HIV2GIFP.cjs} +3 -3
  40. package/dist/{chunk-7VUKIWF6.cjs.map → chunk-HIV2GIFP.cjs.map} +1 -1
  41. package/dist/{chunk-DBR5JLX3.js → chunk-IHNUMRMU.js} +46 -23
  42. package/dist/chunk-IHNUMRMU.js.map +1 -0
  43. package/dist/{chunk-R4HKB3YD.cjs → chunk-IIPBTWQR.cjs} +22 -22
  44. package/dist/{chunk-R4HKB3YD.cjs.map → chunk-IIPBTWQR.cjs.map} +1 -1
  45. package/dist/{chunk-TBGSQTJK.js → chunk-IK2WRYVA.js} +6 -6
  46. package/dist/{chunk-WSAPKKDU.cjs → chunk-JIW5JI3E.cjs} +9 -9
  47. package/dist/{chunk-WSAPKKDU.cjs.map → chunk-JIW5JI3E.cjs.map} +1 -1
  48. package/dist/{chunk-MLGGF24T.js → chunk-JKAX5K7C.js} +8 -8
  49. package/dist/{chunk-UWPT23SA.js → chunk-JO3NPEK6.js} +3 -3
  50. package/dist/{chunk-HCZJZMU7.js → chunk-JOFGJYIT.js} +3 -3
  51. package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
  52. package/dist/chunk-KI4EGKNV.cjs.map +1 -0
  53. package/dist/{chunk-OB7YDOTK.cjs → chunk-NJADHL2G.cjs} +8 -8
  54. package/dist/{chunk-OB7YDOTK.cjs.map → chunk-NJADHL2G.cjs.map} +1 -1
  55. package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
  56. package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
  57. package/dist/{chunk-HX4CJYXL.js → chunk-OT4RV6WY.js} +10 -10
  58. package/dist/{chunk-Z35QWWJH.cjs → chunk-RJXI25TJ.cjs} +34 -34
  59. package/dist/{chunk-Z35QWWJH.cjs.map → chunk-RJXI25TJ.cjs.map} +1 -1
  60. package/dist/{chunk-6TL5XIAW.cjs → chunk-SG25J6LV.cjs} +119 -119
  61. package/dist/{chunk-6TL5XIAW.cjs.map → chunk-SG25J6LV.cjs.map} +1 -1
  62. package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
  63. package/dist/{chunk-FEGEC6FY.js → chunk-V5WLIFKN.js} +5660 -5442
  64. package/dist/chunk-V5WLIFKN.js.map +1 -0
  65. package/dist/{chunk-EJ3EBYNV.cjs → chunk-VVXUUBML.cjs} +32 -32
  66. package/dist/{chunk-EJ3EBYNV.cjs.map → chunk-VVXUUBML.cjs.map} +1 -1
  67. package/dist/{chunk-SRYV4H3M.js → chunk-VZX3SSFL.js} +3 -3
  68. package/dist/{chunk-5CDPRCPS.cjs → chunk-WVZHCDB5.cjs} +23 -23
  69. package/dist/{chunk-5CDPRCPS.cjs.map → chunk-WVZHCDB5.cjs.map} +1 -1
  70. package/dist/{chunk-VDJF4ZAU.js → chunk-XG7FLAKQ.js} +3 -3
  71. package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
  72. package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
  73. package/dist/{chunk-2AASLY6D.js → chunk-ZIQMVAGA.js} +3 -3
  74. package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
  75. package/dist/components/calendar.cjs +4 -4
  76. package/dist/components/calendar.js +3 -3
  77. package/dist/components/case-studies/index.cjs +17 -17
  78. package/dist/components/case-studies/index.js +7 -7
  79. package/dist/components/chat/approval-batch-message.d.ts +2 -2
  80. package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
  81. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  82. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  83. package/dist/components/chat/entity-cards/deleted-data-card.d.ts +27 -0
  84. package/dist/components/chat/entity-cards/deleted-data-card.d.ts.map +1 -0
  85. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  86. package/dist/components/chat/hooks/use-chat-card-item.d.ts +3 -0
  87. package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
  88. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  89. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  90. package/dist/components/chat/index.cjs +13 -7
  91. package/dist/components/chat/index.cjs.map +1 -1
  92. package/dist/components/chat/index.js +12 -6
  93. package/dist/components/chat/remark-mention-chips.d.ts +5 -2
  94. package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
  95. package/dist/components/chat/remark-strip-citations.d.ts +22 -0
  96. package/dist/components/chat/remark-strip-citations.d.ts.map +1 -0
  97. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  98. package/dist/components/chat/stream/message-mutations.d.ts +49 -5
  99. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
  100. package/dist/components/chat/types/component.types.d.ts +7 -7
  101. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  102. package/dist/components/chat/types/message.types.d.ts +11 -4
  103. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  104. package/dist/components/chat/types/unified-chat-state.types.d.ts +4 -1
  105. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  106. package/dist/components/chat/utils/discuss-ref-prompt.d.ts +48 -0
  107. package/dist/components/chat/utils/discuss-ref-prompt.d.ts.map +1 -0
  108. package/dist/components/chat/utils/index.d.ts +2 -0
  109. package/dist/components/chat/utils/index.d.ts.map +1 -1
  110. package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -3
  111. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  112. package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +63 -0
  113. package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -0
  114. package/dist/components/chat/utils/source-row-cta.d.ts.map +1 -1
  115. package/dist/components/contact/index.cjs +8 -8
  116. package/dist/components/contact/index.js +7 -7
  117. package/dist/components/docs/index.cjs +10 -10
  118. package/dist/components/docs/index.js +9 -9
  119. package/dist/components/embeds/index.cjs +8 -8
  120. package/dist/components/embeds/index.js +7 -7
  121. package/dist/components/faq/index.cjs +9 -9
  122. package/dist/components/faq/index.js +8 -8
  123. package/dist/components/features/index.cjs +7 -7
  124. package/dist/components/features/index.js +6 -6
  125. package/dist/components/help-center-pages/index.cjs +28 -28
  126. package/dist/components/help-center-pages/index.js +19 -19
  127. package/dist/components/icons/index.cjs +3 -3
  128. package/dist/components/icons/index.js +2 -2
  129. package/dist/components/index.cjs +253 -247
  130. package/dist/components/index.cjs.map +1 -1
  131. package/dist/components/index.js +23 -17
  132. package/dist/components/index.js.map +1 -1
  133. package/dist/components/meeting-scheduler/index.cjs +92 -92
  134. package/dist/components/meeting-scheduler/index.js +9 -9
  135. package/dist/components/navigation/index.cjs +7 -7
  136. package/dist/components/navigation/index.js +6 -6
  137. package/dist/components/onboarding-guides/index.cjs +11 -11
  138. package/dist/components/onboarding-guides/index.js +10 -10
  139. package/dist/components/related-content/index.cjs +9 -9
  140. package/dist/components/related-content/index.js +8 -8
  141. package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +11 -3
  142. package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -1
  143. package/dist/components/shared/doc-search/use-doc-search.d.ts +7 -0
  144. package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
  145. package/dist/components/tickets/index.cjs +11 -11
  146. package/dist/components/tickets/index.js +10 -10
  147. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  148. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  149. package/dist/components/ui/file-manager/index.cjs +70 -70
  150. package/dist/components/ui/file-manager/index.js +3 -3
  151. package/dist/components/ui/index.cjs +13 -7
  152. package/dist/components/ui/index.cjs.map +1 -1
  153. package/dist/components/ui/index.js +12 -6
  154. package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
  155. package/dist/components/ui/overlay-open-registry.d.ts +50 -0
  156. package/dist/components/ui/overlay-open-registry.d.ts.map +1 -0
  157. package/dist/components/ui/portal-container.d.ts +24 -0
  158. package/dist/components/ui/portal-container.d.ts.map +1 -1
  159. package/dist/hooks/index.cjs +4 -4
  160. package/dist/hooks/index.js +3 -3
  161. package/dist/index.cjs +20 -10
  162. package/dist/index.cjs.map +1 -1
  163. package/dist/index.js +24 -14
  164. package/dist/utils/anchor-id.d.ts +32 -0
  165. package/dist/utils/anchor-id.d.ts.map +1 -0
  166. package/dist/utils/content-href.d.ts +27 -6
  167. package/dist/utils/content-href.d.ts.map +1 -1
  168. package/dist/utils/index.cjs +213 -152
  169. package/dist/utils/index.cjs.map +1 -1
  170. package/dist/utils/index.d.ts +1 -0
  171. package/dist/utils/index.d.ts.map +1 -1
  172. package/dist/utils/index.js +212 -153
  173. package/dist/utils/index.js.map +1 -1
  174. package/dist/utils/list-url.d.ts.map +1 -1
  175. package/dist/utils/same-page-hash-nav.d.ts +17 -6
  176. package/dist/utils/same-page-hash-nav.d.ts.map +1 -1
  177. package/package.json +1 -1
  178. package/src/chat-protocol/decode.ts +43 -0
  179. package/src/chat-protocol/frames.ts +27 -0
  180. package/src/components/chat/__tests__/remark-mention-chips.test.ts +67 -0
  181. package/src/components/chat/approval-batch-message.tsx +2 -2
  182. package/src/components/chat/chat-message-enhanced.tsx +5 -3
  183. package/src/components/chat/chat-message-list.tsx +49 -5
  184. package/src/components/chat/embeddable-chat.tsx +19 -2
  185. package/src/components/chat/entity-cards/deleted-data-card.tsx +61 -0
  186. package/src/components/chat/entity-cards/dispatch.tsx +205 -102
  187. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +1 -1
  188. package/src/components/chat/hooks/use-chat-card-item.ts +15 -1
  189. package/src/components/chat/hooks/use-nats-chat-adapter.ts +28 -4
  190. package/src/components/chat/hooks/use-sse-chat-adapter.ts +57 -32
  191. package/src/components/chat/remark-mention-chips.ts +6 -3
  192. package/src/components/chat/remark-strip-citations.ts +39 -0
  193. package/src/components/chat/stream/__tests__/chat-stream-reducer-approval-batch.test.ts +196 -0
  194. package/src/components/chat/stream/chat-stream-reducer.ts +67 -2
  195. package/src/components/chat/stream/message-mutations.ts +170 -40
  196. package/src/components/chat/types/component.types.ts +9 -7
  197. package/src/components/chat/types/message.types.ts +14 -4
  198. package/src/components/chat/types/unified-chat-state.types.ts +4 -1
  199. package/src/components/chat/utils/__tests__/discuss-ref-prompt.test.ts +67 -0
  200. package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +156 -0
  201. package/src/components/chat/utils/discuss-ref-prompt.ts +63 -0
  202. package/src/components/chat/utils/index.ts +16 -0
  203. package/src/components/chat/utils/message-segment-accumulator.ts +24 -29
  204. package/src/components/chat/utils/resolve-fetched-card-href.ts +104 -0
  205. package/src/components/chat/utils/source-row-cta.ts +5 -1
  206. package/src/components/shared/doc-search/__tests__/resolve-search-result-action.test.ts +145 -0
  207. package/src/components/shared/doc-search/resolve-search-result-action.ts +46 -10
  208. package/src/components/shared/doc-search/use-doc-search.ts +50 -14
  209. package/src/components/ui/__tests__/overlay-open-registry.test.tsx +87 -0
  210. package/src/components/ui/actions-menu.tsx +65 -2
  211. package/src/components/ui/dropdown-menu.tsx +62 -12
  212. package/src/components/ui/markdown/base-components.tsx +26 -0
  213. package/src/components/ui/overlay-open-registry.tsx +99 -0
  214. package/src/components/ui/portal-container.tsx +31 -0
  215. package/src/utils/__tests__/anchor-id.test.ts +139 -0
  216. package/src/utils/__tests__/content-href.test.ts +74 -0
  217. package/src/utils/__tests__/list-url.test.ts +11 -1
  218. package/src/utils/anchor-id.ts +106 -0
  219. package/src/utils/content-href.ts +113 -13
  220. package/src/utils/index.ts +8 -0
  221. package/src/utils/list-url.ts +6 -1
  222. package/src/utils/same-page-hash-nav.ts +28 -9
  223. package/dist/chunk-A62C4C4V.cjs.map +0 -1
  224. package/dist/chunk-BTUHMCYW.js.map +0 -1
  225. package/dist/chunk-DBR5JLX3.js.map +0 -1
  226. package/dist/chunk-EYTXD62B.cjs.map +0 -1
  227. package/dist/chunk-FEGEC6FY.js.map +0 -1
  228. package/dist/chunk-GE3JUTW2.js.map +0 -1
  229. package/dist/chunk-Q4K3TGNP.cjs.map +0 -1
  230. package/dist/chunk-RB2EW6IZ.cjs.map +0 -1
  231. package/dist/chunk-XJVRAKEX.cjs.map +0 -1
  232. /package/dist/{chunk-JOCFPAS6.js.map → chunk-4NFDSMB5.js.map} +0 -0
  233. /package/dist/{chunk-WNOGR5AA.js.map → chunk-6LMTBNLN.js.map} +0 -0
  234. /package/dist/{chunk-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
  235. /package/dist/{chunk-LBOWSKGM.js.map → chunk-FI35LJ3L.js.map} +0 -0
  236. /package/dist/{chunk-CTMLY2GN.js.map → chunk-HGZPH7S5.js.map} +0 -0
  237. /package/dist/{chunk-TBGSQTJK.js.map → chunk-IK2WRYVA.js.map} +0 -0
  238. /package/dist/{chunk-MLGGF24T.js.map → chunk-JKAX5K7C.js.map} +0 -0
  239. /package/dist/{chunk-UWPT23SA.js.map → chunk-JO3NPEK6.js.map} +0 -0
  240. /package/dist/{chunk-HCZJZMU7.js.map → chunk-JOFGJYIT.js.map} +0 -0
  241. /package/dist/{chunk-HX4CJYXL.js.map → chunk-OT4RV6WY.js.map} +0 -0
  242. /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
  243. /package/dist/{chunk-SRYV4H3M.js.map → chunk-VZX3SSFL.js.map} +0 -0
  244. /package/dist/{chunk-VDJF4ZAU.js.map → chunk-XG7FLAKQ.js.map} +0 -0
  245. /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
  246. /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
  247. /package/dist/{chunk-2AASLY6D.js.map → chunk-ZIQMVAGA.js.map} +0 -0
  248. /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
@@ -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,
@@ -48,6 +49,7 @@ import { SourceActionButton } from '../source-action-button'
48
49
  import { NavLinkAnchorViaRuntime } from '../nav-link-anchor-via-runtime'
49
50
  import { defaultBuildProductReleaseCardProps } from './product-release-card-defaults'
50
51
  import { ChatVideoEntityCard } from './chat-video-entity-card'
52
+ import { DeletedDataCard } from './deleted-data-card'
51
53
  import { BlockCard } from './block-card'
52
54
  import { BlogCardSkeleton } from './blog-card'
53
55
  import { CaseStudyCardSkeleton } from './case-study-card'
@@ -1000,8 +1002,15 @@ type ChatCardRegistryEntry =
1000
1002
  * resolved ref — so the wrapper sees a non-null `href`, the
1001
1003
  * pre-computed `isNewTab` reflects the actual destination, and
1002
1004
  * the click goes through `handleChatNavClick` (no silent
1003
- * bypass of embed-mode / close-on-nav). */
1005
+ * bypass of embed-mode / close-on-nav). Takes precedence over the
1006
+ * `composeContentUrl` fallback below. */
1004
1007
  fallbackHref?: (item: any) => string | null
1008
+ /** Opt OUT of the post-fetch `composeContentUrl` fallback (see
1009
+ * `resolveFetchedCardHref`). Set for types with NO public destination
1010
+ * — the seam would otherwise synthesize `<contentOrigin>/<type>/<id>`
1011
+ * from its default branch and hand the user a 404. Types with a real
1012
+ * route (hosted, or covered by a host `override`) leave it unset. */
1013
+ noComposedHref?: boolean
1005
1014
  }
1006
1015
 
1007
1016
  interface FinancialCardConfig {
@@ -1017,26 +1026,34 @@ const FINANCIAL_CARD_CONFIGS: Record<string, FinancialCardConfig> = {
1017
1026
  balance_sheet: { label: 'Balance sheet', icon: () => <BankIcon size={24} /> },
1018
1027
  cash_flow: { label: 'Cash flow', icon: () => <CoinsExchangeCurrencyIcon size={24} /> },
1019
1028
  }
1020
- function financialRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1029
+ /** Expand a per-family config map into registry entries — ONE loop shell
1030
+ * for every card family (financial, github, program, roadmap); adding a
1031
+ * family is a config map + one `registryEntries(...)` call. */
1032
+ function registryEntries<C>(
1033
+ configs: Record<string, C>,
1034
+ build: (cfg: C) => ChatCardRegistryEntry,
1035
+ ): Record<string, ChatCardRegistryEntry> {
1021
1036
  const out: Record<string, ChatCardRegistryEntry> = {}
1022
- for (const [docType, cfg] of Object.entries(FINANCIAL_CARD_CONFIGS)) {
1023
- out[docType] = {
1024
- mode: 'no-fetch',
1025
- label: cfg.label,
1026
- bareInline: true,
1027
- render: (chatRef, opts) => (
1028
- <GenericFinancialChatCard
1029
- chatRef={chatRef}
1030
- icon={cfg.icon()}
1031
- isNewTab={opts.isNewTab}
1032
- discuss={opts.discuss}
1033
- />
1034
- ),
1035
- }
1036
- }
1037
+ for (const [docType, cfg] of Object.entries(configs)) out[docType] = build(cfg)
1037
1038
  return out
1038
1039
  }
1039
1040
 
1041
+ function financialRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1042
+ return registryEntries(FINANCIAL_CARD_CONFIGS, (cfg) => ({
1043
+ mode: 'no-fetch',
1044
+ label: cfg.label,
1045
+ bareInline: true,
1046
+ render: (chatRef, opts) => (
1047
+ <GenericFinancialChatCard
1048
+ chatRef={chatRef}
1049
+ icon={cfg.icon()}
1050
+ isNewTab={opts.isNewTab}
1051
+ discuss={opts.discuss}
1052
+ />
1053
+ ),
1054
+ }))
1055
+ }
1056
+
1040
1057
  interface GitHubCardConfig {
1041
1058
  label: string
1042
1059
  kind: GitHubActivityKind
@@ -1050,19 +1067,15 @@ const GITHUB_CARD_CONFIGS: Record<string, GitHubCardConfig> = {
1050
1067
  github_pr_review_public: { label: 'GitHub review (public)', kind: 'pr_review' },
1051
1068
  }
1052
1069
  function githubRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1053
- const out: Record<string, ChatCardRegistryEntry> = {}
1054
- for (const [docType, cfg] of Object.entries(GITHUB_CARD_CONFIGS)) {
1055
- out[docType] = {
1056
- mode: 'no-fetch',
1057
- label: cfg.label,
1058
- bareInline: true,
1059
- render: (chatRef, opts) => (
1060
- <GitHubChatCard chatRef={chatRef} kind={cfg.kind} isNewTab={opts.isNewTab}
1061
- discuss={opts.discuss} />
1062
- ),
1063
- }
1064
- }
1065
- return out
1070
+ return registryEntries(GITHUB_CARD_CONFIGS, (cfg) => ({
1071
+ mode: 'no-fetch',
1072
+ label: cfg.label,
1073
+ bareInline: true,
1074
+ render: (chatRef, opts) => (
1075
+ <GitHubChatCard chatRef={chatRef} kind={cfg.kind} isNewTab={opts.isNewTab}
1076
+ discuss={opts.discuss} />
1077
+ ),
1078
+ }))
1066
1079
  }
1067
1080
 
1068
1081
  type ProgramConfigKey = 'podcast' | 'webinar' | 'event'
@@ -1077,28 +1090,24 @@ const PROGRAM_CARD_CONFIGS: Record<string, ProgramCardConfig> = {
1077
1090
  event: { label: 'Event', configKey: 'event', contentRefType: 'event' },
1078
1091
  }
1079
1092
  function programRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1080
- const out: Record<string, ChatCardRegistryEntry> = {}
1081
- for (const [docType, cfg] of Object.entries(PROGRAM_CARD_CONFIGS)) {
1082
- out[docType] = {
1083
- mode: 'fetch',
1084
- label: cfg.label,
1085
- contentRefType: cfg.contentRefType,
1086
- bareInline: true,
1087
- skeleton: () => <ProgramCardSkeleton size="sm" />,
1088
- render: (item, chatRef, opts) => (
1089
- <ProgramChatCard
1090
- item={item}
1091
- chatRef={chatRef}
1092
- isNewTab={opts.isNewTab}
1093
- discuss={opts.discuss}
1094
- configKey={cfg.configKey}
1095
- label={cfg.configKey.charAt(0).toUpperCase() + cfg.configKey.slice(1)}
1096
- ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
1097
- />
1098
- ),
1099
- }
1100
- }
1101
- return out
1093
+ return registryEntries(PROGRAM_CARD_CONFIGS, (cfg) => ({
1094
+ mode: 'fetch',
1095
+ label: cfg.label,
1096
+ contentRefType: cfg.contentRefType,
1097
+ bareInline: true,
1098
+ skeleton: () => <ProgramCardSkeleton size="sm" />,
1099
+ render: (item, chatRef, opts) => (
1100
+ <ProgramChatCard
1101
+ item={item}
1102
+ chatRef={chatRef}
1103
+ isNewTab={opts.isNewTab}
1104
+ discuss={opts.discuss}
1105
+ configKey={cfg.configKey}
1106
+ label={cfg.configKey.charAt(0).toUpperCase() + cfg.configKey.slice(1)}
1107
+ ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
1108
+ />
1109
+ ),
1110
+ }))
1102
1111
  }
1103
1112
 
1104
1113
  type RoadmapCardType = 'roadmap_item' | 'delivery_item' | 'internal_task'
@@ -1106,6 +1115,8 @@ interface RoadmapEntryConfig {
1106
1115
  label: string
1107
1116
  cardType: RoadmapCardType
1108
1117
  contentRefType: string
1118
+ /** See `ChatCardRegistryEntry.noComposedHref`. */
1119
+ noComposedHref?: boolean
1109
1120
  }
1110
1121
  const ROADMAP_CARD_CONFIGS: Record<string, RoadmapEntryConfig> = {
1111
1122
  roadmap_item: { label: 'Roadmap item', cardType: 'roadmap_item', contentRefType: 'roadmap_item' },
@@ -1118,34 +1129,50 @@ const ROADMAP_CARD_CONFIGS: Record<string, RoadmapEntryConfig> = {
1118
1129
  label: 'Internal task',
1119
1130
  cardType: 'internal_task',
1120
1131
  contentRefType: 'internal_task',
1132
+ // Internal ClickUp work has no public destination on any platform — the
1133
+ // seam's default branch would synthesize `<hub>/internal_task/<id>`.
1134
+ noComposedHref: true,
1121
1135
  },
1122
1136
  }
1123
1137
  function roadmapRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1124
- const out: Record<string, ChatCardRegistryEntry> = {}
1125
- for (const [docType, cfg] of Object.entries(ROADMAP_CARD_CONFIGS)) {
1126
- out[docType] = {
1127
- mode: 'fetch',
1128
- label: cfg.label,
1129
- contentRefType: cfg.contentRefType,
1130
- bareInline: true,
1131
- skeleton: () => <RoadmapCardSkeleton size="sm" />,
1132
- render: (item, chatRef, opts) => (
1133
- <RoadmapChatCard
1134
- item={item}
1135
- chatRef={chatRef}
1136
- isNewTab={opts.isNewTab}
1137
- discuss={opts.discuss}
1138
- cardType={cfg.cardType}
1139
- />
1140
- ),
1141
- }
1142
- }
1143
- return out
1138
+ return registryEntries(ROADMAP_CARD_CONFIGS, (cfg) => ({
1139
+ mode: 'fetch',
1140
+ label: cfg.label,
1141
+ contentRefType: cfg.contentRefType,
1142
+ bareInline: true,
1143
+ ...(cfg.noComposedHref ? { noComposedHref: true } : {}),
1144
+ skeleton: () => <RoadmapCardSkeleton size="sm" />,
1145
+ render: (item, chatRef, opts) => (
1146
+ <RoadmapChatCard
1147
+ item={item}
1148
+ chatRef={chatRef}
1149
+ isNewTab={opts.isNewTab}
1150
+ discuss={opts.discuss}
1151
+ cardType={cfg.cardType}
1152
+ />
1153
+ ),
1154
+ }))
1144
1155
  }
1145
1156
 
1146
1157
  const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
1147
1158
  // ───────── no-fetch: ChatRef carries everything ─────────
1148
1159
  ...githubRegistryEntries(),
1160
+ // Generic TOMBSTONE for entities a chat action deleted (ClickUp task
1161
+ // today; any deletable entity tomorrow). No fetch, no navigation —
1162
+ // the entity no longer resolves anywhere; the placeholder keeps the
1163
+ // thread's integrity (Teams/Slack "This message was deleted" pattern).
1164
+ deleted_data: {
1165
+ mode: 'no-fetch',
1166
+ label: 'Deleted',
1167
+ bareInline: true,
1168
+ render: (chatRef) => (
1169
+ <DeletedDataCard
1170
+ title={chatRef.title ?? null}
1171
+ entityLabel={(chatRef.metadata?.entity_label as string | undefined) ?? null}
1172
+ recoveryNote={(chatRef.metadata?.recovery_note as string | undefined) ?? null}
1173
+ />
1174
+ ),
1175
+ },
1149
1176
  slack_message: {
1150
1177
  mode: 'no-fetch',
1151
1178
  label: 'Slack message',
@@ -1420,6 +1447,17 @@ function ChatCardNavWrap({
1420
1447
  const onClickCapture = (e: React.MouseEvent<HTMLElement>) => {
1421
1448
  if (!href) return
1422
1449
  const targetEl = e.target as HTMLElement
1450
+ // Only claim clicks that physically happened INSIDE this card.
1451
+ //
1452
+ // The card's "⋯" menu renders through a React PORTAL: its DOM lives in the
1453
+ // panel's portal host, but React still bubbles its events through this
1454
+ // subtree — so this capture handler sees them first and, being a capture
1455
+ // phase, runs BEFORE the menu row's own `stopPropagation`. That swallowed
1456
+ // the menu's trailing "↗ Open in new tab" button: `executeNavigation`
1457
+ // called `preventDefault()` and routed the card's href in the SAME tab,
1458
+ // so the anchor's `target="_blank"` never applied.
1459
+ const host = e.currentTarget as HTMLElement | null
1460
+ if (!host?.contains(targetEl)) return
1423
1461
  // Buttons rendered INSIDE the card's outer `<a>` (e.g. RoadmapCard
1424
1462
  // vote buttons, ImageGallery thumbnails) bubble up with
1425
1463
  // `closest('a')` truthy — without this guard, clicking them would
@@ -1516,24 +1554,60 @@ export function ChatCardLoader({
1516
1554
  // `contentRefType` is empty so the hook returns `isLoading=false` and
1517
1555
  // `item=undefined`, which we ignore.
1518
1556
  const fetchEntry = entry && entry.mode === 'fetch' ? entry : null
1519
- const { item, isLoading } = useChatCardItem<any>(
1557
+ const { item, isLoading, isError, isFetched } = useChatCardItem<any>(
1520
1558
  fetchEntry?.contentRefType ?? '',
1521
1559
  fetchEntry ? resolvedChatRef.id : '',
1522
1560
  )
1523
1561
  if (!entry) return null
1524
1562
 
1525
- // Apply per-type fallback URL AFTER fetch (e.g. campaign → /admin/...).
1563
+ // Apply the post-fetch URL fallback (the ref carried no `externalUrl`).
1526
1564
  // We mutate `resolvedChatRef.url` BEFORE computing isNewTab so the
1527
1565
  // wrapper's interceptor sees the destination the user will actually
1528
1566
  // visit. `safeHref` blocks `javascript:` / `data:` payloads even
1529
1567
  // though the registry callers compose hub-internal strings today.
1530
- const finalChatRef: ChatRef =
1568
+ //
1569
+ // Two sources, in order:
1570
+ // 1. the registry's per-type `fallbackHref` — an explicit non-content
1571
+ // destination (marketing campaign → `/admin/...`);
1572
+ // 2. the host's `composeContentUrl` seam via `resolveFetchedCardHref` —
1573
+ // the SAME resolver page cards and SSE chat cards go through. This is
1574
+ // what makes Mingo/NATS cards clickable: that transport ships bare
1575
+ // `[card://type:id]` markers with no refs metadata, so the ref reaches
1576
+ // us with `url: null` and `resolveSourceRowCTA` has nothing to route.
1577
+ const composedHref =
1578
+ fetchEntry && !resolvedChatRef.url && item && !fetchEntry.fallbackHref && !fetchEntry.noComposedHref
1579
+ ? resolveFetchedCardHref({
1580
+ contentRefType: fetchEntry.contentRefType,
1581
+ id: resolvedChatRef.id,
1582
+ item,
1583
+ composeContentUrl: runtime.composeContentUrl,
1584
+ })
1585
+ : null
1586
+ const hrefResolvedChatRef: ChatRef =
1531
1587
  fetchEntry && !resolvedChatRef.url && item && fetchEntry.fallbackHref
1532
1588
  ? {
1533
1589
  ...resolvedChatRef,
1534
1590
  url: safeHref(fetchEntry.fallbackHref(item)),
1535
1591
  }
1536
- : resolvedChatRef
1592
+ : composedHref
1593
+ ? {
1594
+ ...resolvedChatRef,
1595
+ url: safeHref(composedHref.href),
1596
+ targetPlatform: composedHref.targetPlatform ?? resolvedChatRef.targetPlatform ?? null,
1597
+ }
1598
+ : resolvedChatRef
1599
+
1600
+ // Title enrichment, same synthetic-ref gap as the href above: a Mingo
1601
+ // `[card://type:id]` marker produces `title: <id>` because the transport
1602
+ // ships no ref metadata. Downstream consumers that read `ref.title` — most
1603
+ // visibly the "Ask Mingo" prompt, which would otherwise send
1604
+ // "Tell me more about 86ad3qvv5" — get the row's real title once it loads.
1605
+ // A ref that already carries a distinct title (the SSE path) is untouched.
1606
+ const fetchedTitle = fetchEntry && item ? readFetchedCardTitle(item) : null
1607
+ const finalChatRef: ChatRef =
1608
+ fetchedTitle && (!hrefResolvedChatRef.title || hrefResolvedChatRef.title === hrefResolvedChatRef.id)
1609
+ ? { ...hrefResolvedChatRef, title: fetchedTitle }
1610
+ : hrefResolvedChatRef
1537
1611
 
1538
1612
  // Pre-compute new-tab decision ONCE here (the same rule chips use).
1539
1613
  // Render branches that pass `target` / `rel` to their card pull this
@@ -1590,6 +1664,23 @@ export function ChatCardLoader({
1590
1664
  {children}
1591
1665
  </ChatCardNavWrap>
1592
1666
  )
1667
+ // ONE render tail for both modes — bare-inline cards skip the discuss
1668
+ // wrapper; everything else gets the ChatCardWithDiscuss chrome. Kept
1669
+ // here (not per-branch) so the two paths can never drift on the
1670
+ // discuss-menu wiring.
1671
+ const finish = (node: React.ReactNode) =>
1672
+ entry.bareInline ? (
1673
+ <>{navWrap(node)}</>
1674
+ ) : (
1675
+ <ChatCardWithDiscuss
1676
+ chatRef={finalChatRef}
1677
+ onDiscuss={onDiscuss}
1678
+ onDisplay={onDisplay}
1679
+ displayAction={entry.displayAction}
1680
+ >
1681
+ {navWrap(node)}
1682
+ </ChatCardWithDiscuss>
1683
+ )
1593
1684
  if (entry.mode === 'no-fetch') {
1594
1685
  // Synthetic-ref gate. `chat-message-enhanced.tsx` builds a minimal
1595
1686
  // `{ type, id, title: cardId, url: null }` ChatRef when the LLM
@@ -1612,35 +1703,47 @@ export function ChatCardLoader({
1612
1703
  // Fetch-mode types already handle this gracefully: a synthetic
1613
1704
  // id leads to a fetch miss → `!item` → null at line ~1034.
1614
1705
  if (!finalChatRef.sourceRepo) return null
1615
- if (entry.bareInline) {
1616
- return navWrap(entry.render(finalChatRef, renderOpts))
1617
- }
1618
- return (
1619
- <ChatCardWithDiscuss
1620
- chatRef={finalChatRef}
1621
- onDiscuss={onDiscuss}
1622
- onDisplay={onDisplay}
1623
- displayAction={entry.displayAction}
1624
- >
1625
- {navWrap(entry.render(finalChatRef, renderOpts))}
1626
- </ChatCardWithDiscuss>
1627
- )
1706
+ return finish(entry.render(finalChatRef, renderOpts))
1628
1707
  }
1629
1708
  if (isLoading) return <>{entry.skeleton()}</>
1630
- if (!item) return null
1631
- if (entry.bareInline) {
1632
- return <>{navWrap(entry.render(item, finalChatRef, renderOpts))}</>
1709
+ if (!item) {
1710
+ // FETCH FAILURE (non-OK response — auth blip, 5xx, rate limit) is
1711
+ // NOT evidence of deletion: render nothing rather than a false
1712
+ // "deleted" claim. Only a SUCCESSFUL fetch that lacks the id
1713
+ // reaches the tombstone below.
1714
+ if (isError) return null
1715
+ // NEVER-FETCHED (query disabled — no list URL registered for this
1716
+ // type, or an empty id): no request was made, so absence of `item`
1717
+ // proves nothing. Render nothing rather than a false "deleted".
1718
+ if (!isFetched) return null
1719
+ // FETCH MISS. Two distinct cases, split by ref provenance:
1720
+ //
1721
+ // - SERVER-BUILT ref (`sourceRepo` present — the entity provably
1722
+ // existed when the answer was written, its ref was persisted
1723
+ // with the message): the entity is GONE now (deleted /
1724
+ // tombstoned). Render the generic deleted-data TOMBSTONE so the
1725
+ // thread keeps its integrity instead of a silent gap — works
1726
+ // for ANY fetch-mode entity type, no per-type wiring.
1727
+ //
1728
+ // - SYNTHETIC client-built ref (bare marker with no refs entry —
1729
+ // possibly a hallucinated id): keep the existing hide-it
1730
+ // behavior; a "deleted" placeholder would assert an existence
1731
+ // we can't vouch for.
1732
+ if (finalChatRef.sourceRepo) {
1733
+ return (
1734
+ <DeletedDataCard
1735
+ title={finalChatRef.title ?? null}
1736
+ entityLabel={entry.label}
1737
+ recoveryNote={
1738
+ (finalChatRef.metadata as Record<string, unknown> | null | undefined)
1739
+ ?.recovery_note as string | undefined ?? null
1740
+ }
1741
+ />
1742
+ )
1743
+ }
1744
+ return null
1633
1745
  }
1634
- return (
1635
- <ChatCardWithDiscuss
1636
- chatRef={finalChatRef}
1637
- onDiscuss={onDiscuss}
1638
- onDisplay={onDisplay}
1639
- displayAction={entry.displayAction}
1640
- >
1641
- {navWrap(entry.render(item, finalChatRef, renderOpts))}
1642
- </ChatCardWithDiscuss>
1643
- )
1746
+ return finish(entry.render(item, finalChatRef, renderOpts))
1644
1747
  }
1645
1748
 
1646
1749
  // =============================================================================
@@ -250,7 +250,7 @@ describe('SSE wire decode — golden fixtures (full hook path)', () => {
250
250
  vi.stubGlobal('fetch', fetchMock)
251
251
 
252
252
  const { result } = renderAdapter()
253
- let sendPromise!: Promise<void>
253
+ let sendPromise!: Promise<void | boolean>
254
254
  act(() => {
255
255
  sendPromise = result.current.sendMessage('will be aborted')
256
256
  })
@@ -28,6 +28,9 @@ export interface UseChatCardItemResult<T = unknown> {
28
28
  item: T | undefined
29
29
  isLoading: boolean
30
30
  isError: boolean
31
+ /** True only after a fetch actually completed — false for disabled
32
+ * queries (no list URL for the type / empty id). */
33
+ isFetched: boolean
31
34
  }
32
35
 
33
36
  // `extractItems` / `extractItemId` hoisted to `src/utils/extract-items.ts`
@@ -53,7 +56,12 @@ export function useChatCardItem<T = unknown>(
53
56
  // sent no credentials, so list endpoints behind the gateway returned
54
57
  // 401 and the card rendered blank.
55
58
  const res = await embedAuthedFetch(url)
56
- if (!res.ok) return null
59
+ // THROW on non-OK (was `return null`): callers must be able to
60
+ // tell "fetched fine, entity absent" (→ deleted tombstone) from
61
+ // "fetch failed" (401 refresh miss, 5xx, 429 → transient; render
62
+ // nothing, never a false 'deleted' claim). TanStack surfaces the
63
+ // throw as `isError`.
64
+ if (!res.ok) throw new Error(`chat card fetch failed: ${res.status}`)
57
65
  const data = await res.json()
58
66
  const items = extractItems(data)
59
67
  const match = items.find((it) => extractItemId(type, it) === id)
@@ -79,5 +87,11 @@ export function useChatCardItem<T = unknown>(
79
87
  item: (query.data ?? undefined) as T | undefined,
80
88
  isLoading: query.isLoading,
81
89
  isError: query.isError,
90
+ // True only after the query actually completed a fetch. DISABLED
91
+ // queries (no list URL registered for the type, empty id) never
92
+ // fetch — `item` is undefined there as well, and callers must not
93
+ // read that as "fetched fine, entity absent" (tombstone gate in
94
+ // entity-cards/dispatch.tsx).
95
+ isFetched: query.isFetched,
82
96
  }
83
97
  }
@@ -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'
@@ -248,23 +249,26 @@ export function useSseChatAdapter(
248
249
  // creation-time callbacks stay stable.
249
250
 
250
251
  const sendMessageRef = useRef<
251
- (text: string, options?: InternalSendMessageOptions) => Promise<void>
252
- >(async () => {})
253
-
254
- const cardApprove = useCallback((reqId?: string): void | Promise<void> => {
255
- if (!reqId) return
256
- return sendMessageRef.current('', {
257
- hidden: true,
258
- approvalAction: { proposalId: reqId, action: 'approve' },
259
- })
260
- }, [])
261
- const cardReject = useCallback((reqId?: string): void | Promise<void> => {
262
- if (!reqId) return
263
- return sendMessageRef.current('', {
264
- hidden: true,
265
- approvalAction: { proposalId: reqId, action: 'reject' },
266
- })
267
- }, [])
252
+ (text: string, options?: InternalSendMessageOptions) => Promise<boolean>
253
+ >(async () => true)
254
+
255
+ // Approve/reject pass the transport's boolean outcome through so batch
256
+ // approve-all loops can mark a FAILED row (expired proposal, network
257
+ // error) instead of leaving its execution loader spinning. ONE factory
258
+ // — the two handlers differ only in the action literal.
259
+ const cardResolve = useCallback(
260
+ (action: 'approve' | 'reject') =>
261
+ (reqId?: string): void | Promise<boolean> => {
262
+ if (!reqId) return
263
+ return sendMessageRef.current('', {
264
+ hidden: true,
265
+ approvalAction: { proposalId: reqId, action },
266
+ })
267
+ },
268
+ [],
269
+ )
270
+ const cardApprove = useMemo(() => cardResolve('approve'), [cardResolve])
271
+ const cardReject = useMemo(() => cardResolve('reject'), [cardResolve])
268
272
 
269
273
  const createReducerOptions = useCallback(
270
274
  (): ChatStreamReducerOptions => ({
@@ -415,7 +419,9 @@ export function useSseChatAdapter(
415
419
  )
416
420
 
417
421
  const sendMessage = useCallback(
418
- async (text: string, sendOptions?: InternalSendMessageOptions): Promise<void> => {
422
+ // Resolves `true` on a clean turn, `false` when the request failed
423
+ // (surfaced to batch approve-all loops via the card callbacks).
424
+ async (text: string, sendOptions?: InternalSendMessageOptions): Promise<boolean> => {
419
425
  const { hidden, attachments, commandOverride, approvalAction } = sendOptions ?? {}
420
426
 
421
427
  // URL + body branch — approvalAction routes to the approval-tool
@@ -466,7 +472,20 @@ export function useSseChatAdapter(
466
472
  signal: ctrl.signal,
467
473
  })
468
474
  if (!response.ok) {
469
- throw new Error(`Chat request failed: ${response.status}`)
475
+ // Surface the SERVER's error copy (route-base envelope
476
+ // `{error, code}`) — a bare "Chat request failed: 409" told
477
+ // the user nothing when e.g. a batch approval expired; the
478
+ // server ships real copy ("This approval expired — ...").
479
+ let serverMessage: string | null = null
480
+ try {
481
+ const errBody = (await response.json()) as { error?: unknown }
482
+ if (typeof errBody?.error === 'string' && errBody.error.length > 0) {
483
+ serverMessage = errBody.error
484
+ }
485
+ } catch {
486
+ /* non-JSON error body — fall through to the generic copy */
487
+ }
488
+ throw new Error(serverMessage ?? `Chat request failed: ${response.status}`)
470
489
  }
471
490
  const reader = response.body?.getReader()
472
491
  if (!reader) throw new Error('No response body')
@@ -496,19 +515,24 @@ export function useSseChatAdapter(
496
515
  for (const event of endEvents) applyEvent(event)
497
516
  }
498
517
  }
518
+ return true
499
519
  } catch (err) {
500
520
  // AbortError on user-initiated stop is expected — keep the partial
501
- // message, no error row.
502
- if ((err as { name?: string })?.name !== 'AbortError' && !ctrl.signal.aborted) {
503
- flushDeltas()
504
- mutate((r) =>
505
- r.failSseTurn(
506
- err instanceof Error
507
- ? err.message
508
- : 'An error occurred while processing your request.',
509
- ),
510
- )
521
+ // message, no error row, and DON'T report failure (`true`): the
522
+ // request may have committed server-side, and a `false` here
523
+ // would falsely tick a batch row's failure cross.
524
+ if ((err as { name?: string })?.name === 'AbortError' || ctrl.signal.aborted) {
525
+ return true
511
526
  }
527
+ flushDeltas()
528
+ mutate((r) =>
529
+ r.failSseTurn(
530
+ err instanceof Error
531
+ ? err.message
532
+ : 'An error occurred while processing your request.',
533
+ ),
534
+ )
535
+ return false
512
536
  } finally {
513
537
  if (abortControllerRef.current === ctrl) {
514
538
  abortControllerRef.current = null
@@ -657,9 +681,10 @@ export function useSseChatAdapter(
657
681
  }
658
682
  // RETRIEVAL IS STRICTLY PRIMARY-KEY-DRIVEN. The visible prose
659
683
  // ("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'}`
684
+ // via `entityIdFilter` before the LLM is invoked. The sentence itself
685
+ // comes from the shared builder so the Mingo transport (which has no
686
+ // id filter and leans on the prose) phrases it identically.
687
+ const prompt = buildDiscussPrompt(reference)
663
688
  sendMessage(prompt, {
664
689
  commandOverride: { entityIdFilter: { tableId, id: refId } },
665
690
  })