@flamingo-stack/openframe-frontend-core 0.0.490 → 0.0.492-1506.3956.1

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 (353) hide show
  1. package/dist/chat-protocol/decode.d.ts +58 -0
  2. package/dist/chat-protocol/decode.d.ts.map +1 -0
  3. package/dist/chat-protocol/encode.d.ts +44 -0
  4. package/dist/chat-protocol/encode.d.ts.map +1 -0
  5. package/dist/chat-protocol/env-flag.d.ts +35 -0
  6. package/dist/chat-protocol/env-flag.d.ts.map +1 -0
  7. package/dist/chat-protocol/events.d.ts +207 -0
  8. package/dist/chat-protocol/events.d.ts.map +1 -0
  9. package/dist/chat-protocol/frames.d.ts +213 -0
  10. package/dist/chat-protocol/frames.d.ts.map +1 -0
  11. package/dist/chat-protocol/index.cjs +500 -0
  12. package/dist/chat-protocol/index.cjs.map +1 -0
  13. package/dist/chat-protocol/index.d.ts +18 -0
  14. package/dist/chat-protocol/index.d.ts.map +1 -0
  15. package/dist/chat-protocol/index.js +484 -0
  16. package/dist/chat-protocol/index.js.map +1 -0
  17. package/dist/chat-protocol/ip-normalize.d.ts +44 -0
  18. package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
  19. package/dist/chat-protocol/nats-decoder.d.ts +26 -0
  20. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
  21. package/dist/{chunk-WLM3F3WU.js → chunk-34TDA22K.js} +3 -3
  22. package/dist/{chunk-EGXCWZYP.js → chunk-3LKB722U.js} +3 -3
  23. package/dist/{chunk-RW2LPL72.js → chunk-6WTCJI5R.js} +2 -2
  24. package/dist/{chunk-VUXNXWOQ.cjs → chunk-7N7YZCHR.cjs} +7503 -5167
  25. package/dist/chunk-7N7YZCHR.cjs.map +1 -0
  26. package/dist/{chunk-AW63ET2A.cjs → chunk-A6FPYAUD.cjs} +19 -19
  27. package/dist/{chunk-AW63ET2A.cjs.map → chunk-A6FPYAUD.cjs.map} +1 -1
  28. package/dist/{chunk-BZMOEWWS.js → chunk-BEEPU44B.js} +12 -12
  29. package/dist/chunk-BEEPU44B.js.map +1 -0
  30. package/dist/{chunk-EJQUYZXF.cjs → chunk-C65NCIMV.cjs} +12 -12
  31. package/dist/{chunk-EJQUYZXF.cjs.map → chunk-C65NCIMV.cjs.map} +1 -1
  32. package/dist/{chunk-VL2YMDPH.js → chunk-J3VBVR4W.js} +6 -6
  33. package/dist/{chunk-AIRNG27G.js → chunk-JHEIOXXZ.js} +8 -7
  34. package/dist/chunk-JHEIOXXZ.js.map +1 -0
  35. package/dist/{chunk-D5LQMRPU.cjs → chunk-JSEJ5MH5.cjs} +5 -5
  36. package/dist/{chunk-D5LQMRPU.cjs.map → chunk-JSEJ5MH5.cjs.map} +1 -1
  37. package/dist/{chunk-FY5XAW3H.cjs → chunk-KX3K2EH4.cjs} +18 -18
  38. package/dist/{chunk-FY5XAW3H.cjs.map → chunk-KX3K2EH4.cjs.map} +1 -1
  39. package/dist/{chunk-3IEVA5AO.cjs → chunk-M6FEGPQJ.cjs} +93 -93
  40. package/dist/{chunk-3IEVA5AO.cjs.map → chunk-M6FEGPQJ.cjs.map} +1 -1
  41. package/dist/{chunk-7I42V2OD.cjs → chunk-MEIGDK7M.cjs} +74 -74
  42. package/dist/chunk-MEIGDK7M.cjs.map +1 -0
  43. package/dist/{chunk-OQIMH2WQ.js → chunk-MEMRQTOI.js} +12263 -9927
  44. package/dist/chunk-MEMRQTOI.js.map +1 -0
  45. package/dist/{chunk-GUTYUMGV.cjs → chunk-MPUQFRJG.cjs} +12 -12
  46. package/dist/{chunk-GUTYUMGV.cjs.map → chunk-MPUQFRJG.cjs.map} +1 -1
  47. package/dist/{chunk-UZZT65PQ.cjs → chunk-MSL5NE3N.cjs} +40 -40
  48. package/dist/{chunk-UZZT65PQ.cjs.map → chunk-MSL5NE3N.cjs.map} +1 -1
  49. package/dist/{chunk-6CJ2K7BD.js → chunk-MZRIRQYV.js} +3 -3
  50. package/dist/{chunk-NGEFYPY7.js → chunk-NCAN6VUF.js} +4 -4
  51. package/dist/{chunk-6QK3ONDD.js → chunk-OL3MJKUI.js} +2 -2
  52. package/dist/{chunk-MPYDX5WX.js → chunk-PLB6CQSO.js} +4 -4
  53. package/dist/{chunk-6CFFISUC.js → chunk-PTXZB2KW.js} +3 -3
  54. package/dist/{chunk-GMPYR3MX.cjs → chunk-QDGB2DKY.cjs} +33 -33
  55. package/dist/{chunk-GMPYR3MX.cjs.map → chunk-QDGB2DKY.cjs.map} +1 -1
  56. package/dist/{chunk-2MKUHJ3O.cjs → chunk-QG2EBIH5.cjs} +40 -40
  57. package/dist/{chunk-2MKUHJ3O.cjs.map → chunk-QG2EBIH5.cjs.map} +1 -1
  58. package/dist/{chunk-GZK7FWJJ.js → chunk-QKWOBV55.js} +12 -12
  59. package/dist/chunk-QKWOBV55.js.map +1 -0
  60. package/dist/{chunk-STYA77PH.cjs → chunk-SGL6URIJ.cjs} +12 -12
  61. package/dist/chunk-SGL6URIJ.cjs.map +1 -0
  62. package/dist/{chunk-KP5KRRD6.cjs → chunk-THC3K262.cjs} +3 -3
  63. package/dist/{chunk-KP5KRRD6.cjs.map → chunk-THC3K262.cjs.map} +1 -1
  64. package/dist/{chunk-BIKOJ4FH.cjs → chunk-TTYJAZ7P.cjs} +58 -32
  65. package/dist/chunk-TTYJAZ7P.cjs.map +1 -0
  66. package/dist/{chunk-IEZ3YBAP.cjs → chunk-W56JYDSC.cjs} +34 -33
  67. package/dist/chunk-W56JYDSC.cjs.map +1 -0
  68. package/dist/{chunk-PB3BTZDU.js → chunk-WEUNPE3S.js} +139 -113
  69. package/dist/chunk-WEUNPE3S.js.map +1 -0
  70. package/dist/{chunk-VDZO6IDR.js → chunk-XOFE2RT4.js} +5 -5
  71. package/dist/components/case-studies/index.cjs +10 -10
  72. package/dist/components/case-studies/index.js +4 -4
  73. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  74. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  75. package/dist/components/chat/hooks/index.d.ts +0 -1
  76. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  77. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  78. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  79. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  80. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
  81. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  82. package/dist/components/chat/index.cjs +12 -4
  83. package/dist/components/chat/index.cjs.map +1 -1
  84. package/dist/components/chat/index.d.ts +1 -0
  85. package/dist/components/chat/index.d.ts.map +1 -1
  86. package/dist/components/chat/index.js +23 -15
  87. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  88. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  89. package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
  90. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  91. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  92. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  93. package/dist/components/chat/stream/index.d.ts +17 -0
  94. package/dist/components/chat/stream/index.d.ts.map +1 -0
  95. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  96. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  97. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  98. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  99. package/dist/components/chat/types/api.types.d.ts +1 -81
  100. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  101. package/dist/components/chat/types/processing.types.d.ts +2 -93
  102. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  103. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  104. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  105. package/dist/components/chat/utils/index.d.ts +2 -3
  106. package/dist/components/chat/utils/index.d.ts.map +1 -1
  107. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  108. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  109. package/dist/components/contact/index.cjs +5 -5
  110. package/dist/components/contact/index.js +4 -4
  111. package/dist/components/docs/index.cjs +7 -7
  112. package/dist/components/docs/index.js +6 -6
  113. package/dist/components/embeds/index.cjs +5 -5
  114. package/dist/components/embeds/index.js +4 -4
  115. package/dist/components/faq/index.cjs +6 -6
  116. package/dist/components/faq/index.js +5 -5
  117. package/dist/components/features/index.cjs +4 -4
  118. package/dist/components/features/index.js +3 -3
  119. package/dist/components/features/select-button.d.ts.map +1 -1
  120. package/dist/components/help-center-pages/index.cjs +23 -23
  121. package/dist/components/help-center-pages/index.js +14 -14
  122. package/dist/components/index.cjs +203 -161
  123. package/dist/components/index.cjs.map +1 -1
  124. package/dist/components/index.js +66 -24
  125. package/dist/components/index.js.map +1 -1
  126. package/dist/components/layout/page-layout.d.ts +7 -1
  127. package/dist/components/layout/page-layout.d.ts.map +1 -1
  128. package/dist/components/layout/title-block.d.ts +23 -1
  129. package/dist/components/layout/title-block.d.ts.map +1 -1
  130. package/dist/components/navigation/index.cjs +4 -4
  131. package/dist/components/navigation/index.js +3 -3
  132. package/dist/components/onboarding-guides/index.cjs +8 -8
  133. package/dist/components/onboarding-guides/index.js +7 -7
  134. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  135. package/dist/components/related-content/index.cjs +6 -6
  136. package/dist/components/related-content/index.js +5 -5
  137. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  138. package/dist/components/tickets/index.cjs +7 -7
  139. package/dist/components/tickets/index.js +6 -6
  140. package/dist/components/ui/autocomplete.d.ts.map +1 -1
  141. package/dist/components/ui/checkbox-block.d.ts.map +1 -1
  142. package/dist/components/ui/date-picker.d.ts.map +1 -1
  143. package/dist/components/ui/field-wrapper.d.ts +10 -1
  144. package/dist/components/ui/field-wrapper.d.ts.map +1 -1
  145. package/dist/components/ui/file-manager/index.cjs +50 -50
  146. package/dist/components/ui/file-manager/index.js +1 -1
  147. package/dist/components/ui/floating-tooltip.d.ts +8 -1
  148. package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
  149. package/dist/components/ui/index.cjs +46 -4
  150. package/dist/components/ui/index.cjs.map +1 -1
  151. package/dist/components/ui/index.d.ts +1 -2
  152. package/dist/components/ui/index.d.ts.map +1 -1
  153. package/dist/components/ui/index.js +57 -15
  154. package/dist/components/ui/input-trigger.d.ts.map +1 -1
  155. package/dist/components/ui/input.d.ts.map +1 -1
  156. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  157. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  158. package/dist/components/ui/markdown/engine.d.ts +99 -0
  159. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  160. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  161. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  162. package/dist/components/ui/markdown/index.d.ts +18 -0
  163. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  164. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  165. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  166. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  167. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  168. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  169. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  170. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  171. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  172. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  173. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  174. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  175. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  176. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  177. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  178. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  179. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  180. package/dist/components/ui/page-actions.d.ts +7 -1
  181. package/dist/components/ui/page-actions.d.ts.map +1 -1
  182. package/dist/components/ui/radio-group.d.ts.map +1 -1
  183. package/dist/components/ui/search-input.d.ts.map +1 -1
  184. package/dist/components/ui/select.d.ts.map +1 -1
  185. package/dist/components/ui/tab-navigation.d.ts +10 -1
  186. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  187. package/dist/components/ui/tag-search-input.d.ts.map +1 -1
  188. package/dist/components/ui/textarea.d.ts.map +1 -1
  189. package/dist/components/ui/truncate-text.d.ts +2 -2
  190. package/dist/components/ui/truncate-text.d.ts.map +1 -1
  191. package/dist/hooks/index.cjs +4 -2
  192. package/dist/hooks/index.cjs.map +1 -1
  193. package/dist/hooks/index.js +3 -1
  194. package/dist/hooks/ui/index.d.ts +1 -0
  195. package/dist/hooks/ui/index.d.ts.map +1 -1
  196. package/dist/hooks/ui/use-is-truncated.d.ts +33 -0
  197. package/dist/hooks/ui/use-is-truncated.d.ts.map +1 -0
  198. package/dist/index.cjs +64 -4
  199. package/dist/index.cjs.map +1 -1
  200. package/dist/index.js +75 -15
  201. package/dist/utils/index.cjs +169 -40
  202. package/dist/utils/index.cjs.map +1 -1
  203. package/dist/utils/index.d.ts +1 -0
  204. package/dist/utils/index.d.ts.map +1 -1
  205. package/dist/utils/index.js +162 -41
  206. package/dist/utils/index.js.map +1 -1
  207. package/dist/utils/markdown-fences.d.ts +42 -0
  208. package/dist/utils/markdown-fences.d.ts.map +1 -0
  209. package/dist/utils/markdown-heading-id.d.ts +85 -0
  210. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  211. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  212. package/package.json +7 -1
  213. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +317 -0
  214. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  215. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  216. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  217. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +232 -0
  218. package/src/chat-protocol/decode.ts +278 -0
  219. package/src/chat-protocol/encode.ts +71 -0
  220. package/src/chat-protocol/env-flag.ts +39 -0
  221. package/src/chat-protocol/events.ts +245 -0
  222. package/src/chat-protocol/frames.ts +245 -0
  223. package/src/chat-protocol/index.ts +21 -0
  224. package/src/chat-protocol/ip-normalize.ts +146 -0
  225. package/src/chat-protocol/nats-decoder.ts +258 -0
  226. package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
  227. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  228. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +230 -0
  229. package/src/components/chat/chat-message-enhanced.tsx +122 -20
  230. package/src/components/chat/chat-message-list.tsx +358 -109
  231. package/src/components/chat/embeddable-chat.tsx +6 -0
  232. package/src/components/chat/hooks/.index.md +30 -33
  233. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  234. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  235. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  236. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  237. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  238. package/src/components/chat/hooks/index.ts +0 -1
  239. package/src/components/chat/hooks/use-chat.ts +8 -0
  240. package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -825
  241. package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
  242. package/src/components/chat/index.ts +5 -0
  243. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +940 -0
  244. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
  245. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
  246. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +973 -0
  247. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  248. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
  249. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  250. package/src/components/chat/stream/chat-stream-reducer.ts +1808 -0
  251. package/src/components/chat/stream/delta-batcher.ts +168 -0
  252. package/src/components/chat/stream/index.ts +55 -0
  253. package/src/components/chat/stream/message-mutations.ts +340 -0
  254. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  255. package/src/components/chat/thinking-display.tsx +1 -1
  256. package/src/components/chat/types/.api.types.md +39 -49
  257. package/src/components/chat/types/.processing.types.md +29 -58
  258. package/src/components/chat/types/api.types.ts +6 -73
  259. package/src/components/chat/types/processing.types.ts +11 -53
  260. package/src/components/chat/utils/.index.md +31 -40
  261. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  262. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  263. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
  264. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  265. package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
  266. package/src/components/chat/utils/index.ts +5 -8
  267. package/src/components/chat/utils/process-historical-messages.ts +358 -386
  268. package/src/components/features/select-button.tsx +4 -1
  269. package/src/components/layout/page-layout.tsx +16 -0
  270. package/src/components/layout/title-block.tsx +106 -7
  271. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  272. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  273. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  274. package/src/components/ui/.field-wrapper.md +5 -4
  275. package/src/components/ui/.tab-navigation.md +14 -1
  276. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  277. package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
  278. package/src/components/ui/autocomplete.tsx +13 -1
  279. package/src/components/ui/button/button.tsx +1 -1
  280. package/src/components/ui/checkbox-block.tsx +27 -3
  281. package/src/components/ui/date-picker.tsx +15 -1
  282. package/src/components/ui/field-wrapper.tsx +12 -6
  283. package/src/components/ui/floating-tooltip.tsx +9 -1
  284. package/src/components/ui/index.ts +1 -2
  285. package/src/components/ui/input-trigger.tsx +8 -1
  286. package/src/components/ui/input.tsx +8 -1
  287. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
  288. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
  289. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
  290. package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
  291. package/src/components/ui/markdown/base-components.tsx +360 -0
  292. package/src/components/ui/markdown/engine.tsx +315 -0
  293. package/src/components/ui/markdown/heading-ids.ts +239 -0
  294. package/src/components/ui/markdown/index.ts +49 -0
  295. package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
  296. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  297. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  298. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  299. package/src/components/ui/markdown/sanitize.ts +3499 -0
  300. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  301. package/src/components/ui/markdown/streaming.ts +362 -0
  302. package/src/components/ui/markdown/text-size.ts +106 -0
  303. package/src/components/ui/page-actions.tsx +142 -30
  304. package/src/components/ui/radio-group.tsx +28 -3
  305. package/src/components/ui/release-changelog-section.tsx +1 -1
  306. package/src/components/ui/search-input.tsx +2 -1
  307. package/src/components/ui/select.tsx +7 -0
  308. package/src/components/ui/tab-navigation.tsx +26 -4
  309. package/src/components/ui/tag-search-input.tsx +9 -2
  310. package/src/components/ui/textarea.tsx +8 -1
  311. package/src/components/ui/ticket-info-section.tsx +1 -1
  312. package/src/components/ui/truncate-text.tsx +3 -18
  313. package/src/hooks/ui/index.ts +1 -0
  314. package/src/hooks/ui/use-is-truncated.ts +66 -0
  315. package/src/utils/index.ts +1 -0
  316. package/src/utils/markdown-fences.ts +127 -0
  317. package/src/utils/markdown-heading-id.ts +348 -0
  318. package/src/utils/markdown-section-extractor.ts +41 -56
  319. package/dist/chunk-7I42V2OD.cjs.map +0 -1
  320. package/dist/chunk-AIRNG27G.js.map +0 -1
  321. package/dist/chunk-BIKOJ4FH.cjs.map +0 -1
  322. package/dist/chunk-BZMOEWWS.js.map +0 -1
  323. package/dist/chunk-GZK7FWJJ.js.map +0 -1
  324. package/dist/chunk-IEZ3YBAP.cjs.map +0 -1
  325. package/dist/chunk-OQIMH2WQ.js.map +0 -1
  326. package/dist/chunk-PB3BTZDU.js.map +0 -1
  327. package/dist/chunk-STYA77PH.cjs.map +0 -1
  328. package/dist/chunk-VUXNXWOQ.cjs.map +0 -1
  329. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  330. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  331. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  332. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  333. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  334. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  335. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  336. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  337. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  338. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
  339. package/src/components/chat/utils/.chunk-parser.md +0 -62
  340. package/src/components/chat/utils/chunk-parser.ts +0 -262
  341. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  342. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  343. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  344. /package/dist/{chunk-WLM3F3WU.js.map → chunk-34TDA22K.js.map} +0 -0
  345. /package/dist/{chunk-EGXCWZYP.js.map → chunk-3LKB722U.js.map} +0 -0
  346. /package/dist/{chunk-RW2LPL72.js.map → chunk-6WTCJI5R.js.map} +0 -0
  347. /package/dist/{chunk-VL2YMDPH.js.map → chunk-J3VBVR4W.js.map} +0 -0
  348. /package/dist/{chunk-6CJ2K7BD.js.map → chunk-MZRIRQYV.js.map} +0 -0
  349. /package/dist/{chunk-NGEFYPY7.js.map → chunk-NCAN6VUF.js.map} +0 -0
  350. /package/dist/{chunk-6QK3ONDD.js.map → chunk-OL3MJKUI.js.map} +0 -0
  351. /package/dist/{chunk-MPYDX5WX.js.map → chunk-PLB6CQSO.js.map} +0 -0
  352. /package/dist/{chunk-6CFFISUC.js.map → chunk-PTXZB2KW.js.map} +0 -0
  353. /package/dist/{chunk-VDZO6IDR.js.map → chunk-XOFE2RT4.js.map} +0 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Pending-turn + hidden-row contracts.
