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

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 (296) hide show
  1. package/dist/{chunk-KX3K2EH4.cjs → chunk-2XAAFAQT.cjs} +11 -11
  2. package/dist/{chunk-KX3K2EH4.cjs.map → chunk-2XAAFAQT.cjs.map} +1 -1
  3. package/dist/{chunk-JHEIOXXZ.js → chunk-4AWRPTKE.js} +5 -6
  4. package/dist/chunk-4AWRPTKE.js.map +1 -0
  5. package/dist/{chunk-PTXZB2KW.js → chunk-4J3CB4KO.js} +2 -2
  6. package/dist/{chunk-7N7YZCHR.cjs → chunk-4MLOMMDX.cjs} +5519 -7555
  7. package/dist/chunk-4MLOMMDX.cjs.map +1 -0
  8. package/dist/{chunk-MEMRQTOI.js → chunk-56J7RLMM.js} +10434 -12470
  9. package/dist/chunk-56J7RLMM.js.map +1 -0
  10. package/dist/{chunk-MEIGDK7M.cjs → chunk-5ICPQAUI.cjs} +60 -60
  11. package/dist/chunk-5ICPQAUI.cjs.map +1 -0
  12. package/dist/{chunk-BEEPU44B.js → chunk-5RHJVPL7.js} +10 -10
  13. package/dist/chunk-5RHJVPL7.js.map +1 -0
  14. package/dist/{chunk-XOFE2RT4.js → chunk-BTIRYGDT.js} +4 -4
  15. package/dist/{chunk-JSEJ5MH5.cjs → chunk-DY6AJNVK.cjs} +5 -5
  16. package/dist/{chunk-JSEJ5MH5.cjs.map → chunk-DY6AJNVK.cjs.map} +1 -1
  17. package/dist/{chunk-PLB6CQSO.js → chunk-FG5ALORQ.js} +2 -2
  18. package/dist/{chunk-QG2EBIH5.cjs → chunk-J2J36PEN.cjs} +31 -31
  19. package/dist/{chunk-QG2EBIH5.cjs.map → chunk-J2J36PEN.cjs.map} +1 -1
  20. package/dist/{chunk-A6FPYAUD.cjs → chunk-LIRXKEOE.cjs} +15 -15
  21. package/dist/{chunk-A6FPYAUD.cjs.map → chunk-LIRXKEOE.cjs.map} +1 -1
  22. package/dist/{chunk-MSL5NE3N.cjs → chunk-LVDI7R7T.cjs} +37 -37
  23. package/dist/{chunk-MSL5NE3N.cjs.map → chunk-LVDI7R7T.cjs.map} +1 -1
  24. package/dist/{chunk-MPUQFRJG.cjs → chunk-M4SCYPCR.cjs} +7 -7
  25. package/dist/{chunk-MPUQFRJG.cjs.map → chunk-M4SCYPCR.cjs.map} +1 -1
  26. package/dist/{chunk-QDGB2DKY.cjs → chunk-O6S6Q52W.cjs} +26 -26
  27. package/dist/{chunk-QDGB2DKY.cjs.map → chunk-O6S6Q52W.cjs.map} +1 -1
  28. package/dist/{chunk-W56JYDSC.cjs → chunk-OFFYASUY.cjs} +27 -28
  29. package/dist/chunk-OFFYASUY.cjs.map +1 -0
  30. package/dist/{chunk-NCAN6VUF.js → chunk-P3ITKTUE.js} +2 -2
  31. package/dist/{chunk-M6FEGPQJ.cjs → chunk-PI5BMKV3.cjs} +62 -62
  32. package/dist/{chunk-M6FEGPQJ.cjs.map → chunk-PI5BMKV3.cjs.map} +1 -1
  33. package/dist/{chunk-34TDA22K.js → chunk-QHRNQTW5.js} +2 -2
  34. package/dist/{chunk-OL3MJKUI.js → chunk-SZWMJIH3.js} +2 -2
  35. package/dist/{chunk-MZRIRQYV.js → chunk-U5LU56JE.js} +2 -2
  36. package/dist/{chunk-C65NCIMV.cjs → chunk-VML66FJQ.cjs} +9 -9
  37. package/dist/{chunk-C65NCIMV.cjs.map → chunk-VML66FJQ.cjs.map} +1 -1
  38. package/dist/{chunk-3LKB722U.js → chunk-WKWZ7LVC.js} +2 -2
  39. package/dist/{chunk-J3VBVR4W.js → chunk-WPFLV4QJ.js} +4 -4
  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-nats-chat-adapter.d.ts +48 -15
  48. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  49. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +6 -0
  50. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -0
  51. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +57 -10
  52. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  53. package/dist/components/chat/index.cjs +2 -10
  54. package/dist/components/chat/index.cjs.map +1 -1
  55. package/dist/components/chat/index.d.ts +0 -1
  56. package/dist/components/chat/index.d.ts.map +1 -1
  57. package/dist/components/chat/index.js +13 -21
  58. package/dist/components/chat/types/api.types.d.ts +81 -1
  59. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  60. package/dist/components/chat/types/processing.types.d.ts +93 -2
  61. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  62. package/dist/components/chat/utils/chunk-parser.d.ts +25 -0
  63. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -0
  64. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +6 -34
  65. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  66. package/dist/components/chat/utils/index.d.ts +3 -2
  67. package/dist/components/chat/utils/index.d.ts.map +1 -1
  68. package/dist/components/chat/utils/process-historical-messages.d.ts +11 -59
  69. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  70. package/dist/components/contact/index.cjs +3 -3
  71. package/dist/components/contact/index.js +2 -2
  72. package/dist/components/docs/index.cjs +5 -5
  73. package/dist/components/docs/index.js +4 -4
  74. package/dist/components/embeds/index.cjs +3 -3
  75. package/dist/components/embeds/index.js +2 -2
  76. package/dist/components/faq/index.cjs +3 -3
  77. package/dist/components/faq/index.js +2 -2
  78. package/dist/components/features/floating-walkthrough-video.d.ts.map +1 -1
  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/index.cjs +2 -2
  88. package/dist/components/navigation/index.js +1 -1
  89. package/dist/components/navigation/navigation-sidebar-item.d.ts +23 -1
  90. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  91. package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
  92. package/dist/components/onboarding-guides/index.cjs +5 -5
  93. package/dist/components/onboarding-guides/index.js +4 -4
  94. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  95. package/dist/components/related-content/index.cjs +3 -3
  96. package/dist/components/related-content/index.js +2 -2
  97. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  98. package/dist/components/tickets/index.cjs +5 -5
  99. package/dist/components/tickets/index.js +4 -4
  100. package/dist/components/ui/data-table/data-table-column-filter.d.ts.map +1 -1
  101. package/dist/components/ui/data-table/data-table-header.d.ts.map +1 -1
  102. package/dist/components/ui/data-table/data-table-skeleton.d.ts.map +1 -1
  103. package/dist/components/ui/index.cjs +2 -44
  104. package/dist/components/ui/index.cjs.map +1 -1
  105. package/dist/components/ui/index.d.ts +2 -1
  106. package/dist/components/ui/index.d.ts.map +1 -1
  107. package/dist/components/ui/index.js +13 -55
  108. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  109. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  110. package/dist/components/ui/simple-markdown-renderer.d.ts +73 -0
  111. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -0
  112. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  113. package/dist/index.cjs +2 -60
  114. package/dist/index.cjs.map +1 -1
  115. package/dist/index.js +13 -71
  116. package/dist/utils/index.cjs +40 -169
  117. package/dist/utils/index.cjs.map +1 -1
  118. package/dist/utils/index.d.ts +0 -1
  119. package/dist/utils/index.d.ts.map +1 -1
  120. package/dist/utils/index.js +41 -162
  121. package/dist/utils/index.js.map +1 -1
  122. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  123. package/package.json +1 -7
  124. package/src/components/chat/__tests__/chat-message-list.test.tsx +5 -293
  125. package/src/components/chat/chat-message-enhanced.tsx +20 -122
  126. package/src/components/chat/chat-message-list.tsx +109 -358
  127. package/src/components/chat/embeddable-chat.tsx +0 -6
  128. package/src/components/chat/hooks/.index.md +33 -30
  129. package/src/components/chat/hooks/.use-nats-chat-adapter.md +55 -36
  130. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +77 -0
  131. package/src/components/chat/hooks/index.ts +1 -0
  132. package/src/components/chat/hooks/use-chat.ts +0 -8
  133. package/src/components/chat/hooks/use-nats-chat-adapter.ts +825 -243
  134. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +489 -0
  135. package/src/components/chat/hooks/use-sse-chat-adapter.ts +684 -380
  136. package/src/components/chat/index.ts +0 -5
  137. package/src/components/chat/thinking-display.tsx +1 -1
  138. package/src/components/chat/types/.api.types.md +49 -39
  139. package/src/components/chat/types/.processing.types.md +58 -29
  140. package/src/components/chat/types/api.types.ts +73 -6
  141. package/src/components/chat/types/processing.types.ts +53 -11
  142. package/src/components/chat/utils/.chunk-parser.md +62 -0
  143. package/src/components/chat/utils/.index.md +40 -31
  144. package/src/components/chat/utils/chunk-parser.ts +262 -0
  145. package/src/components/chat/utils/extract-incomplete-message-state.ts +5 -63
  146. package/src/components/chat/utils/index.ts +8 -5
  147. package/src/components/chat/utils/process-historical-messages.ts +386 -358
  148. package/src/components/features/floating-walkthrough-video.tsx +3 -1
  149. package/src/components/navigation/navigation-sidebar-item.tsx +80 -30
  150. package/src/components/navigation/navigation-sidebar.tsx +106 -46
  151. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +4 -22
  152. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  153. package/src/components/shared/product-release/release-detail-page.tsx +4 -19
  154. package/src/components/ui/.simple-markdown-renderer.md +52 -0
  155. package/src/components/ui/__tests__/tab-navigation.test.tsx +159 -0
  156. package/src/components/ui/data-table/data-table-column-filter.tsx +7 -1
  157. package/src/components/ui/data-table/data-table-header.tsx +4 -1
  158. package/src/components/ui/data-table/data-table-skeleton.tsx +7 -0
  159. package/src/components/ui/index.ts +2 -1
  160. package/src/components/ui/release-changelog-section.tsx +1 -1
  161. package/src/components/ui/rich-markdown-renderer.tsx +1223 -0
  162. package/src/components/ui/simple-markdown-renderer.tsx +964 -0
  163. package/src/components/ui/tab-navigation.tsx +370 -171
  164. package/src/components/ui/ticket-info-section.tsx +1 -1
  165. package/src/utils/index.ts +0 -1
  166. package/src/utils/markdown-section-extractor.ts +56 -41
  167. package/dist/chat-protocol/decode.d.ts +0 -58
  168. package/dist/chat-protocol/decode.d.ts.map +0 -1
  169. package/dist/chat-protocol/encode.d.ts +0 -44
  170. package/dist/chat-protocol/encode.d.ts.map +0 -1
  171. package/dist/chat-protocol/env-flag.d.ts +0 -35
  172. package/dist/chat-protocol/env-flag.d.ts.map +0 -1
  173. package/dist/chat-protocol/events.d.ts +0 -207
  174. package/dist/chat-protocol/events.d.ts.map +0 -1
  175. package/dist/chat-protocol/frames.d.ts +0 -213
  176. package/dist/chat-protocol/frames.d.ts.map +0 -1
  177. package/dist/chat-protocol/index.cjs +0 -500
  178. package/dist/chat-protocol/index.cjs.map +0 -1
  179. package/dist/chat-protocol/index.d.ts +0 -18
  180. package/dist/chat-protocol/index.d.ts.map +0 -1
  181. package/dist/chat-protocol/index.js +0 -484
  182. package/dist/chat-protocol/index.js.map +0 -1
  183. package/dist/chat-protocol/ip-normalize.d.ts +0 -44
  184. package/dist/chat-protocol/ip-normalize.d.ts.map +0 -1
  185. package/dist/chat-protocol/nats-decoder.d.ts +0 -26
  186. package/dist/chat-protocol/nats-decoder.d.ts.map +0 -1
  187. package/dist/chunk-7N7YZCHR.cjs.map +0 -1
  188. package/dist/chunk-BEEPU44B.js.map +0 -1
  189. package/dist/chunk-JHEIOXXZ.js.map +0 -1
  190. package/dist/chunk-MEIGDK7M.cjs.map +0 -1
  191. package/dist/chunk-MEMRQTOI.js.map +0 -1
  192. package/dist/chunk-W56JYDSC.cjs.map +0 -1
  193. package/dist/components/chat/stream/chat-dialog-store.d.ts +0 -184
  194. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +0 -1
  195. package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -367
  196. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +0 -1
  197. package/dist/components/chat/stream/delta-batcher.d.ts +0 -51
  198. package/dist/components/chat/stream/delta-batcher.d.ts.map +0 -1
  199. package/dist/components/chat/stream/index.d.ts +0 -17
  200. package/dist/components/chat/stream/index.d.ts.map +0 -1
  201. package/dist/components/chat/stream/message-mutations.d.ts +0 -77
  202. package/dist/components/chat/stream/message-mutations.d.ts.map +0 -1
  203. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +0 -22
  204. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +0 -1
  205. package/dist/components/ui/markdown/base-components.d.ts +0 -51
  206. package/dist/components/ui/markdown/base-components.d.ts.map +0 -1
  207. package/dist/components/ui/markdown/engine.d.ts +0 -99
  208. package/dist/components/ui/markdown/engine.d.ts.map +0 -1
  209. package/dist/components/ui/markdown/heading-ids.d.ts +0 -65
  210. package/dist/components/ui/markdown/heading-ids.d.ts.map +0 -1
  211. package/dist/components/ui/markdown/index.d.ts +0 -18
  212. package/dist/components/ui/markdown/index.d.ts.map +0 -1
  213. package/dist/components/ui/markdown/mermaid-diagram.d.ts +0 -79
  214. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +0 -1
  215. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +0 -14
  216. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +0 -1
  217. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -40
  218. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +0 -1
  219. package/dist/components/ui/markdown/rich/shortcodes.d.ts +0 -8
  220. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +0 -1
  221. package/dist/components/ui/markdown/sanitize.d.ts +0 -153
  222. package/dist/components/ui/markdown/sanitize.d.ts.map +0 -1
  223. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +0 -23
  224. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +0 -1
  225. package/dist/components/ui/markdown/streaming.d.ts +0 -78
  226. package/dist/components/ui/markdown/streaming.d.ts.map +0 -1
  227. package/dist/components/ui/markdown/text-size.d.ts +0 -34
  228. package/dist/components/ui/markdown/text-size.d.ts.map +0 -1
  229. package/dist/utils/markdown-fences.d.ts +0 -42
  230. package/dist/utils/markdown-fences.d.ts.map +0 -1
  231. package/dist/utils/markdown-heading-id.d.ts +0 -85
  232. package/dist/utils/markdown-heading-id.d.ts.map +0 -1
  233. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -317
  234. package/src/chat-protocol/__tests__/chat-protocol.test.ts +0 -516
  235. package/src/chat-protocol/__tests__/env-flag.test.ts +0 -52
  236. package/src/chat-protocol/__tests__/ip-normalize.test.ts +0 -136
  237. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -232
  238. package/src/chat-protocol/decode.ts +0 -278
  239. package/src/chat-protocol/encode.ts +0 -71
  240. package/src/chat-protocol/env-flag.ts +0 -39
  241. package/src/chat-protocol/events.ts +0 -245
  242. package/src/chat-protocol/frames.ts +0 -245
  243. package/src/chat-protocol/index.ts +0 -21
  244. package/src/chat-protocol/ip-normalize.ts +0 -146
  245. package/src/chat-protocol/nats-decoder.ts +0 -258
  246. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -207
  247. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -230
  248. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +0 -37
  249. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  250. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +0 -438
  251. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +0 -318
  252. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +0 -940
  253. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +0 -756
  254. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +0 -460
  255. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -973
  256. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +0 -255
  257. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +0 -98
  258. package/src/components/chat/stream/chat-dialog-store.ts +0 -536
  259. package/src/components/chat/stream/chat-stream-reducer.ts +0 -1808
  260. package/src/components/chat/stream/delta-batcher.ts +0 -168
  261. package/src/components/chat/stream/index.ts +0 -55
  262. package/src/components/chat/stream/message-mutations.ts +0 -340
  263. package/src/components/chat/stream/use-chat-stream-reducer.ts +0 -126
  264. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +0 -420
  265. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +0 -605
  266. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -394
  267. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +0 -270
  268. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +0 -2420
  269. package/src/components/ui/__tests__/markdown-parity.test.tsx +0 -2528
  270. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +0 -115
  271. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +0 -95
  272. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +0 -309
  273. package/src/components/ui/markdown/__tests__/streaming.test.tsx +0 -388
  274. package/src/components/ui/markdown/base-components.tsx +0 -360
  275. package/src/components/ui/markdown/engine.tsx +0 -315
  276. package/src/components/ui/markdown/heading-ids.ts +0 -239
  277. package/src/components/ui/markdown/index.ts +0 -49
  278. package/src/components/ui/markdown/mermaid-diagram.tsx +0 -291
  279. package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -199
  280. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +0 -184
  281. package/src/components/ui/markdown/rich/shortcodes.ts +0 -170
  282. package/src/components/ui/markdown/sanitize.ts +0 -3499
  283. package/src/components/ui/markdown/simple-markdown-renderer.tsx +0 -28
  284. package/src/components/ui/markdown/streaming.ts +0 -362
  285. package/src/components/ui/markdown/text-size.ts +0 -106
  286. package/src/utils/markdown-fences.ts +0 -127
  287. package/src/utils/markdown-heading-id.ts +0 -348
  288. /package/dist/{chunk-PTXZB2KW.js.map → chunk-4J3CB4KO.js.map} +0 -0
  289. /package/dist/{chunk-XOFE2RT4.js.map → chunk-BTIRYGDT.js.map} +0 -0
  290. /package/dist/{chunk-PLB6CQSO.js.map → chunk-FG5ALORQ.js.map} +0 -0
  291. /package/dist/{chunk-NCAN6VUF.js.map → chunk-P3ITKTUE.js.map} +0 -0
  292. /package/dist/{chunk-34TDA22K.js.map → chunk-QHRNQTW5.js.map} +0 -0
  293. /package/dist/{chunk-OL3MJKUI.js.map → chunk-SZWMJIH3.js.map} +0 -0
  294. /package/dist/{chunk-MZRIRQYV.js.map → chunk-U5LU56JE.js.map} +0 -0
  295. /package/dist/{chunk-3LKB722U.js.map → chunk-WKWZ7LVC.js.map} +0 -0
  296. /package/dist/{chunk-J3VBVR4W.js.map → chunk-WPFLV4QJ.js.map} +0 -0
