@flamingo-stack/openframe-frontend-core 0.0.498-1506.4000.1 → 0.0.498

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 (290) hide show
  1. package/dist/{chunk-ZHC2BCXK.js → chunk-2OPD6KRB.js} +4 -4
  2. package/dist/{chunk-5BQWVNRS.cjs → chunk-2R25CGSO.cjs} +62 -62
  3. package/dist/{chunk-5BQWVNRS.cjs.map → chunk-2R25CGSO.cjs.map} +1 -1
  4. package/dist/{chunk-242K7LZY.cjs → chunk-4EHQEMN4.cjs} +11 -11
  5. package/dist/{chunk-242K7LZY.cjs.map → chunk-4EHQEMN4.cjs.map} +1 -1
  6. package/dist/{chunk-4KKGHPNE.cjs → chunk-4NIBKEED.cjs} +9 -9
  7. package/dist/{chunk-4KKGHPNE.cjs.map → chunk-4NIBKEED.cjs.map} +1 -1
  8. package/dist/{chunk-5QWYIPMH.js → chunk-5ZL7PA63.js} +2 -2
  9. package/dist/{chunk-3VGRZ62V.js → chunk-6GAJETAP.js} +2 -2
  10. package/dist/{chunk-EO35UHFT.cjs → chunk-A3Z63ZL5.cjs} +60 -60
  11. package/dist/chunk-A3Z63ZL5.cjs.map +1 -0
  12. package/dist/{chunk-34C2JV2C.js → chunk-G6ARA2HD.js} +2 -2
  13. package/dist/{chunk-WNXIG6YY.js → chunk-K5HHLTSG.js} +2 -2
  14. package/dist/{chunk-42NYDEHH.js → chunk-KK4M4URN.js} +2 -2
  15. package/dist/{chunk-42SMPKGY.js → chunk-MF3VJKHT.js} +2 -2
  16. package/dist/{chunk-EORFD5WB.cjs → chunk-MSB3PEPP.cjs} +15 -15
  17. package/dist/{chunk-EORFD5WB.cjs.map → chunk-MSB3PEPP.cjs.map} +1 -1
  18. package/dist/{chunk-F7XPMG25.cjs → chunk-MVORKLWX.cjs} +27 -28
  19. package/dist/chunk-MVORKLWX.cjs.map +1 -0
  20. package/dist/{chunk-D3LUKR2Y.js → chunk-OYPUQJJ2.js} +10044 -12292
  21. package/dist/chunk-OYPUQJJ2.js.map +1 -0
  22. package/dist/{chunk-62TM2RVZ.cjs → chunk-T67MZAQJ.cjs} +26 -26
  23. package/dist/{chunk-62TM2RVZ.cjs.map → chunk-T67MZAQJ.cjs.map} +1 -1
  24. package/dist/{chunk-BUBQEZ5J.js → chunk-VLWVM6ZS.js} +2 -2
  25. package/dist/{chunk-632XJ2ZT.js → chunk-VOOEX4IZ.js} +5 -6
  26. package/dist/chunk-VOOEX4IZ.js.map +1 -0
  27. package/dist/{chunk-5ZY5JM4Z.js → chunk-W4XZ3FBZ.js} +10 -10
  28. package/dist/chunk-W4XZ3FBZ.js.map +1 -0
  29. package/dist/{chunk-2LYA3RBT.cjs → chunk-XI44QDCD.cjs} +37 -37
  30. package/dist/{chunk-2LYA3RBT.cjs.map → chunk-XI44QDCD.cjs.map} +1 -1
  31. package/dist/{chunk-WZCQW4MW.js → chunk-XSNKSMB3.js} +4 -4
  32. package/dist/{chunk-J3NHQ4JB.cjs → chunk-ZBTIN7RC.cjs} +31 -31
  33. package/dist/{chunk-J3NHQ4JB.cjs.map → chunk-ZBTIN7RC.cjs.map} +1 -1
  34. package/dist/{chunk-SS3D3DHL.cjs → chunk-ZEZTITQZ.cjs} +4886 -7134
  35. package/dist/chunk-ZEZTITQZ.cjs.map +1 -0
  36. package/dist/{chunk-44C23SSC.cjs → chunk-ZHNWWFIM.cjs} +5 -5
  37. package/dist/{chunk-44C23SSC.cjs.map → chunk-ZHNWWFIM.cjs.map} +1 -1
  38. package/dist/{chunk-H6GFQ4AO.cjs → chunk-ZTE32A7C.cjs} +7 -7
  39. package/dist/{chunk-H6GFQ4AO.cjs.map → chunk-ZTE32A7C.cjs.map} +1 -1
  40. package/dist/components/case-studies/index.cjs +8 -8
  41. package/dist/components/case-studies/index.js +2 -2
  42. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  43. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  44. package/dist/components/chat/hooks/index.d.ts +1 -0
  45. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
  48. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +48 -15
  49. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  50. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +6 -0
  51. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -0
  52. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +57 -10
  53. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  54. package/dist/components/chat/index.cjs +2 -10
  55. package/dist/components/chat/index.cjs.map +1 -1
  56. package/dist/components/chat/index.d.ts +0 -1
  57. package/dist/components/chat/index.d.ts.map +1 -1
  58. package/dist/components/chat/index.js +13 -21
  59. package/dist/components/chat/types/api.types.d.ts +81 -1
  60. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  61. package/dist/components/chat/types/processing.types.d.ts +93 -2
  62. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  63. package/dist/components/chat/utils/chunk-parser.d.ts +25 -0
  64. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -0
  65. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +6 -34
  66. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  67. package/dist/components/chat/utils/index.d.ts +3 -2
  68. package/dist/components/chat/utils/index.d.ts.map +1 -1
  69. package/dist/components/chat/utils/process-historical-messages.d.ts +11 -59
  70. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  71. package/dist/components/contact/index.cjs +3 -3
  72. package/dist/components/contact/index.js +2 -2
  73. package/dist/components/docs/index.cjs +5 -5
  74. package/dist/components/docs/index.js +4 -4
  75. package/dist/components/embeds/index.cjs +3 -3
  76. package/dist/components/embeds/index.js +2 -2
  77. package/dist/components/faq/index.cjs +3 -3
  78. package/dist/components/faq/index.js +2 -2
  79. package/dist/components/features/index.cjs +2 -2
  80. package/dist/components/features/index.js +1 -1
  81. package/dist/components/help-center-pages/index.cjs +20 -20
  82. package/dist/components/help-center-pages/index.js +11 -11
  83. package/dist/components/index.cjs +130 -172
  84. package/dist/components/index.cjs.map +1 -1
  85. package/dist/components/index.js +21 -63
  86. package/dist/components/index.js.map +1 -1
  87. package/dist/components/navigation/header.d.ts.map +1 -1
  88. package/dist/components/navigation/index.cjs +2 -2
  89. package/dist/components/navigation/index.js +1 -1
  90. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  91. package/dist/components/onboarding-guides/index.cjs +5 -5
  92. package/dist/components/onboarding-guides/index.js +4 -4
  93. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  94. package/dist/components/related-content/index.cjs +3 -3
  95. package/dist/components/related-content/index.js +2 -2
  96. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  97. package/dist/components/tickets/index.cjs +5 -5
  98. package/dist/components/tickets/index.js +4 -4
  99. package/dist/components/ui/index.cjs +2 -44
  100. package/dist/components/ui/index.cjs.map +1 -1
  101. package/dist/components/ui/index.d.ts +2 -1
  102. package/dist/components/ui/index.d.ts.map +1 -1
  103. package/dist/components/ui/index.js +13 -55
  104. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  105. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  106. package/dist/components/ui/simple-markdown-renderer.d.ts +73 -0
  107. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -0
  108. package/dist/index.cjs +2 -60
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.js +13 -71
  111. package/dist/utils/index.cjs +40 -169
  112. package/dist/utils/index.cjs.map +1 -1
  113. package/dist/utils/index.d.ts +0 -1
  114. package/dist/utils/index.d.ts.map +1 -1
  115. package/dist/utils/index.js +41 -162
  116. package/dist/utils/index.js.map +1 -1
  117. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  118. package/package.json +1 -7
  119. package/src/components/chat/__tests__/chat-message-list.test.tsx +6 -381
  120. package/src/components/chat/chat-message-enhanced.tsx +20 -122
  121. package/src/components/chat/chat-message-list.tsx +132 -409
  122. package/src/components/chat/embeddable-chat.tsx +0 -6
  123. package/src/components/chat/hooks/.index.md +33 -30
  124. package/src/components/chat/hooks/.use-nats-chat-adapter.md +55 -36
  125. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +77 -0
  126. package/src/components/chat/hooks/index.ts +1 -0
  127. package/src/components/chat/hooks/use-chat.ts +0 -8
  128. package/src/components/chat/hooks/use-chunk-catchup.ts +1 -26
  129. package/src/components/chat/hooks/use-nats-chat-adapter.ts +825 -254
  130. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +489 -0
  131. package/src/components/chat/hooks/use-sse-chat-adapter.ts +684 -380
  132. package/src/components/chat/index.ts +0 -5
  133. package/src/components/chat/thinking-display.tsx +1 -1
  134. package/src/components/chat/types/.api.types.md +49 -39
  135. package/src/components/chat/types/.processing.types.md +58 -29
  136. package/src/components/chat/types/api.types.ts +73 -6
  137. package/src/components/chat/types/processing.types.ts +53 -11
  138. package/src/components/chat/utils/.chunk-parser.md +62 -0
  139. package/src/components/chat/utils/.index.md +40 -31
  140. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +0 -61
  141. package/src/components/chat/utils/chunk-parser.ts +262 -0
  142. package/src/components/chat/utils/extract-incomplete-message-state.ts +5 -63
  143. package/src/components/chat/utils/index.ts +8 -5
  144. package/src/components/chat/utils/process-historical-messages.ts +386 -358
  145. package/src/components/navigation/header.tsx +2 -1
  146. package/src/components/navigation/mingo-ai-button.tsx +4 -1
  147. package/src/components/navigation/mobile-nav-panel.tsx +5 -5
  148. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +4 -22
  149. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  150. package/src/components/shared/product-release/release-detail-page.tsx +4 -19
  151. package/src/components/ui/.simple-markdown-renderer.md +52 -0
  152. package/src/components/ui/index.ts +2 -1
  153. package/src/components/ui/release-changelog-section.tsx +1 -1
  154. package/src/components/ui/rich-markdown-renderer.tsx +1223 -0
  155. package/src/components/ui/simple-markdown-renderer.tsx +964 -0
  156. package/src/components/ui/ticket-info-section.tsx +1 -1
  157. package/src/utils/index.ts +0 -1
  158. package/src/utils/markdown-section-extractor.ts +56 -41
  159. package/dist/chat-protocol/decode.d.ts +0 -58
  160. package/dist/chat-protocol/decode.d.ts.map +0 -1
  161. package/dist/chat-protocol/encode.d.ts +0 -44
  162. package/dist/chat-protocol/encode.d.ts.map +0 -1
  163. package/dist/chat-protocol/env-flag.d.ts +0 -35
  164. package/dist/chat-protocol/env-flag.d.ts.map +0 -1
  165. package/dist/chat-protocol/events.d.ts +0 -207
  166. package/dist/chat-protocol/events.d.ts.map +0 -1
  167. package/dist/chat-protocol/frames.d.ts +0 -213
  168. package/dist/chat-protocol/frames.d.ts.map +0 -1
  169. package/dist/chat-protocol/index.cjs +0 -500
  170. package/dist/chat-protocol/index.cjs.map +0 -1
  171. package/dist/chat-protocol/index.d.ts +0 -18
  172. package/dist/chat-protocol/index.d.ts.map +0 -1
  173. package/dist/chat-protocol/index.js +0 -484
  174. package/dist/chat-protocol/index.js.map +0 -1
  175. package/dist/chat-protocol/ip-normalize.d.ts +0 -44
  176. package/dist/chat-protocol/ip-normalize.d.ts.map +0 -1
  177. package/dist/chat-protocol/nats-decoder.d.ts +0 -26
  178. package/dist/chat-protocol/nats-decoder.d.ts.map +0 -1
  179. package/dist/chunk-5ZY5JM4Z.js.map +0 -1
  180. package/dist/chunk-632XJ2ZT.js.map +0 -1
  181. package/dist/chunk-D3LUKR2Y.js.map +0 -1
  182. package/dist/chunk-EO35UHFT.cjs.map +0 -1
  183. package/dist/chunk-F7XPMG25.cjs.map +0 -1
  184. package/dist/chunk-SS3D3DHL.cjs.map +0 -1
  185. package/dist/components/chat/stream/chat-dialog-store.d.ts +0 -184
  186. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +0 -1
  187. package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -367
  188. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +0 -1
  189. package/dist/components/chat/stream/delta-batcher.d.ts +0 -51
  190. package/dist/components/chat/stream/delta-batcher.d.ts.map +0 -1
  191. package/dist/components/chat/stream/index.d.ts +0 -17
  192. package/dist/components/chat/stream/index.d.ts.map +0 -1
  193. package/dist/components/chat/stream/message-mutations.d.ts +0 -77
  194. package/dist/components/chat/stream/message-mutations.d.ts.map +0 -1
  195. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +0 -22
  196. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +0 -1
  197. package/dist/components/ui/markdown/base-components.d.ts +0 -51
  198. package/dist/components/ui/markdown/base-components.d.ts.map +0 -1
  199. package/dist/components/ui/markdown/engine.d.ts +0 -99
  200. package/dist/components/ui/markdown/engine.d.ts.map +0 -1
  201. package/dist/components/ui/markdown/heading-ids.d.ts +0 -65
  202. package/dist/components/ui/markdown/heading-ids.d.ts.map +0 -1
  203. package/dist/components/ui/markdown/index.d.ts +0 -18
  204. package/dist/components/ui/markdown/index.d.ts.map +0 -1
  205. package/dist/components/ui/markdown/mermaid-diagram.d.ts +0 -79
  206. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +0 -1
  207. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +0 -14
  208. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +0 -1
  209. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -40
  210. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +0 -1
  211. package/dist/components/ui/markdown/rich/shortcodes.d.ts +0 -8
  212. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +0 -1
  213. package/dist/components/ui/markdown/sanitize.d.ts +0 -153
  214. package/dist/components/ui/markdown/sanitize.d.ts.map +0 -1
  215. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +0 -23
  216. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +0 -1
  217. package/dist/components/ui/markdown/streaming.d.ts +0 -78
  218. package/dist/components/ui/markdown/streaming.d.ts.map +0 -1
  219. package/dist/components/ui/markdown/text-size.d.ts +0 -34
  220. package/dist/components/ui/markdown/text-size.d.ts.map +0 -1
  221. package/dist/utils/markdown-fences.d.ts +0 -42
  222. package/dist/utils/markdown-fences.d.ts.map +0 -1
  223. package/dist/utils/markdown-heading-id.d.ts +0 -85
  224. package/dist/utils/markdown-heading-id.d.ts.map +0 -1
  225. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -317
  226. package/src/chat-protocol/__tests__/chat-protocol.test.ts +0 -516
  227. package/src/chat-protocol/__tests__/env-flag.test.ts +0 -52
  228. package/src/chat-protocol/__tests__/ip-normalize.test.ts +0 -136
  229. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -232
  230. package/src/chat-protocol/decode.ts +0 -278
  231. package/src/chat-protocol/encode.ts +0 -71
  232. package/src/chat-protocol/env-flag.ts +0 -39
  233. package/src/chat-protocol/events.ts +0 -245
  234. package/src/chat-protocol/frames.ts +0 -245
  235. package/src/chat-protocol/index.ts +0 -21
  236. package/src/chat-protocol/ip-normalize.ts +0 -146
  237. package/src/chat-protocol/nats-decoder.ts +0 -258
  238. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -207
  239. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -250
  240. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +0 -37
  241. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  242. package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +0 -92
  243. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +0 -438
  244. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +0 -318
  245. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +0 -940
  246. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +0 -806
  247. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +0 -467
  248. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -1248
  249. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +0 -255
  250. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +0 -105
  251. package/src/components/chat/stream/chat-dialog-store.ts +0 -536
  252. package/src/components/chat/stream/chat-stream-reducer.ts +0 -1876
  253. package/src/components/chat/stream/delta-batcher.ts +0 -168
  254. package/src/components/chat/stream/index.ts +0 -55
  255. package/src/components/chat/stream/message-mutations.ts +0 -340
  256. package/src/components/chat/stream/use-chat-stream-reducer.ts +0 -126
  257. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +0 -420
  258. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +0 -605
  259. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -394
  260. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +0 -270
  261. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +0 -2420
  262. package/src/components/ui/__tests__/markdown-parity.test.tsx +0 -2540
  263. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +0 -176
  264. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +0 -151
  265. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +0 -354
  266. package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +0 -118
  267. package/src/components/ui/markdown/__tests__/streaming.test.tsx +0 -436
  268. package/src/components/ui/markdown/base-components.tsx +0 -360
  269. package/src/components/ui/markdown/engine.tsx +0 -315
  270. package/src/components/ui/markdown/heading-ids.ts +0 -239
  271. package/src/components/ui/markdown/index.ts +0 -49
  272. package/src/components/ui/markdown/mermaid-diagram.tsx +0 -323
  273. package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -199
  274. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +0 -184
  275. package/src/components/ui/markdown/rich/shortcodes.ts +0 -170
  276. package/src/components/ui/markdown/sanitize.ts +0 -3584
  277. package/src/components/ui/markdown/simple-markdown-renderer.tsx +0 -28
  278. package/src/components/ui/markdown/streaming.ts +0 -390
  279. package/src/components/ui/markdown/text-size.ts +0 -106
  280. package/src/utils/markdown-fences.ts +0 -127
  281. package/src/utils/markdown-heading-id.ts +0 -348
  282. /package/dist/{chunk-ZHC2BCXK.js.map → chunk-2OPD6KRB.js.map} +0 -0
  283. /package/dist/{chunk-5QWYIPMH.js.map → chunk-5ZL7PA63.js.map} +0 -0
  284. /package/dist/{chunk-3VGRZ62V.js.map → chunk-6GAJETAP.js.map} +0 -0
  285. /package/dist/{chunk-34C2JV2C.js.map → chunk-G6ARA2HD.js.map} +0 -0
  286. /package/dist/{chunk-WNXIG6YY.js.map → chunk-K5HHLTSG.js.map} +0 -0
  287. /package/dist/{chunk-42NYDEHH.js.map → chunk-KK4M4URN.js.map} +0 -0
  288. /package/dist/{chunk-42SMPKGY.js.map → chunk-MF3VJKHT.js.map} +0 -0
  289. /package/dist/{chunk-BUBQEZ5J.js.map → chunk-VLWVM6ZS.js.map} +0 -0
  290. /package/dist/{chunk-WZCQW4MW.js.map → chunk-XSNKSMB3.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-section-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/markdown-section-extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,4DAA4D;IAC5D,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAUD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,eAAe,EAAE,CA+CnB"}
1
+ {"version":3,"file":"markdown-section-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/markdown-section-extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,4DAA4D;IAC5D,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAUD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,eAAe,EAAE,CAoEnB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.498-1506.4000.1",
3
+ "version": "0.0.498",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -102,12 +102,6 @@
102
102
  "require": "./dist/platform-domains.cjs",
103
103
  "default": "./dist/platform-domains.js"
104
104
  },
105
- "./chat-protocol": {
106
- "types": "./dist/chat-protocol/index.d.ts",
107
- "import": "./dist/chat-protocol/index.js",
108
- "require": "./dist/chat-protocol/index.cjs",
109
- "default": "./dist/chat-protocol/index.js"
110
- },
111
105
  "./components/ui/file-manager": {
112
106
  "types": "./dist/components/ui/file-manager/index.d.ts",
113
107
  "import": "./dist/components/ui/file-manager/index.js",
@@ -1,28 +1,15 @@
1
- import { act, render } from '@testing-library/react'
1
+ import { render } from '@testing-library/react'
2
2
  import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
  import type { Message } from '../types/message.types'
4
4
 
5
5
  const scrollToBottom = vi.fn()
6
6
  const stopScroll = vi.fn()
7
7
 
8
- /**
9
- * `detachCount` records `ref(null)` invocations. React calls a ref callback with
10
- * null and then with the element whenever the callback IDENTITY changes, and the
11
- * real `use-stick-to-bottom` responds to the null by disconnecting its
12
- * ResizeObserver and dropping its scroll/wheel listeners — so counting detaches
13
- * is how a test can see the adapter-identity churn that a plain recording ref
14
- * would silently absorb.
15
- */
16
8
  function makeRef() {
17
9
  const fn = ((el: HTMLElement | null) => {
18
- if (el === null) (fn as { detachCount: number }).detachCount += 1
19
10
  ;(fn as { current: HTMLElement | null }).current = el
20
- }) as ((el: HTMLElement | null) => void) & {
21
- current: HTMLElement | null
22
- detachCount: number
23
- }
11
+ }) as ((el: HTMLElement | null) => void) & { current: HTMLElement | null }
24
12
  fn.current = null
25
- fn.detachCount = 0
26
13
  return fn
27
14
  }