3
+ *
4
+ * Two reader-visible defects this pins against:
5
+ *
6
+ * 1. Approving a tool call minted an author-label row with NO body
7
+ * ("Michael Assraf:" then nothing). The approval decision is
8
+ * out-of-band metadata, so the reducer must not materialize a user
9
+ * row for it at all — fixing it at render would leave dead state.
10
+ * 2. While the model was thinking, the transcript showed a second,
11
+ * empty "Mingo:" row ABOVE the footer progress indicator — two
12
+ * owners for one pending turn, and an `aria-live` log announcing a
13
+ * bare "Mingo". Exactly one affordance must be present.
14
+ *
15
+ * Plus the two idioms that must SURVIVE both fixes: a hidden row that
16
+ * carries real text (auto-continuation directive) stays in state while
17
+ * never rendering, and the SYSTEM name-only row still renders.
18
+ */
19
+
20
+ import { render } from '@testing-library/react'
21
+ import { describe, expect, it, vi } from 'vitest'
22
+ import { createChatStreamReducer } from '../stream/chat-stream-reducer'
23
+ import {
24
+ AUTO_CONTINUATION_DIRECTIVE_PREFIX,
25
+ buildAutoContinuationDirective,
26
+ } from '../utils/auto-continuation-directive'
27
+ import type { Message } from '../types/message.types'
28
+
29
+ // ─── Render harness (mirrors chat-message-list.test.tsx) ────────────────────
30
+
31
+ const scrollToBottom = vi.fn()
32
+ const stopScroll = vi.fn()
33
+
34
+ function makeRef() {
35
+ const fn = ((el: HTMLElement | null) => {
36
+ ;(fn as { current: HTMLElement | null }).current = el
37
+ }) as ((el: HTMLElement | null) => void) & { current: HTMLElement | null }
38
+ fn.current = null
39
+ return fn
40
+ }
41
+
42
+ vi.mock('use-stick-to-bottom', () => ({
43
+ useStickToBottom: () => ({
44
+ scrollRef: makeRef(),
45
+ contentRef: makeRef(),
46
+ scrollToBottom,
47
+ stopScroll,
48
+ }),
49
+ }))
50
+
51
+ // Row stub that renders the author label the way the real component does
52
+ // (`Name:`), so "labeled row with no body" is directly assertable.
53
+ vi.mock('../chat-message-enhanced', () => ({
54
+ ChatMessageEnhanced: ({ name, content }: { name?: string; content: unknown }) => (
55
+ <div data-testid="row">
56
+ <span data-testid="row-name">{name ?? ''}</span>
57
+ <span data-testid="row-body">
58
+ {typeof content === 'string'
59
+ ? content
60
+ : Array.isArray(content)
61
+ ? content
62
+ .map((s) => (s && (s as { type?: string }).type === 'text' ? (s as { text: string }).text : '[seg]'))
63
+ .join('')
64
+ : ''}
65
+ </span>
66
+ </div>
67
+ ),
68
+ }))
69
+
70
+ import { ChatMessageList } from '../chat-message-list'
71
+
72
+ class ObserverStub {
73
+ observe() {}
74
+ unobserve() {}
75
+ disconnect() {}
76
+ }
77
+ vi.stubGlobal('IntersectionObserver', ObserverStub)
78
+ vi.stubGlobal('ResizeObserver', ObserverStub)
79
+
80
+ const names = (c: HTMLElement) =>
81
+ Array.from(c.querySelectorAll('[data-testid="row-name"]')).map((n) => n.textContent)
82
+ const bodies = (c: HTMLElement) =>
83
+ Array.from(c.querySelectorAll('[data-testid="row-body"]')).map((n) => n.textContent)
84
+
85
+ // ─── State-level contracts ─────────────────────────────────────────────────
86
+
87
+ describe('approval sends mint no visible user row', () => {
88
+ it('beginSseSend with empty text pushes only the assistant placeholder', () => {
89
+ const r = createChatStreamReducer({ transport: 'sse' })
90
+ // What `cardApprove` triggers: ('', { hidden: true, approvalAction }).
91
+ r.beginSseSend({ text: '', hidden: true, assistantName: 'Mingo AI' })
92
+
93
+ expect(r.state.messages).toHaveLength(1)
94
+ expect(r.state.messages[0].role).toBe('assistant')
95
+ expect(r.state.messages.some((m) => m.role === 'user')).toBe(false)
96
+ expect(r.state.streamingPhase).toBe('thinking')
97
+ })
98
+
99
+ it('pushOptimisticSend with empty text likewise mints no user row', () => {
100
+ const r = createChatStreamReducer({ transport: 'nats' })
101
+ r.pushOptimisticSend('', true)
102
+
103
+ expect(r.state.messages).toHaveLength(1)
104
+ expect(r.state.messages[0].role).toBe('assistant')
105
+ })
106
+
107
+ it('a normal send still mints the user row', () => {
108
+ const r = createChatStreamReducer({ transport: 'sse' })
109
+ r.beginSseSend({ text: 'hello', assistantName: 'Mingo AI' })
110
+
111
+ expect(r.state.messages).toHaveLength(2)
112
+ expect(r.state.messages[0]).toMatchObject({ role: 'user', content: 'hello' })
113
+ expect(r.state.messages[0].hidden).toBeUndefined()
114
+ })
115
+ })
116
+
117
+ describe('hidden rows that carry text', () => {
118
+ it('an auto-continuation directive IS state (the model sees it) but is flagged hidden', () => {
119
+ const directive = buildAutoContinuationDirective('create_ticket', { ticketId: '42' })
120
+ expect(directive.startsWith(AUTO_CONTINUATION_DIRECTIVE_PREFIX)).toBe(true)
121
+
122
+ const r = createChatStreamReducer({ transport: 'sse' })
123
+ r.beginSseSend({ text: directive, hidden: true, assistantName: 'Mingo AI' })
124
+
125
+ const userRow = r.state.messages.find((m) => m.role === 'user')
126
+ expect(userRow?.content).toBe(directive)
127
+ expect(userRow?.hidden).toBe(true)
128
+ })
129
+
130
+ it('never renders — the raw directive text must not reach the transcript', () => {
131
+ const directive = buildAutoContinuationDirective('create_ticket', { ticketId: '42' })
132
+ const messages: Message[] = [
133
+ { id: 'u1', role: 'user', name: 'Michael Assraf', content: 'my laptop is dead' },
134
+ { id: 'h1', role: 'user', name: 'Michael Assraf', content: directive, hidden: true },
135
+ { id: 'a1', role: 'assistant', name: 'Mingo', content: 'Opened a ticket.' },
136
+ ]
137
+ const { container } = render(<ChatMessageList dialogId="d" messages={messages} />)
138
+
139
+ expect(container.textContent).not.toContain(AUTO_CONTINUATION_DIRECTIVE_PREFIX)
140
+ expect(bodies(container)).toEqual(['my laptop is dead', 'Opened a ticket.'])
141
+ })
142
+ })
143
+
144
+ // ─── Render-level contracts ────────────────────────────────────────────────
145
+
146
+ describe('a thinking turn has exactly ONE pending affordance', () => {
147
+ const pending: Message[] = [
148
+ { id: 'u1', role: 'user', name: 'Michael Assraf', content: 'what is broken?' },
149
+ { id: 'a1', role: 'assistant', name: 'Mingo', content: '', segments: [] },
150
+ ]
151
+
152
+ it('does not render the empty labeled assistant row while typing', () => {
153
+ const { container } = render(
154
+ <ChatMessageList dialogId="d" messages={pending} isTyping />,
155
+ )
156
+ // The bare "Mingo" label must be gone from the transcript entirely —
157
+ // removed from the DOM, not visually hidden (the list is an aria-live
158
+ // log; a zero-height node would still be announced).
159
+ expect(names(container)).toEqual(['Michael Assraf'])
160
+ expect(bodies(container)).toEqual(['what is broken?'])
161
+ // …and the single remaining owner is the polite status indicator.
162
+ expect(container.querySelectorAll('[role="status"]')).toHaveLength(1)
163
+ })
164
+
165
+ it('renders the assistant row as soon as it carries text', () => {
166
+ const withText: Message[] = [
167
+ pending[0],
168
+ { id: 'a1', role: 'assistant', name: 'Mingo', content: [{ type: 'text', text: 'Chec' }] },
169
+ ]
170
+ const { container } = render(
171
+ <ChatMessageList dialogId="d" messages={withText} isTyping />,
172
+ )
173
+ expect(names(container)).toEqual(['Michael Assraf', 'Mingo'])
174
+ })
175
+
176
+ it('renders the assistant row when it carries a NON-text segment (tool call)', () => {
177
+ const withTool: Message[] = [
178
+ pending[0],
179
+ {
180
+ id: 'a1',
181
+ role: 'assistant',
182
+ name: 'Mingo',
183
+ content: [
184
+ { type: 'tool_execution', toolExecutionRequestId: 'e1', status: 'executing' },
185
+ ] as Message['content'],
186
+ },
187
+ ]
188
+ const { container } = render(
189
+ <ChatMessageList dialogId="d" messages={withTool} isTyping />,
190
+ )
191
+ expect(names(container)).toEqual(['Michael Assraf', 'Mingo'])
192
+ })
193
+
194
+ it('renders the assistant row when it carries ONLY a guide segment', () => {
195
+ // A `guide` body is the whole visible answer for a how-to question. If
196
+ // the empty-pending-turn predicate ever counted it as "nothing visible"
197
+ // (e.g. by widening its text-only clause), a guide-only turn would render
198
+ // NOTHING until the turn ended — the footer loader would be the sole
199
+ // affordance while a fully-formed guide card sat in state.
200
+ const withGuide: Message[] = [
201
+ pending[0],
202
+ {
203
+ id: 'a1',
204
+ role: 'assistant',
205
+ name: 'Mingo',
206
+ content: [{ type: 'guide', text: '## Enroll a device' }],
207
+ },
208
+ ]
209
+ const { container } = render(
210
+ <ChatMessageList dialogId="d" messages={withGuide} isTyping />,
211
+ )
212
+ expect(names(container)).toEqual(['Michael Assraf', 'Mingo'])
213
+ })
214
+
215
+ it('renders a completed empty-ish assistant row when NOT typing', () => {
216
+ const { container } = render(<ChatMessageList dialogId="d" messages={pending} />)
217
+ expect(names(container)).toEqual(['Michael Assraf', 'Mingo'])
218
+ })
219
+ })
220
+
221
+ describe('the SYSTEM name-only row is a legitimate label-without-body', () => {
222
+ it('still renders', () => {
223
+ const messages: Message[] = [
224
+ { id: 'u1', role: 'user', name: 'Michael Assraf', content: 'hi' },
225
+ { id: 's1', role: 'user', name: 'Technician joined', content: '', authorType: 'system' },
226
+ ]
227
+ const { container } = render(<ChatMessageList dialogId="d" messages={messages} isTyping />)
228
+ expect(names(container)).toContain('Technician joined')
229
+ })
230
+ })
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import React, { forwardRef, memo, useMemo, useRef } from "react"
3
+ import React, { forwardRef, memo, useEffect, 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/simple-markdown-renderer"
15
+ import { SimpleMarkdownRenderer } from "../ui/markdown/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
- if (!render) return { inlineByKey, partsBySegment }
168
- const cache = renderedCardNodeCache.current
169
167
  const usedKeys = new Set<string>()
168
+ if (!render) return { inlineByKey, partsBySegment, usedKeys }
169
+ const cache = renderedCardNodeCache.current
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,20 +281,99 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
281
281
  partsBySegment.set(segIdx, parts)
282
282
  }
