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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/dist/chat-protocol/decode.d.ts +58 -0
  2. package/dist/chat-protocol/decode.d.ts.map +1 -0
  3. package/dist/chat-protocol/encode.d.ts +44 -0
  4. package/dist/chat-protocol/encode.d.ts.map +1 -0
  5. package/dist/chat-protocol/env-flag.d.ts +35 -0
  6. package/dist/chat-protocol/env-flag.d.ts.map +1 -0
  7. package/dist/chat-protocol/events.d.ts +196 -0
  8. package/dist/chat-protocol/events.d.ts.map +1 -0
  9. package/dist/chat-protocol/frames.d.ts +213 -0
  10. package/dist/chat-protocol/frames.d.ts.map +1 -0
  11. package/dist/chat-protocol/index.cjs +494 -0
  12. package/dist/chat-protocol/index.cjs.map +1 -0
  13. package/dist/chat-protocol/index.d.ts +18 -0
  14. package/dist/chat-protocol/index.d.ts.map +1 -0
  15. package/dist/chat-protocol/index.js +478 -0
  16. package/dist/chat-protocol/index.js.map +1 -0
  17. package/dist/chat-protocol/ip-normalize.d.ts +44 -0
  18. package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
  19. package/dist/chat-protocol/nats-decoder.d.ts +26 -0
  20. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
  21. package/dist/{chunk-AJNDDXKC.js → chunk-4NWARV5I.js} +3 -3
  22. package/dist/{chunk-GQX6LFFA.cjs → chunk-5CJR2GZ4.cjs} +42 -42
  23. package/dist/{chunk-GQX6LFFA.cjs.map → chunk-5CJR2GZ4.cjs.map} +1 -1
  24. package/dist/{chunk-EUYIFALO.cjs → chunk-5EYRXSY5.cjs} +14 -14
  25. package/dist/{chunk-EUYIFALO.cjs.map → chunk-5EYRXSY5.cjs.map} +1 -1
  26. package/dist/{chunk-5RXKOGH6.cjs → chunk-5LMGFI4J.cjs} +7011 -4777
  27. package/dist/chunk-5LMGFI4J.cjs.map +1 -0
  28. package/dist/{chunk-KFQXO7QB.js → chunk-6GT557KN.js} +1 -1
  29. package/dist/chunk-6GT557KN.js.map +1 -0
  30. package/dist/{chunk-77DFPD35.js → chunk-BE6DK7QY.js} +12267 -10033
  31. package/dist/chunk-BE6DK7QY.js.map +1 -0
  32. package/dist/{chunk-A7IPON2V.cjs → chunk-BIKOJ4FH.cjs} +5 -5
  33. package/dist/{chunk-A7IPON2V.cjs.map → chunk-BIKOJ4FH.cjs.map} +1 -1
  34. package/dist/{chunk-HQYJINFV.js → chunk-CORIBKHG.js} +4 -4
  35. package/dist/{chunk-YEEHB4T6.cjs → chunk-CSYSYJCF.cjs} +40 -40
  36. package/dist/{chunk-YEEHB4T6.cjs.map → chunk-CSYSYJCF.cjs.map} +1 -1
  37. package/dist/{chunk-SIKCXKGB.cjs → chunk-DSDZ5T4P.cjs} +7 -7
  38. package/dist/{chunk-SIKCXKGB.cjs.map → chunk-DSDZ5T4P.cjs.map} +1 -1
  39. package/dist/{chunk-PNXNWGBY.js → chunk-GTWO5NAY.js} +2 -2
  40. package/dist/{chunk-SCCTJ2UI.js → chunk-HSACATCD.js} +4 -4
  41. package/dist/{chunk-PECGZWQR.cjs → chunk-KP5KRRD6.cjs} +3 -3
  42. package/dist/{chunk-PECGZWQR.cjs.map → chunk-KP5KRRD6.cjs.map} +1 -1
  43. package/dist/{chunk-2B6773QM.cjs → chunk-LILQJ3UZ.cjs} +1 -1
  44. package/dist/chunk-LILQJ3UZ.cjs.map +1 -0
  45. package/dist/{chunk-RZSA6MZL.cjs → chunk-NWX7MMF5.cjs} +15 -15
  46. package/dist/{chunk-RZSA6MZL.cjs.map → chunk-NWX7MMF5.cjs.map} +1 -1
  47. package/dist/{chunk-V3CNNMY3.cjs → chunk-P4WTZU6L.cjs} +18 -18
  48. package/dist/{chunk-V3CNNMY3.cjs.map → chunk-P4WTZU6L.cjs.map} +1 -1
  49. package/dist/{chunk-KLYHZRAD.js → chunk-PB3BTZDU.js} +2 -2
  50. package/dist/{chunk-FZOBQG6M.cjs → chunk-Q6E7VJ4W.cjs} +37 -36
  51. package/dist/chunk-Q6E7VJ4W.cjs.map +1 -0
  52. package/dist/{chunk-EYEIVYYF.cjs → chunk-QSCHCZL3.cjs} +7 -7
  53. package/dist/{chunk-EYEIVYYF.cjs.map → chunk-QSCHCZL3.cjs.map} +1 -1
  54. package/dist/{chunk-YJSKQ2ZV.js → chunk-RQLBHKXN.js} +3 -3
  55. package/dist/{chunk-CWQACI4Z.js → chunk-RW2LPL72.js} +2 -2
  56. package/dist/{chunk-DX6AOHNU.cjs → chunk-S2NHJDKQ.cjs} +77 -77
  57. package/dist/{chunk-DX6AOHNU.cjs.map → chunk-S2NHJDKQ.cjs.map} +1 -1
  58. package/dist/{chunk-AHDJMEEN.js → chunk-T2EGSZFN.js} +9 -8
  59. package/dist/chunk-T2EGSZFN.js.map +1 -0
  60. package/dist/{chunk-S52LJNMU.js → chunk-TKN5667N.js} +13 -13
  61. package/dist/chunk-TKN5667N.js.map +1 -0
  62. package/dist/{chunk-3BFX6HJM.js → chunk-UUYRJPGJ.js} +6 -6
  63. package/dist/{chunk-F6BYUO5U.js → chunk-VHG6AX5V.js} +3 -3
  64. package/dist/{chunk-FOQV6RCO.cjs → chunk-VQ523ORK.cjs} +29 -29
  65. package/dist/{chunk-FOQV6RCO.cjs.map → chunk-VQ523ORK.cjs.map} +1 -1
  66. package/dist/{chunk-PXF5J24T.js → chunk-XQ2B74SI.js} +2 -2
  67. package/dist/{chunk-H7JX2BG7.cjs → chunk-YHOIAA2E.cjs} +76 -76
  68. package/dist/chunk-YHOIAA2E.cjs.map +1 -0
  69. package/dist/{chunk-LLWBPPZ4.js → chunk-YZ2OIYQ3.js} +6 -6
  70. package/dist/components/case-studies/index.cjs +10 -10
  71. package/dist/components/case-studies/index.js +4 -4
  72. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  73. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  74. package/dist/components/chat/hooks/index.d.ts +0 -1
  75. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  76. package/dist/components/chat/hooks/use-chat-history-hydration.d.ts +47 -0
  77. package/dist/components/chat/hooks/use-chat-history-hydration.d.ts.map +1 -0
  78. package/dist/components/chat/hooks/use-chat.d.ts +1 -0
  79. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  80. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  81. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  82. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -53
  83. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  84. package/dist/components/chat/index.cjs +12 -4
  85. package/dist/components/chat/index.cjs.map +1 -1
  86. package/dist/components/chat/index.d.ts +1 -0
  87. package/dist/components/chat/index.d.ts.map +1 -1
  88. package/dist/components/chat/index.js +23 -15
  89. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  90. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  91. package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
  92. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  93. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  94. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  95. package/dist/components/chat/stream/index.d.ts +17 -0
  96. package/dist/components/chat/stream/index.d.ts.map +1 -0
  97. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  98. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  99. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  100. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  101. package/dist/components/chat/types/api.types.d.ts +1 -81
  102. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  103. package/dist/components/chat/types/processing.types.d.ts +2 -90
  104. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  105. package/dist/components/chat/types/unified-chat-state.types.d.ts +5 -0
  106. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  107. package/dist/components/chat/utils/chat-conversation-storage.d.ts +34 -0
  108. package/dist/components/chat/utils/chat-conversation-storage.d.ts.map +1 -0
  109. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  110. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  111. package/dist/components/chat/utils/index.d.ts +2 -3
  112. package/dist/components/chat/utils/index.d.ts.map +1 -1
  113. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  114. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  115. package/dist/components/contact/index.cjs +5 -5
  116. package/dist/components/contact/index.js +4 -4
  117. package/dist/components/docs/index.cjs +7 -7
  118. package/dist/components/docs/index.js +6 -6
  119. package/dist/components/embeds/index.cjs +5 -5
  120. package/dist/components/embeds/index.js +4 -4
  121. package/dist/components/faq/index.cjs +6 -6
  122. package/dist/components/faq/index.js +5 -5
  123. package/dist/components/features/index.cjs +4 -4
  124. package/dist/components/features/index.js +3 -3
  125. package/dist/components/help-center-pages/index.cjs +23 -23
  126. package/dist/components/help-center-pages/index.js +14 -14
  127. package/dist/components/index.cjs +182 -140
  128. package/dist/components/index.cjs.map +1 -1
  129. package/dist/components/index.js +66 -24
  130. package/dist/components/index.js.map +1 -1
  131. package/dist/components/navigation/index.cjs +4 -4
  132. package/dist/components/navigation/index.js +3 -3
  133. package/dist/components/onboarding-guides/index.cjs +8 -8
  134. package/dist/components/onboarding-guides/index.js +7 -7
  135. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  136. package/dist/components/related-content/index.cjs +6 -6
  137. package/dist/components/related-content/index.js +5 -5
  138. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  139. package/dist/components/tickets/index.cjs +7 -7
  140. package/dist/components/tickets/index.js +6 -6
  141. package/dist/components/ui/index.cjs +46 -4
  142. package/dist/components/ui/index.cjs.map +1 -1
  143. package/dist/components/ui/index.d.ts +1 -2
  144. package/dist/components/ui/index.d.ts.map +1 -1
  145. package/dist/components/ui/index.js +57 -15
  146. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  147. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  148. package/dist/components/ui/markdown/engine.d.ts +99 -0
  149. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  150. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  151. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  152. package/dist/components/ui/markdown/index.d.ts +18 -0
  153. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  154. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  155. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  156. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  157. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  158. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  159. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  160. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  161. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  162. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  163. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  164. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  165. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  166. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  167. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  168. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  169. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  170. package/dist/contexts/chat-runtime-context.d.ts +8 -0
  171. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  172. package/dist/contexts/index.cjs +2 -2
  173. package/dist/contexts/index.js +1 -1
  174. package/dist/hooks/index.cjs +3 -3
  175. package/dist/hooks/index.js +2 -2
  176. package/dist/index.cjs +62 -4
  177. package/dist/index.cjs.map +1 -1
  178. package/dist/index.js +73 -15
  179. package/dist/utils/index.cjs +169 -40
  180. package/dist/utils/index.cjs.map +1 -1
  181. package/dist/utils/index.d.ts +1 -0
  182. package/dist/utils/index.d.ts.map +1 -1
  183. package/dist/utils/index.js +162 -41
  184. package/dist/utils/index.js.map +1 -1
  185. package/dist/utils/markdown-fences.d.ts +42 -0
  186. package/dist/utils/markdown-fences.d.ts.map +1 -0
  187. package/dist/utils/markdown-heading-id.d.ts +85 -0
  188. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  189. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  190. package/package.json +7 -1
  191. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +307 -0
  192. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  193. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  194. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  195. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +228 -0
  196. package/src/chat-protocol/decode.ts +278 -0
  197. package/src/chat-protocol/encode.ts +71 -0
  198. package/src/chat-protocol/env-flag.ts +39 -0
  199. package/src/chat-protocol/events.ts +232 -0
  200. package/src/chat-protocol/frames.ts +245 -0
  201. package/src/chat-protocol/index.ts +21 -0
  202. package/src/chat-protocol/ip-normalize.ts +146 -0
  203. package/src/chat-protocol/nats-decoder.ts +252 -0
  204. package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
  205. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  206. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +209 -0
  207. package/src/components/chat/chat-message-enhanced.tsx +111 -17
  208. package/src/components/chat/chat-message-list.tsx +358 -109
  209. package/src/components/chat/embeddable-chat.tsx +6 -0
  210. package/src/components/chat/hooks/.index.md +30 -33
  211. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  212. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  213. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  214. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  215. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  216. package/src/components/chat/hooks/index.ts +0 -1
  217. package/src/components/chat/hooks/use-chat-history-hydration.ts +142 -0
  218. package/src/components/chat/hooks/use-chat.ts +20 -0
  219. package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -823
  220. package/src/components/chat/hooks/use-sse-chat-adapter.ts +417 -774
  221. package/src/components/chat/index.ts +5 -0
  222. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +940 -0
  223. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
  224. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
  225. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +894 -0
  226. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  227. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
  228. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  229. package/src/components/chat/stream/chat-stream-reducer.ts +1796 -0
  230. package/src/components/chat/stream/delta-batcher.ts +159 -0
  231. package/src/components/chat/stream/index.ts +55 -0
  232. package/src/components/chat/stream/message-mutations.ts +338 -0
  233. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  234. package/src/components/chat/thinking-display.tsx +1 -1
  235. package/src/components/chat/types/.api.types.md +39 -49
  236. package/src/components/chat/types/.processing.types.md +29 -58
  237. package/src/components/chat/types/api.types.ts +6 -73
  238. package/src/components/chat/types/processing.types.ts +11 -52
  239. package/src/components/chat/types/unified-chat-state.types.ts +6 -0
  240. package/src/components/chat/utils/.index.md +31 -40
  241. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  242. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  243. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +317 -0
  244. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  245. package/src/components/chat/utils/chat-conversation-storage.ts +83 -0
  246. package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
  247. package/src/components/chat/utils/index.ts +5 -8
  248. package/src/components/chat/utils/process-historical-messages.ts +352 -384
  249. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  250. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  251. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  252. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  253. package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
  254. package/src/components/ui/index.ts +1 -2
  255. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
  256. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
  257. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
  258. package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
  259. package/src/components/ui/markdown/base-components.tsx +360 -0
  260. package/src/components/ui/markdown/engine.tsx +315 -0
  261. package/src/components/ui/markdown/heading-ids.ts +239 -0
  262. package/src/components/ui/markdown/index.ts +49 -0
  263. package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
  264. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  265. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  266. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  267. package/src/components/ui/markdown/sanitize.ts +3499 -0
  268. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  269. package/src/components/ui/markdown/streaming.ts +362 -0
  270. package/src/components/ui/markdown/text-size.ts +106 -0
  271. package/src/components/ui/release-changelog-section.tsx +1 -1
  272. package/src/components/ui/ticket-info-section.tsx +1 -1
  273. package/src/contexts/chat-runtime-context.tsx +8 -0
  274. package/src/utils/index.ts +1 -0
  275. package/src/utils/markdown-fences.ts +127 -0
  276. package/src/utils/markdown-heading-id.ts +348 -0
  277. package/src/utils/markdown-section-extractor.ts +41 -56
  278. package/dist/chunk-2B6773QM.cjs.map +0 -1
  279. package/dist/chunk-5RXKOGH6.cjs.map +0 -1
  280. package/dist/chunk-77DFPD35.js.map +0 -1
  281. package/dist/chunk-AHDJMEEN.js.map +0 -1
  282. package/dist/chunk-FZOBQG6M.cjs.map +0 -1
  283. package/dist/chunk-H7JX2BG7.cjs.map +0 -1
  284. package/dist/chunk-KFQXO7QB.js.map +0 -1
  285. package/dist/chunk-S52LJNMU.js.map +0 -1
  286. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  287. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  288. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  289. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  290. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  291. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  292. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  293. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  294. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  295. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -479
  296. package/src/components/chat/utils/.chunk-parser.md +0 -62
  297. package/src/components/chat/utils/chunk-parser.ts +0 -256
  298. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  299. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  300. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  301. /package/dist/{chunk-AJNDDXKC.js.map → chunk-4NWARV5I.js.map} +0 -0
  302. /package/dist/{chunk-HQYJINFV.js.map → chunk-CORIBKHG.js.map} +0 -0
  303. /package/dist/{chunk-PNXNWGBY.js.map → chunk-GTWO5NAY.js.map} +0 -0
  304. /package/dist/{chunk-SCCTJ2UI.js.map → chunk-HSACATCD.js.map} +0 -0
  305. /package/dist/{chunk-KLYHZRAD.js.map → chunk-PB3BTZDU.js.map} +0 -0
  306. /package/dist/{chunk-YJSKQ2ZV.js.map → chunk-RQLBHKXN.js.map} +0 -0
  307. /package/dist/{chunk-CWQACI4Z.js.map → chunk-RW2LPL72.js.map} +0 -0
  308. /package/dist/{chunk-3BFX6HJM.js.map → chunk-UUYRJPGJ.js.map} +0 -0
  309. /package/dist/{chunk-F6BYUO5U.js.map → chunk-VHG6AX5V.js.map} +0 -0
  310. /package/dist/{chunk-PXF5J24T.js.map → chunk-XQ2B74SI.js.map} +0 -0
  311. /package/dist/{chunk-LLWBPPZ4.js.map → chunk-YZ2OIYQ3.js.map} +0 -0
