@flamingo-stack/openframe-frontend-core 0.0.490-1506.3944.1 → 0.0.490

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 (293) hide show
  1. package/dist/{chunk-5CJR2GZ4.cjs → chunk-2MKUHJ3O.cjs} +31 -31
  2. package/dist/{chunk-5CJR2GZ4.cjs.map → chunk-2MKUHJ3O.cjs.map} +1 -1
  3. package/dist/{chunk-S2NHJDKQ.cjs → chunk-3IEVA5AO.cjs} +62 -62
  4. package/dist/{chunk-S2NHJDKQ.cjs.map → chunk-3IEVA5AO.cjs.map} +1 -1
  5. package/dist/{chunk-GTWO5NAY.js → chunk-6CFFISUC.js} +2 -2
  6. package/dist/{chunk-VHG6AX5V.js → chunk-6CJ2K7BD.js} +2 -2
  7. package/dist/{chunk-4NWARV5I.js → chunk-6QK3ONDD.js} +2 -2
  8. package/dist/{chunk-YHOIAA2E.cjs → chunk-7I42V2OD.cjs} +60 -60
  9. package/dist/chunk-7I42V2OD.cjs.map +1 -0
  10. package/dist/{chunk-T2EGSZFN.js → chunk-AIRNG27G.js} +5 -6
  11. package/dist/chunk-AIRNG27G.js.map +1 -0
  12. package/dist/{chunk-NWX7MMF5.cjs → chunk-AW63ET2A.cjs} +15 -15
  13. package/dist/{chunk-NWX7MMF5.cjs.map → chunk-AW63ET2A.cjs.map} +1 -1
  14. package/dist/{chunk-TKN5667N.js → chunk-BZMOEWWS.js} +10 -10
  15. package/dist/chunk-BZMOEWWS.js.map +1 -0
  16. package/dist/{chunk-DSDZ5T4P.cjs → chunk-D5LQMRPU.cjs} +5 -5
  17. package/dist/{chunk-DSDZ5T4P.cjs.map → chunk-D5LQMRPU.cjs.map} +1 -1
  18. package/dist/{chunk-CORIBKHG.js → chunk-EGXCWZYP.js} +2 -2
  19. package/dist/{chunk-5EYRXSY5.cjs → chunk-EJQUYZXF.cjs} +9 -9
  20. package/dist/{chunk-5EYRXSY5.cjs.map → chunk-EJQUYZXF.cjs.map} +1 -1
  21. package/dist/{chunk-P4WTZU6L.cjs → chunk-FY5XAW3H.cjs} +11 -11
  22. package/dist/{chunk-P4WTZU6L.cjs.map → chunk-FY5XAW3H.cjs.map} +1 -1
  23. package/dist/{chunk-VQ523ORK.cjs → chunk-GMPYR3MX.cjs} +26 -26
  24. package/dist/{chunk-VQ523ORK.cjs.map → chunk-GMPYR3MX.cjs.map} +1 -1
  25. package/dist/{chunk-QSCHCZL3.cjs → chunk-GUTYUMGV.cjs} +7 -7
  26. package/dist/{chunk-QSCHCZL3.cjs.map → chunk-GUTYUMGV.cjs.map} +1 -1
  27. package/dist/{chunk-Q6E7VJ4W.cjs → chunk-IEZ3YBAP.cjs} +27 -28
  28. package/dist/chunk-IEZ3YBAP.cjs.map +1 -0
  29. package/dist/{chunk-HSACATCD.js → chunk-MPYDX5WX.js} +2 -2
  30. package/dist/{chunk-RQLBHKXN.js → chunk-NGEFYPY7.js} +2 -2
  31. package/dist/{chunk-BE6DK7QY.js → chunk-OQIMH2WQ.js} +8985 -11081
  32. package/dist/chunk-OQIMH2WQ.js.map +1 -0
  33. package/dist/{chunk-CSYSYJCF.cjs → chunk-UZZT65PQ.cjs} +37 -37
  34. package/dist/{chunk-CSYSYJCF.cjs.map → chunk-UZZT65PQ.cjs.map} +1 -1
  35. package/dist/{chunk-YZ2OIYQ3.js → chunk-VDZO6IDR.js} +4 -4
  36. package/dist/{chunk-UUYRJPGJ.js → chunk-VL2YMDPH.js} +4 -4
  37. package/dist/{chunk-5LMGFI4J.cjs → chunk-VUXNXWOQ.cjs} +4946 -7042
  38. package/dist/chunk-VUXNXWOQ.cjs.map +1 -0
  39. package/dist/{chunk-XQ2B74SI.js → chunk-WLM3F3WU.js} +2 -2
  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/guide-display.d.ts +14 -0
  45. package/dist/components/chat/guide-display.d.ts.map +1 -0
  46. package/dist/components/chat/hooks/index.d.ts +1 -0
  47. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  48. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  49. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +48 -15
  50. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  51. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +6 -0
  52. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -0
  53. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +57 -10
  54. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  55. package/dist/components/chat/index.cjs +2 -8
  56. package/dist/components/chat/index.cjs.map +1 -1
  57. package/dist/components/chat/index.d.ts +1 -1
  58. package/dist/components/chat/index.d.ts.map +1 -1
  59. package/dist/components/chat/index.js +15 -21
  60. package/dist/components/chat/types/api.types.d.ts +81 -1
  61. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  62. package/dist/components/chat/types/component.types.d.ts +6 -0
  63. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  64. package/dist/components/chat/types/message.types.d.ts +15 -2
  65. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  66. package/dist/components/chat/types/processing.types.d.ts +93 -2
  67. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  68. package/dist/components/chat/utils/chunk-parser.d.ts +25 -0
  69. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -0
  70. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +6 -34
  71. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  72. package/dist/components/chat/utils/index.d.ts +3 -2
  73. package/dist/components/chat/utils/index.d.ts.map +1 -1
  74. package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -0
  75. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  76. package/dist/components/chat/utils/process-historical-messages.d.ts +11 -59
  77. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  78. package/dist/components/contact/index.cjs +3 -3
  79. package/dist/components/contact/index.js +2 -2
  80. package/dist/components/docs/index.cjs +5 -5
  81. package/dist/components/docs/index.js +4 -4
  82. package/dist/components/embeds/index.cjs +3 -3
  83. package/dist/components/embeds/index.js +2 -2
  84. package/dist/components/faq/index.cjs +3 -3
  85. package/dist/components/faq/index.js +2 -2
  86. package/dist/components/features/index.cjs +2 -2
  87. package/dist/components/features/index.js +1 -1
  88. package/dist/components/help-center-pages/index.cjs +20 -20
  89. package/dist/components/help-center-pages/index.js +11 -11
  90. package/dist/components/index.cjs +130 -170
  91. package/dist/components/index.cjs.map +1 -1
  92. package/dist/components/index.js +23 -63
  93. package/dist/components/index.js.map +1 -1
  94. package/dist/components/navigation/index.cjs +2 -2
  95. package/dist/components/navigation/index.js +1 -1
  96. package/dist/components/onboarding-guides/index.cjs +5 -5
  97. package/dist/components/onboarding-guides/index.js +4 -4
  98. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  99. package/dist/components/related-content/index.cjs +3 -3
  100. package/dist/components/related-content/index.js +2 -2
  101. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  102. package/dist/components/tickets/index.cjs +5 -5
  103. package/dist/components/tickets/index.js +4 -4
  104. package/dist/components/ui/index.cjs +2 -42
  105. package/dist/components/ui/index.cjs.map +1 -1
  106. package/dist/components/ui/index.d.ts +2 -1
  107. package/dist/components/ui/index.d.ts.map +1 -1
  108. package/dist/components/ui/index.js +15 -55
  109. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  110. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  111. package/dist/components/ui/simple-markdown-renderer.d.ts +73 -0
  112. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -0
  113. package/dist/index.cjs +2 -58
  114. package/dist/index.cjs.map +1 -1
  115. package/dist/index.js +15 -71
  116. package/dist/utils/index.cjs +40 -169
  117. package/dist/utils/index.cjs.map +1 -1
  118. package/dist/utils/index.d.ts +0 -1
  119. package/dist/utils/index.d.ts.map +1 -1
  120. package/dist/utils/index.js +41 -162
  121. package/dist/utils/index.js.map +1 -1
  122. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  123. package/package.json +1 -7
  124. package/src/components/chat/__tests__/chat-message-list.test.tsx +5 -293
  125. package/src/components/chat/chat-message-enhanced.tsx +80 -163
  126. package/src/components/chat/chat-message-list.tsx +109 -358
  127. package/src/components/chat/embeddable-chat.tsx +0 -6
  128. package/src/components/chat/guide-display.tsx +44 -0
  129. package/src/components/chat/hooks/.index.md +33 -30
  130. package/src/components/chat/hooks/.use-nats-chat-adapter.md +55 -36
  131. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +77 -0
  132. package/src/components/chat/hooks/index.ts +1 -0
  133. package/src/components/chat/hooks/use-chat.ts +0 -8
  134. package/src/components/chat/hooks/use-nats-chat-adapter.ts +825 -243
  135. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +489 -0
  136. package/src/components/chat/hooks/use-sse-chat-adapter.ts +684 -380
  137. package/src/components/chat/index.ts +1 -5
  138. package/src/components/chat/thinking-display.tsx +1 -1
  139. package/src/components/chat/types/.api.types.md +49 -39
  140. package/src/components/chat/types/.processing.types.md +58 -29
  141. package/src/components/chat/types/api.types.ts +73 -6
  142. package/src/components/chat/types/component.types.ts +9 -0
  143. package/src/components/chat/types/message.types.ts +17 -1
  144. package/src/components/chat/types/processing.types.ts +53 -11
  145. package/src/components/chat/utils/.chunk-parser.md +62 -0
  146. package/src/components/chat/utils/.index.md +40 -31
  147. package/src/components/chat/utils/chunk-parser.ts +262 -0
  148. package/src/components/chat/utils/extract-incomplete-message-state.ts +5 -63
  149. package/src/components/chat/utils/index.ts +8 -5
  150. package/src/components/chat/utils/message-segment-accumulator.ts +19 -0
  151. package/src/components/chat/utils/process-historical-messages.ts +390 -352
  152. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +4 -22
  153. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  154. package/src/components/shared/product-release/release-detail-page.tsx +4 -19
  155. package/src/components/ui/.simple-markdown-renderer.md +52 -0
  156. package/src/components/ui/index.ts +2 -1
  157. package/src/components/ui/release-changelog-section.tsx +1 -1
  158. package/src/components/ui/rich-markdown-renderer.tsx +1223 -0
  159. package/src/components/ui/simple-markdown-renderer.tsx +964 -0
  160. package/src/components/ui/ticket-info-section.tsx +1 -1
  161. package/src/stories/ChatMessageEnhanced.stories.tsx +48 -1
  162. package/src/utils/index.ts +0 -1
  163. package/src/utils/markdown-section-extractor.ts +56 -41
  164. package/dist/chat-protocol/decode.d.ts +0 -58
  165. package/dist/chat-protocol/decode.d.ts.map +0 -1
  166. package/dist/chat-protocol/encode.d.ts +0 -44
  167. package/dist/chat-protocol/encode.d.ts.map +0 -1
  168. package/dist/chat-protocol/env-flag.d.ts +0 -35
  169. package/dist/chat-protocol/env-flag.d.ts.map +0 -1
  170. package/dist/chat-protocol/events.d.ts +0 -196
  171. package/dist/chat-protocol/events.d.ts.map +0 -1
  172. package/dist/chat-protocol/frames.d.ts +0 -213
  173. package/dist/chat-protocol/frames.d.ts.map +0 -1
  174. package/dist/chat-protocol/index.cjs +0 -494
  175. package/dist/chat-protocol/index.cjs.map +0 -1
  176. package/dist/chat-protocol/index.d.ts +0 -18
  177. package/dist/chat-protocol/index.d.ts.map +0 -1
  178. package/dist/chat-protocol/index.js +0 -478
  179. package/dist/chat-protocol/index.js.map +0 -1
  180. package/dist/chat-protocol/ip-normalize.d.ts +0 -44
  181. package/dist/chat-protocol/ip-normalize.d.ts.map +0 -1
  182. package/dist/chat-protocol/nats-decoder.d.ts +0 -26
  183. package/dist/chat-protocol/nats-decoder.d.ts.map +0 -1
  184. package/dist/chunk-5LMGFI4J.cjs.map +0 -1
  185. package/dist/chunk-BE6DK7QY.js.map +0 -1
  186. package/dist/chunk-Q6E7VJ4W.cjs.map +0 -1
  187. package/dist/chunk-T2EGSZFN.js.map +0 -1
  188. package/dist/chunk-TKN5667N.js.map +0 -1
  189. package/dist/chunk-YHOIAA2E.cjs.map +0 -1
  190. package/dist/components/chat/stream/chat-dialog-store.d.ts +0 -184
  191. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +0 -1
  192. package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -367
  193. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +0 -1
  194. package/dist/components/chat/stream/delta-batcher.d.ts +0 -51
  195. package/dist/components/chat/stream/delta-batcher.d.ts.map +0 -1
  196. package/dist/components/chat/stream/index.d.ts +0 -17
  197. package/dist/components/chat/stream/index.d.ts.map +0 -1
  198. package/dist/components/chat/stream/message-mutations.d.ts +0 -77
  199. package/dist/components/chat/stream/message-mutations.d.ts.map +0 -1
  200. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +0 -22
  201. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +0 -1
  202. package/dist/components/ui/markdown/base-components.d.ts +0 -51
  203. package/dist/components/ui/markdown/base-components.d.ts.map +0 -1
  204. package/dist/components/ui/markdown/engine.d.ts +0 -99
  205. package/dist/components/ui/markdown/engine.d.ts.map +0 -1
  206. package/dist/components/ui/markdown/heading-ids.d.ts +0 -65
  207. package/dist/components/ui/markdown/heading-ids.d.ts.map +0 -1
  208. package/dist/components/ui/markdown/index.d.ts +0 -18
  209. package/dist/components/ui/markdown/index.d.ts.map +0 -1
  210. package/dist/components/ui/markdown/mermaid-diagram.d.ts +0 -79
  211. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +0 -1
  212. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +0 -14
  213. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +0 -1
  214. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -40
  215. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +0 -1
  216. package/dist/components/ui/markdown/rich/shortcodes.d.ts +0 -8
  217. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +0 -1
  218. package/dist/components/ui/markdown/sanitize.d.ts +0 -153
  219. package/dist/components/ui/markdown/sanitize.d.ts.map +0 -1
  220. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +0 -23
  221. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +0 -1
  222. package/dist/components/ui/markdown/streaming.d.ts +0 -78
  223. package/dist/components/ui/markdown/streaming.d.ts.map +0 -1
  224. package/dist/components/ui/markdown/text-size.d.ts +0 -34
  225. package/dist/components/ui/markdown/text-size.d.ts.map +0 -1
  226. package/dist/utils/markdown-fences.d.ts +0 -42
  227. package/dist/utils/markdown-fences.d.ts.map +0 -1
  228. package/dist/utils/markdown-heading-id.d.ts +0 -85
  229. package/dist/utils/markdown-heading-id.d.ts.map +0 -1
  230. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -307
  231. package/src/chat-protocol/__tests__/chat-protocol.test.ts +0 -516
  232. package/src/chat-protocol/__tests__/env-flag.test.ts +0 -52
  233. package/src/chat-protocol/__tests__/ip-normalize.test.ts +0 -136
  234. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -228
  235. package/src/chat-protocol/decode.ts +0 -278
  236. package/src/chat-protocol/encode.ts +0 -71
  237. package/src/chat-protocol/env-flag.ts +0 -39
  238. package/src/chat-protocol/events.ts +0 -232
  239. package/src/chat-protocol/frames.ts +0 -245
  240. package/src/chat-protocol/index.ts +0 -21
  241. package/src/chat-protocol/ip-normalize.ts +0 -146
  242. package/src/chat-protocol/nats-decoder.ts +0 -252
  243. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -207
  244. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -209
  245. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +0 -37
  246. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  247. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +0 -438
  248. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +0 -318
  249. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +0 -940
  250. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +0 -756
  251. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +0 -460
  252. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -894
  253. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +0 -255
  254. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +0 -98
  255. package/src/components/chat/stream/chat-dialog-store.ts +0 -536
  256. package/src/components/chat/stream/chat-stream-reducer.ts +0 -1796
  257. package/src/components/chat/stream/delta-batcher.ts +0 -159
  258. package/src/components/chat/stream/index.ts +0 -55
  259. package/src/components/chat/stream/message-mutations.ts +0 -338
  260. package/src/components/chat/stream/use-chat-stream-reducer.ts +0 -126
  261. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +0 -420
  262. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +0 -605
  263. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -317
  264. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +0 -270
  265. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +0 -2420
  266. package/src/components/ui/__tests__/markdown-parity.test.tsx +0 -2528
  267. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +0 -115
  268. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +0 -95
  269. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +0 -309
  270. package/src/components/ui/markdown/__tests__/streaming.test.tsx +0 -388
  271. package/src/components/ui/markdown/base-components.tsx +0 -360
  272. package/src/components/ui/markdown/engine.tsx +0 -315
  273. package/src/components/ui/markdown/heading-ids.ts +0 -239
  274. package/src/components/ui/markdown/index.ts +0 -49
  275. package/src/components/ui/markdown/mermaid-diagram.tsx +0 -291
  276. package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -199
  277. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +0 -184
  278. package/src/components/ui/markdown/rich/shortcodes.ts +0 -170
  279. package/src/components/ui/markdown/sanitize.ts +0 -3499
  280. package/src/components/ui/markdown/simple-markdown-renderer.tsx +0 -28
  281. package/src/components/ui/markdown/streaming.ts +0 -362
  282. package/src/components/ui/markdown/text-size.ts +0 -106
  283. package/src/utils/markdown-fences.ts +0 -127
  284. package/src/utils/markdown-heading-id.ts +0 -348
  285. /package/dist/{chunk-GTWO5NAY.js.map → chunk-6CFFISUC.js.map} +0 -0
  286. /package/dist/{chunk-VHG6AX5V.js.map → chunk-6CJ2K7BD.js.map} +0 -0
  287. /package/dist/{chunk-4NWARV5I.js.map → chunk-6QK3ONDD.js.map} +0 -0
  288. /package/dist/{chunk-CORIBKHG.js.map → chunk-EGXCWZYP.js.map} +0 -0
  289. /package/dist/{chunk-HSACATCD.js.map → chunk-MPYDX5WX.js.map} +0 -0
  290. /package/dist/{chunk-RQLBHKXN.js.map → chunk-NGEFYPY7.js.map} +0 -0
  291. /package/dist/{chunk-YZ2OIYQ3.js.map → chunk-VDZO6IDR.js.map} +0 -0
  292. /package/dist/{chunk-UUYRJPGJ.js.map → chunk-VL2YMDPH.js.map} +0 -0
  293. /package/dist/{chunk-XQ2B74SI.js.map → chunk-WLM3F3WU.js.map} +0 -0