28
15
  const scrollRefMock = makeRef()
@@ -39,16 +26,9 @@ vi.mock('use-stick-to-bottom', () => ({
39
26
 
40
27
  // Keep the row renderer trivial — these tests exercise ChatMessageList's
41
28
  // scroll orchestration, not markdown rendering.
42
- // Forwards the ref because the list registers each row element for the
43
- // per-message `scrollAnchor: 'top'` path.
44
- vi.mock('../chat-message-enhanced', async () => {
45
- const { forwardRef } = await import('react')
46
- return {
47
- ChatMessageEnhanced: forwardRef<HTMLDivElement, { content: string }>(({ content }, ref) => (
48
- <div ref={ref}>{content}</div>
49
- )),
50
- }
51
- })
29
+ vi.mock('../chat-message-enhanced', () => ({
30
+ ChatMessageEnhanced: ({ content }: { content: string }) => <div>{content}</div>,
31
+ }))
52
32
 
53
33
  import { ChatMessageList } from '../chat-message-list'
54
34
 
@@ -60,36 +40,7 @@ class ObserverStub {
60
40
  disconnect() {}
61
41
  }
62
42
  vi.stubGlobal('IntersectionObserver', ObserverStub)
63
-
64
- // Recording RO so the bottom-follow tests can drive a resize by hand.
65
- const resizeCallbacks: Array<() => void> = []
66
- class RecordingResizeObserver {
67
- constructor(cb: () => void) {
68
- resizeCallbacks.push(cb)
69
- }
70
- observe() {}
71
- unobserve() {}
72
- disconnect() {}
73
- }
74
- vi.stubGlobal('ResizeObserver', RecordingResizeObserver)
75
- /** Fire every live ResizeObserver callback — same effect as any content
76
- * or scroller-box size change in the real DOM. */
77
- const fireResize = () => {
78
- for (const cb of resizeCallbacks) cb()
79
- }
80
-
81
- /** jsdom has no layout, so every box metric reads 0. The jump-to-bottom
82
- * button is driven off real geometry, so the tests supply it. */
83
- const setGeometry = (geo: { scrollHeight: number; clientHeight: number; scrollTop: number }) => {
84
- const el = scrollRefMock.current
85
- if (!el) throw new Error('scroller not mounted')
86
- for (const [key, value] of Object.entries(geo)) {
87
- Object.defineProperty(el, key, { value, configurable: true, writable: true })
88
- }
89
- }
90
-
91
- // jsdom has no layout, so `scrollIntoView` is undefined.
92
- Element.prototype.scrollIntoView = vi.fn()
43
+ vi.stubGlobal('ResizeObserver', ObserverStub)
93
44
 
94
45
  const msg = (id: string, role: Message['role']): Message => ({
95
46
  id,
@@ -139,329 +90,3 @@ describe('ChatMessageList force-scroll decisions', () => {
139
90
  expect(scrollToBottom).not.toHaveBeenCalled()
140
91
  })
141
92
  })
142
-
143
- // The library's own `isAtBottom` lock is lost silently when the scroller's
144
- // box changes (source chips mounting below it) or when a card settling out
145
- // of its skeleton produces a resize-driven scroll the library reads as a
146
- // user gesture. The list therefore owns the INTENT and re-asserts on every
147
- // geometry change until a real gesture releases it.
148
- describe('ChatMessageList bottom-follow intent', () => {
149
- beforeEach(() => {
150
- scrollToBottom.mockClear()
151
- resizeCallbacks.length = 0
152
- })
153
-
154
- const sendTurn = () => {
155
- const initial = [msg('m1', 'user'), msg('m2', 'assistant')]
156
- const view = render(<ChatMessageList dialogId="d1" messages={initial} />)
157
- // A user send arms the follow intent for the whole turn.
158
- view.rerender(<ChatMessageList dialogId="d1" messages={[...initial, msg('m3', 'user')]} />)
159
- scrollToBottom.mockClear()
160
- return view
161
- }
162
-
163
- it('re-asserts the bottom on geometry changes after the send', () => {
164
- sendTurn()
165
- fireResize()
166
- expect(scrollToBottom).toHaveBeenCalled()
167
- })
168
-
169
- it('keeps re-asserting for late async growth (cards + covers resolving)', () => {
170
- sendTurn()
171
- fireResize()
172
- fireResize()
173
- fireResize()
174
- expect(scrollToBottom).toHaveBeenCalledTimes(3)
175
- })
176
-
177
- it('releases the intent once the user scrolls up', () => {
178
- sendTurn()
179
- scrollRefMock.current?.dispatchEvent(
180
- new WheelEvent('wheel', { deltaY: -120, bubbles: true }),
181
- )
182
- fireResize()
183
- expect(scrollToBottom).not.toHaveBeenCalled()
184
- })
185
-
186
- it('ignores downward wheel (never an escape from the bottom)', () => {
187
- sendTurn()
188
- scrollRefMock.current?.dispatchEvent(
189
- new WheelEvent('wheel', { deltaY: 120, bubbles: true }),
190
- )
191
- fireResize()
192
- expect(scrollToBottom).toHaveBeenCalled()
193
- })
194
-
195
- it('does not follow when autoScroll is off (passive demo hosts)', () => {
196
- const initial = [msg('m1', 'user'), msg('m2', 'assistant')]
197
- const { rerender } = render(
198
- <ChatMessageList dialogId="d1" messages={initial} autoScroll={false} />,
199
- )
200
- rerender(
201
- <ChatMessageList dialogId="d1" messages={[...initial, msg('m3', 'user')]} autoScroll={false} />,
202
- )
203
- scrollToBottom.mockClear()
204
- fireResize()
205
- expect(scrollToBottom).not.toHaveBeenCalled()
206
- })
207
-
208
- it('hides jump-to-bottom while the thread is at the bottom', () => {
209
- const view = sendTurn()
210
- setGeometry({ scrollHeight: 1000, clientHeight: 500, scrollTop: 500 })
211
- act(() => fireResize())
212
- expect(view.queryByLabelText('Scroll to latest message')).toBeNull()
213
- })
214
-
215
- it('shows jump-to-bottom once the thread is away from the bottom', () => {
216
- const view = sendTurn()
217
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 500 })
218
- act(() => fireResize())
219
- expect(view.queryByLabelText('Scroll to latest message')).not.toBeNull()
220
- })
221
-
222
- it('re-arms the intent when the user clicks jump-to-bottom', () => {
223
- const view = sendTurn()
224
- // User scrolls away: intent released, button appears.
225
- scrollRefMock.current?.dispatchEvent(
226
- new WheelEvent('wheel', { deltaY: -120, bubbles: true }),
227
- )
228
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 500 })
229
- act(() => fireResize())
230
- expect(scrollToBottom).not.toHaveBeenCalled()
231
-
232
- const button = view.getByLabelText('Scroll to latest message')
233
- act(() => {
234
- button.click()
235
- })
236
- expect(scrollToBottom).toHaveBeenCalled()
237
-
238
- // …and the click re-armed the lock, so growth follows again.
239
- scrollToBottom.mockClear()
240
- act(() => fireResize())
241
- expect(scrollToBottom).toHaveBeenCalled()
242
- })
243
-
244
- // A drag that ends OUTSIDE the window (button released off-screen, or the
245
- // tab losing focus mid-drag) never delivers `pointerup`, so a naive
246
- // `pointerDown` flag stays true forever. Afterwards the very thing this
247
- // component exists for — a fetch-mode card settling from skeleton to a
248
- // SHORTER height, which clamps scrollTop down — is misread as a scroll-up
249
- // gesture and silently drops the lock.
250
- const dragThenScrollDown = (endEvent: { target: 'window'; type: string }) => {
251
- sendTurn()
252
- // Park away from the bottom so the at-bottom re-arm below can't mask the
253
- // result, and seed `lastScrollTop`.
254
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 4000 })
255
- window.dispatchEvent(new Event('pointerdown'))
256
- act(() => {
257
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
258
- })
259
- // Drag ends where we can't see it.
260
- if (endEvent.target === 'window') window.dispatchEvent(new Event(endEvent.type))
261
- // Resize clamp: content shrank, scrollTop drops. NOT a gesture.
262
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 3800 })
263
- act(() => {
264
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
265
- })
266
- scrollToBottom.mockClear()
267
- act(() => fireResize())
268
- }
269
-
270
- it('keeps the intent when a pointercancel ended the drag (resize clamp is not a gesture)', () => {
271
- dragThenScrollDown({ target: 'window', type: 'pointercancel' })
272
- expect(scrollToBottom).toHaveBeenCalled()
273
- })
274
-
275
- it('keeps the intent when the window lost focus mid-drag', () => {
276
- dragThenScrollDown({ target: 'window', type: 'blur' })
277
- expect(scrollToBottom).toHaveBeenCalled()
278
- })
279
-
280
- // Focus events do not bubble but they DO capture, so a capture-phase `blur`
281
- // listener on `window` fires for EVERY element blur in the document — the
282
- // classic focus-delegation idiom. Registering the drag-end `blur` that way
283
- // made any focus change during a pointer hold (most commonly the chat input
284
- // blurring as the user presses down on the scrollbar) clear `pointerDown`,
285
- // which is exactly what re-opens the yank-back-mid-drag bug. Only a TRUE
286
- // window blur (which targets `window` itself) may end the drag.
287
- it('a descendant element losing focus does not end the drag', () => {
288
- sendTurn()
289
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 4000 })
290
- window.dispatchEvent(new Event('pointerdown'))
291
- act(() => {
292
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
293
- })
294
- // The chat input loses focus as the pointer goes down elsewhere.
295
- const input = document.createElement('input')
296
- document.body.appendChild(input)
297
- input.focus()
298
- input.blur()
299
- input.remove()
300
- // The drag is still live, so this held scroll-up IS a gesture.
301
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 3800 })
302
- act(() => {
303
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
304
- })
305
- scrollToBottom.mockClear()
306
- act(() => fireResize())
307
- expect(scrollToBottom).not.toHaveBeenCalled()
308
- window.dispatchEvent(new Event('pointerup'))
309
- })
310
-
311
- it('still disarms on a genuine scrollbar drag upward', () => {
312
- sendTurn()
313
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 4000 })
314
- window.dispatchEvent(new Event('pointerdown'))
315
- act(() => {
316
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
317
- })
318
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 3800 })
319
- act(() => {
320
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
321
- })
322
- scrollToBottom.mockClear()
323
- act(() => fireResize())
324
- expect(scrollToBottom).not.toHaveBeenCalled()
325
- window.dispatchEvent(new Event('pointerup'))
326
- })
327
-
328
- // Scrolling back down to the live end by hand expresses exactly the intent
329
- // the jump-to-bottom button does — the lock has to re-arm, or the thread
330
- // renders at-bottom (button hidden) and then silently drifts away.
331
- it('re-arms the intent when the user scrolls back to the bottom by hand', () => {
332
- sendTurn()
333
- scrollRefMock.current?.dispatchEvent(new WheelEvent('wheel', { deltaY: -120, bubbles: true }))
334
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 500 })
335
- act(() => fireResize())
336
- expect(scrollToBottom).not.toHaveBeenCalled()
337
-
338
- // …and back down to the end.
339
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 19500 })
340
- act(() => {
341
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
342
- })
343
- scrollToBottom.mockClear()
344
- act(() => fireResize())
345
- expect(scrollToBottom).toHaveBeenCalled()
346
- })
347
-
348
- it('does not re-arm while the user is still parked above the bottom', () => {
349
- sendTurn()
350
- scrollRefMock.current?.dispatchEvent(new WheelEvent('wheel', { deltaY: -120, bubbles: true }))
351
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 8000 })
352
- act(() => {
353
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
354
- })
355
- scrollToBottom.mockClear()
356
- act(() => fireResize())
357
- expect(scrollToBottom).not.toHaveBeenCalled()
358
- })
359
-
360
- // A short scrollbar drag that starts at the live end and stays inside the
361
- // 70px band disarms on the gesture and would immediately re-arm on the very
362
- // same scroll event if the re-arm didn't wait for the drag to END — yanking
363
- // the reader back down mid-drag, a miniature of the bug this block exists
364
- // to prevent.
365
- it('does not re-arm mid-drag when the drag stays inside the bottom band', () => {
366
- sendTurn()
367
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 19500 })
368
- act(() => {
369
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
370
- })
371
- window.dispatchEvent(new Event('pointerdown'))
372
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 19460 })
373
- act(() => {
374
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
375
- })
376
- scrollToBottom.mockClear()
377
- act(() => fireResize())
378
- expect(scrollToBottom).not.toHaveBeenCalled()
379
- window.dispatchEvent(new Event('pointerup'))
380
- })
381
-
382
- it('releases the intent for a top-anchored turn (it parks at the message top)', () => {
383
- const initial = [msg('m1', 'user'), msg('m2', 'assistant')]
384
- const { rerender } = render(<ChatMessageList dialogId="d1" messages={initial} />)
385
- const sent = [...initial, msg('m3', 'user')]
386
- rerender(<ChatMessageList dialogId="d1" messages={sent} />)
387
- const anchored: Message = { ...msg('m4', 'assistant'), scrollAnchor: 'top' }
388
- rerender(<ChatMessageList dialogId="d1" messages={[...sent, anchored]} />)
389
- scrollToBottom.mockClear()
390
- fireResize()
391
- expect(scrollToBottom).not.toHaveBeenCalled()
392
- })
393
- })
394
-
395
- /**
396
- * The whole bottom-follow subsystem — ResizeObserver, wheel/touch/keyboard
397
- * escape hatches, the jump-to-bottom button — is installed by ONE effect whose
398
- * deps were all identity-stable (`autoScroll` plus three `[]`-memoized values
399
- * from `useStickToBottom`). `isLoading` was missing, and on the loading branch
400
- * the component returns the skeleton, so `scrollRef.current` is null and the
401
- * effect bails.
402
- *
403
- * Net result for any consumer following this repo's own prescribed
404
- * "skeleton during fetch" pattern: the effect ran once against null refs and
405
- * NEVER re-ran, so nothing was ever observed, `atBottom` stayed at its initial
406
- * `true`, the WCAG escape hatch could not render, and the follow lock was never
407
- * re-asserted — while the observers/listeners bound on a later round trip kept
408
- * detached nodes alive.
409
- */
410
- describe('ChatMessageList bottom-follow survives the loading skeleton', () => {
411
- beforeEach(() => {
412
- scrollToBottom.mockClear()
413
- resizeCallbacks.length = 0
414
- })
415
-
416
- it('installs the follow observers when the skeleton is replaced by the list', () => {
417
- const messages = [msg('m1', 'user'), msg('m2', 'assistant')]
418
- const { rerender } = render(
419
- <ChatMessageList dialogId="d1" messages={messages} isLoading />,
420
- )
421
- // Nothing to observe while the skeleton is up.
422
- expect(resizeCallbacks).toHaveLength(0)
423
-
424
- rerender(<ChatMessageList dialogId="d1" messages={messages} />)
425
- expect(resizeCallbacks.length).toBeGreaterThan(0)
426
- })
427
-
428
- it('shows the jump-to-bottom escape hatch after a loading round trip', () => {
429
- const messages = [msg('m1', 'user'), msg('m2', 'assistant')]
430
- const view = render(<ChatMessageList dialogId="d1" messages={messages} />)
431
- // A host loading another page / another dialog swaps the scroller out…
432
- view.rerender(<ChatMessageList dialogId="d1" messages={messages} isLoading />)
433
- view.rerender(<ChatMessageList dialogId="d1" messages={messages} />)
434
-
435
- // …and the list must still notice it is scrolled away from the bottom.
436
- setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 0 })
437
- act(() => {
438
- scrollRefMock.current?.dispatchEvent(new Event('scroll'))
439
- })
440
- expect(view.queryByLabelText('Scroll to latest message')).not.toBeNull()
441
- })
442
-
443
- it('keeps the ref adapters identity-stable across re-renders', () => {
444
- // The adapters are handed to `useStickToBottom`'s ref callbacks, and the
445
- // library disconnects its ResizeObserver on `ref(null)` and builds a NEW
446
- // one on re-attach. A fresh arrow per render therefore churned an observer
447
- // (and two scroll listeners) on every committed frame — ~60/s while
448
- // streaming — and reset the library's `previousHeight`, which is what it
449
- // uses to choose between its `initial` and `resize` animations.
450
- const messages = [msg('m1', 'user'), msg('m2', 'assistant')]
451
- const view = render(<ChatMessageList dialogId="d1" messages={messages} />)
452
- const detachesAfterMount = {
453
- scroll: scrollRefMock.detachCount,
454
- content: contentRefMock.detachCount,
455
- }
456
-
457
- for (let i = 0; i < 5; i++) {
458
- view.rerender(<ChatMessageList dialogId="d1" messages={[...messages]} />)
459
- }
460
-
461
- // A memoized adapter is never re-invoked with null, so the library keeps its
462
- // observer and listeners for the whole mount.
463
- expect(scrollRefMock.detachCount).toBe(detachesAfterMount.scroll)
464
- expect(contentRefMock.detachCount).toBe(detachesAfterMount.content)
465
- expect(scrollRefMock.current).not.toBeNull()
466
- })
467
- })
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import React, { forwardRef, memo, useEffect, useMemo, useRef } from "react"
3
+ import React, { forwardRef, memo, useMemo, useRef } from "react"
4
4
  import { cn } from "../../utils/cn"
