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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/dist/chat-protocol/decode.d.ts +58 -0
  2. package/dist/chat-protocol/decode.d.ts.map +1 -0
  3. package/dist/chat-protocol/encode.d.ts +44 -0
  4. package/dist/chat-protocol/encode.d.ts.map +1 -0
  5. package/dist/chat-protocol/env-flag.d.ts +35 -0
  6. package/dist/chat-protocol/env-flag.d.ts.map +1 -0
  7. package/dist/chat-protocol/events.d.ts +207 -0
  8. package/dist/chat-protocol/events.d.ts.map +1 -0
  9. package/dist/chat-protocol/frames.d.ts +213 -0
  10. package/dist/chat-protocol/frames.d.ts.map +1 -0
  11. package/dist/chat-protocol/index.cjs +500 -0
  12. package/dist/chat-protocol/index.cjs.map +1 -0
  13. package/dist/chat-protocol/index.d.ts +18 -0
  14. package/dist/chat-protocol/index.d.ts.map +1 -0
  15. package/dist/chat-protocol/index.js +484 -0
  16. package/dist/chat-protocol/index.js.map +1 -0
  17. package/dist/chat-protocol/ip-normalize.d.ts +44 -0
  18. package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
  19. package/dist/chat-protocol/nats-decoder.d.ts +26 -0
  20. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
  21. package/dist/{chunk-WLM3F3WU.js → chunk-34TDA22K.js} +3 -3
  22. package/dist/{chunk-EGXCWZYP.js → chunk-3LKB722U.js} +3 -3
  23. package/dist/{chunk-RW2LPL72.js → chunk-6WTCJI5R.js} +2 -2
  24. package/dist/{chunk-VUXNXWOQ.cjs → chunk-7N7YZCHR.cjs} +7503 -5167
  25. package/dist/chunk-7N7YZCHR.cjs.map +1 -0
  26. package/dist/{chunk-AW63ET2A.cjs → chunk-A6FPYAUD.cjs} +19 -19
  27. package/dist/{chunk-AW63ET2A.cjs.map → chunk-A6FPYAUD.cjs.map} +1 -1
  28. package/dist/{chunk-BZMOEWWS.js → chunk-BEEPU44B.js} +12 -12
  29. package/dist/chunk-BEEPU44B.js.map +1 -0
  30. package/dist/{chunk-EJQUYZXF.cjs → chunk-C65NCIMV.cjs} +12 -12
  31. package/dist/{chunk-EJQUYZXF.cjs.map → chunk-C65NCIMV.cjs.map} +1 -1
  32. package/dist/{chunk-VL2YMDPH.js → chunk-J3VBVR4W.js} +6 -6
  33. package/dist/{chunk-AIRNG27G.js → chunk-JHEIOXXZ.js} +8 -7
  34. package/dist/chunk-JHEIOXXZ.js.map +1 -0
  35. package/dist/{chunk-D5LQMRPU.cjs → chunk-JSEJ5MH5.cjs} +5 -5
  36. package/dist/{chunk-D5LQMRPU.cjs.map → chunk-JSEJ5MH5.cjs.map} +1 -1
  37. package/dist/{chunk-FY5XAW3H.cjs → chunk-KX3K2EH4.cjs} +18 -18
  38. package/dist/{chunk-FY5XAW3H.cjs.map → chunk-KX3K2EH4.cjs.map} +1 -1
  39. package/dist/{chunk-3IEVA5AO.cjs → chunk-M6FEGPQJ.cjs} +93 -93
  40. package/dist/{chunk-3IEVA5AO.cjs.map → chunk-M6FEGPQJ.cjs.map} +1 -1
  41. package/dist/{chunk-7I42V2OD.cjs → chunk-MEIGDK7M.cjs} +74 -74
  42. package/dist/chunk-MEIGDK7M.cjs.map +1 -0
  43. package/dist/{chunk-OQIMH2WQ.js → chunk-MEMRQTOI.js} +12263 -9927
  44. package/dist/chunk-MEMRQTOI.js.map +1 -0
  45. package/dist/{chunk-GUTYUMGV.cjs → chunk-MPUQFRJG.cjs} +12 -12
  46. package/dist/{chunk-GUTYUMGV.cjs.map → chunk-MPUQFRJG.cjs.map} +1 -1
  47. package/dist/{chunk-UZZT65PQ.cjs → chunk-MSL5NE3N.cjs} +40 -40
  48. package/dist/{chunk-UZZT65PQ.cjs.map → chunk-MSL5NE3N.cjs.map} +1 -1
  49. package/dist/{chunk-6CJ2K7BD.js → chunk-MZRIRQYV.js} +3 -3
  50. package/dist/{chunk-NGEFYPY7.js → chunk-NCAN6VUF.js} +4 -4
  51. package/dist/{chunk-6QK3ONDD.js → chunk-OL3MJKUI.js} +2 -2
  52. package/dist/{chunk-MPYDX5WX.js → chunk-PLB6CQSO.js} +4 -4
  53. package/dist/{chunk-6CFFISUC.js → chunk-PTXZB2KW.js} +3 -3
  54. package/dist/{chunk-GMPYR3MX.cjs → chunk-QDGB2DKY.cjs} +33 -33
  55. package/dist/{chunk-GMPYR3MX.cjs.map → chunk-QDGB2DKY.cjs.map} +1 -1
  56. package/dist/{chunk-2MKUHJ3O.cjs → chunk-QG2EBIH5.cjs} +40 -40
  57. package/dist/{chunk-2MKUHJ3O.cjs.map → chunk-QG2EBIH5.cjs.map} +1 -1
  58. package/dist/{chunk-GZK7FWJJ.js → chunk-QKWOBV55.js} +12 -12
  59. package/dist/chunk-QKWOBV55.js.map +1 -0
  60. package/dist/{chunk-STYA77PH.cjs → chunk-SGL6URIJ.cjs} +12 -12
  61. package/dist/chunk-SGL6URIJ.cjs.map +1 -0
  62. package/dist/{chunk-KP5KRRD6.cjs → chunk-THC3K262.cjs} +3 -3
  63. package/dist/{chunk-KP5KRRD6.cjs.map → chunk-THC3K262.cjs.map} +1 -1
  64. package/dist/{chunk-BIKOJ4FH.cjs → chunk-TTYJAZ7P.cjs} +58 -32
  65. package/dist/chunk-TTYJAZ7P.cjs.map +1 -0
  66. package/dist/{chunk-IEZ3YBAP.cjs → chunk-W56JYDSC.cjs} +34 -33
  67. package/dist/chunk-W56JYDSC.cjs.map +1 -0
  68. package/dist/{chunk-PB3BTZDU.js → chunk-WEUNPE3S.js} +139 -113
  69. package/dist/chunk-WEUNPE3S.js.map +1 -0
  70. package/dist/{chunk-VDZO6IDR.js → chunk-XOFE2RT4.js} +5 -5
  71. package/dist/components/case-studies/index.cjs +10 -10
  72. package/dist/components/case-studies/index.js +4 -4
  73. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  74. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  75. package/dist/components/chat/hooks/index.d.ts +0 -1
  76. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  77. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  78. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  79. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  80. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
  81. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  82. package/dist/components/chat/index.cjs +12 -4
  83. package/dist/components/chat/index.cjs.map +1 -1
  84. package/dist/components/chat/index.d.ts +1 -0
  85. package/dist/components/chat/index.d.ts.map +1 -1
  86. package/dist/components/chat/index.js +23 -15
  87. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  88. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  89. package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
  90. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  91. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  92. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  93. package/dist/components/chat/stream/index.d.ts +17 -0
  94. package/dist/components/chat/stream/index.d.ts.map +1 -0
  95. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  96. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  97. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  98. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  99. package/dist/components/chat/types/api.types.d.ts +1 -81
  100. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  101. package/dist/components/chat/types/processing.types.d.ts +2 -93
  102. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  103. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  104. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  105. package/dist/components/chat/utils/index.d.ts +2 -3
  106. package/dist/components/chat/utils/index.d.ts.map +1 -1
  107. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  108. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  109. package/dist/components/contact/index.cjs +5 -5
  110. package/dist/components/contact/index.js +4 -4
  111. package/dist/components/docs/index.cjs +7 -7
  112. package/dist/components/docs/index.js +6 -6
  113. package/dist/components/embeds/index.cjs +5 -5
  114. package/dist/components/embeds/index.js +4 -4
  115. package/dist/components/faq/index.cjs +6 -6
  116. package/dist/components/faq/index.js +5 -5
  117. package/dist/components/features/index.cjs +4 -4
  118. package/dist/components/features/index.js +3 -3
  119. package/dist/components/features/select-button.d.ts.map +1 -1
  120. package/dist/components/help-center-pages/index.cjs +23 -23
  121. package/dist/components/help-center-pages/index.js +14 -14
  122. package/dist/components/index.cjs +203 -161
  123. package/dist/components/index.cjs.map +1 -1
  124. package/dist/components/index.js +66 -24
  125. package/dist/components/index.js.map +1 -1
  126. package/dist/components/layout/page-layout.d.ts +7 -1
  127. package/dist/components/layout/page-layout.d.ts.map +1 -1
  128. package/dist/components/layout/title-block.d.ts +23 -1
  129. package/dist/components/layout/title-block.d.ts.map +1 -1
  130. package/dist/components/navigation/index.cjs +4 -4
  131. package/dist/components/navigation/index.js +3 -3
  132. package/dist/components/onboarding-guides/index.cjs +8 -8
  133. package/dist/components/onboarding-guides/index.js +7 -7
  134. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  135. package/dist/components/related-content/index.cjs +6 -6
  136. package/dist/components/related-content/index.js +5 -5
  137. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  138. package/dist/components/tickets/index.cjs +7 -7
  139. package/dist/components/tickets/index.js +6 -6
  140. package/dist/components/ui/autocomplete.d.ts.map +1 -1
  141. package/dist/components/ui/checkbox-block.d.ts.map +1 -1
  142. package/dist/components/ui/date-picker.d.ts.map +1 -1
  143. package/dist/components/ui/field-wrapper.d.ts +10 -1
  144. package/dist/components/ui/field-wrapper.d.ts.map +1 -1
  145. package/dist/components/ui/file-manager/index.cjs +50 -50
  146. package/dist/components/ui/file-manager/index.js +1 -1
  147. package/dist/components/ui/floating-tooltip.d.ts +8 -1
  148. package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
  149. package/dist/components/ui/index.cjs +46 -4
  150. package/dist/components/ui/index.cjs.map +1 -1
  151. package/dist/components/ui/index.d.ts +1 -2
  152. package/dist/components/ui/index.d.ts.map +1 -1
  153. package/dist/components/ui/index.js +57 -15
  154. package/dist/components/ui/input-trigger.d.ts.map +1 -1
  155. package/dist/components/ui/input.d.ts.map +1 -1
  156. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  157. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  158. package/dist/components/ui/markdown/engine.d.ts +99 -0
  159. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  160. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  161. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  162. package/dist/components/ui/markdown/index.d.ts +18 -0
  163. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  164. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  165. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  166. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  167. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  168. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  169. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  170. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  171. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  172. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  173. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  174. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  175. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  176. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  177. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  178. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  179. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  180. package/dist/components/ui/page-actions.d.ts +7 -1
  181. package/dist/components/ui/page-actions.d.ts.map +1 -1
  182. package/dist/components/ui/radio-group.d.ts.map +1 -1
  183. package/dist/components/ui/search-input.d.ts.map +1 -1
  184. package/dist/components/ui/select.d.ts.map +1 -1
  185. package/dist/components/ui/tab-navigation.d.ts +10 -1
  186. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  187. package/dist/components/ui/tag-search-input.d.ts.map +1 -1
  188. package/dist/components/ui/textarea.d.ts.map +1 -1
  189. package/dist/components/ui/truncate-text.d.ts +2 -2
  190. package/dist/components/ui/truncate-text.d.ts.map +1 -1
  191. package/dist/hooks/index.cjs +4 -2
  192. package/dist/hooks/index.cjs.map +1 -1
  193. package/dist/hooks/index.js +3 -1
  194. package/dist/hooks/ui/index.d.ts +1 -0
  195. package/dist/hooks/ui/index.d.ts.map +1 -1
  196. package/dist/hooks/ui/use-is-truncated.d.ts +33 -0
  197. package/dist/hooks/ui/use-is-truncated.d.ts.map +1 -0
  198. package/dist/index.cjs +64 -4
  199. package/dist/index.cjs.map +1 -1
  200. package/dist/index.js +75 -15
  201. package/dist/utils/index.cjs +169 -40
  202. package/dist/utils/index.cjs.map +1 -1
  203. package/dist/utils/index.d.ts +1 -0
  204. package/dist/utils/index.d.ts.map +1 -1
  205. package/dist/utils/index.js +162 -41
  206. package/dist/utils/index.js.map +1 -1
  207. package/dist/utils/markdown-fences.d.ts +42 -0
  208. package/dist/utils/markdown-fences.d.ts.map +1 -0
  209. package/dist/utils/markdown-heading-id.d.ts +85 -0
  210. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  211. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  212. package/package.json +7 -1
  213. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +317 -0
  214. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  215. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  216. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  217. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +232 -0
  218. package/src/chat-protocol/decode.ts +278 -0
  219. package/src/chat-protocol/encode.ts +71 -0
  220. package/src/chat-protocol/env-flag.ts +39 -0
  221. package/src/chat-protocol/events.ts +245 -0
  222. package/src/chat-protocol/frames.ts +245 -0
  223. package/src/chat-protocol/index.ts +21 -0
  224. package/src/chat-protocol/ip-normalize.ts +146 -0
  225. package/src/chat-protocol/nats-decoder.ts +258 -0
  226. package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
  227. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  228. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +230 -0
  229. package/src/components/chat/chat-message-enhanced.tsx +122 -20
  230. package/src/components/chat/chat-message-list.tsx +358 -109
  231. package/src/components/chat/embeddable-chat.tsx +6 -0
  232. package/src/components/chat/hooks/.index.md +30 -33
  233. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  234. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  235. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  236. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  237. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  238. package/src/components/chat/hooks/index.ts +0 -1
  239. package/src/components/chat/hooks/use-chat.ts +8 -0
  240. package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -825
  241. package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
  242. package/src/components/chat/index.ts +5 -0
  243. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +940 -0
  244. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
  245. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
  246. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +973 -0
  247. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  248. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
  249. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  250. package/src/components/chat/stream/chat-stream-reducer.ts +1808 -0
  251. package/src/components/chat/stream/delta-batcher.ts +168 -0
  252. package/src/components/chat/stream/index.ts +55 -0
  253. package/src/components/chat/stream/message-mutations.ts +340 -0
  254. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  255. package/src/components/chat/thinking-display.tsx +1 -1
  256. package/src/components/chat/types/.api.types.md +39 -49
  257. package/src/components/chat/types/.processing.types.md +29 -58
  258. package/src/components/chat/types/api.types.ts +6 -73
  259. package/src/components/chat/types/processing.types.ts +11 -53
  260. package/src/components/chat/utils/.index.md +31 -40
  261. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  262. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  263. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
  264. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  265. package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
  266. package/src/components/chat/utils/index.ts +5 -8
  267. package/src/components/chat/utils/process-historical-messages.ts +358 -386
  268. package/src/components/features/select-button.tsx +4 -1
  269. package/src/components/layout/page-layout.tsx +16 -0
  270. package/src/components/layout/title-block.tsx +106 -7
  271. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  272. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  273. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  274. package/src/components/ui/.field-wrapper.md +5 -4
  275. package/src/components/ui/.tab-navigation.md +14 -1
  276. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  277. package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
  278. package/src/components/ui/autocomplete.tsx +13 -1
  279. package/src/components/ui/button/button.tsx +1 -1
  280. package/src/components/ui/checkbox-block.tsx +27 -3
  281. package/src/components/ui/date-picker.tsx +15 -1
  282. package/src/components/ui/field-wrapper.tsx +12 -6
  283. package/src/components/ui/floating-tooltip.tsx +9 -1
  284. package/src/components/ui/index.ts +1 -2
  285. package/src/components/ui/input-trigger.tsx +8 -1
  286. package/src/components/ui/input.tsx +8 -1
  287. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
  288. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
  289. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
  290. package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
  291. package/src/components/ui/markdown/base-components.tsx +360 -0
  292. package/src/components/ui/markdown/engine.tsx +315 -0
  293. package/src/components/ui/markdown/heading-ids.ts +239 -0
  294. package/src/components/ui/markdown/index.ts +49 -0
  295. package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
  296. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  297. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  298. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  299. package/src/components/ui/markdown/sanitize.ts +3499 -0
  300. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  301. package/src/components/ui/markdown/streaming.ts +362 -0
  302. package/src/components/ui/markdown/text-size.ts +106 -0
  303. package/src/components/ui/page-actions.tsx +142 -30
  304. package/src/components/ui/radio-group.tsx +28 -3
  305. package/src/components/ui/release-changelog-section.tsx +1 -1
  306. package/src/components/ui/search-input.tsx +2 -1
  307. package/src/components/ui/select.tsx +7 -0
  308. package/src/components/ui/tab-navigation.tsx +26 -4
  309. package/src/components/ui/tag-search-input.tsx +9 -2
  310. package/src/components/ui/textarea.tsx +8 -1
  311. package/src/components/ui/ticket-info-section.tsx +1 -1
  312. package/src/components/ui/truncate-text.tsx +3 -18
  313. package/src/hooks/ui/index.ts +1 -0
  314. package/src/hooks/ui/use-is-truncated.ts +66 -0
  315. package/src/utils/index.ts +1 -0
  316. package/src/utils/markdown-fences.ts +127 -0
  317. package/src/utils/markdown-heading-id.ts +348 -0
  318. package/src/utils/markdown-section-extractor.ts +41 -56
  319. package/dist/chunk-7I42V2OD.cjs.map +0 -1
  320. package/dist/chunk-AIRNG27G.js.map +0 -1
  321. package/dist/chunk-BIKOJ4FH.cjs.map +0 -1
  322. package/dist/chunk-BZMOEWWS.js.map +0 -1
  323. package/dist/chunk-GZK7FWJJ.js.map +0 -1
  324. package/dist/chunk-IEZ3YBAP.cjs.map +0 -1
  325. package/dist/chunk-OQIMH2WQ.js.map +0 -1
  326. package/dist/chunk-PB3BTZDU.js.map +0 -1
  327. package/dist/chunk-STYA77PH.cjs.map +0 -1
  328. package/dist/chunk-VUXNXWOQ.cjs.map +0 -1
  329. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  330. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  331. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  332. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  333. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  334. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  335. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  336. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  337. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  338. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
  339. package/src/components/chat/utils/.chunk-parser.md +0 -62
  340. package/src/components/chat/utils/chunk-parser.ts +0 -262
  341. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  342. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  343. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  344. /package/dist/{chunk-WLM3F3WU.js.map → chunk-34TDA22K.js.map} +0 -0
  345. /package/dist/{chunk-EGXCWZYP.js.map → chunk-3LKB722U.js.map} +0 -0
  346. /package/dist/{chunk-RW2LPL72.js.map → chunk-6WTCJI5R.js.map} +0 -0
  347. /package/dist/{chunk-VL2YMDPH.js.map → chunk-J3VBVR4W.js.map} +0 -0
  348. /package/dist/{chunk-6CJ2K7BD.js.map → chunk-MZRIRQYV.js.map} +0 -0
  349. /package/dist/{chunk-NGEFYPY7.js.map → chunk-NCAN6VUF.js.map} +0 -0
  350. /package/dist/{chunk-6QK3ONDD.js.map → chunk-OL3MJKUI.js.map} +0 -0
  351. /package/dist/{chunk-MPYDX5WX.js.map → chunk-PLB6CQSO.js.map} +0 -0
  352. /package/dist/{chunk-6CFFISUC.js.map → chunk-PTXZB2KW.js.map} +0 -0
  353. /package/dist/{chunk-VDZO6IDR.js.map → chunk-XOFE2RT4.js.map} +0 -0
