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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/dist/{chunk-ZHC2BCXK.js → chunk-2OPD6KRB.js} +4 -4
  2. package/dist/{chunk-5BQWVNRS.cjs → chunk-2R25CGSO.cjs} +62 -62
  3. package/dist/{chunk-5BQWVNRS.cjs.map → chunk-2R25CGSO.cjs.map} +1 -1
  4. package/dist/{chunk-242K7LZY.cjs → chunk-4EHQEMN4.cjs} +11 -11
  5. package/dist/{chunk-242K7LZY.cjs.map → chunk-4EHQEMN4.cjs.map} +1 -1
  6. package/dist/{chunk-4KKGHPNE.cjs → chunk-4NIBKEED.cjs} +9 -9
  7. package/dist/{chunk-4KKGHPNE.cjs.map → chunk-4NIBKEED.cjs.map} +1 -1
  8. package/dist/{chunk-5QWYIPMH.js → chunk-5ZL7PA63.js} +2 -2
  9. package/dist/{chunk-3VGRZ62V.js → chunk-6GAJETAP.js} +2 -2
  10. package/dist/{chunk-EO35UHFT.cjs → chunk-A3Z63ZL5.cjs} +60 -60
  11. package/dist/chunk-A3Z63ZL5.cjs.map +1 -0
  12. package/dist/{chunk-34C2JV2C.js → chunk-G6ARA2HD.js} +2 -2
  13. package/dist/{chunk-WNXIG6YY.js → chunk-K5HHLTSG.js} +2 -2
  14. package/dist/{chunk-42NYDEHH.js → chunk-KK4M4URN.js} +2 -2
  15. package/dist/{chunk-42SMPKGY.js → chunk-MF3VJKHT.js} +2 -2
  16. package/dist/{chunk-EORFD5WB.cjs → chunk-MSB3PEPP.cjs} +15 -15
  17. package/dist/{chunk-EORFD5WB.cjs.map → chunk-MSB3PEPP.cjs.map} +1 -1
  18. package/dist/{chunk-F7XPMG25.cjs → chunk-MVORKLWX.cjs} +27 -28
  19. package/dist/chunk-MVORKLWX.cjs.map +1 -0
  20. package/dist/{chunk-D3LUKR2Y.js → chunk-OYPUQJJ2.js} +10044 -12292
  21. package/dist/chunk-OYPUQJJ2.js.map +1 -0
  22. package/dist/{chunk-62TM2RVZ.cjs → chunk-T67MZAQJ.cjs} +26 -26
  23. package/dist/{chunk-62TM2RVZ.cjs.map → chunk-T67MZAQJ.cjs.map} +1 -1
  24. package/dist/{chunk-BUBQEZ5J.js → chunk-VLWVM6ZS.js} +2 -2
  25. package/dist/{chunk-632XJ2ZT.js → chunk-VOOEX4IZ.js} +5 -6
  26. package/dist/chunk-VOOEX4IZ.js.map +1 -0
  27. package/dist/{chunk-5ZY5JM4Z.js → chunk-W4XZ3FBZ.js} +10 -10
  28. package/dist/chunk-W4XZ3FBZ.js.map +1 -0
  29. package/dist/{chunk-2LYA3RBT.cjs → chunk-XI44QDCD.cjs} +37 -37
  30. package/dist/{chunk-2LYA3RBT.cjs.map → chunk-XI44QDCD.cjs.map} +1 -1
  31. package/dist/{chunk-WZCQW4MW.js → chunk-XSNKSMB3.js} +4 -4
  32. package/dist/{chunk-J3NHQ4JB.cjs → chunk-ZBTIN7RC.cjs} +31 -31
  33. package/dist/{chunk-J3NHQ4JB.cjs.map → chunk-ZBTIN7RC.cjs.map} +1 -1
  34. package/dist/{chunk-SS3D3DHL.cjs → chunk-ZEZTITQZ.cjs} +4886 -7134
  35. package/dist/chunk-ZEZTITQZ.cjs.map +1 -0
  36. package/dist/{chunk-44C23SSC.cjs → chunk-ZHNWWFIM.cjs} +5 -5
  37. package/dist/{chunk-44C23SSC.cjs.map → chunk-ZHNWWFIM.cjs.map} +1 -1
  38. package/dist/{chunk-H6GFQ4AO.cjs → chunk-ZTE32A7C.cjs} +7 -7
  39. package/dist/{chunk-H6GFQ4AO.cjs.map → chunk-ZTE32A7C.cjs.map} +1 -1
  40. package/dist/components/case-studies/index.cjs +8 -8
  41. package/dist/components/case-studies/index.js +2 -2
  42. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  43. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  44. package/dist/components/chat/hooks/index.d.ts +1 -0
  45. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
  48. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +48 -15
  49. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  50. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +6 -0
  51. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -0
  52. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +57 -10
  53. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  54. package/dist/components/chat/index.cjs +2 -10
  55. package/dist/components/chat/index.cjs.map +1 -1
  56. package/dist/components/chat/index.d.ts +0 -1
  57. package/dist/components/chat/index.d.ts.map +1 -1
  58. package/dist/components/chat/index.js +13 -21
  59. package/dist/components/chat/types/api.types.d.ts +81 -1
  60. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  61. package/dist/components/chat/types/processing.types.d.ts +93 -2
  62. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  63. package/dist/components/chat/utils/chunk-parser.d.ts +25 -0
  64. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -0
  65. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +6 -34
  66. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  67. package/dist/components/chat/utils/index.d.ts +3 -2
  68. package/dist/components/chat/utils/index.d.ts.map +1 -1
  69. package/dist/components/chat/utils/process-historical-messages.d.ts +11 -59
  70. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  71. package/dist/components/contact/index.cjs +3 -3
  72. package/dist/components/contact/index.js +2 -2
  73. package/dist/components/docs/index.cjs +5 -5
  74. package/dist/components/docs/index.js +4 -4
  75. package/dist/components/embeds/index.cjs +3 -3
  76. package/dist/components/embeds/index.js +2 -2
  77. package/dist/components/faq/index.cjs +3 -3
  78. package/dist/components/faq/index.js +2 -2
  79. package/dist/components/features/index.cjs +2 -2
  80. package/dist/components/features/index.js +1 -1
  81. package/dist/components/help-center-pages/index.cjs +20 -20
  82. package/dist/components/help-center-pages/index.js +11 -11
  83. package/dist/components/index.cjs +130 -172
  84. package/dist/components/index.cjs.map +1 -1
  85. package/dist/components/index.js +21 -63
  86. package/dist/components/index.js.map +1 -1
  87. package/dist/components/navigation/header.d.ts.map +1 -1
  88. package/dist/components/navigation/index.cjs +2 -2
  89. package/dist/components/navigation/index.js +1 -1
  90. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  91. package/dist/components/onboarding-guides/index.cjs +5 -5
  92. package/dist/components/onboarding-guides/index.js +4 -4
  93. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  94. package/dist/components/related-content/index.cjs +3 -3
  95. package/dist/components/related-content/index.js +2 -2
  96. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  97. package/dist/components/tickets/index.cjs +5 -5
  98. package/dist/components/tickets/index.js +4 -4
  99. package/dist/components/ui/index.cjs +2 -44
  100. package/dist/components/ui/index.cjs.map +1 -1
  101. package/dist/components/ui/index.d.ts +2 -1
  102. package/dist/components/ui/index.d.ts.map +1 -1
  103. package/dist/components/ui/index.js +13 -55
  104. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  105. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  106. package/dist/components/ui/simple-markdown-renderer.d.ts +73 -0
  107. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -0
  108. package/dist/index.cjs +2 -60
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.js +13 -71
  111. package/dist/utils/index.cjs +40 -169
  112. package/dist/utils/index.cjs.map +1 -1
  113. package/dist/utils/index.d.ts +0 -1
  114. package/dist/utils/index.d.ts.map +1 -1
  115. package/dist/utils/index.js +41 -162
  116. package/dist/utils/index.js.map +1 -1
  117. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  118. package/package.json +1 -7
  119. package/src/components/chat/__tests__/chat-message-list.test.tsx +6 -381
  120. package/src/components/chat/chat-message-enhanced.tsx +20 -122
  121. package/src/components/chat/chat-message-list.tsx +132 -409
  122. package/src/components/chat/embeddable-chat.tsx +0 -6
  123. package/src/components/chat/hooks/.index.md +33 -30
  124. package/src/components/chat/hooks/.use-nats-chat-adapter.md +55 -36
  125. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +77 -0
  126. package/src/components/chat/hooks/index.ts +1 -0
  127. package/src/components/chat/hooks/use-chat.ts +0 -8
  128. package/src/components/chat/hooks/use-chunk-catchup.ts +1 -26
  129. package/src/components/chat/hooks/use-nats-chat-adapter.ts +825 -254
  130. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +489 -0
  131. package/src/components/chat/hooks/use-sse-chat-adapter.ts +684 -380
  132. package/src/components/chat/index.ts +0 -5
  133. package/src/components/chat/thinking-display.tsx +1 -1
  134. package/src/components/chat/types/.api.types.md +49 -39
  135. package/src/components/chat/types/.processing.types.md +58 -29
  136. package/src/components/chat/types/api.types.ts +73 -6
  137. package/src/components/chat/types/processing.types.ts +53 -11
  138. package/src/components/chat/utils/.chunk-parser.md +62 -0
  139. package/src/components/chat/utils/.index.md +40 -31
  140. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +0 -61
  141. package/src/components/chat/utils/chunk-parser.ts +262 -0
  142. package/src/components/chat/utils/extract-incomplete-message-state.ts +5 -63
  143. package/src/components/chat/utils/index.ts +8 -5
  144. package/src/components/chat/utils/process-historical-messages.ts +386 -358
  145. package/src/components/navigation/header.tsx +2 -1
  146. package/src/components/navigation/mingo-ai-button.tsx +4 -1
  147. package/src/components/navigation/mobile-nav-panel.tsx +5 -5
  148. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +4 -22
  149. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  150. package/src/components/shared/product-release/release-detail-page.tsx +4 -19
  151. package/src/components/ui/.simple-markdown-renderer.md +52 -0
  152. package/src/components/ui/index.ts +2 -1
  153. package/src/components/ui/release-changelog-section.tsx +1 -1
  154. package/src/components/ui/rich-markdown-renderer.tsx +1223 -0
  155. package/src/components/ui/simple-markdown-renderer.tsx +964 -0
  156. package/src/components/ui/ticket-info-section.tsx +1 -1
  157. package/src/utils/index.ts +0 -1
  158. package/src/utils/markdown-section-extractor.ts +56 -41
  159. package/dist/chat-protocol/decode.d.ts +0 -58
  160. package/dist/chat-protocol/decode.d.ts.map +0 -1
  161. package/dist/chat-protocol/encode.d.ts +0 -44
  162. package/dist/chat-protocol/encode.d.ts.map +0 -1
  163. package/dist/chat-protocol/env-flag.d.ts +0 -35
  164. package/dist/chat-protocol/env-flag.d.ts.map +0 -1
  165. package/dist/chat-protocol/events.d.ts +0 -207
  166. package/dist/chat-protocol/events.d.ts.map +0 -1
  167. package/dist/chat-protocol/frames.d.ts +0 -213
  168. package/dist/chat-protocol/frames.d.ts.map +0 -1
  169. package/dist/chat-protocol/index.cjs +0 -500
  170. package/dist/chat-protocol/index.cjs.map +0 -1
  171. package/dist/chat-protocol/index.d.ts +0 -18
  172. package/dist/chat-protocol/index.d.ts.map +0 -1
  173. package/dist/chat-protocol/index.js +0 -484
  174. package/dist/chat-protocol/index.js.map +0 -1
  175. package/dist/chat-protocol/ip-normalize.d.ts +0 -44
  176. package/dist/chat-protocol/ip-normalize.d.ts.map +0 -1
  177. package/dist/chat-protocol/nats-decoder.d.ts +0 -26
  178. package/dist/chat-protocol/nats-decoder.d.ts.map +0 -1
  179. package/dist/chunk-5ZY5JM4Z.js.map +0 -1
  180. package/dist/chunk-632XJ2ZT.js.map +0 -1
  181. package/dist/chunk-D3LUKR2Y.js.map +0 -1
  182. package/dist/chunk-EO35UHFT.cjs.map +0 -1
  183. package/dist/chunk-F7XPMG25.cjs.map +0 -1
  184. package/dist/chunk-SS3D3DHL.cjs.map +0 -1
  185. package/dist/components/chat/stream/chat-dialog-store.d.ts +0 -184
  186. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +0 -1
  187. package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -367
  188. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +0 -1
  189. package/dist/components/chat/stream/delta-batcher.d.ts +0 -51
  190. package/dist/components/chat/stream/delta-batcher.d.ts.map +0 -1
  191. package/dist/components/chat/stream/index.d.ts +0 -17
  192. package/dist/components/chat/stream/index.d.ts.map +0 -1
  193. package/dist/components/chat/stream/message-mutations.d.ts +0 -77
  194. package/dist/components/chat/stream/message-mutations.d.ts.map +0 -1
  195. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +0 -22
  196. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +0 -1
  197. package/dist/components/ui/markdown/base-components.d.ts +0 -51
  198. package/dist/components/ui/markdown/base-components.d.ts.map +0 -1
  199. package/dist/components/ui/markdown/engine.d.ts +0 -99
  200. package/dist/components/ui/markdown/engine.d.ts.map +0 -1
  201. package/dist/components/ui/markdown/heading-ids.d.ts +0 -65
  202. package/dist/components/ui/markdown/heading-ids.d.ts.map +0 -1
  203. package/dist/components/ui/markdown/index.d.ts +0 -18
  204. package/dist/components/ui/markdown/index.d.ts.map +0 -1
  205. package/dist/components/ui/markdown/mermaid-diagram.d.ts +0 -79
  206. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +0 -1
  207. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +0 -14
  208. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +0 -1
  209. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -40
  210. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +0 -1
  211. package/dist/components/ui/markdown/rich/shortcodes.d.ts +0 -8
  212. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +0 -1
  213. package/dist/components/ui/markdown/sanitize.d.ts +0 -153
  214. package/dist/components/ui/markdown/sanitize.d.ts.map +0 -1
  215. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +0 -23
  216. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +0 -1
  217. package/dist/components/ui/markdown/streaming.d.ts +0 -78
  218. package/dist/components/ui/markdown/streaming.d.ts.map +0 -1
  219. package/dist/components/ui/markdown/text-size.d.ts +0 -34
  220. package/dist/components/ui/markdown/text-size.d.ts.map +0 -1
  221. package/dist/utils/markdown-fences.d.ts +0 -42
  222. package/dist/utils/markdown-fences.d.ts.map +0 -1
  223. package/dist/utils/markdown-heading-id.d.ts +0 -85
  224. package/dist/utils/markdown-heading-id.d.ts.map +0 -1
  225. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -317
  226. package/src/chat-protocol/__tests__/chat-protocol.test.ts +0 -516
  227. package/src/chat-protocol/__tests__/env-flag.test.ts +0 -52
  228. package/src/chat-protocol/__tests__/ip-normalize.test.ts +0 -136
  229. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -232
  230. package/src/chat-protocol/decode.ts +0 -278
  231. package/src/chat-protocol/encode.ts +0 -71
  232. package/src/chat-protocol/env-flag.ts +0 -39
  233. package/src/chat-protocol/events.ts +0 -245
  234. package/src/chat-protocol/frames.ts +0 -245
  235. package/src/chat-protocol/index.ts +0 -21
  236. package/src/chat-protocol/ip-normalize.ts +0 -146
  237. package/src/chat-protocol/nats-decoder.ts +0 -258
  238. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -207
  239. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -250
  240. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +0 -37
  241. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  242. package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +0 -92
  243. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +0 -438
  244. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +0 -318
  245. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +0 -940
  246. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +0 -806
  247. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +0 -467
  248. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -1248
  249. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +0 -255
  250. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +0 -105
  251. package/src/components/chat/stream/chat-dialog-store.ts +0 -536
  252. package/src/components/chat/stream/chat-stream-reducer.ts +0 -1876
  253. package/src/components/chat/stream/delta-batcher.ts +0 -168
  254. package/src/components/chat/stream/index.ts +0 -55
  255. package/src/components/chat/stream/message-mutations.ts +0 -340
  256. package/src/components/chat/stream/use-chat-stream-reducer.ts +0 -126
  257. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +0 -420
  258. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +0 -605
  259. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -394
  260. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +0 -270
  261. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +0 -2420
  262. package/src/components/ui/__tests__/markdown-parity.test.tsx +0 -2540
  263. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +0 -176
  264. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +0 -151
  265. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +0 -354
  266. package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +0 -118
  267. package/src/components/ui/markdown/__tests__/streaming.test.tsx +0 -436
  268. package/src/components/ui/markdown/base-components.tsx +0 -360
  269. package/src/components/ui/markdown/engine.tsx +0 -315
  270. package/src/components/ui/markdown/heading-ids.ts +0 -239
  271. package/src/components/ui/markdown/index.ts +0 -49
  272. package/src/components/ui/markdown/mermaid-diagram.tsx +0 -323
  273. package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -199
  274. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +0 -184
  275. package/src/components/ui/markdown/rich/shortcodes.ts +0 -170
  276. package/src/components/ui/markdown/sanitize.ts +0 -3584
  277. package/src/components/ui/markdown/simple-markdown-renderer.tsx +0 -28
  278. package/src/components/ui/markdown/streaming.ts +0 -390
  279. package/src/components/ui/markdown/text-size.ts +0 -106
  280. package/src/utils/markdown-fences.ts +0 -127
  281. package/src/utils/markdown-heading-id.ts +0 -348
  282. /package/dist/{chunk-ZHC2BCXK.js.map → chunk-2OPD6KRB.js.map} +0 -0
  283. /package/dist/{chunk-5QWYIPMH.js.map → chunk-5ZL7PA63.js.map} +0 -0
  284. /package/dist/{chunk-3VGRZ62V.js.map → chunk-6GAJETAP.js.map} +0 -0
  285. /package/dist/{chunk-34C2JV2C.js.map → chunk-G6ARA2HD.js.map} +0 -0
  286. /package/dist/{chunk-WNXIG6YY.js.map → chunk-K5HHLTSG.js.map} +0 -0
  287. /package/dist/{chunk-42NYDEHH.js.map → chunk-KK4M4URN.js.map} +0 -0
  288. /package/dist/{chunk-42SMPKGY.js.map → chunk-MF3VJKHT.js.map} +0 -0
  289. /package/dist/{chunk-BUBQEZ5J.js.map → chunk-VLWVM6ZS.js.map} +0 -0
  290. /package/dist/{chunk-WZCQW4MW.js.map → chunk-XSNKSMB3.js.map} +0 -0
