@flamingo-stack/openframe-frontend-core 0.0.508 → 0.0.509

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 (306) 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 +216 -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 +506 -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 +490 -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-3SQ5KXHQ.cjs → chunk-2T4GTV25.cjs} +9 -9
  22. package/dist/{chunk-3SQ5KXHQ.cjs.map → chunk-2T4GTV25.cjs.map} +1 -1
  23. package/dist/{chunk-CWAOV2FK.cjs → chunk-3CGZPAGR.cjs} +3 -3
  24. package/dist/{chunk-CWAOV2FK.cjs.map → chunk-3CGZPAGR.cjs.map} +1 -1
  25. package/dist/{chunk-TLMJHMXJ.js → chunk-3QSR22IC.js} +12 -12
  26. package/dist/chunk-3QSR22IC.js.map +1 -0
  27. package/dist/{chunk-TWYNR4TZ.cjs → chunk-4G5TLHJD.cjs} +7141 -4799
  28. package/dist/chunk-4G5TLHJD.cjs.map +1 -0
  29. package/dist/{chunk-ADPMHWOE.js → chunk-72XAID7Y.js} +4 -4
  30. package/dist/{chunk-PAGKRNWK.js → chunk-7WZHBQ4J.js} +2 -2
  31. package/dist/{chunk-OCEKO5CW.js → chunk-AEELJC4N.js} +12106 -9764
  32. package/dist/chunk-AEELJC4N.js.map +1 -0
  33. package/dist/{chunk-Z42BGM6Q.cjs → chunk-BT2WWDFP.cjs} +5 -5
  34. package/dist/{chunk-Z42BGM6Q.cjs.map → chunk-BT2WWDFP.cjs.map} +1 -1
  35. package/dist/{chunk-7LEL3RIX.cjs → chunk-C7OY3PQP.cjs} +11 -11
  36. package/dist/{chunk-7LEL3RIX.cjs.map → chunk-C7OY3PQP.cjs.map} +1 -1
  37. package/dist/{chunk-RHGGEKPQ.cjs → chunk-CIOLOQKQ.cjs} +4 -4
  38. package/dist/{chunk-RHGGEKPQ.cjs.map → chunk-CIOLOQKQ.cjs.map} +1 -1
  39. package/dist/{chunk-TSZHM74B.js → chunk-DC2TKS7C.js} +2 -2
  40. package/dist/{chunk-M2GOR3XQ.cjs → chunk-DYPYR7FR.cjs} +87 -87
  41. package/dist/{chunk-M2GOR3XQ.cjs.map → chunk-DYPYR7FR.cjs.map} +1 -1
  42. package/dist/{chunk-I64ABCDX.js → chunk-I2B6X77L.js} +2 -2
  43. package/dist/{chunk-NIZDKTGL.cjs → chunk-K5MSCMDK.cjs} +37 -37
  44. package/dist/{chunk-NIZDKTGL.cjs.map → chunk-K5MSCMDK.cjs.map} +1 -1
  45. package/dist/{chunk-DTYRYB2N.js → chunk-K6QTZ7CZ.js} +2 -2
  46. package/dist/{chunk-4LTMXDUS.js → chunk-KBN7PMFT.js} +2 -2
  47. package/dist/{chunk-TBMV7I5N.js → chunk-KXZO2WZX.js} +2 -2
  48. package/dist/{chunk-QTYZMP6D.cjs → chunk-LBAKLT6T.cjs} +62 -62
  49. package/dist/chunk-LBAKLT6T.cjs.map +1 -0
  50. package/dist/{chunk-KEBYLU3U.js → chunk-LFUPI7VX.js} +2 -2
  51. package/dist/{chunk-LDTR4IVZ.cjs → chunk-M37PBG4N.cjs} +31 -31
  52. package/dist/{chunk-LDTR4IVZ.cjs.map → chunk-M37PBG4N.cjs.map} +1 -1
  53. package/dist/{chunk-WA7RR64F.js → chunk-PO5O654H.js} +6 -5
  54. package/dist/chunk-PO5O654H.js.map +1 -0
  55. package/dist/{chunk-WK4N5VBX.cjs → chunk-QJJISLPA.cjs} +28 -27
  56. package/dist/chunk-QJJISLPA.cjs.map +1 -0
  57. package/dist/{chunk-2F44PLTV.cjs → chunk-T7MCSA55.cjs} +26 -26
  58. package/dist/{chunk-2F44PLTV.cjs.map → chunk-T7MCSA55.cjs.map} +1 -1
  59. package/dist/{chunk-WJQHJD7J.js → chunk-TWSKTJHW.js} +2 -2
  60. package/dist/{chunk-ZSQHZYCO.cjs → chunk-UQDMJ6DH.cjs} +13 -13
  61. package/dist/{chunk-ZSQHZYCO.cjs.map → chunk-UQDMJ6DH.cjs.map} +1 -1
  62. package/dist/{chunk-TZRUCD56.js → chunk-V3ZAAFE3.js} +5 -5
  63. package/dist/{chunk-LRGHJPET.js → chunk-XGIFOHKE.js} +2 -2
  64. package/dist/{chunk-POOMO3PA.cjs → chunk-YQEPRYT5.cjs} +7 -7
  65. package/dist/{chunk-POOMO3PA.cjs.map → chunk-YQEPRYT5.cjs.map} +1 -1
  66. package/dist/components/case-studies/index.cjs +8 -8
  67. package/dist/components/case-studies/index.js +2 -2
  68. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  69. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  70. package/dist/components/chat/hooks/index.d.ts +0 -1
  71. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  72. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  73. package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
  74. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  75. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  76. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
  77. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  78. package/dist/components/chat/index.cjs +10 -2
  79. package/dist/components/chat/index.cjs.map +1 -1
  80. package/dist/components/chat/index.d.ts +1 -0
  81. package/dist/components/chat/index.d.ts.map +1 -1
  82. package/dist/components/chat/index.js +21 -13
  83. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  84. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  85. package/dist/components/chat/stream/chat-stream-reducer.d.ts +380 -0
  86. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  87. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  88. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  89. package/dist/components/chat/stream/index.d.ts +17 -0
  90. package/dist/components/chat/stream/index.d.ts.map +1 -0
  91. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  92. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  93. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  94. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  95. package/dist/components/chat/types/api.types.d.ts +1 -81
  96. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  97. package/dist/components/chat/types/processing.types.d.ts +2 -93
  98. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  99. package/dist/components/chat/types/unified-chat-state.types.d.ts +15 -0
  100. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  101. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  102. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  103. package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
  104. package/dist/components/chat/utils/index.d.ts +2 -3
  105. package/dist/components/chat/utils/index.d.ts.map +1 -1
  106. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  107. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  108. package/dist/components/contact/index.cjs +3 -3
  109. package/dist/components/contact/index.js +2 -2
  110. package/dist/components/docs/index.cjs +5 -5
  111. package/dist/components/docs/index.js +4 -4
  112. package/dist/components/embeds/index.cjs +3 -3
  113. package/dist/components/embeds/index.js +2 -2
  114. package/dist/components/faq/index.cjs +3 -3
  115. package/dist/components/faq/index.js +2 -2
  116. package/dist/components/features/index.cjs +2 -2
  117. package/dist/components/features/index.js +1 -1
  118. package/dist/components/help-center-pages/index.cjs +22 -22
  119. package/dist/components/help-center-pages/index.js +13 -13
  120. package/dist/components/index.cjs +174 -132
  121. package/dist/components/index.cjs.map +1 -1
  122. package/dist/components/index.js +65 -23
  123. package/dist/components/index.js.map +1 -1
  124. package/dist/components/meeting-scheduler/index.cjs +34 -34
  125. package/dist/components/meeting-scheduler/index.js +3 -3
  126. package/dist/components/navigation/index.cjs +2 -2
  127. package/dist/components/navigation/index.js +1 -1
  128. package/dist/components/onboarding-guides/index.cjs +5 -5
  129. package/dist/components/onboarding-guides/index.js +4 -4
  130. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  131. package/dist/components/related-content/index.cjs +3 -3
  132. package/dist/components/related-content/index.js +2 -2
  133. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  134. package/dist/components/tickets/index.cjs +6 -6
  135. package/dist/components/tickets/index.js +5 -5
  136. package/dist/components/ui/index.cjs +44 -2
  137. package/dist/components/ui/index.cjs.map +1 -1
  138. package/dist/components/ui/index.d.ts +1 -2
  139. package/dist/components/ui/index.d.ts.map +1 -1
  140. package/dist/components/ui/index.js +55 -13
  141. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  142. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  143. package/dist/components/ui/markdown/engine.d.ts +99 -0
  144. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  145. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  146. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  147. package/dist/components/ui/markdown/index.d.ts +18 -0
  148. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  149. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  150. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  151. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  152. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  153. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  154. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  155. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  156. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  157. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  158. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  159. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  160. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  161. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  162. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  163. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  164. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  165. package/dist/index.cjs +60 -2
  166. package/dist/index.cjs.map +1 -1
  167. package/dist/index.js +71 -13
  168. package/dist/utils/index.cjs +169 -40
  169. package/dist/utils/index.cjs.map +1 -1
  170. package/dist/utils/index.d.ts +1 -0
  171. package/dist/utils/index.d.ts.map +1 -1
  172. package/dist/utils/index.js +162 -41
  173. package/dist/utils/index.js.map +1 -1
  174. package/dist/utils/markdown-fences.d.ts +42 -0
  175. package/dist/utils/markdown-fences.d.ts.map +1 -0
  176. package/dist/utils/markdown-heading-id.d.ts +85 -0
  177. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  178. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  179. package/package.json +7 -1
  180. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +319 -0
  181. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  182. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  183. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  184. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +237 -0
  185. package/src/chat-protocol/decode.ts +278 -0
  186. package/src/chat-protocol/encode.ts +71 -0
  187. package/src/chat-protocol/env-flag.ts +39 -0
  188. package/src/chat-protocol/events.ts +254 -0
  189. package/src/chat-protocol/frames.ts +245 -0
  190. package/src/chat-protocol/index.ts +21 -0
  191. package/src/chat-protocol/ip-normalize.ts +146 -0
  192. package/src/chat-protocol/nats-decoder.ts +264 -0
  193. package/src/components/chat/.chat-message-list.md +10 -6
  194. package/src/components/chat/__tests__/chat-message-list.test.tsx +450 -9
  195. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  196. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +250 -0
  197. package/src/components/chat/chat-message-enhanced.tsx +122 -20
  198. package/src/components/chat/chat-message-list.tsx +515 -133
  199. package/src/components/chat/embeddable-chat.tsx +6 -0
  200. package/src/components/chat/hooks/.index.md +30 -33
  201. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  202. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  203. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  204. package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +92 -0
  205. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  206. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  207. package/src/components/chat/hooks/index.ts +0 -1
  208. package/src/components/chat/hooks/use-chat.ts +8 -0
  209. package/src/components/chat/hooks/use-chunk-catchup.ts +26 -1
  210. package/src/components/chat/hooks/use-nats-chat-adapter.ts +254 -825
  211. package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
  212. package/src/components/chat/index.ts +5 -0
  213. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +944 -0
  214. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +806 -0
  215. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +467 -0
  216. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +1330 -0
  217. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  218. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +105 -0
  219. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  220. package/src/components/chat/stream/chat-stream-reducer.ts +1928 -0
  221. package/src/components/chat/stream/delta-batcher.ts +168 -0
  222. package/src/components/chat/stream/index.ts +55 -0
  223. package/src/components/chat/stream/message-mutations.ts +340 -0
  224. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  225. package/src/components/chat/thinking-display.tsx +1 -1
  226. package/src/components/chat/types/.api.types.md +39 -49
  227. package/src/components/chat/types/.processing.types.md +29 -58
  228. package/src/components/chat/types/.unified-chat-state.types.md +2 -2
  229. package/src/components/chat/types/api.types.ts +6 -73
  230. package/src/components/chat/types/processing.types.ts +11 -53
  231. package/src/components/chat/types/unified-chat-state.types.ts +16 -0
  232. package/src/components/chat/utils/.extract-incomplete-message-state.md +21 -3
  233. package/src/components/chat/utils/.history-merge.md +44 -3
  234. package/src/components/chat/utils/.index.md +31 -40
  235. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  236. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  237. package/src/components/chat/utils/__tests__/extract-incomplete-message-state.test.ts +106 -0
  238. package/src/components/chat/utils/__tests__/history-merge.test.ts +160 -0
  239. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +61 -0
  240. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
  241. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  242. package/src/components/chat/utils/extract-incomplete-message-state.ts +93 -6
  243. package/src/components/chat/utils/history-merge.ts +85 -5
  244. package/src/components/chat/utils/index.ts +5 -8
  245. package/src/components/chat/utils/process-historical-messages.ts +361 -385
  246. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  247. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  248. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  249. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  250. package/src/components/ui/__tests__/markdown-parity.test.tsx +2540 -0
  251. package/src/components/ui/index.ts +1 -2
  252. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +176 -0
  253. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +151 -0
  254. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +354 -0
  255. package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +118 -0
  256. package/src/components/ui/markdown/__tests__/streaming.test.tsx +436 -0
  257. package/src/components/ui/markdown/base-components.tsx +360 -0
  258. package/src/components/ui/markdown/engine.tsx +315 -0
  259. package/src/components/ui/markdown/heading-ids.ts +239 -0
  260. package/src/components/ui/markdown/index.ts +49 -0
  261. package/src/components/ui/markdown/mermaid-diagram.tsx +323 -0
  262. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  263. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  264. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  265. package/src/components/ui/markdown/sanitize.ts +3584 -0
  266. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  267. package/src/components/ui/markdown/streaming.ts +390 -0
  268. package/src/components/ui/markdown/text-size.ts +106 -0
  269. package/src/components/ui/release-changelog-section.tsx +1 -1
  270. package/src/components/ui/ticket-info-section.tsx +1 -1
  271. package/src/utils/index.ts +1 -0
  272. package/src/utils/markdown-fences.ts +127 -0
  273. package/src/utils/markdown-heading-id.ts +348 -0
  274. package/src/utils/markdown-section-extractor.ts +41 -56
  275. package/dist/chunk-OCEKO5CW.js.map +0 -1
  276. package/dist/chunk-QTYZMP6D.cjs.map +0 -1
  277. package/dist/chunk-TLMJHMXJ.js.map +0 -1
  278. package/dist/chunk-TWYNR4TZ.cjs.map +0 -1
  279. package/dist/chunk-WA7RR64F.js.map +0 -1
  280. package/dist/chunk-WK4N5VBX.cjs.map +0 -1
  281. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  282. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  283. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  284. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  285. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  286. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  287. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  288. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  289. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  290. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
  291. package/src/components/chat/utils/.chunk-parser.md +0 -62
  292. package/src/components/chat/utils/chunk-parser.ts +0 -262
  293. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  294. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  295. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  296. /package/dist/{chunk-ADPMHWOE.js.map → chunk-72XAID7Y.js.map} +0 -0
  297. /package/dist/{chunk-PAGKRNWK.js.map → chunk-7WZHBQ4J.js.map} +0 -0
  298. /package/dist/{chunk-TSZHM74B.js.map → chunk-DC2TKS7C.js.map} +0 -0
  299. /package/dist/{chunk-I64ABCDX.js.map → chunk-I2B6X77L.js.map} +0 -0
  300. /package/dist/{chunk-DTYRYB2N.js.map → chunk-K6QTZ7CZ.js.map} +0 -0
  301. /package/dist/{chunk-4LTMXDUS.js.map → chunk-KBN7PMFT.js.map} +0 -0
  302. /package/dist/{chunk-TBMV7I5N.js.map → chunk-KXZO2WZX.js.map} +0 -0
  303. /package/dist/{chunk-KEBYLU3U.js.map → chunk-LFUPI7VX.js.map} +0 -0
  304. /package/dist/{chunk-WJQHJD7J.js.map → chunk-TWSKTJHW.js.map} +0 -0
  305. /package/dist/{chunk-TZRUCD56.js.map → chunk-V3ZAAFE3.js.map} +0 -0
  306. /package/dist/{chunk-LRGHJPET.js.map → chunk-XGIFOHKE.js.map} +0 -0