5
5
  import { isToday } from "../../utils/date-utils"
6
6
  import { formatDate, formatTime } from "../../utils/format-date"
@@ -12,7 +12,7 @@ import { ErrorMessageDisplay } from "./error-message-display"
12
12
  import { ContextCompactionDisplay } from "./context-compaction-display"
13
13
  import { ThinkingDisplay } from "./thinking-display"
14
14
  import { GuideDisplay } from "./guide-display"
15
- import { SimpleMarkdownRenderer } from "../ui/markdown/simple-markdown-renderer"
15
+ import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
16
16
  import type { ChatRef } from "./chat-ref.types"
17
17
  import { remarkCardLinks } from "./remark-card-links"
18
18
  import { remarkMentionChips } from "./remark-mention-chips"
@@ -164,9 +164,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
164
164
  | { kind: 'text'; text: string }
165
165
  | { kind: 'block'; key: string; node: React.ReactNode }
166
166
  const partsBySegment = new Map<number, SegmentPart[]>()
167
- const usedKeys = new Set<string>()
168
- if (!render) return { inlineByKey, partsBySegment, usedKeys }
167
+ if (!render) return { inlineByKey, partsBySegment }
169
168
  const cache = renderedCardNodeCache.current
169
+ const usedKeys = new Set<string>()
170
170
  // Card keys already emitted as a hoisted block (`b-<key>`). The same
