@flamingo-stack/openframe-frontend-core 0.0.498-1506.4000.1 → 0.0.498

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 (290) hide show
  1. package/dist/{chunk-ZHC2BCXK.js → chunk-2OPD6KRB.js} +4 -4
  2. package/dist/{chunk-5BQWVNRS.cjs → chunk-2R25CGSO.cjs} +62 -62
  3. package/dist/{chunk-5BQWVNRS.cjs.map → chunk-2R25CGSO.cjs.map} +1 -1
  4. package/dist/{chunk-242K7LZY.cjs → chunk-4EHQEMN4.cjs} +11 -11
  5. package/dist/{chunk-242K7LZY.cjs.map → chunk-4EHQEMN4.cjs.map} +1 -1
  6. package/dist/{chunk-4KKGHPNE.cjs → chunk-4NIBKEED.cjs} +9 -9
  7. package/dist/{chunk-4KKGHPNE.cjs.map → chunk-4NIBKEED.cjs.map} +1 -1
  8. package/dist/{chunk-5QWYIPMH.js → chunk-5ZL7PA63.js} +2 -2
  9. package/dist/{chunk-3VGRZ62V.js → chunk-6GAJETAP.js} +2 -2
  10. package/dist/{chunk-EO35UHFT.cjs → chunk-A3Z63ZL5.cjs} +60 -60
  11. package/dist/chunk-A3Z63ZL5.cjs.map +1 -0
  12. package/dist/{chunk-34C2JV2C.js → chunk-G6ARA2HD.js} +2 -2
  13. package/dist/{chunk-WNXIG6YY.js → chunk-K5HHLTSG.js} +2 -2
  14. package/dist/{chunk-42NYDEHH.js → chunk-KK4M4URN.js} +2 -2
  15. package/dist/{chunk-42SMPKGY.js → chunk-MF3VJKHT.js} +2 -2
  16. package/dist/{chunk-EORFD5WB.cjs → chunk-MSB3PEPP.cjs} +15 -15
  17. package/dist/{chunk-EORFD5WB.cjs.map → chunk-MSB3PEPP.cjs.map} +1 -1
  18. package/dist/{chunk-F7XPMG25.cjs → chunk-MVORKLWX.cjs} +27 -28
  19. package/dist/chunk-MVORKLWX.cjs.map +1 -0
  20. package/dist/{chunk-D3LUKR2Y.js → chunk-OYPUQJJ2.js} +10044 -12292
  21. package/dist/chunk-OYPUQJJ2.js.map +1 -0
  22. package/dist/{chunk-62TM2RVZ.cjs → chunk-T67MZAQJ.cjs} +26 -26
  23. package/dist/{chunk-62TM2RVZ.cjs.map → chunk-T67MZAQJ.cjs.map} +1 -1
  24. package/dist/{chunk-BUBQEZ5J.js → chunk-VLWVM6ZS.js} +2 -2
  25. package/dist/{chunk-632XJ2ZT.js → chunk-VOOEX4IZ.js} +5 -6
  26. package/dist/chunk-VOOEX4IZ.js.map +1 -0
  27. package/dist/{chunk-5ZY5JM4Z.js → chunk-W4XZ3FBZ.js} +10 -10
  28. package/dist/chunk-W4XZ3FBZ.js.map +1 -0
  29. package/dist/{chunk-2LYA3RBT.cjs → chunk-XI44QDCD.cjs} +37 -37
  30. package/dist/{chunk-2LYA3RBT.cjs.map → chunk-XI44QDCD.cjs.map} +1 -1
  31. package/dist/{chunk-WZCQW4MW.js → chunk-XSNKSMB3.js} +4 -4
  32. package/dist/{chunk-J3NHQ4JB.cjs → chunk-ZBTIN7RC.cjs} +31 -31
  33. package/dist/{chunk-J3NHQ4JB.cjs.map → chunk-ZBTIN7RC.cjs.map} +1 -1
  34. package/dist/{chunk-SS3D3DHL.cjs → chunk-ZEZTITQZ.cjs} +4886 -7134
  35. package/dist/chunk-ZEZTITQZ.cjs.map +1 -0
  36. package/dist/{chunk-44C23SSC.cjs → chunk-ZHNWWFIM.cjs} +5 -5
  37. package/dist/{chunk-44C23SSC.cjs.map → chunk-ZHNWWFIM.cjs.map} +1 -1
  38. package/dist/{chunk-H6GFQ4AO.cjs → chunk-ZTE32A7C.cjs} +7 -7
  39. package/dist/{chunk-H6GFQ4AO.cjs.map → chunk-ZTE32A7C.cjs.map} +1 -1
  40. package/dist/components/case-studies/index.cjs +8 -8
  41. package/dist/components/case-studies/index.js +2 -2
  42. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  43. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  44. package/dist/components/chat/hooks/index.d.ts +1 -0
  45. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-chunk-catchup.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 +57 -10
  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 +93 -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 +130 -172
  84. package/dist/components/index.cjs.map +1 -1
  85. package/dist/components/index.js +21 -63
  86. package/dist/components/index.js.map +1 -1
  87. package/dist/components/navigation/header.d.ts.map +1 -1
  88. package/dist/components/navigation/index.cjs +2 -2
  89. package/dist/components/navigation/index.js +1 -1
  90. package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
  91. package/dist/components/onboarding-guides/index.cjs +5 -5
  92. package/dist/components/onboarding-guides/index.js +4 -4
  93. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  94. package/dist/components/related-content/index.cjs +3 -3
  95. package/dist/components/related-content/index.js +2 -2
  96. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  97. package/dist/components/tickets/index.cjs +5 -5
  98. package/dist/components/tickets/index.js +4 -4
  99. package/dist/components/ui/index.cjs +2 -44
  100. package/dist/components/ui/index.cjs.map +1 -1
  101. package/dist/components/ui/index.d.ts +2 -1
  102. package/dist/components/ui/index.d.ts.map +1 -1
  103. package/dist/components/ui/index.js +13 -55
  104. package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
  105. package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
  106. package/dist/components/ui/simple-markdown-renderer.d.ts +73 -0
  107. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -0
  108. package/dist/index.cjs +2 -60
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.js +13 -71
  111. package/dist/utils/index.cjs +40 -169
  112. package/dist/utils/index.cjs.map +1 -1
  113. package/dist/utils/index.d.ts +0 -1
  114. package/dist/utils/index.d.ts.map +1 -1
  115. package/dist/utils/index.js +41 -162
  116. package/dist/utils/index.js.map +1 -1
  117. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  118. package/package.json +1 -7
  119. package/src/components/chat/__tests__/chat-message-list.test.tsx +6 -381
  120. package/src/components/chat/chat-message-enhanced.tsx +20 -122
  121. package/src/components/chat/chat-message-list.tsx +132 -409
  122. package/src/components/chat/embeddable-chat.tsx +0 -6
  123. package/src/components/chat/hooks/.index.md +33 -30
  124. package/src/components/chat/hooks/.use-nats-chat-adapter.md +55 -36
  125. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +77 -0
  126. package/src/components/chat/hooks/index.ts +1 -0
  127. package/src/components/chat/hooks/use-chat.ts +0 -8
  128. package/src/components/chat/hooks/use-chunk-catchup.ts +1 -26
  129. package/src/components/chat/hooks/use-nats-chat-adapter.ts +825 -254
  130. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +489 -0
  131. package/src/components/chat/hooks/use-sse-chat-adapter.ts +684 -380
  132. package/src/components/chat/index.ts +0 -5
  133. package/src/components/chat/thinking-display.tsx +1 -1
  134. package/src/components/chat/types/.api.types.md +49 -39
  135. package/src/components/chat/types/.processing.types.md +58 -29
  136. package/src/components/chat/types/api.types.ts +73 -6
  137. package/src/components/chat/types/processing.types.ts +53 -11
  138. package/src/components/chat/utils/.chunk-parser.md +62 -0
  139. package/src/components/chat/utils/.index.md +40 -31
  140. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +0 -61
  141. package/src/components/chat/utils/chunk-parser.ts +262 -0
  142. package/src/components/chat/utils/extract-incomplete-message-state.ts +5 -63
  143. package/src/components/chat/utils/index.ts +8 -5
  144. package/src/components/chat/utils/process-historical-messages.ts +386 -358
  145. package/src/components/navigation/header.tsx +2 -1
  146. package/src/components/navigation/mingo-ai-button.tsx +4 -1
  147. package/src/components/navigation/mobile-nav-panel.tsx +5 -5
  148. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +4 -22
  149. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  150. package/src/components/shared/product-release/release-detail-page.tsx +4 -19
  151. package/src/components/ui/.simple-markdown-renderer.md +52 -0
  152. package/src/components/ui/index.ts +2 -1
  153. package/src/components/ui/release-changelog-section.tsx +1 -1
  154. package/src/components/ui/rich-markdown-renderer.tsx +1223 -0
  155. package/src/components/ui/simple-markdown-renderer.tsx +964 -0
  156. package/src/components/ui/ticket-info-section.tsx +1 -1
  157. package/src/utils/index.ts +0 -1
  158. package/src/utils/markdown-section-extractor.ts +56 -41
  159. package/dist/chat-protocol/decode.d.ts +0 -58
  160. package/dist/chat-protocol/decode.d.ts.map +0 -1
  161. package/dist/chat-protocol/encode.d.ts +0 -44
  162. package/dist/chat-protocol/encode.d.ts.map +0 -1
  163. package/dist/chat-protocol/env-flag.d.ts +0 -35
  164. package/dist/chat-protocol/env-flag.d.ts.map +0 -1
  165. package/dist/chat-protocol/events.d.ts +0 -207
  166. package/dist/chat-protocol/events.d.ts.map +0 -1
  167. package/dist/chat-protocol/frames.d.ts +0 -213
  168. package/dist/chat-protocol/frames.d.ts.map +0 -1
  169. package/dist/chat-protocol/index.cjs +0 -500
  170. package/dist/chat-protocol/index.cjs.map +0 -1
  171. package/dist/chat-protocol/index.d.ts +0 -18
  172. package/dist/chat-protocol/index.d.ts.map +0 -1
  173. package/dist/chat-protocol/index.js +0 -484
  174. package/dist/chat-protocol/index.js.map +0 -1
  175. package/dist/chat-protocol/ip-normalize.d.ts +0 -44
  176. package/dist/chat-protocol/ip-normalize.d.ts.map +0 -1
  177. package/dist/chat-protocol/nats-decoder.d.ts +0 -26
  178. package/dist/chat-protocol/nats-decoder.d.ts.map +0 -1
  179. package/dist/chunk-5ZY5JM4Z.js.map +0 -1
  180. package/dist/chunk-632XJ2ZT.js.map +0 -1
  181. package/dist/chunk-D3LUKR2Y.js.map +0 -1
  182. package/dist/chunk-EO35UHFT.cjs.map +0 -1
  183. package/dist/chunk-F7XPMG25.cjs.map +0 -1
  184. package/dist/chunk-SS3D3DHL.cjs.map +0 -1
  185. package/dist/components/chat/stream/chat-dialog-store.d.ts +0 -184
  186. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +0 -1
  187. package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -367
  188. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +0 -1
  189. package/dist/components/chat/stream/delta-batcher.d.ts +0 -51
  190. package/dist/components/chat/stream/delta-batcher.d.ts.map +0 -1
  191. package/dist/components/chat/stream/index.d.ts +0 -17
  192. package/dist/components/chat/stream/index.d.ts.map +0 -1
  193. package/dist/components/chat/stream/message-mutations.d.ts +0 -77
  194. package/dist/components/chat/stream/message-mutations.d.ts.map +0 -1
  195. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +0 -22
  196. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +0 -1
  197. package/dist/components/ui/markdown/base-components.d.ts +0 -51
  198. package/dist/components/ui/markdown/base-components.d.ts.map +0 -1
  199. package/dist/components/ui/markdown/engine.d.ts +0 -99
  200. package/dist/components/ui/markdown/engine.d.ts.map +0 -1
  201. package/dist/components/ui/markdown/heading-ids.d.ts +0 -65
  202. package/dist/components/ui/markdown/heading-ids.d.ts.map +0 -1
  203. package/dist/components/ui/markdown/index.d.ts +0 -18
  204. package/dist/components/ui/markdown/index.d.ts.map +0 -1
  205. package/dist/components/ui/markdown/mermaid-diagram.d.ts +0 -79
  206. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +0 -1
  207. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +0 -14
  208. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +0 -1
  209. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -40
  210. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +0 -1
  211. package/dist/components/ui/markdown/rich/shortcodes.d.ts +0 -8
  212. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +0 -1
  213. package/dist/components/ui/markdown/sanitize.d.ts +0 -153
  214. package/dist/components/ui/markdown/sanitize.d.ts.map +0 -1
  215. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +0 -23
  216. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +0 -1
  217. package/dist/components/ui/markdown/streaming.d.ts +0 -78
  218. package/dist/components/ui/markdown/streaming.d.ts.map +0 -1
  219. package/dist/components/ui/markdown/text-size.d.ts +0 -34
  220. package/dist/components/ui/markdown/text-size.d.ts.map +0 -1
  221. package/dist/utils/markdown-fences.d.ts +0 -42
  222. package/dist/utils/markdown-fences.d.ts.map +0 -1
  223. package/dist/utils/markdown-heading-id.d.ts +0 -85
  224. package/dist/utils/markdown-heading-id.d.ts.map +0 -1
  225. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -317
  226. package/src/chat-protocol/__tests__/chat-protocol.test.ts +0 -516
  227. package/src/chat-protocol/__tests__/env-flag.test.ts +0 -52
  228. package/src/chat-protocol/__tests__/ip-normalize.test.ts +0 -136
  229. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -232
  230. package/src/chat-protocol/decode.ts +0 -278
  231. package/src/chat-protocol/encode.ts +0 -71
  232. package/src/chat-protocol/env-flag.ts +0 -39
  233. package/src/chat-protocol/events.ts +0 -245
  234. package/src/chat-protocol/frames.ts +0 -245
  235. package/src/chat-protocol/index.ts +0 -21
  236. package/src/chat-protocol/ip-normalize.ts +0 -146
  237. package/src/chat-protocol/nats-decoder.ts +0 -258
  238. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -207
  239. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -250
  240. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +0 -37
  241. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  242. package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +0 -92
  243. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +0 -438
  244. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +0 -318
  245. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +0 -940
  246. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +0 -806
  247. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +0 -467
  248. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -1248
  249. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +0 -255
  250. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +0 -105
  251. package/src/components/chat/stream/chat-dialog-store.ts +0 -536
  252. package/src/components/chat/stream/chat-stream-reducer.ts +0 -1876
  253. package/src/components/chat/stream/delta-batcher.ts +0 -168
  254. package/src/components/chat/stream/index.ts +0 -55
  255. package/src/components/chat/stream/message-mutations.ts +0 -340
  256. package/src/components/chat/stream/use-chat-stream-reducer.ts +0 -126
  257. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +0 -420
  258. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +0 -605
  259. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -394
  260. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +0 -270
  261. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +0 -2420
  262. package/src/components/ui/__tests__/markdown-parity.test.tsx +0 -2540
  263. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +0 -176
  264. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +0 -151
  265. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +0 -354
  266. package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +0 -118
  267. package/src/components/ui/markdown/__tests__/streaming.test.tsx +0 -436
  268. package/src/components/ui/markdown/base-components.tsx +0 -360
  269. package/src/components/ui/markdown/engine.tsx +0 -315
  270. package/src/components/ui/markdown/heading-ids.ts +0 -239
  271. package/src/components/ui/markdown/index.ts +0 -49
  272. package/src/components/ui/markdown/mermaid-diagram.tsx +0 -323
  273. package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -199
  274. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +0 -184
  275. package/src/components/ui/markdown/rich/shortcodes.ts +0 -170
  276. package/src/components/ui/markdown/sanitize.ts +0 -3584
  277. package/src/components/ui/markdown/simple-markdown-renderer.tsx +0 -28
  278. package/src/components/ui/markdown/streaming.ts +0 -390
  279. package/src/components/ui/markdown/text-size.ts +0 -106
  280. package/src/utils/markdown-fences.ts +0 -127
  281. package/src/utils/markdown-heading-id.ts +0 -348
  282. /package/dist/{chunk-ZHC2BCXK.js.map → chunk-2OPD6KRB.js.map} +0 -0
  283. /package/dist/{chunk-5QWYIPMH.js.map → chunk-5ZL7PA63.js.map} +0 -0
  284. /package/dist/{chunk-3VGRZ62V.js.map → chunk-6GAJETAP.js.map} +0 -0
  285. /package/dist/{chunk-34C2JV2C.js.map → chunk-G6ARA2HD.js.map} +0 -0
  286. /package/dist/{chunk-WNXIG6YY.js.map → chunk-K5HHLTSG.js.map} +0 -0
  287. /package/dist/{chunk-42NYDEHH.js.map → chunk-KK4M4URN.js.map} +0 -0
  288. /package/dist/{chunk-42SMPKGY.js.map → chunk-MF3VJKHT.js.map} +0 -0
  289. /package/dist/{chunk-BUBQEZ5J.js.map → chunk-VLWVM6ZS.js.map} +0 -0
  290. /package/dist/{chunk-WZCQW4MW.js.map → chunk-XSNKSMB3.js.map} +0 -0