@@ -1,11 +1,11 @@
1
1
  "use client"
2
2
 
3
- import { useRef, useState, useCallback, 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,199 +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
- // `isLoading` IS load-bearing here, even though the body never reads it.
441
- // On the loading branch this component returns the skeleton, so
442
- // `scrollRef.current` is null and the bail-out above fires. With all four
443
- // other deps identity-stable (`autoScroll` plus three `[]`-memoized values
444
- // from `useStickToBottom`), the effect then never ran again: no
445
- // ResizeObserver, no wheel/touch/keyboard escape hatches, `atBottom` stuck
446
- // at its initial `true` so the jump-to-bottom button could not render, and
447
- // the follow lock never re-asserted — for the rest of the component's
448
- // life. A `false → true → false` round trip (host loads another page or
449
- // dialog) also swaps the scroller element, so the stale closure kept
450
- // observing and listening on DETACHED nodes.
451
- }, [autoScroll, scrollRef, contentRef, scrollToBottom, isLoading])
257
+ }, [autoScroll, messages, dialogId, scrollToBottom, contentRef])
452
258
 
453
259
  // ---- Passive-demo hard pin (pinBottom) ---------------------------
454
260
  // Scripted in-page replays (the Fae/Mingo demos) have no human at the
@@ -578,10 +384,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
578
384
  // `stopScroll` MUST precede scrollIntoView — the imperative scrollTop