171
171
  // marker can legitimately appear twice in one message (LLM references
172
172
  // the same entity twice); we hoist the FIRST occurrence and skip the
@@ -281,99 +281,20 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
281
281
  partsBySegment.set(segIdx, parts)
282
282
  }
283
283
  })
284
- return { inlineByKey, partsBySegment, usedKeys }
285
- }, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
286
-
287
- // Drop cached nodes for markers no longer present so the cache can't grow
288
- // unbounded as a long message's markers change. Deliberately an EFFECT,
289
- // not part of the memo above: pruning is a mutation, and doing it in the
290
- // render phase is non-idempotent (StrictMode's double-invoke, or a render
291
- // React throws away, would evict nodes the committed tree still uses and
292
- // remount the cards it was built to preserve).
293
- useEffect(() => {
294
- const cache = renderedCardNodeCache.current
295
- const usedKeys = renderingPlan?.usedKeys
296
- if (!usedKeys) return
297
- if (cache.size <= usedKeys.size) return
298
- for (const k of [...cache.keys()]) {
299
- if (!usedKeys.has(k)) cache.delete(k)
284
+ // Drop cached nodes for markers no longer present so the cache can't
285
+ // grow unbounded as a long message's markers change.
286
+ if (cache.size > usedKeys.size) {
287
+ for (const k of cache.keys()) {
288
+ if (!usedKeys.has(k)) cache.delete(k)
289
+ }
300
290
  }
301
- }, [renderingPlan])
302
-
303
- // The plan is read through a REF inside the `<a>` override, not captured
304
- // in the override's closure. `renderingPlan` is rebuilt on every text
305
- // delta (its `segments` dep is a fresh array per delta), so depending on
306
- // it here would give `cardComponentOverrides` a new identity every token
307
- // — which re-creates the markdown engine's `components` memo and defeats
308
- // its per-block `StreamingBlockRenderer` memoization for the WHOLE
309
- // message, on every token. The ref keeps the override identity stable
310
- // while still reading the current plan at call time.
311
- const renderingPlanRef = useRef(renderingPlan)
312
- renderingPlanRef.current = renderingPlan
313
- const chatRefsRef = useRef(chatRefs)
314
- chatRefsRef.current = chatRefs
315
-
316
- // Ref-SET token. The override reads `chatRefsRef` at CALL time, but the
317
- // markdown engine memoizes completed blocks on the override map's
318
- // identity — so on a COMPLETED (no longer streaming) message, a ref that
319
- // resolves late would never be re-read: nothing else about the renderer's
320
- // props changes, the engine's memo bails, and the pill stays stuck on its
321
- // fallback (`refMatch.title`, or the raw cardId) forever. Streaming is
322
- // covered because `card://` blocks are excluded from the block cache; the
323
- // completed path has no other escape.
324
- //
325
- // The token fingerprints VALUES, not just the key set: a later refs frame
326
- // REPLACES the whole map for a send index, and the enrichment case that
327
- // matters most — a `title` going from the raw cardId to the resolved
328
- // document title — keeps the key set IDENTICAL. A key-only token would be
329
- // unchanged, the override map would keep its identity, the engine's
330
- // completed-block memo would bail, and the pill would stay on its fallback
331
- // forever. Every `ChatRef` field is included because the host's
332
- // `renderEntityCard` receives the whole ref and may render any of them.
333
- //
334
- // Still STABLE per text delta (the refs map doesn't change while tokens
335
- // stream), and the cost is one pass over a handful of refs per CHANGED
336
- // `chatRefs` identity — the dep list is unchanged.
337
- //
338
- // Fields are joined by `JSON.stringify`, NOT by a separator character.
339
- // `title`, `preview` and `url` are free-form HOST strings, so any
340
- // delimiter we pick is one the data may also contain: joined with a plain
341
- // space, `title: 'a b'` + empty `url` fingerprints identically to
342
- // `title: 'a'` + `url: 'b'`, silently reintroducing the stale-pill memo
343
- // bail this token exists to fix. (An earlier revision used a literal NUL
344
- // byte as the separator: collision-free in practice, but it made the
345
- // source file itself binary to grep and diff tooling.) `JSON.stringify`
346
- // escapes every field, so no value can forge a boundary.
347
- //
348
- // Caveat worth stating: `JSON.stringify` on `metadata` is key-ORDER
349
- // dependent: two structurally equal objects built in different insertion
350
- // orders fingerprint differently. Acceptable here because every refs map
351
- // arrives from ONE decoder with a fixed field order, and the failure mode
352
- // is a redundant re-render, never a stale pill.
353
- const refsKey = useMemo(
354
- () =>
355
- Object.entries(chatRefs ?? {})
356
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
357
- .map(([k, v]) =>
358
- JSON.stringify([
359
- k,
360
- v.type,
361
- v.sourceRepo ?? null,
362
- v.id,
363
- v.title,
364
- v.url ?? null,
365
- v.targetPlatform ?? null,
366
- v.date ?? null,
367
- v.preview ?? null,
368
- v.metadata ?? null,
369
- ]),
370
- )
371
- .join('|'),
372
- [chatRefs],
373
- )
291
+ return { inlineByKey, partsBySegment }
292
+ }, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
374
293
 
