@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
@@ -159,6 +159,16 @@ export interface ApprovalBatchData {
159
159
  executions?: Record<string, ApprovalBatchExecutionState>
160
160
  }
161
161
 
162
+ /** Approve/reject handler stamped onto approval segments. MAY resolve a
163
+ * boolean success flag — `false` means the confirm FAILED (expired
164
+ * proposal, network error); batch approve-all loops use it to tick the
165
+ * row's failure cross. `void` (legacy transports) is treated as
166
+ * success. Single source of truth for this signature — component
167
+ * props, accumulator callbacks, and adapters all reference it. */
168
+ export type ApprovalResolutionHandler = (
169
+ requestId?: string,
170
+ ) => void | boolean | Promise<void | boolean>
171
+
162
172
  // ========== Message Segment Types ==========
163
173
 
164
174
  export type TextSegment = {
@@ -192,8 +202,8 @@ export type ApprovalRequestSegment = {
192
202
  /** Display name of the user who resolved the request; baked into the client
193
203
  * variant's full-text status pill ("Approved by {name}"). */
194
204
  resolvedByName?: string | null
195
- onApprove?: (requestId?: string) => void | Promise<void>
196
- onReject?: (requestId?: string) => void | Promise<void>
205
+ onApprove?: ApprovalResolutionHandler
206
+ onReject?: ApprovalResolutionHandler
197
207
  }
198
208
 
199
209
  export type ApprovalBatchSegment = {
@@ -202,8 +212,8 @@ export type ApprovalBatchSegment = {
202
212
  status?: ChatApprovalStatus
203
213
  /** Display name of the user who resolved the request; set when the batch is resolved (null/absent for system actions). */
204
214
  resolvedByName?: string | null
205
- onApprove?: (requestId?: string) => void | Promise<void>
206
- onReject?: (requestId?: string) => void | Promise<void>
215
+ onApprove?: ApprovalResolutionHandler
216
+ onReject?: ApprovalResolutionHandler
207
217
  }
208
218
 
209
219
  export type ErrorSegment = {
@@ -274,10 +274,13 @@ export interface UnifiedChatState {
274
274
  isHydratingHistory?: boolean
275
275
 
276
276
  // ─── Actions ──────────────────────────────────────────────────────────────
277
+ /** Resolution MAY carry a boolean success flag (`false` = the request
278
+ * failed) — consumed by batch approve-all loops to mark failed rows.
279
+ * Callers that only await it are unaffected (`void` = success). */
277
280
  sendMessage: (
278
281
  text: string,
279
282
  options?: UnifiedSendMessageOptions,
280
- ) => Promise<void>
283
+ ) => Promise<void | boolean>
281
284
 
282
285
  /** Abort the in-flight stream. No-op when idle. */
283
286
  stopMessage: () => void
@@ -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
+ })
@@ -0,0 +1,156 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { makeComposeContentUrl } from '../../../../utils/content-href'
3
+ import { resolveFetchedCardHref } from '../resolve-fetched-card-href'
4
+
5
+ const HUB = 'https://www.flamingo.run'
6
+
7
+ /** The OpenFrame Help Center seam, verbatim in shape from
8
+ * `openframe-oss-frontend/src/app/(app)/help-center/help-center-content-href.ts`. */
9
+ const compose = makeComposeContentUrl({
10
+ hostedTypes: new Set(['onboarding_guide', 'product_release']),
11
+ contentOrigin: HUB,
12
+ suffixes: {
13
+ onboarding_guide: 'help-center/onboarding-guides',
14
+ product_release: 'help-center/releases',
15
+ blog_post: 'blog',
16
+ case_study: 'case-studies',
17
+ },
18
+ overrides: {
19
+ roadmap_item: (id) => ({
20
+ href: `/help-center/roadmap?search=${encodeURIComponent(id)}`,
21
+ targetPlatform: null,
22
+ }),
23
+ },
24
+ })
25
+
26
+ describe('resolveFetchedCardHref — Mingo cards with no ref metadata', () => {
27
+ it('override type deep-links on the MARKER id, not the fetched row id', () => {
28
+ // The `[card://roadmap_item:86ad3qvv5]` id is the ClickUp task id the
29
+ // roadmap list filters by; the fetched row carries its own db id.
30
+ expect(
31
+ resolveFetchedCardHref({
32
+ contentRefType: 'roadmap_item',
33
+ id: '86ad3qvv5',
34
+ item: { id: 'db-uuid-9', title: 'Deep Google Workspace tenant management' },
35
+ composeContentUrl: compose,
36
+ }),
37
+ ).toEqual({ href: '/help-center/roadmap?search=86ad3qvv5', targetPlatform: null })
38
+ })
39
+
40
+ it('hosted type uses the fetched row slug for its in-app detail route', () => {
41
+ expect(
42
+ resolveFetchedCardHref({
43
+ contentRefType: 'product_release',
44
+ id: 'row-uuid-1',
45
+ item: { id: 'row-uuid-1', slug: 'v1-2-0' },
46
+ composeContentUrl: compose,
47
+ }),
48
+ ).toEqual({ href: '/help-center/releases/v1-2-0', targetPlatform: null })
49
+ })
50
+
51
+ it('hosted type with no slug falls back to the marker id', () => {
52
+ expect(
53
+ resolveFetchedCardHref({
54
+ contentRefType: 'onboarding_guide',
55
+ id: 'guide-7',
56
+ item: { id: 'guide-7' },
57
+ composeContentUrl: compose,
58
+ }),
59
+ ).toEqual({ href: '/help-center/onboarding-guides/guide-7', targetPlatform: null })
60
+ })
61
+
62
+ it('non-hosted type resolves to the content hub', () => {
63
+ expect(
64
+ resolveFetchedCardHref({
65
+ contentRefType: 'case_study',
66
+ id: 'cs-1',
67
+ item: { id: 'cs-1', slug: 'acme-migration' },
68
+ composeContentUrl: compose,
69
+ }),
70
+ ).toEqual({ href: `${HUB}/case-studies/acme-migration`, targetPlatform: null })
71
+ })
72
+
73
+ it('legacy rail-vocab alias resolves to the SAME url as the canonical type', () => {
74
+ // The card registry keys blog entries by `blog_post_existing` (what
75
+ // `buildListUrl` wants). Both spellings must produce one url — the reported
76
+ // split was `/blog/what-is-mdr` vs `/blog_post_existing/bitwarden-…`.
77
+ const canonical = resolveFetchedCardHref({
78
+ contentRefType: 'blog_post',
79
+ id: 'p1',
80
+ item: { id: 'p1', slug: 'bitwarden-review-for-msps' },
81
+ composeContentUrl: compose,
82
+ })
83
+ const alias = resolveFetchedCardHref({
84
+ contentRefType: 'blog_post_existing',
85
+ id: 'p1',
86
+ item: { id: 'p1', slug: 'bitwarden-review-for-msps' },
87
+ composeContentUrl: compose,
88
+ })
89
+ expect(alias).toEqual(canonical)
90
+ expect(alias).toEqual({
91
+ href: `${HUB}/blog/bitwarden-review-for-msps`,
92
+ targetPlatform: null,
93
+ })
94
+ })
95
+
96
+ it('no seam wired → null (card stays unlinked, pre-existing behavior)', () => {
97
+ expect(
98
+ resolveFetchedCardHref({
99
+ contentRefType: 'roadmap_item',
100
+ id: '86ad3qvv5',
101
+ item: { id: '86ad3qvv5' },
102
+ }),
103
+ ).toBeNull()
104
+ })
105
+
106
+ it('guards a missing type / id / malformed item', () => {
107
+ expect(
108
+ resolveFetchedCardHref({ contentRefType: '', id: 'x', item: {}, composeContentUrl: compose }),
109
+ ).toBeNull()
110
+ expect(
111
+ resolveFetchedCardHref({
112
+ contentRefType: 'case_study',
113
+ id: '',
114
+ item: {},
115
+ composeContentUrl: compose,
116
+ }),
117
+ ).toBeNull()
118
+ // A non-object item (or a non-string slug) must not throw — it just loses
119
+ // the slug hint and falls back to the marker id.
120
+ expect(
121
+ resolveFetchedCardHref({
122
+ contentRefType: 'case_study',
123
+ id: 'cs-2',
124
+ item: { slug: 42 },
125
+ composeContentUrl: compose,
126
+ }),
127
+ ).toEqual({ href: `${HUB}/case-studies/cs-2`, targetPlatform: null })
128
+ })
129
+ })
130
+
131
+ describe('makeComposeContentUrl — the new `slug` hint', () => {
132
+ it('externalUrl still wins over the hint (SSE path unchanged)', () => {
133
+ expect(
134
+ compose({
135
+ type: 'product_release',
136
+ identifier: 'row-uuid-1',
137
+ slug: 'from-the-row',
138
+ externalUrl: `${HUB}/releases/from-the-url`,
139
+ }),
140
+ ).toEqual({ href: '/help-center/releases/from-the-url', targetPlatform: null })
141
+ })
142
+
143
+ it('applies to non-hosted types too — hub detail routes are slug-based', () => {
144
+ expect(compose({ type: 'blog_post', identifier: 'id-1', slug: 'hello-world' })).toEqual({
145
+ href: `${HUB}/blog/hello-world`,
146
+ targetPlatform: null,
147
+ })
148
+ })
149
+
150
+ it('no hint → identifier, unchanged for page-view and SSE callers', () => {
151
+ expect(compose({ type: 'blog_post', identifier: 'hello-world' })).toEqual({
152
+ href: `${HUB}/blog/hello-world`,
153
+ targetPlatform: null,
154
+ })
155
+ })
156
+ })
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The ONE place the "Ask Mingo" (discuss) prompt for an inline entity card is
3
+ * composed — shared by every transport so the affordance reads identically
4
+ * wherever it is clicked.
5
+ *
6
+ * ## Why the two transports still differ underneath
7
+ *
8
+ * The prose is UX only. What narrows retrieval differs:
9
+ *
10
+ * - **SSE / guide** — `sendMessage(prompt, { commandOverride: { entityIdFilter:
11
+ * { tableId, id } } })`. Retrieval is strictly primary-key-driven; the
12
+ * sentence is decoration.
13
+ * - **Mingo / NATS** — the agent backend has no entity-id-filtered retrieval
14
+ * (`ContextItemType` covers DEVICE / SCRIPT / TICKET / ORGANIZATION / USER /
15
+ * KB_ARTICLE / POLICY / QUERY / SCHEDULED_SCRIPT — no content types), so the
16
+ * prompt is ALL the agent gets. Hence `includeReference`, which appends the
17
+ * type + id so the agent can still identify the exact row by text.
18
+ *
19
+ * When the backend grows an id-filter for the agent transport, the Mingo callers
20
+ * switch to it and drop `includeReference` — the prose stays put.
21
+ */
22
+
23
+ import { sanitizeTitleForChat } from './slash-dispatch-utils'
24
+
25
+ export interface DiscussPromptRef {
26
+ /** documentType, e.g. `'roadmap_item'`. */
27
+ type: string
28
+ /** Primary key — the id from the `[card://type:id]` marker. */
29
+ id: string
30
+ /** Display title. May be the bare id on a synthetic ref (Mingo). */
31
+ title?: string | null
32
+ }
33
+
34
+ export interface BuildDiscussPromptOptions {
35
+ /**
36
+ * Append `(<type> <id>)` so a transport WITHOUT structured entity filtering
37
+ * still points the agent at the exact row. Default `false` — the SSE path
38
+ * sends `entityIdFilter` and must keep the sentence clean.
39
+ */
40
+ includeReference?: boolean
41
+ }
42
+
43
+ /**
44
+ * `Tell me more about <title>` — plus `(<type> <id>)` when
45
+ * `includeReference` is set and the title alone is ambiguous.
46
+ *
47
+ * Falls back to `this item` when the ref has no usable title, matching the
48
+ * long-standing SSE behavior. A title that is just the id (the synthetic ref a
49
+ * Mingo `[card://…]` marker produces before its row loads) is treated as
50
+ * missing, so the prompt never degrades to `Tell me more about 86ad3qvv5`.
51
+ */
52
+ export function buildDiscussPrompt(
53
+ reference: DiscussPromptRef,
54
+ options: BuildDiscussPromptOptions = {},
55
+ ): string {
56
+ const id = (reference.id ?? '').trim()
57
+ const rawTitle = (reference.title ?? '').trim()
58
+ const usableTitle = rawTitle && rawTitle !== id ? sanitizeTitleForChat(rawTitle) : ''
59
+ const prompt = `Tell me more about ${usableTitle || 'this item'}`
60
+ if (!options.includeReference || !id) return prompt
61
+ // Only worth the noise when the reference adds something the title doesn't.
62
+ return `${prompt} (${reference.type} ${id})`
63
+ }
@@ -196,4 +196,20 @@ export {
196
196
  resolveSourceIcon,
197
197
  } from './source-row-cta'
198
198
 
199
+ // Post-fetch enrichment for the synthetic ChatRef a `[card://type:id]` marker
200
+ // produces when the transport ships no refs metadata (Mingo/NATS).
201
+ export {
202
+ resolveFetchedCardHref,
203
+ readFetchedCardTitle,
204
+ type FetchedCardHrefInput,
205
+ } from './resolve-fetched-card-href'
206
+
207
+ // The ONE "Ask Mingo" prompt builder — shared by the SSE adapter, the NATS
208
+ // adapter, and hosts that inject their own Mingo state (openframe-frontend).
209
+ export {
210
+ buildDiscussPrompt,
211
+ type DiscussPromptRef,
212
+ type BuildDiscussPromptOptions,
213
+ } from './discuss-ref-prompt'
214
+
199
215
  export * from './scripted-stream'
@@ -14,6 +14,7 @@ import type {
14
14
  ApprovalRequestSegment,
15
15
  ApprovalBatchSegment,
16
16
  ApprovalBatchExecutionState,
17
+ ApprovalResolutionHandler,
17
18
  ContextCompactionSegment,
18
19
  ErrorSegment,
19
20
  PendingApproval,
@@ -22,10 +23,17 @@ import type {
22
23
  ChatApprovalStatus,
23
24
  ExecutingToolState,
24
25
  } from '../types'
26
+ import {
27
+ applyApprovalStatusToSegment,
28
+ nextBatchExecution,
29
+ withBatchExecution,
30
+ } from '../stream/message-mutations'
25
31
 
26
32
  export interface AccumulatorCallbacks {
27
- onApprove?: (requestId?: string) => Promise<void> | void
28
- onReject?: (requestId?: string) => Promise<void> | void
33
+ /** See `ApprovalResolutionHandler` (message.types) — the SSOT for the
34
+ * approve/reject signature incl. the boolean failure flag. */
35
+ onApprove?: ApprovalResolutionHandler
36
+ onReject?: ApprovalResolutionHandler
29
37
  }
30
38
 
31
39
  /**
@@ -244,25 +252,13 @@ export class MessageSegmentAccumulator {
244
252
  if (!hasCall) return seg
245
253
 
246
254
  const prev: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
247
- // Never downgrade a done slot back to executing (redelivered EXECUTING
248
- // after its EXECUTED already landed) — matched, but unchanged.
249
- if (toolData.type === 'EXECUTING_TOOL' && prev?.status === 'done') {
250
- matched = true
251
- return seg
252
- }
253
- const next: ApprovalBatchExecutionState =
254
- toolData.type === 'EXECUTED_TOOL'
255
- ? { status: 'done', result: toolData.result, success: toolData.success }
256
- : { status: 'executing', result: prev?.result, success: prev?.success }
257
-
255
+ // Shared rule (`nextBatchExecution` from message-mutations): the
256
+ // never-downgrade guard + EXECUTED/EXECUTING ternary — one
257
+ // implementation with the message-array projection.
258
+ const next = nextBatchExecution(prev, toolData)
258
259
  matched = true
259
- return {
260
- ...seg,
261
- data: {
262
- ...seg.data,
263
- executions: { ...(seg.data.executions ?? {}), [execId]: next },
264
- },
265
- }
260
+ if (next === null) return seg
261
+ return withBatchExecution(seg, execId, next)
266
262
  })
267
263
  return matched
268
264
  }
@@ -429,15 +425,14 @@ export class MessageSegmentAccumulator {
429
425
  * resolved card shows "by {name}"; omit it to leave any existing value untouched.
430
426
  */
431
427
  updateApprovalStatus(requestId: string, status: ChatApprovalStatus, resolvedByName?: string | null): MessageSegment[] {
432
- this.segments = this.segments.map(segment => {
433
- if (segment.type === 'approval_request' && segment.data.requestId === requestId) {
434
- return { ...segment, status }
435
- }
436
- if (segment.type === 'approval_batch' && segment.data.approvalRequestId === requestId) {
437
- return { ...segment, status, resolvedByName: resolvedByName ?? segment.resolvedByName }
438
- }
439
- return segment
440
- })
428
+ // ONE rule, two containers: `applyApprovalStatusToSegment` is the
429
+ // same predicate the message-array projection uses
430
+ // (`projectApprovalResolutionToMessages`) — anchor status flip AND
431
+ // batch-row execution tick, so this flat-list path can never drift
432
+ // from the projection again (it previously missed the row case).
433
+ this.segments = this.segments.map((segment) =>
434
+ applyApprovalStatusToSegment(segment, requestId, status, resolvedByName),
435
+ )
441
436
  return this.getSegments()
442
437
  }
443
438
 
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Post-fetch href fallback for an inline chat card whose `ChatRef` carried no
3
+ * `externalUrl`.
4
+ *
5
+ * ## Why a fetch-mode card can arrive without a URL
6
+ *
7
+ * The two chat transports deliver entity references differently:
8
+ *
9
+ * - **SSE / guide transport** — the server ships a `refs` metadata frame, so
10
+ * each `[card://type:id]` marker expands into a FULL `ChatRef`
11
+ * (`title` / `url` / `targetPlatform` / `sourceRepo`). `resolveSourceRowCTA`
12
+ * then routes that `externalUrl` through the host's `composeContentUrl`
13
+ * seam and the card is clickable.
14
+ * - **Mingo / NATS transport** (`messageData: [{ type: 'GUIDE' | 'TEXT', text }]`)
15
+ * — the body carries bare `[card://type:id]` markers and NOTHING else.
16
+ * `chat-message-enhanced.tsx` synthesizes a minimal `{ type, id, title: id,
17
+ * url: null }` ref so fetch-mode cards can still self-fetch their row, but
18
+ * `resolveSourceRowCTA` has no `externalUrl`, no `path` and no
19
+ * `targetPlatform` to work with → `href: null` → the card renders with its
20
+ * real title and description but goes nowhere when clicked.
21
+ *
22
+ * This resolver closes that gap WITHOUT a second URL vocabulary: it calls the
23
+ * exact same `composeContentUrl` seam the page cards (`resolveContentHref`) and
24
+ * the SSE cards (`resolveSourceRowCTA`) use, so one host config —
25
+ * `hostedTypes` / `suffixes` / `overrides` — governs all three.
26
+ *
27
+ * `identifier` stays the marker's primary key (that is what `overrides` such as
28
+ * roadmap's `?search=<task id>` deep-link on); the row's `slug` rides the
29
+ * separate `slug` hint for hosted types whose detail route is slug-based.
30
+ *
31
+ * Pure — no React, no DOM. The caller owns `safeHref` validation.
32
+ */
33
+
34
+ import type { ComposeContentUrl } from '../../../utils/content-href'
35
+ import { canonicalContentRefType } from '../../../utils/list-url'
36
+
37
+ export interface FetchedCardHrefInput {
38
+ /** Canonical `documentType` for the seam (registry `contentRefType`). */
39
+ contentRefType: string
40
+ /** The marker's primary key — `ChatRef.id`, NOT the fetched row's id. */
41
+ id: string
42
+ /** The row the card fetched. Shape is per-type and opaque here; only
43
+ * `slug` and `platforms` are read, both defensively. */
44
+ item: unknown
45
+ /** Host seam. Absent (unwired runtime) → no fallback, same as today. */
46
+ composeContentUrl?: ComposeContentUrl
47
+ }
48
+
49
+ /** Read `item.slug` when it is a non-empty string. */
50
+ function readSlug(item: unknown): string | null {
51
+ if (!item || typeof item !== 'object') return null
52
+ const slug = (item as { slug?: unknown }).slug
53
+ return typeof slug === 'string' && slug.trim() ? slug.trim() : null
54
+ }
55
+
56
+ /**
57
+ * Human title of a fetched row — `title`, else `name`, else null.
58
+ *
59
+ * A Mingo synthetic ref carries `title: <the marker id>` because that is all
60
+ * the transport shipped. Everything downstream that prints or sends the title
61
+ * (the "Ask Mingo" prompt, host renderers falling back to `ref.title`) would
62
+ * otherwise say `86ad3qvv5`. Once the row loads, this is the real title.
63
+ */
64
+ export function readFetchedCardTitle(item: unknown): string | null {
65
+ if (!item || typeof item !== 'object') return null
66
+ for (const key of ['title', 'name'] as const) {
67
+ const value = (item as Record<string, unknown>)[key]
68
+ if (typeof value === 'string' && value.trim()) return value.trim()
69
+ }
70
+ return null
71
+ }
72
+
73
+ /** Read `item.platforms` when it is an array — the hub composer keys its
74
+ * cross-platform topology off it; the embedder default ignores it. */
75
+ function readPlatforms(item: unknown): Array<{ name?: string }> | undefined {
76
+ if (!item || typeof item !== 'object') return undefined
77
+ const platforms = (item as { platforms?: unknown }).platforms
78
+ return Array.isArray(platforms) ? (platforms as Array<{ name?: string }>) : undefined
79
+ }
80
+
81
+ /**
82
+ * Resolve `{href, targetPlatform}` for a freshly fetched card row, or `null`
83
+ * when the host wired no seam (the caller then keeps the card unlinked, which
84
+ * is the pre-existing behavior).
85
+ */
86
+ export function resolveFetchedCardHref({
87
+ contentRefType,
88
+ id,
89
+ item,
90
+ composeContentUrl,
91
+ }: FetchedCardHrefInput): { href: string; targetPlatform: string | null } | null {
92
+ if (!composeContentUrl || !contentRefType || !id) return null
93
+ return composeContentUrl({
94
+ // The card registry keys some entries by the legacy rail vocabulary
95
+ // (`blog_post_existing`) because that is what `buildListUrl` expects.
96
+ // The seam speaks the CANONICAL vocabulary — a host's `hostedTypes` /
97
+ // `overrides` are written against `blog_post`, so an un-aliased type
98
+ // misses them all and lands on `<origin>/blog_post_existing/<slug>`.
99
+ type: canonicalContentRefType(contentRefType),
100
+ identifier: id,
101
+ slug: readSlug(item),
102
+ platforms: readPlatforms(item),
103
+ })
104
+ }
@@ -29,6 +29,7 @@ import { getBaseUrl } from '../../../utils/cn'
29
29
  import { safeHref } from './compact-card-classes'
30
30
  import type { ChatRef } from '../chat-ref.types'
31
31
  import type { ComposeContentUrl } from '../../../utils/content-href'
32
+ import { canonicalContentRefType } from '../../../utils/list-url'
32
33
 
33
34
  /** Path sanitization — keep alphanumerics, slash, dash, dot, underscore;
34
35
  * strip everything else. Defends against a hostile mapper returning a
@@ -233,7 +234,10 @@ export function resolveSourceRowCTA(
233
234
  // from the externalUrl) and everything else to the externalUrl verbatim —
234
235
  // the SAME resolver the page-view cards use, so chat + page links match.
235
236
  const composed = ctx.composeContentUrl({
236
- type: row.documentType,
237
+ // Canonical vocabulary only — a rail-vocab alias would miss the host's
238
+ // `hostedTypes` / `overrides` and fall through to the seam's default
239
+ // `<origin>/<type>/<id>` branch. Same canonicalizer `buildListUrl` uses.
240
+ type: canonicalContentRefType(row.documentType),
237
241
  identifier: idValue,
238
242
  externalUrl: row.externalUrl,
239
243
  targetPlatform: row.targetPlatform ?? null,