@flamingo-stack/openframe-frontend-core 0.0.510 → 0.0.511-snapshot.20260804140004
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.map +1 -1
- package/dist/chat-protocol/frames.d.ts +26 -1
- package/dist/chat-protocol/frames.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +24 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +24 -0
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
- package/dist/chunk-27OKD7ZV.js.map +1 -0
- package/dist/{chunk-RHQ7BJKK.cjs → chunk-2WWDZR4D.cjs} +73 -73
- package/dist/{chunk-RHQ7BJKK.cjs.map → chunk-2WWDZR4D.cjs.map} +1 -1
- package/dist/{chunk-J2JUAS4C.cjs → chunk-2XL7UK5Y.cjs} +11 -11
- package/dist/{chunk-J2JUAS4C.cjs.map → chunk-2XL7UK5Y.cjs.map} +1 -1
- package/dist/{chunk-Q4K3TGNP.cjs → chunk-3NECEXDF.cjs} +74 -44
- package/dist/chunk-3NECEXDF.cjs.map +1 -0
- package/dist/{chunk-JOCFPAS6.js → chunk-4NFDSMB5.js} +6 -6
- package/dist/{chunk-SYZFFRVD.cjs → chunk-5BAGE7AI.cjs} +13 -13
- package/dist/{chunk-SYZFFRVD.cjs.map → chunk-5BAGE7AI.cjs.map} +1 -1
- package/dist/{chunk-53JKCFFE.cjs → chunk-5JBDT2MQ.cjs} +12 -12
- package/dist/{chunk-53JKCFFE.cjs.map → chunk-5JBDT2MQ.cjs.map} +1 -1
- package/dist/{chunk-XJVRAKEX.cjs → chunk-67YDSTH7.cjs} +56 -33
- package/dist/chunk-67YDSTH7.cjs.map +1 -0
- package/dist/{chunk-WNOGR5AA.js → chunk-6LMTBNLN.js} +3 -3
- package/dist/{chunk-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
- package/dist/{chunk-JMBIGH4H.cjs → chunk-7VBEOGCR.cjs} +83 -83
- package/dist/{chunk-JMBIGH4H.cjs.map → chunk-7VBEOGCR.cjs.map} +1 -1
- package/dist/{chunk-5AZZQQYS.cjs → chunk-AWLFJMOJ.cjs} +40 -40
- package/dist/{chunk-5AZZQQYS.cjs.map → chunk-AWLFJMOJ.cjs.map} +1 -1
- package/dist/{chunk-RJ3D3PE2.cjs → chunk-DQDPDUI3.cjs} +9 -9
- package/dist/{chunk-RJ3D3PE2.cjs.map → chunk-DQDPDUI3.cjs.map} +1 -1
- package/dist/{chunk-2RLI5XVI.cjs → chunk-F2YI5N7E.cjs} +16 -16
- package/dist/{chunk-2RLI5XVI.cjs.map → chunk-F2YI5N7E.cjs.map} +1 -1
- package/dist/{chunk-LBOWSKGM.js → chunk-FI35LJ3L.js} +5 -5
- package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
- package/dist/chunk-FS3YSNSU.js.map +1 -0
- package/dist/{chunk-RB2EW6IZ.cjs → chunk-GOFTC2HV.cjs} +3690 -3472
- package/dist/chunk-GOFTC2HV.cjs.map +1 -0
- package/dist/{chunk-CTMLY2GN.js → chunk-HGZPH7S5.js} +5 -5
- package/dist/{chunk-7VUKIWF6.cjs → chunk-HIV2GIFP.cjs} +3 -3
- package/dist/{chunk-7VUKIWF6.cjs.map → chunk-HIV2GIFP.cjs.map} +1 -1
- package/dist/{chunk-DBR5JLX3.js → chunk-IHNUMRMU.js} +46 -23
- package/dist/chunk-IHNUMRMU.js.map +1 -0
- package/dist/{chunk-R4HKB3YD.cjs → chunk-IIPBTWQR.cjs} +22 -22
- package/dist/{chunk-R4HKB3YD.cjs.map → chunk-IIPBTWQR.cjs.map} +1 -1
- package/dist/{chunk-TBGSQTJK.js → chunk-IK2WRYVA.js} +6 -6
- package/dist/{chunk-WSAPKKDU.cjs → chunk-JIW5JI3E.cjs} +9 -9
- package/dist/{chunk-WSAPKKDU.cjs.map → chunk-JIW5JI3E.cjs.map} +1 -1
- package/dist/{chunk-MLGGF24T.js → chunk-JKAX5K7C.js} +8 -8
- package/dist/{chunk-UWPT23SA.js → chunk-JO3NPEK6.js} +3 -3
- package/dist/{chunk-HCZJZMU7.js → chunk-JOFGJYIT.js} +3 -3
- package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
- package/dist/chunk-KI4EGKNV.cjs.map +1 -0
- package/dist/{chunk-OB7YDOTK.cjs → chunk-NJADHL2G.cjs} +8 -8
- package/dist/{chunk-OB7YDOTK.cjs.map → chunk-NJADHL2G.cjs.map} +1 -1
- package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
- package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
- package/dist/{chunk-HX4CJYXL.js → chunk-OT4RV6WY.js} +10 -10
- package/dist/{chunk-Z35QWWJH.cjs → chunk-RJXI25TJ.cjs} +34 -34
- package/dist/{chunk-Z35QWWJH.cjs.map → chunk-RJXI25TJ.cjs.map} +1 -1
- package/dist/{chunk-6TL5XIAW.cjs → chunk-SG25J6LV.cjs} +119 -119
- package/dist/{chunk-6TL5XIAW.cjs.map → chunk-SG25J6LV.cjs.map} +1 -1
- package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
- package/dist/{chunk-FEGEC6FY.js → chunk-V5WLIFKN.js} +5660 -5442
- package/dist/chunk-V5WLIFKN.js.map +1 -0
- package/dist/{chunk-EJ3EBYNV.cjs → chunk-VVXUUBML.cjs} +32 -32
- package/dist/{chunk-EJ3EBYNV.cjs.map → chunk-VVXUUBML.cjs.map} +1 -1
- package/dist/{chunk-SRYV4H3M.js → chunk-VZX3SSFL.js} +3 -3
- package/dist/{chunk-5CDPRCPS.cjs → chunk-WVZHCDB5.cjs} +23 -23
- package/dist/{chunk-5CDPRCPS.cjs.map → chunk-WVZHCDB5.cjs.map} +1 -1
- package/dist/{chunk-VDJF4ZAU.js → chunk-XG7FLAKQ.js} +3 -3
- package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
- package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
- package/dist/{chunk-2AASLY6D.js → chunk-ZIQMVAGA.js} +3 -3
- package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/calendar.js +3 -3
- package/dist/components/case-studies/index.cjs +17 -17
- package/dist/components/case-studies/index.js +7 -7
- package/dist/components/chat/approval-batch-message.d.ts +2 -2
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
- 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/entity-cards/deleted-data-card.d.ts +27 -0
- package/dist/components/chat/entity-cards/deleted-data-card.d.ts.map +1 -0
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-card-item.d.ts +3 -0
- package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/components/chat/index.cjs +13 -7
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +12 -6
- package/dist/components/chat/remark-mention-chips.d.ts +5 -2
- package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
- package/dist/components/chat/remark-strip-citations.d.ts +22 -0
- package/dist/components/chat/remark-strip-citations.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/stream/message-mutations.d.ts +49 -5
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +7 -7
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +11 -4
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +4 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts +48 -0
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts.map +1 -0
- package/dist/components/chat/utils/index.d.ts +2 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -3
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +63 -0
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -0
- package/dist/components/chat/utils/source-row-cta.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +10 -10
- package/dist/components/docs/index.js +9 -9
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/embeds/index.js +7 -7
- package/dist/components/faq/index.cjs +9 -9
- package/dist/components/faq/index.js +8 -8
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/features/index.js +6 -6
- package/dist/components/help-center-pages/index.cjs +28 -28
- package/dist/components/help-center-pages/index.js +19 -19
- package/dist/components/icons/index.cjs +3 -3
- package/dist/components/icons/index.js +2 -2
- package/dist/components/index.cjs +253 -247
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -17
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +92 -92
- package/dist/components/meeting-scheduler/index.js +9 -9
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +11 -11
- package/dist/components/onboarding-guides/index.js +10 -10
- package/dist/components/related-content/index.cjs +9 -9
- package/dist/components/related-content/index.js +8 -8
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +11 -3
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -1
- package/dist/components/shared/doc-search/use-doc-search.d.ts +7 -0
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +11 -11
- package/dist/components/tickets/index.js +10 -10
- package/dist/components/ui/actions-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +70 -70
- package/dist/components/ui/file-manager/index.js +3 -3
- package/dist/components/ui/index.cjs +13 -7
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +12 -6
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/overlay-open-registry.d.ts +50 -0
- package/dist/components/ui/overlay-open-registry.d.ts.map +1 -0
- package/dist/components/ui/portal-container.d.ts +24 -0
- package/dist/components/ui/portal-container.d.ts.map +1 -1
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/index.js +3 -3
- package/dist/index.cjs +20 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -14
- package/dist/utils/anchor-id.d.ts +32 -0
- package/dist/utils/anchor-id.d.ts.map +1 -0
- package/dist/utils/content-href.d.ts +27 -6
- package/dist/utils/content-href.d.ts.map +1 -1
- package/dist/utils/index.cjs +213 -152
- 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 +212 -153
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/same-page-hash-nav.d.ts +17 -6
- package/dist/utils/same-page-hash-nav.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/chat-protocol/decode.ts +43 -0
- package/src/chat-protocol/frames.ts +27 -0
- package/src/components/chat/__tests__/remark-mention-chips.test.ts +67 -0
- package/src/components/chat/approval-batch-message.tsx +2 -2
- package/src/components/chat/chat-message-enhanced.tsx +5 -3
- package/src/components/chat/chat-message-list.tsx +49 -5
- package/src/components/chat/embeddable-chat.tsx +19 -2
- package/src/components/chat/entity-cards/deleted-data-card.tsx +61 -0
- package/src/components/chat/entity-cards/dispatch.tsx +205 -102
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +1 -1
- package/src/components/chat/hooks/use-chat-card-item.ts +15 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +28 -4
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +57 -32
- package/src/components/chat/remark-mention-chips.ts +6 -3
- package/src/components/chat/remark-strip-citations.ts +39 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-approval-batch.test.ts +196 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +67 -2
- package/src/components/chat/stream/message-mutations.ts +170 -40
- package/src/components/chat/types/component.types.ts +9 -7
- package/src/components/chat/types/message.types.ts +14 -4
- package/src/components/chat/types/unified-chat-state.types.ts +4 -1
- package/src/components/chat/utils/__tests__/discuss-ref-prompt.test.ts +67 -0
- package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +156 -0
- package/src/components/chat/utils/discuss-ref-prompt.ts +63 -0
- package/src/components/chat/utils/index.ts +16 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +24 -29
- package/src/components/chat/utils/resolve-fetched-card-href.ts +104 -0
- package/src/components/chat/utils/source-row-cta.ts +5 -1
- package/src/components/shared/doc-search/__tests__/resolve-search-result-action.test.ts +145 -0
- package/src/components/shared/doc-search/resolve-search-result-action.ts +46 -10
- package/src/components/shared/doc-search/use-doc-search.ts +50 -14
- package/src/components/ui/__tests__/overlay-open-registry.test.tsx +87 -0
- package/src/components/ui/actions-menu.tsx +65 -2
- package/src/components/ui/dropdown-menu.tsx +62 -12
- package/src/components/ui/markdown/base-components.tsx +26 -0
- package/src/components/ui/overlay-open-registry.tsx +99 -0
- package/src/components/ui/portal-container.tsx +31 -0
- package/src/utils/__tests__/anchor-id.test.ts +139 -0
- package/src/utils/__tests__/content-href.test.ts +74 -0
- package/src/utils/__tests__/list-url.test.ts +11 -1
- package/src/utils/anchor-id.ts +106 -0
- package/src/utils/content-href.ts +113 -13
- package/src/utils/index.ts +8 -0
- package/src/utils/list-url.ts +6 -1
- package/src/utils/same-page-hash-nav.ts +28 -9
- package/dist/chunk-A62C4C4V.cjs.map +0 -1
- package/dist/chunk-BTUHMCYW.js.map +0 -1
- package/dist/chunk-DBR5JLX3.js.map +0 -1
- package/dist/chunk-EYTXD62B.cjs.map +0 -1
- package/dist/chunk-FEGEC6FY.js.map +0 -1
- package/dist/chunk-GE3JUTW2.js.map +0 -1
- package/dist/chunk-Q4K3TGNP.cjs.map +0 -1
- package/dist/chunk-RB2EW6IZ.cjs.map +0 -1
- package/dist/chunk-XJVRAKEX.cjs.map +0 -1
- /package/dist/{chunk-JOCFPAS6.js.map → chunk-4NFDSMB5.js.map} +0 -0
- /package/dist/{chunk-WNOGR5AA.js.map → chunk-6LMTBNLN.js.map} +0 -0
- /package/dist/{chunk-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
- /package/dist/{chunk-LBOWSKGM.js.map → chunk-FI35LJ3L.js.map} +0 -0
- /package/dist/{chunk-CTMLY2GN.js.map → chunk-HGZPH7S5.js.map} +0 -0
- /package/dist/{chunk-TBGSQTJK.js.map → chunk-IK2WRYVA.js.map} +0 -0
- /package/dist/{chunk-MLGGF24T.js.map → chunk-JKAX5K7C.js.map} +0 -0
- /package/dist/{chunk-UWPT23SA.js.map → chunk-JO3NPEK6.js.map} +0 -0
- /package/dist/{chunk-HCZJZMU7.js.map → chunk-JOFGJYIT.js.map} +0 -0
- /package/dist/{chunk-HX4CJYXL.js.map → chunk-OT4RV6WY.js.map} +0 -0
- /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
- /package/dist/{chunk-SRYV4H3M.js.map → chunk-VZX3SSFL.js.map} +0 -0
- /package/dist/{chunk-VDJF4ZAU.js.map → chunk-XG7FLAKQ.js.map} +0 -0
- /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
- /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
- /package/dist/{chunk-2AASLY6D.js.map → chunk-ZIQMVAGA.js.map} +0 -0
- /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
* intact, and the marker disappears before any rehype-raw HTML pass.
|
|
16
16
|
*
|
|
17
17
|
* Grammar:
|
|
18
|
-
* - marker: `[a-
|
|
18
|
+
* - marker: `[a-zA-Z]+` — the backend `ContextItemType.marker()` short form
|
|
19
19
|
* (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,
|
|
20
|
-
* `query`, …).
|
|
20
|
+
* `query`, `scheduledScript`, …). Mostly lowercase, but NOT guaranteed to be:
|
|
21
|
+
* the marker is matched verbatim by the backend's `MentionParser`, and
|
|
22
|
+
* `SCHEDULED_SCRIPT` ships as camelCase `scheduledScript` — a lowercase-only
|
|
23
|
+
* grammar silently dropped that whole token (no chip, raw text in the bubble).
|
|
21
24
|
* - id: charset `[A-Za-z0-9_.+/=-]` (UUIDs with hyphens, fleet numeric ids,
|
|
22
25
|
* base64 global ids, etc.; matches the composer's `MENTION_TOKEN`) — but the
|
|
23
26
|
* id may NOT END in `.` or `-`, so a trailing SENTENCE period/dash isn't
|
|
@@ -35,7 +38,7 @@ import type { Plugin } from 'unified'
|
|
|
35
38
|
import type { Root, Text, Link } from 'mdast'
|
|
36
39
|
import { visit, SKIP } from 'unist-util-visit'
|
|
37
40
|
|
|
38
|
-
const MENTION_REGEX = /(^|[^\w@])@([a-
|
|
41
|
+
const MENTION_REGEX = /(^|[^\w@])@([a-zA-Z]+):([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
|
|
39
42
|
|
|
40
43
|
export const remarkMentionChips: Plugin<[], Root> = () => {
|
|
41
44
|
return (tree: Root) => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remark plugin that strips bare numeric citation tokens — `[1]`,
|
|
3
|
+
* `[2, 3]` — from assistant chat markdown.
|
|
4
|
+
*
|
|
5
|
+
* RAG models habitually cite retrieved sources with bracketed indices
|
|
6
|
+
* next to entity mentions (`[card://delivery_item:x] [1] **Title**`).
|
|
7
|
+
* The chat surfaces sources as chips and entities as inline cards, so
|
|
8
|
+
* the naked indices are pure noise — and when a card fetch misses they
|
|
9
|
+
* were the ONLY thing left on the line (observed live: candidate lists
|
|
10
|
+
* rendering as "[1]" + bold title).
|
|
11
|
+
*
|
|
12
|
+
* Deliberately conservative:
|
|
13
|
+
* - text leaves only (code/inline-code nodes are never visited);
|
|
14
|
+
* - 1–2 digit indices, optionally comma-separated;
|
|
15
|
+
* - the token must be delimiter-bounded on BOTH sides (start/whitespace
|
|
16
|
+
* before; whitespace/punctuation/end after) so technical prose like
|
|
17
|
+
* `array[1]` or `foo[12]bar` is untouched.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Plugin } from "unified"
|
|
21
|
+
import type { Root, Text } from "mdast"
|
|
22
|
+
import { visit } from "unist-util-visit"
|
|
23
|
+
|
|
24
|
+
const CITATION_REGEX = /(^|\s)\[\d{1,2}(?:\s*,\s*\d{1,2})*\](?=[\s.,;:!?)\]]|$)/g
|
|
25
|
+
|
|
26
|
+
export const remarkStripCitations: Plugin<[], Root> = () => {
|
|
27
|
+
return (tree: Root) => {
|
|
28
|
+
visit(tree, "text", (node: Text) => {
|
|
29
|
+
if (!node.value || !node.value.includes("[")) return
|
|
30
|
+
const withoutCitations = node.value.replace(CITATION_REGEX, "$1")
|
|
31
|
+
// No citation matched → leave the node untouched, INCLUDING its
|
|
32
|
+
// whitespace (collapsing unconditionally would rewrite deliberate
|
|
33
|
+
// spacing in prose like `array[1] value`).
|
|
34
|
+
if (withoutCitations === node.value) return
|
|
35
|
+
// Collapse doubled spaces the removal can leave mid-sentence.
|
|
36
|
+
node.value = withoutCitations.replace(/ {2,}/g, " ")
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-native approval batches on the SSE kernel.
|
|
3
|
+
*
|
|
4
|
+
* The SERVER groups a multi-proposal turn into ONE `approval_batch`
|
|
5
|
+
* frame; the decoder surfaces it as an `approval-request` event
|
|
6
|
+
* carrying `toolCalls` (one row per proposal, row id = proposal id).
|
|
7
|
+
* These tests pin the reducer contracts:
|
|
8
|
+
*
|
|
9
|
+
* - a batch event renders ONE `approval_batch` segment (no per-
|
|
10
|
+
* proposal cards, no adjacency heuristics);
|
|
11
|
+
* - batch onApprove flips status optimistically and resolves EVERY
|
|
12
|
+
* row sequentially through the per-proposal callback;
|
|
13
|
+
* - each `approval-resolved` (per proposal id) ticks that row's
|
|
14
|
+
* execution icon; the anchor row also carries the status flip;
|
|
15
|
+
* - a single-proposal event keeps the classic `approval_request`
|
|
16
|
+
* card (editable per-card flow).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
20
|
+
import { createChatStreamReducer } from '../chat-stream-reducer'
|
|
21
|
+
import { createSseFrameDecoder } from '../../../../chat-protocol/decode'
|
|
22
|
+
import type { ApprovalBatchSegment } from '../../types'
|
|
23
|
+
|
|
24
|
+
const batchEvent = {
|
|
25
|
+
type: 'approval-request' as const,
|
|
26
|
+
// Anchor is DISTINCT from every row's proposal id (server emits
|
|
27
|
+
// `batch:<first proposalId>`) — the optimistic click flip keys on it
|
|
28
|
+
// and must never pre-tick a row's execution.
|
|
29
|
+
requestId: 'batch:prop-1',
|
|
30
|
+
approvalType: 'chat',
|
|
31
|
+
toolCalls: [
|
|
32
|
+
{
|
|
33
|
+
toolExecutionRequestId: 'prop-1',
|
|
34
|
+
toolName: 'delete_clickup_task',
|
|
35
|
+
toolTitle: 'Delete ClickUp task — Fix Website Header',
|
|
36
|
+
requiresApproval: true,
|
|
37
|
+
toolCallArguments: { Task: '86aaa', Title: 'Fix Website Header' },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
toolExecutionRequestId: 'prop-2',
|
|
41
|
+
toolName: 'delete_clickup_task',
|
|
42
|
+
toolTitle: 'Delete ClickUp task — Fix Website Header (2)',
|
|
43
|
+
requiresApproval: true,
|
|
44
|
+
toolCallArguments: { Task: '86bbb', Title: 'Fix Website Header' },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findBatch(r: ReturnType<typeof createChatStreamReducer>): ApprovalBatchSegment {
|
|
50
|
+
const trailing = r.state.messages[r.state.messages.length - 1]
|
|
51
|
+
const batch = trailing.segments?.find(
|
|
52
|
+
(s): s is ApprovalBatchSegment => s.type === 'approval_batch',
|
|
53
|
+
)
|
|
54
|
+
expect(batch).toBeTruthy()
|
|
55
|
+
return batch as ApprovalBatchSegment
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('SSE kernel — wire-native approval batch', () => {
|
|
59
|
+
it('renders ONE approval_batch segment for a toolCalls event', () => {
|
|
60
|
+
const r = createChatStreamReducer({ transport: 'sse' })
|
|
61
|
+
r.beginSseSend({ text: 'delete both', assistantName: 'Mingo AI' })
|
|
62
|
+
r.apply(batchEvent)
|
|
63
|
+
const trailing = r.state.messages[r.state.messages.length - 1]
|
|
64
|
+
expect(
|
|
65
|
+
trailing.segments?.filter((s) => s.type === 'approval_request').length ?? 0,
|
|
66
|
+
).toBe(0)
|
|
67
|
+
const batch = findBatch(r)
|
|
68
|
+
expect(batch.status).toBe('pending')
|
|
69
|
+
expect(batch.data.approvalRequestId).toBe('batch:prop-1')
|
|
70
|
+
expect(batch.data.toolCalls.map((c) => c.toolExecutionRequestId)).toEqual([
|
|
71
|
+
'prop-1',
|
|
72
|
+
'prop-2',
|
|
73
|
+
])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('onApprove flips status optimistically and resolves every row sequentially', async () => {
|
|
77
|
+
const onApprove = vi.fn().mockResolvedValue(undefined)
|
|
78
|
+
const r = createChatStreamReducer({ transport: 'sse', callbacks: { onApprove } })
|
|
79
|
+
r.beginSseSend({ text: 'delete both', assistantName: 'Mingo AI' })
|
|
80
|
+
r.apply(batchEvent)
|
|
81
|
+
const batch = findBatch(r)
|
|
82
|
+
await batch.onApprove?.(batch.data.approvalRequestId)
|
|
83
|
+
expect(onApprove.mock.calls.map((c) => c[0])).toEqual(['prop-1', 'prop-2'])
|
|
84
|
+
const after = findBatch(r)
|
|
85
|
+
expect(after.status).toBe('approved')
|
|
86
|
+
// REGRESSION PIN: the optimistic flip must NOT pre-tick any row's
|
|
87
|
+
// execution — a pre-ticked green check would mask a failed confirm
|
|
88
|
+
// (observed when the anchor doubled as row 1's proposal id).
|
|
89
|
+
expect(after.data.executions ?? {}).toEqual({})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('a failed row confirm (false) ticks that row cross, others still attempted', async () => {
|
|
93
|
+
const onApprove = vi
|
|
94
|
+
.fn()
|
|
95
|
+
.mockResolvedValueOnce(false) // row 1 confirm fails (e.g. expired)
|
|
96
|
+
.mockResolvedValueOnce(true)
|
|
97
|
+
const r = createChatStreamReducer({ transport: 'sse', callbacks: { onApprove } })
|
|
98
|
+
r.beginSseSend({ text: 'delete both', assistantName: 'Mingo AI' })
|
|
99
|
+
r.apply(batchEvent)
|
|
100
|
+
const batch = findBatch(r)
|
|
101
|
+
await batch.onApprove?.(batch.data.approvalRequestId)
|
|
102
|
+
expect(onApprove).toHaveBeenCalledTimes(2)
|
|
103
|
+
const after = findBatch(r)
|
|
104
|
+
expect(after.data.executions?.['prop-1']).toEqual({ status: 'done', success: false })
|
|
105
|
+
expect(after.data.executions?.['prop-2']).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('per-proposal approval-resolved events tick the matching row execution', () => {
|
|
109
|
+
const r = createChatStreamReducer({ transport: 'sse' })
|
|
110
|
+
r.beginSseSend({ text: 'delete both', assistantName: 'Mingo AI' })
|
|
111
|
+
r.apply(batchEvent)
|
|
112
|
+
r.apply({ type: 'approval-resolved', requestId: 'prop-2', status: 'approved', ok: true })
|
|
113
|
+
let batch = findBatch(r)
|
|
114
|
+
expect(batch.data.executions?.['prop-2']).toEqual({ status: 'done', success: true })
|
|
115
|
+
expect(batch.data.executions?.['prop-1']).toBeUndefined()
|
|
116
|
+
r.apply({ type: 'approval-resolved', requestId: 'prop-1', status: 'approved', ok: true })
|
|
117
|
+
batch = findBatch(r)
|
|
118
|
+
expect(batch.data.executions?.['prop-1']).toEqual({ status: 'done', success: true })
|
|
119
|
+
// Status flips at CLICK time (anchor never matches a row id, and no
|
|
120
|
+
// decision frame carries the anchor) — row resolutions alone leave
|
|
121
|
+
// the pending status untouched.
|
|
122
|
+
expect(batch.status).toBe('pending')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('a single proposal keeps the classic approval_request card', () => {
|
|
126
|
+
const r = createChatStreamReducer({ transport: 'sse' })
|
|
127
|
+
r.beginSseSend({ text: 'delete it', assistantName: 'Mingo AI' })
|
|
128
|
+
r.apply({
|
|
129
|
+
type: 'approval-request',
|
|
130
|
+
requestId: 'prop-solo',
|
|
131
|
+
approvalType: 'delete_clickup_task',
|
|
132
|
+
command: 'Delete ClickUp task',
|
|
133
|
+
fields: [{ label: 'Task', value: '86ccc' }],
|
|
134
|
+
})
|
|
135
|
+
const trailing = r.state.messages[r.state.messages.length - 1]
|
|
136
|
+
expect(trailing.segments?.some((s) => s.type === 'approval_batch')).toBe(false)
|
|
137
|
+
expect(trailing.segments?.some((s) => s.type === 'approval_request')).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('decoder — approval_batch frame mapping', () => {
|
|
142
|
+
it('maps the wire frame to ONE batched approval-request event', () => {
|
|
143
|
+
const frame = {
|
|
144
|
+
kind: 'approval_batch',
|
|
145
|
+
batchId: 'batch:prop-1',
|
|
146
|
+
proposals: [
|
|
147
|
+
{
|
|
148
|
+
proposalId: 'prop-1',
|
|
149
|
+
toolName: 'delete_clickup_task',
|
|
150
|
+
title: 'Delete ClickUp task',
|
|
151
|
+
fields: [
|
|
152
|
+
{ label: 'Task', value: '86aaa' },
|
|
153
|
+
{ label: 'Title', value: 'Fix Website Header' },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
proposalId: 'prop-2',
|
|
158
|
+
toolName: 'delete_clickup_task',
|
|
159
|
+
title: 'Delete ClickUp task',
|
|
160
|
+
fields: [{ label: 'Task', value: '86bbb' }],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
const decoder = createSseFrameDecoder()
|
|
165
|
+
const bytes = new TextEncoder().encode(JSON.stringify(frame) + '\0')
|
|
166
|
+
const events = decoder.push(bytes)
|
|
167
|
+
const approval = events.find((e) => e.type === 'approval-request') as
|
|
168
|
+
| { requestId: string; toolCalls?: Array<Record<string, unknown>> }
|
|
169
|
+
| undefined
|
|
170
|
+
expect(approval).toBeTruthy()
|
|
171
|
+
expect(approval!.requestId).toBe('batch:prop-1')
|
|
172
|
+
expect(approval!.toolCalls?.length).toBe(2)
|
|
173
|
+
expect(approval!.toolCalls?.[0].toolExecutionRequestId).toBe('prop-1')
|
|
174
|
+
// Title field drives the row's disambiguating toolTitle.
|
|
175
|
+
expect(approval!.toolCalls?.[0].toolTitle).toContain('Fix Website Header')
|
|
176
|
+
expect(approval!.toolCalls?.[1].toolExecutionRequestId).toBe('prop-2')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('a colliding anchor id (misbehaving server) still cannot pre-tick a row execution', async () => {
|
|
180
|
+
// Defense-in-depth pin: the click-time flip uses the STATUS-ONLY
|
|
181
|
+
// projection, so even a server that (incorrectly) reuses a row's
|
|
182
|
+
// proposal id as the batchId cannot mark that row done before its
|
|
183
|
+
// confirm actually resolves.
|
|
184
|
+
const onApprove = vi.fn().mockResolvedValue(undefined)
|
|
185
|
+
const r = createChatStreamReducer({ transport: 'sse', callbacks: { onApprove } })
|
|
186
|
+
r.beginSseSend({ text: 'delete both', assistantName: 'Mingo AI' })
|
|
187
|
+
r.apply({ ...batchEvent, requestId: 'prop-1' })
|
|
188
|
+
const batch = findBatch(r)
|
|
189
|
+
await batch.onApprove?.(batch.data.approvalRequestId)
|
|
190
|
+
const after = findBatch(r)
|
|
191
|
+
expect(after.status).toBe('approved')
|
|
192
|
+
// The flip itself wrote NO execution for prop-1 — its icon state
|
|
193
|
+
// comes only from the confirm result / approval-resolved event.
|
|
194
|
+
expect(after.data.executions ?? {}).toEqual({})
|
|
195
|
+
})
|
|
196
|
+
})
|
|
@@ -60,6 +60,7 @@ import { parseScrollAnchor, type ScrollAnchor } from '../utils/scroll-anchor'
|
|
|
60
60
|
import { escapeThinkingTags } from '../../../chat-protocol/decode'
|
|
61
61
|
import { buildChatRefKey } from '../types/chat.types'
|
|
62
62
|
import type {
|
|
63
|
+
ApprovalBatchSegment,
|
|
63
64
|
ApprovalRequestSegment,
|
|
64
65
|
ChatApprovalStatus,
|
|
65
66
|
MessageSegment,
|
|
@@ -93,6 +94,8 @@ import {
|
|
|
93
94
|
mergeToolExecutionIfPresent,
|
|
94
95
|
nextId,
|
|
95
96
|
projectApprovalResolutionToMessages,
|
|
97
|
+
projectBatchRowFailureToMessages,
|
|
98
|
+
projectBatchStatusToMessages,
|
|
96
99
|
updateTrailingAssistant,
|
|
97
100
|
upsertTrailingCompaction,
|
|
98
101
|
} from './message-mutations'
|
|
@@ -1526,12 +1529,68 @@ export function createChatStreamReducer(
|
|
|
1526
1529
|
onReject: callbacks.onReject,
|
|
1527
1530
|
}
|
|
1528
1531
|
const last = messages[messages.length - 1]
|
|
1529
|
-
if (last
|
|
1532
|
+
if (!last || last.role !== 'assistant') break
|
|
1533
|
+
// ── WIRE-NATIVE BATCH ─────────────────────────────────────────
|
|
1534
|
+
// The SERVER groups a multi-proposal turn into ONE
|
|
1535
|
+
// `approval_batch` frame (decoded to `event.toolCalls`) — the
|
|
1536
|
+
// client performs NO adjacency heuristics. Render the bulk-
|
|
1537
|
+
// approval component: one Approve/Reject resolving every row
|
|
1538
|
+
// sequentially through its own per-proposal confirm; per-row
|
|
1539
|
+
// execution icons ticked by each `approval-resolved` event
|
|
1540
|
+
// (see `projectApprovalResolutionToMessages`). Single-proposal
|
|
1541
|
+
// turns keep the classic editable per-card flow below.
|
|
1542
|
+
if (event.toolCalls && event.toolCalls.length > 0) {
|
|
1543
|
+
const rows = event.toolCalls
|
|
1544
|
+
const anchorId = event.requestId
|
|
1545
|
+
const ids = rows.map((c) => c.toolExecutionRequestId)
|
|
1546
|
+
const batchSegment: ApprovalBatchSegment = {
|
|
1547
|
+
type: 'approval_batch',
|
|
1548
|
+
status: 'pending',
|
|
1549
|
+
data: {
|
|
1550
|
+
approvalRequestId: anchorId,
|
|
1551
|
+
approvalType: event.approvalType ?? 'chat',
|
|
1552
|
+
toolCalls: rows,
|
|
1553
|
+
executions: {},
|
|
1554
|
+
},
|
|
1555
|
+
// Optimistic status flip so the per-row loaders show while
|
|
1556
|
+
// the sequential per-proposal confirms run (the anchor id is
|
|
1557
|
+
// NEVER a row id — see ApprovalBatchFrame.batchId — so the
|
|
1558
|
+
// flip cannot pre-tick a row's execution). A confirm that
|
|
1559
|
+
// reports failure (`false` — expired proposal, network
|
|
1560
|
+
// error) ticks THAT row's cross so no loader spins forever;
|
|
1561
|
+
// remaining rows still get their attempt.
|
|
1562
|
+
...(() => {
|
|
1563
|
+
const resolveBatch =
|
|
1564
|
+
(status: 'approved' | 'rejected', confirm: typeof callbacks.onApprove) =>
|
|
1565
|
+
async () => {
|
|
1566
|
+
// STATUS-ONLY projection (defense-in-depth): even a
|
|
1567
|
+
// batchId colliding with a row id cannot pre-tick an
|
|
1568
|
+
// execution here — rows resolve exclusively via their
|
|
1569
|
+
// own confirm results / approval-resolved events.
|
|
1570
|
+
setMessagesInternal(projectBatchStatusToMessages(messages, anchorId, status))
|
|
1571
|
+
for (const id of ids) {
|
|
1572
|
+
const ok = await confirm?.(id)
|
|
1573
|
+
if (ok === false) {
|
|
1574
|
+
setMessagesInternal(projectBatchRowFailureToMessages(messages, id))
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return {
|
|
1579
|
+
onApprove: resolveBatch('approved', callbacks.onApprove),
|
|
1580
|
+
onReject: resolveBatch('rejected', callbacks.onReject),
|
|
1581
|
+
}
|
|
1582
|
+
})(),
|
|
1583
|
+
}
|
|
1530
1584
|
setMessagesInternal([
|
|
1531
1585
|
...messages.slice(0, -1),
|
|
1532
|
-
{ ...last, segments: [...(last.segments ?? []),
|
|
1586
|
+
{ ...last, segments: [...(last.segments ?? []), batchSegment] },
|
|
1533
1587
|
])
|
|
1588
|
+
break
|
|
1534
1589
|
}
|
|
1590
|
+
setMessagesInternal([
|
|
1591
|
+
...messages.slice(0, -1),
|
|
1592
|
+
{ ...last, segments: [...(last.segments ?? []), segment] },
|
|
1593
|
+
])
|
|
1535
1594
|
break
|
|
1536
1595
|
}
|
|
1537
1596
|
case 'approval-resolved':
|
|
@@ -1919,6 +1978,12 @@ export function createChatStreamReducer(
|
|
|
1919
1978
|
setMessagesInternal(
|
|
1920
1979
|
projectApprovalResolutionToMessages(messages, requestId, status, resolvedByName),
|
|
1921
1980
|
)
|
|
1981
|
+
// Status-map mirror — kept in lockstep with the other two flip
|
|
1982
|
+
// paths (SSE approval-resolved at ~1240, NATS at ~1950): the map
|
|
1983
|
+
// restores status when a replayed approval-request event
|
|
1984
|
+
// re-renders the card, and a flip applied through THIS path must
|
|
1985
|
+
// survive that replay the same way.
|
|
1986
|
+
mirrorApprovalStatus(requestId, status)
|
|
1922
1987
|
return segments
|
|
1923
1988
|
},
|
|
1924
1989
|
getPendingEscalated() {
|
|
@@ -134,6 +134,162 @@ export function appendToTrailingAssistant(
|
|
|
134
134
|
return [...prev.slice(0, -1), { ...last, segments: merged }]
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// Segment-projection kernel — shared scaffolding + single-rule predicates
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Map every assistant message's segments through `fn`, preserving the
|
|
143
|
+
* REFERENTIAL-STABILITY CONTRACT in ONE place: `fn` returns the SAME
|
|
144
|
+
* segment reference to signal "unchanged"; untouched messages (and the
|
|
145
|
+
* whole array on a global no-op) keep their prior references. Every
|
|
146
|
+
* segment projection below rides this scaffold instead of re-implementing
|
|
147
|
+
* the changed-flag dance.
|
|
148
|
+
*/
|
|
149
|
+
function mapSegments(
|
|
150
|
+
prev: UnifiedChatMessage[],
|
|
151
|
+
fn: (s: MessageSegment) => MessageSegment,
|
|
152
|
+
): UnifiedChatMessage[] {
|
|
153
|
+
let changed = false
|
|
154
|
+
const next = prev.map((m) => {
|
|
155
|
+
if (m.role !== 'assistant' || !m.segments) return m
|
|
156
|
+
let msgChanged = false
|
|
157
|
+
const segs = m.segments.map((s) => {
|
|
158
|
+
const out = fn(s)
|
|
159
|
+
if (out !== s) msgChanged = true
|
|
160
|
+
return out
|
|
161
|
+
})
|
|
162
|
+
if (!msgChanged) return m
|
|
163
|
+
changed = true
|
|
164
|
+
return { ...m, segments: segs }
|
|
165
|
+
})
|
|
166
|
+
return changed ? next : prev
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* THE approval-resolution rule for a single segment — one predicate for
|
|
171
|
+
* both containers (the message-array projections here AND the flat
|
|
172
|
+
* segment list in `MessageSegmentAccumulator.updateApprovalStatus`), so
|
|
173
|
+
* the two paths can never drift:
|
|
174
|
+
* - `approval_request` matched on `data.requestId` → status flip;
|
|
175
|
+
* - `approval_batch` anchor (`data.approvalRequestId`, the server's
|
|
176
|
+
* `batch:<first proposalId>` — never a row id) → status flip;
|
|
177
|
+
* - `approval_batch` ROW (`toolCalls[].toolExecutionRequestId`) →
|
|
178
|
+
* tick that row's execution (check on approved, cross otherwise)
|
|
179
|
+
* without touching the batch status.
|
|
180
|
+
* Returns the SAME reference when nothing matched or changed.
|
|
181
|
+
*/
|
|
182
|
+
export function applyApprovalStatusToSegment(
|
|
183
|
+
s: MessageSegment,
|
|
184
|
+
requestId: string,
|
|
185
|
+
status: ChatApprovalStatus,
|
|
186
|
+
resolvedByName?: string | null,
|
|
187
|
+
): MessageSegment {
|
|
188
|
+
if (s.type === 'approval_request' && s.data.requestId === requestId && s.status !== status) {
|
|
189
|
+
return { ...s, status }
|
|
190
|
+
}
|
|
191
|
+
if (s.type !== 'approval_batch') return s
|
|
192
|
+
const isAnchor = s.data.approvalRequestId === requestId
|
|
193
|
+
const hasRow = s.data.toolCalls?.some((c) => c.toolExecutionRequestId === requestId)
|
|
194
|
+
if (!isAnchor && !hasRow) return s
|
|
195
|
+
const nextResolvedBy = resolvedByName ?? s.resolvedByName
|
|
196
|
+
const nextExecutions = hasRow
|
|
197
|
+
? {
|
|
198
|
+
...(s.data.executions ?? {}),
|
|
199
|
+
[requestId]: { status: 'done' as const, success: status === 'approved' },
|
|
200
|
+
}
|
|
201
|
+
: s.data.executions
|
|
202
|
+
const nextStatus = isAnchor ? status : s.status
|
|
203
|
+
if (
|
|
204
|
+
s.status === nextStatus &&
|
|
205
|
+
nextResolvedBy === s.resolvedByName &&
|
|
206
|
+
nextExecutions === s.data.executions
|
|
207
|
+
) {
|
|
208
|
+
return s
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
...s,
|
|
212
|
+
status: nextStatus,
|
|
213
|
+
resolvedByName: nextResolvedBy,
|
|
214
|
+
data: {
|
|
215
|
+
...s.data,
|
|
216
|
+
...(nextExecutions ? { executions: nextExecutions } : {}),
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Next state for a batch row's execution slot from a tool-execution
|
|
223
|
+
* chunk — folds the never-downgrade guard (redelivered EXECUTING after
|
|
224
|
+
* EXECUTED landed) and the EXECUTED/EXECUTING ternary into ONE rule
|
|
225
|
+
* shared by the message projection and the accumulator. Returns null
|
|
226
|
+
* for the no-op case (caller keeps prior references).
|
|
227
|
+
*/
|
|
228
|
+
export function nextBatchExecution(
|
|
229
|
+
prevExec: ApprovalBatchExecutionState | undefined,
|
|
230
|
+
toolData: ToolExecutionData,
|
|
231
|
+
): ApprovalBatchExecutionState | null {
|
|
232
|
+
if (toolData.type === 'EXECUTING_TOOL' && prevExec?.status === 'done') return null
|
|
233
|
+
return toolData.type === 'EXECUTED_TOOL'
|
|
234
|
+
? { status: 'done', result: toolData.result, success: toolData.success }
|
|
235
|
+
: { status: 'executing', result: prevExec?.result, success: prevExec?.success }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The executions-map triple-spread — one home for the write shape. */
|
|
239
|
+
export function withBatchExecution<S extends { data: { executions?: Record<string, ApprovalBatchExecutionState> } }>(
|
|
240
|
+
seg: S,
|
|
241
|
+
execId: string,
|
|
242
|
+
state: ApprovalBatchExecutionState,
|
|
243
|
+
): S {
|
|
244
|
+
return {
|
|
245
|
+
...seg,
|
|
246
|
+
data: { ...seg.data, executions: { ...(seg.data.executions ?? {}), [execId]: state } },
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Flip ONLY an approval_batch segment's status — no execution writes.
|
|
252
|
+
* The click-time optimistic flip uses this instead of
|
|
253
|
+
* `projectApprovalResolutionToMessages` as defense-in-depth: even if a
|
|
254
|
+
* (misbehaving) server emitted a batchId colliding with a row's
|
|
255
|
+
* proposal id, the flip could not pre-tick that row's execution to a
|
|
256
|
+
* false success before its confirm actually ran.
|
|
257
|
+
*/
|
|
258
|
+
export function projectBatchStatusToMessages(
|
|
259
|
+
prev: UnifiedChatMessage[],
|
|
260
|
+
anchorId: string,
|
|
261
|
+
status: ChatApprovalStatus,
|
|
262
|
+
): UnifiedChatMessage[] {
|
|
263
|
+
return mapSegments(prev, (s) => {
|
|
264
|
+
if (s.type !== 'approval_batch') return s
|
|
265
|
+
if (s.data.approvalRequestId !== anchorId || s.status === status) return s
|
|
266
|
+
return { ...s, status }
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Mark ONE batch row's confirm as FAILED (expired proposal, network
|
|
272
|
+
* error): tick its execution icon to the failure cross so the row's
|
|
273
|
+
* loader doesn't spin forever. Batch status is untouched — other rows
|
|
274
|
+
* may still be resolving.
|
|
275
|
+
*/
|
|
276
|
+
export function projectBatchRowFailureToMessages(
|
|
277
|
+
prev: UnifiedChatMessage[],
|
|
278
|
+
rowRequestId: string,
|
|
279
|
+
): UnifiedChatMessage[] {
|
|
280
|
+
return mapSegments(prev, (s) => {
|
|
281
|
+
if (
|
|
282
|
+
s.type !== 'approval_batch' ||
|
|
283
|
+
!s.data.toolCalls?.some((c) => c.toolExecutionRequestId === rowRequestId)
|
|
284
|
+
) {
|
|
285
|
+
return s
|
|
286
|
+
}
|
|
287
|
+
const existing = s.data.executions?.[rowRequestId]
|
|
288
|
+
if (existing?.status === 'done') return s
|
|
289
|
+
return withBatchExecution(s, rowRequestId, { status: 'done', success: false })
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
137
293
|
/**
|
|
138
294
|
* Upsert a standalone context-compaction segment into the trailing assistant
|
|
139
295
|
* bubble. Compaction emissions arrive as the accumulator's CUMULATIVE array —
|
|
@@ -242,23 +398,16 @@ export function mergeToolExecutionIfPresent(
|
|
|
242
398
|
seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId)
|
|
243
399
|
) {
|
|
244
400
|
const prevExec: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
|
|
245
|
-
//
|
|
246
|
-
// redelivery of
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const nextExec: ApprovalBatchExecutionState =
|
|
252
|
-
toolData.type === 'EXECUTED_TOOL'
|
|
253
|
-
? { status: 'done', result: toolData.result, success: toolData.success }
|
|
254
|
-
: { status: 'executing', result: prevExec?.result, success: prevExec?.success }
|
|
401
|
+
// Shared rule (`nextBatchExecution`): folds the never-downgrade
|
|
402
|
+
// guard (JetStream redelivery of EXECUTING after EXECUTED
|
|
403
|
+
// landed — null = matched-with-no-change, caller doesn't append
|
|
404
|
+
// either) and the EXECUTED/EXECUTING ternary.
|
|
405
|
+
const nextExec = nextBatchExecution(prevExec, toolData)
|
|
406
|
+
if (nextExec === null) return prev
|
|
255
407
|
// Value-level no-op (replayed duplicate) → prior references.
|
|
256
408
|
if (sameExecutionState(prevExec, nextExec)) return prev
|
|
257
409
|
const nextSegments = [...message.segments]
|
|
258
|
-
nextSegments[j] =
|
|
259
|
-
...seg,
|
|
260
|
-
data: { ...seg.data, executions: { ...(seg.data.executions ?? {}), [execId]: nextExec } },
|
|
261
|
-
}
|
|
410
|
+
nextSegments[j] = withBatchExecution(seg, execId, nextExec)
|
|
262
411
|
const next = [...prev]
|
|
263
412
|
next[i] = { ...message, segments: nextSegments }
|
|
264
413
|
return next
|
|
@@ -304,10 +453,10 @@ export function mergeToolExecutionIfPresent(
|
|
|
304
453
|
}
|
|
305
454
|
|
|
306
455
|
/**
|
|
307
|
-
*
|
|
308
|
-
* `
|
|
309
|
-
*
|
|
310
|
-
*
|
|
456
|
+
* Project an approval resolution onto the thread — the ONE rule lives in
|
|
457
|
+
* `applyApprovalStatusToSegment` (shared with the accumulator's flat
|
|
458
|
+
* segment list); this is just its message-array projection via
|
|
459
|
+
* `mapSegments`.
|
|
311
460
|
*/
|
|
312
461
|
export function projectApprovalResolutionToMessages(
|
|
313
462
|
prev: UnifiedChatMessage[],
|
|
@@ -315,26 +464,7 @@ export function projectApprovalResolutionToMessages(
|
|
|
315
464
|
status: ChatApprovalStatus,
|
|
316
465
|
resolvedByName?: string | null,
|
|
317
466
|
): UnifiedChatMessage[] {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
let msgChanged = false
|
|
322
|
-
const segs = m.segments.map((s) => {
|
|
323
|
-
if (s.type === 'approval_request' && s.data.requestId === requestId && s.status !== status) {
|
|
324
|
-
msgChanged = true
|
|
325
|
-
return { ...s, status }
|
|
326
|
-
}
|
|
327
|
-
if (s.type === 'approval_batch' && s.data.approvalRequestId === requestId) {
|
|
328
|
-
const nextResolvedBy = resolvedByName ?? s.resolvedByName
|
|
329
|
-
if (s.status === status && nextResolvedBy === s.resolvedByName) return s
|
|
330
|
-
msgChanged = true
|
|
331
|
-
return { ...s, status, resolvedByName: nextResolvedBy }
|
|
332
|
-
}
|
|
333
|
-
return s
|
|
334
|
-
})
|
|
335
|
-
if (!msgChanged) return m
|
|
336
|
-
changed = true
|
|
337
|
-
return { ...m, segments: segs }
|
|
338
|
-
})
|
|
339
|
-
return changed ? next : prev
|
|
467
|
+
return mapSegments(prev, (s) =>
|
|
468
|
+
applyApprovalStatusToSegment(s, requestId, status, resolvedByName),
|
|
469
|
+
)
|
|
340
470
|
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ComponentType, HTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react'
|
|
6
6
|
import type { ApprovalBlockVariant, AssistantType, AuthorType, ChatApprovalStatus, ConnectionStatus } from './chat.types'
|
|
7
|
-
import type { ApprovalRequestData, Message, MessageSegment, ToolExecutionData
|
|
7
|
+
import type { ApprovalRequestData, Message, MessageSegment, ToolExecutionData,
|
|
8
|
+
ApprovalResolutionHandler,
|
|
9
|
+
} from './message.types'
|
|
8
10
|
import type { ChatRef } from '../chat-ref.types'
|
|
9
11
|
import type { ChatContextItem } from './context-item.types'
|
|
10
12
|
import type { MspOrganizationCardProps } from '../msp-organization-card'
|
|
@@ -129,8 +131,8 @@ export interface ChatMessageEnhancedProps extends Omit<HTMLAttributes<HTMLDivEle
|
|
|
129
131
|
timestamp?: Date
|
|
130
132
|
showAvatar?: boolean
|
|
131
133
|
isTyping?: boolean
|
|
132
|
-
onApprove?:
|
|
133
|
-
onReject?:
|
|
134
|
+
onApprove?: ApprovalResolutionHandler
|
|
135
|
+
onReject?: ApprovalResolutionHandler
|
|
134
136
|
/**
|
|
135
137
|
* Per-row metadata for inline entity-card rendering (v6.1 §B.2.6+§B.2.7).
|
|
136
138
|
* Keyed by `<documentType>:<primaryKey>`. When present AND
|
|
@@ -259,8 +261,8 @@ export interface ChatMessageListProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
259
261
|
approvalVariant?: ApprovalBlockVariant
|
|
260
262
|
assistantIcon?: React.ReactNode
|
|
261
263
|
pendingApprovals?: MessageSegment[]
|
|
262
|
-
onApprove?:
|
|
263
|
-
onReject?:
|
|
264
|
+
onApprove?: ApprovalResolutionHandler
|
|
265
|
+
onReject?: ApprovalResolutionHandler
|
|
264
266
|
// Infinite scroll for loading older messages
|
|
265
267
|
hasNextPage?: boolean
|
|
266
268
|
isFetchingNextPage?: boolean
|
|
@@ -518,8 +520,8 @@ export interface ToolExecutionDisplayProps extends HTMLAttributes<HTMLDivElement
|
|
|
518
520
|
|
|
519
521
|
export interface ApprovalRequestMessageProps extends HTMLAttributes<HTMLDivElement> {
|
|
520
522
|
data: ApprovalRequestData
|
|
521
|
-
onApprove?:
|
|
522
|
-
onReject?:
|
|
523
|
+
onApprove?: ApprovalResolutionHandler
|
|
524
|
+
onReject?: ApprovalResolutionHandler
|
|
523
525
|
status?: ChatApprovalStatus
|
|
524
526
|
disabled?: boolean
|
|
525
527
|
/** Chat identity. Accepted for parity with the batch card; does NOT drive
|