@@ -1,71 +1,52 @@
1
- <!-- source-hash: 3fbde71f6e5fc7f9b4576423ebdb08e2 -->
2
- Thin wrapper that wires together NATS subscriptions, real-time chunk processing, and dialog management into a single `UnifiedChatState` object for the Mingo/NATS transport mode of the unified chat surface.
1
+ <!-- source-hash: 6c5780e92306ea381bac35aaf97c435f -->
2
+ Provides the NATS/Mingo-mode transport adapter for the unified chat surface, implementing the `UnifiedChatState` contract so the `useChat({ mode })` dispatcher can switch between NATS and SSE adapters without shell-side branching.
3
3
 
4
4
  ## Key Components
5
5
 
6
6
  ### `UseNatsChatAdapterConfig`
7
- Primary configuration interface with two operational modes:
8
- - **Bare-transport mode** — caller supplies `dialogId` explicitly and owns dialog lifecycle (original v0 API, used by Tauri Fae Chat)
9
- - **Managed-dialog mode** — caller supplies `fetchDialogs` and related callbacks; the adapter owns dialog state, pagination, and history
7
+ Consumer-supplied configuration interface covering:
8
+ - `dialogId` active conversation identifier
9
+ - `getNatsWsUrl` factory returning the NATS WebSocket URL
10
+ - `publishUserMessage` — consumer-owned send callback (HTTP POST or NATS publish)
11
+ - `fetchChunks` — optional historical chunk fetcher for catchup
12
+ - `clientConfig` — optional NATS auth credentials
13
+ - `enableThinking` / `batchApprovalsEnabled` — processor feature flags
10
14
 