@@ -1,6 +1,28 @@
1
1
  /**
2
- * Utility for processing historical messages from GraphQL/API
3
- * into display-ready format
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`).
4
26
  */
5
27
 
6
28
  import {
@@ -15,6 +37,7 @@ import {
15
37
  type MessageData,
16
38
  type MessageOwner,
17
39
  } from '../types'
40
+ import type { ChatStreamEvent } from '../../../chat-protocol/events'
18
41
  import { MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator'
19
42
  import { getCommandText } from './tool-call-helpers'
20
43
 
@@ -53,255 +76,67 @@ function pushStandaloneMessages(
53
76
  })
54
77
  }
55
78
 
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
- }
79
+ // =============================================================================
80
+ // Decode persisted MessageData item shared ChatStreamEvent vocabulary
81
+ // =============================================================================
66
82
 
67
83
  /**
68
- * Process an array of historical messages into display-ready format
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).
69
99
  */
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
100
+ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent | null {
265
101
  switch (data.type) {
266
102
  case MESSAGE_TYPE.TEXT:
267
103
  if ('text' in data && data.text) {
268
- accumulator.appendText(data.text)
104
+ return { type: 'text-delta', text: data.text }
269
105
  }
270
- break
106
+ return null
271
107
 
272
108
  case MESSAGE_TYPE.THINKING:
273
109
  if ('text' in data && data.text) {
274
- accumulator.appendThinking(data.text)
110
+ return { type: 'thinking-delta', text: data.text }
275
111
  }
276
- break
112
+ return null
277
113
 
278
114
  case MESSAGE_TYPE.GUIDE:
279
115
  if ('text' in data && data.text) {
280
- accumulator.appendGuide(data.text)
116
+ return { type: 'guide-delta', text: data.text }
281
117
  }
282
- break
118
+ return null
283
119
 
284
120
  case MESSAGE_TYPE.EXECUTING_TOOL:
285
121
  if ('integratedToolType' in data) {
286
- accumulator.addToolExecution({
287
- type: 'tool_execution',
122
+ return {
123
+ type: 'tool-execution',
288
124
  data: {
289
125
  type: 'EXECUTING_TOOL',
290
126
  integratedToolType: data.integratedToolType || '',
291
127
  toolFunction: data.toolFunction || '',
292
128
  toolTitle: typeof data.title === 'string' ? data.title : undefined,
293
- toolExplanation: typeof data.toolExplanation === 'string' ? data.toolExplanation : undefined,
294
129
  parameters: data.parameters,
295
130
  toolExecutionRequestId: data.toolExecutionRequestId,
296
131
  },
297
- })
132
+ }
298
133
  }
299
- break
134
+ return null
300
135
 
301
136
  case MESSAGE_TYPE.EXECUTED_TOOL:
302
137
  if ('integratedToolType' in data) {
303
- accumulator.addToolExecution({
304
- type: 'tool_execution',
138
+ return {
139
+ type: 'tool-execution',
305
140
  data: {
306
141
  type: 'EXECUTED_TOOL',
307
142
  integratedToolType: data.integratedToolType || '',
@@ -312,218 +147,298 @@ function processMessageData(
312
147
  success: data.success,
313
148
  toolExecutionRequestId: data.toolExecutionRequestId,
314
149
  },
315
- })
150
+ }
316
151
  }
317
- break
152
+ return null
318
153
 
319
154
  case MESSAGE_TYPE.APPROVAL_REQUEST:
320
155
  if ('approvalRequestId' in data && data.approvalRequestId) {
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
- })
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 } : {}),
377
163
  }
