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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/dist/chat-protocol/decode.d.ts +58 -0
  2. package/dist/chat-protocol/decode.d.ts.map +1 -0
  3. package/dist/chat-protocol/encode.d.ts +44 -0
  4. package/dist/chat-protocol/encode.d.ts.map +1 -0
  5. package/dist/chat-protocol/env-flag.d.ts +35 -0
  6. package/dist/chat-protocol/env-flag.d.ts.map +1 -0
  7. package/dist/chat-protocol/events.d.ts +216 -0
  8. package/dist/chat-protocol/events.d.ts.map +1 -0
  9. package/dist/chat-protocol/frames.d.ts +213 -0
  10. package/dist/chat-protocol/frames.d.ts.map +1 -0
  11. package/dist/chat-protocol/index.cjs +506 -0
  12. package/dist/chat-protocol/index.cjs.map +1 -0
  13. package/dist/chat-protocol/index.d.ts +18 -0
  14. package/dist/chat-protocol/index.d.ts.map +1 -0
  15. package/dist/chat-protocol/index.js +490 -0
  16. package/dist/chat-protocol/index.js.map +1 -0
  17. package/dist/chat-protocol/ip-normalize.d.ts +44 -0
  18. package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
  19. package/dist/chat-protocol/nats-decoder.d.ts +26 -0
  20. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
  21. package/dist/{chunk-3SQ5KXHQ.cjs → chunk-2T4GTV25.cjs} +9 -9
  22. package/dist/{chunk-3SQ5KXHQ.cjs.map → chunk-2T4GTV25.cjs.map} +1 -1
  23. package/dist/{chunk-CWAOV2FK.cjs → chunk-3CGZPAGR.cjs} +3 -3
  24. package/dist/{chunk-CWAOV2FK.cjs.map → chunk-3CGZPAGR.cjs.map} +1 -1
  25. package/dist/{chunk-TLMJHMXJ.js → chunk-3QSR22IC.js} +12 -12
  26. package/dist/chunk-3QSR22IC.js.map +1 -0
  27. package/dist/{chunk-TWYNR4TZ.cjs → chunk-4G5TLHJD.cjs} +7141 -4799
  28. package/dist/chunk-4G5TLHJD.cjs.map +1 -0
  29. package/dist/{chunk-ADPMHWOE.js → chunk-72XAID7Y.js} +4 -4
  30. package/dist/{chunk-PAGKRNWK.js → chunk-7WZHBQ4J.js} +2 -2
  31. package/dist/{chunk-OCEKO5CW.js → chunk-AEELJC4N.js} +12106 -9764
  32. package/dist/chunk-AEELJC4N.js.map +1 -0
  33. package/dist/{chunk-Z42BGM6Q.cjs → chunk-BT2WWDFP.cjs} +5 -5
  34. package/dist/{chunk-Z42BGM6Q.cjs.map → chunk-BT2WWDFP.cjs.map} +1 -1
  35. package/dist/{chunk-7LEL3RIX.cjs → chunk-C7OY3PQP.cjs} +11 -11
  36. package/dist/{chunk-7LEL3RIX.cjs.map → chunk-C7OY3PQP.cjs.map} +1 -1
  37. package/dist/{chunk-RHGGEKPQ.cjs → chunk-CIOLOQKQ.cjs} +4 -4
  38. package/dist/{chunk-RHGGEKPQ.cjs.map → chunk-CIOLOQKQ.cjs.map} +1 -1
  39. package/dist/{chunk-TSZHM74B.js → chunk-DC2TKS7C.js} +2 -2
  40. package/dist/{chunk-M2GOR3XQ.cjs → chunk-DYPYR7FR.cjs} +87 -87
  41. package/dist/{chunk-M2GOR3XQ.cjs.map → chunk-DYPYR7FR.cjs.map} +1 -1
  42. package/dist/{chunk-I64ABCDX.js → chunk-I2B6X77L.js} +2 -2
  43. package/dist/{chunk-NIZDKTGL.cjs → chunk-K5MSCMDK.cjs} +37 -37
  44. package/dist/{chunk-NIZDKTGL.cjs.map → chunk-K5MSCMDK.cjs.map} +1 -1
  45. package/dist/{chunk-DTYRYB2N.js → chunk-K6QTZ7CZ.js} +2 -2
  46. package/dist/{chunk-4LTMXDUS.js → chunk-KBN7PMFT.js} +2 -2
  47. package/dist/{chunk-TBMV7I5N.js → chunk-KXZO2WZX.js} +2 -2
  48. package/dist/{chunk-QTYZMP6D.cjs → chunk-LBAKLT6T.cjs} +62 -62
  49. package/dist/chunk-LBAKLT6T.cjs.map +1 -0
  50. package/dist/{chunk-KEBYLU3U.js → chunk-LFUPI7VX.js} +2 -2
  51. package/dist/{chunk-LDTR4IVZ.cjs → chunk-M37PBG4N.cjs} +31 -31
  52. package/dist/{chunk-LDTR4IVZ.cjs.map → chunk-M37PBG4N.cjs.map} +1 -1
  53. package/dist/{chunk-WA7RR64F.js → chunk-PO5O654H.js} +6 -5
  54. package/dist/chunk-PO5O654H.js.map +1 -0
  55. package/dist/{chunk-WK4N5VBX.cjs → chunk-QJJISLPA.cjs} +28 -27
  56. package/dist/chunk-QJJISLPA.cjs.map +1 -0
  57. package/dist/{chunk-2F44PLTV.cjs → chunk-T7MCSA55.cjs} +26 -26
  58. package/dist/{chunk-2F44PLTV.cjs.map → chunk-T7MCSA55.cjs.map} +1 -1
  59. package/dist/{chunk-WJQHJD7J.js → chunk-TWSKTJHW.js} +2 -2
  60. package/dist/{chunk-ZSQHZYCO.cjs → chunk-UQDMJ6DH.cjs} +13 -13
  61. package/dist/{chunk-ZSQHZYCO.cjs.map → chunk-UQDMJ6DH.cjs.map} +1 -1
  62. package/dist/{chunk-TZRUCD56.js → chunk-V3ZAAFE3.js} +5 -5
  63. package/dist/{chunk-LRGHJPET.js → chunk-XGIFOHKE.js} +2 -2
  64. package/dist/{chunk-POOMO3PA.cjs → chunk-YQEPRYT5.cjs} +7 -7
  65. package/dist/{chunk-POOMO3PA.cjs.map → chunk-YQEPRYT5.cjs.map} +1 -1
  66. package/dist/components/case-studies/index.cjs +8 -8
  67. package/dist/components/case-studies/index.js +2 -2
  68. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  69. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  70. package/dist/components/chat/hooks/index.d.ts +0 -1
  71. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  72. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  73. package/dist/components/chat/hooks/use-chunk-catchup.d.ts.map +1 -1
  74. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  75. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  76. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
  77. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  78. package/dist/components/chat/index.cjs +10 -2
  79. package/dist/components/chat/index.cjs.map +1 -1
  80. package/dist/components/chat/index.d.ts +1 -0
  81. package/dist/components/chat/index.d.ts.map +1 -1
  82. package/dist/components/chat/index.js +21 -13
  83. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  84. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  85. package/dist/components/chat/stream/chat-stream-reducer.d.ts +380 -0
  86. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  87. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  88. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  89. package/dist/components/chat/stream/index.d.ts +17 -0
  90. package/dist/components/chat/stream/index.d.ts.map +1 -0
  91. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  92. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  93. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  94. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  95. package/dist/components/chat/types/api.types.d.ts +1 -81
  96. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  97. package/dist/components/chat/types/processing.types.d.ts +2 -93
  98. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  99. package/dist/components/chat/types/unified-chat-state.types.d.ts +15 -0
  100. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  101. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  102. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  103. package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
  104. package/dist/components/chat/utils/index.d.ts +2 -3
  105. package/dist/components/chat/utils/index.d.ts.map +1 -1
  106. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  107. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  108. package/dist/components/contact/index.cjs +3 -3
  109. package/dist/components/contact/index.js +2 -2
  110. package/dist/components/docs/index.cjs +5 -5
  111. package/dist/components/docs/index.js +4 -4
  112. package/dist/components/embeds/index.cjs +3 -3
  113. package/dist/components/embeds/index.js +2 -2
  114. package/dist/components/faq/index.cjs +3 -3
  115. package/dist/components/faq/index.js +2 -2
  116. package/dist/components/features/index.cjs +2 -2
  117. package/dist/components/features/index.js +1 -1
  118. package/dist/components/help-center-pages/index.cjs +22 -22
  119. package/dist/components/help-center-pages/index.js +13 -13
  120. package/dist/components/index.cjs +174 -132
  121. package/dist/components/index.cjs.map +1 -1
  122. package/dist/components/index.js +65 -23
  123. package/dist/components/index.js.map +1 -1
  124. package/dist/components/meeting-scheduler/index.cjs +34 -34
  125. package/dist/components/meeting-scheduler/index.js +3 -3
  126. package/dist/components/navigation/index.cjs +2 -2
  127. package/dist/components/navigation/index.js +1 -1
  128. package/dist/components/onboarding-guides/index.cjs +5 -5
  129. package/dist/components/onboarding-guides/index.js +4 -4
  130. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  131. package/dist/components/related-content/index.cjs +3 -3
  132. package/dist/components/related-content/index.js +2 -2
  133. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  134. package/dist/components/tickets/index.cjs +6 -6
  135. package/dist/components/tickets/index.js +5 -5
  136. package/dist/components/ui/index.cjs +44 -2
  137. package/dist/components/ui/index.cjs.map +1 -1
  138. package/dist/components/ui/index.d.ts +1 -2
  139. package/dist/components/ui/index.d.ts.map +1 -1
  140. package/dist/components/ui/index.js +55 -13
  141. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  142. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  143. package/dist/components/ui/markdown/engine.d.ts +99 -0
  144. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  145. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  146. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  147. package/dist/components/ui/markdown/index.d.ts +18 -0
  148. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  149. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  150. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  151. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  152. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  153. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  154. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  155. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  156. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  157. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  158. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  159. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  160. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  161. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  162. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  163. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  164. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  165. package/dist/index.cjs +60 -2
  166. package/dist/index.cjs.map +1 -1
  167. package/dist/index.js +71 -13
  168. package/dist/utils/index.cjs +169 -40
  169. package/dist/utils/index.cjs.map +1 -1
  170. package/dist/utils/index.d.ts +1 -0
  171. package/dist/utils/index.d.ts.map +1 -1
  172. package/dist/utils/index.js +162 -41
  173. package/dist/utils/index.js.map +1 -1
  174. package/dist/utils/markdown-fences.d.ts +42 -0
  175. package/dist/utils/markdown-fences.d.ts.map +1 -0
  176. package/dist/utils/markdown-heading-id.d.ts +85 -0
  177. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  178. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  179. package/package.json +7 -1
  180. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +319 -0
  181. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  182. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  183. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  184. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +237 -0
  185. package/src/chat-protocol/decode.ts +278 -0
  186. package/src/chat-protocol/encode.ts +71 -0
  187. package/src/chat-protocol/env-flag.ts +39 -0
  188. package/src/chat-protocol/events.ts +254 -0
  189. package/src/chat-protocol/frames.ts +245 -0
  190. package/src/chat-protocol/index.ts +21 -0
  191. package/src/chat-protocol/ip-normalize.ts +146 -0
  192. package/src/chat-protocol/nats-decoder.ts +264 -0
  193. package/src/components/chat/.chat-message-list.md +10 -6
  194. package/src/components/chat/__tests__/chat-message-list.test.tsx +450 -9
  195. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  196. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +250 -0
  197. package/src/components/chat/chat-message-enhanced.tsx +122 -20
  198. package/src/components/chat/chat-message-list.tsx +515 -133
  199. package/src/components/chat/embeddable-chat.tsx +6 -0
  200. package/src/components/chat/hooks/.index.md +30 -33
  201. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  202. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  203. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  204. package/src/components/chat/hooks/__tests__/chunk-catchup-dialog-staleness.test.ts +92 -0
  205. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  206. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  207. package/src/components/chat/hooks/index.ts +0 -1
  208. package/src/components/chat/hooks/use-chat.ts +8 -0
  209. package/src/components/chat/hooks/use-chunk-catchup.ts +26 -1
  210. package/src/components/chat/hooks/use-nats-chat-adapter.ts +254 -825
  211. package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
  212. package/src/components/chat/index.ts +5 -0
  213. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +944 -0
  214. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +806 -0
  215. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +467 -0
  216. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +1330 -0
  217. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  218. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +105 -0
  219. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  220. package/src/components/chat/stream/chat-stream-reducer.ts +1928 -0
  221. package/src/components/chat/stream/delta-batcher.ts +168 -0
  222. package/src/components/chat/stream/index.ts +55 -0
  223. package/src/components/chat/stream/message-mutations.ts +340 -0
  224. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  225. package/src/components/chat/thinking-display.tsx +1 -1
  226. package/src/components/chat/types/.api.types.md +39 -49
  227. package/src/components/chat/types/.processing.types.md +29 -58
  228. package/src/components/chat/types/.unified-chat-state.types.md +2 -2
  229. package/src/components/chat/types/api.types.ts +6 -73
  230. package/src/components/chat/types/processing.types.ts +11 -53
  231. package/src/components/chat/types/unified-chat-state.types.ts +16 -0
  232. package/src/components/chat/utils/.extract-incomplete-message-state.md +21 -3
  233. package/src/components/chat/utils/.history-merge.md +44 -3
  234. package/src/components/chat/utils/.index.md +31 -40
  235. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  236. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  237. package/src/components/chat/utils/__tests__/extract-incomplete-message-state.test.ts +106 -0
  238. package/src/components/chat/utils/__tests__/history-merge.test.ts +160 -0
  239. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +61 -0
  240. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
  241. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  242. package/src/components/chat/utils/extract-incomplete-message-state.ts +93 -6
  243. package/src/components/chat/utils/history-merge.ts +85 -5
  244. package/src/components/chat/utils/index.ts +5 -8
  245. package/src/components/chat/utils/process-historical-messages.ts +361 -385
  246. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  247. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  248. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  249. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  250. package/src/components/ui/__tests__/markdown-parity.test.tsx +2540 -0
  251. package/src/components/ui/index.ts +1 -2
  252. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +176 -0
  253. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +151 -0
  254. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +354 -0
  255. package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +118 -0
  256. package/src/components/ui/markdown/__tests__/streaming.test.tsx +436 -0
  257. package/src/components/ui/markdown/base-components.tsx +360 -0
  258. package/src/components/ui/markdown/engine.tsx +315 -0
  259. package/src/components/ui/markdown/heading-ids.ts +239 -0
  260. package/src/components/ui/markdown/index.ts +49 -0
  261. package/src/components/ui/markdown/mermaid-diagram.tsx +323 -0
  262. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  263. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  264. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  265. package/src/components/ui/markdown/sanitize.ts +3584 -0
  266. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  267. package/src/components/ui/markdown/streaming.ts +390 -0
  268. package/src/components/ui/markdown/text-size.ts +106 -0
  269. package/src/components/ui/release-changelog-section.tsx +1 -1
  270. package/src/components/ui/ticket-info-section.tsx +1 -1
  271. package/src/utils/index.ts +1 -0
  272. package/src/utils/markdown-fences.ts +127 -0
  273. package/src/utils/markdown-heading-id.ts +348 -0
  274. package/src/utils/markdown-section-extractor.ts +41 -56
  275. package/dist/chunk-OCEKO5CW.js.map +0 -1
  276. package/dist/chunk-QTYZMP6D.cjs.map +0 -1
  277. package/dist/chunk-TLMJHMXJ.js.map +0 -1
  278. package/dist/chunk-TWYNR4TZ.cjs.map +0 -1
  279. package/dist/chunk-WA7RR64F.js.map +0 -1
  280. package/dist/chunk-WK4N5VBX.cjs.map +0 -1
  281. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  282. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  283. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  284. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  285. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  286. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  287. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  288. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  289. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  290. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
  291. package/src/components/chat/utils/.chunk-parser.md +0 -62
  292. package/src/components/chat/utils/chunk-parser.ts +0 -262
  293. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  294. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  295. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  296. /package/dist/{chunk-ADPMHWOE.js.map → chunk-72XAID7Y.js.map} +0 -0
  297. /package/dist/{chunk-PAGKRNWK.js.map → chunk-7WZHBQ4J.js.map} +0 -0
  298. /package/dist/{chunk-TSZHM74B.js.map → chunk-DC2TKS7C.js.map} +0 -0
  299. /package/dist/{chunk-I64ABCDX.js.map → chunk-I2B6X77L.js.map} +0 -0
  300. /package/dist/{chunk-DTYRYB2N.js.map → chunk-K6QTZ7CZ.js.map} +0 -0
  301. /package/dist/{chunk-4LTMXDUS.js.map → chunk-KBN7PMFT.js.map} +0 -0
  302. /package/dist/{chunk-TBMV7I5N.js.map → chunk-KXZO2WZX.js.map} +0 -0
  303. /package/dist/{chunk-KEBYLU3U.js.map → chunk-LFUPI7VX.js.map} +0 -0
  304. /package/dist/{chunk-WJQHJD7J.js.map → chunk-TWSKTJHW.js.map} +0 -0
  305. /package/dist/{chunk-TZRUCD56.js.map → chunk-V3ZAAFE3.js.map} +0 -0
  306. /package/dist/{chunk-LRGHJPET.js.map → chunk-XGIFOHKE.js.map} +0 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * createDeltaBatcher — framework-free, append-only batching of the