@@ -1,11 +1,11 @@
1
1
  "use client"
2
2
 
3
- import { useRef, useState, useEffect, useLayoutEffect, useImperativeHandle, forwardRef } from "react"
3
+ import { useRef, useEffect, useLayoutEffect, useImperativeHandle, forwardRef } from "react"
4
4
  import { useStickToBottom } from "use-stick-to-bottom"
5
5
  import { cn } from "../../utils/cn"
6
6
  import { ChatMessageEnhanced } from "./chat-message-enhanced"
7
7
  import { ChatMessageListSkeleton } from "./chat-message-skeleton"
8
- import { DotsLoaderIcon, Arrow02DownIcon } from "../icons-v2-generated"
8
+ import { DotsLoaderIcon } from "../icons-v2-generated"
9
9
  import { CyclingPhrase } from "./cycling-phrase"
10
10
  import type { ChatMessageListProps } from "./types"
11
11
  import { SCROLL_ANCHOR } from "./types/message.types"
@@ -27,11 +27,6 @@ const STREAMING_WORDS = [
27
27
  'Riffing',
28
28
  ] as const
29
29
 
30
- /** Distance from the bottom (px) still counted as "at the bottom".
31
- * Mirrors `use-stick-to-bottom`'s internal `STICK_TO_BOTTOM_OFFSET_PX`
32
- * so the jump-to-bottom button and the library agree on the boundary. */
33
- const BOTTOM_THRESHOLD_PX = 70
34
-
35
30
  /*
36
31
  * Stick-to-bottom: `use-stick-to-bottom` (stackblitz-labs)
37
32
  *
@@ -58,9 +53,6 @@ const BOTTOM_THRESHOLD_PX = 70
58
53
  * chat / sent a message; show me the bottom immediately").
59
54
  * - **Load-more sentinel** (top-of-list IntersectionObserver) for
60
55
  * infinite-scroll-UP. Distinct concern from stick-to-bottom.
61
- * - **Bottom-follow intent** (see `followBottomRef` below). The
62
- * library's follow is gated on its own `isAtBottom` flag, which this
63
- * list's layout can lose silently; we own the INTENT and re-assert.
64
56
  *
65
57
  * `useStickToBottom`'s returned refs are MUTABLE refs + ref callbacks
66
58
  * — pass them directly as `ref={scrollRef}` / `ref={contentRef}`.
@@ -150,45 +142,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
150
142
  const dialogIdRef = useRef(dialogId)
151
143
  dialogIdRef.current = dialogId
152
144
 
153
- // ---- Bottom-follow intent (list-owned lock) ----------------------
154
- // The library follows the bottom only while ITS OWN `isAtBottom` flag
155
- // is true, and this list's layout loses that flag silently in two
156
- // ways:
157
- //
158
- // 1. **The scroller's own box changes.** The library's
159
- // ResizeObserver watches `contentRef` (INSIDE the scroller) only.
160
- // The streaming loader unmounting and the host's source-chip row
161
- // mounting — both siblings BELOW the scroller (see render) —
162
- // change the scroller's `clientHeight`, which moves the bottom
163
- // without emitting anything the library can observe.
164
- // 2. **Resize-induced scroll events read as a user gesture.**
165
- // Fetch-mode entity cards mount as a skeleton and settle to a
166
- // different height; a shrink clamps `scrollTop` down, and the
167
- // library's scroll handler can see that as a scroll-UP and
168
- // permanently escape the lock (its `resizeDifference` guard is a
169
- // `setTimeout(…, 1)` race, so this is intermittent).
170
- //
171
- // Either way the thread stops following mid-turn. It bites hardest on
172
- // card-heavy answers — a slash-command listing (`/getting-started`)
173
- // renders a dozen fetch-mode cards with cover images that keep
174
- // resolving for SECONDS after the stream ends, so everything after
175
- // the lock was lost lands below the fold.
176
- //
177
- // Fix: this list owns the INTENT. Sending a message (or opening a
178
- // dialog) arms it; only a real user scroll-up gesture — or a
179
- // top-anchored turn, which deliberately parks elsewhere — disarms it.
180
- // While armed, ANY geometry change re-asserts `scrollToBottom`, and
181
- // because that call also re-sets the library's `isAtBottom`, a lost
182
- // lock self-heals instead of being permanent.
183
- const followBottomRef = useRef(false)
184
-
185
- // Drives the jump-to-bottom affordance. Derived from GEOMETRY, not
186
- // from the library's `isAtBottom`: that flag is the one this list
187
- // loses silently (see above), and a stale `true` would hide the
188
- // button in exactly the situation it exists for. Threshold mirrors
189
- // the library's own `STICK_TO_BOTTOM_OFFSET_PX`.
190
- const [atBottom, setAtBottom] = useState(true)
191
-
192
145
  // Tracks previous render state for explicit "force-stick" decisions
193
146
  // (dialog change, first message, new user message). The library
194
147
  // covers ongoing streaming; these are about INTENT transitions.
@@ -225,13 +178,11 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
225
178
 
226
179
  if (dialogChanged) {
227
180
  // Opening a different chat — instant snap to bottom.
228
- followBottomRef.current = true
229
181
  void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
230
182
  return
231
183
  }
232
184
  if (newCount > 0 && prevCount === 0) {
233
185
  // First message arrived in an empty chat — instant snap.
234
- followBottomRef.current = true
235
186
  void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
236
187
  return
237
188
  }
@@ -256,188 +207,54 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
256
207
  const newSlice = prevLastIdx >= 0 ? messages.slice(prevLastIdx + 1) : messages.slice(prevCount)
257
208
  const hasNewUser = newSlice.some((m) => m.role === 'user')
258
209
  if (hasNewUser) {
259
- // Arms the follow intent for the WHOLE turn: the answer's
260
- // async growth (attachment images, streamed tokens, entity
261
- // cards resolving out of their skeletons long after the
262
- // stream ends) is re-asserted by the follow effect below.
263
- // This replaces the old per-`<img>` `load`-handler hack —
264
- // same defect class, but the ResizeObserver sees every
265
- // source of growth, not just images present at send time.
266
- followBottomRef.current = true
267
210
  void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
211
+
212
+ // Image-attachment growth follow-up: if the new user message
213
+ // carries inline `<img>` elements (chat-attachment markdown
214
+ // emits `![alt](/api/storage/view/...)` which the markdown
215
+ // renderer turns into `<img>` / Next.js `<Image>`), those
216
+ // images load AFTER the initial paint. Each image-load grows
217
+ // the bubble height, but `useStickToBottom`'s `resize:
218
+ // 'smooth'` spring sometimes loses anchor under rapid
219
+ // resize events from Next.js Image's progressive
220
+ // dimensioning (observed: scroll lands 50-100px short of
221
+ // bottom — exactly one image-load short).
222
+ //
223
+ // Fix: after the initial scrollToBottom snaps, scan the
224
+ // content area for `<img>` elements that haven't loaded
225
+ // yet, and attach one-time `load` handlers that re-trigger
226
+ // `scrollToBottom`. Idempotent — runs for any image,
227
+ // affects only the new-user-message diff (effect dep is
228
+ // `messages`).
229
+ //
230
+ // `requestAnimationFrame` so the DOM has the new bubble
231
+ // before we query. Without rAF the new-message `<img>`s
232
+ // haven't been committed yet — querySelectorAll returns
233
+ // an empty set.
234
+ requestAnimationFrame(() => {
235
+ const el = contentRef.current
236
+ if (!el) return
237
+ const imgs = el.querySelectorAll('img')
238
+ imgs.forEach((img) => {
239
+ if (img.complete) return
240
+ const onLoad = () => {
241
+ img.removeEventListener('load', onLoad)
242
+ img.removeEventListener('error', onLoad)
243
+ // `ignoreEscapes: true` because the user may have
244
+ // accidentally moved scroll during the image-load
245
+ // window; we want their fresh send to be visible.
246
+ void scrollToBottom({ animation: 'instant', ignoreEscapes: true })
247
+ }
248
+ img.addEventListener('load', onLoad, { once: true })
249
+ img.addEventListener('error', onLoad, { once: true })
250
+ })
251
+ })
268
252
  }
269
253
  // Assistant-only new messages → the library's resize-watch
270
254
  // already keeps the bottom locked when the user hasn't
271
255
  // escaped. No explicit call needed; spring animation runs.
272
256
  }
273
- }, [autoScroll, messages, dialogId, scrollToBottom])
274
-
275
- // ---- Follow the bottom while the intent is armed -----------------
276
- // Installs once per mount (all deps are stable). See the
277
- // `followBottomRef` comment above for WHY this exists.
278
- useEffect(() => {
279
- if (!autoScroll) return
280
- const scroller = scrollRef.current
281
- const content = contentRef.current
282
- if (!scroller || !content) return
283
-
284
- // Re-assert on ANY geometry change. Deliberately WITHOUT
285
- // `ignoreEscapes`: the library must stay free to hand control back
286
- // on a real gesture mid-animation. Re-asserting is precisely what
287
- // re-sets its `isAtBottom`, so a silently-lost lock recovers on the
288
- // next resize instead of staying broken for the rest of the turn.
289
- // Cheap to call repeatedly — the library returns the in-flight
290
- // promise when an animation with the same behaviour is running.
291
- const measure = () => {
292
- const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight
293
- setAtBottom(distance <= BOTTOM_THRESHOLD_PX)
294
- }
295
-
296
- const reassert = () => {
297
- if (followBottomRef.current) void scrollToBottom({ animation: 'smooth' })
298
- // Measured on every geometry change, follow armed or not: a
299
- // sibling growing below the scroller moves the bottom without
300
- // any scroll event, and the button has to notice.
301
- measure()
302
- }
303
-
304
- const ro = new ResizeObserver(reassert)
305
- ro.observe(content) // tokens, entity cards, images
306
- ro.observe(scroller) // streaming loader / source chips below it
307
-
308
- // --- user-intent disarm ---------------------------------------
309
- // Only a deliberate move AWAY from the bottom releases the lock.
310
- // Resize-derived scroll events are NOT gestures, which is exactly
311
- // the distinction the library's heuristic gets wrong.
312
- const disarm = () => {
313
- followBottomRef.current = false
314
- }
315
- const onWheel = (e: WheelEvent) => {
316
- if (e.deltaY < 0) disarm()
317
- }
318
- const onKeyDown = (e: KeyboardEvent) => {
319
- if (e.key === 'ArrowUp' || e.key === 'PageUp' || e.key === 'Home') disarm()
320
- }
321
- let touchY: number | null = null
322
- const onTouchStart = (e: TouchEvent) => {
323
- touchY = e.touches[0]?.clientY ?? null
324
- }
325
- const onTouchMove = (e: TouchEvent) => {
326
- const y = e.touches[0]?.clientY
327
- if (y == null || touchY == null) return
328
- if (y > touchY) disarm() // finger travels DOWN → content moves up
329
- touchY = y
330
- }
331
- // Scrollbar drag emits neither wheel nor touch — a scrollTop
332
- // DECREASE while the pointer is held down is the same intent.
333
- //
334
- // ALL pointer listeners are on `window`, deliberately. The scroller is a
335
- // native `overflow-y-auto` div (OverlayScrollbars was reverted in #1512),
336
- // so its native scrollbar IS part of `scroller` and a `pointerdown` on the
337
- // thumb does reach it — but `window` is a safe superset that also survives
338
- // any future host component (or a return to a custom-scrollbar library
339
- // that renders the thumb OUTSIDE the viewport) without stranding
340
- // `pointerDown` at `false` and reintroducing the yank-back-mid-drag bug.
341
- //
342
- // The POINTER listeners use CAPTURE phase, so the guarantee is structural
343
- // rather than contingent on nothing between us and `window` ever calling
344
- // `stopPropagation` on the pointer path.
345
- //
346
- // `blur` deliberately does NOT use capture. Focus events don't bubble
347
- // but they DO capture, so a capture listener on `window` fires for
348
- // EVERY element blur in the document (the focus-delegation idiom) —
349
- // any focus change during a hold, e.g. the chat input blurring as the
350
- // user presses down on the scrollbar, would clear `pointerDown` and
351
- // re-open the very yank-back bug this block prevents. A TRUE window
352
- // blur targets `window` itself, so an at-target (bubble-phase)
353
- // listener still receives it: capture buys nothing and costs
354
- // correctness. Verified in jsdom: descendant blur reaches the capture
355
- // listener only; window blur reaches both.
356
- //
357
- // `pointerDown` must be cleared by every way a drag can END, not just
358
- // `pointerup`: a button released OUTSIDE the window, or the tab losing
359
- // focus mid-drag, delivers no `pointerup` at all and would leave the
360
- // flag stuck true. From then on ANY resize-induced scrollTop clamp — a
361
- // fetch-mode card settling from skeleton to a shorter height, i.e. the
362
- // exact case this whole block exists for — reads as a scroll-up gesture
363
- // and drops the lock. Same for merely HOLDING the button down (text
364
- // selection) while a card shrinks. Hence `pointercancel` + window
365
- // `blur`.
366
- let pointerDown = false
367
- let lastScrollTop = scroller.scrollTop
368
- const onPointerDown = () => {
369
- pointerDown = true
370
- }
371
- // A drag that RELEASES at the live end expresses the same intent as
372
- // clicking jump-to-bottom. The scroll events during that drag can't
373
- // re-arm (see the gate in `onScroll`), and releasing the button emits
374
- // no further `scroll`, so the re-arm is evaluated once here, when the
375
- // gesture is actually over.
376
- const rearmIfAtBottom = () => {
377
- if (followBottomRef.current) return
378
- const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight
379
- if (distance <= BOTTOM_THRESHOLD_PX) followBottomRef.current = true
380
- }
381
- const endPointer = () => {
382
- pointerDown = false
383
- rearmIfAtBottom()
384
- }
385
- const onScroll = () => {
386
- const st = scroller.scrollTop
387
- if (pointerDown && st < lastScrollTop) disarm()
388
- lastScrollTop = st
389
- // Landing at the bottom by hand is the SAME expressed intent as
390
- // clicking jump-to-bottom, so it re-arms a lock an earlier scroll-up
391
- // released. Without it the thread sits at-bottom (button hidden,
392
- // because `atBottom` is true) and then silently drifts away as the
393
- // answer grows.
394
- //
395
- // Gated on the drag being OVER (`!pointerDown`). A mid-drag `scroll`
396
- // is not a gesture that has ended, and a short scrollbar drag that
397
- // starts at the live end stays inside the 70px band — so re-arming
398
- // here would undo the `disarm()` two lines above on the very same
399
- // event and yank the reader back down mid-drag. Releasing the button
400
- // at the bottom still re-arms on the next scroll (or, if none
401
- // follows, on the next deliberate return to the end).
402
- if (
403
- !pointerDown &&
404
- !followBottomRef.current &&
405
- scroller.scrollHeight - st - scroller.clientHeight <= BOTTOM_THRESHOLD_PX
406
- ) {
407
- followBottomRef.current = true
408
- }
409
- measure()
410
- }
411
- measure()
412
-
413
- const winCapture = { capture: true, passive: true } as const
414
-
415
- scroller.addEventListener('wheel', onWheel, { passive: true })
416
- scroller.addEventListener('keydown', onKeyDown)
417
- scroller.addEventListener('touchstart', onTouchStart, { passive: true })
418
- scroller.addEventListener('touchmove', onTouchMove, { passive: true })
419
- window.addEventListener('pointerdown', onPointerDown, winCapture)
420
- scroller.addEventListener('scroll', onScroll, { passive: true })
421
- window.addEventListener('pointerup', endPointer, winCapture)
422
- window.addEventListener('pointercancel', endPointer, winCapture)
423
- // Bubble phase, NOT capture — see the note above.
424
- window.addEventListener('blur', endPointer, { passive: true })
425
-
426
- return () => {
427
- ro.disconnect()
428
- scroller.removeEventListener('wheel', onWheel)
429
- scroller.removeEventListener('keydown', onKeyDown)
430
- scroller.removeEventListener('touchstart', onTouchStart)
431
- scroller.removeEventListener('touchmove', onTouchMove)
432
- // `capture` must match the add call or the listener is not removed.
433
- window.removeEventListener('pointerdown', onPointerDown, { capture: true })
434
- scroller.removeEventListener('scroll', onScroll)
435
- window.removeEventListener('pointerup', endPointer, { capture: true })
436
- window.removeEventListener('pointercancel', endPointer, { capture: true })
437
- // Added WITHOUT capture — the remove must match.
438
- window.removeEventListener('blur', endPointer)
439
- }
440
- }, [autoScroll, scrollRef, contentRef, scrollToBottom])
257
+ }, [autoScroll, messages, dialogId, scrollToBottom, contentRef])
441
258
 
442
259
  // ---- Passive-demo hard pin (pinBottom) ---------------------------
443
260
  // Scripted in-page replays (the Fae/Mingo demos) have no human at the
@@ -567,10 +384,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
567
384
  // `stopScroll` MUST precede scrollIntoView — the imperative scrollTop
568
385
  // write is what the library's geometry tracker uses to flip
569
386
  // `escapedFromLock`, so any in-flight scrollToBottom spring is
570
- // first cancelled. The list-owned follow intent is released for the
571
- // same reason: this turn deliberately parks at the message TOP, so
572
- // its own async growth must NOT drag the reader to the bottom.
573
- followBottomRef.current = false
387
+ // first cancelled.
574
388
  stopScroll()
575
389
  // Instant (not smooth): the markdown body contains async <img>
576
390
  // children whose loads race a 300ms smooth animation and leave
@@ -714,138 +528,75 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
714
528
  !(pendingApprovals && pendingApprovals.length > 0)
715
529
 
716
530
  return (
717
- <div className="flex-1 min-h-0 flex flex-col">
718
- {/* Positioning box for the jump-to-bottom button: it wraps ONLY the
719
- scroller, so `absolute bottom-…` really is the scroller's lower
720
- edge. The outer flex column also holds the streaming loader and the
721
- approvals bar, and anchoring against THAT box put the button over
722
- the loader for the whole of every stream — precisely when it is
723
- most likely to be visible. */}
724
- <div className="relative flex-1 min-h-0 flex flex-col">
531
+ <div className="relative flex-1 min-h-0 flex flex-col">
532
+ <div
533
+ ref={setScrollRef}
534
+ className={cn(
535
+ "flex h-full w-full flex-col overflow-y-auto overflow-x-hidden flex-1",
536
+ // `overscroll-contain` (default ON, opt-out via `overscrollContain={false}`):
537
+ // reaching the top/bottom of the thread must NOT chain the wheel/
538
+ // touch scroll to the page behind the chat — e.g. the company-hub
539
+ // deck (native body scroll + sticky slide panels) would otherwise
540
+ // advance slides while you scroll the chat (fixed in #1501; kept as
541
+ // the prop default here). Passive in-page DEMO chats disable it so
542
+ // the surrounding page keeps scrolling normally when the pointer is
543
+ // over the (non-interactive) thread.
544
+ overscrollContain && "overscroll-contain",
545
+ "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
546
+ className,
547
+ )}
548
+ {...props}
549
+ >
725
550
  <div