378
164
  }
379
- break
165
+ return null
380
166
 
381
167
  case MESSAGE_TYPE.APPROVAL_RESULT:
382
168
  if ('approvalRequestId' in data && data.approvalRequestId) {
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)
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
387
254
 
388
- if (escalatedData?.toolCalls && escalatedData.toolCalls.length > 0) {
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'
389
263
  if (batchApprovalsEnabled) {
390
- accumulator.addApprovalBatch(
391
- data.approvalRequestId,
392
- escalatedData.approvalType,
393
- escalatedData.toolCalls,
394
- status,
395
- undefined,
396
- resolvedByName,
397
- )
264
+ accumulator.addApprovalBatch(event.requestId, approvalType, toolCalls!, status)
398
265
  } else {
399
- for (const call of escalatedData.toolCalls) {
266
+ // Flag OFF unfold batch into N legacy approval cards (same id).
267
+ for (const call of toolCalls!) {
400
268
  if (!call.requiresApproval) continue
401
269
  accumulator.addApprovalRequest(
402
- data.approvalRequestId,
270
+ event.requestId,
403
271
  getCommandText(call),
404
272
  call.toolExplanation,
405
- escalatedData.approvalType,
273
+ approvalType,
406
274
  status,
407
275
  )
408
276
  }
409
277
  }
410
- escalatedApprovals?.delete(data.approvalRequestId)
411
- break
412
- }
413
-
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)
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
+ }
421
302
  }