375
294
  const cardComponentOverrides = useMemo(() => {
376
295
  if (!hasMarkerSupport && !hasMentionSupport) return undefined
296
+ const refs = chatRefs ?? {}
297
+ const inlineByKey = renderingPlan?.inlineByKey
377
298
  return {
378
299
  // Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`.
379
300
  // The render result was pre-computed in `renderingPlan` so block-level
@@ -401,7 +322,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
401
322
  const cardType = stripped.slice(0, sepIdx)
402
323
  const cardId = stripped.slice(sepIdx + 1)
403
324
  const key = `${cardType}:${cardId}`
404
- const inline = renderingPlanRef.current?.inlineByKey.get(key)
325
+ const inline = inlineByKey?.get(key)
405
326
  if (inline != null) return inline
406
327
  // Three fallback cases — keep them DISTINCT, never blur them
407
328
  // together. Mixing them up (which the old code did by reaching
@@ -425,7 +346,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
425
346
  // title from an unrelated ref — that hides the bug and
426
347
  // deceives the reader into thinking they're looking at
427
348
  // a real card.
428
- const refMatch: ChatRef | undefined = chatRefsRef.current?.[key]
349
+ const refMatch: ChatRef | undefined = refs[key]
429
350
  if (refMatch) {
430
351
  return <span className="text-ods-text-primary">{refMatch.title}</span>
431
352
  }
@@ -467,13 +388,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
467
388
  )
468
389
  },
469
390
  }
