@flamingo-stack/openframe-frontend-core 0.0.490-1506.3944.1 → 0.0.490
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/{chunk-5CJR2GZ4.cjs → chunk-2MKUHJ3O.cjs} +31 -31
- package/dist/{chunk-5CJR2GZ4.cjs.map → chunk-2MKUHJ3O.cjs.map} +1 -1
- package/dist/{chunk-S2NHJDKQ.cjs → chunk-3IEVA5AO.cjs} +62 -62
- package/dist/{chunk-S2NHJDKQ.cjs.map → chunk-3IEVA5AO.cjs.map} +1 -1
- package/dist/{chunk-GTWO5NAY.js → chunk-6CFFISUC.js} +2 -2
- package/dist/{chunk-VHG6AX5V.js → chunk-6CJ2K7BD.js} +2 -2
- package/dist/{chunk-4NWARV5I.js → chunk-6QK3ONDD.js} +2 -2
- package/dist/{chunk-YHOIAA2E.cjs → chunk-7I42V2OD.cjs} +60 -60
- package/dist/chunk-7I42V2OD.cjs.map +1 -0
- package/dist/{chunk-T2EGSZFN.js → chunk-AIRNG27G.js} +5 -6
- package/dist/chunk-AIRNG27G.js.map +1 -0
- package/dist/{chunk-NWX7MMF5.cjs → chunk-AW63ET2A.cjs} +15 -15
- package/dist/{chunk-NWX7MMF5.cjs.map → chunk-AW63ET2A.cjs.map} +1 -1
- package/dist/{chunk-TKN5667N.js → chunk-BZMOEWWS.js} +10 -10
- package/dist/chunk-BZMOEWWS.js.map +1 -0
- package/dist/{chunk-DSDZ5T4P.cjs → chunk-D5LQMRPU.cjs} +5 -5
- package/dist/{chunk-DSDZ5T4P.cjs.map → chunk-D5LQMRPU.cjs.map} +1 -1
- package/dist/{chunk-CORIBKHG.js → chunk-EGXCWZYP.js} +2 -2
- package/dist/{chunk-5EYRXSY5.cjs → chunk-EJQUYZXF.cjs} +9 -9
- package/dist/{chunk-5EYRXSY5.cjs.map → chunk-EJQUYZXF.cjs.map} +1 -1
- package/dist/{chunk-P4WTZU6L.cjs → chunk-FY5XAW3H.cjs} +11 -11
- package/dist/{chunk-P4WTZU6L.cjs.map → chunk-FY5XAW3H.cjs.map} +1 -1
- package/dist/{chunk-VQ523ORK.cjs → chunk-GMPYR3MX.cjs} +26 -26
- package/dist/{chunk-VQ523ORK.cjs.map → chunk-GMPYR3MX.cjs.map} +1 -1
- package/dist/{chunk-QSCHCZL3.cjs → chunk-GUTYUMGV.cjs} +7 -7
- package/dist/{chunk-QSCHCZL3.cjs.map → chunk-GUTYUMGV.cjs.map} +1 -1
- package/dist/{chunk-Q6E7VJ4W.cjs → chunk-IEZ3YBAP.cjs} +27 -28
- package/dist/chunk-IEZ3YBAP.cjs.map +1 -0
- package/dist/{chunk-HSACATCD.js → chunk-MPYDX5WX.js} +2 -2
- package/dist/{chunk-RQLBHKXN.js → chunk-NGEFYPY7.js} +2 -2
- package/dist/{chunk-BE6DK7QY.js → chunk-OQIMH2WQ.js} +8985 -11081
- package/dist/chunk-OQIMH2WQ.js.map +1 -0
- package/dist/{chunk-CSYSYJCF.cjs → chunk-UZZT65PQ.cjs} +37 -37
- package/dist/{chunk-CSYSYJCF.cjs.map → chunk-UZZT65PQ.cjs.map} +1 -1
- package/dist/{chunk-YZ2OIYQ3.js → chunk-VDZO6IDR.js} +4 -4
- package/dist/{chunk-UUYRJPGJ.js → chunk-VL2YMDPH.js} +4 -4
- package/dist/{chunk-5LMGFI4J.cjs → chunk-VUXNXWOQ.cjs} +4946 -7042
- package/dist/chunk-VUXNXWOQ.cjs.map +1 -0
- package/dist/{chunk-XQ2B74SI.js → chunk-WLM3F3WU.js} +2 -2
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- 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/guide-display.d.ts +14 -0
- package/dist/components/chat/guide-display.d.ts.map +1 -0
- package/dist/components/chat/hooks/index.d.ts +1 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +48 -15
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +6 -0
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +57 -10
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -8
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +15 -21
- package/dist/components/chat/types/api.types.d.ts +81 -1
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +6 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +15 -2
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +93 -2
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chunk-parser.d.ts +25 -0
- package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -0
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +6 -34
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +3 -2
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -0
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +11 -59
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +130 -170
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -63
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +2 -42
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +2 -1
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +15 -55
- package/dist/components/ui/rich-markdown-renderer.d.ts +34 -0
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/simple-markdown-renderer.d.ts +73 -0
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -0
- package/dist/index.cjs +2 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -71
- package/dist/utils/index.cjs +40 -169
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +41 -162
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
- package/package.json +1 -7
- package/src/components/chat/__tests__/chat-message-list.test.tsx +5 -293
- package/src/components/chat/chat-message-enhanced.tsx +80 -163
- package/src/components/chat/chat-message-list.tsx +109 -358
- package/src/components/chat/embeddable-chat.tsx +0 -6
- package/src/components/chat/guide-display.tsx +44 -0
- package/src/components/chat/hooks/.index.md +33 -30
- package/src/components/chat/hooks/.use-nats-chat-adapter.md +55 -36
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +77 -0
- package/src/components/chat/hooks/index.ts +1 -0
- package/src/components/chat/hooks/use-chat.ts +0 -8
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +825 -243
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +489 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +684 -380
- package/src/components/chat/index.ts +1 -5
- package/src/components/chat/thinking-display.tsx +1 -1
- package/src/components/chat/types/.api.types.md +49 -39
- package/src/components/chat/types/.processing.types.md +58 -29
- package/src/components/chat/types/api.types.ts +73 -6
- package/src/components/chat/types/component.types.ts +9 -0
- package/src/components/chat/types/message.types.ts +17 -1
- package/src/components/chat/types/processing.types.ts +53 -11
- package/src/components/chat/utils/.chunk-parser.md +62 -0
- package/src/components/chat/utils/.index.md +40 -31
- package/src/components/chat/utils/chunk-parser.ts +262 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +5 -63
- package/src/components/chat/utils/index.ts +8 -5
- package/src/components/chat/utils/message-segment-accumulator.ts +19 -0
- package/src/components/chat/utils/process-historical-messages.ts +390 -352
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +4 -22
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +4 -19
- package/src/components/ui/.simple-markdown-renderer.md +52 -0
- package/src/components/ui/index.ts +2 -1
- package/src/components/ui/release-changelog-section.tsx +1 -1
- package/src/components/ui/rich-markdown-renderer.tsx +1223 -0
- package/src/components/ui/simple-markdown-renderer.tsx +964 -0
- package/src/components/ui/ticket-info-section.tsx +1 -1
- package/src/stories/ChatMessageEnhanced.stories.tsx +48 -1
- package/src/utils/index.ts +0 -1
- package/src/utils/markdown-section-extractor.ts +56 -41
- package/dist/chat-protocol/decode.d.ts +0 -58
- package/dist/chat-protocol/decode.d.ts.map +0 -1
- package/dist/chat-protocol/encode.d.ts +0 -44
- package/dist/chat-protocol/encode.d.ts.map +0 -1
- package/dist/chat-protocol/env-flag.d.ts +0 -35
- package/dist/chat-protocol/env-flag.d.ts.map +0 -1
- package/dist/chat-protocol/events.d.ts +0 -196
- package/dist/chat-protocol/events.d.ts.map +0 -1
- package/dist/chat-protocol/frames.d.ts +0 -213
- package/dist/chat-protocol/frames.d.ts.map +0 -1
- package/dist/chat-protocol/index.cjs +0 -494
- package/dist/chat-protocol/index.cjs.map +0 -1
- package/dist/chat-protocol/index.d.ts +0 -18
- package/dist/chat-protocol/index.d.ts.map +0 -1
- package/dist/chat-protocol/index.js +0 -478
- package/dist/chat-protocol/index.js.map +0 -1
- package/dist/chat-protocol/ip-normalize.d.ts +0 -44
- package/dist/chat-protocol/ip-normalize.d.ts.map +0 -1
- package/dist/chat-protocol/nats-decoder.d.ts +0 -26
- package/dist/chat-protocol/nats-decoder.d.ts.map +0 -1
- package/dist/chunk-5LMGFI4J.cjs.map +0 -1
- package/dist/chunk-BE6DK7QY.js.map +0 -1
- package/dist/chunk-Q6E7VJ4W.cjs.map +0 -1
- package/dist/chunk-T2EGSZFN.js.map +0 -1
- package/dist/chunk-TKN5667N.js.map +0 -1
- package/dist/chunk-YHOIAA2E.cjs.map +0 -1
- package/dist/components/chat/stream/chat-dialog-store.d.ts +0 -184
- package/dist/components/chat/stream/chat-dialog-store.d.ts.map +0 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +0 -367
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +0 -1
- package/dist/components/chat/stream/delta-batcher.d.ts +0 -51
- package/dist/components/chat/stream/delta-batcher.d.ts.map +0 -1
- package/dist/components/chat/stream/index.d.ts +0 -17
- package/dist/components/chat/stream/index.d.ts.map +0 -1
- package/dist/components/chat/stream/message-mutations.d.ts +0 -77
- package/dist/components/chat/stream/message-mutations.d.ts.map +0 -1
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +0 -22
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +0 -1
- package/dist/components/ui/markdown/base-components.d.ts +0 -51
- package/dist/components/ui/markdown/base-components.d.ts.map +0 -1
- package/dist/components/ui/markdown/engine.d.ts +0 -99
- package/dist/components/ui/markdown/engine.d.ts.map +0 -1
- package/dist/components/ui/markdown/heading-ids.d.ts +0 -65
- package/dist/components/ui/markdown/heading-ids.d.ts.map +0 -1
- package/dist/components/ui/markdown/index.d.ts +0 -18
- package/dist/components/ui/markdown/index.d.ts.map +0 -1
- package/dist/components/ui/markdown/mermaid-diagram.d.ts +0 -79
- package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +0 -1
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts +0 -14
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +0 -1
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -40
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/markdown/rich/shortcodes.d.ts +0 -8
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +0 -1
- package/dist/components/ui/markdown/sanitize.d.ts +0 -153
- package/dist/components/ui/markdown/sanitize.d.ts.map +0 -1
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +0 -23
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/markdown/streaming.d.ts +0 -78
- package/dist/components/ui/markdown/streaming.d.ts.map +0 -1
- package/dist/components/ui/markdown/text-size.d.ts +0 -34
- package/dist/components/ui/markdown/text-size.d.ts.map +0 -1
- package/dist/utils/markdown-fences.d.ts +0 -42
- package/dist/utils/markdown-fences.d.ts.map +0 -1
- package/dist/utils/markdown-heading-id.d.ts +0 -85
- package/dist/utils/markdown-heading-id.d.ts.map +0 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +0 -307
- package/src/chat-protocol/__tests__/chat-protocol.test.ts +0 -516
- package/src/chat-protocol/__tests__/env-flag.test.ts +0 -52
- package/src/chat-protocol/__tests__/ip-normalize.test.ts +0 -136
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +0 -228
- package/src/chat-protocol/decode.ts +0 -278
- package/src/chat-protocol/encode.ts +0 -71
- package/src/chat-protocol/env-flag.ts +0 -39
- package/src/chat-protocol/events.ts +0 -232
- package/src/chat-protocol/frames.ts +0 -245
- package/src/chat-protocol/index.ts +0 -21
- package/src/chat-protocol/ip-normalize.ts +0 -146
- package/src/chat-protocol/nats-decoder.ts +0 -252
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +0 -207
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +0 -209
- package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +0 -37
- 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 +0 -438
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +0 -318
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +0 -940
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +0 -756
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +0 -460
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +0 -894
- package/src/components/chat/stream/__tests__/delta-batcher.test.ts +0 -255
- package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +0 -98
- package/src/components/chat/stream/chat-dialog-store.ts +0 -536
- package/src/components/chat/stream/chat-stream-reducer.ts +0 -1796
- package/src/components/chat/stream/delta-batcher.ts +0 -159
- package/src/components/chat/stream/index.ts +0 -55
- package/src/components/chat/stream/message-mutations.ts +0 -338
- package/src/components/chat/stream/use-chat-stream-reducer.ts +0 -126
- package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +0 -420
- package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +0 -605
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +0 -317
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +0 -270
- package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +0 -2420
- package/src/components/ui/__tests__/markdown-parity.test.tsx +0 -2528
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +0 -115
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +0 -95
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +0 -309
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +0 -388
- package/src/components/ui/markdown/base-components.tsx +0 -360
- package/src/components/ui/markdown/engine.tsx +0 -315
- package/src/components/ui/markdown/heading-ids.ts +0 -239
- package/src/components/ui/markdown/index.ts +0 -49
- package/src/components/ui/markdown/mermaid-diagram.tsx +0 -291
- package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -199
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +0 -184
- package/src/components/ui/markdown/rich/shortcodes.ts +0 -170
- package/src/components/ui/markdown/sanitize.ts +0 -3499
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +0 -28
- package/src/components/ui/markdown/streaming.ts +0 -362
- package/src/components/ui/markdown/text-size.ts +0 -106
- package/src/utils/markdown-fences.ts +0 -127
- package/src/utils/markdown-heading-id.ts +0 -348
- /package/dist/{chunk-GTWO5NAY.js.map → chunk-6CFFISUC.js.map} +0 -0
- /package/dist/{chunk-VHG6AX5V.js.map → chunk-6CJ2K7BD.js.map} +0 -0
- /package/dist/{chunk-4NWARV5I.js.map → chunk-6QK3ONDD.js.map} +0 -0
- /package/dist/{chunk-CORIBKHG.js.map → chunk-EGXCWZYP.js.map} +0 -0
- /package/dist/{chunk-HSACATCD.js.map → chunk-MPYDX5WX.js.map} +0 -0
- /package/dist/{chunk-RQLBHKXN.js.map → chunk-NGEFYPY7.js.map} +0 -0
- /package/dist/{chunk-YZ2OIYQ3.js.map → chunk-VDZO6IDR.js.map} +0 -0
- /package/dist/{chunk-UUYRJPGJ.js.map → chunk-VL2YMDPH.js.map} +0 -0
- /package/dist/{chunk-XQ2B74SI.js.map → chunk-WLM3F3WU.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-section-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/markdown-section-extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"markdown-section-extractor.d.ts","sourceRoot":"","sources":["../../src/utils/markdown-section-extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,4DAA4D;IAC5D,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAUD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,eAAe,EAAE,CAoEnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flamingo-stack/openframe-frontend-core",
|
|
3
|
-
"version": "0.0.490
|
|
3
|
+
"version": "0.0.490",
|
|
4
4
|
"description": "Shared design system and components for all Flamingo platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -102,12 +102,6 @@
|
|
|
102
102
|
"require": "./dist/platform-domains.cjs",
|
|
103
103
|
"default": "./dist/platform-domains.js"
|
|
104
104
|
},
|
|
105
|
-
"./chat-protocol": {
|
|
106
|
-
"types": "./dist/chat-protocol/index.d.ts",
|
|
107
|
-
"import": "./dist/chat-protocol/index.js",
|
|
108
|
-
"require": "./dist/chat-protocol/index.cjs",
|
|
109
|
-
"default": "./dist/chat-protocol/index.js"
|
|
110
|
-
},
|
|
111
105
|
"./components/ui/file-manager": {
|
|
112
106
|
"types": "./dist/components/ui/file-manager/index.d.ts",
|
|
113
107
|
"import": "./dist/components/ui/file-manager/index.js",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import type { Message } from '../types/message.types'
|
|
4
4
|
|
|
@@ -26,16 +26,9 @@ vi.mock('use-stick-to-bottom', () => ({
|
|
|
26
26
|
|
|
27
27
|
// Keep the row renderer trivial — these tests exercise ChatMessageList's
|
|
28
28
|
// scroll orchestration, not markdown rendering.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const { forwardRef } = await import('react')
|
|
33
|
-
return {
|
|
34
|
-
ChatMessageEnhanced: forwardRef<HTMLDivElement, { content: string }>(({ content }, ref) => (
|
|
35
|
-
<div ref={ref}>{content}</div>
|
|
36
|
-
)),
|
|
37
|
-
}
|
|
38
|
-
})
|
|
29
|
+
vi.mock('../chat-message-enhanced', () => ({
|
|
30
|
+
ChatMessageEnhanced: ({ content }: { content: string }) => <div>{content}</div>,
|
|
31
|
+
}))
|
|
39
32
|
|
|
40
33
|
import { ChatMessageList } from '../chat-message-list'
|
|
41
34
|
|
|
@@ -47,36 +40,7 @@ class ObserverStub {
|
|
|
47
40
|
disconnect() {}
|
|
48
41
|
}
|
|
49
42
|
vi.stubGlobal('IntersectionObserver', ObserverStub)
|
|
50
|
-
|
|
51
|
-
// Recording RO so the bottom-follow tests can drive a resize by hand.
|
|
52
|
-
const resizeCallbacks: Array<() => void> = []
|
|
53
|
-
class RecordingResizeObserver {
|
|
54
|
-
constructor(cb: () => void) {
|
|
55
|
-
resizeCallbacks.push(cb)
|
|
56
|
-
}
|
|
57
|
-
observe() {}
|
|
58
|
-
unobserve() {}
|
|
59
|
-
disconnect() {}
|
|
60
|
-
}
|
|
61
|
-
vi.stubGlobal('ResizeObserver', RecordingResizeObserver)
|
|
62
|
-
/** Fire every live ResizeObserver callback — same effect as any content
|
|
63
|
-
* or scroller-box size change in the real DOM. */
|
|
64
|
-
const fireResize = () => {
|
|
65
|
-
for (const cb of resizeCallbacks) cb()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** jsdom has no layout, so every box metric reads 0. The jump-to-bottom
|
|
69
|
-
* button is driven off real geometry, so the tests supply it. */
|
|
70
|
-
const setGeometry = (geo: { scrollHeight: number; clientHeight: number; scrollTop: number }) => {
|
|
71
|
-
const el = scrollRefMock.current
|
|
72
|
-
if (!el) throw new Error('scroller not mounted')
|
|
73
|
-
for (const [key, value] of Object.entries(geo)) {
|
|
74
|
-
Object.defineProperty(el, key, { value, configurable: true, writable: true })
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// jsdom has no layout, so `scrollIntoView` is undefined.
|
|
79
|
-
Element.prototype.scrollIntoView = vi.fn()
|
|
43
|
+
vi.stubGlobal('ResizeObserver', ObserverStub)
|
|
80
44
|
|
|
81
45
|
const msg = (id: string, role: Message['role']): Message => ({
|
|
82
46
|
id,
|
|
@@ -126,255 +90,3 @@ describe('ChatMessageList force-scroll decisions', () => {
|
|
|
126
90
|
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
127
91
|
})
|
|
128
92
|
})
|
|
129
|
-
|
|
130
|
-
// The library's own `isAtBottom` lock is lost silently when the scroller's
|
|
131
|
-
// box changes (source chips mounting below it) or when a card settling out
|
|
132
|
-
// of its skeleton produces a resize-driven scroll the library reads as a
|
|
133
|
-
// user gesture. The list therefore owns the INTENT and re-asserts on every
|
|
134
|
-
// geometry change until a real gesture releases it.
|
|
135
|
-
describe('ChatMessageList bottom-follow intent', () => {
|
|
136
|
-
beforeEach(() => {
|
|
137
|
-
scrollToBottom.mockClear()
|
|
138
|
-
resizeCallbacks.length = 0
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
const sendTurn = () => {
|
|
142
|
-
const initial = [msg('m1', 'user'), msg('m2', 'assistant')]
|
|
143
|
-
const view = render(<ChatMessageList dialogId="d1" messages={initial} />)
|
|
144
|
-
// A user send arms the follow intent for the whole turn.
|
|
145
|
-
view.rerender(<ChatMessageList dialogId="d1" messages={[...initial, msg('m3', 'user')]} />)
|
|
146
|
-
scrollToBottom.mockClear()
|
|
147
|
-
return view
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
it('re-asserts the bottom on geometry changes after the send', () => {
|
|
151
|
-
sendTurn()
|
|
152
|
-
fireResize()
|
|
153
|
-
expect(scrollToBottom).toHaveBeenCalled()
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('keeps re-asserting for late async growth (cards + covers resolving)', () => {
|
|
157
|
-
sendTurn()
|
|
158
|
-
fireResize()
|
|
159
|
-
fireResize()
|
|
160
|
-
fireResize()
|
|
161
|
-
expect(scrollToBottom).toHaveBeenCalledTimes(3)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('releases the intent once the user scrolls up', () => {
|
|
165
|
-
sendTurn()
|
|
166
|
-
scrollRefMock.current?.dispatchEvent(
|
|
167
|
-
new WheelEvent('wheel', { deltaY: -120, bubbles: true }),
|
|
168
|
-
)
|
|
169
|
-
fireResize()
|
|
170
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('ignores downward wheel (never an escape from the bottom)', () => {
|
|
174
|
-
sendTurn()
|
|
175
|
-
scrollRefMock.current?.dispatchEvent(
|
|
176
|
-
new WheelEvent('wheel', { deltaY: 120, bubbles: true }),
|
|
177
|
-
)
|
|
178
|
-
fireResize()
|
|
179
|
-
expect(scrollToBottom).toHaveBeenCalled()
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('does not follow when autoScroll is off (passive demo hosts)', () => {
|
|
183
|
-
const initial = [msg('m1', 'user'), msg('m2', 'assistant')]
|
|
184
|
-
const { rerender } = render(
|
|
185
|
-
<ChatMessageList dialogId="d1" messages={initial} autoScroll={false} />,
|
|
186
|
-
)
|
|
187
|
-
rerender(
|
|
188
|
-
<ChatMessageList dialogId="d1" messages={[...initial, msg('m3', 'user')]} autoScroll={false} />,
|
|
189
|
-
)
|
|
190
|
-
scrollToBottom.mockClear()
|
|
191
|
-
fireResize()
|
|
192
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
it('hides jump-to-bottom while the thread is at the bottom', () => {
|
|
196
|
-
const view = sendTurn()
|
|
197
|
-
setGeometry({ scrollHeight: 1000, clientHeight: 500, scrollTop: 500 })
|
|
198
|
-
act(() => fireResize())
|
|
199
|
-
expect(view.queryByLabelText('Scroll to latest message')).toBeNull()
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('shows jump-to-bottom once the thread is away from the bottom', () => {
|
|
203
|
-
const view = sendTurn()
|
|
204
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 500 })
|
|
205
|
-
act(() => fireResize())
|
|
206
|
-
expect(view.queryByLabelText('Scroll to latest message')).not.toBeNull()
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('re-arms the intent when the user clicks jump-to-bottom', () => {
|
|
210
|
-
const view = sendTurn()
|
|
211
|
-
// User scrolls away: intent released, button appears.
|
|
212
|
-
scrollRefMock.current?.dispatchEvent(
|
|
213
|
-
new WheelEvent('wheel', { deltaY: -120, bubbles: true }),
|
|
214
|
-
)
|
|
215
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 500 })
|
|
216
|
-
act(() => fireResize())
|
|
217
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
218
|
-
|
|
219
|
-
const button = view.getByLabelText('Scroll to latest message')
|
|
220
|
-
act(() => {
|
|
221
|
-
button.click()
|
|
222
|
-
})
|
|
223
|
-
expect(scrollToBottom).toHaveBeenCalled()
|
|
224
|
-
|
|
225
|
-
// …and the click re-armed the lock, so growth follows again.
|
|
226
|
-
scrollToBottom.mockClear()
|
|
227
|
-
act(() => fireResize())
|
|
228
|
-
expect(scrollToBottom).toHaveBeenCalled()
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
// A drag that ends OUTSIDE the window (button released off-screen, or the
|
|
232
|
-
// tab losing focus mid-drag) never delivers `pointerup`, so a naive
|
|
233
|
-
// `pointerDown` flag stays true forever. Afterwards the very thing this
|
|
234
|
-
// component exists for — a fetch-mode card settling from skeleton to a
|
|
235
|
-
// SHORTER height, which clamps scrollTop down — is misread as a scroll-up
|
|
236
|
-
// gesture and silently drops the lock.
|
|
237
|
-
const dragThenScrollDown = (endEvent: { target: 'window'; type: string }) => {
|
|
238
|
-
sendTurn()
|
|
239
|
-
// Park away from the bottom so the at-bottom re-arm below can't mask the
|
|
240
|
-
// result, and seed `lastScrollTop`.
|
|
241
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 4000 })
|
|
242
|
-
window.dispatchEvent(new Event('pointerdown'))
|
|
243
|
-
act(() => {
|
|
244
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
245
|
-
})
|
|
246
|
-
// Drag ends where we can't see it.
|
|
247
|
-
if (endEvent.target === 'window') window.dispatchEvent(new Event(endEvent.type))
|
|
248
|
-
// Resize clamp: content shrank, scrollTop drops. NOT a gesture.
|
|
249
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 3800 })
|
|
250
|
-
act(() => {
|
|
251
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
252
|
-
})
|
|
253
|
-
scrollToBottom.mockClear()
|
|
254
|
-
act(() => fireResize())
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
it('keeps the intent when a pointercancel ended the drag (resize clamp is not a gesture)', () => {
|
|
258
|
-
dragThenScrollDown({ target: 'window', type: 'pointercancel' })
|
|
259
|
-
expect(scrollToBottom).toHaveBeenCalled()
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
it('keeps the intent when the window lost focus mid-drag', () => {
|
|
263
|
-
dragThenScrollDown({ target: 'window', type: 'blur' })
|
|
264
|
-
expect(scrollToBottom).toHaveBeenCalled()
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
// Focus events do not bubble but they DO capture, so a capture-phase `blur`
|
|
268
|
-
// listener on `window` fires for EVERY element blur in the document — the
|
|
269
|
-
// classic focus-delegation idiom. Registering the drag-end `blur` that way
|
|
270
|
-
// made any focus change during a pointer hold (most commonly the chat input
|
|
271
|
-
// blurring as the user presses down on the scrollbar) clear `pointerDown`,
|
|
272
|
-
// which is exactly what re-opens the yank-back-mid-drag bug. Only a TRUE
|
|
273
|
-
// window blur (which targets `window` itself) may end the drag.
|
|
274
|
-
it('a descendant element losing focus does not end the drag', () => {
|
|
275
|
-
sendTurn()
|
|
276
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 4000 })
|
|
277
|
-
window.dispatchEvent(new Event('pointerdown'))
|
|
278
|
-
act(() => {
|
|
279
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
280
|
-
})
|
|
281
|
-
// The chat input loses focus as the pointer goes down elsewhere.
|
|
282
|
-
const input = document.createElement('input')
|
|
283
|
-
document.body.appendChild(input)
|
|
284
|
-
input.focus()
|
|
285
|
-
input.blur()
|
|
286
|
-
input.remove()
|
|
287
|
-
// The drag is still live, so this held scroll-up IS a gesture.
|
|
288
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 3800 })
|
|
289
|
-
act(() => {
|
|
290
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
291
|
-
})
|
|
292
|
-
scrollToBottom.mockClear()
|
|
293
|
-
act(() => fireResize())
|
|
294
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
295
|
-
window.dispatchEvent(new Event('pointerup'))
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it('still disarms on a genuine scrollbar drag upward', () => {
|
|
299
|
-
sendTurn()
|
|
300
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 4000 })
|
|
301
|
-
window.dispatchEvent(new Event('pointerdown'))
|
|
302
|
-
act(() => {
|
|
303
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
304
|
-
})
|
|
305
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 3800 })
|
|
306
|
-
act(() => {
|
|
307
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
308
|
-
})
|
|
309
|
-
scrollToBottom.mockClear()
|
|
310
|
-
act(() => fireResize())
|
|
311
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
312
|
-
window.dispatchEvent(new Event('pointerup'))
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
// Scrolling back down to the live end by hand expresses exactly the intent
|
|
316
|
-
// the jump-to-bottom button does — the lock has to re-arm, or the thread
|
|
317
|
-
// renders at-bottom (button hidden) and then silently drifts away.
|
|
318
|
-
it('re-arms the intent when the user scrolls back to the bottom by hand', () => {
|
|
319
|
-
sendTurn()
|
|
320
|
-
scrollRefMock.current?.dispatchEvent(new WheelEvent('wheel', { deltaY: -120, bubbles: true }))
|
|
321
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 500 })
|
|
322
|
-
act(() => fireResize())
|
|
323
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
324
|
-
|
|
325
|
-
// …and back down to the end.
|
|
326
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 19500 })
|
|
327
|
-
act(() => {
|
|
328
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
329
|
-
})
|
|
330
|
-
scrollToBottom.mockClear()
|
|
331
|
-
act(() => fireResize())
|
|
332
|
-
expect(scrollToBottom).toHaveBeenCalled()
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
it('does not re-arm while the user is still parked above the bottom', () => {
|
|
336
|
-
sendTurn()
|
|
337
|
-
scrollRefMock.current?.dispatchEvent(new WheelEvent('wheel', { deltaY: -120, bubbles: true }))
|
|
338
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 8000 })
|
|
339
|
-
act(() => {
|
|
340
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
341
|
-
})
|
|
342
|
-
scrollToBottom.mockClear()
|
|
343
|
-
act(() => fireResize())
|
|
344
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
// A short scrollbar drag that starts at the live end and stays inside the
|
|
348
|
-
// 70px band disarms on the gesture and would immediately re-arm on the very
|
|
349
|
-
// same scroll event if the re-arm didn't wait for the drag to END — yanking
|
|
350
|
-
// the reader back down mid-drag, a miniature of the bug this block exists
|
|
351
|
-
// to prevent.
|
|
352
|
-
it('does not re-arm mid-drag when the drag stays inside the bottom band', () => {
|
|
353
|
-
sendTurn()
|
|
354
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 19500 })
|
|
355
|
-
act(() => {
|
|
356
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
357
|
-
})
|
|
358
|
-
window.dispatchEvent(new Event('pointerdown'))
|
|
359
|
-
setGeometry({ scrollHeight: 20000, clientHeight: 500, scrollTop: 19460 })
|
|
360
|
-
act(() => {
|
|
361
|
-
scrollRefMock.current?.dispatchEvent(new Event('scroll'))
|
|
362
|
-
})
|
|
363
|
-
scrollToBottom.mockClear()
|
|
364
|
-
act(() => fireResize())
|
|
365
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
366
|
-
window.dispatchEvent(new Event('pointerup'))
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
it('releases the intent for a top-anchored turn (it parks at the message top)', () => {
|
|
370
|
-
const initial = [msg('m1', 'user'), msg('m2', 'assistant')]
|
|
371
|
-
const { rerender } = render(<ChatMessageList dialogId="d1" messages={initial} />)
|
|
372
|
-
const sent = [...initial, msg('m3', 'user')]
|
|
373
|
-
rerender(<ChatMessageList dialogId="d1" messages={sent} />)
|
|
374
|
-
const anchored: Message = { ...msg('m4', 'assistant'), scrollAnchor: 'top' }
|
|
375
|
-
rerender(<ChatMessageList dialogId="d1" messages={[...sent, anchored]} />)
|
|
376
|
-
scrollToBottom.mockClear()
|
|
377
|
-
fireResize()
|
|
378
|
-
expect(scrollToBottom).not.toHaveBeenCalled()
|
|
379
|
-
})
|
|
380
|
-
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import React, { forwardRef, memo,
|
|
3
|
+
import React, { forwardRef, memo, useMemo, useRef } from "react"
|
|
4
4
|
import { cn } from "../../utils/cn"
|
|
5
5
|
import { isToday } from "../../utils/date-utils"
|
|
6
6
|
import { formatDate, formatTime } from "../../utils/format-date"
|
|
@@ -11,7 +11,8 @@ import { ApprovalBatchMessage } from "./approval-batch-message"
|
|
|
11
11
|
import { ErrorMessageDisplay } from "./error-message-display"
|
|
12
12
|
import { ContextCompactionDisplay } from "./context-compaction-display"
|
|
13
13
|
import { ThinkingDisplay } from "./thinking-display"
|
|
14
|
-
import {
|
|
14
|
+
import { GuideDisplay } from "./guide-display"
|
|
15
|
+
import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
|
|
15
16
|
import type { ChatRef } from "./chat-ref.types"
|
|
16
17
|
import { remarkCardLinks } from "./remark-card-links"
|
|
17
18
|
import { remarkMentionChips } from "./remark-mention-chips"
|
|
@@ -163,16 +164,21 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
163
164
|
| { kind: 'text'; text: string }
|
|
164
165
|
| { kind: 'block'; key: string; node: React.ReactNode }
|
|
165
166
|
const partsBySegment = new Map<number, SegmentPart[]>()
|
|
166
|
-
|
|
167
|
-
if (!render) return { inlineByKey, partsBySegment, usedKeys }
|
|
167
|
+
if (!render) return { inlineByKey, partsBySegment }
|
|
168
168
|
const cache = renderedCardNodeCache.current
|
|
169
|
+
const usedKeys = new Set<string>()
|
|
169
170
|
// Card keys already emitted as a hoisted block (`b-<key>`). The same
|
|
170
171
|
// marker can legitimately appear twice in one message (LLM references
|
|
171
172
|
// the same entity twice); we hoist the FIRST occurrence and skip the
|
|
172
173
|
// duplicates so two siblings never collide on the same React key.
|
|
173
174
|
const emittedBlockKeys = new Set<string>()
|
|
174
175
|
segments.forEach((segment, segIdx) => {
|
|
175
|
-
|
|
176
|
+
// Both markdown-bearing segment types take part: a `guide` body is
|
|
177
|
+
// authored by the same LLM and carries the same `[card://]` markers,
|
|
178
|
+
// so skipping it here left its markers with no `inlineByKey` entry and
|
|
179
|
+
// no hoisted card — the `<a card://>` override silently degraded them
|
|
180
|
+
// to a bare title / id.
|
|
181
|
+
if (segment.type !== 'text' && segment.type !== 'guide') return
|
|
176
182
|
const text = segment.text
|
|
177
183
|
const parts: SegmentPart[] = []
|
|
178
184
|
let cursor = 0
|
|
@@ -275,99 +281,20 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
275
281
|
partsBySegment.set(segIdx, parts)
|
|
276
282
|
}
|
|
277
283
|
})
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// render phase is non-idempotent (StrictMode's double-invoke, or a render
|
|
285
|
-
// React throws away, would evict nodes the committed tree still uses and
|
|
286
|
-
// remount the cards it was built to preserve).
|
|
287
|
-
useEffect(() => {
|
|
288
|
-
const cache = renderedCardNodeCache.current
|
|
289
|
-
const usedKeys = renderingPlan?.usedKeys
|
|
290
|
-
if (!usedKeys) return
|
|
291
|
-
if (cache.size <= usedKeys.size) return
|
|
292
|
-
for (const k of [...cache.keys()]) {
|
|
293
|
-
if (!usedKeys.has(k)) cache.delete(k)
|
|
284
|
+
// Drop cached nodes for markers no longer present so the cache can't
|
|
285
|
+
// grow unbounded as a long message's markers change.
|
|
286
|
+
if (cache.size > usedKeys.size) {
|
|
287
|
+
for (const k of cache.keys()) {
|
|
288
|
+
if (!usedKeys.has(k)) cache.delete(k)
|
|
289
|
+
}
|
|
294
290
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// The plan is read through a REF inside the `<a>` override, not captured
|
|
298
|
-
// in the override's closure. `renderingPlan` is rebuilt on every text
|
|
299
|
-
// delta (its `segments` dep is a fresh array per delta), so depending on
|
|
300
|
-
// it here would give `cardComponentOverrides` a new identity every token
|
|
301
|
-
// — which re-creates the markdown engine's `components` memo and defeats
|
|
302
|
-
// its per-block `StreamingBlockRenderer` memoization for the WHOLE
|
|
303
|
-
// message, on every token. The ref keeps the override identity stable
|
|
304
|
-
// while still reading the current plan at call time.
|
|
305
|
-
const renderingPlanRef = useRef(renderingPlan)
|
|
306
|
-
renderingPlanRef.current = renderingPlan
|
|
307
|
-
const chatRefsRef = useRef(chatRefs)
|
|
308
|
-
chatRefsRef.current = chatRefs
|
|
309
|
-
|
|
310
|
-
// Ref-SET token. The override reads `chatRefsRef` at CALL time, but the
|
|
311
|
-
// markdown engine memoizes completed blocks on the override map's
|
|
312
|
-
// identity — so on a COMPLETED (no longer streaming) message, a ref that
|
|
313
|
-
// resolves late would never be re-read: nothing else about the renderer's
|
|
314
|
-
// props changes, the engine's memo bails, and the pill stays stuck on its
|
|
315
|
-
// fallback (`refMatch.title`, or the raw cardId) forever. Streaming is
|
|
316
|
-
// covered because `card://` blocks are excluded from the block cache; the
|
|
317
|
-
// completed path has no other escape.
|
|
318
|
-
//
|
|
319
|
-
// The token fingerprints VALUES, not just the key set: a later refs frame
|
|
320
|
-
// REPLACES the whole map for a send index, and the enrichment case that
|
|
321
|
-
// matters most — a `title` going from the raw cardId to the resolved
|
|
322
|
-
// document title — keeps the key set IDENTICAL. A key-only token would be
|
|
323
|
-
// unchanged, the override map would keep its identity, the engine's
|
|
324
|
-
// completed-block memo would bail, and the pill would stay on its fallback
|
|
325
|
-
// forever. Every `ChatRef` field is included because the host's
|
|
326
|
-
// `renderEntityCard` receives the whole ref and may render any of them.
|
|
327
|
-
//
|
|
328
|
-
// Still STABLE per text delta (the refs map doesn't change while tokens
|
|
329
|
-
// stream), and the cost is one pass over a handful of refs per CHANGED
|
|
330
|
-
// `chatRefs` identity — the dep list is unchanged.
|
|
331
|
-
//
|
|
332
|
-
// Fields are joined by `JSON.stringify`, NOT by a separator character.
|
|
333
|
-
// `title`, `preview` and `url` are free-form HOST strings, so any
|
|
334
|
-
// delimiter we pick is one the data may also contain: joined with a plain
|
|
335
|
-
// space, `title: 'a b'` + empty `url` fingerprints identically to
|
|
336
|
-
// `title: 'a'` + `url: 'b'`, silently reintroducing the stale-pill memo
|
|
337
|
-
// bail this token exists to fix. (An earlier revision used a literal NUL
|
|
338
|
-
// byte as the separator: collision-free in practice, but it made the
|
|
339
|
-
// source file itself binary to grep and diff tooling.) `JSON.stringify`
|
|
340
|
-
// escapes every field, so no value can forge a boundary.
|
|
341
|
-
//
|
|
342
|
-
// Caveat worth stating: `JSON.stringify` on `metadata` is key-ORDER
|
|
343
|
-
// dependent: two structurally equal objects built in different insertion
|
|
344
|
-
// orders fingerprint differently. Acceptable here because every refs map
|
|
345
|
-
// arrives from ONE decoder with a fixed field order, and the failure mode
|
|
346
|
-
// is a redundant re-render, never a stale pill.
|
|
347
|
-
const refsKey = useMemo(
|
|
348
|
-
() =>
|
|
349
|
-
Object.entries(chatRefs ?? {})
|
|
350
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
351
|
-
.map(([k, v]) =>
|
|
352
|
-
JSON.stringify([
|
|
353
|
-
k,
|
|
354
|
-
v.type,
|
|
355
|
-
v.sourceRepo ?? null,
|
|
356
|
-
v.id,
|
|
357
|
-
v.title,
|
|
358
|
-
v.url ?? null,
|
|
359
|
-
v.targetPlatform ?? null,
|
|
360
|
-
v.date ?? null,
|
|
361
|
-
v.preview ?? null,
|
|
362
|
-
v.metadata ?? null,
|
|
363
|
-
]),
|
|
364
|
-
)
|
|
365
|
-
.join('|'),
|
|
366
|
-
[chatRefs],
|
|
367
|
-
)
|
|
291
|
+
return { inlineByKey, partsBySegment }
|
|
292
|
+
}, [hasMarkerSupport, chatRefs, renderEntityCard, segments])
|
|
368
293
|
|
|
369
294
|
const cardComponentOverrides = useMemo(() => {
|
|
370
295
|
if (!hasMarkerSupport && !hasMentionSupport) return undefined
|
|
296
|
+
const refs = chatRefs ?? {}
|
|
297
|
+
const inlineByKey = renderingPlan?.inlineByKey
|
|
371
298
|
return {
|
|
372
299
|
// Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`.
|
|
373
300
|
// The render result was pre-computed in `renderingPlan` so block-level
|
|
@@ -395,7 +322,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
395
322
|
const cardType = stripped.slice(0, sepIdx)
|
|
396
323
|
const cardId = stripped.slice(sepIdx + 1)
|
|
397
324
|
const key = `${cardType}:${cardId}`
|
|
398
|
-
const inline =
|
|
325
|
+
const inline = inlineByKey?.get(key)
|
|
399
326
|
if (inline != null) return inline
|
|
400
327
|
// Three fallback cases — keep them DISTINCT, never blur them
|
|
401
328
|
// together. Mixing them up (which the old code did by reaching
|
|
@@ -419,7 +346,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
419
346
|
// title from an unrelated ref — that hides the bug and
|
|
420
347
|
// deceives the reader into thinking they're looking at
|
|
421
348
|
// a real card.
|
|
422
|
-
const refMatch: ChatRef | undefined =
|
|
349
|
+
const refMatch: ChatRef | undefined = refs[key]
|
|
423
350
|
if (refMatch) {
|
|
424
351
|
return <span className="text-ods-text-primary">{refMatch.title}</span>
|
|
425
352
|
}
|
|
@@ -461,13 +388,53 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
461
388
|
)
|
|
462
389
|
},
|
|
463
390
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
391
|
+
}, [hasMarkerSupport, hasMentionSupport, renderMention, chatRefs, renderingPlan, NavLinkAnchor])
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Body of a markdown-bearing segment (`text` / `guide`).
|
|
395
|
+
*
|
|
396
|
+
* No block markers in this segment → a single SimpleMarkdownRenderer call
|
|
397
|
+
* (the vast majority of messages). Otherwise the text is split at each
|
|
398
|
+
* marker and block payloads are interleaved: each text chunk includes its
|
|
399
|
+
* trailing marker so the inline pill renders at the right position via the
|
|
400
|
+
* `<a>` override, and block payloads land AS SIBLINGS between text chunks —
|
|
401
|
+
* HTML-valid (block DOM never nests inside `<p>`) AND positionally correct
|
|
402
|
+
* (the block appears where the marker is in the flow, not at the segment's
|
|
403
|
+
* end). Stable React keys come from the card key (block) and chunk position
|
|
404
|
+
* (text); streaming token-by-token reuses the same React instances so
|
|
405
|
+
* `<Video>` doesn't remount mid-play.
|
|
406
|
+
*/
|
|
407
|
+
const renderSegmentBody = (segIndex: number, text: string) => {
|
|
408
|
+
const parts = renderingPlan?.partsBySegment.get(segIndex)
|
|
409
|
+
if (!parts || parts.length === 0) {
|
|
410
|
+
return (
|
|
411
|
+
<SimpleMarkdownRenderer
|
|
412
|
+
content={text}
|
|
413
|
+
textSize="compact"
|
|
414
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
415
|
+
componentOverrides={cardComponentOverrides}
|
|
416
|
+
/>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
return parts.map((part, pIdx) => {
|
|
420
|
+
if (part.kind === 'text') {
|
|
421
|
+
return (
|
|
422
|
+
<SimpleMarkdownRenderer
|
|
423
|
+
key={`t-${pIdx}`}
|
|
424
|
+
content={part.text}
|
|
425
|
+
textSize="compact"
|
|
426
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
427
|
+
componentOverrides={cardComponentOverrides}
|
|
428
|
+
/>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
return (
|
|
432
|
+
<div key={`b-${part.key}`} className="my-3">
|
|
433
|
+
{part.node}
|
|
434
|
+
</div>
|
|
435
|
+
)
|
|
436
|
+
})
|
|
437
|
+
}
|
|
471
438
|
|
|
472
439
|
const getAvatarProps = () => {
|
|
473
440
|
const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
|
|
@@ -573,69 +540,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
573
540
|
{(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
|
|
574
541
|
{segments.map((segment, index) => {
|
|
575
542
|
if (segment.type === 'text') {
|
|
576
|
-
const parts = renderingPlan?.partsBySegment.get(index)
|
|
577
|
-
const wrapperClass = cn(
|
|
578
|
-
"min-w-0 w-full break-words text-h4",
|
|
579
|
-
isError ? "text-ods-error" : "text-ods-text-primary",
|
|
580
|
-
)
|
|
581
|
-
// The engine's streaming path (atomic-block memoization +
|
|
582
|
-
// fence tail-completion + aria-live) applies ONLY to the
|
|
583
|
-
// actively streaming segment: last segment of a message
|
|
584
|
-
// that is still typing. On completion `isTyping` flips
|
|
585
|
-
// false and the engine does one authoritative
|
|
586
|
-
// whole-document parse.
|
|
587
|
-
const segmentIsStreaming = index === segments.length - 1 && !!isTyping
|
|
588
|
-
// No block markers in this segment → single
|
|
589
|
-
// SimpleMarkdownRenderer call (existing behaviour
|
|
590
|
-
// preserved for the vast majority of messages).
|
|
591
|
-
if (!parts || parts.length === 0) {
|
|
592
|
-
return (
|
|
593
|
-
<div key={index} className={wrapperClass}>
|
|
594
|
-
<SimpleMarkdownRenderer
|
|
595
|
-
content={segment.text}
|
|
596
|
-
textSize="compact"
|
|
597
|
-
additionalRemarkPlugins={cardRemarkPlugins}
|
|
598
|
-
componentOverrides={cardComponentOverrides}
|
|
599
|
-
streaming={segmentIsStreaming}
|
|
600
|
-
/>
|
|
601
|
-
</div>
|
|
602
|
-
)
|
|
603
|
-
}
|
|
604
|
-
// Block markers present → split text at each marker
|
|
605
|
-
// and interleave block payloads. Each text chunk
|
|
606
|
-
// includes its trailing marker so the inline pill
|
|
607
|
-
// renders at the right position via the `<a>`
|
|
608
|
-
// override. Block payloads land AS SIBLINGS between
|
|
609
|
-
// text chunks — HTML-valid (block DOM never nests
|
|
610
|
-
// inside `<p>`) AND positionally correct (block
|
|
611
|
-
// appears where the marker is in the flow, not at
|
|
612
|
-
// the segment's end). Stable React keys come from
|
|
613
|
-
// the card key (block) and chunk position (text);
|
|
614
|
-
// streaming token-by-token reuses the same React
|
|
615
|
-
// instances so `<Video>` doesn't remount mid-play.
|
|
616
543
|
return (
|
|
617
|
-
<div
|
|
618
|
-
{
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
additionalRemarkPlugins={cardRemarkPlugins}
|
|
626
|
-
componentOverrides={cardComponentOverrides}
|
|
627
|
-
streaming={segmentIsStreaming && pIdx === parts.length - 1}
|
|
628
|
-
/>
|
|
629
|
-
)
|
|
630
|
-
}
|
|
631
|
-
return (
|
|
632
|
-
<div key={`b-${part.key}`} className="my-3">
|
|
633
|
-
{part.node}
|
|
634
|
-
</div>
|
|
635
|
-
)
|
|
636
|
-
})}
|
|
544
|
+
<div
|
|
545
|
+
key={index}
|
|
546
|
+
className={cn(
|
|
547
|
+
"min-w-0 w-full break-words text-h4",
|
|
548
|
+
isError ? "text-ods-error" : "text-ods-text-primary",
|
|
549
|
+
)}
|
|
550
|
+
>
|
|
551
|
+
{renderSegmentBody(index, segment.text)}
|
|
637
552
|
</div>
|
|
638
553
|
)
|
|
554
|
+
} else if (segment.type === 'guide') {
|
|
555
|
+
return <GuideDisplay key={index}>{renderSegmentBody(index, segment.text)}</GuideDisplay>
|
|
639
556
|
} else if (segment.type === 'tool_execution') {
|
|
640
557
|
return (
|
|
641
558
|
<ToolExecutionDisplay
|