726
- ref={setScrollRef}
551
+ ref={setContentRef}
727
552
  className={cn(
728
- "flex h-full w-full flex-col overflow-y-auto overflow-x-hidden flex-1",
729
- // `overscroll-contain` (default ON, opt-out via `overscrollContain={false}`):
730
- // reaching the top/bottom of the thread must NOT chain the wheel/
731
- // touch scroll to the page behind the chat — e.g. the company-hub
732
- // deck (native body scroll + sticky slide panels) would otherwise
733
- // advance slides while you scroll the chat (fixed in #1501; kept as
734
- // the prop default here). Passive in-page DEMO chats disable it so
735
- // the surrounding page keeps scrolling normally when the pointer is
736
- // over the (non-interactive) thread.
737
- overscrollContain && "overscroll-contain",
738
- "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
739
- className,
553
+ // `fullWidth=true` drops the centered-narrow column for
554
+ // side-panel hosts (e.g. multi-platform-hub Mingo). Same
555
+ // semantics as ChatHeader / ChatInput / ChatFooter.
556
+ fullWidth
557
+ ? "flex w-full flex-col pb-[var(--spacing-system-xs)] min-w-0"
558
+ : "mx-auto flex w-full max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)] min-w-0",
559
+ contentClassName ?? "px-[var(--spacing-system-m)]",
740
560
  )}