@@ -0,0 +1,489 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Hook for processing real-time NATS chunks
5
+ * Provides a consistent interface for handling streaming messages
6
+ */
7
+
8
+ import { useCallback, useRef, useEffect } from 'react'
9
+ import { parseChunkToAction } from '../utils/chunk-parser'
10
+ import {
11
+ MessageSegmentAccumulator,
12
+ createMessageSegmentAccumulator,
13
+ } from '../utils/message-segment-accumulator'
14
+ import type { UseRealtimeChunkProcessorReturn, UseRealtimeChunkProcessorOptions, ChatApprovalStatus, PendingToolCallData, MessageSegment, SegmentsUpdateMetadata } from '../types'
15
+ import { getCommandText } from '../utils/tool-call-helpers'
16
+
17
+ // Actions allowed through once the dialog is in direct mode. Everything else is
18
+ // an AI-assistant chunk and gets dropped — an allowlist so any future assistant
19
+ // action is blocked by default.
20
+ const DIRECT_MODE_ALLOWED = new Set(['direct_message', 'message_request', 'system', 'dialog_closed'])
21
+
22
+ /**
23
+ * Hook for processing real-time NATS chunks into message segments
24
+ */
25
+ export function useRealtimeChunkProcessor(
26
+ options: UseRealtimeChunkProcessorOptions
27
+ ): UseRealtimeChunkProcessorReturn {
28
+ const {
29
+ callbacks,
30
+ displayApprovalTypes = ['CLIENT'],
31
+ approvalStatuses = {},
32
+ initialState,
33
+ // Owned by the consumer (e.g. oss-tenant chat client / openframe-frontend
34
+ // tickets view). Default ON so consumers that haven't wired the flag yet
35
+ // get the new batch UI; pass `false` explicitly to fall back to legacy.
36
+ batchApprovalsEnabled = true,
37
+ isDirectMode = false,
38
+ } = options
39
+
40
+ const accumulatorRef = useRef<MessageSegmentAccumulator>(
41
+ createMessageSegmentAccumulator({
42
+ onApprove: callbacks.onApprove,
43
+ onReject: callbacks.onReject,
44
+ })
45
+ )
46
+
47
+ const hasInitializedWithData = useRef(false)
48
+
49
+ // Initialize accumulator when we get initial state data
50
+ useEffect(() => {
51
+ if (initialState && !hasInitializedWithData.current) {
52
+ accumulatorRef.current.initializeWithState(initialState)
53
+
54
+ if (initialState.escalatedApprovals) {
55
+ pendingEscalatedRef.current = new Map(initialState.escalatedApprovals)
56
+
57
+ initialState.escalatedApprovals.forEach((data, requestId) => {
58
+ callbacks.onEscalatedApproval?.(requestId, data)
59
+ })
60
+ }
61
+
62
+ // Resumed dialog: a MESSAGE_START already fired server-side. Treat
63
+ // subsequent continuation chunks (after the next MESSAGE_END) as
64
+ // post-stream so they append into the existing bubble instead of
65
+ // replacing its content via the cold-start cumulative path.
66
+ hasEverStreamedRef.current = true
67
+ hasInitializedWithData.current = true
68
+ }
69
+ }, [initialState, callbacks])
70
+
71
+ const isInStreamRef = useRef(false)
72
+ // Distinguishes post-MESSAGE_END continuation (append into prior bubble)
73
+ // from cold-start before any MESSAGE_START (cumulative; otherwise
74
+ // appendSegmentsToLastAssistant silently drops the chunk when no
75
+ // assistant bubble exists yet). Flipped true on MESSAGE_START and on
76
+ // resumed-dialog initializeWithState.
77
+ const hasEverStreamedRef = useRef(false)
78
+
79
+ // Direct-mode barrier: once engaged, AI-assistant chunks are dropped. Engaged
80
+ // by the host flag (optimistic) or by an in-order DIRECT_MESSAGE chunk.
81
+ const directModeFlagRef = useRef(isDirectMode)
82
+ const sawDirectMessageRef = useRef(false)
83
+ // One-shot teardown guard for the barrier. Busy state can be asserted
84
+ // OUTSIDE an open stream (onAgentBusy on tool/approval chunks), so the
85
+ // teardown must fire once even when isInStreamRef is false — otherwise a
86
+ // human takeover right after an approval leaves the consumer's composer
87
+ // locked forever (all releasing AI chunks get dropped by the barrier).
88
+ const directTeardownFiredRef = useRef(false)
89
+ useEffect(() => {
90
+ directModeFlagRef.current = isDirectMode
91
+ }, [isDirectMode])
92
+
93
+ // Track pending escalated approvals (single or batch)
94
+ const pendingEscalatedRef = useRef<
95
+ Map<string, { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }>
96
+ >(new Map())
97
+
98
+ const processChunk = useCallback(
99
+ (chunk: unknown) => {
100
+ const action = parseChunkToAction(chunk)
101
+ if (!action) return
102
+
103
+ const accumulator = accumulatorRef.current
104
+
105
+ // streamSeq of the chunk currently being processed (JetStream only).
106
+ // Only content emissions below carry it through to the host, so the
107
+ // streaming bubble is stamped with the highest CONTENT seq it saw — the
108
+ // non-persisted MESSAGE_END / TOKEN_USAGE chunks never reach emitSegments.
109
+ const streamSeq =
110
+ chunk && typeof chunk === 'object' && typeof (chunk as { streamSeq?: unknown }).streamSeq === 'number'
111
+ ? (chunk as { streamSeq: number }).streamSeq
112
+ : undefined
113
+ const emitSegments = (s: MessageSegment[], meta?: SegmentsUpdateMetadata) =>
114
+ callbacks.onSegmentsUpdate?.(s, streamSeq != null ? { ...meta, streamSeq } : meta)
115
+
116
+ if (action.action === 'direct_message') sawDirectMessageRef.current = true
117
+
118
+ if ((directModeFlagRef.current || sawDirectMessageRef.current) && !DIRECT_MODE_ALLOWED.has(action.action)) {
119
+ // Hard stop: tear down AI activity exactly once so the typing
120
+ // indicator, composer lock, and half-rendered bubble clear, then drop
121
+ // the chunk. Fires even when no stream is open — an onAgentBusy lock
122
+ // (approved commands executing post-MESSAGE_END) has no other release
123
+ // once the barrier starts dropping the continuation chunks.
124
+ if (isInStreamRef.current || !directTeardownFiredRef.current) {
125
+ isInStreamRef.current = false
126
+ directTeardownFiredRef.current = true
127
+ callbacks.onStreamEnd?.()
128
+ accumulator.resetSegments()
129
+ }
130
+ return
131
+ }
132
+
133
+ switch (action.action) {
134
+ case 'message_start':
135
+ isInStreamRef.current = true
136
+ hasEverStreamedRef.current = true
137
+ callbacks.onStreamStart?.()
138
+ accumulator.resetSegments()
139
+ break
140
+
141
+ case 'message_end':
142
+ isInStreamRef.current = false
143
+ callbacks.onStreamEnd?.()
144
+ accumulator.resetSegments()
145
+ break
146
+
147
+ case 'metadata':
148
+ callbacks.onMetadata?.(action)
149
+ break
150
+
151
+ case 'text': {
152
+ const segments = accumulator.appendText(action.text)
153
+ // Append-mode only for *true* post-stream continuation (after a
154
+ // MESSAGE_END we actually saw). Cold-start chunks (no prior
155
+ // MESSAGE_START) emit cumulative segments so the consumer can
156
+ // spawn the first assistant bubble — otherwise appendSegmentsToLastAssistant
157
+ // silently drops the chunk when no last assistant exists.
158
+ if (isInStreamRef.current || !hasEverStreamedRef.current) {
159
+ emitSegments(segments)
160
+ } else {
161
+ emitSegments([{ type: 'text', text: action.text }], { append: true })
162
+ }
163
+ break
164
+ }
165
+
166
+ case 'thinking': {
167
+ const segments = accumulator.appendThinking(action.text)
168
+ if (isInStreamRef.current || !hasEverStreamedRef.current) {
169
+ emitSegments(segments)
170
+ } else {
171
+ emitSegments([{ type: 'thinking', text: action.text }], { append: true })
172
+ }
173
+ break
174
+ }
175
+
176
+ case 'guide': {
177
+ const segments = accumulator.appendGuide(action.text)
178
+ if (isInStreamRef.current || !hasEverStreamedRef.current) {
179
+ emitSegments(segments)
180
+ } else {
181
+ emitSegments([{ type: 'guide', text: action.text }], { append: true })
182
+ }
183
+ break
184
+ }
185
+
186
+ case 'tool_execution': {
187
+ // A starting tool run means the agent's turn is in progress even
188
+ // when this lands after MESSAGE_END (approved commands execute
189
+ // between the approval bubble and the continuation stream).
190
+ if (action.segment.data.type === 'EXECUTING_TOOL') {
191
+ callbacks.onAgentBusy?.()
192
+ }
193
+ // Post-MESSAGE_END tool chunks (cancellations / async batch
194
+ // results for a batch in a prior bubble) flow only through the
195
+ // cross-message updater. Skipping the accumulator avoids
196
+ // pushing a standalone segment that the next text chunk would
197
+ // replay into a new bubble.
198
+ if (!isInStreamRef.current && callbacks.onToolExecuted) {
199
+ callbacks.onToolExecuted(action.segment)
200
+ break
201
+ }
202
+ // In-stream: accumulator-driven update of the streaming bubble
203
+ // is the source of truth. Don't fire onToolExecuted here — its
204
+ // cross-message scan is first-match-wins and could touch a
205
+ // same-execId segment in a prior bubble (agent retry case).
206
+ //
207
+ // KNOWN EDGE (accepted): a slow async batch tool whose EXECUTED
208
+ // lands only AFTER the continuation stream's MESSAGE_START takes
209
+ // this path too — the freshly reset accumulator has no batch
210
+ // segment to merge into, so the chunk renders as a standalone card
211
+ // in the new bubble while the prior batch card's executions slot
212
+ // stays 'executing' until the next history refetch. Routing such
213
+ // chunks cross-message would reintroduce the retry hazard above;
214
+ // revisit only if the backend confirms batch executions can
215
+ // overlap the continuation stream.
216
+ const segments = accumulator.addToolExecution(action.segment)
217
+ emitSegments(segments)
218
+ break
219
+ }
220
+
221
+ case 'approval_request': {
222
+ const { requestId, command, explanation, approvalType } = action
223
+
224
+ if (displayApprovalTypes.includes(approvalType)) {
225
+ // Display directly
226
+ const status = approvalStatuses[requestId] || 'pending'
227
+ const segments = accumulator.addApprovalRequest(
228
+ requestId,
229
+ command,
230
+ explanation,
231
+ approvalType,
232
+ status as ChatApprovalStatus
233
+ )
234
+ emitSegments(segments)
235
+ } else {
236
+ // Track as escalated
237
+ pendingEscalatedRef.current.set(requestId, { command, explanation, approvalType })
238
+ callbacks.onEscalatedApproval?.(requestId, { command, explanation, approvalType })
239
+ }
240
+ break
241
+ }
242
+
243
+ case 'approval_batch': {
244
+ const { requestId, approvalType, toolCalls } = action
245
+ const status = (approvalStatuses[requestId] || 'pending') as ChatApprovalStatus
246
+
247
+ if (!displayApprovalTypes.includes(approvalType)) {
248
+ // Escalated: keep batch context locally for replay on result; surface a
249
+ // summary command via the legacy escalation callback.
250
+ const required = toolCalls.find((c) => c.requiresApproval) ?? toolCalls[0]
251
+ const summary = required ? getCommandText(required) : `Batch of ${toolCalls.length} tool calls`
252
+ pendingEscalatedRef.current.set(requestId, {
253
+ command: summary,
254
+ explanation: required?.toolExplanation,
255
+ approvalType,
256
+ toolCalls,
257
+ })
258
+ callbacks.onEscalatedApproval?.(requestId, {
259
+ command: summary,
260
+ explanation: required?.toolExplanation,
261
+ approvalType,
262
+ })
263
+ break
264
+ }
265
+
266
+ if (batchApprovalsEnabled) {
267
+ const segments = accumulator.addApprovalBatch(requestId, approvalType, toolCalls, status)
268
+ emitSegments(segments)
269
+ break
270
+ }
271
+
272
+ // Flag OFF — unfold batch into N legacy approval cards. They share
273
+ // `requestId`, so a click on any will approve the whole batch via a
274
+ // single backend call, and the resulting APPROVAL_RESULT chunk will
275
+ // flip status on every matching segment (see updateApprovalStatus).
276
+ let segments = accumulator.getSegments()
277
+ for (const call of toolCalls) {
278
+ if (!call.requiresApproval) continue
279
+ segments = accumulator.addApprovalRequest(
280
+ requestId,
281
+ getCommandText(call),
282
+ call.toolExplanation,
283
+ approvalType,
284
+ status,
285
+ )
286
+ }
287
+ emitSegments(segments)
288
+ break
289
+ }
290
+
291
+ case 'approval_result': {
292
+ const { requestId, approved, approvalType, resolvedByName } = action
293
+ // Approved → the agent resumes to execute the command(s); surface
294
+ // busy immediately so the composer locks before EXECUTING_TOOL
295
+ // lands. Rejection keeps the input free — the user may want to
296
+ // type a correction right away.
297
+ if (approved) callbacks.onAgentBusy?.()
298
+ const escalatedData = pendingEscalatedRef.current.get(requestId)
299
+ const status: ChatApprovalStatus = approved ? 'approved' : 'rejected'
300
+
301
+ if (escalatedData) {
302
+ pendingEscalatedRef.current.delete(requestId)
303
+ callbacks.onEscalatedApprovalResult?.(requestId, approved, {
304
+ command: escalatedData.command,
305
+ explanation: escalatedData.explanation,
306
+ approvalType: escalatedData.approvalType,
307
+ })
308
+
309
+ // The escalated card was never displayed inline, so this emit is
310
+ // what surfaces it after resolution. In-stream the cumulative
311
+ // array is correct; post-MESSAGE_END the accumulator was RESET,
312
+ // so a cumulative (replace-mode) emit would wipe the trailing
313
+ // bubble down to the lone card — emit only the resolved card(s)
314
+ // as an append instead. Store hosts upsert batches by request id,
315
+ // so a replayed append stays idempotent.
316
+ const emitResolved = (segments: MessageSegment[]) => {
317
+ if (isInStreamRef.current) {
318
+ emitSegments(segments)
319
+ return
320
+ }
321
+ const delta = segments.filter(
322
+ (s) =>
323
+ (s.type === 'approval_request' && s.data.requestId === requestId) ||
324
+ (s.type === 'approval_batch' && s.data.approvalRequestId === requestId),
325
+ )
326
+ emitSegments(delta, { append: true })
327
+ }
328
+
329
+ if (escalatedData.toolCalls && escalatedData.toolCalls.length > 0) {
330
+ if (batchApprovalsEnabled) {
331
+ const segments = accumulator.addApprovalBatch(
332
+ requestId,
333
+ escalatedData.approvalType,
334
+ escalatedData.toolCalls,
335
+ status,
336
+ undefined,
337
+ resolvedByName,
338
+ )
339
+ emitResolved(segments)
340
+ } else {
341
+ let segments = accumulator.getSegments()
342
+ for (const call of escalatedData.toolCalls) {
343
+ if (!call.requiresApproval) continue
344
+ segments = accumulator.addApprovalRequest(
345
+ requestId,
346
+ getCommandText(call),
347
+ call.toolExplanation,
348
+ escalatedData.approvalType,
349
+ status,
350
+ )
351
+ }
352
+ emitResolved(segments)
353
+ }
354
+ } else {
355
+ const segments = accumulator.addApprovalRequest(
356
+ requestId,
357
+ escalatedData.command,
358
+ escalatedData.explanation,
359
+ escalatedData.approvalType,
360
+ status,
361
+ )
362
+ emitResolved(segments)
363
+ }
364
+ } else {
365
+ // Always keep the in-memory accumulator in sync so a following
366
+ // text/tool chunk replays the resolved status into the message.
367
+ accumulator.updateApprovalStatus(requestId, status, resolvedByName)
368
+ // When the consumer wires cross-message resolution via
369
+ // `onApprovalResolved`, skip `onSegmentsUpdate` here: this path
370
+ // routes through `ensureAssistantMessage` + `updateStreamingMessageSegments`,
371
+ // which adopts/creates an assistant bubble and replays the
372
+ // accumulator's segments into it — turning a status flip into a
373
+ // bubble overwrite that wipes the original card.
374
+ if (!callbacks.onApprovalResolved) {
375
+ emitSegments(accumulator.getSegments())
376
+ }
377
+ }
378
+ callbacks.onApprovalResolved?.(requestId, status, approvalType, resolvedByName)
379
+ break
380
+ }
381
+
382
+ case 'error': {
383
+ let message: string | undefined
384
+ if ('details' in action && action?.details) {
385
+ try {
386
+ message = JSON.parse(action.details)?.error?.message
387
+ } catch {
388
+ message = action.details
389
+ }
390
+ }
391
+ const segments = accumulator.addError(action.error, message)
392
+ emitSegments(segments)
393
+ callbacks.onError?.(action.error, message)
394
+ break
395
+ }
396
+
397
+ case 'system': {
398
+ callbacks.onSystemMessage?.(action.text, { streamSeq })
399
+ break
400
+ }
401
+
402
+ case 'direct_message': {
403
+ callbacks.onDirectMessage?.(action.text, {
404
+ ownerType: action.ownerType,
405
+ displayName: action.displayName,
406
+ userId: action.userId,
407
+ streamSeq,
408
+ })
409
+ break
410
+ }
411
+
412
+ case 'message_request':
413
+ callbacks.onUserMessage?.(action.text, {
414
+ ownerType: action.ownerType,
415
+ displayName: action.displayName,
416
+ userId: action.userId,
417
+ streamSeq,
418
+ contextItems: action.contextItems,
419
+ })
420
+ break
421
+
422
+ case 'token_usage':
423
+ callbacks.onTokenUsage?.(action.data)
424
+ break
425
+
426
+ case 'context_compaction_start': {
427
+ const standalone = !isInStreamRef.current
428
+ const segments = accumulator.addContextCompaction()
429
+ emitSegments(segments, standalone ? { append: true, isCompacting: true } : undefined)
430
+ break
431
+ }
432
+
433
+ case 'context_compaction_end': {
434
+ const standalone = !isInStreamRef.current
435
+ const segments = accumulator.completeContextCompaction(action.summary)
436
+ emitSegments(segments, standalone ? { append: true, isCompacting: true } : undefined)
437
+ break
438
+ }
439
+
440
+ case 'dialog_closed':
441
+ callbacks.onDialogClosed?.()
442
+ break
443
+
444
+ default:
445
+ // Unknown action - ignore
446
+ break
447
+ }
448
+ },
449
+ [callbacks, displayApprovalTypes, approvalStatuses, initialState]
450
+ )
451
+
452
+ const getSegments = useCallback(() => {
453
+ return accumulatorRef.current.getSegments()
454
+ }, [])
455
+
456
+ const reset = useCallback(() => {
457
+ accumulatorRef.current.reset()
458
+ pendingEscalatedRef.current.clear()
459
+ hasInitializedWithData.current = false
460
+ sawDirectMessageRef.current = false
461
+ directTeardownFiredRef.current = false
462
+ // Stream-state flags are per-dialog too. Leaking them across a reset
463
+ // made the next dialog's cold-start chunks take the post-stream append
464
+ // path (hasEverStreamed stuck true) — which consumers may silently drop
465
+ // when no assistant bubble exists yet — and left tool/compaction routing
466
+ // thinking a stream was still open (isInStream stuck true).
467
+ isInStreamRef.current = false
468
+ hasEverStreamedRef.current = false
469
+ }, [])
470
+
471
+ const updateApprovalStatus = useCallback(
472
+ (requestId: string, status: ChatApprovalStatus, resolvedByName?: string | null) => {
473
+ return accumulatorRef.current.updateApprovalStatus(requestId, status, resolvedByName)
474
+ },
475
+ []
476
+ )
477
+
478
+ const getPendingApprovals = useCallback(() => {
479
+ return new Map(pendingEscalatedRef.current)
480
+ }, [])
481
+
482
+ return {
483
+ processChunk,
484
+ getSegments,
485
+ reset,
486
+ updateApprovalStatus,
487
+ getPendingApprovals,
488
+ }
489
+ }