11
- ### Core Config Fields
15
+ ### `UseNatsChatAdapterOptions`
16
+ - `active` — gates the live subscription; keeps message state alive while the mode is hidden
12
17
 
13
- | Field | Required | Description |
14
- |-------|----------|-------------|
15
- | `getNatsWsUrl` | ✅ | Returns the NATS WebSocket URL or `null` to suspend |
16
- | `publishUserMessage` | ✅ | Consumer-owned send callback (HTTP POST or NATS publish) |
17
- | `dialogId` | ❌ | Explicit dialog ID (bare-transport mode) |
18
- | `fetchDialogs` | ❌ | Enables managed-dialog mode with sidebar support |
19
- | `fetchDialogMessages` | ❌ | Populates history on dialog switch |
20
- | `topics` | ❌ | NATS subject suffixes to tail (default: `['message']`) |
21
-
22
- ### Supporting Interfaces
23
-
24
- - **`FetchDialogsParams`** / **`FetchDialogsResult`** — paginated dialog list with cursor-based pagination
25
- - **`FetchDialogMessagesParams`** / **`FetchDialogMessagesResult`** — paginated message history with optional `tokenUsage`
26
-
27
- ### Composition Stack
18
+ ### `useNatsChatAdapter(config, options)`
19
+ Main hook. Composes three internal hooks:
28
20
 