422
-
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
- )
303
+ } else {
304
+ escalatedApprovals?.set(event.requestId, {
305
+ command: event.command || '',
306
+ explanation: event.explanation,
307
+ approvalType,
308
+ ...(isBatch ? { toolCalls } : {}),
309
+ })
435
310
  }
436
311
  break
312
+ }
437
313
 
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
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
+ )
446
342
  }
447
343
  }
448
- accumulator.addError(
449
- data.error || 'An error occurred',
450
- message
451
- )
344
+ escalatedApprovals?.delete(requestId)
345
+ break
452
346
  }
453
- break
454
347
 
455
- case MESSAGE_TYPE.CONTEXT_COMPACTION_START:
456
- accumulator.addContextCompaction()
348
+ if (escalatedData) {
349
+ accumulator.trackApprovalRequest(requestId, {
350
+ command: escalatedData.command,
351
+ explanation: escalatedData.explanation,
352
+ approvalType: escalatedData.approvalType,
353
+ })
354
+ escalatedApprovals?.delete(requestId)
355
+ }
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
+ )
457
370
  break
371
+ }
458
372
 
459
- case MESSAGE_TYPE.CONTEXT_COMPACTION_END: {
460
- const summary = 'summary' in data && typeof data.summary === 'string' ? data.summary : undefined
461
- accumulator.completeContextCompaction(summary)
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
380
+ }
381
+ }
382
+ accumulator.addError(event.title, message)
462
383
  break