741
- {...props}
561
+ style={{ minHeight: '100%' }}
742
562
  >
743
- <div
744
- ref={setContentRef}
745
- className={cn(
746
- // `fullWidth=true` drops the centered-narrow column for
747
- // side-panel hosts (e.g. multi-platform-hub Mingo). Same
748
- // semantics as ChatHeader / ChatInput / ChatFooter.
749
- fullWidth
750
- ? "flex w-full flex-col pb-[var(--spacing-system-xs)] min-w-0"
751
- : "mx-auto flex w-full max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)] min-w-0",
752
- contentClassName ?? "px-[var(--spacing-system-m)]",
753
- )}
754
- style={{ minHeight: '100%' }}
755
- >
756
- {hasNextPage && (
757
- <div ref={sentinelRef} className="h-px" />
758
- )}
759
- <div className="flex-1" />
760
- {messages.map((message, index) => {
761
- // Hidden messages (synthetic continuation prompts the host
762
- // injects after an approval card) are part of the API
763
- // conversation history but never render. Skipping here
764
- // keeps the visible thread coherent — see
765
- // `Message.hidden` doc-comment in message.types.ts.
766
- if (message.hidden) return null
767
- // ONE OWNER for the pending turn. Both send paths mint an empty
768
- // assistant placeholder (the accumulation target — it MUST stay
769
- // in state) and flip the phase to 'thinking'. Rendering it here
770
- // too would put an author label with no body in the transcript
771
- // WHILE the footer loader below already represents the same
772
- // pending turn — two owners, and an `aria-live` log that
773
- // announces a bare "Mingo". The footer loader wins (it is
774
- // already `role="status" aria-live="polite"`, sits outside the
775
- // scroller so it can't jitter the thread, and cycles the
776
- // progress phrase), so the placeholder is not rendered AT ALL
777
- // until it carries something visible — removed from the live
778
- // region, not merely hidden. Mirrors `endSseTurn`, which prunes
779
- // an empty trailing assistant AFTER a turn; this closes the same
780
- // window BEFORE the first token. Any non-text segment (tool
781
- // execution, approval card, thinking) counts as visible.
782
- const isEmptyPendingTurn =
783
- index === messages.length - 1 &&
784
- isTyping &&
785
- message.role === 'assistant' &&
786
- !hasNonEmptyContent(message.content) &&
787
- (!Array.isArray(message.content) ||
788
- message.content.every((s) => s.type === 'text'))
789
- if (isEmptyPendingTurn) return null
790
- return (
791
- <ChatMessageEnhanced
792
- key={message.id}
793
- ref={getRegisterMessageEl(message.id)}
794
- role={message.role}
795
- name={message.name}
796
- content={message.content}
797
- timestamp={message.timestamp}
798
- isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
799
- avatar={showAvatars ? message.avatar : null}
800
- showAvatar={showAvatars}
801
- assistantType={message.assistantType || assistantType}
802
- approvalVariant={approvalVariant}
803
- authorType={message.authorType}
804
- assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
805
- chatRefs={message.chatRefs}
806
- contextItems={message.contextItems}
807
- resolveContextIcon={resolveContextIcon}
808
- renderContextItem={renderContextItem}
809
- renderMention={renderMention}
810
- renderEntityCard={renderEntityCard}
811
- NavLinkAnchor={NavLinkAnchor}
812
- />
813
- )
814
- })}
815
- </div>
563
+ {hasNextPage && (
564
+ <div ref={sentinelRef} className="h-px" />
565
+ )}
566
+ <div className="flex-1" />
567
+ {messages.map((message, index) => {
568
+ // Hidden messages (synthetic continuation prompts the host
569
+ // injects after an approval card) are part of the API
570
+ // conversation history but never render. Skipping here
571
+ // keeps the visible thread coherent see
572
+ // `Message.hidden` doc-comment in message.types.ts.
573
+ if (message.hidden) return null
574
+ return (
575
+ <ChatMessageEnhanced
576
+ key={message.id}
577
+ ref={getRegisterMessageEl(message.id)}
578
+ role={message.role}
579
+ name={message.name}
580
+ content={message.content}
581
+ timestamp={message.timestamp}
582
+ isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
583
+ avatar={showAvatars ? message.avatar : null}
584
+ showAvatar={showAvatars}
585
+ assistantType={message.assistantType || assistantType}
586
+ approvalVariant={approvalVariant}
587
+ authorType={message.authorType}
588
+ assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
589
+ chatRefs={message.chatRefs}
590
+ contextItems={message.contextItems}
591
+ resolveContextIcon={resolveContextIcon}
592
+ renderContextItem={renderContextItem}
593
+ renderMention={renderMention}
594
+ renderEntityCard={renderEntityCard}
595
+ NavLinkAnchor={NavLinkAnchor}
596
+ />
597
+ )
598
+ })}
816
599
  </div>