470
- // DEPS ARE DELIBERATELY MINIMAL every one of them is stable across a
471
- // streaming turn (booleans + host-stable fn/component identities, both
472
- // enforced by this file's `memo` comparator). The rendering plan and the
473
- // refs MAP are read through refs above precisely so they do NOT appear
474
- // here; `refsKey` is the one ref-derived dep, and it only moves when the
475
- // ref set OR any ref VALUE does (see its definition).
476
- }, [hasMarkerSupport, hasMentionSupport, renderMention, NavLinkAnchor, refsKey])
391
+ }, [hasMarkerSupport, hasMentionSupport, renderMention, chatRefs, renderingPlan, NavLinkAnchor])
477
392
 
478
393
  /**
479
394
  * Body of a markdown-bearing segment (`text` / `guide`).
@@ -488,13 +403,8 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
488
403
  * end). Stable React keys come from the card key (block) and chunk position
489
404
  * (text); streaming token-by-token reuses the same React instances so
490
405
  * `<Video>` doesn't remount mid-play.
491
- *
492
- * `isStreaming` opts the segment into the markdown engine's streaming path
493
- * (atomic-block memoization + fence tail-completion + aria-live). Only the
494
- * actively streaming segment gets it, and within a marker-split segment
495
- * only the LAST text chunk — the earlier chunks are already final.
496
406
  */
