@flamingo-stack/openframe-frontend-core 0.0.458-snapshot.20260720215024 → 0.0.458

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 (281) hide show
  1. package/dist/{chunk-BLZZXZNM.cjs → chunk-5USSSRAL.cjs} +26 -26
  2. package/dist/{chunk-BLZZXZNM.cjs.map → chunk-5USSSRAL.cjs.map} +1 -1
  3. package/dist/{chunk-CGNDAJ7V.js → chunk-6B27O3ZB.js} +4 -4
  4. package/dist/{chunk-P5JQDFRM.cjs → chunk-7IRPMNRL.cjs} +60 -60
  5. package/dist/chunk-7IRPMNRL.cjs.map +1 -0
  6. package/dist/{chunk-4XDWNAZW.cjs → chunk-DQ3EHM6G.cjs} +9 -9
  7. package/dist/{chunk-4XDWNAZW.cjs.map → chunk-DQ3EHM6G.cjs.map} +1 -1
  8. package/dist/{chunk-7KSEINAE.cjs → chunk-FTXDIDNX.cjs} +15 -15
  9. package/dist/{chunk-7KSEINAE.cjs.map → chunk-FTXDIDNX.cjs.map} +1 -1
  10. package/dist/{chunk-4R6H2U77.js → chunk-H7ETLG65.js} +4 -4
  11. package/dist/{chunk-LAOO7XHO.js → chunk-HWTUVLYB.js} +2 -2
  12. package/dist/{chunk-F2NMWV44.js → chunk-IDKDOQZL.js} +2 -2
  13. package/dist/{chunk-BQ7PMWLM.cjs → chunk-KIIOBK7G.cjs} +27 -28
  14. package/dist/chunk-KIIOBK7G.cjs.map +1 -0
  15. package/dist/{chunk-QO2XD3FR.js → chunk-KQGFKG6Y.js} +2 -2
  16. package/dist/{chunk-3N3TMZ62.cjs → chunk-L676XVP4.cjs} +7 -7
  17. package/dist/{chunk-3N3TMZ62.cjs.map → chunk-L676XVP4.cjs.map} +1 -1
  18. package/dist/{chunk-F7F43U7Q.cjs → chunk-LDAOPWXZ.cjs} +11 -11
  19. package/dist/{chunk-F7F43U7Q.cjs.map → chunk-LDAOPWXZ.cjs.map} +1 -1
  20. package/dist/{chunk-ADD3FPDG.cjs → chunk-MKLP2SOZ.cjs} +4886 -7004
  21. package/dist/chunk-MKLP2SOZ.cjs.map +1 -0
  22. package/dist/{chunk-VXJH5TDN.js → chunk-QL7ODLJA.js} +10 -10
  23. package/dist/chunk-QL7ODLJA.js.map +1 -0
  24. package/dist/{chunk-OR5AMXYN.js → chunk-R5QYMUKN.js} +2 -2
  25. package/dist/{chunk-REWRUV3K.js → chunk-TNLFAJPA.js} +9673 -11791
  26. package/dist/chunk-TNLFAJPA.js.map +1 -0
  27. package/dist/{chunk-XF5ROTMQ.js → chunk-U2YDR5DT.js} +2 -2
  28. package/dist/{chunk-PXZ7YQ5V.cjs → chunk-VTQSB6QB.cjs} +5 -5
  29. package/dist/{chunk-PXZ7YQ5V.cjs.map → chunk-VTQSB6QB.cjs.map} +1 -1
  30. package/dist/{chunk-UI7CIUAR.js → chunk-WGJPTFRH.js} +2 -2
  31. package/dist/{chunk-WCE66C2Z.cjs → chunk-XUNJILJT.cjs} +62 -62
  32. package/dist/{chunk-WCE66C2Z.cjs.map → chunk-XUNJILJT.cjs.map} +1 -1
  33. package/dist/{chunk-APF5QBAU.js → chunk-YQBPIUZU.js} +2 -2
  34. package/dist/{chunk-TA7MFVUS.cjs → chunk-YV2YA3QH.cjs} +31 -31
  35. package/dist/{chunk-TA7MFVUS.cjs.map → chunk-YV2YA3QH.cjs.map} +1 -1
  36. package/dist/{chunk-QSGZE6AZ.cjs → chunk-ZDJ4ZUQ4.cjs} +37 -37
  37. package/dist/{chunk-QSGZE6AZ.cjs.map → chunk-ZDJ4ZUQ4.cjs.map} +1 -1
  38. package/dist/{chunk-LJKZIIK4.js → chunk-ZHQQRYMB.js} +5 -6
  39. package/dist/chunk-ZHQQRYMB.js.map +1 -0
  40. package/dist/components/case-studies/index.cjs +8 -8
  41. package/dist/components/case-studies/index.js +2 -2
  42. package/dist/components/chart.d.ts +1 -1
  43. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  44. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  45. package/dist/components/chat/hooks/index.d.ts +1 -0
  46. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  48. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +48 -15
  49. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  50. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +6 -0
  51. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -0
  52. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +47 -3
  53. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  54. package/dist/components/chat/index.cjs +2 -10
  55. package/dist/components/chat/index.cjs.map +1 -1
  56. package/dist/components/chat/index.d.ts +0 -1
  57. package/dist/components/chat/index.d.ts.map +1 -1
  58. package/dist/components/chat/index.js +13 -21
  59. package/dist/components/chat/types/api.types.d.ts +81 -1
  60. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  61. package/dist/components/chat/types/processing.types.d.ts +90 -2
  62. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  63. package/dist/components/chat/utils/chunk-parser.d.ts +25 -0
  64. package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -0
  65. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +6 -34
  66. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  67. package/dist/components/chat/utils/index.d.ts +3 -2
  68. package/dist/components/chat/utils/index.d.ts.map +1 -1
  69. package/dist/components/chat/utils/process-historical-messages.d.ts +11 -59
  70. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  71. package/dist/components/contact/index.cjs +3 -3
  72. package/dist/components/contact/index.js +2 -2
  73. package/dist/components/docs/index.cjs +5 -5
  74. package/dist/components/docs/index.js +4 -4
  75. package/dist/components/embeds/index.cjs +3 -3
  76. package/dist/components/embeds/index.js +2 -2
  77. package/dist/components/faq/index.cjs +3 -3
  78. package/dist/components/faq/index.js +2 -2
  79. package/dist/components/features/index.cjs +2 -2
  80. package/dist/components/features/index.js +1 -1
  81. package/dist/components/help-center-pages/index.cjs +20 -20
  82. package/dist/components/help-center-pages/index.js +11 -11
  83. package/dist/components/index.cjs +129 -171
  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/onboarding-guides/index.cjs +5 -5
  90. package/dist/components/onboarding-guides/index.js +4 -4
  91. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  92. package/dist/components/related-content/index.cjs +3 -3
  93. package/dist/components/related-content/index.js +2 -2
  94. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  95. package/dist/components/tickets/index.cjs +5 -5
  96. package/dist/components/tickets/index.js +4 -4
  97. package/dist/components/ui/index.cjs +2 -44
  98. package/dist/components/ui/index.cjs.map +1 -1
  99. package/dist/components/ui/index.d.ts +2 -1
  100. package/dist/components/ui/index.d.ts.map +1 -1
  101. package/dist/components/ui/index.js +13 -55
  102. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  103. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  104. package/dist/components/ui/simple-markdown-renderer.d.ts +73 -0
  105. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -0
  106. package/dist/index.cjs +2 -60
  107. package/dist/index.cjs.map +1 -1
  108. package/dist/index.js +13 -71
  109. package/dist/utils/index.cjs +40 -169
  110. package/dist/utils/index.cjs.map +1 -1
  111. package/dist/utils/index.d.ts +0 -1
  112. package/dist/utils/index.d.ts.map +1 -1
  113. package/dist/utils/index.js +41 -162
  114. package/dist/utils/index.js.map +1 -1
  115. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  116. package/package.json +1 -7
  117. package/src/components/chat/__tests__/chat-message-list.test.tsx +5 -293
  118. package/src/components/chat/chat-message-enhanced.tsx +17 -111
  119. package/src/components/chat/chat-message-list.tsx +113 -367
  120. package/src/components/chat/embeddable-chat.tsx +0 -6
  121. package/src/components/chat/hooks/.index.md +1 -1
  122. package/src/components/chat/hooks/.use-nats-chat-adapter.md +1 -1
  123. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +50 -0
  124. package/src/components/chat/hooks/index.ts +1 -0
  125. package/src/components/chat/hooks/use-chat.ts +0 -8
  126. package/src/components/chat/hooks/use-nats-chat-adapter.ts +822 -243
  127. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +479 -0
  128. package/src/components/chat/hooks/use-sse-chat-adapter.ts +666 -318
  129. package/src/components/chat/index.ts +0 -5
  130. package/src/components/chat/thinking-display.tsx +1 -1
  131. package/src/components/chat/types/.api.types.md +3 -1
  132. package/src/components/chat/types/.processing.types.md +23 -6
  133. package/src/components/chat/types/api.types.ts +73 -6
  134. package/src/components/chat/types/processing.types.ts +52 -11
  135. package/src/components/chat/utils/.chunk-parser.md +51 -0
  136. package/src/components/chat/utils/.index.md +5 -0
  137. package/src/components/chat/utils/chunk-parser.ts +255 -0
  138. package/src/components/chat/utils/extract-incomplete-message-state.ts +5 -63
  139. package/src/components/chat/utils/index.ts +8 -5
  140. package/src/components/chat/utils/process-historical-messages.ts +383 -352
  141. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +4 -22
  142. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  143. package/src/components/shared/product-release/release-detail-page.tsx +4 -19
  144. package/src/components/ui/.simple-markdown-renderer.md +40 -0
  145. package/src/components/ui/index.ts +2 -1
  146. package/src/components/ui/release-changelog-section.tsx +1 -1
  147. package/src/components/ui/rich-markdown-renderer.tsx +1223 -0
  148. package/src/components/ui/simple-markdown-renderer.tsx +965 -0
  149. package/src/components/ui/ticket-info-section.tsx +1 -1
  150. package/src/utils/index.ts +0 -1
  151. package/src/utils/markdown-section-extractor.ts +56 -41
  152. package/dist/chat-protocol/decode.d.ts +0 -58
  153. package/dist/chat-protocol/decode.d.ts.map +0 -1
  154. package/dist/chat-protocol/encode.d.ts +0 -44
  155. package/dist/chat-protocol/encode.d.ts.map +0 -1
  156. package/dist/chat-protocol/env-flag.d.ts +0 -35
  157. package/dist/chat-protocol/env-flag.d.ts.map +0 -1
  158. package/dist/chat-protocol/events.d.ts +0 -192
  159. package/dist/chat-protocol/events.d.ts.map +0 -1
  160. package/dist/chat-protocol/frames.d.ts +0 -206
  161. package/dist/chat-protocol/frames.d.ts.map +0 -1
  162. package/dist/chat-protocol/index.cjs +0 -493
  163. package/dist/chat-protocol/index.cjs.map +0 -1
  164. package/dist/chat-protocol/index.d.ts +0 -18
  165. package/dist/chat-protocol/index.d.ts.map +0 -1
  166. package/dist/chat-protocol/index.js +0 -477
  167. package/dist/chat-protocol/index.js.map +0 -1
  168. package/dist/chat-protocol/ip-normalize.d.ts +0 -44
  169. package/dist/chat-protocol/ip-normalize.d.ts.map +0 -1
  170. package/dist/chat-protocol/nats-decoder.d.ts +0 -26
  171. package/dist/chat-protocol/nats-decoder.d.ts.map +0 -1
  172. package/dist/chunk-ADD3FPDG.cjs.map +0 -1
  173. package/dist/chunk-BQ7PMWLM.cjs.map +0 -1
  174. package/dist/chunk-LJKZIIK4.js.map +0 -1
  175. package/dist/chunk-P5JQDFRM.cjs.map +0 -1
  176. package/dist/chunk-REWRUV3K.js.map +0 -1
  177. package/dist/chunk-VXJH5TDN.js.map +0 -1
  178. package/dist/components/chat/stream/chat-dialog-store.d.ts +0 -184
  179. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +0 -1
  180. package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -367
  181. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +0 -1
  182. package/dist/components/chat/stream/delta-batcher.d.ts +0 -51
  183. package/dist/components/chat/stream/delta-batcher.d.ts.map +0 -1
  184. package/dist/components/chat/stream/index.d.ts +0 -17
  185. package/dist/components/chat/stream/index.d.ts.map +0 -1
  186. package/dist/components/chat/stream/message-mutations.d.ts +0 -77
  187. package/dist/components/chat/stream/message-mutations.d.ts.map +0 -1
  188. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +0 -22
  189. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +0 -1
  190. package/dist/components/ui/markdown/base-components.d.ts +0 -56
  191. package/dist/components/ui/markdown/base-components.d.ts.map +0 -1
  192. package/dist/components/ui/markdown/engine.d.ts +0 -99
  193. package/dist/components/ui/markdown/engine.d.ts.map +0 -1
  194. package/dist/components/ui/markdown/heading-ids.d.ts +0 -65
  195. package/dist/components/ui/markdown/heading-ids.d.ts.map +0 -1
  196. package/dist/components/ui/markdown/index.d.ts +0 -18
  197. package/dist/components/ui/markdown/index.d.ts.map +0 -1
  198. package/dist/components/ui/markdown/mermaid-diagram.d.ts +0 -79
  199. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +0 -1
  200. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +0 -14
  201. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +0 -1
  202. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -40
  203. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +0 -1
  204. package/dist/components/ui/markdown/rich/shortcodes.d.ts +0 -8
  205. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +0 -1
  206. package/dist/components/ui/markdown/sanitize.d.ts +0 -153
  207. package/dist/components/ui/markdown/sanitize.d.ts.map +0 -1
  208. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +0 -23
  209. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +0 -1
  210. package/dist/components/ui/markdown/streaming.d.ts +0 -78
  211. package/dist/components/ui/markdown/streaming.d.ts.map +0 -1
  212. package/dist/components/ui/markdown/text-size.d.ts +0 -34
  213. package/dist/components/ui/markdown/text-size.d.ts.map +0 -1
  214. package/dist/utils/markdown-fences.d.ts +0 -42
  215. package/dist/utils/markdown-fences.d.ts.map +0 -1
  216. package/dist/utils/markdown-heading-id.d.ts +0 -85
  217. package/dist/utils/markdown-heading-id.d.ts.map +0 -1
  218. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -307
  219. package/src/chat-protocol/__tests__/chat-protocol.test.ts +0 -515
  220. package/src/chat-protocol/__tests__/env-flag.test.ts +0 -52
  221. package/src/chat-protocol/__tests__/ip-normalize.test.ts +0 -136
  222. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -228
  223. package/src/chat-protocol/decode.ts +0 -277
  224. package/src/chat-protocol/encode.ts +0 -71
  225. package/src/chat-protocol/env-flag.ts +0 -39
  226. package/src/chat-protocol/events.ts +0 -228
  227. package/src/chat-protocol/frames.ts +0 -238
  228. package/src/chat-protocol/index.ts +0 -21
  229. package/src/chat-protocol/ip-normalize.ts +0 -146
  230. package/src/chat-protocol/nats-decoder.ts +0 -252
  231. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -207
  232. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -209
  233. package/src/components/chat/hooks/__tests__/__snapshots__/persisted-state-v1-golden.test.ts.snap +0 -297
  234. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  235. package/src/components/chat/hooks/__tests__/persisted-state-v1-golden.test.ts +0 -272
  236. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +0 -318
  237. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +0 -937
  238. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +0 -756
  239. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +0 -460
  240. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -894
  241. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +0 -255
  242. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +0 -98
  243. package/src/components/chat/stream/chat-dialog-store.ts +0 -536
  244. package/src/components/chat/stream/chat-stream-reducer.ts +0 -1796
  245. package/src/components/chat/stream/delta-batcher.ts +0 -159
  246. package/src/components/chat/stream/index.ts +0 -55
  247. package/src/components/chat/stream/message-mutations.ts +0 -338
  248. package/src/components/chat/stream/use-chat-stream-reducer.ts +0 -126
  249. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +0 -418
  250. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +0 -599
  251. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -317
  252. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +0 -270
  253. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +0 -2420
  254. package/src/components/ui/__tests__/markdown-parity.test.tsx +0 -2528
  255. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +0 -115
  256. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +0 -95
  257. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +0 -309
  258. package/src/components/ui/markdown/__tests__/streaming.test.tsx +0 -388
  259. package/src/components/ui/markdown/base-components.tsx +0 -341
  260. package/src/components/ui/markdown/engine.tsx +0 -315
  261. package/src/components/ui/markdown/heading-ids.ts +0 -239
  262. package/src/components/ui/markdown/index.ts +0 -49
  263. package/src/components/ui/markdown/mermaid-diagram.tsx +0 -291
  264. package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -199
  265. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +0 -184
  266. package/src/components/ui/markdown/rich/shortcodes.ts +0 -170
  267. package/src/components/ui/markdown/sanitize.ts +0 -3499
  268. package/src/components/ui/markdown/simple-markdown-renderer.tsx +0 -28
  269. package/src/components/ui/markdown/streaming.ts +0 -362
  270. package/src/components/ui/markdown/text-size.ts +0 -106
  271. package/src/utils/markdown-fences.ts +0 -127
  272. package/src/utils/markdown-heading-id.ts +0 -348
  273. /package/dist/{chunk-CGNDAJ7V.js.map → chunk-6B27O3ZB.js.map} +0 -0
  274. /package/dist/{chunk-4R6H2U77.js.map → chunk-H7ETLG65.js.map} +0 -0
  275. /package/dist/{chunk-LAOO7XHO.js.map → chunk-HWTUVLYB.js.map} +0 -0
  276. /package/dist/{chunk-F2NMWV44.js.map → chunk-IDKDOQZL.js.map} +0 -0
  277. /package/dist/{chunk-QO2XD3FR.js.map → chunk-KQGFKG6Y.js.map} +0 -0
  278. /package/dist/{chunk-OR5AMXYN.js.map → chunk-R5QYMUKN.js.map} +0 -0
  279. /package/dist/{chunk-XF5ROTMQ.js.map → chunk-U2YDR5DT.js.map} +0 -0
  280. /package/dist/{chunk-UI7CIUAR.js.map → chunk-WGJPTFRH.js.map} +0 -0
  281. /package/dist/{chunk-APF5QBAU.js.map → chunk-YQBPIUZU.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,45 +53,232 @@ 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.EXECUTING_TOOL:
115
279
  if ('integratedToolType' in data) {
116
- return {
117
- type: 'tool-execution',
280
+ accumulator.addToolExecution({
281
+ type: 'tool_execution',
118
282
  data: {
119
283
  type: 'EXECUTING_TOOL',
120
284
  integratedToolType: data.integratedToolType || '',
@@ -123,14 +287,14 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
123
287
  parameters: data.parameters,
124
288
  toolExecutionRequestId: data.toolExecutionRequestId,
125
289
  },
126
- }
290
+ })
127
291
  }
128
- return null
292
+ break
129
293
 
130
294
  case MESSAGE_TYPE.EXECUTED_TOOL:
131
295
  if ('integratedToolType' in data) {
132
- return {
133
- type: 'tool-execution',
296
+ accumulator.addToolExecution({
297
+ type: 'tool_execution',
134
298
  data: {
135
299
  type: 'EXECUTED_TOOL',
136
300
  integratedToolType: data.integratedToolType || '',
@@ -141,294 +305,218 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
141
305
  success: data.success,
142
306
  toolExecutionRequestId: data.toolExecutionRequestId,
143
307
  },
144
- }
308
+ })
145
309
  }
146
- return null
310
+ break
147
311
 
148
312
  case MESSAGE_TYPE.APPROVAL_REQUEST:
149
313
  if ('approvalRequestId' in data && data.approvalRequestId) {
150
- return {
151
- type: 'approval-request',
152
- requestId: data.approvalRequestId,
153
- approvalType: data.approvalType || 'CLIENT',
154
- command: data.command || '',
155
- explanation: data.explanation,
156
- ...(Array.isArray(data.toolCalls) ? { toolCalls: data.toolCalls } : {}),
314
+ const approvalType = data.approvalType || 'CLIENT'
315
+ const toolCalls: PendingToolCallData[] | undefined = Array.isArray(data.toolCalls)
316
+ ? data.toolCalls
317
+ : undefined
318
+ const isBatch = !!toolCalls && toolCalls.length > 0
319
+
320
+ if (!displayApprovalTypes || displayApprovalTypes.includes(approvalType)) {
321
+ if (isBatch) {
322
+ const status = (approvalStatuses[data.approvalRequestId] as ChatApprovalStatus) || 'pending'
323
+ if (batchApprovalsEnabled) {
324
+ accumulator.addApprovalBatch(data.approvalRequestId, approvalType, toolCalls!, status)
325
+ } else {
326
+ // Flag OFF — unfold batch into N legacy approval cards (same id).
327
+ for (const call of toolCalls!) {
328
+ if (!call.requiresApproval) continue
329
+ accumulator.addApprovalRequest(
330
+ data.approvalRequestId,
331
+ getCommandText(call),
332
+ call.toolExplanation,
333
+ approvalType,
334
+ status,
335
+ )
336
+ }
337
+ }
338
+ } else {
339
+ // The resolution may already be known to the consumer (realtime
340
+ // flipped it and fed it back via `approvalStatuses`) while the
341
+ // matching APPROVAL_RESULT row is absent from the fetched history
342
+ // pages. Without this, the single-approval path tracks it as
343
+ // pending and `flushPendingApprovals()` resurrects it as a stale
344
+ // sticky card on every history re-process. Mirror the batch path
345
+ // and honor `approvalStatuses`.
346
+ const resolvedStatus = approvalStatuses[data.approvalRequestId] as ChatApprovalStatus | undefined
347
+ if (resolvedStatus === 'approved' || resolvedStatus === 'rejected') {
348
+ accumulator.addApprovalRequest(
349
+ data.approvalRequestId,
350
+ data.command || '',
351
+ data.explanation,
352
+ approvalType,
353
+ resolvedStatus,
354
+ )
355
+ } else {
356
+ accumulator.trackApprovalRequest(data.approvalRequestId, {
357
+ command: data.command || '',
358
+ explanation: data.explanation,
359
+ approvalType,
360
+ })
361
+ }
362
+ }
363
+ } else {
364
+ escalatedApprovals?.set(data.approvalRequestId, {
365
+ command: data.command || '',
366
+ explanation: data.explanation,
367
+ approvalType,
368
+ ...(isBatch ? { toolCalls } : {}),
369
+ })
157
370
  }
158
371
  }
159
- return null
372
+ break
160
373
 
161
374
  case MESSAGE_TYPE.APPROVAL_RESULT:
162
375
  if ('approvalRequestId' in data && data.approvalRequestId) {
163
- return {
164
- type: 'approval-resolved',
165
- requestId: data.approvalRequestId,
166
- status: data.approved ? 'approved' : 'rejected',
167
- approvalType: data.approvalType,
168
- resolvedByName: 'resolvedByName' in data ? data.resolvedByName : undefined,
169
- }
170
- }
171
- return null
172
-
173
- case MESSAGE_TYPE.ERROR:
174
- if ('error' in data) {
175
- return {
176
- type: 'error',
177
- title: data.error || 'An error occurred',
178
- details: 'details' in data ? data.details : undefined,
179
- }
180
- }
181
- return null
182
-
183
- case MESSAGE_TYPE.CONTEXT_COMPACTION_START:
184
- return { type: 'compaction', phase: 'start' }
185
-
186
- case MESSAGE_TYPE.CONTEXT_COMPACTION_END:
187
- return {
188
- type: 'compaction',
189
- phase: 'end',
190
- summary: 'summary' in data && typeof data.summary === 'string' ? data.summary : undefined,
191
- }
192
-
193
- case MESSAGE_TYPE.SYSTEM:
194
- if ('text' in data && data.text) {
195
- return { type: 'participant', kind: 'system', text: data.text }
196
- }
197
- return null
198
-
199
- default:
200
- // Unknown message type — ignore.
201
- return null
202
- }
203
- }
204
-
205
- // =============================================================================
206
- // Replay — decoded event → per-turn kernel (history approval semantics)
207
- // =============================================================================
208
-
209
- type EscalatedApprovals = Map<
210
- string,
211
- { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
212
- >
213
-
214
- /**
215
- * Replay one decoded event into the shared per-turn segment kernel with the
216
- * HISTORY approval semantics (an omitted `displayApprovalTypes` means
217
- * "display every approval type" — the original history behavior; the
218
- * realtime reducer defaults to `['CLIENT']`).
219
- */
220
- function applyHistoryEvent(
221
- event: ChatStreamEvent,
222
- accumulator: MessageSegmentAccumulator,
223
- approvalStatuses: Record<string, string>,
224
- options: MessageProcessingOptions,
225
- escalatedApprovals?: EscalatedApprovals,
226
- ): void {
227
- // batchApprovalsEnabled is owned by the consumer (oss-tenant chat client /
228
- // openframe-frontend tickets). Defaults to ON so consumers that haven't
229
- // wired the flag yet get the batch UI; pass `false` to force legacy.
230
- const { displayApprovalTypes, batchApprovalsEnabled = true } = options
231
-
232
- switch (event.type) {
233
- case 'text-delta':
234
- accumulator.appendText(event.text)
235
- break
236
-
237
- case 'thinking-delta':
238
- accumulator.appendThinking(event.text)
239
- break
376
+ const existingStatus = approvalStatuses[data.approvalRequestId] as ChatApprovalStatus | undefined
377
+ const status: ChatApprovalStatus = existingStatus || (data.approved ? 'approved' : 'rejected')
378
+ const resolvedByName = 'resolvedByName' in data ? data.resolvedByName : undefined
379
+ const escalatedData = escalatedApprovals?.get(data.approvalRequestId)
240
380
 
241
- case 'tool-execution':
242
- accumulator.addToolExecution({ type: 'tool_execution', data: event.data })
243
- break
244
-
245
- case 'approval-request': {
246
- const approvalType = event.approvalType || 'CLIENT'
247
- const toolCalls = event.toolCalls as PendingToolCallData[] | undefined
248
- const isBatch = !!toolCalls && toolCalls.length > 0
249
-
250
- if (!displayApprovalTypes || displayApprovalTypes.includes(approvalType)) {
251
- if (isBatch) {
252
- const status = (approvalStatuses[event.requestId] as ChatApprovalStatus) || 'pending'
381
+ if (escalatedData?.toolCalls && escalatedData.toolCalls.length > 0) {
253
382
  if (batchApprovalsEnabled) {
254
- accumulator.addApprovalBatch(event.requestId, approvalType, toolCalls!, status)
383
+ accumulator.addApprovalBatch(
384
+ data.approvalRequestId,
385
+ escalatedData.approvalType,
386
+ escalatedData.toolCalls,
387
+ status,
388
+ undefined,
389
+ resolvedByName,
390
+ )
255
391
  } else {
256
- // Flag OFF — unfold batch into N legacy approval cards (same id).
257
- for (const call of toolCalls!) {
392
+ for (const call of escalatedData.toolCalls) {
258
393
  if (!call.requiresApproval) continue
259
394
  accumulator.addApprovalRequest(
260
- event.requestId,
395
+ data.approvalRequestId,
261
396
  getCommandText(call),
262
397
  call.toolExplanation,
263
- approvalType,
398
+ escalatedData.approvalType,
264
399
  status,
265
400
  )
266
401
  }
267
402
  }
268
- } else {
269
- // The resolution may already be known to the consumer (realtime
270
- // flipped it and fed it back via `approvalStatuses`) while the
271
- // matching APPROVAL_RESULT row is absent from the fetched history
272
- // pages. Without this, the single-approval path tracks it as
273
- // pending and `flushPendingApprovals()` resurrects it as a stale
274
- // sticky card on every history re-process. Mirror the batch path
275
- // and honor `approvalStatuses`.
276
- const resolvedStatus = approvalStatuses[event.requestId] as ChatApprovalStatus | undefined
277
- if (resolvedStatus === 'approved' || resolvedStatus === 'rejected') {
278
- accumulator.addApprovalRequest(
279
- event.requestId,
280
- event.command || '',
281
- event.explanation,
282
- approvalType,
283
- resolvedStatus,
284
- )
285
- } else {
286
- accumulator.trackApprovalRequest(event.requestId, {
287
- command: event.command || '',
288
- explanation: event.explanation,
289
- approvalType,
290
- })
291
- }
403
+ escalatedApprovals?.delete(data.approvalRequestId)
404
+ break
292
405
  }
293
- } else {
294
- escalatedApprovals?.set(event.requestId, {
295
- command: event.command || '',
296
- explanation: event.explanation,
297
- approvalType,
298
- ...(isBatch ? { toolCalls } : {}),
299
- })
300
- }
301
- break
302
- }
303
406
 
304
- case 'approval-resolved': {
305
- const requestId = event.requestId
306
- if (!requestId) break
307
- const existingStatus = approvalStatuses[requestId] as ChatApprovalStatus | undefined
308
- const status: ChatApprovalStatus = existingStatus || event.status
309
- const resolvedByName = event.resolvedByName
310
- const escalatedData = escalatedApprovals?.get(requestId)
311
-
312
- if (escalatedData?.toolCalls && escalatedData.toolCalls.length > 0) {
313
- if (batchApprovalsEnabled) {
314
- accumulator.addApprovalBatch(
315
- requestId,
316
- escalatedData.approvalType,
317
- escalatedData.toolCalls,
318
- status,
319
- undefined,
320
- resolvedByName,
321
- )
322
- } else {
323
- for (const call of escalatedData.toolCalls) {
324
- if (!call.requiresApproval) continue
325
- accumulator.addApprovalRequest(
326
- requestId,
327
- getCommandText(call),
328
- call.toolExplanation,
329
- escalatedData.approvalType,
330
- status,
331
- )
332
- }
407
+ if (escalatedData) {
408
+ accumulator.trackApprovalRequest(data.approvalRequestId, {
409
+ command: escalatedData.command,
410
+ explanation: escalatedData.explanation,
411
+ approvalType: escalatedData.approvalType,
412
+ })
413
+ escalatedApprovals?.delete(data.approvalRequestId)
333
414
  }
334
- escalatedApprovals?.delete(requestId)
335
- break
336
- }
337
415
 
338
- if (escalatedData) {
339
- accumulator.trackApprovalRequest(requestId, {
340
- command: escalatedData.command,
341
- explanation: escalatedData.explanation,
342
- approvalType: escalatedData.approvalType,
343
- })
344
- escalatedApprovals?.delete(requestId)
416
+ // If a segment with this id is already present (batch or legacy), just flip its status.
417
+ // updateApprovalStatus matches both `approval_batch` and `approval_request` segments.
418
+ const before = accumulator.getSegments()
419
+ const after = accumulator.updateApprovalStatus(data.approvalRequestId, status, resolvedByName)
420
+ const updatedExisting = before.some((s, i) => after[i] !== s)
421
+ if (updatedExisting) break
422
+
423
+ accumulator.processApprovalResult(
424
+ data.approvalRequestId,
425
+ status === 'approved',
426
+ data.approvalType || 'USER'
427
+ )
345
428
  }
346
-
347
- // If a segment with this id is already present (batch or legacy), just
348
- // flip its status. updateApprovalStatus matches both `approval_batch`
349
- // and `approval_request` segments.
350
- const before = accumulator.getSegments()
351
- const after = accumulator.updateApprovalStatus(requestId, status, resolvedByName)
352
- const updatedExisting = before.some((s, i) => after[i] !== s)
353
- if (updatedExisting) break
354
-
355
- accumulator.processApprovalResult(
356
- requestId,
357
- status === 'approved',
358
- event.approvalType || 'USER',
359
- )
360
429
  break
361
- }
362
430
 
363
- case 'error': {
364
- let message: string | undefined
365
- if (event.details) {
366
- try {
367
- message = JSON.parse(event.details)?.error?.message
368
- } catch {
369
- message = event.details
431
+ case MESSAGE_TYPE.ERROR:
432
+ if ('error' in data) {
433
+ let message: string | undefined
434
+ if ('details' in data && data?.details) {
435
+ try {
436
+ message = JSON.parse(data.details)?.error?.message
437
+ } catch {
438
+ message = data.details
439
+ }
370
440
  }
441
+ accumulator.addError(
442
+ data.error || 'An error occurred',
443
+ message
444
+ )
371
445
  }
372
- accumulator.addError(event.title, message)
373
446
  break
374
- }
375
447
 
376
- case 'compaction':
377
- if (event.phase === 'start') {
378
- accumulator.addContextCompaction()
379
- } else {
380
- accumulator.completeContextCompaction(event.summary)
381
- }
448
+ case MESSAGE_TYPE.CONTEXT_COMPACTION_START:
449
+ accumulator.addContextCompaction()
382
450
  break
383
451
 
452
+ case MESSAGE_TYPE.CONTEXT_COMPACTION_END: {
453
+ const summary = 'summary' in data && typeof data.summary === 'string' ? data.summary : undefined
454
+ accumulator.completeContextCompaction(summary)
455
+ break
456
+ }
457
+
384
458
  default:
385
- // Participant/system rows are envelope concerns; everything else is
386
- // realtime-only vocabulary that never appears in persisted rows.
459
+ // Unknown message type - ignore
387
460
  break
388
461
  }
389
462
  }
390
463
 
391
- // =============================================================================
392
- // Envelope — ONE implementation (previous duplicate deleted)
393
- // =============================================================================
394
-
395
464
  /**
396
- * Result type for historical message processing
465
+ * Extract error messages from historical messages
466
+ * Returns a separate array of error messages that should be displayed
397
467
  */
398
- export interface ProcessHistoricalMessagesResult {
399
- messages: ProcessedMessage[]
400
- escalatedApprovals: EscalatedApprovals
468
+ export function extractErrorMessages(
469
+ messages: HistoricalMessage[],
470
+ options: MessageProcessingOptions = {}
471
+ ): ProcessedMessage[] {
472
+ const { assistantName = 'Fae', assistantType = 'fae', assistantAvatar, chatTypeFilter } = options
473
+
474
+ const errorMessages: ProcessedMessage[] = []
475
+
476
+ messages.forEach((msg) => {
477
+ if (chatTypeFilter && msg.chatType !== chatTypeFilter) return
478
+
479
+ const messageDataArray = Array.isArray(msg.messageData)
480
+ ? msg.messageData
481
+ : msg.messageData
482
+ ? [msg.messageData]
483
+ : []
484
+
485
+ messageDataArray.forEach((data) => {
486
+ if (data.type === MESSAGE_TYPE.ERROR) {
487
+ errorMessages.push({
488
+ id: `${msg.id}-error`,
489
+ role: 'error',
490
+ content: 'error' in data && data.error ? data.error : 'An error occurred',
491
+ name: assistantName,
492
+ assistantType,
493
+ timestamp: new Date(msg.createdAt),
494
+ avatar: assistantAvatar,
495
+ })
496
+ }
497
+ })
498
+ })
499
+
500
+ return errorMessages
401
501
  }
402
502
 
403
503
  /**
404
- * Process an array of historical messages into display-ready format
504
+ * Process messages and include error messages in the correct order
405
505
  */
406
- export function processHistoricalMessages(
506
+ export function processHistoricalMessagesWithErrors(
407
507
  messages: HistoricalMessage[],
408
- options: MessageProcessingOptions = {},
508
+ options: MessageProcessingOptions = {}
409
509
  ): ProcessHistoricalMessagesResult {
410
- const {
411
- assistantName = 'Fae',
412
- assistantType = 'fae',
413
- assistantAvatar,
414
- onApprove,
415
- onReject,
416
- chatTypeFilter,
417
- approvalStatuses = {},
418
- // An omitted option means "display every approval type" — the original
419
- // history semantics. Deliberately NOT defaulted to the realtime
420
- // reducer's ['CLIENT']: consumers that pass a wider list to their
421
- // realtime path but omit it on the history path would silently lose
422
- // pending non-CLIENT approval cards on every reload/reconnect refetch
423
- // (they also ignore `escalatedApprovals`). Realtime/history parity is
424
- // opt-in: pass the same explicit list to both.
425
- displayApprovalTypes,
426
- batchApprovalsEnabled,
427
- } = options
510
+ // displayApprovalTypes omitted = display every approval type (original
511
+ // history semantics — see the matching note in processHistoricalMessages).
512
+ const { chatTypeFilter, assistantName = 'Fae', assistantType = 'fae', assistantAvatar, onApprove, onReject, approvalStatuses = {}, displayApprovalTypes, batchApprovalsEnabled } = options
428
513
 
429
514
  const processedMessages: ProcessedMessage[] = []
430
515
  const accumulator = createMessageSegmentAccumulator({ onApprove, onReject })
431
- const escalatedApprovals: EscalatedApprovals = new Map()
516
+ const escalatedApprovals = new Map<
517
+ string,
518
+ { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
519
+ >()
432
520
 
433
521
  let currentAssistantId: string | null = null
434
522
  let currentAssistantTimestamp: Date | null = null
@@ -437,10 +525,6 @@ export function processHistoricalMessages(
437
525
  // — carried onto the flushed message's streamSeq for per-role merge coverage.
438
526
  let currentAssistantStreamSeq: number | undefined
439
527
 
440
- /**
441
- * Flush the current assistant message to processedMessages.
442
- * Uses the LAST message ID in the group for stable React keys across page boundaries.
443
- */
444
528
  const flushAssistantMessage = () => {
445
529
  const idToUse = lastAssistantId || currentAssistantId
446
530
  if (idToUse && accumulator.hasContent()) {
@@ -471,7 +555,6 @@ export function processHistoricalMessages(
471
555
  }
472
556
 
473
557
  messages.forEach((msg, index) => {
474
- // Filter by chat type if specified
475
558
  if (chatTypeFilter && msg.chatType !== chatTypeFilter) return
476
559
 
477
560
  const messageDataArray = Array.isArray(msg.messageData)
@@ -535,10 +618,8 @@ export function processHistoricalMessages(
535
618
  }
536
619
 
537
620
  messageDataArray.forEach((data) => {
538
- const event = decodeHistoricalMessageData(data)
539
- if (!event) return
540
- applyHistoryEvent(
541
- event,
621
+ processMessageData(
622
+ data,
542
623
  accumulator,
543
624
  approvalStatuses,
544
625
  { displayApprovalTypes, batchApprovalsEnabled },
@@ -546,7 +627,6 @@ export function processHistoricalMessages(
546
627
  )
547
628
  })
548
629
 
549
- // Check if we should flush (next message is from user or last message)
550
630
  const nextMsg = messages[index + 1]
551
631
  const isLastMessage = index === messages.length - 1
552
632
  const nextIsFromUser =
@@ -579,52 +659,3 @@ export function processHistoricalMessages(
579
659
  escalatedApprovals: escalatedApprovals
580
660
  }
581
661
  }
582
-
583
- /**
584
- * Extract error messages from historical messages
585
- * Returns a separate array of error messages that should be displayed
586
- */
587
- export function extractErrorMessages(
588
- messages: HistoricalMessage[],
589
- options: MessageProcessingOptions = {}
590
- ): ProcessedMessage[] {
591
- const { assistantName = 'Fae', assistantType = 'fae', assistantAvatar, chatTypeFilter } = options
592
-
593
- const errorMessages: ProcessedMessage[] = []
594
-
595
- messages.forEach((msg) => {
596
- if (chatTypeFilter && msg.chatType !== chatTypeFilter) return
597
-
598
- const messageDataArray = Array.isArray(msg.messageData)
599
- ? msg.messageData
600
- : msg.messageData
601
- ? [msg.messageData]
602
- : []
603
-
604
- messageDataArray.forEach((data) => {
605
- if (data.type === MESSAGE_TYPE.ERROR) {
606
- errorMessages.push({
607
- id: `${msg.id}-error`,
608
- role: 'error',
609
- content: 'error' in data && data.error ? data.error : 'An error occurred',
610
- name: assistantName,
611
- assistantType,
612
- timestamp: new Date(msg.createdAt),
613
- avatar: assistantAvatar,
614
- })
615
- }
616
- })
617
- })
618
-
619
- return errorMessages
620
- }
621
-
622
- /**
623
- * Process messages and include error messages in the correct order.
624
- *
625
- * HISTORICAL ALIAS: this was a byte-identical second copy of
626
- * `processHistoricalMessages` (both goldens snapshot identical output on the
627
- * same corpus). The duplicate envelope was deleted in Phase 3 — kept as an
628
- * alias for the established import sites.
629
- */
630
- export const processHistoricalMessagesWithErrors = processHistoricalMessages