@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,536 @@
1
+ /**
2
+ * createChatDialogStore — framework-free registry of `ChatStreamReducer`
3
+ * instances keyed by `(dialogId, side)`. Reducer state PERSISTS across
4
+ * component unmounts (the store outlives React), and the store is the
5
+ * `useSyncExternalStore` source for the `useChatStreamReducer` wrapper.
6
+ *
7
+ * CROSS-SIDE PROJECTION: exactly two operations fan out across sides of the
8
+ * same dialogId —
9
+ * 1. approval RESOLUTION by requestId (`approval-resolved` events), and
10
+ * 2. tool-execution MERGE by execId (`tool-execution` events)
11
+ * — implemented as idempotent pure projections over the other side's state
12
+ * (recomputed from state, never re-injected into the other side's seq
13
+ * stream; per-side seq gates stay untouched). Everything else — text,
14
+ * thinking, participant rows — is strictly per-side.
15
+ *
16
+ * LIFECYCLE / MEMORY: the reducer map is LRU-capped (`maxReducers`, default
17
+ * `DEFAULT_MAX_REDUCERS`) and evicted through the same path as `remove()`.
18
+ * Without a cap an agent cycling hundreds of dialogs would retain a reducer
19
+ * (thread + accumulator) per dialog for the process's lifetime; under SSR,
20
+ * where the store is module-scoped, that growth is shared across every
21
+ * request. `getSnapshot` is PURE — it never creates an entry — and
22
+ * `getServerSnapshot` always returns the shared frozen `EMPTY_STATE`.
23
+ *
24
+ * EVICTION SAFETY (LRU recency alone is NOT liveness):
25
+ * 1. RETAINED keys are never evicted. `retain(dialogId, side)` returns a
26
+ * release fn; the `useChatStreamReducer` hook calls it for as long as it
27
+ * is mounted, so a quiet-but-visible panel can't be dropped just because
28
+ * ten other keys were touched. NON-React hosts must call it too.
29
+ * Because the hook retains in an EFFECT but takes its reducer during
30
+ * RENDER, a key created by a public `getReducer` is protected from
31
+ * creation until its FIRST `retain()` (bounded by
32
+ * `MAX_PENDING_RETAIN_KEYS`) — see `pendingRetain`.
33
+ * 2. A reducer whose `streamingPhase !== 'idle'` is never evicted — dropping
34
+ * mid-turn would recreate an EMPTY reducer on the next `getReducer`
35
+ * while the host's seq cursor still says "caught up", i.e. the thread
36
+ * visually vanishes with no path back.
37
+ * 3. Eviction hands the host the dropped instance's PARKED state
38
+ * (`onEvict(..., { messages, approvalStatuses, lastAppliedSeq,
39
+ * pendingEchoes })`). A recreated reducer is pristine, so re-seeding
40
+ * messages alone would
41
+ * resurrect resolved approvals as actionable and reset the seq gate to
42
+ * `-Infinity`; the parked payload is what makes the round-trip lossless.
43
+ * 4. Eviction NEVER notifies synchronously. `getReducer` is called during
44
+ * render (by the hook), and notifying there means updating other
45
+ * components mid-render. The notify is deferred to a microtask.
46
+ */
47
+
48
+ import {
49
+ createChatStreamReducer,
50
+ type ChatReducerState,
51
+ type ChatStreamReducer,
52
+ type ChatStreamReducerOptions,
53
+ type PendingEcho,
54
+ } from './chat-stream-reducer'
55
+ import type { ChatStreamEvent } from '../../../chat-protocol/events'
56
+
57
+ export type ChatDialogSide = string
58
+
59
+ /** Factory for a key's reducer options, consulted ONCE at creation. A plain
60
+ * `() => options` thunk remains assignable. */
61
+ export type CreateReducerOptionsFn = (
62
+ dialogId: string,
63
+ side: ChatDialogSide,
64
+ ) => ChatStreamReducerOptions
65
+
66
+ export interface ChatDialogStore {
67
+ /** Create-or-get the reducer for `(dialogId, side)`. `createOptions` is
68
+ * consulted ONLY when the instance is first created. */
69
+ getReducer(
70
+ dialogId: string,
71
+ side?: ChatDialogSide,
72
+ createOptions?: CreateReducerOptionsFn,
73
+ ): ChatStreamReducer
74
+ /** Apply one event to a side, then fan out the cross-side projections to
75
+ * the other sides of the same dialog. */
76
+ apply(dialogId: string, side: ChatDialogSide, event: ChatStreamEvent): void
77
+ /** Run adapter commands against a reducer; subscribers are notified. */
78
+ mutate<T>(dialogId: string, side: ChatDialogSide, fn: (reducer: ChatStreamReducer) => T): T
79
+ subscribe(listener: () => void): () => void
80
+ /**
81
+ * Referentially-stable snapshot for `useSyncExternalStore`. PURE — an
82
+ * unknown key returns the shared frozen `EMPTY_STATE` rather than
83
+ * creating a reducer (a create-on-read would let SSR renders accumulate
84
+ * reducers in a process-global map, and would make React's snapshot read
85
+ * a mutation).
86
+ */
87
+ getSnapshot(dialogId: string, side?: ChatDialogSide): ChatReducerState
88
+ /** `useSyncExternalStore`'s server getter — always `EMPTY_STATE`. There is
89
+ * no per-request store on the server, so a live thread cannot be rendered
90
+ * server-side; the client hydrates from the real reducer. */
91
+ getServerSnapshot(): ChatReducerState
92
+ /** Drop a side's reducer (or every side of the dialog when omitted). */
93
+ remove(dialogId: string, side?: ChatDialogSide): void
94
+ /**
95
+ * PIN `(dialogId, side)` against LRU eviction while a consumer is live.
96
+ * Returns the release fn (idempotent; refcounted, so nested retains are
97
+ * safe). Creation is NOT implied — retaining an absent key simply means the
98
+ * key is protected once it exists.
99
+ */
100
+ retain(dialogId: string, side?: ChatDialogSide): () => void
101
+ /**
102
+ * POLICY retain: keep exactly `keys` pinned by this store-level set and
103
+ * release every key the set previously pinned. Independent of the refcounted
104
+ * `retain()` handles a host's components hold — the two compose.
105
+ *
106
+ * Exists because "retain this set, release the rest" is generic store
107
+ * policy that every multi-panel host otherwise re-derives (diffing its own
108
+ * key list against a map of release fns). Retains BEFORE releasing, so a key
109
+ * present in both the old and the new set never momentarily drops to zero
110
+ * retains and becomes evictable mid-swap.
111
+ */
112
+ setRetained(keys: Array<{ dialogId: string; side?: ChatDialogSide }>): void
113
+ }
114
+
115
+ export const DEFAULT_DIALOG_SIDE: ChatDialogSide = 'main'
116
+
117
+ /** Most-recently-used reducer keys retained before eviction. Ten covers
118
+ * every realistic multi-panel / dialog-switching session while bounding an
119
+ * agent that cycles hundreds of dialogs. */
120
+ export const DEFAULT_MAX_REDUCERS = 10
121
+
122
+ /**
123
+ * Hard cap on keys held in the "created by `getReducer`, `retain()` still
124
+ * pending" protection set — the ONLY thing bounding the map while retains are
125
+ * outstanding, since that protection ends at the retain rather than on a timer.
126
+ *
127
+ * Deliberately far above `maxReducers`: a host may legitimately mount MORE
128
+ * panels than the LRU cap (once their effects run, retained keys already let
129
+ * the map exceed it), so capping the pending set at `maxReducers` would blank
130
+ * exactly the panels round 10 fixed. It is a backstop against a caller that
131
+ * reads a reducer once and drops it — or a concurrent render React discards
132
+ * before any effect runs — not a per-commit budget. Past it, the oldest pending
133
+ * key falls through to ordinary LRU.
134
+ */
135
+ export const MAX_PENDING_RETAIN_KEYS = 256
136
+
137
+ export interface CreateChatDialogStoreOptions {
138
+ /** LRU cap on retained reducers (keys are `(dialogId, side)` pairs).
139
+ * Values < 1 are clamped to 1. Default `DEFAULT_MAX_REDUCERS`. */
140
+ maxReducers?: number
141
+ /**
142
+ * Options applied to any reducer this store creates WITHOUT an explicit
143
+ * per-call `createOptions` — i.e. every reducer first materialized by
144
+ * `apply()` or `mutate()`. Hosts with non-default reducer semantics
145
+ * (`ownEchoIncludesAdmin`, `selfUserId`, approve/reject `callbacks`, …)
146
+ * MUST set this: creation options are consulted once, so a bare
147
+ * apply-created instance would keep those semantics missing for its whole
148
+ * lifetime, including for a later `getReducer(..., options)` caller.
149
+ */
150
+ defaultCreateOptions?: CreateReducerOptionsFn
151
+ /**
152
+ * Called when LRU eviction actually DROPS a key. The store knows this
153
+ * exactly; without publishing it, hosts are left inferring eviction by
154
+ * comparing reducer OBJECT IDENTITY across `getReducer` calls and then
155
+ * suppressing the "pristine empty" snapshot that a silently-recreated
156
+ * reducer returns — archaeology about a fact this callback states outright.
157
+ *
158
+ * Fires only for LRU eviction. Host-initiated `remove()` needs no
159
+ * notification: the caller already knows which key it dropped.
160
+ *
161
+ * Called AFTER the key is gone, from `getReducer` (i.e. potentially during a
162
+ * React render) — treat it as a "schedule a refetch/rehydrate" signal, not a
163
+ * place to set state synchronously.
164
+ *
165
+ * `parked` carries the dropped reducer's non-refetchable state so the host
166
+ * can restore it on the recreated instance (`initializeWithState(messages,
167
+ * { approvalStatuses, lastAppliedSeq, pendingEchoes })`). Refetching
168
+ * messages alone is NOT
169
+ * equivalent: a recreated reducer starts with `approvalStatuses = {}` and
170
+ * `lastAppliedSeq = -Infinity`, so a resolved approval whose APPROVAL_RESULT
171
+ * row is not in the refetched history page re-renders as ACTIONABLE (the
172
+ * exact hazard `resetForDialogSwitch` preserves that map to avoid), and a
173
+ * replay from the host's own cursor re-applies events the dropped instance
174
+ * had already consumed.
175
+ */
176
+ onEvict?: (
177
+ dialogId: string,
178
+ side: ChatDialogSide,
179
+ parked: EvictedReducerState,
180
+ ) => void
181
+ }
182
+
183
+ /**
184
+ * Snapshot of an LRU-evicted reducer's state, captured just before the
185
+ * instance is dropped. Everything here either cannot be refetched
186
+ * (`lastAppliedSeq`) or is not reliably present in a refetched history page
187
+ * (`approvalStatuses`); `messages` rides along so a host that keeps its own
188
+ * copy does not have to.
189
+ */
190
+ export interface EvictedReducerState {
191
+ messages: ChatReducerState['messages']
192
+ approvalStatuses: ChatReducerState['approvalStatuses']
193
+ /** `-Infinity` when the instance never applied a seq-carrying event. */
194
+ lastAppliedSeq: number
195
+ /**
196
+ * Optimistic-echo entries still armed at eviction time. A key dropped
197
+ * between `pushOptimisticSend` and its `MESSAGE_REQUEST` echo would
198
+ * otherwise leave the recreated reducer with nothing armed, and the echo
199
+ * renders a DUPLICATE user bubble. Replay via
200
+ * `initializeWithState(messages, { pendingEchoes })`; the reducer drops
201
+ * entries already past `OWN_ECHO_AUTHOR_TTL_MS`. Empty for a key with no
202
+ * send in flight (the common case).
203
+ */
204
+ pendingEchoes: readonly PendingEcho[]
205
+ }
206
+
207
+ /** Shared, frozen "no reducer for this key" snapshot. ONE instance, so
208
+ * repeated `getSnapshot` reads of an absent key are referentially stable
209
+ * for `useSyncExternalStore`. */
210
+ const EMPTY_STATE: ChatReducerState = Object.freeze({
211
+ messages: Object.freeze([]) as unknown as ChatReducerState['messages'],
212
+ streamingPhase: 'idle',
213
+ turnMeta: Object.freeze({
214
+ meta: new Map(),
215
+ sources: new Map(),
216
+ refs: new Map(),
217
+ sendCount: 0,
218
+ }) as ChatReducerState['turnMeta'],
219
+ dialogTokenUsage: null,
220
+ liveModel: null,
221
+ approvalStatuses: Object.freeze({}) as ChatReducerState['approvalStatuses'],
222
+ }) as ChatReducerState
223
+
224
+ /** Shared frozen empty list for a key with no send in flight. */
225
+ const EMPTY_PENDING_ECHOES: readonly PendingEcho[] = Object.freeze([])
226
+
227
+ const KEY_SEP = '\u0000'
228
+ const keyOf = (dialogId: string, side: ChatDialogSide) => `${dialogId}${KEY_SEP}${side}`
229
+
230
+ export function createChatDialogStore(
231
+ options: CreateChatDialogStoreOptions = {},
232
+ ): ChatDialogStore {
233
+ const maxReducers = Math.max(1, options.maxReducers ?? DEFAULT_MAX_REDUCERS)
234
+ const defaultCreateOptions = options.defaultCreateOptions
235
+ const onEvict = options.onEvict
236
+ // Insertion order IS the LRU order: `touch` re-inserts at the tail, so the
237
+ // FIRST key is always the least-recently-used one.
238
+ const reducers = new Map<string, ChatStreamReducer>()
239
+ const listeners = new Set<() => void>()
240
+ /** key → live retain count (see EVICTION SAFETY #1). */
241
+ const retained = new Map<string, number>()
242
+
243
+ function notify(): void {
244
+ for (const listener of listeners) listener()
245
+ }
246
+
247
+ /** Notify OUTSIDE the current task — `getReducer` (and therefore eviction)
248
+ * runs during React's render phase. See EVICTION SAFETY #4. */
249
+ function notifyDeferred(): void {
250
+ if (typeof queueMicrotask === 'function') queueMicrotask(notify)
251
+ else Promise.resolve().then(notify)
252
+ }
253
+
254
+ /**
255
+ * Keys handed out by a PUBLIC `getReducer` whose `retain()` has not landed
256
+ * yet. ALL of them are protected from eviction, not just the one being
257
+ * inserted right now.
258
+ *
259
+ * `useChatStreamReducer` calls `getReducer` during RENDER (to hand the
260
+ * component its reducer) and `retain()` in an EFFECT, so React can render N
261
+ * panels with N distinct keys before a single retain lands. Protecting only
262
+ * the key being inserted let render #(cap+1) evict the key render #1 had
263
+ * just created: panel #1 already holds that reducer object, but
264
+ * `getSnapshot` is a pure map read, so its thread renders as `EMPTY_STATE`
265
+ * forever and its later `retain()` pins a key that no longer exists — a
266
+ * blank thread unless the host re-seeds from `onEvict`.
267
+ *
268
+ * PROTECTION ENDS AT THE FIRST `retain()` (`retainKey` deletes the entry),
269
+ * NOT on a timer. This set exists solely to stand in for the retain that has
270
+ * not happened yet, so the retain itself is the only non-guessed end of the
271
+ * window — and from that moment the retain COUNT protects the key instead.
272
+ * The previous microtask window assumed a render/commit pass never yields to
273
+ * the microtask queue; under CONCURRENT rendering React time-slices and
274
+ * resumes in a later macrotask (microtasks drain in between), so keys created
275
+ * before the yield silently lost protection mid-render and the exact
276
+ * blank-thread defect above could recur.
277
+ *
278
+ * BOUND: a caller that reads a reducer once and drops it never retains, so
279
+ * the set is FIFO-capped at `MAX_PENDING_RETAIN_KEYS`; the oldest pending key
280
+ * falls through to ordinary LRU when a newer one arrives. Hence
281
+ * `reducers.size <= max(maxReducers, R + MAX_PENDING_RETAIN_KEYS)` where `R`
282
+ * counts retained / mid-stream keys — the same "protected keys may exceed the
283
+ * cap" rule that has always applied to retained ones. Eviction itself stays
284
+ * SYNCHRONOUS, and the loop always catches back up because `overBy` is
285
+ * recomputed from `reducers.size`.
286
+ *
287
+ * Event-phase creations (`apply()` / `mutate()` first-touching a key) are
288
+ * deliberately NOT grouped: nobody holds those instances past the call, so a
289
+ * burst of events across many dialogs must still evict down to the cap
290
+ * within the same task. They protect only the key currently being written,
291
+ * via `evictExcess`'s `protectKey`.
292
+ */
293
+ const pendingRetain = new Set<string>()
294
+
295
+ function markPendingRetain(key: string): void {
296
+ pendingRetain.add(key)
297
+ if (pendingRetain.size <= MAX_PENDING_RETAIN_KEYS) return
298
+ // Set iteration is insertion-ordered: the oldest still-unretained key is
299
+ // the one least likely to belong to a live consumer.
300
+ const oldest = pendingRetain.values().next().value
301
+ if (oldest !== undefined) pendingRetain.delete(oldest)
302
+ }
303
+
304
+ /** Mark `key` most-recently-used. */
305
+ function touch(key: string): void {
306
+ const reducer = reducers.get(key)
307
+ if (reducer === undefined) return
308
+ reducers.delete(key)
309
+ reducers.set(key, reducer)
310
+ }
311
+
312
+ /** A key is evictable only when nothing pins it and its turn is finished. */
313
+ function isEvictable(key: string): boolean {
314
+ if ((retained.get(key) ?? 0) > 0) return false
315
+ const reducer = reducers.get(key)
316
+ return reducer === undefined || reducer.state.streamingPhase === 'idle'
317
+ }
318
+
319
+ /** Evict least-recently-used EVICTABLE keys down to the cap, through the
320
+ * same drop path as `remove()`. Protected keys (retained / mid-stream) are
321
+ * skipped, so the map can legitimately sit above `maxReducers` for as long
322
+ * as that many live threads exist. Returns true when anything was
323
+ * evicted.
324
+ *
325
+ * Keys awaiting their first `retain()` (`pendingRetain`) are protected as a
326
+ * GROUP: they are unretained (the hook retains in an effect, i.e. after
327
+ * render) and idle, so without that guard a full map of protected older keys
328
+ * would walk the loop all the way to the tail and drop a brand-new entry —
329
+ * `getReducer` would return a DETACHED reducer whose mutations no snapshot
330
+ * can ever see (`getSnapshot` is a pure map read, so that key returns
331
+ * `EMPTY_STATE` forever). See `pendingRetain` for why every key still
332
+ * awaiting a retain is protected, not just the one being inserted. */
333
+ function evictExcess(protectKey: string): boolean {
334
+ let evicted = false
335
+ let overBy = reducers.size - maxReducers
336
+ if (overBy <= 0) return false
337
+ for (const key of [...reducers.keys()]) {
338
+ if (overBy <= 0) break
339
+ if (key === protectKey || pendingRetain.has(key)) continue
340
+ if (!isEvictable(key)) continue
341
+ // Capture BEFORE the drop — after `delete` the instance is unreachable
342
+ // and its approval statuses / seq cursor would be lost with it.
343
+ const dropped = onEvict ? reducers.get(key) : undefined
344
+ reducers.delete(key)
345
+ evicted = true
346
+ overBy -= 1
347
+ if (onEvict) {
348
+ const sep = key.indexOf(KEY_SEP)
349
+ onEvict(key.slice(0, sep), key.slice(sep + 1), {
350
+ messages: dropped?.state.messages ?? EMPTY_STATE.messages,
351
+ approvalStatuses: dropped?.state.approvalStatuses ?? EMPTY_STATE.approvalStatuses,
352
+ lastAppliedSeq: dropped?.getLastAppliedSeq() ?? Number.NEGATIVE_INFINITY,
353
+ pendingEchoes: dropped?.getPendingEchoes() ?? EMPTY_PENDING_ECHOES,
354
+ })
355
+ }
356
+ }
357
+ return evicted
358
+ }
359
+
360
+ /**
361
+ * @param groupProtect true for PUBLIC (render-phase) calls, whose caller
362
+ * keeps the returned instance until its `retain()` lands — see
363
+ * `pendingRetain`. Event-phase callers (`apply` / `mutate`) pass false: they
364
+ * use the reducer within the call and must not defer eviction of a burst.
365
+ */
366
+ function getReducerInternal(
367
+ dialogId: string,
368
+ side: ChatDialogSide = DEFAULT_DIALOG_SIDE,
369
+ createOptions?: CreateReducerOptionsFn,
370
+ groupProtect = true,
371
+ ): ChatStreamReducer {
372
+ const key = keyOf(dialogId, side)
373
+ let reducer = reducers.get(key)
374
+ if (!reducer) {
375
+ // `apply()` / `mutate()` can be the FIRST toucher of a key (an event for
376
+ // a dialog no component has rendered yet). Without the store-level
377
+ // default they would create a reducer missing the host's
378
+ // `ownEchoIncludesAdmin` / `callbacks` / `batchApprovalsEnabled`, and a
379
+ // later `getReducer(..., options)` would hand back that mis-configured
380
+ // instance (create-options are consulted at creation only).
381
+ const base = (createOptions ?? defaultCreateOptions)?.(dialogId, side) ?? {}
382
+ const userOnChange = base.onChange
383
+ reducer = createChatStreamReducer({
384
+ ...base,
385
+ onChange: () => {
386
+ userOnChange?.()
387
+ notify()
388
+ },
389
+ })
390
+ reducers.set(key, reducer)
391
+ // An already-retained key needs no stand-in — it is protected by its
392
+ // retain count, and marking it would burn a slot of the bounded set.
393
+ if (groupProtect && (retained.get(key) ?? 0) === 0) markPendingRetain(key)
394
+ if (evictExcess(key)) notifyDeferred()
395
+ } else {
396
+ touch(key)
397
+ }
398
+ return reducer
399
+ }
400
+
401
+ const getReducer: ChatDialogStore['getReducer'] = (dialogId, side, createOptions) =>
402
+ getReducerInternal(dialogId, side, createOptions, true)
403
+
404
+ function otherSides(dialogId: string, side: ChatDialogSide): ChatStreamReducer[] {
405
+ const prefix = `${dialogId}${KEY_SEP}`
406
+ const selfKey = keyOf(dialogId, side)
407
+ const out: ChatStreamReducer[] = []
408
+ for (const [key, reducer] of reducers) {
409
+ if (key !== selfKey && key.startsWith(prefix)) out.push(reducer)
410
+ }
411
+ return out
412
+ }
413
+
414
+ function apply(dialogId: string, side: ChatDialogSide, event: ChatStreamEvent): void {
415
+ const reducer = getReducerInternal(dialogId, side, undefined, false)
416
+ reducer.apply(event)
417
+
418
+ // Cross-side fan-out — the ONLY two operations that cross sides.
419
+ if (event.type === 'approval-resolved' && event.requestId) {
420
+ for (const other of otherSides(dialogId, side)) {
421
+ other.projectApprovalResolution(event.requestId, event.status, event.resolvedByName)
422
+ }
423
+ } else if (event.type === 'tool-execution') {
424
+ for (const other of otherSides(dialogId, side)) {
425
+ other.projectToolExecution({ type: 'tool_execution', data: event.data })
426
+ }
427
+ }
428
+ }
429
+
430
+ function mutate<T>(
431
+ dialogId: string,
432
+ side: ChatDialogSide,
433
+ fn: (reducer: ChatStreamReducer) => T,
434
+ ): T {
435
+ return fn(getReducerInternal(dialogId, side, undefined, false))
436
+ }
437
+
438
+ function subscribe(listener: () => void): () => void {
439
+ listeners.add(listener)
440
+ return () => {
441
+ listeners.delete(listener)
442
+ }
443
+ }
444
+
445
+ /** PURE read — deliberately does NOT go through `getReducer`, which would
446
+ * insert into the module-lived map on every miss (unbounded growth under
447
+ * SSR) and make a React snapshot read mutate the store. Callers that need
448
+ * a reducer to exist call `getReducer` explicitly first (the
449
+ * `useChatStreamReducer` hook does exactly that, during render). */
450
+ function getSnapshot(
451
+ dialogId: string,
452
+ side: ChatDialogSide = DEFAULT_DIALOG_SIDE,
453
+ ): ChatReducerState {
454
+ return reducers.get(keyOf(dialogId, side))?.state ?? EMPTY_STATE
455
+ }
456
+
457
+ function getServerSnapshot(): ChatReducerState {
458
+ return EMPTY_STATE
459
+ }
460
+
461
+ /** Notifies only when something was ACTUALLY dropped — an unconditional
462
+ * notify wakes every subscriber for a no-op removal, and the snapshot each
463
+ * would read is byte-identical.
464
+ *
465
+ * Deliberately does NOT touch `retained` / `policyRetains`. A retain is a
466
+ * claim on the KEY, not on the instance (`retain()` never implies creation),
467
+ * and `remove()` is routinely followed by a re-render that recreates the key
468
+ * — a still-mounted consumer must keep protecting it across that round trip.
469
+ * Dropping the counter here would also desync the outstanding release
470
+ * closures: with two live retainers, the first release would decrement from
471
+ * the `?? 1` fallback straight to 0 and unpin a key the second consumer is
472
+ * still holding. The entries are bounded by live retainers (never by removed
473
+ * dialogs) and are deleted when the last release runs. */
474
+ function remove(dialogId: string, side?: ChatDialogSide): void {
475
+ let removed = false
476
+ if (side !== undefined) {
477
+ removed = reducers.delete(keyOf(dialogId, side))
478
+ } else {
479
+ const prefix = `${dialogId}${KEY_SEP}`
480
+ for (const key of [...reducers.keys()]) {
481
+ if (key.startsWith(prefix) && reducers.delete(key)) removed = true
482
+ }
483
+ }
484
+ if (removed) notify()
485
+ }
486
+
487
+ function retainKey(key: string): () => void {
488
+ // The retain this key was waiting for has landed: the refcount below now
489
+ // protects it, so the stand-in must go (leaving it would keep an unmounted
490
+ // key pinned and hold a slot of the bounded `pendingRetain` set).
491
+ pendingRetain.delete(key)
492
+ retained.set(key, (retained.get(key) ?? 0) + 1)
493
+ let released = false
494
+ return () => {
495
+ if (released) return
496
+ released = true
497
+ const next = (retained.get(key) ?? 1) - 1
498
+ if (next <= 0) retained.delete(key)
499
+ else retained.set(key, next)
500
+ }
501
+ }
502
+
503
+ function retain(dialogId: string, side: ChatDialogSide = DEFAULT_DIALOG_SIDE): () => void {
504
+ return retainKey(keyOf(dialogId, side))
505
+ }
506
+
507
+ /** Store-level policy retains, keyed the same way. Held separately from the
508
+ * per-consumer `retain()` handles so the two never clobber each other. */
509
+ const policyRetains = new Map<string, () => void>()
510
+
511
+ function setRetained(keys: Array<{ dialogId: string; side?: ChatDialogSide }>): void {
512
+ const next = new Set(keys.map((k) => keyOf(k.dialogId, k.side ?? DEFAULT_DIALOG_SIDE)))
513
+ // Retain the additions BEFORE releasing the removals: a key in both sets is
514
+ // simply left alone, and nothing in the new set is ever unpinned in between.
515
+ for (const key of next) {
516
+ if (!policyRetains.has(key)) policyRetains.set(key, retainKey(key))
517
+ }
518
+ for (const [key, release] of [...policyRetains]) {
519
+ if (next.has(key)) continue
520
+ release()
521
+ policyRetains.delete(key)
522
+ }
523
+ }
524
+
525
+ return {
526
+ getReducer,
527
+ apply,
528
+ mutate,
529
+ subscribe,
530
+ getSnapshot,
531
+ getServerSnapshot,
532
+ remove,
533
+ retain,
534
+ setRetained,
535
+ }
536
+ }