@@ -770,3 +770,163 @@ describe('page helpers', () => {
770
770
  expect(maxPersistedStreamSeq(undefined)).toBe(0)
771
771
  })
772
772
  })
773
+
774
+ /**
775
+ * The scenario the reducer's `streamSeq` stamp exists for: a history refetch
776
+ * lands MID-STREAM (a reconnect after the window lost focus fires one). History
777
+ * holds only what the backend has persisted so far — often just the user row —
778
+ * while the store holds several live assistant bubbles of the turn in flight.
779
+ */
780
+ describe('mid-stream refetch (live turn not yet persisted)', () => {
781
+ // Two bubbles of ONE live turn: the backend splits a turn (a preamble, then
782
+ // the tool bubble), so only ONE of them can be `streamingMessageId`.
783
+ const LIVE_A: TestMessage = {
784
+ id: 'assistant-3000-a',
785
+ role: 'assistant',
786
+ content: txt('let me check that'),
787
+ timestamp: t(3000),
788
+ streamSeq: 41,
789
+ }
790
+ const LIVE_B: TestMessage = {
791
+ id: 'assistant-3100-b',
792
+ role: 'assistant',
793
+ content: [tool('exec-7', 'EXECUTING_TOOL')],
794
+ timestamp: t(3100),
795
+ streamSeq: 42,
796
+ }
797
+ // What history returns mid-turn: the user row only. It carries no
798
+ // `lastChunkStreamSeq` (the backend does not stamp user rows).
799
+ const U_LIVE: TestMessage = { id: 'aaaa0009', role: 'user', content: 'check the disk', timestamp: t(2900) }
800
+
801
+ it('keeps every live bubble, not just the streaming one', () => {
802
+ const merged = mergeHistoryWithRealtime({
803
+ processedHistory: [U0, A0, U_LIVE],
804
+ existingMessages: [U0, A0, U_LIVE, LIVE_A, LIVE_B],
805
+ // Only the bubble currently being written is exempt by id — the earlier
806
+ // one of the same turn has to survive on its own seq evidence.
807
+ streamingMessageId: LIVE_B.id,
808
+ // The refetch instant is AFTER both bubbles were created, which is
809
+ // exactly what the wall-clock fallback misreads as "history has them".
810
+ historyFetchedAt: 3500,
811
+ historyMaxStreamSeq: 0,
812
+ realtimeSeenStreamSeq: 42,
813
+ })
814
+
815
+ expect(ids(merged)).toEqual([U0.id, A0.id, U_LIVE.id, LIVE_A.id, LIVE_B.id])
816
+ })
817
+
818
+ it('drops a live bubble once a persisted row of its role reaches its seq', () => {
819
+ // Self-healing side of the same rule: when the turn IS persisted, the
820
+ // synthetic must go or it renders twice.
821
+ const persisted: TestMessage = {
822
+ id: 'aaaa0010',
823
+ role: 'assistant',
824
+ content: txt('let me check that'),
825
+ timestamp: t(3000),
826
+ streamSeq: 41,
827
+ }
828
+ const merged = mergeHistoryWithRealtime({
829
+ processedHistory: [U0, A0, U_LIVE, persisted],
830
+ existingMessages: [U0, A0, U_LIVE, LIVE_A],
831
+ streamingMessageId: null,
832
+ historyFetchedAt: 3500,
833
+ historyMaxStreamSeq: 41,
834
+ realtimeSeenStreamSeq: 41,
835
+ })
836
+
837
+ expect(ids(merged)).toEqual([U0.id, A0.id, U_LIVE.id, persisted.id])
838
+ })
839
+ })
840
+
841
+ /**
842
+ * The other half of the mid-stream refetch: history has begun persisting the
843
+ * turn that is STILL STREAMING. Its snapshot stops at whatever chunk the
844
+ * backend had written, so the two copies disagree — the same tool row reads
845
+ * `pending` in one and `failed` in the other. Ids cannot match here: adoption
846
+ * only happens on a replay, and a refetch mid-stream has none.
847
+ */
848
+ describe('mid-stream refetch (turn partially persisted)', () => {
849
+ const LIVE: TestMessage = {
850
+ id: 'assistant-4000-live',
851
+ role: 'assistant',
852
+ content: [tool('exec-1', 'EXECUTED_TOOL'), tool('exec-2', 'EXECUTED_TOOL'), tool('exec-3', 'EXECUTING_TOOL')],
853
+ timestamp: t(4000),
854
+ streamSeq: 90,
855
+ }
856
+ // History's snapshot of the SAME turn, under its Mongo id, two chunks behind.
857
+ const PERSISTED_PARTIAL: TestMessage = {
858
+ id: 'aaaa0020',
859
+ role: 'assistant',
860
+ content: [tool('exec-1', 'EXECUTED_TOOL'), tool('exec-2', 'EXECUTING_TOOL')],
861
+ timestamp: t(3900),
862
+ streamSeq: 70,
863
+ }
864
+
865
+ it('keeps ONE copy — the live one — when the turn is still streaming', () => {
866
+ const merged = mergeHistoryWithRealtime({
867
+ processedHistory: [U0, A0, PERSISTED_PARTIAL],
868
+ existingMessages: [U0, A0, LIVE],
869
+ streamingMessageId: LIVE.id,
870
+ historyFetchedAt: 4200,
871
+ historyMaxStreamSeq: 70,
872
+ realtimeSeenStreamSeq: 90,
873
+ })
874
+
875
+ expect(ids(merged)).toEqual([U0.id, A0.id, LIVE.id])
876
+ })
877
+
878
+ it('keeps the live copy when it is richer even after the stream ends', () => {
879
+ // No `streamingMessageId` any more, but the turn's own request ids still
880
+ // identify the twin and the live copy still holds more of it.
881
+ const merged = mergeHistoryWithRealtime({
882
+ processedHistory: [U0, A0, PERSISTED_PARTIAL],
883
+ existingMessages: [U0, A0, LIVE],
884
+ streamingMessageId: null,
885
+ historyFetchedAt: 4200,
886
+ historyMaxStreamSeq: 70,
887
+ realtimeSeenStreamSeq: 90,
888
+ })
889
+
890
+ expect(ids(merged)).toEqual([U0.id, A0.id, LIVE.id])
891
+ })
892
+
893
+ it('lets the persisted copy win once it has caught up (self-heals)', () => {
894
+ const persistedFull: TestMessage = {
895
+ ...PERSISTED_PARTIAL,
896
+ content: [tool('exec-1', 'EXECUTED_TOOL'), tool('exec-2', 'EXECUTED_TOOL'), tool('exec-3', 'EXECUTED_TOOL')],
897
+ streamSeq: 95,
898
+ }
899
+ const merged = mergeHistoryWithRealtime({
900
+ processedHistory: [U0, A0, persistedFull],
901
+ existingMessages: [U0, A0, LIVE],
902
+ streamingMessageId: null,
903
+ historyFetchedAt: 4500,
904
+ historyMaxStreamSeq: 95,
905
+ realtimeSeenStreamSeq: 90,
906
+ })
907
+
908
+ expect(ids(merged)).toEqual([U0.id, A0.id, persistedFull.id])
909
+ })
910
+
911
+ it('does not pair unrelated turns that merely sit next to each other', () => {
912
+ // No shared request id → no turn identity → both stand. Guards the widened
913
+ // match from swallowing a genuinely separate turn.
914
+ const unrelatedLive: TestMessage = {
915
+ id: 'assistant-4100-other',
916
+ role: 'assistant',
917
+ content: [tool('exec-77', 'EXECUTING_TOOL')],
918
+ timestamp: t(4100),
919
+ streamSeq: 91,
920
+ }
921
+ const merged = mergeHistoryWithRealtime({
922
+ processedHistory: [U0, A0, PERSISTED_PARTIAL],
923
+ existingMessages: [U0, A0, unrelatedLive],
924
+ streamingMessageId: unrelatedLive.id,
925
+ historyFetchedAt: 4200,
926
+ historyMaxStreamSeq: 70,
927
+ realtimeSeenStreamSeq: 91,
928
+ })
929
+
930
+ expect(ids(merged)).toEqual([U0.id, A0.id, PERSISTED_PARTIAL.id, unrelatedLive.id])
931
+ })
932
+ })
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { describe, it, expect } from 'vitest'
12
12
  import { processHistoricalMessagesWithErrors } from '../process-historical-messages'