29
21
  ```text
30
- useNatsDialogSubscription → live NATS event tail
31
-
32
- useRealtimeChunkProcessor chunk → MessageSegment[]
33
-
34
- useChunkCatchup → back-fills missed chunks on reconnect
35
-
36
- config.publishUserMessage → consumer-owned send (HTTP/NATS)
22
+ useNatsDialogSubscription → live NATS event tail
23
+
24
+ decodeNatsChunk + reducer chunk → ChatStreamEvent → state
25
+
26
+ useChunkCatchup → back-fills missed chunks on reconnect/activation
37
27
  ```
38
28
 
29
+ Returns a full `UnifiedChatState` object including `messages`, `isLoading`, `streamingPhase`, `sendMessage`, `stopMessage`, and `clearMessages`.
30
+
39
31
  ## Usage Example
40
32
 
41
33
  ```typescript
42
- // Bare-transport mode (v0 — Tauri Fae Chat)
43
- const chatState = useNatsChatAdapter({
44
- dialogId: activeDialogId,
45
- getNatsWsUrl: () => `wss://nats.example.com`,
46
- publishUserMessage: async (text, { dialogId }) => {
47
- await fetch(`/api/chat/${dialogId}/message`, {
48
- method: 'POST',
49
- body: JSON.stringify({ text }),
50
- })
34
+ const chatState = useNatsChatAdapter(
35
+ {
36
+ dialogId: 'dialog-abc123',
37
+ getNatsWsUrl: () => `wss://nats.example.com`,
38
+ publishUserMessage: async (text, { dialogId }) => {
39
+ await fetch('/api/chat', {
40
+ method: 'POST',
41
+ body: JSON.stringify({ text, dialogId }),
42
+ })
43
+ },
44
+ fetchChunks: ({ dialogId, after }) =>
45
+ fetch(`/api/chunks?dialogId=${dialogId}&after=${after}`).then(r => r.json()),
46
+ batchApprovalsEnabled: true,
51
47
  },
52
- topics: ['admin-message'],
53
- })
54
-
55
- // Managed-dialog mode (openframe-frontend / EmbeddableChat)
56
- const chatState = useNatsChatAdapter({
57
- getNatsWsUrl: () => natsUrl,
58
- publishUserMessage: sendMessage,
59
- fetchDialogs: async ({ cursor, limit }) => api.listDialogs({ cursor, limit }),
60
- fetchDialogMessages: async ({ dialogId, cursor }) =>
61
- api.getMessages(dialogId, { cursor }),
62
- createDialog: () => api.createDialog(),
63
- deleteDialog: (id) => api.deleteDialog(id),
64
- approveRequest: (requestId) => api.approve(requestId),
65
- rejectRequest: (requestId, reason) => api.reject(requestId, reason),
66
- })
48
+ { active: isNatsModeVisible },
49
+ )
67
50
  ```
68
51
 
69
- > **Note:** Set `topics: ['admin-message']` for Mingo/admin deployments. The default `'message'` topic is for Fae client-chat. Mismatched topics cause the assistant to hang in the `thinking` phase indefinitely.
70
-
71
- **Source:** [`use-nats-chat-adapter.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/use-nats-chat-adapter.ts)
52
+ > **Note:** `publishUserMessage` is intentionally consumer-owned the adapter only handles optimistic UI updates and phase transitions; the actual transport is wired externally.
@@ -0,0 +1,37 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`server-history hydration (materializes through the reducer) > rebuilds messages + sendIdx-keyed refs + send counter from the transcript 1`] = `
4
+ [
5
+ {
6
+ "content": "open a ticket for the broken printer",
7
+ "id": "hydrated-1",
8
+ "role": "user",
9
+ },
10
+ {
11
+ "chatRefs": {
12
+ "ticket:77": {
13
+ "id": "77",
14
+ "metadata": {
15
+ "status": "OPEN",
16
+ },
17
+ "title": "Broken printer",
18
+ "type": "ticket",
19
+ "url": null,
20
+ },
21
+ },
22
+ "content": "✅ Approved — ticket created: [card://ticket:77]",
23
+ "id": "hydrated-2",
24
+ "role": "assistant",
25
+ },
26
+ {
27
+ "content": "what is the pricing?",
28
+ "id": "hydrated-3",
29
+ "role": "user",
30
+ },
31
+ {
32
+ "content": "Pricing starts at $99/month per site.",
33
+ "id": "hydrated-4",
34
+ "role": "assistant",
35
+ },
36
+ ]
37
+ `;
@@ -0,0 +1,438 @@
1
+ /**
2
+ * GOLDEN CONTRACT TESTS — server-issued conversation ids + server-side
3
+ * history hydration in the SSE adapter.
4
+ *
5
+ * Successor to the retired `persisted-state-v1-golden.test.ts`: the
6
+ * PersistedChatState v1 full-history localStorage blob is DELETED by design
7
+ * (memory `project_chat_server_history_ssot`). The contract pinned here is
8
+ * its replacement:
9
+ *
10
+ * - conversation ids are SERVER-minted, captured from the leading
11
+ * metadata frame, and localStorage stores ONLY the echoed id
12
+ * (`mingo-chat-<source>.conversation`);
13
+ * - the wire carries ONLY the newest user message (+ the echoed
14
+ * conversation id) — never a replay of client history;
15
+ * - mount-time history hydrates from `GET <chatStreamUrl>/history` and
16
+ * materializes THROUGH THE REDUCER (messages, sendIdx-keyed refs, send
17
+ * counter — same fan-out lookup as live turns);
18
+ * - stale keys — other proxy-auth identities AND the retired v1
19
+ * full-history blobs — are swept on mount; a leftover v1 blob neither
20
+ * crashes the adapter nor resurrects its messages.
21
+ */
22
+
23
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
24
+ import { renderHook, act, waitFor } from '@testing-library/react'
25
+ import { createElement, type ReactNode } from 'react'
26
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
27
+ import { useSseChatAdapter } from '../use-sse-chat-adapter'
28
+ import { ChatRuntimeContext, type ChatRuntime } from '../../../../contexts/chat-runtime-context'
29
+ import { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../../utils/auto-continuation-directive'
30
+ import {
31
+ createChatConversationStorage,
32
+ pruneStaleChatConversationStorage,
33
+ } from '../../utils/chat-conversation-storage'
34
+
35
+ const SOURCE = 'goldensrc'
36
+ /** `createLocalStorageAdapter` key shape: `<namespace>.<key>`. */
37
+ const CONVERSATION_KEY = `mingo-chat-${SOURCE}.conversation`
38
+ /** Retired v1 full-history key (pre-server-SSOT design). */
39
+ const V1_KEY = `mingo-chat-${SOURCE}-v1`
40
+
41
+ const runtime: ChatRuntime = {
42
+ endpoints: {
43
+ chatStreamUrl: '/api/docs/chat',
44
+ approvalToolUrl: '/api/chat/agent/confirm-tool',
45
+ commandsUrl: '/api/docs/commands',
46
+ buildListUrl: () => null,
47
+ attachmentUploadUrl: '/api/storage/generate-upload-url',
48
+ attachmentViewUrlPrefix: '/api/storage/view/chat-attachments/',
49
+ identityUrl: '/api/chat/identity',
50
+ },
51
+ navigation: { mode: 'host' },
52
+ source: SOURCE,
53
+ }
54
+
55
+ function wrapper({ children }: { children: ReactNode }) {
56
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
57
+ return createElement(
58
+ QueryClientProvider,
59
+ { client: queryClient },
60
+ createElement(ChatRuntimeContext.Provider, { value: runtime }, children),
61
+ )
62
+ }
63
+
64
+ const enc = new TextEncoder()
65
+
66
+ function streamOf(chunks: Array<string | Uint8Array>): ReadableStream<Uint8Array> {
67
+ return new ReadableStream<Uint8Array>({
68
+ start(controller) {
69
+ for (const c of chunks) {
70
+ controller.enqueue(typeof c === 'string' ? enc.encode(c) : c)
71
+ }
72
+ controller.close()
73
+ },
74
+ })
75
+ }
76
+
77
+ /**
78
+ * URL-routing fetch mock:
79
+ * GET …/history → the provided history payload (404 when omitted)
80
+ * GET …/commands → empty registry (adapter degrades silently)
81
+ * POST chat stream → one streamed wire response per call, in order
82
+ * (the LAST entry repeats for any further sends).
83
+ */
84
+ function mockRoutedFetch(options: {
85
+ history?: unknown
86
+ streams: Array<Array<string | Uint8Array>>
87
+ }) {
88
+ let streamCall = 0
89
+ const fetchMock = vi.fn(async (url: string, _init?: RequestInit) => {
90
+ const u = String(url)
91
+ if (u.includes('/history')) {
92
+ if (options.history === undefined) {
93
+ return { ok: false, status: 404 } as unknown as Response
94
+ }
95
+ return {
96
+ ok: true,
97
+ status: 200,
98
+ json: async () => options.history,
99
+ } as unknown as Response
100
+ }
101
+ if (u.includes('/commands')) {
102
+ return {
103
+ ok: true,
104
+ status: 200,
105
+ json: async () => ({ commands: [] }),
106
+ } as unknown as Response
107
+ }
108
+ const chunks = options.streams[Math.min(streamCall, options.streams.length - 1)]
109
+ streamCall += 1
110
+ return { ok: true, status: 200, body: streamOf(chunks) } as unknown as Response
111
+ })
112
+ vi.stubGlobal('fetch', fetchMock)
113
+ return fetchMock
114
+ }
115
+
116
+ /** POST bodies the mock received for the chat-stream endpoint only. */
117
+ function chatBodies(fetchMock: ReturnType<typeof vi.fn>): Array<Record<string, unknown>> {
118
+ return fetchMock.mock.calls
119
+ .filter(([url]) => String(url).includes('/api/docs/chat') && !String(url).includes('/history'))
120
+ .map(([, init]) => JSON.parse((init as RequestInit).body as string))
121
+ }
122
+
123
+ const USAGE_END_TRAILER =
124
+ '{"kind":"usage","stage":"end","input_tokens":10,"output_tokens":5,"hit_rate_pct":50}'
125
+
126
+ /** Wire fixture: metadata frame echoing a server-minted conversation id. */
127
+ function wireTurn(conversationId: string, answer: string): Array<string> {
128
+ return [
129
+ `{"modelLabel":"Claude Sonnet","provider":"anthropic","conversationId":"${conversationId}"}\0`,
130
+ '\x1E',
131
+ answer,
132
+ '\x1F' + USAGE_END_TRAILER,
133
+ ]
134
+ }
135
+
136
+ /** Server transcript fixture — 2 turns; turn #0's assistant row carries a
137
+ * card ref that must land at sendIdx 0 (same fan-out scheme as live). */
138
+ const HISTORY_PAYLOAD = {
139
+ data: {
140
+ messages: [
141
+ {
142
+ seq: 1,
143
+ role: 'user',
144
+ content: 'open a ticket for the broken printer',
145
+ created_at: '2026-07-19T09:00:00.000Z',
146
+ },
147
+ {
148
+ seq: 2,
149
+ role: 'assistant',
150
+ content: '✅ Approved — ticket created: [card://ticket:77]',
151
+ created_at: '2026-07-19T09:00:12.000Z',
152
+ chat_refs: {
153
+ 'ticket:77': {
154
+ type: 'ticket',
155
+ id: '77',
156
+ title: 'Broken printer',
157
+ url: null,
158
+ metadata: { status: 'OPEN' },
159
+ },
160
+ },
161
+ },
162
+ {
163
+ seq: 3,
164
+ role: 'user',
165
+ content: 'what is the pricing?',
166
+ created_at: '2026-07-19T09:01:40.000Z',
167
+ },
168
+ {
169
+ seq: 4,
170
+ role: 'assistant',
171
+ content: 'Pricing starts at $99/month per site.',
172
+ created_at: '2026-07-19T09:01:45.000Z',
173
+ },
174
+ ],
175
+ },
176
+ }
177
+
178
+ /** The retired v1 blob shape (abbreviated) — must be swept, never read. */
179
+ const V1_BLOB = {
180
+ messages: [
181
+ { id: 'user-1', role: 'user', name: 'You', content: 'old v1 question' },
182
+ {
183
+ id: 'assistant-1',
184
+ role: 'assistant',
185
+ name: 'Mingo AI',
186
+ content: [{ type: 'text', text: 'old v1 answer' }],
187
+ },
188
+ ],
189
+ sources: [],
190
+ refs: [],
191
+ sendCount: 1,
192
+ }
193
+
194
+ const tick = () => new Promise<void>((r) => setTimeout(r, 0))
195
+
196
+ beforeEach(() => {
197
+ window.localStorage.clear()
198
+ vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000)
199
+ })
200
+
201
+ afterEach(() => {
202
+ vi.unstubAllGlobals()
203
+ vi.restoreAllMocks()
204
+ })
205
+
206
+ describe('conversation-id storage — id-only round-trip', () => {
207
+ it('save → load → clear round-trips exactly the conversation id', () => {
208
+ const storage = createChatConversationStorage(SOURCE)
209
+ expect(storage.load()).toBeNull()
210
+ storage.save({ conversationId: 'conv-rt-1' })
211
+ // On-disk shape is the id and NOTHING else — no messages, sources,
212
+ // refs, or send counts ever reach localStorage.
213
+ expect(JSON.parse(window.localStorage.getItem(CONVERSATION_KEY)!)).toEqual({
214
+ conversationId: 'conv-rt-1',
215
+ })
216
+ expect(storage.load()).toEqual({ conversationId: 'conv-rt-1' })
217
+ storage.clear()
218
+ expect(storage.load()).toBeNull()
219
+ expect(window.localStorage.getItem(CONVERSATION_KEY)).toBeNull()
220
+ })
221
+
222
+ it('rejects malformed blobs via the validate gate', () => {
223
+ window.localStorage.setItem(CONVERSATION_KEY, JSON.stringify({ conversationId: '' }))
224
+ expect(createChatConversationStorage(SOURCE).load()).toBeNull()
225
+ window.localStorage.setItem(CONVERSATION_KEY, JSON.stringify({ nope: true }))
226
+ expect(createChatConversationStorage(SOURCE).load()).toBeNull()
227
+ })
228
+ })
229
+
230
+ describe('stale-key pruning', () => {
231
+ it('sweeps other-identity keys and retired v1/full-history shapes, keeps the current key and sibling sources', () => {
232
+ window.localStorage.setItem(CONVERSATION_KEY, JSON.stringify({ conversationId: 'conv-keep' }))
233
+ window.localStorage.setItem(`mingo-chat-${SOURCE}`, 'retired raw shape')
234
+ window.localStorage.setItem(V1_KEY, JSON.stringify(V1_BLOB))
235
+ window.localStorage.setItem(
236
+ `mingo-chat-${SOURCE}-u-someone%40example.com.conversation`,
237
+ JSON.stringify({ conversationId: 'conv-other-identity' }),
238
+ )
239
+ // Hyphen-extended SIBLING source — NOT this source's namespace.
240
+ window.localStorage.setItem(
241
+ `mingo-chat-${SOURCE}-teaser.conversation`,
242
+ JSON.stringify({ conversationId: 'conv-sibling' }),
243
+ )
244
+
245
+ pruneStaleChatConversationStorage(SOURCE)
246
+
247
+ expect(window.localStorage.getItem(CONVERSATION_KEY)).not.toBeNull()
248
+ expect(window.localStorage.getItem(`mingo-chat-${SOURCE}`)).toBeNull()
249
+ expect(window.localStorage.getItem(V1_KEY)).toBeNull()
250
+ expect(
251
+ window.localStorage.getItem(`mingo-chat-${SOURCE}-u-someone%40example.com.conversation`),
252
+ ).toBeNull()
253
+ expect(window.localStorage.getItem(`mingo-chat-${SOURCE}-teaser.conversation`)).not.toBeNull()
254
+ })
255
+ })
256
+
257
+ describe('server-minted conversation id (full hook path)', () => {
258
+ it('captures the echoed id, persists id-only, and sends newest-message-only bodies', async () => {
259
+ const fetchMock = mockRoutedFetch({
260
+ streams: [wireTurn('conv-golden-1', 'First answer.'), wireTurn('conv-golden-1', 'Second answer.')],
261
+ })
262
+ const { result } = renderHook(() => useSseChatAdapter(undefined, { active: false }), {
263
+ wrapper,
264
+ })
265
+
266
+ await act(async () => {
267
+ await result.current.sendMessage('first question')
268
+ })
269
+ // The echoed id — and ONLY the id — is persisted.
270
+ expect(JSON.parse(window.localStorage.getItem(CONVERSATION_KEY)!)).toEqual({
271
+ conversationId: 'conv-golden-1',
272
+ })
273
+
274
+ await act(async () => {
275
+ await result.current.sendMessage('second question')
276
+ })
277
+
278
+ const bodies = chatBodies(fetchMock)
279
+ // First send of the session: no id yet — the server mints one.
280
+ expect(bodies[0]).toEqual({ messages: [{ role: 'user', content: 'first question' }] })
281
+ // Every later send: NEWEST message only + the echoed id. The client
282
+ // thread (4 messages by now) is never replayed on the wire.
283
+ expect(bodies[1]).toEqual({
284
+ messages: [{ role: 'user', content: 'second question' }],
285
+ conversationId: 'conv-golden-1',
286
+ })
287
+ expect(result.current.messages).toHaveLength(4)
288
+ })
289
+
290
+ it('clearMessages drops the stored id so the next send is id-less (fresh conversation)', async () => {
291
+ const fetchMock = mockRoutedFetch({
292
+ streams: [wireTurn('conv-golden-2', 'Answer.'), wireTurn('conv-golden-3', 'Fresh answer.')],
293
+ })
294
+ const { result } = renderHook(() => useSseChatAdapter(undefined, { active: false }), {
295
+ wrapper,
296
+ })
297
+ await act(async () => {
298
+ await result.current.sendMessage('question')
299
+ })
300
+ expect(window.localStorage.getItem(CONVERSATION_KEY)).not.toBeNull()
301
+
302
+ act(() => {
303
+ result.current.clearMessages()
304
+ })
305
+ expect(result.current.messages).toEqual([])
306
+ expect(window.localStorage.getItem(CONVERSATION_KEY)).toBeNull()
307
+
308
+ await act(async () => {
309
+ await result.current.sendMessage('fresh question')
310
+ })
311
+ const bodies = chatBodies(fetchMock)
312
+ expect(bodies[1]).toEqual({ messages: [{ role: 'user', content: 'fresh question' }] })
313
+ // The new conversation's id is re-captured from the fresh stream.
314
+ expect(JSON.parse(window.localStorage.getItem(CONVERSATION_KEY)!)).toEqual({
315
+ conversationId: 'conv-golden-3',
316
+ })
317
+ })
318
+ })
319
+
320
+ describe('server-history hydration (materializes through the reducer)', () => {
321
+ it('rebuilds messages + sendIdx-keyed refs + send counter from the transcript', async () => {
322
+ window.localStorage.setItem(CONVERSATION_KEY, JSON.stringify({ conversationId: 'conv-h1' }))
323
+ const fetchMock = mockRoutedFetch({
324
+ history: HISTORY_PAYLOAD,
325
+ streams: [
326
+ [
327
+ // The live turn after hydration carries sources — they must land
328
+ // at the CONTINUED send index (sendIdx 2), proving the hydrated
329
+ // send counter seeds the reducer's maps.
330
+ '{"sources":[{"index":1,"name":"Pricing overview","path":"docs/pricing.md","documentType":"markdown"}],"conversationId":"conv-h1"}\0',
331
+ '\x1E',
332
+ 'Live answer after hydration.',
333
+ '\x1F' + USAGE_END_TRAILER,
334
+ ],
335
+ ],
336
+ })
337
+ const { result } = renderHook(() => useSseChatAdapter(undefined, { active: true }), {
338
+ wrapper,
339
+ })
340
+
341
+ await waitFor(() => expect(result.current.messages).toHaveLength(4))
342
+ expect(result.current.isHydratingHistory).toBe(false)
343
+
344
+ // History fetched by stored conversation id.
345
+ const historyCall = fetchMock.mock.calls.find(([url]) => String(url).includes('/history'))
346
+ expect(String(historyCall![0])).toContain('conversationId=conv-h1')
347
+
348
+ // Full hydrated projection — messages, roles, hidden flags, refs
349
+ // resolved through the reducer's fan-out lookup (send #0's assistant
350
+ // resolves ticket:77; send #1's does not).
351
+ expect(result.current.messages).toMatchSnapshot()
352
+ const assistants = result.current.messages.filter((m) => m.role === 'assistant')
353
+ expect(assistants[0].chatRefs?.['ticket:77']?.id).toBe('77')
354
+ expect(assistants[1].chatRefs).toBeUndefined()
355
+
356
+ // A live send continues the SAME conversation: newest-message-only body
357
+ // with the stored id, and its sources key at the continued send index.
358
+ await act(async () => {
359
+ await result.current.sendMessage('and the enterprise tier?')
360
+ })
361
+ const bodies = chatBodies(fetchMock)
362
+ expect(bodies[0]).toEqual({
363
+ messages: [{ role: 'user', content: 'and the enterprise tier?' }],
364
+ conversationId: 'conv-h1',
365
+ })
366
+ const liveAssistant = result.current.messages[result.current.messages.length - 1]
367
+ expect(liveAssistant.role).toBe('assistant')
368
+ expect(liveAssistant.sources?.[0]?.name).toBe('Pricing overview')
369
+ })
370
+
371
+ it('marks approval placeholders and auto-continuation directives hidden', async () => {
372
+ window.localStorage.setItem(CONVERSATION_KEY, JSON.stringify({ conversationId: 'conv-h2' }))
373
+ mockRoutedFetch({
374
+ history: {
375
+ data: {
376
+ messages: [
377
+ { seq: 1, role: 'user', content: 'open a ticket', created_at: '2026-07-19T09:00:00.000Z' },
378
+ { seq: 2, role: 'assistant', content: 'Done.', created_at: '2026-07-19T09:00:05.000Z' },
379
+ // Approval placeholder ('') and the server-built directive are
380
+ // LLM history, never rendered.
381
+ { seq: 3, role: 'user', content: '', created_at: '2026-07-19T09:00:10.000Z' },
382
+ {
383
+ seq: 4,
384
+ role: 'user',
385
+ content: `${AUTO_CONTINUATION_DIRECTIVE_PREFIX} ask a follow-up.`,
386
+ created_at: '2026-07-19T09:00:11.000Z',
387
+ },
388
+ ],
389
+ },
390
+ },
391
+ streams: [],
392
+ })
393
+ const { result } = renderHook(() => useSseChatAdapter(undefined, { active: true }), {
394
+ wrapper,
395
+ })
396
+ await waitFor(() => expect(result.current.messages).toHaveLength(4))
397
+ expect(result.current.messages[2].hidden).toBe(true)
398
+ expect(result.current.messages[3].hidden).toBe(true)
399
+ })
400
+
401
+ it('a hydration miss starts the UI empty (server still owns history)', async () => {
402
+ window.localStorage.setItem(CONVERSATION_KEY, JSON.stringify({ conversationId: 'conv-h3' }))
403
+ mockRoutedFetch({ streams: [] }) // history → 404
404
+ const { result } = renderHook(() => useSseChatAdapter(undefined, { active: true }), {
405
+ wrapper,
406
+ })
407
+ await act(async () => {
408
+ await tick()
409
+ await tick()
410
+ })
411
+ expect(result.current.messages).toEqual([])
412
+ expect(result.current.isHydratingHistory).toBe(false)
413
+ })
414
+ })
415
+
416
+ describe('leftover PersistedChatState v1 blobs', () => {
417
+ it('neither crash the adapter nor resurrect messages — the blob is swept on mount', () => {
418
+ window.localStorage.setItem(V1_KEY, JSON.stringify(V1_BLOB))
419
+ mockRoutedFetch({ streams: [] })
420
+ const { result } = renderHook(() => useSseChatAdapter(undefined, { active: false }), {
421
+ wrapper,
422
+ })
423
+ // v1 history is server-side now — the blob is never read back.
424
+ expect(result.current.messages).toEqual([])
425
+ // …and the mount-time sweep removed it.
426
+ expect(window.localStorage.getItem(V1_KEY)).toBeNull()
427
+ })
428
+
429
+ it('a corrupt v1 blob is equally harmless', () => {
430
+ window.localStorage.setItem(V1_KEY, '{not json')
431
+ mockRoutedFetch({ streams: [] })
432
+ const { result } = renderHook(() => useSseChatAdapter(undefined, { active: false }), {
433
+ wrapper,
434
+ })
435
+ expect(result.current.messages).toEqual([])
436
+ expect(window.localStorage.getItem(V1_KEY)).toBeNull()
437
+ })
438
+ })