579
385
  // write is what the library's geometry tracker uses to flip
580
386
  // `escapedFromLock`, so any in-flight scrollToBottom spring is
581
- // first cancelled. The list-owned follow intent is released for the
582
- // same reason: this turn deliberately parks at the message TOP, so
583
- // its own async growth must NOT drag the reader to the bottom.
584
- followBottomRef.current = false
387
+ // first cancelled.
585
388
  stopScroll()
586
389
  // Instant (not smooth): the markdown body contains async <img>
587
390
  // children whose loads race a 300ms smooth animation and leave
@@ -668,46 +471,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
668
471
  // Expose the scroll container ref to parents that need it (rare,
669
472
  // but the existing public contract). Library's `scrollRef` is a
670
473
  // MutableRefObject<HTMLElement> so we cast to the public type.
671
- // `isLoading` for the same reason as the follow effect: on the loading
672
- // branch `scrollRef.current` is null, and without it in the deps a parent
673
- // that mounted this list while loading held `null` forever.
674
- useImperativeHandle(ref, () => scrollRef.current as HTMLDivElement, [scrollRef, isLoading])
675
-
676
- // Ref adapters — MEMOIZED, and declared above the early return so the hook
677
- // order is unconditional (see CLAUDE.md rule 3).
678
- //
679
- // `useStickToBottom` treats these as ref callbacks: React invokes
680
- // `ref(null)` then `ref(el)` whenever the callback IDENTITY changes, and the
681
- // library responds by disconnecting its ResizeObserver and building a fresh
682
- // one (plus removing/re-adding its scroll and wheel listeners). A plain arrow
683
- // per render therefore churned all of that on EVERY committed frame — one per
684
- // batched delta flush, so ~60/s while streaming — and, because the rebuild
685
- // resets the library's `previousHeight` to undefined, it permanently pinned
686
- // the library to its `initial` animation and never used `resize`, inverting
687
- // the policy configured above.
688
- //
689
- // The library types its refs against `HTMLElement` (broader than
690
- // `HTMLDivElement`) and `MutableRefObject` is INVARIANT in its type
691
- // parameter, so a direct assignment into the JSX `Ref<HTMLDivElement>` slot
692
- // fails type-check; a callback is contravariant in its argument, so this
693
- // adapter type-checks AND preserves the library's mount bookkeeping (the
694
- // intersection `MutableRefObject<T> & RefCallback<T>` means calling
695
- // `scrollRef(el)` both writes `scrollRef.current` and runs that bookkeeping).
696
- // The explicit `: void` return is required too: the library's signature
697
- // returns `void | (() => void)` (React 19's optional ref-cleanup contract),
698
- // which the hub's React types reject in a `Ref<T>` slot.
699
- const setScrollRef = useCallback(
700
- (el: HTMLDivElement | null): void => {
701
- scrollRef(el)
702
- },
703
- [scrollRef],
704
- )
705
- const setContentRef = useCallback(
706
- (el: HTMLDivElement | null): void => {
707
- contentRef(el)
708
- },
709
- [contentRef],
710
- )
474
+ useImperativeHandle(ref, () => scrollRef.current as HTMLDivElement, [scrollRef])
711
475
 