283
283
  })
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
- }
290
- }
291
- return { inlineByKey, partsBySegment }
284
+ return { inlineByKey, partsBySegment, usedKeys }
292
285
  }, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
293
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)
300
+ }
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
+ )
374
+
294
375
  const cardComponentOverrides = useMemo(() => {
295
376
  if (!hasMarkerSupport && !hasMentionSupport) return undefined
296
- const refs = chatRefs ?? {}
297
- const inlineByKey = renderingPlan?.inlineByKey
298
377
  return {
299
378
  // Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`.
300
379
  // The render result was pre-computed in `renderingPlan` so block-level
@@ -322,7 +401,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
322
401
  const cardType = stripped.slice(0, sepIdx)
323
402
  const cardId = stripped.slice(sepIdx + 1)
324
403
  const key = `${cardType}:${cardId}`
325
- const inline = inlineByKey?.get(key)
404
+ const inline = renderingPlanRef.current?.inlineByKey.get(key)
326
405
  if (inline != null) return inline
327
406
  // Three fallback cases — keep them DISTINCT, never blur them
328
407
  // together. Mixing them up (which the old code did by reaching
@@ -346,7 +425,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
346
425
  // title from an unrelated ref — that hides the bug and
347
426
  // deceives the reader into thinking they're looking at
348
427
  // a real card.
349
- const refMatch: ChatRef | undefined = refs[key]
428
+ const refMatch: ChatRef | undefined = chatRefsRef.current?.[key]
350
429
  if (refMatch) {
351
430
  return <span className="text-ods-text-primary">{refMatch.title}</span>
352
431
  }
@@ -388,7 +467,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
388
467
  )
389
468
  },
390
469
  }
