@flamingo-stack/openframe-frontend-core 0.0.488 → 0.0.490-1506.3944.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-protocol/decode.d.ts +58 -0
- package/dist/chat-protocol/decode.d.ts.map +1 -0
- package/dist/chat-protocol/encode.d.ts +44 -0
- package/dist/chat-protocol/encode.d.ts.map +1 -0
- package/dist/chat-protocol/env-flag.d.ts +35 -0
- package/dist/chat-protocol/env-flag.d.ts.map +1 -0
- package/dist/chat-protocol/events.d.ts +196 -0
- package/dist/chat-protocol/events.d.ts.map +1 -0
- package/dist/chat-protocol/frames.d.ts +213 -0
- package/dist/chat-protocol/frames.d.ts.map +1 -0
- package/dist/chat-protocol/index.cjs +494 -0
- package/dist/chat-protocol/index.cjs.map +1 -0
- package/dist/chat-protocol/index.d.ts +18 -0
- package/dist/chat-protocol/index.d.ts.map +1 -0
- package/dist/chat-protocol/index.js +478 -0
- package/dist/chat-protocol/index.js.map +1 -0
- package/dist/chat-protocol/ip-normalize.d.ts +44 -0
- package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
- package/dist/chat-protocol/nats-decoder.d.ts +26 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
- package/dist/{chunk-AJNDDXKC.js → chunk-4NWARV5I.js} +3 -3
- package/dist/{chunk-GQX6LFFA.cjs → chunk-5CJR2GZ4.cjs} +42 -42
- package/dist/{chunk-GQX6LFFA.cjs.map → chunk-5CJR2GZ4.cjs.map} +1 -1
- package/dist/{chunk-EUYIFALO.cjs → chunk-5EYRXSY5.cjs} +14 -14
- package/dist/{chunk-EUYIFALO.cjs.map → chunk-5EYRXSY5.cjs.map} +1 -1
- package/dist/{chunk-5RXKOGH6.cjs → chunk-5LMGFI4J.cjs} +7011 -4777
- package/dist/chunk-5LMGFI4J.cjs.map +1 -0
- package/dist/{chunk-KFQXO7QB.js → chunk-6GT557KN.js} +1 -1
- package/dist/chunk-6GT557KN.js.map +1 -0
- package/dist/{chunk-77DFPD35.js → chunk-BE6DK7QY.js} +12267 -10033
- package/dist/chunk-BE6DK7QY.js.map +1 -0
- package/dist/{chunk-A7IPON2V.cjs → chunk-BIKOJ4FH.cjs} +5 -5
- package/dist/{chunk-A7IPON2V.cjs.map → chunk-BIKOJ4FH.cjs.map} +1 -1
- package/dist/{chunk-HQYJINFV.js → chunk-CORIBKHG.js} +4 -4
- package/dist/{chunk-YEEHB4T6.cjs → chunk-CSYSYJCF.cjs} +40 -40
- package/dist/{chunk-YEEHB4T6.cjs.map → chunk-CSYSYJCF.cjs.map} +1 -1
- package/dist/{chunk-SIKCXKGB.cjs → chunk-DSDZ5T4P.cjs} +7 -7
- package/dist/{chunk-SIKCXKGB.cjs.map → chunk-DSDZ5T4P.cjs.map} +1 -1
- package/dist/{chunk-PNXNWGBY.js → chunk-GTWO5NAY.js} +2 -2
- package/dist/{chunk-SCCTJ2UI.js → chunk-HSACATCD.js} +4 -4
- package/dist/{chunk-PECGZWQR.cjs → chunk-KP5KRRD6.cjs} +3 -3
- package/dist/{chunk-PECGZWQR.cjs.map → chunk-KP5KRRD6.cjs.map} +1 -1
- package/dist/{chunk-2B6773QM.cjs → chunk-LILQJ3UZ.cjs} +1 -1
- package/dist/chunk-LILQJ3UZ.cjs.map +1 -0
- package/dist/{chunk-RZSA6MZL.cjs → chunk-NWX7MMF5.cjs} +15 -15
- package/dist/{chunk-RZSA6MZL.cjs.map → chunk-NWX7MMF5.cjs.map} +1 -1
- package/dist/{chunk-V3CNNMY3.cjs → chunk-P4WTZU6L.cjs} +18 -18
- package/dist/{chunk-V3CNNMY3.cjs.map → chunk-P4WTZU6L.cjs.map} +1 -1
- package/dist/{chunk-KLYHZRAD.js → chunk-PB3BTZDU.js} +2 -2
- package/dist/{chunk-FZOBQG6M.cjs → chunk-Q6E7VJ4W.cjs} +37 -36
- package/dist/chunk-Q6E7VJ4W.cjs.map +1 -0
- package/dist/{chunk-EYEIVYYF.cjs → chunk-QSCHCZL3.cjs} +7 -7
- package/dist/{chunk-EYEIVYYF.cjs.map → chunk-QSCHCZL3.cjs.map} +1 -1
- package/dist/{chunk-YJSKQ2ZV.js → chunk-RQLBHKXN.js} +3 -3
- package/dist/{chunk-CWQACI4Z.js → chunk-RW2LPL72.js} +2 -2
- package/dist/{chunk-DX6AOHNU.cjs → chunk-S2NHJDKQ.cjs} +77 -77
- package/dist/{chunk-DX6AOHNU.cjs.map → chunk-S2NHJDKQ.cjs.map} +1 -1
- package/dist/{chunk-AHDJMEEN.js → chunk-T2EGSZFN.js} +9 -8
- package/dist/chunk-T2EGSZFN.js.map +1 -0
- package/dist/{chunk-S52LJNMU.js → chunk-TKN5667N.js} +13 -13
- package/dist/chunk-TKN5667N.js.map +1 -0
- package/dist/{chunk-3BFX6HJM.js → chunk-UUYRJPGJ.js} +6 -6
- package/dist/{chunk-F6BYUO5U.js → chunk-VHG6AX5V.js} +3 -3
- package/dist/{chunk-FOQV6RCO.cjs → chunk-VQ523ORK.cjs} +29 -29
- package/dist/{chunk-FOQV6RCO.cjs.map → chunk-VQ523ORK.cjs.map} +1 -1
- package/dist/{chunk-PXF5J24T.js → chunk-XQ2B74SI.js} +2 -2
- package/dist/{chunk-H7JX2BG7.cjs → chunk-YHOIAA2E.cjs} +76 -76
- package/dist/chunk-YHOIAA2E.cjs.map +1 -0
- package/dist/{chunk-LLWBPPZ4.js → chunk-YZ2OIYQ3.js} +6 -6
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +0 -1
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-history-hydration.d.ts +47 -0
- package/dist/components/chat/hooks/use-chat-history-hydration.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-chat.d.ts +1 -0
- package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -53
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +12 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +23 -15
- package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
- package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
- package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
- package/dist/components/chat/stream/index.d.ts +17 -0
- package/dist/components/chat/stream/index.d.ts.map +1 -0
- package/dist/components/chat/stream/message-mutations.d.ts +77 -0
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/types/api.types.d.ts +1 -81
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +2 -90
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +5 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chat-conversation-storage.d.ts +34 -0
- package/dist/components/chat/utils/chat-conversation-storage.d.ts.map +1 -0
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +2 -3
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +182 -140
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +66 -24
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/index.cjs +46 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +57 -15
- package/dist/components/ui/markdown/base-components.d.ts +51 -0
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
- package/dist/components/ui/markdown/engine.d.ts +99 -0
- package/dist/components/ui/markdown/engine.d.ts.map +1 -0
- package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
- package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
- package/dist/components/ui/markdown/index.d.ts +18 -0
- package/dist/components/ui/markdown/index.d.ts.map +1 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
- package/dist/components/ui/markdown/sanitize.d.ts +153 -0
- package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/streaming.d.ts +78 -0
- package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
- package/dist/components/ui/markdown/text-size.d.ts +34 -0
- package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
- package/dist/contexts/chat-runtime-context.d.ts +8 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +62 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +73 -15
- package/dist/utils/index.cjs +169 -40
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +162 -41
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markdown-fences.d.ts +42 -0
- package/dist/utils/markdown-fences.d.ts.map +1 -0
- package/dist/utils/markdown-heading-id.d.ts +85 -0
- package/dist/utils/markdown-heading-id.d.ts.map +1 -0
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +307 -0
- package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
- package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
- package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +228 -0
- package/src/chat-protocol/decode.ts +278 -0
- package/src/chat-protocol/encode.ts +71 -0
- package/src/chat-protocol/env-flag.ts +39 -0
- package/src/chat-protocol/events.ts +232 -0
- package/src/chat-protocol/frames.ts +245 -0
- package/src/chat-protocol/index.ts +21 -0
- package/src/chat-protocol/ip-normalize.ts +146 -0
- package/src/chat-protocol/nats-decoder.ts +252 -0
- package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +209 -0
- package/src/components/chat/chat-message-enhanced.tsx +111 -17
- package/src/components/chat/chat-message-list.tsx +358 -109
- package/src/components/chat/embeddable-chat.tsx +6 -0
- package/src/components/chat/hooks/.index.md +30 -33
- package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
- package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
- package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
- package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
- package/src/components/chat/hooks/index.ts +0 -1
- package/src/components/chat/hooks/use-chat-history-hydration.ts +142 -0
- package/src/components/chat/hooks/use-chat.ts +20 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -823
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +417 -774
- package/src/components/chat/index.ts +5 -0
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +940 -0
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +894 -0
- package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
- package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
- package/src/components/chat/stream/chat-dialog-store.ts +536 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +1796 -0
- package/src/components/chat/stream/delta-batcher.ts +159 -0
- package/src/components/chat/stream/index.ts +55 -0
- package/src/components/chat/stream/message-mutations.ts +338 -0
- package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
- package/src/components/chat/thinking-display.tsx +1 -1
- package/src/components/chat/types/.api.types.md +39 -49
- package/src/components/chat/types/.processing.types.md +29 -58
- package/src/components/chat/types/api.types.ts +6 -73
- package/src/components/chat/types/processing.types.ts +11 -52
- package/src/components/chat/types/unified-chat-state.types.ts +6 -0
- package/src/components/chat/utils/.index.md +31 -40
- package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
- package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +317 -0
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
- package/src/components/chat/utils/chat-conversation-storage.ts +83 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
- package/src/components/chat/utils/index.ts +5 -8
- package/src/components/chat/utils/process-historical-messages.ts +352 -384
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +19 -4
- package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
- package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
- package/src/components/ui/index.ts +1 -2
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
- package/src/components/ui/markdown/base-components.tsx +360 -0
- package/src/components/ui/markdown/engine.tsx +315 -0
- package/src/components/ui/markdown/heading-ids.ts +239 -0
- package/src/components/ui/markdown/index.ts +49 -0
- package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
- package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
- package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
- package/src/components/ui/markdown/sanitize.ts +3499 -0
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
- package/src/components/ui/markdown/streaming.ts +362 -0
- package/src/components/ui/markdown/text-size.ts +106 -0
- package/src/components/ui/release-changelog-section.tsx +1 -1
- package/src/components/ui/ticket-info-section.tsx +1 -1
- package/src/contexts/chat-runtime-context.tsx +8 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown-fences.ts +127 -0
- package/src/utils/markdown-heading-id.ts +348 -0
- package/src/utils/markdown-section-extractor.ts +41 -56
- package/dist/chunk-2B6773QM.cjs.map +0 -1
- package/dist/chunk-5RXKOGH6.cjs.map +0 -1
- package/dist/chunk-77DFPD35.js.map +0 -1
- package/dist/chunk-AHDJMEEN.js.map +0 -1
- package/dist/chunk-FZOBQG6M.cjs.map +0 -1
- package/dist/chunk-H7JX2BG7.cjs.map +0 -1
- package/dist/chunk-KFQXO7QB.js.map +0 -1
- package/dist/chunk-S52LJNMU.js.map +0 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
- package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
- package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -479
- package/src/components/chat/utils/.chunk-parser.md +0 -62
- package/src/components/chat/utils/chunk-parser.ts +0 -256
- package/src/components/ui/.simple-markdown-renderer.md +0 -52
- package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
- package/src/components/ui/simple-markdown-renderer.tsx +0 -964
- /package/dist/{chunk-AJNDDXKC.js.map → chunk-4NWARV5I.js.map} +0 -0
- /package/dist/{chunk-HQYJINFV.js.map → chunk-CORIBKHG.js.map} +0 -0
- /package/dist/{chunk-PNXNWGBY.js.map → chunk-GTWO5NAY.js.map} +0 -0
- /package/dist/{chunk-SCCTJ2UI.js.map → chunk-HSACATCD.js.map} +0 -0
- /package/dist/{chunk-KLYHZRAD.js.map → chunk-PB3BTZDU.js.map} +0 -0
- /package/dist/{chunk-YJSKQ2ZV.js.map → chunk-RQLBHKXN.js.map} +0 -0
- /package/dist/{chunk-CWQACI4Z.js.map → chunk-RW2LPL72.js.map} +0 -0
- /package/dist/{chunk-3BFX6HJM.js.map → chunk-UUYRJPGJ.js.map} +0 -0
- /package/dist/{chunk-F6BYUO5U.js.map → chunk-VHG6AX5V.js.map} +0 -0
- /package/dist/{chunk-PXF5J24T.js.map → chunk-XQ2B74SI.js.map} +0 -0
- /package/dist/{chunk-LLWBPPZ4.js.map → chunk-YZ2OIYQ3.js.map} +0 -0
|
@@ -6,59 +6,84 @@
|
|
|
6
6
|
* counterpart is `useNatsChatAdapter` (Mingo mode). The public
|
|
7
7
|
* `useChat({ mode })` dispatches between them based on `mode.transport`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Phase 3 of the chat unification: the adapter no longer owns a parser or a
|
|
10
|
+
* message-merge layer. Raw response bytes flow
|
|
11
|
+
*
|
|
12
|
+
* fetch body → createSseFrameDecoder → ChatStreamEvent →
|
|
13
|
+
* createChatStreamReducer (transport: 'sse') — THE master reader
|
|
14
|
+
*
|
|
15
|
+
* The reducer absorbed `useChat`'s trailing-assistant merge, the
|
|
16
|
+
* `decision_resolved` receipt path, and the sendIdx-keyed
|
|
17
|
+
* sources/refs/meta maps; this file keeps only the transport wiring
|
|
18
|
+
* (fetch/abort, request-body building), the server-issued conversation-id
|
|
19
|
+
* persistence (id ONLY — see below), the mount-time server-history
|
|
20
|
+
* hydration seam, and the public `UnifiedChatState` mapping.
|
|
21
|
+
*
|
|
22
|
+
* Conversation identity + history (server SSOT):
|
|
23
|
+
*
|
|
24
|
+
* - Conversation ids are SERVER-minted. The first send of a session goes
|
|
25
|
+
* out id-less; the server mints an id and echoes it in the leading
|
|
26
|
+
* metadata frame (surfaced by the decoder as
|
|
27
|
+
* `ChatMetadataEvent.conversationId`), and every later turn echoes it
|
|
28
|
+
* back in the request body.
|
|
29
|
+
* - localStorage stores ONLY that echoed id (`chat-conversation-storage`).
|
|
30
|
+
* Message history lives server-side in `chat_conversations` /
|
|
31
|
+
* `chat_messages` and is rebuilt on mount via `useChatHistoryHydration`,
|
|
32
|
+
* which materializes through the reducer (`initializeWithState` /
|
|
33
|
+
* `prependMessages` + `seedSseMaps`).
|
|
34
|
+
* - The wire carries ONLY the newest user message — never a replay of the
|
|
35
|
+
* client-side thread (the server re-reads `chat_messages` by
|
|
36
|
+
* conversation id on every turn).
|
|
13
37
|
*
|
|
14
38
|
* Two key contracts vs hub-side chat hooks:
|
|
15
39
|
*
|
|
16
40
|
* 1. `source` is READ FROM THE RUNTIME, not a parameter. It is OPTIONAL —
|
|
17
41
|
* platform-agnostic embedders leave it unset; an empty `source` falls back
|
|
18
|
-
* to a stable constant (`DEFAULT_CHAT_SOURCE`) for the localStorage
|
|
19
|
-
* namespace. It is NEVER sent on the wire (the hub
|
|
20
|
-
* server-side via `currentPlatform()`).
|
|
21
|
-
*
|
|
22
|
-
* 2. Navigation is runtime-provided. The hub-side `useDocSearch` keeps
|
|
23
|
-
* its own `decideNewTab` + `useDocNavigation.navigate` plumbing for
|
|
24
|
-
* the search autocomplete (it calls `router.push`). The chat body
|
|
25
|
-
* doesn't navigate via this hook — click handlers on rendered
|
|
26
|
-
* cards/chips go through `handleChatNavClick` instead.
|
|
42
|
+
* to a stable constant (`DEFAULT_CHAT_SOURCE`) for the localStorage
|
|
43
|
+
* conversation-id namespace. It is NEVER sent on the wire (the hub
|
|
44
|
+
* resolves source server-side via `currentPlatform()`).
|
|
27
45
|
*
|
|
28
|
-
*
|
|
46
|
+
* 2. `tableIdForDocumentType` is INJECTED via the optional
|
|
29
47
|
* `tableIdForDocumentType` parameter. Hub callers pass the
|
|
30
48
|
* registry-backed lookup from `lib/config/rag-table-config`;
|
|
31
49
|
* embedders that don't supply one fall back to
|
|
32
|
-
* `defaultTableIdForDocumentType`
|
|
33
|
-
*
|
|
34
|
-
* `displayRef` / `discussRef` Ask + Display buttons WORK out of
|
|
35
|
-
* the box. Override only when you have polymorphic / per-tenant
|
|
36
|
-
* types the default doesn't cover.
|
|
37
|
-
*
|
|
38
|
-
* Wire format and SSE-parser logic mirror the hub original byte-for-byte
|
|
39
|
-
* so server-side and client-side stay in lockstep across the migration.
|
|
50
|
+
* `defaultTableIdForDocumentType` so the `displayRef` / `discussRef`
|
|
51
|
+
* Ask + Display buttons WORK out of the box.
|
|
40
52
|
*/
|
|
41
53
|
|
|
42
|
-
import {
|
|
43
|
-
import { useChat, type Message, type StreamFnExtraOptions } from './use-chat'
|
|
54
|
+
import { useCallback, useMemo, useRef } from 'react'
|
|
44
55
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
45
56
|
import type { ChatRef } from '../chat-ref.types'
|
|
46
|
-
import {
|
|
57
|
+
import type { Message } from '../types/message.types'
|
|
47
58
|
import type { MessageSegment } from '../types/message.types'
|
|
48
59
|
import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'
|
|
49
|
-
import { getChatProxyAuth } from '../utils/chat-proxy-auth-storage'
|
|
50
60
|
import { chatAuthedFetch } from '../utils/chat-authed-fetch'
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
61
|
+
import type { ScrollAnchor } from '../utils/scroll-anchor'
|
|
62
|
+
import { createSseFrameDecoder } from '../../../chat-protocol/decode'
|
|
63
|
+
import type { ChatStreamEvent } from '../../../chat-protocol/events'
|
|
64
|
+
import {
|
|
65
|
+
createChatConversationStorage,
|
|
66
|
+
pruneStaleChatConversationStorage,
|
|
67
|
+
} from '../utils/chat-conversation-storage'
|
|
68
|
+
import { useChatHistoryHydration } from './use-chat-history-hydration'
|
|
54
69
|
import { sanitizeTitleForChat } from '../utils/slash-dispatch-utils'
|
|
55
70
|
import { defaultTableIdForDocumentType } from '../utils/source-icons'
|
|
71
|
+
import type { WireCommandOverride } from '../utils/slash-dispatch-utils'
|
|
72
|
+
import type { ChatAttachment } from '../utils/chat-attachment-markdown'
|
|
56
73
|
import type {
|
|
57
74
|
UnifiedChatState,
|
|
75
|
+
UnifiedChatMessage,
|
|
58
76
|
UnifiedSendMessageOptions,
|
|
59
|
-
StreamingPhase,
|
|
60
77
|
} from '../types/unified-chat-state.types'
|
|
61
78
|
import type { DialogItem } from '../types/component.types'
|
|
79
|
+
import { createChatDialogStore, DEFAULT_DIALOG_SIDE } from '../stream/chat-dialog-store'
|
|
80
|
+
import { useChatStreamReducer } from '../stream/use-chat-stream-reducer'
|
|
81
|
+
import type { ChatDialogStore } from '../stream/chat-dialog-store'
|
|
82
|
+
import type { ChatStreamReducerOptions } from '../stream/chat-stream-reducer'
|
|
83
|
+
|
|
84
|
+
// Canonical home of the per-turn meta row moved to the stream module in
|
|
85
|
+
// Phase 3; re-exported here to keep the legacy import path stable.
|
|
86
|
+
export type { ChatTurnMeta } from '../stream/chat-stream-reducer'
|
|
62
87
|
|
|
63
88
|
// =============================================================================
|
|
64
89
|
// Public types
|
|
@@ -121,33 +146,6 @@ export interface DocChatMessage {
|
|
|
121
146
|
// `types/unified-chat-state.types.ts`.
|
|
122
147
|
export type { StreamingPhase } from '../types/unified-chat-state.types'
|
|
123
148
|
|
|
124
|
-
/** Per-turn metadata extracted from the streamed metadata frame. */
|
|
125
|
-
export interface ChatTurnMeta {
|
|
126
|
-
/** Provider key recognized by `<ModelDisplay>` for icon
|
|
127
|
-
* selection: 'anthropic' | 'openai' | 'google' (case-insensitive). */
|
|
128
|
-
provider: string | null
|
|
129
|
-
modelLabel: string | null
|
|
130
|
-
contextWindowMaxTokens: number | null
|
|
131
|
-
/** Input tokens (from message_start). Includes cached tokens. */
|
|
132
|
-
inputTokens: number | null
|
|
133
|
-
/** Output tokens (from message_delta). Only known after stream end. */
|
|
134
|
-
outputTokens: number | null
|
|
135
|
-
/** Cache hit % (read / total-input × 100). Only known after stream end. */
|
|
136
|
-
cacheHitRatePct: number | null
|
|
137
|
-
/** Cross-call usage breakdown extracted from the trailing usage frame. */
|
|
138
|
-
breakdown: {
|
|
139
|
-
haikuRewriter?: { input: number; output: number }
|
|
140
|
-
haikuClassifier?: { input: number; output: number }
|
|
141
|
-
haikuSummarizer?: { input: number; output: number }
|
|
142
|
-
routedAnswer?: { model: string; complexity: string; thinkingBudget: number }
|
|
143
|
-
} | null
|
|
144
|
-
/** Per-message viewport-positioning hint. */
|
|
145
|
-
scrollAnchor: ScrollAnchor | null
|
|
146
|
-
/** Routing decision from the server's `decideRoute`. */
|
|
147
|
-
routedComplexity: string | null
|
|
148
|
-
routedThinkingBudget: number | null
|
|
149
|
-
}
|
|
150
|
-
|
|
151
149
|
/**
|
|
152
150
|
* Optional dependency-injection options for `useSseChatAdapter`.
|
|
153
151
|
*
|
|
@@ -158,13 +156,7 @@ export interface ChatTurnMeta {
|
|
|
158
156
|
* **Defaults to `defaultTableIdForDocumentType` from
|
|
159
157
|
* `src/utils/source-icons.ts`** — a lib-baked map covering every
|
|
160
158
|
* documentType currently registered in the hub's RAG_TABLE_CONFIGS.
|
|
161
|
-
*
|
|
162
|
-
* any callback wiring.
|
|
163
|
-
*
|
|
164
|
-
* Override only when you have polymorphic / per-tenant document
|
|
165
|
-
* types that map to a different tableId than the default
|
|
166
|
-
* (hub-canonical) registry — pass your own `(docType) => tableId`
|
|
167
|
-
* callback and it wins over the default.
|
|
159
|
+
* Override only for polymorphic / per-tenant document types.
|
|
168
160
|
*/
|
|
169
161
|
export interface UseSseChatAdapterOptions {
|
|
170
162
|
tableIdForDocumentType?: (documentType: string) => string | null
|
|
@@ -173,507 +165,55 @@ export interface UseSseChatAdapterOptions {
|
|
|
173
165
|
export interface UseSseChatAdapterRuntimeOptions {
|
|
174
166
|
/**
|
|
175
167
|
* When `false` the adapter skips its background slash-command registry
|
|
176
|
-
* fetch (used to hydrate the `displayRef` table lookup)
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
168
|
+
* fetch (used to hydrate the `displayRef` table lookup) AND the
|
|
169
|
+
* mount-time server-history hydration. Mirrors the NATS adapter's
|
|
170
|
+
* `active` gate: `useUnifiedChat` passes `false` while Guide mode is
|
|
171
|
+
* configured-but-not-active, so opening the panel in Mingo mode does
|
|
172
|
+
* NOT hit the commands or history endpoints. Default `true` so
|
|
180
173
|
* standalone callers keep the eager prefetch.
|
|
181
174
|
*/
|
|
182
175
|
active?: boolean
|
|
183
176
|
}
|
|
184
177
|
|
|
185
178
|
// =============================================================================
|
|
186
|
-
//
|
|
179
|
+
// Local persistence — the server-issued conversation id ONLY
|
|
187
180
|
// =============================================================================
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
routedComplexity: null,
|
|
201
|
-
routedThinkingBudget: null,
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Escape `<` so the lib's `SimpleMarkdownRenderer` (which uses `rehypeRaw`
|
|
207
|
-
* to pass HTML through) doesn't treat XML-like tokens in Claude's
|
|
208
|
-
* thinking output as React components. Escaping `<` → `<` preserves
|
|
209
|
-
* the visible character without breaking blockquote `>` markers.
|
|
210
|
-
*/
|
|
211
|
-
function escapeThinkingTags(text: string): string {
|
|
212
|
-
return text.replace(/</g, '<')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function createDocStreamFn(
|
|
216
|
-
source: DocSource,
|
|
217
|
-
endpoints: { chatStreamUrl: string; approvalToolUrl: string },
|
|
218
|
-
messagesRef: MutableRefObject<Message[]>,
|
|
219
|
-
sourcesMapRef: MutableRefObject<Map<number, ChatSource[]>>,
|
|
220
|
-
refsMapRef: MutableRefObject<Map<number, Record<string, ChatRef>>>,
|
|
221
|
-
metaMapRef: MutableRefObject<Map<number, ChatTurnMeta>>,
|
|
222
|
-
setStreamingPhase: (phase: StreamingPhase) => void,
|
|
223
|
-
bumpMetaTick: () => void,
|
|
224
|
-
sendCountRef: MutableRefObject<number>,
|
|
225
|
-
) {
|
|
226
|
-
// CRITICAL: the decoder + buffer MUST live INSIDE the returned async-
|
|
227
|
-
// generator function (per-call closure), NOT at the factory level. A
|
|
228
|
-
// rapid send-stop-send sequence with hook-level state would feed the
|
|
229
|
-
// second stream's first chunk into the first stream's tail buffer,
|
|
230
|
-
// corrupting metadata-frame parsing.
|
|
231
|
-
return async function* (
|
|
232
|
-
message: string,
|
|
233
|
-
signal?: AbortSignal,
|
|
234
|
-
extra?: StreamFnExtraOptions,
|
|
235
|
-
): AsyncGenerator<MessageSegment> {
|
|
236
|
-
const currentMessages = messagesRef.current || []
|
|
237
|
-
// Filter `hidden:true` messages out of the API history. The approval-
|
|
238
|
-
// action turn injects a hidden user message with `content=''`.
|
|
239
|
-
// `flattenAssistantContent` joins text-segment arrays into a single
|
|
240
|
-
// string so the server sees the receipt + auto-continuation Qs.
|
|
241
|
-
const apiMessages = [
|
|
242
|
-
...currentMessages
|
|
243
|
-
.filter((m) => (m.role === 'user' || m.role === 'assistant') && !m.hidden)
|
|
244
|
-
.map((m) => ({
|
|
245
|
-
role: m.role,
|
|
246
|
-
content:
|
|
247
|
-
typeof m.content === 'string' ? m.content : flattenAssistantContent(m.content),
|
|
248
|
-
})),
|
|
249
|
-
{ role: 'user', content: message },
|
|
250
|
-
]
|
|
251
|
-
|
|
252
|
-
// URL + body branch — approvalAction routes to the approval-tool
|
|
253
|
-
// endpoint, the standard chat path routes to the chat-stream endpoint.
|
|
254
|
-
const targetPath = extra?.approvalAction
|
|
255
|
-
? endpoints.approvalToolUrl
|
|
256
|
-
: endpoints.chatStreamUrl
|
|
257
|
-
// `source` is INTENTIONALLY NOT in the body. The chat route resolves
|
|
258
|
-
// it server-side via its own platform-detection — tamper-proof binding
|
|
259
|
-
// so a client on one platform can't POST a different platform's
|
|
260
|
-
// conversation.
|
|
261
|
-
const requestBody = extra?.approvalAction
|
|
262
|
-
? {
|
|
263
|
-
proposal_id: extra.approvalAction.proposalId,
|
|
264
|
-
action: extra.approvalAction.action,
|
|
265
|
-
messages: currentMessages
|
|
266
|
-
.filter((m) => (m.role === 'user' || m.role === 'assistant') && !m.hidden)
|
|
267
|
-
.map((m) => ({
|
|
268
|
-
role: m.role,
|
|
269
|
-
content:
|
|
270
|
-
typeof m.content === 'string'
|
|
271
|
-
? m.content
|
|
272
|
-
: flattenAssistantContent(m.content),
|
|
273
|
-
})),
|
|
274
|
-
}
|
|
275
|
-
: {
|
|
276
|
-
messages: apiMessages,
|
|
277
|
-
...(extra?.commandOverride ? { commandOverride: extra.commandOverride } : {}),
|
|
278
|
-
...(extra?.pendingAttachments && extra.pendingAttachments.length > 0
|
|
279
|
-
? { pendingAttachments: extra.pendingAttachments }
|
|
280
|
-
: {}),
|
|
281
|
-
}
|
|
282
|
-
// `chatAuthedFetch` carries the bearer-act-as headers (+ Supabase
|
|
283
|
-
// session cookies) — same wrapper `use-chat-attachments` and
|
|
284
|
-
// `use-chat-identity` use, so all three chat-side fetch sites share
|
|
285
|
-
// one identity-propagation primitive.
|
|
286
|
-
const response = await chatAuthedFetch(targetPath, {
|
|
287
|
-
method: 'POST',
|
|
288
|
-
body: JSON.stringify(requestBody),
|
|
289
|
-
signal,
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
if (!response.ok) {
|
|
293
|
-
throw new Error(`Chat request failed: ${response.status}`)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const reader = response.body?.getReader()
|
|
297
|
-
if (!reader) throw new Error('No response body')
|
|
298
|
-
|
|
299
|
-
const decoder = new TextDecoder()
|
|
300
|
-
let buffer = ''
|
|
301
|
-
let inText = false
|
|
302
|
-
// Live thinking accumulator — coalesced to ~20fps to avoid a re-render
|
|
303
|
-
// storm (Anthropic emits 30-60 deltas/sec).
|
|
304
|
-
let thinkingAccum = ''
|
|
305
|
-
let lastThinkingYieldTime = 0
|
|
306
|
-
let pendingThinkingYield = false
|
|
307
|
-
const THINKING_YIELD_INTERVAL_MS = 50
|
|
308
|
-
let trailerBuffer = ''
|
|
309
|
-
let inTrailer = false
|
|
310
|
-
|
|
311
|
-
const sendIdx = sendCountRef.current - 1
|
|
312
|
-
while (true) {
|
|
313
|
-
const { done, value } = await reader.read()
|
|
314
|
-
if (done) break
|
|
315
|
-
|
|
316
|
-
const chunk = decoder.decode(value, { stream: true })
|
|
317
|
-
|
|
318
|
-
if (!inText) {
|
|
319
|
-
buffer += chunk
|
|
320
|
-
while (!inText) {
|
|
321
|
-
const recIdx = buffer.indexOf('\x1E')
|
|
322
|
-
const nullIdx = buffer.indexOf('\0')
|
|
323
|
-
if (recIdx !== -1 && (nullIdx === -1 || recIdx < nullIdx)) {
|
|
324
|
-
if (pendingThinkingYield) {
|
|
325
|
-
pendingThinkingYield = false
|
|
326
|
-
yield { type: 'thinking', text: escapeThinkingTags(thinkingAccum) }
|
|
327
|
-
}
|
|
328
|
-
inText = true
|
|
329
|
-
setStreamingPhase('streaming')
|
|
330
|
-
const after = buffer.slice(recIdx + 1)
|
|
331
|
-
buffer = ''
|
|
332
|
-
if (after) {
|
|
333
|
-
// The `after` slice may ALSO contain the `\x1F` trailing-
|
|
334
|
-
// usage sentinel — common for fixed-answer responses where
|
|
335
|
-
// the whole frame sequence arrives in ONE TCP chunk.
|
|
336
|
-
const unitIdx = after.indexOf('\x1F')
|
|
337
|
-
if (unitIdx === -1) {
|
|
338
|
-
yield { type: 'text', text: after }
|
|
339
|
-
} else {
|
|
340
|
-
const textBefore = after.slice(0, unitIdx)
|
|
341
|
-
const trailerAfter = after.slice(unitIdx + 1)
|
|
342
|
-
if (textBefore) {
|
|
343
|
-
yield { type: 'text', text: textBefore }
|
|
344
|
-
}
|
|
345
|
-
inTrailer = true
|
|
346
|
-
trailerBuffer = trailerAfter
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
break
|
|
350
|
-
}
|
|
351
|
-
if (nullIdx === -1) break // need more bytes
|
|
352
|
-
const metaStr = buffer.slice(0, nullIdx)
|
|
353
|
-
const remaining = buffer.slice(nullIdx + 1)
|
|
354
|
-
let meta: any
|
|
355
|
-
try {
|
|
356
|
-
meta = JSON.parse(metaStr)
|
|
357
|
-
} catch {
|
|
358
|
-
// Not JSON — start of answer body.
|
|
359
|
-
inText = true
|
|
360
|
-
setStreamingPhase('streaming')
|
|
361
|
-
if (buffer.length > 0) {
|
|
362
|
-
yield { type: 'text', text: buffer }
|
|
363
|
-
buffer = ''
|
|
364
|
-
}
|
|
365
|
-
break
|
|
366
|
-
}
|
|
367
|
-
if (meta.status === 'thinking') {
|
|
368
|
-
setStreamingPhase('thinking')
|
|
369
|
-
} else if (meta.kind === 'thinking-delta' && typeof meta.text === 'string') {
|
|
370
|
-
thinkingAccum += meta.text
|
|
371
|
-
const now = Date.now()
|
|
372
|
-
if (now - lastThinkingYieldTime >= THINKING_YIELD_INTERVAL_MS) {
|
|
373
|
-
lastThinkingYieldTime = now
|
|
374
|
-
pendingThinkingYield = false
|
|
375
|
-
yield { type: 'thinking', text: escapeThinkingTags(thinkingAccum) }
|
|
376
|
-
} else {
|
|
377
|
-
pendingThinkingYield = true
|
|
378
|
-
}
|
|
379
|
-
} else if (meta.kind === 'usage' && meta.stage === 'start') {
|
|
380
|
-
mergeTurnMeta(metaMapRef, sendIdx, {
|
|
381
|
-
inputTokens: meta.input_tokens ?? null,
|
|
382
|
-
})
|
|
383
|
-
bumpMetaTick()
|
|
384
|
-
} else if (
|
|
385
|
-
meta.kind === 'decision_resolved' &&
|
|
386
|
-
typeof meta.action === 'string'
|
|
387
|
-
) {
|
|
388
|
-
// Server-driven post-approve / post-reject frame.
|
|
389
|
-
const action = meta.action === 'rejected' ? 'rejected' : 'approved'
|
|
390
|
-
const toolName =
|
|
391
|
-
typeof meta.tool_name === 'string' ? meta.tool_name : undefined
|
|
392
|
-
const result = (meta.result ?? null) as
|
|
393
|
-
| { ticket_id?: string; status?: string | null; mirror_synced?: boolean }
|
|
394
|
-
| null
|
|
395
|
-
const card = (meta.card ?? null) as
|
|
396
|
-
| { type?: string; marker?: string; ref?: ChatRef }
|
|
397
|
-
| null
|
|
398
|
-
if (card?.ref?.id && card?.type) {
|
|
399
|
-
const existing = refsMapRef.current.get(sendIdx) ?? {}
|
|
400
|
-
const key = buildChatRefKey(card.type, card.ref.id)
|
|
401
|
-
refsMapRef.current.set(sendIdx, { ...existing, [key]: card.ref })
|
|
402
|
-
bumpMetaTick()
|
|
403
|
-
}
|
|
404
|
-
yield {
|
|
405
|
-
type: 'decision_resolved',
|
|
406
|
-
action,
|
|
407
|
-
ok: meta.ok === true,
|
|
408
|
-
willAutoContinue: meta.willAutoContinue === true,
|
|
409
|
-
...(toolName ? { toolName } : {}),
|
|
410
|
-
...(result ? { result } : {}),
|
|
411
|
-
...(card?.marker ? { marker: card.marker } : {}),
|
|
412
|
-
...(card?.ref ? { cardRef: card.ref } : {}),
|
|
413
|
-
...(typeof meta.receiptText === 'string'
|
|
414
|
-
? { receiptText: meta.receiptText }
|
|
415
|
-
: {}),
|
|
416
|
-
proposalId:
|
|
417
|
-
typeof meta.proposalId === 'string' ? meta.proposalId : undefined,
|
|
418
|
-
} as any
|
|
419
|
-
} else if (meta.kind === 'approval_request' && meta.proposalId) {
|
|
420
|
-
// The model called a write tool. Server persisted the
|
|
421
|
-
// proposal and sent us a CARD-READY payload.
|
|
422
|
-
const proposalId = String(meta.proposalId)
|
|
423
|
-
const toolName = String(meta.toolName ?? 'tool')
|
|
424
|
-
const headline =
|
|
425
|
-
typeof meta.title === 'string' && meta.title.length > 0
|
|
426
|
-
? meta.title
|
|
427
|
-
: toolName
|
|
428
|
-
const rawFields = Array.isArray(meta.fields)
|
|
429
|
-
? (meta.fields as Array<{ label?: string; value?: string }>)
|
|
430
|
-
: []
|
|
431
|
-
const fields: Array<{ label: string; value: string }> = []
|
|
432
|
-
for (const f of rawFields) {
|
|
433
|
-
if (!f || !f.label || !f.value) continue
|
|
434
|
-
fields.push({ label: f.label, value: f.value })
|
|
435
|
-
}
|
|
436
|
-
yield {
|
|
437
|
-
type: 'approval_request',
|
|
438
|
-
data: {
|
|
439
|
-
command: headline,
|
|
440
|
-
fields,
|
|
441
|
-
requestId: proposalId,
|
|
442
|
-
approvalType: toolName,
|
|
443
|
-
},
|
|
444
|
-
status: 'pending',
|
|
445
|
-
} as any
|
|
446
|
-
} else if (meta.kind === 'text-leading' && typeof meta.text === 'string') {
|
|
447
|
-
// Model's preamble text (the prose written BEFORE the tool_use
|
|
448
|
-
// block) — emitted as a standalone text segment.
|
|
449
|
-
yield { type: 'text', text: meta.text }
|
|
450
|
-
} else if (meta.kind === 'tool_error') {
|
|
451
|
-
const msg =
|
|
452
|
-
typeof meta.message === 'string' && meta.message.length > 0
|
|
453
|
-
? meta.message
|
|
454
|
-
: 'Could not complete the requested action right now.'
|
|
455
|
-
yield {
|
|
456
|
-
type: 'text',
|
|
457
|
-
text: `⚠️ ${msg}`,
|
|
458
|
-
} as any
|
|
459
|
-
} else if (meta.kind === 'routing') {
|
|
460
|
-
if (typeof meta.routedComplexity === 'string') {
|
|
461
|
-
mergeTurnMeta(metaMapRef, sendIdx, {
|
|
462
|
-
routedComplexity: meta.routedComplexity,
|
|
463
|
-
routedThinkingBudget:
|
|
464
|
-
typeof meta.routedThinkingBudget === 'number'
|
|
465
|
-
? meta.routedThinkingBudget
|
|
466
|
-
: null,
|
|
467
|
-
})
|
|
468
|
-
bumpMetaTick()
|
|
469
|
-
}
|
|
470
|
-
} else {
|
|
471
|
-
if (meta.sources) {
|
|
472
|
-
sourcesMapRef.current.set(sendIdx, meta.sources)
|
|
473
|
-
}
|
|
474
|
-
// Per-row refs for inline object cards.
|
|
475
|
-
if (meta.refs && typeof meta.refs === 'object') {
|
|
476
|
-
refsMapRef.current.set(sendIdx, meta.refs as Record<string, ChatRef>)
|
|
477
|
-
bumpMetaTick()
|
|
478
|
-
}
|
|
479
|
-
if (
|
|
480
|
-
meta.modelLabel ||
|
|
481
|
-
meta.contextWindowMaxTokens ||
|
|
482
|
-
meta.provider ||
|
|
483
|
-
meta.model
|
|
484
|
-
) {
|
|
485
|
-
mergeTurnMeta(metaMapRef, sendIdx, {
|
|
486
|
-
provider: meta.provider ?? null,
|
|
487
|
-
modelLabel: meta.modelLabel ?? null,
|
|
488
|
-
contextWindowMaxTokens: meta.contextWindowMaxTokens ?? null,
|
|
489
|
-
})
|
|
490
|
-
bumpMetaTick()
|
|
491
|
-
}
|
|
492
|
-
const parsedAnchor = parseScrollAnchor(meta.scrollAnchor)
|
|
493
|
-
if (parsedAnchor !== null) {
|
|
494
|
-
mergeTurnMeta(metaMapRef, sendIdx, { scrollAnchor: parsedAnchor })
|
|
495
|
-
bumpMetaTick()
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
buffer = remaining
|
|
499
|
-
}
|
|
500
|
-
} else if (inTrailer) {
|
|
501
|
-
trailerBuffer += chunk
|
|
502
|
-
} else {
|
|
503
|
-
setStreamingPhase('streaming')
|
|
504
|
-
// Trailing usage frame uses \x1F (Unit Separator) as a sentinel.
|
|
505
|
-
const sepIdx = chunk.indexOf('\x1F')
|
|
506
|
-
if (sepIdx === -1) {
|
|
507
|
-
yield { type: 'text', text: chunk }
|
|
508
|
-
} else {
|
|
509
|
-
const before = chunk.slice(0, sepIdx)
|
|
510
|
-
const after = chunk.slice(sepIdx + 1)
|
|
511
|
-
if (before) yield { type: 'text', text: before }
|
|
512
|
-
inTrailer = true
|
|
513
|
-
trailerBuffer = after
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
// Stream ended. Flush any pending thinking text that didn't make it
|
|
518
|
-
// out before the throttle window expired.
|
|
519
|
-
if (pendingThinkingYield) {
|
|
520
|
-
pendingThinkingYield = false
|
|
521
|
-
yield { type: 'thinking', text: escapeThinkingTags(thinkingAccum) }
|
|
522
|
-
}
|
|
523
|
-
// Parse trailer if present.
|
|
524
|
-
if (trailerBuffer.length > 0) {
|
|
525
|
-
try {
|
|
526
|
-
const meta = JSON.parse(trailerBuffer)
|
|
527
|
-
if (
|
|
528
|
-
meta.kind === 'usage' &&
|
|
529
|
-
(meta.stage === 'end' || meta.stage === 'display')
|
|
530
|
-
) {
|
|
531
|
-
const breakdown =
|
|
532
|
-
meta.breakdown && typeof meta.breakdown === 'object'
|
|
533
|
-
? {
|
|
534
|
-
haikuRewriter:
|
|
535
|
-
meta.breakdown.haikuRewriter &&
|
|
536
|
-
typeof meta.breakdown.haikuRewriter.input === 'number'
|
|
537
|
-
? meta.breakdown.haikuRewriter
|
|
538
|
-
: undefined,
|
|
539
|
-
haikuClassifier:
|
|
540
|
-
meta.breakdown.haikuClassifier &&
|
|
541
|
-
typeof meta.breakdown.haikuClassifier.input === 'number'
|
|
542
|
-
? meta.breakdown.haikuClassifier
|
|
543
|
-
: undefined,
|
|
544
|
-
haikuSummarizer:
|
|
545
|
-
meta.breakdown.haikuSummarizer &&
|
|
546
|
-
typeof meta.breakdown.haikuSummarizer.input === 'number'
|
|
547
|
-
? meta.breakdown.haikuSummarizer
|
|
548
|
-
: undefined,
|
|
549
|
-
routedAnswer:
|
|
550
|
-
meta.breakdown.routedAnswer &&
|
|
551
|
-
typeof meta.breakdown.routedAnswer.model === 'string'
|
|
552
|
-
? meta.breakdown.routedAnswer
|
|
553
|
-
: undefined,
|
|
554
|
-
}
|
|
555
|
-
: null
|
|
556
|
-
mergeTurnMeta(metaMapRef, sendIdx, {
|
|
557
|
-
inputTokens: meta.input_tokens ?? null,
|
|
558
|
-
outputTokens: meta.output_tokens ?? null,
|
|
559
|
-
cacheHitRatePct:
|
|
560
|
-
typeof meta.hit_rate_pct === 'number' ? meta.hit_rate_pct : null,
|
|
561
|
-
...(breakdown ? { breakdown } : {}),
|
|
562
|
-
})
|
|
563
|
-
bumpMetaTick()
|
|
564
|
-
}
|
|
565
|
-
} catch {
|
|
566
|
-
// Malformed trailer — silently ignore.
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Merge partial fields into the per-turn meta map. Preserves existing
|
|
574
|
-
* non-null values so a leading frame's data isn't wiped by a later frame
|
|
575
|
-
* that didn't include it.
|
|
576
|
-
*/
|
|
577
|
-
function mergeTurnMeta(
|
|
578
|
-
ref: MutableRefObject<Map<number, ChatTurnMeta>>,
|
|
579
|
-
sendIdx: number,
|
|
580
|
-
partial: Partial<ChatTurnMeta>,
|
|
581
|
-
): void {
|
|
582
|
-
const prev = ref.current.get(sendIdx) ?? createEmptyTurnMeta()
|
|
583
|
-
const filtered = Object.fromEntries(
|
|
584
|
-
Object.entries(partial).filter(([, v]) => v !== undefined && v !== null),
|
|
585
|
-
) as Partial<ChatTurnMeta>
|
|
586
|
-
ref.current.set(sendIdx, { ...prev, ...filtered })
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// =============================================================================
|
|
590
|
-
// localStorage persistence
|
|
591
|
-
// =============================================================================
|
|
592
|
-
|
|
593
|
-
const CHAT_STORAGE_VERSION = 1
|
|
594
|
-
|
|
595
|
-
/** localStorage history namespace used when no `source` is configured on the
|
|
181
|
+
//
|
|
182
|
+
// The ONLY thing persisted locally is the conversation id the SERVER minted
|
|
183
|
+
// and echoed in the leading metadata frame of the session's first turn.
|
|
184
|
+
// The client never generates ids. Message history lives server-side in
|
|
185
|
+
// `chat_conversations` / `chat_messages` (recorded turn-by-turn by the chat
|
|
186
|
+
// route) and is rehydrated on mount via `GET <chatStreamUrl>/history`.
|
|
187
|
+
// localStorage never stores messages, sources, refs, or send counts —
|
|
188
|
+
// the server transcript is the single source of truth for history.
|
|
189
|
+
// (The retired PersistedChatState v1 full-history blobs are swept on mount
|
|
190
|
+
// by `pruneStaleChatConversationStorage` — never read, never migrated.)
|
|
191
|
+
|
|
192
|
+
/** localStorage namespace used when no `source` is configured on the
|
|
596
193
|
* runtime. Embedders are platform-agnostic (see `ChatRuntime.source`), so any
|
|
597
194
|
* stable string works here — the hub passes its real platform instead. */
|
|
598
195
|
const DEFAULT_CHAT_SOURCE = 'embed'
|
|
599
196
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const base = `mingo-chat-${source}-v${CHAT_STORAGE_VERSION}`
|
|
604
|
-
const auth = getChatProxyAuth()
|
|
605
|
-
if (auth?.email) {
|
|
606
|
-
return `${base}-u-${encodeURIComponent(auth.email.toLowerCase())}`
|
|
607
|
-
}
|
|
608
|
-
return base
|
|
609
|
-
}
|
|
197
|
+
// Persistence itself lives in `../utils/chat-conversation-storage.ts` (built
|
|
198
|
+
// on the lib-standard `createLocalStorageAdapter`); the mount-time history
|
|
199
|
+
// rebuild lives in `./use-chat-history-hydration.ts`.
|
|
610
200
|
|
|
611
|
-
/**
|
|
612
|
-
*
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
for (const k of toRemove) {
|
|
629
|
-
window.localStorage.removeItem(k)
|
|
630
|
-
}
|
|
631
|
-
} catch {
|
|
632
|
-
// localStorage access blocked (Safari private mode etc.) — non-fatal.
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
interface PersistedChatState {
|
|
637
|
-
messages: Message[]
|
|
638
|
-
sources: Array<[number, ChatSource[]]>
|
|
639
|
-
/** Per-turn refs for inline object cards. */
|
|
640
|
-
refs?: Array<[number, Record<string, ChatRef>]>
|
|
641
|
-
sendCount: number
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function loadPersistedChat(source: DocSource): PersistedChatState | null {
|
|
645
|
-
if (typeof window === 'undefined') return null
|
|
646
|
-
try {
|
|
647
|
-
const raw = window.localStorage.getItem(chatStorageKey(source))
|
|
648
|
-
if (!raw) return null
|
|
649
|
-
const parsed = JSON.parse(raw) as PersistedChatState
|
|
650
|
-
if (!parsed || !Array.isArray(parsed.messages)) return null
|
|
651
|
-
// Rehydrate Date objects + run forward-compat migrations.
|
|
652
|
-
for (const m of parsed.messages) {
|
|
653
|
-
if (typeof m.timestamp === 'string') m.timestamp = new Date(m.timestamp)
|
|
654
|
-
// Forward-migration for auto-continuation directive bubbles.
|
|
655
|
-
if (
|
|
656
|
-
m.role === 'user' &&
|
|
657
|
-
!m.hidden &&
|
|
658
|
-
typeof m.content === 'string' &&
|
|
659
|
-
m.content.startsWith(AUTO_CONTINUATION_DIRECTIVE_PREFIX)
|
|
660
|
-
) {
|
|
661
|
-
m.hidden = true
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return parsed
|
|
665
|
-
} catch {
|
|
666
|
-
return null
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function savePersistedChat(source: DocSource, state: PersistedChatState) {
|
|
671
|
-
if (typeof window === 'undefined') return
|
|
672
|
-
try {
|
|
673
|
-
window.localStorage.setItem(chatStorageKey(source), JSON.stringify(state))
|
|
674
|
-
} catch {
|
|
675
|
-
// Quota exceeded or private mode — silently drop.
|
|
676
|
-
}
|
|
201
|
+
/** Server-hydrated wire `Message` → reducer message. Hydrated rows carry
|
|
202
|
+
* plain-string content; the defensive segments branch keeps the mapper
|
|
203
|
+
* total over the wider `Message` union the hydration seam is typed with. */
|
|
204
|
+
function historyMessageToReducerMessage(m: Message): UnifiedChatMessage {
|
|
205
|
+
const segments = Array.isArray(m.content) ? (m.content as MessageSegment[]) : undefined
|
|
206
|
+
return {
|
|
207
|
+
id: m.id,
|
|
208
|
+
role: m.role,
|
|
209
|
+
content: typeof m.content === 'string' ? m.content : '',
|
|
210
|
+
...(segments ? { segments } : {}),
|
|
211
|
+
...(m.name !== undefined ? { name: m.name } : {}),
|
|
212
|
+
...(m.avatar != null ? { avatar: m.avatar } : {}),
|
|
213
|
+
...(m.timestamp !== undefined ? { timestamp: m.timestamp } : {}),
|
|
214
|
+
...(m.hidden ? { hidden: true } : {}),
|
|
215
|
+
...(m.chatRefs ? { chatRefs: m.chatRefs } : {}),
|
|
216
|
+
} as UnifiedChatMessage
|
|
677
217
|
}
|
|
678
218
|
|
|
679
219
|
// =============================================================================
|
|
@@ -686,8 +226,8 @@ function savePersistedChat(source: DocSource, state: PersistedChatState) {
|
|
|
686
226
|
* decision) the lib's `<ChatContainer>` consumes.
|
|
687
227
|
*
|
|
688
228
|
* Source identity comes from `useRequiredChatRuntime().source` — no
|
|
689
|
-
* parameter. An empty `source` falls back to `DEFAULT_CHAT_SOURCE` (used only
|
|
690
|
-
* the localStorage
|
|
229
|
+
* parameter. An empty `source` falls back to `DEFAULT_CHAT_SOURCE` (used only
|
|
230
|
+
* for the conversation-id localStorage namespace; never sent on the wire).
|
|
691
231
|
*/
|
|
692
232
|
export function useSseChatAdapter(
|
|
693
233
|
options?: UseSseChatAdapterOptions,
|
|
@@ -697,70 +237,70 @@ export function useSseChatAdapter(
|
|
|
697
237
|
// Chat-specific code REQUIRES a runtime — the lib's `<HubRuntimeProvider>`
|
|
698
238
|
// (hub) / embedder's provider must wrap the tree.
|
|
699
239
|
const runtime = useRequiredChatRuntime()
|
|
700
|
-
// `source` is OPTIONAL — embedders are platform-agnostic (see ChatRuntime.source).
|
|
701
|
-
// Here it's used ONLY for the localStorage history namespace + client-side meta
|
|
702
|
-
// labels; it is NEVER sent on the wire (the hub resolves source server-side via
|
|
703
|
-
// currentPlatform()). Fall back to a stable constant so the persistence key stays
|
|
704
|
-
// well-formed when the embedder leaves source unset.
|
|
705
240
|
const source = runtime.source || DEFAULT_CHAT_SOURCE
|
|
706
|
-
// Fall back to the lib-baked hub-canonical map when the embedder
|
|
707
|
-
// didn't supply an override. Keeps Ask + Display working in any
|
|
708
|
-
// mount that doesn't have a custom documentType registry. Embedders
|
|
709
|
-
// with polymorphic / per-tenant types pass their own callback and it
|
|
710
|
-
// wins.
|
|
711
241
|
const tableIdForDocumentType =
|
|
712
242
|
options?.tableIdForDocumentType ?? defaultTableIdForDocumentType
|
|
713
243
|
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
)
|
|
725
|
-
|
|
726
|
-
|
|
244
|
+
// ─── Reducer wiring ────────────────────────────────────────────────────────
|
|
245
|
+
// One store per hook instance; one reducer keyed by the source. Approval
|
|
246
|
+
// card buttons fire a server-driven confirm-tool turn via `sendMessage`
|
|
247
|
+
// (hidden approval-action send) — routed through a ref so the reducer's
|
|
248
|
+
// creation-time callbacks stay stable.
|
|
249
|
+
|
|
250
|
+
const sendMessageRef = useRef<
|
|
251
|
+
(text: string, options?: InternalSendMessageOptions) => Promise<void>
|
|
252
|
+
>(async () => {})
|
|
253
|
+
|
|
254
|
+
const cardApprove = useCallback((reqId?: string): void | Promise<void> => {
|
|
255
|
+
if (!reqId) return
|
|
256
|
+
return sendMessageRef.current('', {
|
|
257
|
+
hidden: true,
|
|
258
|
+
approvalAction: { proposalId: reqId, action: 'approve' },
|
|
259
|
+
})
|
|
260
|
+
}, [])
|
|
261
|
+
const cardReject = useCallback((reqId?: string): void | Promise<void> => {
|
|
262
|
+
if (!reqId) return
|
|
263
|
+
return sendMessageRef.current('', {
|
|
264
|
+
hidden: true,
|
|
265
|
+
approvalAction: { proposalId: reqId, action: 'reject' },
|
|
266
|
+
})
|
|
267
|
+
}, [])
|
|
268
|
+
|
|
269
|
+
const createReducerOptions = useCallback(
|
|
270
|
+
(): ChatStreamReducerOptions => ({
|
|
271
|
+
transport: 'sse',
|
|
272
|
+
callbacks: { onApprove: cardApprove, onReject: cardReject },
|
|
273
|
+
}),
|
|
274
|
+
[cardApprove, cardReject],
|
|
727
275
|
)
|
|
728
|
-
const metaMapRef = useRef<Map<number, ChatTurnMeta>>(new Map())
|
|
729
|
-
const messagesRef = useRef<Message[]>(persistedRef.current.messages)
|
|
730
|
-
const sendCountRef = useRef(persistedRef.current.sendCount)
|
|
731
|
-
const [streamingPhase, setStreamingPhase] = useState<StreamingPhase>('idle')
|
|
732
|
-
const [metaTick, setMetaTick] = useState(0)
|
|
733
|
-
const bumpMetaTick = useCallback(() => setMetaTick((t) => t + 1), [])
|
|
734
276
|
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
messagesRef,
|
|
744
|
-
sourcesMapRef,
|
|
745
|
-
refsMapRef,
|
|
746
|
-
metaMapRef,
|
|
747
|
-
setStreamingPhase,
|
|
748
|
-
bumpMetaTick,
|
|
749
|
-
sendCountRef,
|
|
750
|
-
),
|
|
751
|
-
[
|
|
752
|
-
source,
|
|
753
|
-
runtime.endpoints.chatStreamUrl,
|
|
754
|
-
runtime.endpoints.approvalToolUrl,
|
|
755
|
-
bumpMetaTick,
|
|
756
|
-
],
|
|
757
|
-
)
|
|
277
|
+
const storeRef = useRef<ChatDialogStore | null>(null)
|
|
278
|
+
if (storeRef.current === null) storeRef.current = createChatDialogStore()
|
|
279
|
+
|
|
280
|
+
const { state, applyEvent, flushDeltas, mutate, reducer } = useChatStreamReducer({
|
|
281
|
+
store: storeRef.current,
|
|
282
|
+
dialogId: source,
|
|
283
|
+
createReducerOptions,
|
|
284
|
+
})
|
|
758
285
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
286
|
+
const abortControllerRef = useRef<AbortController | null>(null)
|
|
287
|
+
|
|
288
|
+
// ─── Conversation identity — server-minted id, restored once on mount ─────
|
|
289
|
+
// Null = no conversation yet: the FIRST send goes out without an id, the
|
|
290
|
+
// server mints one, and the metadata-frame capture (in the send loop below)
|
|
291
|
+
// stores it. The client NEVER generates ids. This is the ONLY local
|
|
292
|
+
// persistence — message history hydrates from the server transcript below.
|
|
293
|
+
const conversationStorage = useMemo(() => createChatConversationStorage(source), [source])
|
|
294
|
+
const conversationIdRef = useRef<string | null>(null)
|
|
295
|
+
const restoredConversationRef = useRef(false)
|
|
296
|
+
if (!restoredConversationRef.current) {
|
|
297
|
+
restoredConversationRef.current = true
|
|
298
|
+
// Sweeps other-identity keys AND the retired v1 full-history blobs.
|
|
299
|
+
pruneStaleChatConversationStorage(source)
|
|
300
|
+
conversationIdRef.current = conversationStorage.load()?.conversationId ?? null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─── Slash-command registry (displayRef lookup) ───────────────────────────
|
|
764
304
|
// Reads from the SAME react-query cache entry as `<EmbeddableChat>`'s
|
|
765
305
|
// onboarding-card list (keyed on `commandsUrl`), so Guide mode fetches
|
|
766
306
|
// `commands` ONCE. Gated on `active` so a Mingo-only panel — where this
|
|
@@ -792,138 +332,271 @@ export function useSseChatAdapter(
|
|
|
792
332
|
return map
|
|
793
333
|
}, [slashCommands])
|
|
794
334
|
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
335
|
+
// ─── Server hydration (history SSOT) ──────────────────────────────────────
|
|
336
|
+
// Mount-time rebuild of the message list from the server transcript — the
|
|
337
|
+
// single store of conversation history. The fetch/guard/decode lives in
|
|
338
|
+
// `use-chat-history-hydration.ts` (see there for the full contract +
|
|
339
|
+
// failure semantics: a miss starts the UI empty, never loses server
|
|
340
|
+
// context). THIS adapter owns the materialization seam: the hook hands
|
|
341
|
+
// back plain `Message[]` rows plus per-send refs + the user-turn count
|
|
342
|
+
// (via the two shim refs below), and `hydrateMessages` feeds ALL of it
|
|
343
|
+
// through the REDUCER — `initializeWithState` on an empty thread,
|
|
344
|
+
// `prependMessages` when the user already sent before hydration landed
|
|
345
|
+
// (nothing typed is lost; the server resolves LLM history from its own
|
|
346
|
+
// store either way), and `seedSseMaps` for the sendIdx-keyed refs + the
|
|
347
|
+
// send counter. No parallel merge path.
|
|
348
|
+
const historyUrl =
|
|
349
|
+
runtime.endpoints.chatHistoryUrl ??
|
|
350
|
+
`${runtime.endpoints.chatStreamUrl.replace(/\/+$/, '')}/history`
|
|
351
|
+
const hydrationRefsMapRef = useRef<Map<number, Record<string, ChatRef>>>(new Map())
|
|
352
|
+
const hydrationSendCountRef = useRef(0)
|
|
353
|
+
const hydrateMessages = useCallback(
|
|
354
|
+
(history: Message[]) => {
|
|
355
|
+
mutate((r) => {
|
|
356
|
+
const mapped = history.map(historyMessageToReducerMessage)
|
|
357
|
+
const liveSendCount = r.state.turnMeta.sendCount
|
|
358
|
+
if (r.state.messages.length === 0) {
|
|
359
|
+
r.initializeWithState(mapped)
|
|
360
|
+
} else {
|
|
361
|
+
r.prependMessages(mapped)
|
|
362
|
+
}
|
|
363
|
+
r.seedSseMaps({
|
|
364
|
+
refs: Array.from(hydrationRefsMapRef.current.entries()),
|
|
365
|
+
sendCount: hydrationSendCountRef.current + liveSendCount,
|
|
366
|
+
})
|
|
804
367
|
})
|
|
805
368
|
},
|
|
806
|
-
[
|
|
369
|
+
[mutate],
|
|
807
370
|
)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
assistantName: 'Mingo AI',
|
|
820
|
-
streamFn,
|
|
821
|
-
initialMessages: persistedRef.current.messages,
|
|
822
|
-
onMessagesChange: persist,
|
|
371
|
+
const { isHydratingHistory, hydratedKeyRef } = useChatHistoryHydration({
|
|
372
|
+
active,
|
|
373
|
+
source,
|
|
374
|
+
historyUrl,
|
|
375
|
+
conversationIdRef,
|
|
376
|
+
refsMapRef: hydrationRefsMapRef,
|
|
377
|
+
sendCountRef: hydrationSendCountRef,
|
|
378
|
+
hydrateMessages,
|
|
379
|
+
// Meta invalidation is a reducer concern here: `seedSseMaps` (inside
|
|
380
|
+
// `hydrateMessages` above) already invalidates the snapshot.
|
|
381
|
+
bumpMetaTick: noopBumpMetaTick,
|
|
823
382
|
})
|
|
824
383
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
// Index sources/refs/scrollAnchor by USER-SEND count (`sendIdx`), not
|
|
828
|
-
// by assistant-message count. Each user send produces exactly ONE
|
|
829
|
-
// refsMapRef entry server-side, but it can produce MULTIPLE assistant
|
|
830
|
-
// messages on the client (main RAG reply + post-approve card +
|
|
831
|
-
// auto-continuation prose). Counting USER sends and mapping every
|
|
832
|
-
// following assistant message to that index keeps the lookup stable.
|
|
833
|
-
let sendIdx = -1
|
|
834
|
-
const docMessages: DocChatMessage[] = messages.map((m) => {
|
|
835
|
-
const segments = Array.isArray(m.content) ? (m.content as MessageSegment[]) : undefined
|
|
836
|
-
const content =
|
|
837
|
-
typeof m.content === 'string'
|
|
838
|
-
? m.content
|
|
839
|
-
: Array.isArray(m.content)
|
|
840
|
-
? m.content
|
|
841
|
-
.filter((s) => s.type === 'text')
|
|
842
|
-
.map((s) => (s as { type: 'text'; text: string }).text)
|
|
843
|
-
.join('')
|
|
844
|
-
: ''
|
|
845
|
-
|
|
846
|
-
let sources: ChatSource[] | undefined
|
|
847
|
-
let chatRefs: Record<string, ChatRef> | undefined
|
|
848
|
-
let scrollAnchor: ScrollAnchor | undefined
|
|
849
|
-
if (m.role === 'user' && !m.hidden) {
|
|
850
|
-
sendIdx++
|
|
851
|
-
}
|
|
852
|
-
if (m.role === 'assistant') {
|
|
853
|
-
const lookupIdx = sendIdx >= 0 ? sendIdx : 0
|
|
854
|
-
sources = sourcesMapRef.current.get(lookupIdx)
|
|
855
|
-
// The receipt path stamps `chatRefs` directly onto the assistant
|
|
856
|
-
// message in useChat. Prefer that message-bound copy when present;
|
|
857
|
-
// falls back to per-turn refs map.
|
|
858
|
-
chatRefs = m.chatRefs ?? refsMapRef.current.get(lookupIdx)
|
|
859
|
-
scrollAnchor =
|
|
860
|
-
(metaMapRef.current.get(lookupIdx)?.scrollAnchor as ScrollAnchor | null) ??
|
|
861
|
-
undefined
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
return {
|
|
865
|
-
id: m.id,
|
|
866
|
-
role: m.role as 'user' | 'assistant',
|
|
867
|
-
content,
|
|
868
|
-
...(segments ? { segments } : {}),
|
|
869
|
-
...(sources ? { sources } : {}),
|
|
870
|
-
...(chatRefs ? { chatRefs } : {}),
|
|
871
|
-
...(scrollAnchor ? { scrollAnchor } : {}),
|
|
872
|
-
...(m.hidden ? { hidden: true } : {}),
|
|
873
|
-
}
|
|
874
|
-
})
|
|
384
|
+
// ─── Send / stream loop ────────────────────────────────────────────────────
|
|
875
385
|
|
|
876
386
|
/**
|
|
877
387
|
* Internal sendMessage options — union of the public
|
|
878
388
|
* `UnifiedSendMessageOptions` (semantic fields: `hidden`, `attachments`)
|
|
879
389
|
* and SSE-only internal extras (`commandOverride`, `approvalAction`)
|
|
880
|
-
* set by `discussRef` / `displayRef` /
|
|
881
|
-
*
|
|
882
|
-
* The public `UnifiedChatState.sendMessage` accepts only the narrow
|
|
883
|
-
* unified shape; this hook accepts the wider one because internal
|
|
884
|
-
* callers need it. Function-param contravariance makes this assignable.
|
|
390
|
+
* set by `discussRef` / `displayRef` / the approval-card callbacks.
|
|
885
391
|
*/
|
|
886
|
-
type InternalSendMessageOptions = UnifiedSendMessageOptions &
|
|
887
|
-
|
|
392
|
+
type InternalSendMessageOptions = UnifiedSendMessageOptions & {
|
|
393
|
+
commandOverride?: WireCommandOverride
|
|
394
|
+
approvalAction?: { proposalId: string; action: 'approve' | 'reject' }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const endpointsRef = useRef(runtime.endpoints)
|
|
398
|
+
endpointsRef.current = runtime.endpoints
|
|
399
|
+
|
|
400
|
+
/** Capture the SERVER-minted conversation id from a decoded metadata
|
|
401
|
+
* event. On the session's FIRST turn this is where the client learns
|
|
402
|
+
* its id — persist it so later turns and future visits continue the
|
|
403
|
+
* same conversation. The reducer ignores the field (identity is a
|
|
404
|
+
* transport/persistence concern, not render state). */
|
|
405
|
+
const captureConversationId = useCallback(
|
|
406
|
+
(event: ChatStreamEvent): void => {
|
|
407
|
+
if (event.type !== 'metadata') return
|
|
408
|
+
const id = event.conversationId
|
|
409
|
+
if (typeof id !== 'string' || !id) return
|
|
410
|
+
if (conversationIdRef.current === id) return
|
|
411
|
+
conversationIdRef.current = id
|
|
412
|
+
conversationStorage.save({ conversationId: id })
|
|
413
|
+
},
|
|
414
|
+
[conversationStorage],
|
|
415
|
+
)
|
|
888
416
|
|
|
889
417
|
const sendMessage = useCallback(
|
|
890
|
-
async (
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
418
|
+
async (text: string, sendOptions?: InternalSendMessageOptions): Promise<void> => {
|
|
419
|
+
const { hidden, attachments, commandOverride, approvalAction } = sendOptions ?? {}
|
|
420
|
+
|
|
421
|
+
// URL + body branch — approvalAction routes to the approval-tool
|
|
422
|
+
// endpoint, the standard chat path routes to the chat-stream endpoint.
|
|
423
|
+
// `source` is INTENTIONALLY NOT in the body: the chat route resolves
|
|
424
|
+
// it server-side via its own platform-detection — tamper-proof binding.
|
|
425
|
+
//
|
|
426
|
+
// The server is the single source of conversation history: it re-reads
|
|
427
|
+
// `chat_messages` by conversation id on every turn. The wire therefore
|
|
428
|
+
// carries ONLY the new user message — never the prior conversation.
|
|
429
|
+
const conversationId = conversationIdRef.current
|
|
430
|
+
const targetPath = approvalAction
|
|
431
|
+
? endpointsRef.current.approvalToolUrl
|
|
432
|
+
: endpointsRef.current.chatStreamUrl
|
|
433
|
+
const requestBody = approvalAction
|
|
434
|
+
? {
|
|
435
|
+
proposal_id: approvalAction.proposalId,
|
|
436
|
+
action: approvalAction.action,
|
|
437
|
+
// Always present here — an approval can only happen inside an
|
|
438
|
+
// established conversation (the proposal turn captured the id).
|
|
439
|
+
conversationId,
|
|
440
|
+
}
|
|
441
|
+
: {
|
|
442
|
+
messages: [{ role: 'user', content: text }],
|
|
443
|
+
...(commandOverride ? { commandOverride } : {}),
|
|
444
|
+
...(attachments && attachments.length > 0
|
|
445
|
+
? { pendingAttachments: attachments as ChatAttachment[] }
|
|
446
|
+
: {}),
|
|
447
|
+
...(conversationId ? { conversationId } : {}),
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Optimistic user bubble + assistant placeholder + phase 'thinking'
|
|
451
|
+
// + sendCount++ — one reducer command.
|
|
452
|
+
mutate((r) =>
|
|
453
|
+
r.beginSseSend({ text, hidden, userName: 'You', assistantName: 'Mingo AI' }),
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
const ctrl = new AbortController()
|
|
457
|
+
abortControllerRef.current = ctrl
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
// `chatAuthedFetch` carries the bearer-act-as headers (+ Supabase
|
|
461
|
+
// session cookies) — same wrapper `use-chat-attachments` and
|
|
462
|
+
// `use-chat-identity` use.
|
|
463
|
+
const response = await chatAuthedFetch(targetPath, {
|
|
464
|
+
method: 'POST',
|
|
465
|
+
body: JSON.stringify(requestBody),
|
|
466
|
+
signal: ctrl.signal,
|
|
467
|
+
})
|
|
468
|
+
if (!response.ok) {
|
|
469
|
+
throw new Error(`Chat request failed: ${response.status}`)
|
|
470
|
+
}
|
|
471
|
+
const reader = response.body?.getReader()
|
|
472
|
+
if (!reader) throw new Error('No response body')
|
|
473
|
+
|
|
474
|
+
// Decoder is per-send (a rapid send-stop-send sequence must never
|
|
475
|
+
// feed the second stream's first chunk into the first stream's
|
|
476
|
+
// tail buffer).
|
|
477
|
+
const frameDecoder = createSseFrameDecoder()
|
|
478
|
+
let finished = false
|
|
479
|
+
try {
|
|
480
|
+
while (true) {
|
|
481
|
+
const { done, value } = await reader.read()
|
|
482
|
+
if (done) break
|
|
483
|
+
if (ctrl.signal.aborted) break
|
|
484
|
+
for (const event of frameDecoder.push(value)) {
|
|
485
|
+
captureConversationId(event)
|
|
486
|
+
applyEvent(event)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
finished = true
|
|
490
|
+
} finally {
|
|
491
|
+
// `end()` always runs so the decoder settles; its events (the
|
|
492
|
+
// trailing usage frame) apply only on a CLEAN end — aborts
|
|
493
|
+
// propagate without extra state (legacy parity).
|
|
494
|
+
const endEvents = frameDecoder.end()
|
|
495
|
+
if (finished) {
|
|
496
|
+
for (const event of endEvents) applyEvent(event)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} catch (err) {
|
|
500
|
+
// AbortError on user-initiated stop is expected — keep the partial
|
|
501
|
+
// message, no error row.
|
|
502
|
+
if ((err as { name?: string })?.name !== 'AbortError' && !ctrl.signal.aborted) {
|
|
503
|
+
flushDeltas()
|
|
504
|
+
mutate((r) =>
|
|
505
|
+
r.failSseTurn(
|
|
506
|
+
err instanceof Error
|
|
507
|
+
? err.message
|
|
508
|
+
: 'An error occurred while processing your request.',
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
} finally {
|
|
513
|
+
if (abortControllerRef.current === ctrl) {
|
|
514
|
+
abortControllerRef.current = null
|
|
515
|
+
}
|
|
516
|
+
// Force-flush pending deltas BEFORE the completion state lands, then
|
|
517
|
+
// settle the turn (drops an empty trailing placeholder — the reject
|
|
518
|
+
// path streams no text — and returns the phase to idle).
|
|
519
|
+
flushDeltas()
|
|
520
|
+
mutate((r) => r.endSseTurn())
|
|
906
521
|
}
|
|
907
|
-
sendCountRef.current++
|
|
908
|
-
setStreamingPhase('thinking')
|
|
909
|
-
await chatSendMessage(text, sseExtras, hidden ? { hidden } : undefined)
|
|
910
522
|
},
|
|
911
|
-
[
|
|
523
|
+
[mutate, applyEvent, flushDeltas, captureConversationId],
|
|
912
524
|
)
|
|
525
|
+
sendMessageRef.current = sendMessage
|
|
526
|
+
|
|
527
|
+
const stopMessage = useCallback(() => {
|
|
528
|
+
abortControllerRef.current?.abort()
|
|
529
|
+
flushDeltas()
|
|
530
|
+
mutate((r) => r.setPhase('idle'))
|
|
531
|
+
}, [mutate, flushDeltas])
|
|
532
|
+
|
|
533
|
+
const clearMessages = useCallback(() => {
|
|
534
|
+
mutate((r) => r.reset())
|
|
535
|
+
// New chat = drop the stored conversation id. The old row stays frozen
|
|
536
|
+
// server-side; the NEXT send goes out id-less and the server mints a
|
|
537
|
+
// fresh conversation (echoed back and re-captured then). Resetting the
|
|
538
|
+
// hydration guard means a re-captured id can hydrate again if needed.
|
|
539
|
+
conversationIdRef.current = null
|
|
540
|
+
conversationStorage.clear()
|
|
541
|
+
hydrationRefsMapRef.current = new Map()
|
|
542
|
+
hydrationSendCountRef.current = 0
|
|
543
|
+
hydratedKeyRef.current = null
|
|
544
|
+
}, [mutate, conversationStorage, hydratedKeyRef])
|
|
545
|
+
|
|
546
|
+
// ─── Public message mapping (sendIdx fan-out lookup) ──────────────────────
|
|
547
|
+
// Index sources/refs/scrollAnchor by USER-SEND count (`sendIdx`), not by
|
|
548
|
+
// assistant-message count. Each user send produces exactly ONE refs entry
|
|
549
|
+
// server-side, but it can produce MULTIPLE assistant messages on the
|
|
550
|
+
// client (main RAG reply + post-approve card + auto-continuation prose).
|
|
551
|
+
// Counting VISIBLE user sends and mapping every following assistant
|
|
552
|
+
// message to that index keeps the lookup stable — server hydration seeds
|
|
553
|
+
// the same maps (via `seedSseMaps`) so hydrated turns resolve identically.
|
|
554
|
+
const docMessages: DocChatMessage[] = useMemo(() => {
|
|
555
|
+
const { meta, sources: sourcesMap, refs: refsMap } = state.turnMeta
|
|
556
|
+
let sendIdx = -1
|
|
557
|
+
return state.messages.map((m) => {
|
|
558
|
+
const segments = m.segments
|
|
559
|
+
const content =
|
|
560
|
+
typeof m.content === 'string' && !segments
|
|
561
|
+
? m.content
|
|
562
|
+
: segments
|
|
563
|
+
?.filter((s) => s.type === 'text')
|
|
564
|
+
.map((s) => (s as { type: 'text'; text: string }).text)
|
|
565
|
+
.join('') ?? ''
|
|
566
|
+
|
|
567
|
+
let sources: ChatSource[] | undefined
|
|
568
|
+
let chatRefs: Record<string, ChatRef> | undefined
|
|
569
|
+
let scrollAnchor: ScrollAnchor | undefined
|
|
570
|
+
if (m.role === 'user' && !m.hidden) {
|
|
571
|
+
sendIdx++
|
|
572
|
+
}
|
|
573
|
+
if (m.role === 'assistant') {
|
|
574
|
+
const lookupIdx = sendIdx >= 0 ? sendIdx : 0
|
|
575
|
+
sources = sourcesMap.get(lookupIdx) as ChatSource[] | undefined
|
|
576
|
+
// The receipt path stamps `chatRefs` directly onto the assistant
|
|
577
|
+
// message; prefer that message-bound copy when present, fall back
|
|
578
|
+
// to the per-turn refs map.
|
|
579
|
+
chatRefs = m.chatRefs ?? refsMap.get(lookupIdx)
|
|
580
|
+
scrollAnchor = (meta.get(lookupIdx)?.scrollAnchor as ScrollAnchor | null) ?? undefined
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
id: m.id,
|
|
585
|
+
role: m.role as 'user' | 'assistant',
|
|
586
|
+
content,
|
|
587
|
+
...(segments ? { segments } : {}),
|
|
588
|
+
...(sources ? { sources } : {}),
|
|
589
|
+
...(chatRefs ? { chatRefs } : {}),
|
|
590
|
+
...(scrollAnchor ? { scrollAnchor } : {}),
|
|
591
|
+
...(m.hidden ? { hidden: true } : {}),
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
}, [state.messages, state.turnMeta])
|
|
913
595
|
|
|
914
596
|
/**
|
|
915
597
|
* "Display" callback for inline cards whose registry entry sets
|
|
916
598
|
* `displayAction: true`. Parallel to `discussRef` but emits a
|
|
917
|
-
* `/<cmd> display "<title>"` slash command instead of the Discuss
|
|
918
|
-
* prose. Resolution of the slash command id for the ref's documentType
|
|
919
|
-
* happens via the `cmdIdByTableId` map hydrated from the commands
|
|
920
|
-
* endpoint.
|
|
921
|
-
*
|
|
922
|
-
* `tableIdForDocumentType` is always defined here — either the
|
|
923
|
-
* embedder-supplied override OR the lib-baked
|
|
924
|
-
* `defaultTableIdForDocumentType` fallback. Unknown documentTypes
|
|
925
|
-
* still short-circuit gracefully via the `tableId === null` check
|
|
926
|
-
* below.
|
|
599
|
+
* `/<cmd> display "<title>"` slash command instead of the Discuss prose.
|
|
927
600
|
*/
|
|
928
601
|
const displayRef = useCallback(
|
|
929
602
|
(reference: ChatRef) => {
|
|
@@ -965,10 +638,6 @@ export function useSseChatAdapter(
|
|
|
965
638
|
* prompt ("Tell me more about <title>"); the structured
|
|
966
639
|
* `commandOverride.entityIdFilter` is sent server-side via the request
|
|
967
640
|
* body so retrieval narrows to the named row.
|
|
968
|
-
*
|
|
969
|
-
* `tableIdForDocumentType` is always defined here — either the
|
|
970
|
-
* embedder-supplied override OR the lib-baked default. Unknown
|
|
971
|
-
* documentTypes still short-circuit via the `tableId === null` check.
|
|
972
641
|
*/
|
|
973
642
|
const discussRef = useCallback(
|
|
974
643
|
(reference: ChatRef) => {
|
|
@@ -998,47 +667,17 @@ export function useSseChatAdapter(
|
|
|
998
667
|
[sendMessage, tableIdForDocumentType],
|
|
999
668
|
)
|
|
1000
669
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
const clearMessages = useCallback(() => {
|
|
1007
|
-
sourcesMapRef.current.clear()
|
|
1008
|
-
refsMapRef.current.clear()
|
|
1009
|
-
metaMapRef.current.clear()
|
|
1010
|
-
sendCountRef.current = 0
|
|
1011
|
-
setStreamingPhase('idle')
|
|
1012
|
-
// Force the latestMeta useMemo to recompute with the cleared map.
|
|
1013
|
-
bumpMetaTick()
|
|
1014
|
-
chatClearMessages()
|
|
1015
|
-
// Clear persisted state too so the next mount starts fresh.
|
|
1016
|
-
if (typeof window !== 'undefined') {
|
|
1017
|
-
try {
|
|
1018
|
-
window.localStorage.removeItem(chatStorageKey(source))
|
|
1019
|
-
} catch {}
|
|
1020
|
-
}
|
|
1021
|
-
}, [chatClearMessages, source, bumpMetaTick])
|
|
1022
|
-
|
|
1023
|
-
// Reset to idle whenever both flags drop off.
|
|
1024
|
-
useEffect(() => {
|
|
1025
|
-
if (!isTyping && !isStreaming && streamingPhase !== 'idle') {
|
|
1026
|
-
setStreamingPhase('idle')
|
|
1027
|
-
}
|
|
1028
|
-
}, [isTyping, isStreaming, streamingPhase])
|
|
670
|
+
// ─── Per-turn metadata resolution ─────────────────────────────────────────
|
|
671
|
+
const latestMeta = useMemo(() => {
|
|
672
|
+
const { meta, sendCount } = state.turnMeta
|
|
673
|
+
return meta.get(sendCount - 1) ?? meta.get(sendCount - 2) ?? null
|
|
674
|
+
}, [state])
|
|
1029
675
|
|
|
1030
|
-
|
|
1031
|
-
const latestMeta = useMemo(
|
|
1032
|
-
() =>
|
|
1033
|
-
metaMapRef.current.get(sendCountRef.current - 1) ??
|
|
1034
|
-
metaMapRef.current.get(sendCountRef.current - 2) ??
|
|
1035
|
-
null,
|
|
1036
|
-
[metaTick, streamingPhase],
|
|
1037
|
-
)
|
|
676
|
+
const streamingPhase = state.streamingPhase
|
|
1038
677
|
|
|
1039
678
|
return {
|
|
1040
679
|
messages: docMessages,
|
|
1041
|
-
isLoading:
|
|
680
|
+
isLoading: streamingPhase !== 'idle',
|
|
1042
681
|
sendMessage,
|
|
1043
682
|
/** "Discuss" affordance for ObjectCard. */
|
|
1044
683
|
discussRef,
|
|
@@ -1047,6 +686,8 @@ export function useSseChatAdapter(
|
|
|
1047
686
|
stopMessage,
|
|
1048
687
|
clearMessages,
|
|
1049
688
|
streamingPhase,
|
|
689
|
+
/** True while the mount-time rebuild from the server transcript runs. */
|
|
690
|
+
isHydratingHistory,
|
|
1050
691
|
/** Provider key for the lib's `<ModelDisplay>` icon. */
|
|
1051
692
|
currentProvider: latestMeta?.provider ?? null,
|
|
1052
693
|
currentModelLabel: latestMeta?.modelLabel ?? null,
|
|
@@ -1061,12 +702,11 @@ export function useSseChatAdapter(
|
|
|
1061
702
|
* token counts). null until the trailing usage frame lands. */
|
|
1062
703
|
currentUsageBreakdown: latestMeta?.breakdown ?? null,
|
|
1063
704
|
// ─── Dialog management — stubs for v1 ────────────────────────────────
|
|
1064
|
-
// Guide mode
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
// `dialogs.length === 0`.
|
|
705
|
+
// Guide mode keeps ONE server-side conversation per stored id
|
|
706
|
+
// (`chat_conversations`, hydrated on mount). Surfacing multiple threads
|
|
707
|
+
// as a structured dialog list is a follow-up; for now the shape is
|
|
708
|
+
// satisfied with empty defaults so the unified contract type-checks and
|
|
709
|
+
// EmbeddableChat hides sidebar affordances when `dialogs.length === 0`.
|
|
1070
710
|
dialogs: SSE_EMPTY_DIALOGS,
|
|
1071
711
|
activeDialogId: null,
|
|
1072
712
|
selectDialog: noopSelectDialog,
|
|
@@ -1100,13 +740,13 @@ const noopSelectDialog = (_id: string | null): void => {
|
|
|
1100
740
|
}
|
|
1101
741
|
const noopStartNewDialog = async (): Promise<string | null> => null
|
|
1102
742
|
const noopDeleteDialog = async (_id: string): Promise<void> => {
|
|
1103
|
-
/* no-op until Guide
|
|
743
|
+
/* no-op until Guide server-side history is exposed as dialogs */
|
|
1104
744
|
}
|
|
1105
745
|
const noopRenameDialog = async (_id: string, _title: string): Promise<void> => {
|
|
1106
|
-
/* no-op until Guide
|
|
746
|
+
/* no-op until Guide server-side history is exposed as dialogs */
|
|
1107
747
|
}
|
|
1108
748
|
const noopArchiveDialog = async (_id: string): Promise<void> => {
|
|
1109
|
-
/* no-op until Guide
|
|
749
|
+
/* no-op until Guide server-side history is exposed as dialogs */
|
|
1110
750
|
}
|
|
1111
751
|
const noopAsync = async (): Promise<void> => {
|
|
1112
752
|
/* no-op pagination stub */
|
|
@@ -1117,3 +757,6 @@ const noopApproveRequest = async (_id: string): Promise<void> => {
|
|
|
1117
757
|
const noopRejectRequest = async (_id: string, _reason?: string): Promise<void> => {
|
|
1118
758
|
/* Guide mode has no tool-call approval workflow */
|
|
1119
759
|
}
|
|
760
|
+
/** Stable no-op for the hydration hook's `bumpMetaTick` seam — reducer
|
|
761
|
+
* invalidation (inside `hydrateMessages`) already re-renders subscribers. */
|
|
762
|
+
const noopBumpMetaTick = (): void => {}
|