712
476
  if (isLoading) {
713
477
  return (
@@ -721,6 +485,28 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
721
485
  )
722
486
  }
723
487
 
488
+ // Adapt the library's refs to React's `Ref<HTMLDivElement>` JSX
489
+ // slot. The library types its refs against `HTMLElement` (broader
490
+ // than `HTMLDivElement`); MutableRefObject is INVARIANT in its
491
+ // type parameter, so a direct assignment fails type-check. The
492
+ // callback form is contravariant in its arg, so a tiny adapter
493
+ // that invokes the library's RefCallback portion type-checks
494
+ // cleanly AND preserves the library's internal observer setup.
495
+ // (The intersection type `MutableRefObject<T> & RefCallback<T>`
496
+ // means calling `scrollRef(el)` writes `el` into `scrollRef.current`
497
+ // AND triggers the library's mount-bookkeeping.)
498
+ // Note the explicit `: void` return type. The library's RefCallback
499
+ // signature returns `void | (() => void)` (React 19's optional ref-
500
+ // cleanup contract). The hub's React types reject that union in a
501
+ // JSX `Ref<T>` slot. Force `void` here — the library doesn't use
502
+ // the cleanup return path in any meaningful way for the public hook.
503
+ const setScrollRef = (el: HTMLDivElement | null): void => {
504
+ scrollRef(el)
505
+ }
506
+ const setContentRef = (el: HTMLDivElement | null): void => {
507
+ contentRef(el)
508
+ }
509
+
724
510
  // Footer-pinned streaming loader. Rendered OUTSIDE the scroller (a