463
384
  }
464
385
 
386
+ case 'compaction':
387
+ if (event.phase === 'start') {
388
+ accumulator.addContextCompaction()
389
+ } else {
390
+ accumulator.completeContextCompaction(event.summary)
391
+ }
392
+ break
393
+
465
394
  default:
466
- // Unknown message type - ignore
395
+ // Participant/system rows are envelope concerns; everything else is
396
+ // realtime-only vocabulary that never appears in persisted rows.
467
397
  break
468
398
  }
469
399
  }
470
400
 
401
+ // =============================================================================
402
+ // Envelope — ONE implementation (previous duplicate deleted)
403
+ // =============================================================================
404
+
471
405
  /**
472
- * Extract error messages from historical messages
473
- * Returns a separate array of error messages that should be displayed
406
+ * Result type for historical message processing
474
407
  */
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
408
+ export interface ProcessHistoricalMessagesResult {
409
+ messages: ProcessedMessage[]
410
+ escalatedApprovals: EscalatedApprovals
508
411
  }
509
412
 
510
413
  /**
511
- * Process messages and include error messages in the correct order
414
+ * Process an array of historical messages into display-ready format
512
415
  */
513
- export function processHistoricalMessagesWithErrors(
416
+ export function processHistoricalMessages(
514
417
  messages: HistoricalMessage[],
515
- options: MessageProcessingOptions = {}
418
+ options: MessageProcessingOptions = {},
516
419
  ): ProcessHistoricalMessagesResult {
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
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
520
438
 
521
439
  const processedMessages: ProcessedMessage[] = []
522
440
  const accumulator = createMessageSegmentAccumulator({ onApprove, onReject })
523
- const escalatedApprovals = new Map<
524
- string,
525
- { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }
526
- >()
441
+ const escalatedApprovals: EscalatedApprovals = new Map()
527
442
 
528
443
  let currentAssistantId: string | null = null
529
444
  let currentAssistantTimestamp: Date | null = null
@@ -532,6 +447,10 @@ export function processHistoricalMessagesWithErrors(
532
447
  // — carried onto the flushed message's streamSeq for per-role merge coverage.
533
448
  let currentAssistantStreamSeq: number | undefined
534
449
 
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
+ */
535
454
  const flushAssistantMessage = () => {
536
455
  const idToUse = lastAssistantId || currentAssistantId
537
456
  if (idToUse && accumulator.hasContent()) {
@@ -562,6 +481,7 @@ export function processHistoricalMessagesWithErrors(
562
481
  }
563
482
 
564
483
  messages.forEach((msg, index) => {
484
+ // Filter by chat type if specified
565
485
  if (chatTypeFilter && msg.chatType !== chatTypeFilter) return
566
486
 
567
487
  const messageDataArray = Array.isArray(msg.messageData)
@@ -625,8 +545,10 @@ export function processHistoricalMessagesWithErrors(
625
545
  }
626
546
 
627
547
  messageDataArray.forEach((data) => {
628
- processMessageData(
629
- data,
548
+ const event = decodeHistoricalMessageData(data)
549
+ if (!event) return
550
+ applyHistoryEvent(
551
+ event,
630
552
  accumulator,
631
553
  approvalStatuses,
632
554
  { displayApprovalTypes, batchApprovalsEnabled },
@@ -634,6 +556,7 @@ export function processHistoricalMessagesWithErrors(
634
556
  )
635
557
  })
636
558
 
559
+ // Check if we should flush (next message is from user or last message)
637
560
  const nextMsg = messages[index + 1]
638
561
  const isLastMessage = index === messages.length - 1
639
562
  const nextIsFromUser =
@@ -666,3 +589,52 @@ export function processHistoricalMessagesWithErrors(
666
589
  escalatedApprovals: escalatedApprovals
667
590
  }
668
591
  }
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