3
+ * `text-delta` / `thinking-delta` / `guide-delta` events.
4
+ *
5
+ * WHY THIS IS SHARED (and not a hook-private detail): every host that drives a
6
+ * `ChatStreamReducer` from a raw transport needs the SAME batching, and a
7
+ * copy-paste of it drifts. The React wrapper (`useChatStreamReducer`) and any
8
+ * non-React mirror in a product app both consume this one implementation.
9
+ *
10
+ * Behaviour:
11
+ * - `push(event, key)` queues delta events and returns true; anything else
12
+ * returns false so the caller can flush + apply it synchronously (ordering:
13
+ * completion state must always land on fully-applied deltas).
14
+ * - CONSECUTIVE same-type deltas are COALESCED into one event (the reducer's
15
+ * append semantics distribute over concatenation), so a 60-delta/sec turn
16
+ * costs ONE `applyOne` per frame instead of 60. `text-delta` additionally
17
+ * requires a matching `leading` flag before coalescing.
18
+ * - A flush is scheduled on `requestAnimationFrame` when available, with a
19
+ * timer fallback that is ALWAYS armed (rAF pauses in background tabs, and a
20
+ * hidden chat panel must still keep its thread current).
21
+ * - `key` is an opaque routing token handed back to `applyOne`. When it
22
+ * changes, the pending batch is flushed against the PREVIOUS key first, so
23
+ * deltas never land on the wrong dialog/side.
24
+ */
25
+
26
+ import type {
27
+ ChatStreamEvent,
28
+ GuideDeltaEvent,
29
+ TextDeltaEvent,
30
+ ThinkingDeltaEvent,
31
+ } from '../../../chat-protocol/events'
32
+
33
+ export const DELTA_FLUSH_FALLBACK_MS = 50
34
+
35
+ export type DeltaEvent = TextDeltaEvent | ThinkingDeltaEvent | GuideDeltaEvent
36
+
37
+ export function isDeltaEvent(event: ChatStreamEvent): event is DeltaEvent {
38
+ return (
39
+ event.type === 'text-delta' ||
40
+ event.type === 'thinking-delta' ||
41
+ event.type === 'guide-delta'
42
+ )
43
+ }
44
+
45
+ export interface CreateDeltaBatcherOptions<K> {
46
+ /** Apply one (possibly coalesced) delta against `key`'s reducer. */
47
+ applyOne: (event: DeltaEvent, key: K | undefined) => void
48
+ /** Called after a flush that applied at least one delta. */
49
+ onFlushed?: (appliedCount: number) => void
50
+ /** Timer fallback in ms (default `DELTA_FLUSH_FALLBACK_MS`). */
51
+ fallbackMs?: number
52
+ }
53
+
54
+ export interface DeltaBatcher<K = unknown> {
55
+ /**
56
+ * Queue `event` when it is a delta (returns true). Non-delta events are NOT
57
+ * queued and return false — the caller flushes and applies them itself.
58
+ */
59
+ push(event: ChatStreamEvent, key?: K): boolean
60
+ /** Synchronously apply the pending batch and cancel any scheduled flush. */
61
+ flush(): void
62
+ /** Flush and release timers — call on teardown so tail deltas aren't lost. */
63
+ dispose(): void
64
+ /** Queued (post-coalescing) delta count. Test/diagnostic surface. */
65
+ readonly pendingCount: number
66
+ }
67
+
68
+ export function createDeltaBatcher<K = unknown>({
69
+ applyOne,
70
+ onFlushed,
71
+ fallbackMs = DELTA_FLUSH_FALLBACK_MS,
72
+ }: CreateDeltaBatcherOptions<K>): DeltaBatcher<K> {
73
+ let pending: DeltaEvent[] = []
74
+ let pendingKey: K | undefined
75
+ let hasKey = false
76
+ let raf: number | null = null
77
+ let timer: ReturnType<typeof setTimeout> | null = null
78
+
79
+ function cancelScheduled(): void {
80
+ if (raf !== null) {
81
+ if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(raf)
82
+ raf = null
83
+ }
84
+ if (timer !== null) {
85
+ clearTimeout(timer)
86
+ timer = null
87
+ }
88
+ }
89
+
90
+ function flush(): void {
91
+ cancelScheduled()
92
+ if (pending.length === 0) return
93
+ const batch = pending
94
+ pending = []
95
+ const key = pendingKey
96
+ for (const delta of batch) applyOne(delta, key)
97
+ onFlushed?.(batch.length)
98
+ }
99
+
100
+ function schedule(): void {
101
+ if (raf !== null || timer !== null) return
102
+ if (typeof requestAnimationFrame === 'function') {
103
+ raf = requestAnimationFrame(() => {
104
+ raf = null
105
+ flush()
106
+ })
107
+ }
108
+ // Timer fallback ALWAYS armed: rAF pauses in background tabs.
109
+ timer = setTimeout(() => {
110
+ timer = null
111
+ flush()
112
+ }, fallbackMs)
113
+ }
114
+
115
+ return {
116
+ push(event, key) {
117
+ // A pending batch belonging to a previous key must land on ITS reducer
118
+ // before we start queueing for the new one.
119
+ if (!hasKey || !Object.is(pendingKey, key)) {
120
+ flush()
121
+ pendingKey = key
122
+ hasKey = true
123
+ }
124
+ if (!isDeltaEvent(event)) return false
125
+ const tail = pending[pending.length - 1]
126
+ // NEVER coalesce a delta whose seq does not ADVANCE past the tail's.
127
+ // Coalescing stamps the merged entry with the LAST pushed seq, so a
128
+ // redelivered / out-of-order delta arriving inside one frame would both
129
+ // concatenate its text (the reducer never sees it as a duplicate) and
130
+ // REWIND the batch's seq below an already-applied one. Pushing it as its
131
+ // own entry hands it to the reducer intact, where the `seq <=
132
+ // lastAppliedSeq` gate drops it — the batcher must not depend on hosts
133
+ // gating at dispatch.
134
+ // A seq-LESS tail is not "always advancing": merging a seq-BEARING delta
135
+ // onto it stamps the merged entry with that seq, which the reducer's gate
136
+ // then treats as the batch's position — a redelivered delta could rewind
137
+ // it. Coalesce onto a seq-less tail only when the incoming delta is
138
+ // likewise seq-less (a uniformly seq-less stream, e.g. SSE).
139
+ const seqAdvances =
140
+ tail?.seq == null ? event.seq == null : event.seq != null && event.seq > tail.seq
141
+ if (
142
+ tail &&
143
+ seqAdvances &&
144
+ tail.type === event.type &&
145
+ (tail.type !== 'text-delta' ||
146
+ (tail as TextDeltaEvent).leading === (event as TextDeltaEvent).leading)
147
+ ) {
148
+ pending[pending.length - 1] = {
149
+ ...tail,
150
+ text: tail.text + event.text,
151
+ ...(event.seq != null ? { seq: event.seq } : {}),
152
+ } as DeltaEvent
153
+ } else {
154
+ pending.push({ ...event })
155
+ }
156
+ schedule()
157
+ return true
158
+ },
159
+ flush,
160
+ dispose() {
161
+ flush()
162
+ cancelScheduled()
163
+ },
164
+ get pendingCount() {
165
+ return pending.length
166
+ },
167
+ }
168
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Chat stream module — ONE master reader for every chat event source
3
+ * (SSE decoder, NATS chunk decoder, historical replay). See
4
+ * `chat-stream-reducer.ts` for the architecture note.
5
+ *
6
+ * NOTE: the pure message-mutation helpers (`appendToTrailingAssistant`,
7
+ * `applyToolExecutionToMessages`, …) are intentionally NOT star-exported
8
+ * here — their legacy public home is `use-nats-chat-adapter`, which
9
+ * re-exports them from `./message-mutations`; exporting the same bindings
10
+ * from two star-exported barrels would make them ambiguous and drop them
11
+ * from the chat barrel entirely.
12
+ */
13
+
14
+ export {
15
+ createChatStreamReducer,
16
+ createEmptyTurnMeta,
17
+ type BeginSseSendOptions,
18
+ type ChatReducerEffect,
19
+ type ChatReducerState,
20
+ type ChatStreamReducer,
21
+ type ChatStreamReducerOptions,
22
+ type ChatTurnMetaState,
23
+ type EscalatedApprovalData,
24
+ type InitializeExtras,
25
+ type PendingEcho,
26
+ } from './chat-stream-reducer'
27
+
28
+ export {
29
+ createChatDialogStore,
30
+ DEFAULT_DIALOG_SIDE,
31
+ DEFAULT_MAX_REDUCERS,
32
+ MAX_PENDING_RETAIN_KEYS,
33
+ type ChatDialogSide,
34
+ type ChatDialogStore,
35
+ type CreateChatDialogStoreOptions,
36
+ type EvictedReducerState,
37
+ } from './chat-dialog-store'
38
+
39
+ // Framework-free delta batching — shared with NON-React hosts that drive a
40
+ // reducer straight from a transport (they must NOT re-implement it).
41
+ // `isDeltaEvent` + `DELTA_FLUSH_FALLBACK_MS` are deliberately NOT re-exported:
42
+ // they are internals of the batcher (its own tests import them from the
43
+ // module), and no consumer needs to classify or re-time deltas by hand.
44
+ export {
45
+ createDeltaBatcher,
46
+ type CreateDeltaBatcherOptions,
47
+ type DeltaBatcher,
48
+ type DeltaEvent,
49
+ } from './delta-batcher'
50
+
51
+ export {
52
+ useChatStreamReducer,
53
+ type UseChatStreamReducerOptions,
54
+ type UseChatStreamReducerReturn,
55
+ } from './use-chat-stream-reducer'
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Pure message-mutation kernel for the master chat-stream reducer.
3
+ *
4
+ * These helpers were absorbed from `use-nats-chat-adapter.ts` in Phase 3 of
5
+ * the chat unification — they are the ONE implementation of "apply a stream
6
+ * fragment to a message thread". The reducer (`chat-stream-reducer.ts`) and
7
+ * the cross-side projections (`chat-dialog-store.ts`) both build on them.
8
+ *
9
+ * REFERENTIAL-STABILITY CONTRACT: every helper clones ONLY the touched
10
+ * message object (and the touched segment slot inside it). A value-level
11
+ * no-op — e.g. a replayed duplicate EXECUTED_TOOL whose merge would write
12
+ * identical values — returns the PRIOR references untouched, so React
13
+ * memoization over untouched messages survives redeliveries.
14
+ *
15
+ * Framework-free: no React, no timers.
16
+ */
17
+
18
+ import type {
19
+ ApprovalBatchExecutionState,
20
+ ChatApprovalStatus,
21
+ MessageSegment,
22
+ ToolExecutionSegment,
23
+ ToolExecutionData,
24
+ } from '../types'
25
+ import type { UnifiedChatMessage } from '../types/unified-chat-state.types'
26
+
27
+ export function nextId(role: 'user' | 'assistant'): string {
28
+ // Date.now() + counter sliver keeps ids monotonic even when two
29
+ // messages are produced inside the same ms tick (user + assistant
30
+ // placeholder fire back-to-back from a single sendMessage call).
31
+ return `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
32
+ }
33
+
34
+ /**
35
+ * Realtime user/direct/system chunks can be replays of rows already on
36
+ * screen. Content-window dedup is the LAST layer (seq-less transports only)
37
+ * — see the layer list in `chat-stream-reducer.ts`.
38
+ */
39
+ export const CONTENT_DEDUP_WINDOW = 4
40
+ export const SYSTEM_DEDUP_WINDOW = 10
41
+
42
+ export function hasRecentMessage(
43
+ prev: UnifiedChatMessage[],
44
+ predicate: (message: UnifiedChatMessage) => boolean,
45
+ window: number,
46
+ ): boolean {
47
+ const start = Math.max(0, prev.length - window)
48
+ for (let i = prev.length - 1; i >= start; i--) {
49
+ if (predicate(prev[i])) return true
50
+ }
51
+ return false
52
+ }
53
+
54
+ /**
55
+ * Replace (or append) the trailing assistant message with the latest
56
+ * accumulated segments.
57
+ */
58
+ export function updateTrailingAssistant(
59
+ prev: UnifiedChatMessage[],
60
+ segments: MessageSegment[],
61
+ ): UnifiedChatMessage[] {
62
+ const last = prev[prev.length - 1]
63
+ if (!last || last.role !== 'assistant') {
64
+ // No placeholder exists — append a fresh assistant message.
65
+ return [
66
+ ...prev,
67
+ {
68
+ id: nextId('assistant'),
69
+ role: 'assistant',
70
+ content: '',
71
+ segments,
72
+ },
73
+ ]
74
+ }
75
+ return [...prev.slice(0, -1), { ...last, segments }]
76
+ }
77
+
78
+ /**
79
+ * Append-mode counterpart of `updateTrailingAssistant` for post-MESSAGE_END
80
+ * continuation fragments (`SegmentsUpdateMetadata.append`). Coalesces
81
+ * trailing fragments of the same type, mirroring the accumulator; approval
82
+ * deltas upsert by request id so a replayed emit stays idempotent.
83
+ */
84
+ export function appendToTrailingAssistant(
85
+ prev: UnifiedChatMessage[],
86
+ segments: MessageSegment[],
87
+ ): UnifiedChatMessage[] {
88
+ if (segments.length === 0) return prev
89
+ const last = prev[prev.length - 1]
90
+ if (!last || last.role !== 'assistant') {
91
+ return [
92
+ ...prev,
93
+ { id: nextId('assistant'), role: 'assistant', content: '', segments },
94
+ ]
95
+ }
96
+ const merged = [...(last.segments ?? [])]
97
+ for (const seg of segments) {
98
+ const tail = merged[merged.length - 1]
99
+ if (seg.type === 'text' && tail?.type === 'text') {
100
+ merged[merged.length - 1] = { type: 'text', text: tail.text + seg.text }
101
+ } else if (seg.type === 'thinking' && tail?.type === 'thinking') {
102
+ merged[merged.length - 1] = { type: 'thinking', text: tail.text + seg.text }
103
+ } else if (seg.type === 'guide' && tail?.type === 'guide') {
104
+ merged[merged.length - 1] = { type: 'guide', text: tail.text + seg.text }
105
+ } else if (seg.type === 'approval_batch') {
106
+ // Approval deltas must be IDEMPOTENT: the escalated-result emit can be
107
+ // seen twice (live + catch-up replay over hydrated history), so upsert
108
+ // by request id instead of raw-appending a duplicate card.
109
+ const idx = merged.findIndex(
110
+ (m) => m.type === 'approval_batch' && m.data.approvalRequestId === seg.data.approvalRequestId,
111
+ )
112
+ if (idx !== -1) merged[idx] = seg
113
+ else merged.push(seg)
114
+ } else if (seg.type === 'approval_request') {
115
+ // Legacy cards share one requestId across an unfolded batch — key on
116
+ // (requestId, command) so each tool's card upserts its own twin.
117
+ const idx = merged.findIndex(
118
+ (m) =>
119
+ m.type === 'approval_request' &&
120
+ m.data.requestId === seg.data.requestId &&
121
+ m.data.command === seg.data.command,
122
+ )
123
+ if (idx !== -1) merged[idx] = seg
124
+ else merged.push(seg)
125
+ } else {
126
+ // Other non-text segments are pushed RAW on purpose: EXECUTING↔EXECUTED
127
+ // pairing and batch merging are `applyToolExecutionToMessages`'s job
128
+ // (post-END tool chunks never reach this helper — the reducer routes
129
+ // them cross-message). Running the accumulator here would double-apply
130
+ // those rules.
131
+ merged.push(seg)
132
+ }
133
+ }
134
+ return [...prev.slice(0, -1), { ...last, segments: merged }]
135
+ }
136
+
137
+ /**
138
+ * Upsert a standalone context-compaction segment into the trailing assistant
139
+ * bubble. Compaction emissions arrive as the accumulator's CUMULATIVE array —
140
+ * only the compaction segment itself may be applied, or interleaved
141
+ * continuation text would duplicate. A `completed` segment replaces the last
142
+ * `started` one in place.
143
+ */
144
+ export function upsertTrailingCompaction(
145
+ prev: UnifiedChatMessage[],
146
+ segments: MessageSegment[],
147
+ ): UnifiedChatMessage[] {
148
+ const compaction = [...segments].reverse().find((s) => s.type === 'context_compaction')
149
+ if (!compaction) return prev
150
+ const last = prev[prev.length - 1]
151
+ if (!last || last.role !== 'assistant') {
152
+ return [
153
+ ...prev,
154
+ { id: nextId('assistant'), role: 'assistant', content: '', segments: [compaction] },
155
+ ]
156
+ }
157
+ const existing = last.segments ?? []
158
+ // LAST 'started' segment (not first): with repeated compactions in one
159
+ // bubble the earlier ones are already completed-in-place, so the newest
160
+ // 'started' is the only one this completion can belong to.
161
+ const startedIdx = existing
162
+ .map((s) => s.type === 'context_compaction' && s.status === 'started')
163
+ .lastIndexOf(true)
164
+ const merged =
165
+ startedIdx !== -1
166
+ ? existing.map((s, i) => (i === startedIdx ? compaction : s))
167
+ : [...existing, compaction]
168
+ return [...prev.slice(0, -1), { ...last, segments: merged }]
169
+ }
170
+
171
+ /** Scoped value-compare for the fields a tool merge writes (status/type,
172
+ * result, success, execId, restored title, parameters). Small objects —
173
+ * JSON compare for `parameters` keeps a replayed duplicate a no-op even
174
+ * when the transport rebuilt the object. */
175
+ function sameToolData(a: ToolExecutionData, b: ToolExecutionData): boolean {
176
+ return (
177
+ a.type === b.type &&
178
+ a.integratedToolType === b.integratedToolType &&
179
+ a.toolFunction === b.toolFunction &&
180
+ a.toolTitle === b.toolTitle &&
181
+ a.result === b.result &&
182
+ a.success === b.success &&
183
+ a.toolExecutionRequestId === b.toolExecutionRequestId &&
184
+ (a.parameters === b.parameters ||
185
+ JSON.stringify(a.parameters ?? null) === JSON.stringify(b.parameters ?? null))
186
+ )
187
+ }
188
+
189
+ function sameExecutionState(
190
+ a: ApprovalBatchExecutionState | undefined,
191
+ b: ApprovalBatchExecutionState,
192
+ ): boolean {
193
+ return !!a && a.status === b.status && a.result === b.result && a.success === b.success
194
+ }
195
+
196
+ /**
197
+ * Cross-message tool-execution updater for post-MESSAGE_END tool chunks
198
+ * (approved commands executing after the approval bubble, async batch
199
+ * results). Scans messages from the end:
200
+ * 1) an `approval_batch` whose `toolCalls` contains the execution id →
201
+ * merge into its `executions` map;
202
+ * 2) a matching `tool_execution` segment (same id, or EXECUTING with the
203
+ * same tool for legacy id-less backends) → update in place;
204
+ * 3) no match → append the segment to the trailing assistant bubble.
205
+ *
206
+ * A value-level no-op (replayed duplicate) returns `prev` untouched.
207
+ */
208
+ export function applyToolExecutionToMessages(
209
+ prev: UnifiedChatMessage[],
210
+ segment: ToolExecutionSegment,
211
+ ): UnifiedChatMessage[] {
212
+ const merged = mergeToolExecutionIfPresent(prev, segment)
213
+ if (merged !== null) return merged
214
+ return appendToTrailingAssistant(prev, [segment])
215
+ }
216
+
217
+ /**
218
+ * Merge-only variant of `applyToolExecutionToMessages` — the pure projection
219
+ * used for the cross-SIDE fan-out in `chat-dialog-store.ts`. Updates a
220
+ * matching batch slot / tool segment when present; returns `null` when
221
+ * nothing matches (the caller decides whether to append — projections never
222
+ * do, so a tool that belongs to one side only never grows a card on the
223
+ * other).
224
+ */
225
+ export function mergeToolExecutionIfPresent(
226
+ prev: UnifiedChatMessage[],
227
+ segment: ToolExecutionSegment,
228
+ ): UnifiedChatMessage[] | null {
229
+ const toolData = segment.data
230
+ const execId = toolData.toolExecutionRequestId
231
+
232
+ for (let i = prev.length - 1; i >= 0; i--) {
233
+ const message = prev[i]
234
+ if (message.role !== 'assistant' || !message.segments) continue
235
+
236
+ for (let j = message.segments.length - 1; j >= 0; j--) {
237
+ const seg = message.segments[j]
238
+
239
+ if (
240
+ execId &&
241
+ seg.type === 'approval_batch' &&
242
+ seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId)
243
+ ) {
244
+ const prevExec: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
245
+ // Never downgrade a done batch slot back to executing (JetStream
246
+ // redelivery of the EXECUTING chunk after EXECUTED landed). Returning
247
+ // prev = matched-with-no-change, so the caller doesn't append either.
248
+ if (toolData.type === 'EXECUTING_TOOL' && prevExec?.status === 'done') {
249
+ return prev
250
+ }
251
+ const nextExec: ApprovalBatchExecutionState =
252
+ toolData.type === 'EXECUTED_TOOL'
253
+ ? { status: 'done', result: toolData.result, success: toolData.success }
254
+ : { status: 'executing', result: prevExec?.result, success: prevExec?.success }
255
+ // Value-level no-op (replayed duplicate) → prior references.
256
+ if (sameExecutionState(prevExec, nextExec)) return prev
257
+ const nextSegments = [...message.segments]
258
+ nextSegments[j] = {
259
+ ...seg,
260
+ data: { ...seg.data, executions: { ...(seg.data.executions ?? {}), [execId]: nextExec } },
261
+ }
262
+ const next = [...prev]
263
+ next[i] = { ...message, segments: nextSegments }
264
+ return next
265
+ }
266
+
267
+ if (seg.type === 'tool_execution') {
268
+ // KNOWN LIMIT (id-less chunks only — current backends always send
269
+ // toolExecutionRequestId): the fuzzy fallback pairs only with a
270
+ // segment still EXECUTING, so a replayed id-less EXECUTING/EXECUTED
271
+ // arriving after the pair completed matches nothing and falls to the
272
+ // caller's append (duplicate card). Widening the predicate to
273
+ // EXECUTED twins would instead swallow a LEGITIMATE second run of the
274
+ // same tool — without ids the two are indistinguishable, and losing a
275
+ // real run is worse than a duplicate card on a legacy transport.
276
+ const matches = execId
277
+ ? seg.data.toolExecutionRequestId === execId
278
+ : seg.data.type === 'EXECUTING_TOOL' &&
279
+ seg.data.integratedToolType === toolData.integratedToolType &&
280
+ seg.data.toolFunction === toolData.toolFunction
281
+ if (!matches) continue
282
+ // Never downgrade a completed segment back to EXECUTING (replayed
283
+ // EXECUTING chunk after its EXECUTED already landed).
284
+ if (toolData.type === 'EXECUTING_TOOL' && seg.data.type === 'EXECUTED_TOOL') {
285
+ return prev
286
+ }
287
+ const mergedData: ToolExecutionData = {
288
+ ...toolData,
289
+ toolTitle: toolData.toolTitle ?? seg.data.toolTitle,
290
+ parameters: toolData.parameters || seg.data.parameters,
291
+ }
292
+ // Value-level no-op (replayed duplicate) → prior references.
293
+ if (sameToolData(seg.data, mergedData)) return prev
294
+ const nextSegments = [...message.segments]
295
+ nextSegments[j] = { type: 'tool_execution', data: mergedData }
296
+ const next = [...prev]
297
+ next[i] = { ...message, segments: nextSegments }
298
+ return next
299
+ }
300
+ }
301
+ }
302
+
303
+ return null
304
+ }
305
+
306
+ /**
307
+ * Cross-message approval status flip (absorbed from the NATS adapter's
308
+ * `onApprovalResolved` handler). Idempotent + reference-preserving: only the
309
+ * messages/segments whose status (or resolvedByName) actually changes are
310
+ * cloned; a no-op returns `prev`.
311
+ */
312
+ export function projectApprovalResolutionToMessages(
313
+ prev: UnifiedChatMessage[],
314
+ requestId: string,
315
+ status: ChatApprovalStatus,
316
+ resolvedByName?: string | null,
317
+ ): UnifiedChatMessage[] {
318
+ let changed = false
319
+ const next = prev.map((m) => {
320
+ if (m.role !== 'assistant' || !m.segments) return m
321
+ let msgChanged = false
322
+ const segs = m.segments.map((s) => {
323
+ if (s.type === 'approval_request' && s.data.requestId === requestId && s.status !== status) {
324
+ msgChanged = true
325
+ return { ...s, status }
326
+ }
327
+ if (s.type === 'approval_batch' && s.data.approvalRequestId === requestId) {
328
+ const nextResolvedBy = resolvedByName ?? s.resolvedByName
329
+ if (s.status === status && nextResolvedBy === s.resolvedByName) return s
330
+ msgChanged = true
331
+ return { ...s, status, resolvedByName: nextResolvedBy }
332
+ }
333
+ return s
334
+ })
335
+ if (!msgChanged) return m
336
+ changed = true
337
+ return { ...m, segments: segs }
338
+ })
339
+ return changed ? next : prev
340
+ }
@@ -0,0 +1,126 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * useChatStreamReducer — thin React wrapper over a `ChatDialogStore` side.
5
+ *
6
+ * - `useSyncExternalStore` over the store (snapshot identity is stable
7
+ * between mutations, so untouched renders bail out);
8
+ * - APPEND-ONLY DELTA BATCHING: delegated wholesale to the framework-free
9
+ * `createDeltaBatcher` (same module) so a non-React host can share the
10
+ * exact same coalescing/flush policy instead of re-implementing it.
11
+ * - Any NON-delta event force-flushes the pending batch first, so ordering
12
+ * is preserved and stream completion (`turn-end`, errors, approval
13
+ * frames) always lands on fully-applied delta state. Adapters whose
14
+ * transport has no terminal event (SSE) call `flushDeltas()` explicitly
15
+ * before their end-of-turn command.
16
+ */
17
+
18
+ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
19
+ import type { ChatStreamEvent } from '../../../chat-protocol/events'
20
+ import {
21
+ DEFAULT_DIALOG_SIDE,
22
+ type ChatDialogSide,
23
+ type ChatDialogStore,
24
+ } from './chat-dialog-store'
25
+ import { createDeltaBatcher, type DeltaBatcher } from './delta-batcher'
26
+ import type {
27
+ ChatReducerState,
28
+ ChatStreamReducer,
29
+ ChatStreamReducerOptions,
30
+ } from './chat-stream-reducer'
31
+
32
+ interface DialogKey {
33
+ dialogId: string
34
+ side: ChatDialogSide
35
+ }
36
+
37
+ export interface UseChatStreamReducerOptions {
38
+ store: ChatDialogStore
39
+ dialogId: string
40
+ side?: ChatDialogSide
41
+ /** Consulted once, when this (dialogId, side) reducer is first created. */
42
+ createReducerOptions?: () => ChatStreamReducerOptions
43
+ }
44
+
45
+ export interface UseChatStreamReducerReturn {
46
+ state: ChatReducerState
47
+ /** Apply a decoded event (deltas are batched; everything else immediate). */
48
+ applyEvent: (event: ChatStreamEvent) => void
49
+ /** Synchronously apply any pending delta batch. */
50
+ flushDeltas: () => void
51
+ /** Run reducer commands (adapter-driven, non-wire mutations). */
52
+ mutate: <T>(fn: (reducer: ChatStreamReducer) => T) => T
53
+ reducer: ChatStreamReducer
54
+ }
55
+
56
+ export function useChatStreamReducer({
57
+ store,
58
+ dialogId,
59
+ side = DEFAULT_DIALOG_SIDE,
60
+ createReducerOptions,
61
+ }: UseChatStreamReducerOptions): UseChatStreamReducerReturn {
62
+ const optionsRef = useRef(createReducerOptions)
63
+ optionsRef.current = createReducerOptions
64
+
65
+ // Ensure the reducer exists before the first snapshot read.
66
+ const reducer = store.getReducer(dialogId, side, () => optionsRef.current?.() ?? {})
67
+
68
+ const getSnapshot = useCallback(() => store.getSnapshot(dialogId, side), [store, dialogId, side])
69
+ // Distinct server getter: `getSnapshot` is a pure read of a CLIENT-lived
70
+ // map, so reusing it for SSR would render a per-request-meaningless value
71
+ // (and, before it was made pure, silently accumulate reducers per request).
72
+ const getServerSnapshot = useCallback(() => store.getServerSnapshot(), [store])
73
+ const subscribe = useCallback((listener: () => void) => store.subscribe(listener), [store])
74
+ const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
75
+
76
+ // Pin this key for as long as the hook is mounted, so the store's LRU can
77
+ // never evict a reducer a live panel is rendering.
78
+ useEffect(() => store.retain(dialogId, side), [store, dialogId, side])
79
+
80
+ // ── Delta batch ──────────────────────────────────────────────────────────
81
+ // Identity-stable key object: the batcher compares keys with `Object.is`,
82
+ // so it must only change when the (dialogId, side) VALUES change.
83
+ const keyRef = useRef<DialogKey>({ dialogId, side })
84
+ if (keyRef.current.dialogId !== dialogId || keyRef.current.side !== side) {
85
+ keyRef.current = { dialogId, side }
86
+ }
87
+ const storeRef = useRef(store)
88
+ storeRef.current = store
89
+
90
+ const batcherRef = useRef<DeltaBatcher<DialogKey> | null>(null)
91
+ if (batcherRef.current === null) {
92
+ batcherRef.current = createDeltaBatcher<DialogKey>({
93
+ applyOne: (event, key) => {
94
+ if (key) storeRef.current.apply(key.dialogId, key.side, event)
95
+ },
96
+ })
97
+ }
98
+ const batcher = batcherRef.current
99
+
100
+ const flushDeltas = useCallback(() => batcher.flush(), [batcher])
101
+
102
+ const applyEvent = useCallback(
103
+ (event: ChatStreamEvent) => {
104
+ // `push` flushes on key change and returns false for non-delta events;
105
+ // those flush the batch first (ordering) and apply synchronously, so
106
+ // completion state always lands on fully-applied deltas.
107
+ if (batcher.push(event, keyRef.current)) return
108
+ batcher.flush()
109
+ store.apply(dialogId, side, event)
110
+ },
111
+ [batcher, store, dialogId, side],
112
+ )
113
+
114
+ // Flush on unmount so a torn-down panel doesn't drop its tail deltas.
115
+ useEffect(() => () => batcher.dispose(), [batcher])
116
+
117
+ const mutate = useCallback(
118
+ <T,>(fn: (reducer: ChatStreamReducer) => T): T => store.mutate(dialogId, side, fn),
119
+ [store, dialogId, side],
120
+ )
121
+
122
+ return useMemo(
123
+ () => ({ state, applyEvent, flushDeltas, mutate, reducer }),
124
+ [state, applyEvent, flushDeltas, mutate, reducer],
125
+ )
126
+ }