725
511
  // sibling of the scroll div, like the sticky approvals below) so it
726
512
  // stays put while messages stream and grow — no jitter from the
@@ -742,138 +528,75 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
742
528
  !(pendingApprovals && pendingApprovals.length > 0)
743
529
 
744
530
  return (
745
- <div className="flex-1 min-h-0 flex flex-col">
746
- {/* Positioning box for the jump-to-bottom button: it wraps ONLY the
747
- scroller, so `absolute bottom-…` really is the scroller's lower
748
- edge. The outer flex column also holds the streaming loader and the
749
- approvals bar, and anchoring against THAT box put the button over
750
- the loader for the whole of every stream — precisely when it is
751
- most likely to be visible. */}
752
- <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
+ >
753
550
  <div
754
- ref={setScrollRef}
551
+ ref={setContentRef}
755
552
  className={cn(
756
- "flex h-full w-full flex-col overflow-y-auto overflow-x-hidden flex-1",
757
- // `overscroll-contain` (default ON, opt-out via `overscrollContain={false}`):
758
- // reaching the top/bottom of the thread must NOT chain the wheel/
759
- // touch scroll to the page behind the chat — e.g. the company-hub
760
- // deck (native body scroll + sticky slide panels) would otherwise
761
- // advance slides while you scroll the chat (fixed in #1501; kept as
762
- // the prop default here). Passive in-page DEMO chats disable it so
763
- // the surrounding page keeps scrolling normally when the pointer is
764
- // over the (non-interactive) thread.
765
- overscrollContain && "overscroll-contain",
766
- "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
767
- 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)]",
768
560
  )}