@@ -1,28 +1,6 @@
1
1
  /**
2
- * Utility for processing historical messages from GraphQL/API into
3
- * display-ready format.
4
- *
5
- * Phase 3 of the chat unification reduced this file to DECODE + ENVELOPE:
6
- *
7
- * - `decodeHistoricalMessageData` maps one persisted `MessageData` item
8
- * into the shared `ChatStreamEvent` vocabulary (historical items share
9
- * the MESSAGE_TYPE vocabulary with NATS chunks but are NOT full chunk
10
- * envelopes — e.g. compaction summaries ride in `summary`, not `text`,
11
- * and APPROVAL_RESULT rows carry `resolvedByName` directly);
12
- * - `applyHistoryEvent` replays each decoded event into the shared
13
- * per-turn kernel (`MessageSegmentAccumulator` — the same kernel the
14
- * master `createChatStreamReducer` instantiates) with the
15
- * history-specific approval semantics (display-all default, pending
16
- * tracking + `flushPendingApprovals`, `approvalStatuses` overrides);
17
- * - the ENVELOPE (user/assistant flush-grouping, OWNER_TYPE author /
18
- * display-name / avatar resolution, standalone SYSTEM handling,
19
- * per-message contextItems mapping, per-turn streamSeq MAX) lives ONCE
20
- * in `processHistory` — the previous near-duplicate second copy was
21
- * deleted (`processHistoricalMessagesWithErrors` is an alias; the two
22
- * snapshots were byte-identical).
23
- *
24
- * Adapters then feed the processed rows into the reducer via
25
- * `initializeWithState` (see `useNatsChatAdapter.loadDialogHistory`).
2
+ * Utility for processing historical messages from GraphQL/API
3
+ * into display-ready format
26
4
  */