817
-
818
- {/* Jump to bottom. The manual escape hatch from auto-scroll that
819
- every 2026 chat surface ships (AI Elements calls it
820
- `ConversationScrollButton`) and the WCAG 2.2.2 counterpart to
821
- moving content: the reader can always get back to the live
822
- end without hunting for it. Positioned inside the scroller's
823
- OWN `relative` wrapper above, so `bottom` really is the
824
- scroller's lower edge and not the outer column's (which also
825
- carries the streaming loader). It costs the thread no layout
826
- height either way — a sibling would shrink the scroller and
827
- move the very bottom it points at. Hidden while at the bottom
828
- and in passive demo hosts. */}
829
- {autoScroll && !atBottom && messages.length > 0 && (
830
- <button
831
- type="button"
832
- onClick={() => {
833
- // Clicking IS the intent to return to the live end, so it
834
- // re-arms the follow lock a scroll-up had released.
835
- followBottomRef.current = true
836
- void scrollToBottom({ animation: 'smooth' })
837
- }}
838
- aria-label="Scroll to latest message"
839
- className={cn(
840
- "absolute bottom-[var(--spacing-system-s)] left-1/2 -translate-x-1/2 z-10",
841
- "flex items-center justify-center size-9 rounded-full",
842
- "bg-ods-card text-ods-text-primary border border-ods-border shadow-lg",
843
- "hover:bg-ods-bg-hover transition-colors",
844
- )}
845
- >
846
- <Arrow02DownIcon size={16} />
847
- </button>
848
- )}
849
600
  </div>
