@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,436 +0,0 @@
1
- /**
2
- * Streaming-path tests for the unified markdown engine (plan §D1):
3
- * atomic-block splitting, fence tail-completion, card-block cache
4
- * exclusion, and — critically — that the COMPLETION render (streaming
5
- * flipped off) equals the whole-document parse for every cross-block
6
- * construct (late reference defs, loose-list numbering, footnote-ish
7
- * splits, blockquote-internal blank lines, streamed tables).
8
- */
9
- import React from 'react'
10
- import { describe, it, expect, vi } from 'vitest'
11
- import type { Components } from 'react-markdown'
12
- import { render, act } from '@testing-library/react'
13
- import { splitStreamingBlocks, completeStreamingTail } from '../streaming'
14
- import { MarkdownEngine } from '../engine'
15
-
16
- vi.mock('mermaid', () => ({
17
- default: {
18
- initialize: vi.fn(),
19
- render: vi.fn(async () => ({ svg: '<svg data-mock-mermaid="true"></svg>' })),
20
- },
21
- }))
22
-
23
- async function renderHtml(ui: React.ReactElement): Promise<string> {
24
- let container: HTMLElement
25
- await act(async () => {
26
- const result = render(ui)
27
- container = result.container
28
- await new Promise((r) => setTimeout(r, 0))
29
- })
30
- return container!.innerHTML
31
- }
32
-
33
- describe('splitStreamingBlocks', () => {
34
- it('splits plain paragraphs at blank lines', () => {
35
- const blocks = splitStreamingBlocks('para one\n\npara two\n\npara three')
36
- expect(blocks.map((b) => b.memoizable)).toEqual([true, true, false])
37
- expect(blocks[0].text).toContain('para one')
38
- expect(blocks[2].text).toContain('para three')
39
- })
40
-
41
- it('never cuts inside a fenced code block', () => {
42
- const blocks = splitStreamingBlocks('```js\ncode\n\nmore code\n```\n\nafter')
43
- expect(blocks).toHaveLength(2)
44
- expect(blocks[0].text).toContain('more code')
45
- })
46
-
47
- it('never cuts a loose ordered list apart', () => {
48
- const md = '1. first\n\n2. second\n\n3. third'
49
- const blocks = splitStreamingBlocks(md)
50
- expect(blocks).toHaveLength(1)
51
- })
52
-
53
- it('never cuts a multi-paragraph blockquote apart', () => {
54
- const md = '> first para\n\n> second para'
55
- expect(splitStreamingBlocks(md)).toHaveLength(1)
56
- })
57
-
58
- it('does not cut before an indented continuation line', () => {
59
- const md = '- item one\n\n continuation of item one\n\nnext para'
60
- const blocks = splitStreamingBlocks(md)
61
- expect(blocks[0].text).toContain('continuation')
62
- })
63
-
64
- it('marks card:// and mention:// blocks non-memoizable (stale-card fix)', () => {
65
- const blocks = splitStreamingBlocks(
66
- 'plain text block\n\nSee [card://blog:x] here\n\nCheck @device:r1 via mention\n\ntail',
67
- )
68
- expect(blocks.map((b) => b.memoizable)).toEqual([true, false, true, false])
69
- // mention text without mention:// scheme stays memoizable; the scheme
70
- // appears post-remark, so the splitter keys on the raw marker too:
71
- const withScheme = splitStreamingBlocks('a [x](mention://device:r1) link\n\ntail')
72
- expect(withScheme[0].memoizable).toBe(false)
73
- })
74
-
75
- it('marks reference-definition/use and footnote blocks non-memoizable', () => {
76
- const blocks = splitStreamingBlocks(
77
- 'See [text][ref] please\n\n[ref]: https://example.com\n\nA footnote[^1] here\n\ntail',
78
- )
79
- expect(blocks.map((b) => b.memoizable)).toEqual([false, false, false, false])
80
- })
81
-
82
- it('keys blocks by position so identical blocks never alias', () => {
83
- const blocks = splitStreamingBlocks('---\n\n---\n\nend')
84
- expect(blocks.map((b) => b.index)).toEqual([0, 1, 2])
85
- })
86
-
87
- // --- fence-marker fidelity (reviewer-reported failures) ---
88
- it('does not let ~~~ close a ``` fence', () => {
89
- const md = '```text\nline\n~~~\n\nstill in code\n```\n\nafter'
90
- const blocks = splitStreamingBlocks(md)
91
- // The whole code block stays ONE unit; only the trailing prose splits.
92
- expect(blocks).toHaveLength(2)
93
- expect(blocks[0].text).toContain('still in code')
94
- expect(blocks[1].text.trim()).toBe('after')
95
- })
96
-
97
- it('does not let ``` close a ~~~ fence', () => {
98
- const blocks = splitStreamingBlocks('~~~text\nline\n```\n\nstill in code\n~~~\n\nafter')
99
- expect(blocks).toHaveLength(2)
100
- expect(blocks[0].text).toContain('still in code')
101
- })
102
-
103
- it('ignores fences inside 4-space-indented code (CommonMark caps indent at 3)', () => {
104
- const blocks = splitStreamingBlocks(' ```\n not a fence\n\nreal para\n\ntail')
105
- expect(blocks.length).toBeGreaterThan(1)
106
- expect(blocks.some((b) => b.text.includes('real para'))).toBe(true)
107
- })
108
-
109
- it('requires the closer to be at least as long as the opener', () => {
110
- // The inner ``` is CONTENT of the ````-opened block.
111
- const blocks = splitStreamingBlocks('````\n```\n\ninner\n````\n\nafter')
112
- expect(blocks).toHaveLength(2)
113
- expect(blocks[0].text).toContain('inner')
114
- })
115
-
116
- // --- raw HTML block integrity ---
117
- it('never cuts inside an unbalanced raw HTML block', () => {
118
- const blocks = splitStreamingBlocks(
119
- '<details>\n<summary>More</summary>\n\nbody text\n\n</details>\n\ntail',
120
- )
121
- expect(blocks).toHaveLength(2)
122
- expect(blocks[0].text).toContain('body text')
123
- expect(blocks[0].text).toContain('</details>')
124
- // Raw HTML units are never cached.
125
- expect(blocks.every((b) => !b.memoizable)).toBe(true)
126
- })
127
-
128
- it('never cuts inside a MULTI-LINE start tag', () => {
129
- // `HTML_TAG_RE` needs `>` on the same line, so `<div\n class="x">` never
130
- // opened the container: the splitter cut at every blank line and marked
131
- // the fragments memoizable — the container's body escaped the div and
132
- // the broken render got cached.
133
- const blocks = splitStreamingBlocks('<div\n class="x">\n\nbody\n\n</div>\n\nafter')
134
- expect(blocks).toHaveLength(2)
135
- expect(blocks[0].text).toContain('body')
136
- expect(blocks[0].text).toContain('</div>')
137
- expect(blocks.every((b) => !b.memoizable)).toBe(true)
138
- })
139
-
140
- it('releases the latch when a container closes MID-LINE', () => {
141
- // A close tag not at column 0..3 (`some text </details>`) never
142
- // decremented, so `htmlDepth` latched >0 and the whole rest of the
143
- // message collapsed into ONE non-memoizable unit for the rest of the
144
- // stream.
145
- const blocks = splitStreamingBlocks(
146
- '<details>\nstuff\n\nsome text </details>\n\nafter\n\nmore\n\ntail',
147
- )
148
- expect(blocks).toHaveLength(4)
149
- expect(blocks[0].text).toContain('</details>')
150
- expect(blocks[0].memoizable).toBe(false)
151
- // Cutting resumed — the plain paragraphs after the container cache again.
152
- expect(blocks.map((b) => b.memoizable)).toEqual([false, true, true, false])
153
- })
154
-
155
- it('sees raw HTML indented to a list item content column', () => {
156
- // The old column-0..3 gate missed this entirely, so the unit was marked
157
- // memoizable despite containing raw HTML.
158
- const blocks = splitStreamingBlocks(
159
- '1. step\n\n <details>\n <summary>x</summary>\n </details>\n\ntail',
160
- )
161
- expect(blocks[0].text).toContain('<details>')
162
- expect(blocks[0].memoizable).toBe(false)
163
- })
164
-
165
- it('caps the latch so an unclosed container cannot disable cutting forever', () => {
166
- const paras = Array.from({ length: 300 }, (_, i) => `para ${i}`).join('\n\n')
167
- const blocks = splitStreamingBlocks(`<div>\n\n${paras}`)
168
- // Past the cap the splitter resumes cutting…
169
- expect(blocks.length).toBeGreaterThan(1)
170
- // …but nothing inside the still-open container is ever cached.
171
- expect(blocks.every((b) => !b.memoizable)).toBe(true)
172
- })
173
-
174
- it('reports each unit’s document-wide startLine', () => {
175
- const blocks = splitStreamingBlocks('one\n\ntwo\n\nthree')
176
- expect(blocks.map((b) => b.startLine)).toEqual([1, 3, 5])
177
- })
178
- })
179
-
180
- describe('completeStreamingTail', () => {
181
- it('closes an unterminated fence', () => {
182
- expect(completeStreamingTail('text\n\n```js\nconst x =')).toMatch(/```$/)
183
- })
184
- it('leaves balanced fences alone', () => {
185
- const md = '```js\ncode\n```'
186
- expect(completeStreamingTail(md)).toBe(md)
187
- })
188
- it('does NOT touch stray emphasis/link openers (no false-positive close)', () => {
189
- for (const md of ['2 * 3 is six', 'a lone _ under', 'an open [ bracket', 'tick ` alone']) {
190
- expect(completeStreamingTail(md)).toBe(md)
191
- }
192
- })
193
-
194
- it('does not append a bogus fence when ``` and ~~~ both appear (closed block)', () => {
195
- const md = '```\n~~~\n```'
196
- expect(completeStreamingTail(md)).toBe(md)
197
- })
198
-
199
- it('does not append a bogus fence to a ```-block containing a ~~~ line', () => {
200
- const md = '```text\nline\n~~~\n\nstill in code\n```\n\nafter'
201
- expect(completeStreamingTail(md)).toBe(md)
202
- })
203
-
204
- it('closes an unterminated ~~~ fence with ~~~ (the RECORDED opener)', () => {
205
- expect(completeStreamingTail('~~~py\nx =')).toBe('~~~py\nx =\n~~~')
206
- })
207
-
208
- it('closes with a run at least as long as the opener', () => {
209
- expect(completeStreamingTail('````\nstuff')).toBe('````\nstuff\n````')
210
- })
211
- })
212
-
213
- describe('MarkdownEngine streaming vs completion equivalence', () => {
214
- const textOf = (html: string) =>
215
- html
216
- .replace(/<style[\s\S]*?<\/style>/, '')
217
- .replace(/<[^>]+>/g, '')
218
- .replace(/\s+/g, '')
219
-
220
- const CASES: Record<string, { md: string; transientTextDivergence?: boolean }> = {
221
- 'loose list keeps numbering': { md: '1. first\n\n2. second\n\n3. third' },
222
- // Reference links are the DOCUMENTED acceptable mid-stream transient:
223
- // the use renders literally until the def arrives; completion self-heals.
224
- 'late reference definition resolves': {
225
- md: 'See [the docs][ref] now.\n\n[ref]: https://example.com/late',
226
- transientTextDivergence: true,
227
- },
228
- 'blockquote with internal blank line': { md: '> first\n>\n> second' },
229
- 'gfm table': { md: '| A | B |\n|---|---|\n| 1 | 2 |' },
230
- 'multi-block document': { md: '# Title\n\npara one\n\n- a\n- b\n\npara two' },
231
- }
232
-
233
- for (const [name, { md, transientTextDivergence }] of Object.entries(CASES)) {
234
- it(`completion render equals whole-document parse: ${name}`, async () => {
235
- const streamingHtml = await renderHtml(<MarkdownEngine content={md} streaming />)
236
- const completedHtml = await renderHtml(<MarkdownEngine content={md} streaming={false} />)
237
- const wholeHtml = await renderHtml(<MarkdownEngine content={md} />)
238
- // Completion IS the authoritative whole-document parse — always.
239
- expect(completedHtml).toBe(wholeHtml)
240
- if (!transientTextDivergence) {
241
- expect(textOf(streamingHtml)).toBe(textOf(wholeHtml))
242
- }
243
- })
244
- }
245
-
246
- /**
247
- * The `textOf` comparison above cannot see this class of divergence: when a
248
- * list is cut apart, every remaining item becomes its own `<ol>`/`<ul>` with
249
- * no `start`, so the TEXT is identical while the visible ordinals restart at
250
- * "1." for every item. The assertion therefore has to be on list STRUCTURE.
251
- *
252
- * The shape is the single most common assistant answer — numbered steps whose
253
- * items carry a code fence, a second paragraph, a table, or a nested list — and
254
- * the broken units were `memoizable`, so the wrong render was CACHED for the
255
- * rest of the turn rather than self-healing on the next token.
256
- */
257
- const LIST_CONTINUATION_CASES: Record<string, string> = {
258
- 'ordered list with per-item code fences':
259
- '1. Install:\n\n ```bash\n npm i\n ```\n\n2. Run it.\n\n3. Done.\n',
260
- 'ordered list with 4-space-indented fences':
261
- '1. Install:\n\n ```bash\n npm i\n ```\n\n2. Run it.\n',
262
- 'list item with a second paragraph': '- item\n\n second para of item\n\n- item2\n',
263
- 'list item with a table continuation':
264
- '1. Options:\n\n | a | b |\n |---|---|\n | 1 | 2 |\n\n2. Next step.\n',
265
- 'list item with a nested sublist': '- item\n - nested\n\n- item2\n',
266
- 'a list that follows an unrelated paragraph':
267
- '- one\n\n- two\n\nplain para\n\n- three\n\n cont\n\n- four\n',
268
- }
269
-
270
- for (const [name, md] of Object.entries(LIST_CONTINUATION_CASES)) {
271
- it(`never cuts a list apart mid-stream: ${name}`, async () => {
272
- const streamingHtml = await renderHtml(<MarkdownEngine content={md} streaming />)
273
- const wholeHtml = await renderHtml(<MarkdownEngine content={md} />)
274
- const listCount = (html: string) => (html.match(/<[ou]l[\s>]/g) ?? []).length
275
- expect(listCount(streamingHtml)).toBe(listCount(wholeHtml))
276
- expect(textOf(streamingHtml)).toBe(textOf(wholeHtml))
277
- })
278
- }
279
-
280
- it('splits a list with a continuation block into ONE unit', () => {
281
- const md = '1. Install:\n\n ```bash\n npm i\n ```\n\n2. Run it.\n\n3. Done.\n'
282
- expect(splitStreamingBlocks(md)).toHaveLength(1)
283
- })
284
-
285
- it('still cuts between a list and a genuinely separate following block', () => {
286
- // The guard must not become "never cut once a list appeared" — a paragraph
287
- // after a list is a real boundary and its unit must stay memoizable.
288
- const blocks = splitStreamingBlocks('- one\n\n- two\n\nplain para\n\ntail para\n')
289
- expect(blocks.length).toBeGreaterThan(1)
290
- expect(blocks[0].text).toContain('- one')
291
- expect(blocks[0].memoizable).toBe(true)
292
- })
293
-
294
- it('streaming wrapper is aria-live polite', async () => {
295
- const html = await renderHtml(<MarkdownEngine content={'hello'} streaming />)
296
- expect(html).toContain('aria-live="polite"')
297
- const done = await renderHtml(<MarkdownEngine content={'hello'} />)
298
- expect(done).not.toContain('aria-live')
299
- })
300
-
301
- it('raw HTML block renders identically mid-stream and completed', async () => {
302
- const md = '<details>\n<summary>More</summary>\n\nbody text\n\n</details>\n\ntail'
303
- const streamingHtml = await renderHtml(<MarkdownEngine content={md} streaming />)
304
- const wholeHtml = await renderHtml(<MarkdownEngine content={md} />)
305
- // Strip only the streaming aria-live wrapper; the disclosure widget and
306
- // its body must be structurally identical (body INSIDE <details>, the
307
- // closing tag not dropped).
308
- const stripWrapper = (html: string) =>
309
- html
310
- .replace('<div aria-live="polite" aria-relevant="additions text">', '')
311
- .replace('</div></article>', '</article>')
312
- expect(streamingHtml).toContain('<details')
313
- expect(streamingHtml).toContain('body text')
314
- expect(stripWrapper(streamingHtml).replace(/\s+/g, '')).toBe(
315
- stripWrapper(wholeHtml).replace(/\s+/g, ''),
316
- )
317
- })
318
-
319
- it('unterminated fence at the tail renders as code, not swallowed prose', async () => {
320
- const html = await renderHtml(
321
- <MarkdownEngine content={'intro\n\n```js\nconst partial ='} streaming />,
322
- )
323
- expect(html).toContain('code-block-container')
324
- // hljs wraps tokens in spans — compare tag-stripped text.
325
- expect(html.replace(/<[^>]+>/g, '')).toContain('const partial =')
326
- })
327
- })
328
-
329
- // ---------------------------------------------------------------------------
330
- // Atomic-block memo (regression: the memo never hit)
331
- // ---------------------------------------------------------------------------
332
- describe('streaming atomic-block memoization', () => {
333
- /**
334
- * Module-scope so the override map's IDENTITY is stable across rerenders —
335
- * otherwise the test would be measuring its own instability instead of the
336
- * engine's. The counter proves memo HITS: without it, DOM reconciliation
337
- * reuses the same nodes either way, so node identity alone cannot fail.
338
- */
339
- let paragraphRenders = 0
340
- const COUNTING_OVERRIDES: Partial<Components> = {
341
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
342
- p: ({ children }: any) => {
343
- paragraphRenders++
344
- return <p>{children}</p>
345
- },
346
- }
347
-
348
- it('does NOT re-render completed blocks when the tail grows', async () => {
349
- const base = 'alpha para\n\nbeta para\n\ngamma tail'
350
- paragraphRenders = 0
351
-
352
- let container!: HTMLElement
353
- let rerender!: (ui: React.ReactElement) => void
354
- await act(async () => {
355
- const result = render(
356
- <MarkdownEngine content={base} streaming componentOverrides={COUNTING_OVERRIDES} />,
357
- )
358
- container = result.container
359
- rerender = result.rerender
360
- await new Promise((r) => setTimeout(r, 0))
361
- })
362
-
363
- const firstParagraph = container.querySelector('p')
364
- expect(firstParagraph?.textContent).toBe('alpha para')
365
- expect(paragraphRenders).toBe(3)
366
-
367
- const before = paragraphRenders
368
- await act(async () => {
369
- rerender(
370
- <MarkdownEngine
371
- content={`${base} and more`}
372
- streaming
373
- componentOverrides={COUNTING_OVERRIDES}
374
- />,
375
- )
376
- await new Promise((r) => setTimeout(r, 0))
377
- })
378
-
379
- // Only the live tail may re-parse. Before the NO_BROKEN_LINKS fix the
380
- // `components` memo re-created every render, so all THREE re-rendered.
381
- expect(paragraphRenders - before).toBe(1)
382
- // …and the completed block's DOM node is the very same element.
383
- expect(Object.is(container.querySelector('p'), firstParagraph)).toBe(true)
384
- })
385
-
386
- it('survives heading ids: appending prose does not bust the block memo', async () => {
387
- // The heading-id map is rebuilt on EVERY token (its input is the growing
388
- // `processedContent`). It therefore reaches the heading renderers via
389
- // CONTEXT, never through the `components` memo — otherwise its changing
390
- // identity would re-create `components` per token and every completed
391
- // block's `memo` would bail, exactly the regression above.
392
- const base = '## Setup\n\nalpha para\n\nbeta para\n\ngamma tail'
393
- paragraphRenders = 0
394
-
395
- let container!: HTMLElement
396
- let rerender!: (ui: React.ReactElement) => void
397
- await act(async () => {
398
- const result = render(
399
- <MarkdownEngine content={base} streaming componentOverrides={COUNTING_OVERRIDES} />,
400
- )
401
- container = result.container
402
- rerender = result.rerender
403
- await new Promise((r) => setTimeout(r, 0))
404
- })
405
- const firstParagraph = container.querySelector('p')
406
- const before = paragraphRenders
407
-
408
- await act(async () => {
409
- rerender(
410
- <MarkdownEngine
411
- content={`${base} and more`}
412
- streaming
413
- componentOverrides={COUNTING_OVERRIDES}
414
- />,
415
- )
416
- await new Promise((r) => setTimeout(r, 0))
417
- })
418
- expect(paragraphRenders - before).toBe(1)
419
- expect(Object.is(container.querySelector('p'), firstParagraph)).toBe(true)
420
-
421
- // A tail that DOES add a heading still gets correct, unique ids.
422
- await act(async () => {
423
- rerender(
424
- <MarkdownEngine
425
- content={`${base}\n\n## Setup\n\nsecond`}
426
- streaming
427
- componentOverrides={COUNTING_OVERRIDES}
428
- />,
429
- )
430
- await new Promise((r) => setTimeout(r, 0))
431
- })
432
- const ids = Array.from(container.querySelectorAll('h2')).map((el) => el.id)
433
- expect(ids).toEqual(['setup', 'setup-2'])
434
- expect(new Set(ids).size).toBe(ids.length)
435
- })
436
- })