391
- }, [hasMarkerSupport, hasMentionSupport, renderMention, chatRefs, renderingPlan, NavLinkAnchor])
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])
392
477
 
393
478
  /**
394
479
  * Body of a markdown-bearing segment (`text` / `guide`).
@@ -403,8 +488,13 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
403
488
  * end). Stable React keys come from the card key (block) and chunk position
404
489
  * (text); streaming token-by-token reuses the same React instances so
405
490
  * `<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.
406
496
  */
407
- const renderSegmentBody = (segIndex: number, text: string) => {
497
+ const renderSegmentBody = (segIndex: number, text: string, isStreaming: boolean) => {
408
498
  const parts = renderingPlan?.partsBySegment.get(segIndex)
409
499
  if (!parts || parts.length === 0) {
410
500
  return (
@@ -413,6 +503,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
413
503
  textSize="compact"
414
504
  additionalRemarkPlugins={cardRemarkPlugins}
415
505
  componentOverrides={cardComponentOverrides}
506
+ streaming={isStreaming}
416
507
  />
417
508
  )
418
509
  }
@@ -425,6 +516,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
425
516
  textSize="compact"
426
517
  additionalRemarkPlugins={cardRemarkPlugins}
427
518
  componentOverrides={cardComponentOverrides}
519
+ streaming={isStreaming && pIdx === parts.length - 1}
428
520
  />
429
521
  )
430
522
  }
@@ -539,6 +631,12 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
539
631
  {/* Message segments — hidden for system messages without content */}
540
632
  {(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
541
633
  {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
542
640
  if (segment.type === 'text') {
543
641
  return (
544
642
  <div
@@ -548,11 +646,15 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
548
646
  isError ? "text-ods-error" : "text-ods-text-primary",
549
647
  )}
550
648
  >
551
- {renderSegmentBody(index, segment.text)}
649
+ {renderSegmentBody(index, segment.text, segmentIsStreaming)}
552
650
  </div>
553
651
  )
554
652
  } else if (segment.type === 'guide') {
555
- return <GuideDisplay key={index}>{renderSegmentBody(index, segment.text)}</GuideDisplay>
653
+ return (
654
+ <GuideDisplay key={index}>
655
+ {renderSegmentBody(index, segment.text, segmentIsStreaming)}
656
+ </GuideDisplay>
657
+ )
556
658
  } else if (segment.type === 'tool_execution') {
557
659
  return (
558
660
  <ToolExecutionDisplay