850
601
 
851
602
  {/* Footer-pinned streaming loader — outside the scroller so it
@@ -1531,12 +1531,6 @@ function EmbeddableChatInner({
1531
1531
  content: m.segments && m.segments.length > 0 ? m.segments : m.content,
1532
1532
  timestamp,
1533
1533
  assistantType: m.role === 'assistant' ? ('mingo' as const) : undefined,
1534
- // `hidden` is load-bearing, NOT cosmetic: it carries synthetic rows
1535
- // (e.g. an auto-continuation directive) that the LLM must see but the
1536
- // reader must not. Dropping it here made the raw directive text render
1537
- // as an ordinary bubble. This field-by-field rebuild has to forward it
1538
- // explicitly — see `Message.hidden` and `chat-message-list`'s skip.
1539
- ...(m.hidden ? { hidden: true } : {}),
1540
1534
  ...(m.chatRefs ? { chatRefs: m.chatRefs } : {}),
1541
1535
  ...(m.scrollAnchor ? { scrollAnchor: m.scrollAnchor } : {}),
1542
1536
  // Forward attached context items so the user bubble renders its chips.
@@ -0,0 +1,44 @@
1
+ "use client"
2
+
3
+ import { forwardRef } from "react"
4
+
5
+ import { cn } from "../../utils/cn"
6
+ import { CompassIcon } from "../icons-v2-generated"
7
+ import type { GuideDisplayProps } from "./types"
8
+
9
+ const GUIDE_LABEL = "OpenFrame Guide"
10
+
11
+ /**
12
+ * `GUIDE` segment — the assistant's how-to/documentation answer, framed as a
13
+ * titled card instead of a bare paragraph.
14
+ *
15
+ * Chrome only: the caller renders the markdown body and passes it as children,
16
+ * so a guide goes through the SAME body pipeline as a text segment
17
+ * (`[card://]` splitting + hoisted entity cards + mention chips). Rendering the
18
+ * raw markdown here instead would bypass that plan and degrade every card
19
+ * marker inside a guide to a bare title.
20
+ */
21
+ const GuideDisplay = forwardRef<HTMLDivElement, GuideDisplayProps>(
22
+ ({ className, children, ...props }, ref) => {
23
+ return (
24
+ <div
25
+ ref={ref}
26
+ className={cn(
27
+ "flex flex-col gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]",
28
+ className
29
+ )}
30
+ {...props}
31
+ >
32
+ <div className="flex items-center gap-[var(--spacing-system-xs)]">
33
+ <CompassIcon size={16} className="shrink-0 text-ods-open-yellow" />
34
+ <span className="text-h5 text-ods-open-yellow">{GUIDE_LABEL}</span>
35
+ </div>
36
+ <div className="min-w-0 w-full break-words text-h4 text-ods-text-primary">{children}</div>
37
+ </div>
38
+ )
39
+ }
40
+ )
41
+
42
+ GuideDisplay.displayName = "GuideDisplay"
43
+
44
+ export { GuideDisplay }