769
- {...props}
561
+ style={{ minHeight: '100%' }}
770
562
  >
771
- <div
772
- ref={setContentRef}
773
- className={cn(
774
- // `fullWidth=true` drops the centered-narrow column for
775
- // side-panel hosts (e.g. multi-platform-hub Mingo). Same
776
- // semantics as ChatHeader / ChatInput / ChatFooter.
777
- fullWidth
778
- ? "flex w-full flex-col pb-[var(--spacing-system-xs)] min-w-0"
779
- : "mx-auto flex w-full max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)] min-w-0",
780
- contentClassName ?? "px-[var(--spacing-system-m)]",
781
- )}
782
- style={{ minHeight: '100%' }}
783
- >
784
- {hasNextPage && (
785
- <div ref={sentinelRef} className="h-px" />
786
- )}
787
- <div className="flex-1" />
788
- {messages.map((message, index) => {
789
- // Hidden messages (synthetic continuation prompts the host
790
- // injects after an approval card) are part of the API
791
- // conversation history but never render. Skipping here
792
- // keeps the visible thread coherent — see
793
- // `Message.hidden` doc-comment in message.types.ts.
794
- if (message.hidden) return null
795
- // ONE OWNER for the pending turn. Both send paths mint an empty
796
- // assistant placeholder (the accumulation target — it MUST stay
797
- // in state) and flip the phase to 'thinking'. Rendering it here
798
- // too would put an author label with no body in the transcript
799
- // WHILE the footer loader below already represents the same
800
- // pending turn — two owners, and an `aria-live` log that
801
- // announces a bare "Mingo". The footer loader wins (it is
802
- // already `role="status" aria-live="polite"`, sits outside the
803
- // scroller so it can't jitter the thread, and cycles the
804
- // progress phrase), so the placeholder is not rendered AT ALL
805
- // until it carries something visible — removed from the live
806
- // region, not merely hidden. Mirrors `endSseTurn`, which prunes
807
- // an empty trailing assistant AFTER a turn; this closes the same
808
- // window BEFORE the first token. Any non-text segment (tool
809
- // execution, approval card, thinking) counts as visible.
810
- const isEmptyPendingTurn =
811
- index === messages.length - 1 &&
812
- isTyping &&
813
- message.role === 'assistant' &&
814
- !hasNonEmptyContent(message.content) &&
815
- (!Array.isArray(message.content) ||
816
- message.content.every((s) => s.type === 'text'))
817
- if (isEmptyPendingTurn) return null
818
- return (
819
- <ChatMessageEnhanced
820
- key={message.id}
821
- ref={getRegisterMessageEl(message.id)}
822
- role={message.role}
823
- name={message.name}
824
- content={message.content}
825
- timestamp={message.timestamp}
826
- isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}
827
- avatar={showAvatars ? message.avatar : null}
828
- showAvatar={showAvatars}
829
- assistantType={message.assistantType || assistantType}
830
- approvalVariant={approvalVariant}
831
- authorType={message.authorType}
832
- assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
833
- chatRefs={message.chatRefs}
834
- contextItems={message.contextItems}
835
- resolveContextIcon={resolveContextIcon}
836
- renderContextItem={renderContextItem}
837
- renderMention={renderMention}
838
- renderEntityCard={renderEntityCard}
839
- NavLinkAnchor={NavLinkAnchor}
840
- />
841
- )
842
- })}
843
- </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
+ })}
844
599
  </div>