27
5
 
28
6
  import {
@@ -37,7 +15,6 @@ import {
37
15
  type MessageData,
38
16
  type MessageOwner,
39
17
  } from '../types'
40
- import type { ChatStreamEvent } from '../../../chat-protocol/events'
41
18
  import { MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator'
42
19
  import { getCommandText } from './tool-call-helpers'
43
20
 
@@ -76,67 +53,255 @@ function pushStandaloneMessages(
76
53
  })
77
54
  }
78
55
 
79
- // =============================================================================
80
- // Decode persisted MessageData item shared ChatStreamEvent vocabulary
81
- // =============================================================================
56
+ /**
57
+ * Result type for historical message processing
58
+ */
59
+ export interface ProcessHistoricalMessagesResult {
60
+ messages: ProcessedMessage[]
61
+ escalatedApprovals: Map<
62
+ string,
63
+ { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
64
+ >
65
+ }
82
66
 
83
67
  /**
84
- * Map one persisted `MessageData` item to a normalized `ChatStreamEvent`.
85
- * Thin shim over the NATS chunk decoder's core mapping — the differences
86
- * are exactly the persisted-row divergences from the realtime envelope:
87
- *
88
- * - compaction summaries arrive in `summary` (realtime: `text`);
89
- * - APPROVAL_RESULT rows carry `resolvedByName` (realtime chunks carry
90
- * the resolver's name as `displayName`);
91
- * - `approvalType` defaults to 'CLIENT' (legacy history parity;
92
- * realtime defaults APPROVAL_REQUEST to 'USER');
93
- * - batch `toolCalls` pass through verbatim (already the persisted
94
- * `PendingToolCallData` shape — no normalization pass).
95
- *
96
- * Returns `null` for rows the assistant-turn path doesn't decode (SYSTEM
97
- * is handled by the envelope's standalone path; user TEXT rows are handled
98
- * by the envelope's user path).
68
+ * Process an array of historical messages into display-ready format
99
69
  */
100
- export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent | null {
70
+ export function processHistoricalMessages(
71
+ messages: HistoricalMessage[],
72
+ options: MessageProcessingOptions = {}
73
+ ): ProcessHistoricalMessagesResult {
74
+ const {
75
+ assistantName = 'Fae',
76
+ assistantType = 'fae',
77
+ assistantAvatar,
78
+ onApprove,
79
+ onReject,
80
+ chatTypeFilter,
81
+ approvalStatuses = {},
82
+ // An omitted option means "display every approval type" — the original
83
+ // history semantics. Deliberately NOT defaulted to the realtime
84
+ // processor's ['CLIENT']: consumers that pass a wider list to their
85
+ // realtime processor but omit it on the history path would silently lose
86
+ // pending non-CLIENT approval cards on every reload/reconnect refetch
87
+ // (they also ignore `escalatedApprovals`). Realtime/history parity is
88
+ // opt-in: pass the same explicit list to both.
89
+ displayApprovalTypes,
90
+ batchApprovalsEnabled,
91
+ } = options
92
+
93
+ const processedMessages: ProcessedMessage[] = []
94
+ const accumulator = createMessageSegmentAccumulator({ onApprove, onReject })
95
+ const escalatedApprovals = new Map<
96
+ string,
97
+ { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
98
+ >()
99
+
100
+ let currentAssistantId: string | null = null
101
+ let currentAssistantTimestamp: Date | null = null
102
+ let lastAssistantId: string | null = null
103
+ // MAX persisted seq across the rows grouped into the current assistant turn
104
+ // — carried onto the flushed message's streamSeq for per-role merge coverage.
105
+ let currentAssistantStreamSeq: number | undefined
106
+
107
+ /**
108
+ * Flush the current assistant message to processedMessages.
109
+ * Uses the LAST message ID in the group for stable React keys across page boundaries.
110
+ */
111
+ const flushAssistantMessage = () => {
112
+ const idToUse = lastAssistantId || currentAssistantId
113
+ if (idToUse && accumulator.hasContent()) {
114
+ processedMessages.push({
115
+ id: idToUse,
116
+ role: 'assistant',
117
+ content: accumulator.getSegments(),
118
+ name: assistantName,
119
+ assistantType,
120
+ authorType: assistantType as AuthorType,
121
+ timestamp: currentAssistantTimestamp || new Date(),
122
+ avatar: assistantAvatar,
123
+ ...(currentAssistantStreamSeq !== undefined ? { streamSeq: currentAssistantStreamSeq } : {}),
124
+ })
125
+ accumulator.resetSegments()
126
+ }
127
+ // Reset grouping identity + seq UNCONDITIONALLY — even on an EMPTY flush (an
128
+ // assistant turn whose only data was a filtered/escalated approval renders
129
+ // nothing, so `hasContent()` is false and nothing is pushed). Left inside
130
+ // the push-block, a stale id/timestamp and (worse) a stale
131
+ // `currentAssistantStreamSeq` bleed into the NEXT assistant turn: `!currentAssistantId`
132
+ // stays false so it keeps the old id/timestamp, and `Math.max` inflates its
133
+ // streamSeq — which then over-covers synthetics in the history merge.
134
+ currentAssistantId = null
135
+ currentAssistantTimestamp = null
136
+ lastAssistantId = null
137
+ currentAssistantStreamSeq = undefined
138
+ }
139
+
140
+ messages.forEach((msg, index) => {
141
+ // Filter by chat type if specified
142
+ if (chatTypeFilter && msg.chatType !== chatTypeFilter) return
143
+
144
+ const messageDataArray = Array.isArray(msg.messageData)
145
+ ? msg.messageData
146
+ : msg.messageData
147
+ ? [msg.messageData]
148
+ : []
149
+
150
+ const hasStandaloneData = messageDataArray.some((data) =>
151
+ data.type === MESSAGE_TYPE.SYSTEM
152
+ )
153
+ if (hasStandaloneData) {
154
+ flushAssistantMessage()
155
+ pushStandaloneMessages(processedMessages, msg, messageDataArray)
156
+ return
157
+ }
158
+
159
+ const isUserMessage =
160
+ msg.owner?.type === OWNER_TYPE.CLIENT || msg.owner?.type === OWNER_TYPE.ADMIN
161
+
162
+ if (isUserMessage) {
163
+ flushAssistantMessage()
164
+
165
+ const userAuthorType: AuthorType = msg.owner?.type === OWNER_TYPE.ADMIN ? 'admin' : 'user'
166
+ messageDataArray.forEach((data) => {
167
+ if (data.type === MESSAGE_TYPE.TEXT && 'text' in data && data.text) {
168
+ // `TextData.contextItems` (server: `[{ type, id }]`) — the entity
169
+ // context the user attached to this message. Surface it so the bubble
170
+ // renders its chip strip from history (no label on the wire → fall
171
+ // back to the id, matching the realtime path).
172
+ const rawContext = (data as { contextItems?: Array<{ type?: unknown; id?: unknown }> }).contextItems
173
+ const contextItems = Array.isArray(rawContext)
174
+ ? rawContext
175
+ .filter((c) => typeof c?.type === 'string' && typeof c?.id === 'string')
176
+ .map((c) => ({ type: c.type as string, id: c.id as string, label: c.id as string }))
177
+ : undefined
178
+ processedMessages.push({
179
+ id: msg.id,
180
+ role: 'user',
181
+ content: data.text,
182
+ name: getOwnerDisplayName(msg.owner),
183
+ avatar: getOwnerAvatar(msg.owner),
184
+ authorType: userAuthorType,
185
+ timestamp: new Date(msg.createdAt),
186
+ ...(contextItems && contextItems.length > 0 ? { contextItems } : {}),
187
+ ...(typeof msg.lastChunkStreamSeq === 'number' ? { streamSeq: msg.lastChunkStreamSeq } : {}),
188
+ })
189
+ }
190
+ })
191
+ } else {
192
+ if (!currentAssistantId) {
193
+ currentAssistantId = msg.id
194
+ currentAssistantTimestamp = new Date(msg.createdAt)
195
+ }
196
+ lastAssistantId = msg.id
197
+ if (typeof msg.lastChunkStreamSeq === 'number') {
198
+ currentAssistantStreamSeq =
199
+ currentAssistantStreamSeq === undefined
200
+ ? msg.lastChunkStreamSeq
201
+ : Math.max(currentAssistantStreamSeq, msg.lastChunkStreamSeq)
202
+ }
203
+
204
+ messageDataArray.forEach((data) => {
205
+ processMessageData(
206
+ data,
207
+ accumulator,
208
+ approvalStatuses,
209
+ { displayApprovalTypes, batchApprovalsEnabled },
210
+ escalatedApprovals,
211
+ )
212
+ })
213
+
214
+ // Check if we should flush (next message is from user or last message)
215
+ const nextMsg = messages[index + 1]
216
+ const isLastMessage = index === messages.length - 1
217
+ const nextIsFromUser =
218
+ nextMsg &&
219
+ (nextMsg.owner?.type === OWNER_TYPE.CLIENT || nextMsg.owner?.type === OWNER_TYPE.ADMIN)
220
+
221
+ if (isLastMessage || nextIsFromUser) {
222
+ flushAssistantMessage()
223
+ }
224
+ }
225
+ })
226
+
227
+ flushAssistantMessage()
228
+
229
+ const pendingApprovalSegments = accumulator.flushPendingApprovals()
230
+ if (pendingApprovalSegments.length > 0) {
231
+ processedMessages.push({
232
+ id: `pending-approvals-${Date.now()}`,
233
+ role: 'assistant',
234
+ content: pendingApprovalSegments,
235
+ name: assistantName,
236
+ assistantType,
237
+ timestamp: new Date(),
238
+ avatar: assistantAvatar,
239
+ })
240
+ }
241
+
242
+ return {
243
+ messages: processedMessages,
244
+ escalatedApprovals: escalatedApprovals
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Process a single message data item into segments
250
+ */
251
+ function processMessageData(
252
+ data: MessageData,
253
+ accumulator: MessageSegmentAccumulator,
254
+ approvalStatuses: Record<string, string>,
255
+ options: MessageProcessingOptions = {},
256
+ escalatedApprovals?: Map<
257
+ string,
258
+ { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
259
+ >
260
+ ): void {
261
+ // batchApprovalsEnabled is owned by the consumer (oss-tenant chat client /
262
+ // openframe-frontend tickets). Defaults to ON so consumers that haven't
263
+ // wired the flag yet get the batch UI; pass `false` to force legacy.
264
+ const { displayApprovalTypes, batchApprovalsEnabled = true } = options
101
265
  switch (data.type) {
102
266
  case MESSAGE_TYPE.TEXT:
103
267
  if ('text' in data && data.text) {
104
- return { type: 'text-delta', text: data.text }
268
+ accumulator.appendText(data.text)
105
269
  }
106
- return null
270
+ break
107
271
 
108
272
  case MESSAGE_TYPE.THINKING:
109
273
  if ('text' in data && data.text) {
110
- return { type: 'thinking-delta', text: data.text }
274
+ accumulator.appendThinking(data.text)
111
275
  }
112
- return null
276
+ break
113
277
 
114
278
  case MESSAGE_TYPE.GUIDE:
115
279
  if ('text' in data && data.text) {
116
- return { type: 'guide-delta', text: data.text }
280
+ accumulator.appendGuide(data.text)
117
281
  }
118
- return null
282
+ break
119
283
 
120
284
  case MESSAGE_TYPE.EXECUTING_TOOL:
121
285
  if ('integratedToolType' in data) {
122
- return {
123
- type: 'tool-execution',
286
+ accumulator.addToolExecution({
287
+ type: 'tool_execution',
124
288
  data: {
125
289
  type: 'EXECUTING_TOOL',
126
290
  integratedToolType: data.integratedToolType || '',
127
291
  toolFunction: data.toolFunction || '',
128
292
  toolTitle: typeof data.title === 'string' ? data.title : undefined,
293
+ toolExplanation: typeof data.toolExplanation === 'string' ? data.toolExplanation : undefined,
129
294
  parameters: data.parameters,
130
295
  toolExecutionRequestId: data.toolExecutionRequestId,
131
296
  },
132
- }
297
+ })
133
298
  }
134
- return null
299
+ break
135
300
 
136
301
  case MESSAGE_TYPE.EXECUTED_TOOL:
137
302
  if ('integratedToolType' in data) {
138
- return {
139
- type: 'tool-execution',
303
+ accumulator.addToolExecution({
304
+ type: 'tool_execution',
140
305
  data: {
141
306
  type: 'EXECUTED_TOOL',
142
307
  integratedToolType: data.integratedToolType || '',
@@ -147,298 +312,218 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
147
312
  success: data.success,
148
313
  toolExecutionRequestId: data.toolExecutionRequestId,
149
314
  },
150
- }
315
+ })
151
316
  }
152
- return null
317
+ break
153
318
 
154
319
  case MESSAGE_TYPE.APPROVAL_REQUEST:
155
320
  if ('approvalRequestId' in data && data.approvalRequestId) {
156
- return {
157
- type: 'approval-request',
158
- requestId: data.approvalRequestId,
159
- approvalType: data.approvalType || 'CLIENT',
160
- command: data.command || '',
161
- explanation: data.explanation,
162
- ...(Array.isArray(data.toolCalls) ? { toolCalls: data.toolCalls } : {}),
321
+ const approvalType = data.approvalType || 'CLIENT'
322
+ const toolCalls: PendingToolCallData[] | undefined = Array.isArray(data.toolCalls)
323
+ ? data.toolCalls
324
+ : undefined
325
+ const isBatch = !!toolCalls && toolCalls.length > 0
326
+
327
+ if (!displayApprovalTypes || displayApprovalTypes.includes(approvalType)) {
328
+ if (isBatch) {
329
+ const status = (approvalStatuses[data.approvalRequestId] as ChatApprovalStatus) || 'pending'
330
+ if (batchApprovalsEnabled) {
331
+ accumulator.addApprovalBatch(data.approvalRequestId, approvalType, toolCalls!, status)
332
+ } else {
333
+ // Flag OFF — unfold batch into N legacy approval cards (same id).
334
+ for (const call of toolCalls!) {
335
+ if (!call.requiresApproval) continue
336
+ accumulator.addApprovalRequest(
337
+ data.approvalRequestId,
338
+ getCommandText(call),
339
+ call.toolExplanation,
340
+ approvalType,
341
+ status,
342
+ )
343
+ }
344
+ }
345
+ } else {
346
+ // The resolution may already be known to the consumer (realtime
347
+ // flipped it and fed it back via `approvalStatuses`) while the
348
+ // matching APPROVAL_RESULT row is absent from the fetched history
349
+ // pages. Without this, the single-approval path tracks it as
350
+ // pending and `flushPendingApprovals()` resurrects it as a stale
351
+ // sticky card on every history re-process. Mirror the batch path
352
+ // and honor `approvalStatuses`.
353
+ const resolvedStatus = approvalStatuses[data.approvalRequestId] as ChatApprovalStatus | undefined
354
+ if (resolvedStatus === 'approved' || resolvedStatus === 'rejected') {
355
+ accumulator.addApprovalRequest(
356
+ data.approvalRequestId,
357
+ data.command || '',
358
+ data.explanation,
359
+ approvalType,
360
+ resolvedStatus,
361
+ )
362
+ } else {
363
+ accumulator.trackApprovalRequest(data.approvalRequestId, {
364
+ command: data.command || '',
365
+ explanation: data.explanation,
366
+ approvalType,
367
+ })
368
+ }
369
+ }
370
+ } else {
371
+ escalatedApprovals?.set(data.approvalRequestId, {
372
+ command: data.command || '',
373
+ explanation: data.explanation,
374
+ approvalType,
375
+ ...(isBatch ? { toolCalls } : {}),
376
+ })
163
377
  }
164
378
  }
165
- return null
379
+ break
166
380
 
167
381
  case MESSAGE_TYPE.APPROVAL_RESULT:
168
382
  if ('approvalRequestId' in data && data.approvalRequestId) {
169
- return {
170
- type: 'approval-resolved',
171
- requestId: data.approvalRequestId,
172
- status: data.approved ? 'approved' : 'rejected',
173
- approvalType: data.approvalType,
174
- resolvedByName: 'resolvedByName' in data ? data.resolvedByName : undefined,
175
- }
176
- }
177
- return null
178
-
179
- case MESSAGE_TYPE.ERROR:
180
- if ('error' in data) {
181
- return {
182
- type: 'error',
183
- title: data.error || 'An error occurred',
184
- details: 'details' in data ? data.details : undefined,
185
- }
186
- }
187
- return null
188
-
189
- case MESSAGE_TYPE.CONTEXT_COMPACTION_START:
190
- return { type: 'compaction', phase: 'start' }
191
-
192
- case MESSAGE_TYPE.CONTEXT_COMPACTION_END:
193
- return {
194
- type: 'compaction',
195
- phase: 'end',
196
- summary: 'summary' in data && typeof data.summary === 'string' ? data.summary : undefined,
197
- }
198
-
199
- case MESSAGE_TYPE.SYSTEM:
200
- if ('text' in data && data.text) {
201
- return { type: 'participant', kind: 'system', text: data.text }
202
- }
203
- return null
204
-
205
- default:
206
- // Unknown message type — ignore.
207
- return null
208
- }
209
- }
210
-
211
- // =============================================================================
212
- // Replay — decoded event → per-turn kernel (history approval semantics)
213
- // =============================================================================
214
-
215
- type EscalatedApprovals = Map<
216
- string,
217
- { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
218
- >
219
-
220
- /**
221
- * Replay one decoded event into the shared per-turn segment kernel with the
222
- * HISTORY approval semantics (an omitted `displayApprovalTypes` means
223
- * "display every approval type" — the original history behavior; the
224
- * realtime reducer defaults to `['CLIENT']`).
225
- */
226
- function applyHistoryEvent(
227
- event: ChatStreamEvent,
228
- accumulator: MessageSegmentAccumulator,
229
- approvalStatuses: Record<string, string>,
230
- options: MessageProcessingOptions,
231
- escalatedApprovals?: EscalatedApprovals,
232
- ): void {
233
- // batchApprovalsEnabled is owned by the consumer (oss-tenant chat client /
234
- // openframe-frontend tickets). Defaults to ON so consumers that haven't
235
- // wired the flag yet get the batch UI; pass `false` to force legacy.
236
- const { displayApprovalTypes, batchApprovalsEnabled = true } = options
237
-
238
- switch (event.type) {
239
- case 'text-delta':
240
- accumulator.appendText(event.text)
241
- break
242
-
243
- case 'thinking-delta':
244
- accumulator.appendThinking(event.text)
245
- break
246
-
247
- case 'guide-delta':
248
- accumulator.appendGuide(event.text)
249
- break
250
-
251
- case 'tool-execution':
252
- accumulator.addToolExecution({ type: 'tool_execution', data: event.data })
253
- break
383
+ const existingStatus = approvalStatuses[data.approvalRequestId] as ChatApprovalStatus | undefined
384
+ const status: ChatApprovalStatus = existingStatus || (data.approved ? 'approved' : 'rejected')
385
+ const resolvedByName = 'resolvedByName' in data ? data.resolvedByName : undefined
386
+ const escalatedData = escalatedApprovals?.get(data.approvalRequestId)
254
387
 
255
- case 'approval-request': {
256
- const approvalType = event.approvalType || 'CLIENT'
257
- const toolCalls = event.toolCalls as PendingToolCallData[] | undefined
258
- const isBatch = !!toolCalls && toolCalls.length > 0
259
-
260
- if (!displayApprovalTypes || displayApprovalTypes.includes(approvalType)) {
261
- if (isBatch) {
262
- const status = (approvalStatuses[event.requestId] as ChatApprovalStatus) || 'pending'
388
+ if (escalatedData?.toolCalls && escalatedData.toolCalls.length > 0) {
263
389
  if (batchApprovalsEnabled) {
264
- accumulator.addApprovalBatch(event.requestId, approvalType, toolCalls!, status)
390
+ accumulator.addApprovalBatch(
391
+ data.approvalRequestId,
392
+ escalatedData.approvalType,
393
+ escalatedData.toolCalls,
394
+ status,
395
+ undefined,
396
+ resolvedByName,
397
+ )
265
398
  } else {
266
- // Flag OFF unfold batch into N legacy approval cards (same id).
267
- for (const call of toolCalls!) {
399
+ for (const call of escalatedData.toolCalls) {
268
400
  if (!call.requiresApproval) continue
269
401
  accumulator.addApprovalRequest(
270
- event.requestId,
402
+ data.approvalRequestId,
271
403
  getCommandText(call),
272
404
  call.toolExplanation,
273
- approvalType,
405
+ escalatedData.approvalType,
274
406
  status,
275
407
  )
276
408
  }
277
409
  }
278
- } else {
279
- // The resolution may already be known to the consumer (realtime
280
- // flipped it and fed it back via `approvalStatuses`) while the
281
- // matching APPROVAL_RESULT row is absent from the fetched history
282
- // pages. Without this, the single-approval path tracks it as
283
- // pending and `flushPendingApprovals()` resurrects it as a stale
284
- // sticky card on every history re-process. Mirror the batch path
285
- // and honor `approvalStatuses`.
286
- const resolvedStatus = approvalStatuses[event.requestId] as ChatApprovalStatus | undefined
287
- if (resolvedStatus === 'approved' || resolvedStatus === 'rejected') {
288
- accumulator.addApprovalRequest(
289
- event.requestId,
290
- event.command || '',
291
- event.explanation,
292
- approvalType,
293
- resolvedStatus,
294
- )
295
- } else {
296
- accumulator.trackApprovalRequest(event.requestId, {
297
- command: event.command || '',
298
- explanation: event.explanation,
299
- approvalType,
300
- })
301
- }
410
+ escalatedApprovals?.delete(data.approvalRequestId)
411
+ break
302
412
  }
303
- } else {
304
- escalatedApprovals?.set(event.requestId, {
305
- command: event.command || '',
306
- explanation: event.explanation,
307
- approvalType,
308
- ...(isBatch ? { toolCalls } : {}),
309
- })
310
- }
311
- break
312
- }
313
413
 
314
- case 'approval-resolved': {
315
- const requestId = event.requestId
316
- if (!requestId) break
317
- const existingStatus = approvalStatuses[requestId] as ChatApprovalStatus | undefined
318
- const status: ChatApprovalStatus = existingStatus || event.status
319
- const resolvedByName = event.resolvedByName
320
- const escalatedData = escalatedApprovals?.get(requestId)
321
-
322
- if (escalatedData?.toolCalls && escalatedData.toolCalls.length > 0) {
323
- if (batchApprovalsEnabled) {
324
- accumulator.addApprovalBatch(
325
- requestId,
326
- escalatedData.approvalType,
327
- escalatedData.toolCalls,
328
- status,
329
- undefined,
330
- resolvedByName,
331
- )
332
- } else {
333
- for (const call of escalatedData.toolCalls) {
334
- if (!call.requiresApproval) continue
335
- accumulator.addApprovalRequest(
336
- requestId,
337
- getCommandText(call),
338
- call.toolExplanation,
339
- escalatedData.approvalType,
340
- status,
341
- )
342
- }
414
+ if (escalatedData) {
415
+ accumulator.trackApprovalRequest(data.approvalRequestId, {
416
+ command: escalatedData.command,
417
+ explanation: escalatedData.explanation,
418
+ approvalType: escalatedData.approvalType,
419
+ })
420
+ escalatedApprovals?.delete(data.approvalRequestId)
343
421
  }
344
- escalatedApprovals?.delete(requestId)
345
- break
346
- }
347
422
 
348
- if (escalatedData) {
349
- accumulator.trackApprovalRequest(requestId, {
350
- command: escalatedData.command,
351
- explanation: escalatedData.explanation,
352
- approvalType: escalatedData.approvalType,
353
- })
354
- escalatedApprovals?.delete(requestId)
423
+ // If a segment with this id is already present (batch or legacy), just flip its status.
424
+ // updateApprovalStatus matches both `approval_batch` and `approval_request` segments.
425
+ const before = accumulator.getSegments()
426
+ const after = accumulator.updateApprovalStatus(data.approvalRequestId, status, resolvedByName)
427
+ const updatedExisting = before.some((s, i) => after[i] !== s)
428
+ if (updatedExisting) break
429
+
430
+ accumulator.processApprovalResult(
431
+ data.approvalRequestId,
432
+ status === 'approved',
433
+ data.approvalType || 'USER'
434
+ )
355
435
  }
356
-
357
- // If a segment with this id is already present (batch or legacy), just
358
- // flip its status. updateApprovalStatus matches both `approval_batch`
359
- // and `approval_request` segments.
360
- const before = accumulator.getSegments()
361
- const after = accumulator.updateApprovalStatus(requestId, status, resolvedByName)
362
- const updatedExisting = before.some((s, i) => after[i] !== s)
363
- if (updatedExisting) break
364
-
365
- accumulator.processApprovalResult(
366
- requestId,
367
- status === 'approved',
368
- event.approvalType || 'USER',
369
- )
370
436
  break
371
- }
372
437
 
373
- case 'error': {
374
- let message: string | undefined
375
- if (event.details) {
376
- try {
377
- message = JSON.parse(event.details)?.error?.message
378
- } catch {
379
- message = event.details
438
+ case MESSAGE_TYPE.ERROR:
439
+ if ('error' in data) {
440
+ let message: string | undefined
441
+ if ('details' in data && data?.details) {
442
+ try {
443
+ message = JSON.parse(data.details)?.error?.message
444
+ } catch {
445
+ message = data.details
446
+ }
380
447
  }
448
+ accumulator.addError(
449
+ data.error || 'An error occurred',
450
+ message
451
+ )
381
452
  }
382
- accumulator.addError(event.title, message)
383
453
  break
384
- }
385
454
 
386
- case 'compaction':
387
- if (event.phase === 'start') {
388
- accumulator.addContextCompaction()
389
- } else {
390
- accumulator.completeContextCompaction(event.summary)
391
- }
455
+ case MESSAGE_TYPE.CONTEXT_COMPACTION_START:
456
+ accumulator.addContextCompaction()
457
+ break
458
+
459
+ case MESSAGE_TYPE.CONTEXT_COMPACTION_END: {
460
+ const summary = 'summary' in data && typeof data.summary === 'string' ? data.summary : undefined
461
+ accumulator.completeContextCompaction(summary)
392
462
  break
463
+ }
393
464
 
394
465
  default:
395
- // Participant/system rows are envelope concerns; everything else is
396
- // realtime-only vocabulary that never appears in persisted rows.
466
+ // Unknown message type - ignore
397
467
  break
398
468
  }
399
469
  }
400
470
 
401
- // =============================================================================
402
- // Envelope — ONE implementation (previous duplicate deleted)
403
- // =============================================================================
404
-
405
471
  /**
406
- * Result type for historical message processing
472
+ * Extract error messages from historical messages
473
+ * Returns a separate array of error messages that should be displayed
407
474
  */
408
- export interface ProcessHistoricalMessagesResult {
409
- messages: ProcessedMessage[]
410
- escalatedApprovals: EscalatedApprovals
475
+ export function extractErrorMessages(
476
+ messages: HistoricalMessage[],
477
+ options: MessageProcessingOptions = {}
478
+ ): ProcessedMessage[] {
479
+ const { assistantName = 'Fae', assistantType = 'fae', assistantAvatar, chatTypeFilter } = options
480
+
481
+ const errorMessages: ProcessedMessage[] = []
482
+
483
+ messages.forEach((msg) => {
484
+ if (chatTypeFilter && msg.chatType !== chatTypeFilter) return
485
+
486
+ const messageDataArray = Array.isArray(msg.messageData)
487
+ ? msg.messageData
488
+ : msg.messageData
489
+ ? [msg.messageData]
490
+ : []
491
+
492
+ messageDataArray.forEach((data) => {
493
+ if (data.type === MESSAGE_TYPE.ERROR) {
494
+ errorMessages.push({
495
+ id: `${msg.id}-error`,
496
+ role: 'error',
497
+ content: 'error' in data && data.error ? data.error : 'An error occurred',
498
+ name: assistantName,
499
+ assistantType,
500
+ timestamp: new Date(msg.createdAt),
501
+ avatar: assistantAvatar,
502
+ })
503
+ }
504
+ })
505
+ })
506
+
507
+ return errorMessages
411
508
  }
412
509
 
413
510
  /**
414
- * Process an array of historical messages into display-ready format
511
+ * Process messages and include error messages in the correct order
415
512
  */
416
- export function processHistoricalMessages(
513
+ export function processHistoricalMessagesWithErrors(
417
514
  messages: HistoricalMessage[],
418
- options: MessageProcessingOptions = {},
515
+ options: MessageProcessingOptions = {}
419
516
  ): ProcessHistoricalMessagesResult {
420
- const {
421
- assistantName = 'Fae',
422
- assistantType = 'fae',
423
- assistantAvatar,
424
- onApprove,
425
- onReject,
426
- chatTypeFilter,
427
- approvalStatuses = {},
428
- // An omitted option means "display every approval type" — the original
429
- // history semantics. Deliberately NOT defaulted to the realtime
430
- // reducer's ['CLIENT']: consumers that pass a wider list to their
431
- // realtime path but omit it on the history path would silently lose
432
- // pending non-CLIENT approval cards on every reload/reconnect refetch
433
- // (they also ignore `escalatedApprovals`). Realtime/history parity is
434
- // opt-in: pass the same explicit list to both.
435
- displayApprovalTypes,
436
- batchApprovalsEnabled,
437
- } = options
517
+ // displayApprovalTypes omitted = display every approval type (original
518
+ // history semantics — see the matching note in processHistoricalMessages).
519
+ const { chatTypeFilter, assistantName = 'Fae', assistantType = 'fae', assistantAvatar, onApprove, onReject, approvalStatuses = {}, displayApprovalTypes, batchApprovalsEnabled } = options
438
520
 
439
521
  const processedMessages: ProcessedMessage[] = []
440
522
  const accumulator = createMessageSegmentAccumulator({ onApprove, onReject })
441
- const escalatedApprovals: EscalatedApprovals = new Map()
523
+ const escalatedApprovals = new Map<
524
+ string,
525
+ { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
526
+ >()
442
527
 
443
528
  let currentAssistantId: string | null = null
444
529
  let currentAssistantTimestamp: Date | null = null
@@ -447,10 +532,6 @@ export function processHistoricalMessages(
447
532
  // — carried onto the flushed message's streamSeq for per-role merge coverage.
448
533
  let currentAssistantStreamSeq: number | undefined
449
534
 
450
- /**
451
- * Flush the current assistant message to processedMessages.
452
- * Uses the LAST message ID in the group for stable React keys across page boundaries.
453
- */
454
535
  const flushAssistantMessage = () => {
455
536
  const idToUse = lastAssistantId || currentAssistantId
456
537
  if (idToUse && accumulator.hasContent()) {
@@ -481,7 +562,6 @@ export function processHistoricalMessages(
481
562
  }
482
563
 
483
564
  messages.forEach((msg, index) => {
484
- // Filter by chat type if specified
485
565
  if (chatTypeFilter && msg.chatType !== chatTypeFilter) return
486
566
 
487
567
  const messageDataArray = Array.isArray(msg.messageData)
@@ -545,10 +625,8 @@ export function processHistoricalMessages(
545
625
  }
546
626
 
547
627
  messageDataArray.forEach((data) => {
548
- const event = decodeHistoricalMessageData(data)
549
- if (!event) return
550
- applyHistoryEvent(
551
- event,
628
+ processMessageData(
629
+ data,
552
630
  accumulator,
553
631
  approvalStatuses,
554
632
  { displayApprovalTypes, batchApprovalsEnabled },
@@ -556,7 +634,6 @@ export function processHistoricalMessages(
556
634
  )
557
635
  })
558
636
 
559
- // Check if we should flush (next message is from user or last message)
560
637
  const nextMsg = messages[index + 1]
561
638
  const isLastMessage = index === messages.length - 1
562
639
  const nextIsFromUser =
@@ -589,52 +666,3 @@ export function processHistoricalMessages(
589
666
  escalatedApprovals: escalatedApprovals
590
667
  }
591
668
  }
592
-
593
- /**
594
- * Extract error messages from historical messages
595
- * Returns a separate array of error messages that should be displayed
596
- */
597
- export function extractErrorMessages(
598
- messages: HistoricalMessage[],
599
- options: MessageProcessingOptions = {}
600
- ): ProcessedMessage[] {
601
- const { assistantName = 'Fae', assistantType = 'fae', assistantAvatar, chatTypeFilter } = options
602
-
603
- const errorMessages: ProcessedMessage[] = []
604
-
605
- messages.forEach((msg) => {
606
- if (chatTypeFilter && msg.chatType !== chatTypeFilter) return
607
-
608
- const messageDataArray = Array.isArray(msg.messageData)
609
- ? msg.messageData
610
- : msg.messageData
611
- ? [msg.messageData]
612
- : []
613
-
614
- messageDataArray.forEach((data) => {
615
- if (data.type === MESSAGE_TYPE.ERROR) {
616
- errorMessages.push({
617
- id: `${msg.id}-error`,
618
- role: 'error',
619
- content: 'error' in data && data.error ? data.error : 'An error occurred',
620
- name: assistantName,
621
- assistantType,
622
- timestamp: new Date(msg.createdAt),
623
- avatar: assistantAvatar,
624
- })
625
- }
626
- })
627
- })
628
-
629
- return errorMessages
630
- }
631
-
632
- /**
633
- * Process messages and include error messages in the correct order.
634
- *
635
- * HISTORICAL ALIAS: this was a byte-identical second copy of
636
- * `processHistoricalMessages` (both goldens snapshot identical output on the
637
- * same corpus). The duplicate envelope was deleted in Phase 3 — kept as an
638
- * alias for the established import sites.
639
- */
640
- export const processHistoricalMessagesWithErrors = processHistoricalMessages