497
- const renderSegmentBody = (segIndex: number, text: string, isStreaming: boolean) => {
407
+ const renderSegmentBody = (segIndex: number, text: string) => {
498
408
  const parts = renderingPlan?.partsBySegment.get(segIndex)
499
409
  if (!parts || parts.length === 0) {
500
410
  return (
@@ -503,7 +413,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
503
413
  textSize="compact"
504
414
  additionalRemarkPlugins={cardRemarkPlugins}
505
415
  componentOverrides={cardComponentOverrides}
506
- streaming={isStreaming}
507
416
  />
508
417
  )
509
418
  }
@@ -516,7 +425,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
516
425
  textSize="compact"
517
426
  additionalRemarkPlugins={cardRemarkPlugins}
518
427
  componentOverrides={cardComponentOverrides}
519
- streaming={isStreaming && pIdx === parts.length - 1}
520
428
  />
521
429
  )
522
430
  }
@@ -631,12 +539,6 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
631
539
  {/* Message segments — hidden for system messages without content */}
632
540
  {(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
633
541
  {segments.map((segment, index) => {
634
- // The engine's streaming path (atomic-block memoization +
635
- // fence tail-completion + aria-live) applies ONLY to the
636
- // actively streaming segment: last segment of a message that
637
- // is still typing. On completion `isTyping` flips false and
638
- // the engine does one authoritative whole-document parse.
639
- const segmentIsStreaming = index === segments.length - 1 && !!isTyping
640
542
  if (segment.type === 'text') {
641
543
  return (
642
544
  <div
@@ -646,15 +548,11 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
646
548
  isError ? "text-ods-error" : "text-ods-text-primary",
647
549
  )}
648
550
  >
649
- {renderSegmentBody(index, segment.text, segmentIsStreaming)}
551
+ {renderSegmentBody(index, segment.text)}
650
552
  </div>
651
553
  )
652
554
  } else if (segment.type === 'guide') {
653
- return (
654
- <GuideDisplay key={index}>
655
- {renderSegmentBody(index, segment.text, segmentIsStreaming)}
656
- </GuideDisplay>
657
- )
555
+ return <GuideDisplay key={index}>{renderSegmentBody(index, segment.text)}</GuideDisplay>
658
556
  } else if (segment.type === 'tool_execution') {
659
557
  return (
660
558
  <ToolExecutionDisplay