845
-
846
- {/* Jump to bottom. The manual escape hatch from auto-scroll that
847
- every 2026 chat surface ships (AI Elements calls it
848
- `ConversationScrollButton`) and the WCAG 2.2.2 counterpart to
849
- moving content: the reader can always get back to the live
850
- end without hunting for it. Positioned inside the scroller's
851
- OWN `relative` wrapper above, so `bottom` really is the
852
- scroller's lower edge and not the outer column's (which also
853
- carries the streaming loader). It costs the thread no layout
854
- height either way — a sibling would shrink the scroller and
855
- move the very bottom it points at. Hidden while at the bottom
856
- and in passive demo hosts. */}
857
- {autoScroll && !atBottom && messages.length > 0 && (
858
- <button
859
- type="button"
860
- onClick={() => {
861
- // Clicking IS the intent to return to the live end, so it
862
- // re-arms the follow lock a scroll-up had released.
863
- followBottomRef.current = true
864
- void scrollToBottom({ animation: 'smooth' })
865
- }}
866
- aria-label="Scroll to latest message"
867
- className={cn(
868
- "absolute bottom-[var(--spacing-system-s)] left-1/2 -translate-x-1/2 z-10",
869
- "flex items-center justify-center size-9 rounded-full",
870
- "bg-ods-card text-ods-text-primary border border-ods-border shadow-lg",
871
- "hover:bg-ods-bg-hover transition-colors",
872
- )}
873
- >
874
- <Arrow02DownIcon size={16} />
875
- </button>
876
- )}
877
600
  </div>
878
601
 
879
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.