13
+ import { createChatStreamReducer } from '../../stream/chat-stream-reducer'
13
14
  import type { HistoricalMessage, MessageSegment } from '../../types'
14
15
 
15
16
  const adminApprovalMessage = (id: string): HistoricalMessage => ({
@@ -95,3 +96,63 @@ describe('processHistoricalMessagesWithErrors — displayApprovalTypes', () => {
95
96
  expect(asst?.streamSeq).toBe(50)
96
97
  })
97
98
  })
99
+
100
+ /**
101
+ * PAIRED-PATH contract. The history processor and the realtime reducer are the
102
+ * two halves of one pipeline, and they DISAGREE by default: history renders
103
+ * every approval type (the deliberate behaviour this file's header argues for),
104
+ * the reducer defaults to `['CLIENT']`. Each half is pinned on its own — above
105
+ * for history, in the reducer's own suite for realtime — but NOTHING asserted
106
+ * the pair, so aligning one default "for consistency" (or a host wiring the
107
+ * option into one call site only) changed observable behaviour with every test
108
+ * still green.
109
+ *
110
+ * These two fixtures state the divergence and its remedy in one place: omitted
111
+ * → the SAME approval is an inline card after a reload and invisible live;
112
+ * explicit list → both paths agree. Read the header for why the asymmetric
113
+ * defaults are the intended trade-off (a `['CLIENT']` history default made
114
+ * pending ADMIN cards vanish on every refetch).
115
+ */
116
+ describe('displayApprovalTypes — history vs realtime default divergence', () => {
117
+ const liveApprovalSegments = (displayApprovalTypes?: string[]) => {
118
+ const effects: Array<{ name: string; args: unknown[] }> = []
119
+ const r = createChatStreamReducer({
120
+ transport: 'nats',
121
+ onEffect: (e) => effects.push(e),
122
+ ...(displayApprovalTypes ? { displayApprovalTypes } : {}),
123
+ })
124
+ r.apply({ type: 'turn-start' })
125
+ r.apply({
126
+ type: 'approval-request',
127
+ requestId: 'req-m1',
128
+ approvalType: 'ADMIN',
129
+ command: 'systemctl restart nats',
130
+ })
131
+ return {
132
+ rendered: r.state.messages.flatMap((m) => approvalSegments(m.segments)),
133
+ escalated: effects.filter((e) => e.name === 'onEscalatedApproval'),
134
+ }
135
+ }
136
+
137
+ it('OMITTED: history renders the ADMIN card inline, realtime escalates it', () => {
138
+ const history = processHistoricalMessagesWithErrors([adminApprovalMessage('m1')])
139
+ expect(history.messages.flatMap((m) => approvalSegments(m.content))).toHaveLength(1)
140
+ expect(history.escalatedApprovals.size).toBe(0)
141
+
142
+ const live = liveApprovalSegments()
143
+ expect(live.rendered).toHaveLength(0)
144
+ expect(live.escalated).toHaveLength(1)
145
+ })
146
+
147
+ it('EXPLICIT list: both paths render the same ADMIN card', () => {
148
+ const types = ['CLIENT', 'ADMIN']
149
+ const history = processHistoricalMessagesWithErrors([adminApprovalMessage('m1')], {
150
+ displayApprovalTypes: types,
151
+ })
152
+ expect(history.messages.flatMap((m) => approvalSegments(m.content))).toHaveLength(1)
153
+
154
+ const live = liveApprovalSegments(types)
155
+ expect(live.rendered).toHaveLength(1)
156
+ expect(live.escalated).toHaveLength(0)
157
+ })
158
+ })
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Phase-0 GOLDEN CHARACTERIZATION TESTS — `processHistoricalMessages`.
3
+ *
4
+ * Pins the CURRENT GraphQL-history → ProcessedMessage[] transformation
5
+ * before the stream-reader unification. A recorded corpus of messageData
6
+ * rows is processed and the FULL output array is snapshotted.
7
+ *
8
+ * NOTE: `process-historical-messages-approvals.test.ts` already guards
9
+ * displayApprovalTypes semantics — this file deliberately does not
10
+ * duplicate it and instead records the full-output shape.
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
14
+ import { processHistoricalMessages, extractErrorMessages, processHistoricalMessagesWithErrors } from '../process-historical-messages'
15
+ import type { HistoricalMessage } from '../../types'
16
+
17
+ // Deterministic Date.now for the `pending-approvals-${Date.now()}` synthetic
18
+ // message id + its `new Date()` timestamp.
19
+ const FIXED_NOW = new Date('2026-07-20T10:00:00.000Z')
20
+
21
+ beforeEach(() => {
22
+ vi.useFakeTimers()
23
+ vi.setSystemTime(FIXED_NOW)
24
+ })
25
+
26
+ afterEach(() => {
27
+ vi.useRealTimers()
28
+ })
29
+
30
+ /** Recorded corpus — one realistic dialog covering every row shape. */
31
+ const CORPUS: HistoricalMessage[] = [
32
+ // 1. User message with contextItems (CLIENT owner).
33
+ {
34
+ id: 'u1',
35
+ chatType: 'CLIENT_CHAT',
36
+ createdAt: '2026-07-19T09:00:00Z',
37
+ owner: { type: 'CLIENT', userId: 'user-42' },
38
+ lastChunkStreamSeq: 10,
39
+ messageData: [
40
+ {
41
+ type: 'TEXT',
42
+ text: 'diagnose this device please',
43
+ // wire shape: [{ type, id }] — label falls back to id
44
+ contextItems: [
45
+ { type: 'device', id: 'dev-1' },
46
+ { type: 'bad' }, // missing id → filtered
47
+ ],
48
+ } as any,
49
+ ],
50
+ },
51
+ // 2. Assistant: thinking + text (grouped into one bubble).
52
+ {
53
+ id: 'a1',
54
+ chatType: 'CLIENT_CHAT',
55
+ createdAt: '2026-07-19T09:00:05Z',
56
+ owner: { type: 'ASSISTANT', model: 'claude-sonnet-x' },
57
+ lastChunkStreamSeq: 20,
58
+ messageData: [
59
+ { type: 'THINKING', text: 'Checking device state… ' },
60
+ { type: 'THINKING', text: 'looks like disk pressure.' },
61
+ { type: 'TEXT', text: 'The device is low on disk. ' },
62
+ ],
63
+ },
64
+ // 3. Assistant continued: EXECUTING + EXECUTED tool pair merges in place.
65
+ {
66
+ id: 'a2',
67
+ chatType: 'CLIENT_CHAT',
68
+ createdAt: '2026-07-19T09:00:10Z',
69
+ owner: { type: 'ASSISTANT' },
70
+ lastChunkStreamSeq: 15, // lower than a1's — MAX (20) must win on the flushed turn
71
+ messageData: [
72
+ {
73
+ type: 'EXECUTING_TOOL',
74
+ integratedToolType: 'TACTICAL_RMM',
75
+ toolFunction: 'run_script',
76
+ title: 'Run cleanup script',
77
+ parameters: { script: 'cleanup.sh' },
78
+ toolExecutionRequestId: 'exec-1',
79
+ },
80
+ {
81
+ type: 'EXECUTED_TOOL',
82
+ integratedToolType: 'TACTICAL_RMM',
83
+ toolFunction: 'run_script',
84
+ parameters: { script: 'cleanup.sh' },
85
+ result: 'Freed 2.3 GB',
86
+ success: true,
87
+ toolExecutionRequestId: 'exec-1',
88
+ },
89
+ { type: 'TEXT', text: 'Cleanup done.' },
90
+ ],
91
+ },
92
+ // 4. ADMIN-owned user message (owner-type author resolution: display name from user).
93
+ {
94
+ id: 'u2',
95
+ chatType: 'CLIENT_CHAT',
96
+ createdAt: '2026-07-19T09:01:00Z',
97
+ owner: {
98
+ type: 'ADMIN',
99
+ user: {
100
+ id: 'admin-7',
101
+ firstName: 'Jane',
102
+ lastName: 'Admin',
103
+ image: { imageUrl: '/avatars/jane.png' },
104
+ },
105
+ },
106
+ messageData: [{ type: 'TEXT', text: 'please restart the broker' }],
107
+ },
108
+ // 5. Assistant: single approval request + result (resolved in-history).
109
+ {
110
+ id: 'a3',
111
+ chatType: 'CLIENT_CHAT',
112
+ createdAt: '2026-07-19T09:01:05Z',
113
+ owner: { type: 'ASSISTANT' },
114
+ lastChunkStreamSeq: 30,
115
+ messageData: [
116
+ { type: 'TEXT', text: 'I need approval to restart it.' },
117
+ {
118
+ type: 'APPROVAL_REQUEST',
119
+ approvalRequestId: 'req-1',
120
+ approvalType: 'CLIENT',
121
+ command: 'systemctl restart nats',
122
+ explanation: 'Restart the message broker',
123
+ },
124
+ {
125
+ type: 'APPROVAL_RESULT',
126
+ approvalRequestId: 'req-1',
127
+ approved: true,
128
+ approvalType: 'CLIENT',
129
+ resolvedByName: 'Jane Admin',
130
+ },
131
+ ],
132
+ },
133
+ // 6. Assistant: batch approval + per-tool executions merged into the batch.
134
+ {
135
+ id: 'a4',
136
+ chatType: 'CLIENT_CHAT',
137
+ createdAt: '2026-07-19T09:02:00Z',
138
+ owner: { type: 'ASSISTANT' },
139
+ messageData: [
140
+ {
141
+ type: 'APPROVAL_REQUEST',
142
+ approvalRequestId: 'batch-1',
143
+ approvalType: 'CLIENT',
144
+ toolCalls: [
145
+ {
146
+ toolExecutionRequestId: 'exec-a',
147
+ toolName: 'create_ticket',
148
+ toolTitle: 'Create ticket',
149
+ requiresApproval: true,
150
+ approvalType: 'CLIENT',
151
+ toolCallArguments: { subject: 'Printer down' },
152
+ },
153
+ {
154
+ toolExecutionRequestId: 'exec-b',
155
+ toolName: 'read_kb',
156
+ requiresApproval: false,
157
+ },
158
+ ],
159
+ },
160
+ {
161
+ type: 'APPROVAL_RESULT',
162
+ approvalRequestId: 'batch-1',
163
+ approved: true,
164
+ resolvedByName: 'Alice Client',
165
+ },
166
+ {
167
+ type: 'EXECUTING_TOOL',
168
+ integratedToolType: 'PSA',
169
+ toolFunction: 'create_ticket',
170
+ toolExecutionRequestId: 'exec-a',
171
+ },
172
+ {
173
+ type: 'EXECUTED_TOOL',
174
+ integratedToolType: 'PSA',
175
+ toolFunction: 'create_ticket',
176
+ result: 'Ticket #77 created',
177
+ success: true,
178
+ toolExecutionRequestId: 'exec-a',
179
+ },
180
+ ],
181
+ },
182
+ // 7. Standalone SYSTEM message row — flushes the assistant turn and
183
+ // renders as a system-authored row.
184
+ {
185
+ id: 's1',
186
+ chatType: 'CLIENT_CHAT',
187
+ createdAt: '2026-07-19T09:03:00Z',
188
+ owner: { type: 'ASSISTANT' },
189
+ lastChunkStreamSeq: 40,
190
+ messageData: [{ type: 'SYSTEM', text: 'Technician joined the chat' } as any],
191
+ },
192
+ // 8. DIRECT_MESSAGE row. CHARACTERIZATION: DIRECT_MESSAGE is a
193
+ // realtime-only concept — the history processors have NO branch for
194
+ // it. This ADMIN-owned row takes the user-message path, which only
195
+ // renders TEXT data, so it contributes NOTHING to the output (the
196
+ // bubble is lost on reload). Recording current behavior as-is; see
197
+ // unification plan.
198
+ {
199
+ id: 'd1',
200
+ chatType: 'CLIENT_CHAT',
201
+ createdAt: '2026-07-19T09:03:30Z',
202
+ owner: { type: 'ADMIN', user: { id: 'admin-7', firstName: 'Jane' } },
203
+ messageData: [{ type: 'DIRECT_MESSAGE', text: 'Human here, taking over' } as any],
204
+ },
205
+ // 9. Error row inside an assistant turn.
206
+ {
207
+ id: 'a5',
208
+ chatType: 'CLIENT_CHAT',
209
+ createdAt: '2026-07-19T09:04:00Z',
210
+ owner: { type: 'ASSISTANT' },
211
+ messageData: [
212
+ { type: 'TEXT', text: 'Attempting one more thing… ' },
213
+ { type: 'ERROR', error: 'Agent crashed', details: '{"error":{"message":"boom"}}' },
214
+ ],
215
+ },
216
+ ]
217
+
218
+ describe('processHistoricalMessages — golden corpus', () => {
219
+ it('processes the full recorded dialog (snapshot)', () => {
220
+ const result = processHistoricalMessages(CORPUS, {
221
+ assistantName: 'Mingo AI',
222
+ assistantType: 'mingo',
223
+ assistantAvatar: '/avatars/mingo.png',
224
+ })
225
+ expect({
226
+ messages: result.messages,
227
+ escalatedApprovals: Array.from(result.escalatedApprovals.entries()),
228
+ }).toMatchSnapshot()
229
+ })
230
+
231
+ it('unresolved single approval is resurrected as a synthetic pending-approvals bubble', () => {
232
+ const result = processHistoricalMessages([
233
+ {
234
+ id: 'a1',
235
+ createdAt: '2026-07-19T09:00:00Z',
236
+ owner: { type: 'ASSISTANT' },
237
+ messageData: [
238
+ {
239
+ type: 'APPROVAL_REQUEST',
240
+ approvalRequestId: 'req-pending',
241
+ approvalType: 'CLIENT',
242
+ command: 'reboot server',
243
+ },
244
+ ],
245
+ },
246
+ ])
247
+ // Date.now is frozen → deterministic synthetic id.
248
+ expect(result.messages).toMatchSnapshot()
249
+ })
250
+
251
+ it('owner-type author resolution: CLIENT vs ADMIN vs ADMIN-without-name display names/avatars', () => {
252
+ const rows: HistoricalMessage[] = [
253
+ {
254
+ id: 'u-client',
255
+ createdAt: '2026-07-19T09:00:00Z',
256
+ owner: { type: 'CLIENT', userId: 'user-1' },
257
+ messageData: [{ type: 'TEXT', text: 'from client' }],
258
+ },
259
+ {
260
+ id: 'u-admin-named',
261
+ createdAt: '2026-07-19T09:00:01Z',
262
+ owner: {
263
+ type: 'ADMIN',
264
+ user: { id: 'adm', firstName: 'Jane', lastName: 'Admin', image: { imageUrl: '/a.png' } },
265
+ },
266
+ messageData: [{ type: 'TEXT', text: 'from named admin' }],
267
+ },
268
+ {
269
+ id: 'u-admin-anon',
270
+ createdAt: '2026-07-19T09:00:02Z',
271
+ owner: { type: 'ADMIN' },
272
+ messageData: [{ type: 'TEXT', text: 'from anonymous admin' }],
273
+ },
274
+ ]
275
+ expect(processHistoricalMessages(rows).messages).toMatchSnapshot()
276
+ })
277
+
278
+ it('chatTypeFilter drops rows of other chat types', () => {
279
+ const rows: HistoricalMessage[] = [
280
+ {
281
+ id: 'u1',
282
+ chatType: 'CLIENT_CHAT',
283
+ createdAt: '2026-07-19T09:00:00Z',
284
+ owner: { type: 'CLIENT' },
285
+ messageData: [{ type: 'TEXT', text: 'visible' }],
286
+ },
287
+ {
288
+ id: 'u2',
289
+ chatType: 'ADMIN_AI_CHAT',
290
+ createdAt: '2026-07-19T09:00:01Z',
291
+ owner: { type: 'CLIENT' },
292
+ messageData: [{ type: 'TEXT', text: 'filtered out' }],
293
+ },
294
+ ]
295
+ expect(
296
+ processHistoricalMessages(rows, { chatTypeFilter: 'CLIENT_CHAT' }).messages,
297
+ ).toMatchSnapshot()
298
+ })
299
+ })
300
+
301
+ describe('extractErrorMessages / processHistoricalMessagesWithErrors — golden', () => {
302
+ it('extractErrorMessages surfaces ERROR rows as standalone error-role messages', () => {
303
+ expect(extractErrorMessages(CORPUS, { assistantName: 'Mingo AI', assistantType: 'mingo' })).toMatchSnapshot()
304
+ })
305
+
306
+ it('processHistoricalMessagesWithErrors output parity on the same corpus (snapshot)', () => {
307
+ const result = processHistoricalMessagesWithErrors(CORPUS, {
308
+ assistantName: 'Mingo AI',
309
+ assistantType: 'mingo',
310
+ assistantAvatar: '/avatars/mingo.png',
311
+ })
312
+ expect({
313
+ messages: result.messages,
314
+ escalatedApprovals: Array.from(result.escalatedApprovals.entries()),
315
+ }).toMatchSnapshot()
316
+ })
317
+ })
318
+
319
+ // ─── GUIDE rows (#1583) ────────────────────────────────────────────────────
320
+ //
321
+ // The realtime path decodes a `GUIDE` chunk into a `guide-delta` event; the
322
+ // history path must decode the persisted `GUIDE` row into the SAME event so a
323
+ // reloaded dialog renders the guide card instead of silently dropping it.
324
+ // Explicit assertions rather than a corpus snapshot — the point is the exact
325
+ // coalescing shape, not the whole-dialog record.
326
+
327
+ describe('processHistoricalMessages — GUIDE rows', () => {
328
+ const guideDialog: HistoricalMessage[] = [
329
+ {
330
+ id: 'u1',
331
+ chatType: 'CLIENT_CHAT',
332
+ createdAt: '2026-07-19T09:00:00Z',
333
+ owner: { type: 'CLIENT', userId: 'user-42' },
334
+ messageData: [{ type: 'TEXT', text: 'how do I enroll a device?' }],
335
+ },
336
+ {
337
+ id: 'a1',
338
+ chatType: 'CLIENT_CHAT',
339
+ createdAt: '2026-07-19T09:00:05Z',
340
+ owner: { type: 'ASSISTANT', model: 'claude-sonnet-x' },
341
+ messageData: [
342
+ { type: 'GUIDE', text: '## Enroll a device\n' },
343
+ { type: 'GUIDE', text: '1. Open Settings' },
344
+ ],
345
+ },
346
+ ]
347
+
348
+ it('replays consecutive GUIDE rows into ONE coalesced guide segment', () => {
349
+ const { messages } = processHistoricalMessages(guideDialog, {
350
+ assistantName: 'Mingo AI',
351
+ assistantType: 'mingo',
352
+ })
353
+ expect(messages).toHaveLength(2)
354
+ expect(messages[1]).toMatchObject({
355
+ role: 'assistant',
356
+ content: [{ type: 'guide', text: '## Enroll a device\n1. Open Settings' }],
357
+ })
358
+ })
359
+
360
+ it('keeps GUIDE and TEXT as separate segments in wire order', () => {
361
+ const mixed: HistoricalMessage[] = [
362
+ {
363
+ id: 'a1',
364
+ chatType: 'CLIENT_CHAT',
365
+ createdAt: '2026-07-19T09:00:05Z',
366
+ owner: { type: 'ASSISTANT' },
367
+ messageData: [
368
+ { type: 'TEXT', text: 'Here is the walkthrough.' },
369
+ { type: 'GUIDE', text: '## Steps' },
370
+ { type: 'TEXT', text: 'Anything else?' },
371
+ ],
372
+ },
373
+ ]
374
+ const { messages } = processHistoricalMessages(mixed)
375
+ expect(messages[0].content).toEqual([
376
+ { type: 'text', text: 'Here is the walkthrough.' },
377
+ { type: 'guide', text: '## Steps' },
378
+ { type: 'text', text: 'Anything else?' },
379
+ ])
380
+ })
381
+
382
+ it('a GUIDE row with no text decodes to nothing (no empty card)', () => {
383
+ const empty: HistoricalMessage[] = [
384
+ {
385
+ id: 'a1',
386
+ chatType: 'CLIENT_CHAT',
387
+ createdAt: '2026-07-19T09:00:05Z',
388
+ owner: { type: 'ASSISTANT' },
389
+ messageData: [{ type: 'GUIDE' } as any],
390
+ },
391
+ ]
392
+ expect(processHistoricalMessages(empty).messages).toEqual([])
393
+ })
394
+ })