@flamingo-stack/openframe-frontend-core 0.0.512-1640.4221.1 → 0.0.512
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-OOJLER5V.cjs → chunk-2WWDZR4D.cjs} +31 -31
- package/dist/{chunk-OOJLER5V.cjs.map → chunk-2WWDZR4D.cjs.map} +1 -1
- package/dist/{chunk-N3CQTN5T.cjs → chunk-2XL7UK5Y.cjs} +5 -5
- package/dist/{chunk-N3CQTN5T.cjs.map → chunk-2XL7UK5Y.cjs.map} +1 -1
- package/dist/{chunk-JEKWIGG6.js → chunk-4NFDSMB5.js} +4 -4
- package/dist/{chunk-BXTVXQGZ.cjs → chunk-5JBDT2MQ.cjs} +7 -7
- package/dist/{chunk-BXTVXQGZ.cjs.map → chunk-5JBDT2MQ.cjs.map} +1 -1
- package/dist/{chunk-GVMHJK3B.cjs → chunk-67YDSTH7.cjs} +9 -9
- package/dist/{chunk-GVMHJK3B.cjs.map → chunk-67YDSTH7.cjs.map} +1 -1
- package/dist/{chunk-XYD3Y7WU.js → chunk-6LMTBNLN.js} +2 -2
- package/dist/{chunk-ZLCYLZFL.cjs → chunk-7VBEOGCR.cjs} +61 -61
- package/dist/{chunk-ZLCYLZFL.cjs.map → chunk-7VBEOGCR.cjs.map} +1 -1
- package/dist/{chunk-OEPGR7FT.cjs → chunk-AWLFJMOJ.cjs} +37 -37
- package/dist/{chunk-OEPGR7FT.cjs.map → chunk-AWLFJMOJ.cjs.map} +1 -1
- package/dist/{chunk-UXEVYNSX.cjs → chunk-DQDPDUI3.cjs} +4 -4
- package/dist/{chunk-UXEVYNSX.cjs.map → chunk-DQDPDUI3.cjs.map} +1 -1
- package/dist/{chunk-MWJARB2T.cjs → chunk-F2YI5N7E.cjs} +13 -13
- package/dist/{chunk-MWJARB2T.cjs.map → chunk-F2YI5N7E.cjs.map} +1 -1
- package/dist/{chunk-BH24GEIY.js → chunk-FI35LJ3L.js} +2 -2
- package/dist/{chunk-PDPNJJLB.cjs → chunk-GOFTC2HV.cjs} +431 -489
- package/dist/chunk-GOFTC2HV.cjs.map +1 -0
- package/dist/{chunk-OYYHTO7U.js → chunk-HGZPH7S5.js} +2 -2
- package/dist/{chunk-VIXKP7G7.js → chunk-IHNUMRMU.js} +2 -2
- package/dist/{chunk-MCFMOSY6.js → chunk-IK2WRYVA.js} +4 -4
- package/dist/{chunk-ORNBZJUZ.cjs → chunk-JIW5JI3E.cjs} +3 -3
- package/dist/{chunk-ORNBZJUZ.cjs.map → chunk-JIW5JI3E.cjs.map} +1 -1
- package/dist/{chunk-MSMSMXST.js → chunk-JKAX5K7C.js} +5 -5
- package/dist/{chunk-AXS2XECN.js → chunk-JO3NPEK6.js} +2 -2
- package/dist/{chunk-NLGPA3VY.js → chunk-JOFGJYIT.js} +2 -2
- package/dist/{chunk-PPVWJQTF.js → chunk-OT4RV6WY.js} +6 -6
- package/dist/{chunk-Z3MUSW3I.cjs → chunk-RJXI25TJ.cjs} +26 -26
- package/dist/{chunk-Z3MUSW3I.cjs.map → chunk-RJXI25TJ.cjs.map} +1 -1
- package/dist/{chunk-SEZR7FMU.cjs → chunk-SG25J6LV.cjs} +87 -87
- package/dist/{chunk-SEZR7FMU.cjs.map → chunk-SG25J6LV.cjs.map} +1 -1
- package/dist/{chunk-Z32PJFCD.js → chunk-V5WLIFKN.js} +312 -370
- package/dist/{chunk-Z32PJFCD.js.map → chunk-V5WLIFKN.js.map} +1 -1
- package/dist/{chunk-DPXBR7O2.cjs → chunk-VVXUUBML.cjs} +26 -26
- package/dist/{chunk-DPXBR7O2.cjs.map → chunk-VVXUUBML.cjs.map} +1 -1
- package/dist/{chunk-O2XT6VUS.js → chunk-VZX3SSFL.js} +2 -2
- package/dist/{chunk-7TKJAGBO.cjs → chunk-WVZHCDB5.cjs} +11 -11
- package/dist/{chunk-7TKJAGBO.cjs.map → chunk-WVZHCDB5.cjs.map} +1 -1
- package/dist/{chunk-ELED5PW4.js → chunk-XG7FLAKQ.js} +2 -2
- package/dist/{chunk-YIPZT3XR.js → chunk-ZIQMVAGA.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/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/stream/message-mutations.d.ts +33 -5
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.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 +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +132 -132
- package/dist/components/index.js +11 -11
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- 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/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/dispatch.tsx +87 -98
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +15 -15
- package/src/components/chat/stream/chat-stream-reducer.ts +6 -0
- package/src/components/chat/stream/message-mutations.ts +140 -116
- package/src/components/chat/utils/message-segment-accumulator.ts +19 -27
- package/dist/chunk-PDPNJJLB.cjs.map +0 -1
- /package/dist/{chunk-JEKWIGG6.js.map → chunk-4NFDSMB5.js.map} +0 -0
- /package/dist/{chunk-XYD3Y7WU.js.map → chunk-6LMTBNLN.js.map} +0 -0
- /package/dist/{chunk-BH24GEIY.js.map → chunk-FI35LJ3L.js.map} +0 -0
- /package/dist/{chunk-OYYHTO7U.js.map → chunk-HGZPH7S5.js.map} +0 -0
- /package/dist/{chunk-VIXKP7G7.js.map → chunk-IHNUMRMU.js.map} +0 -0
- /package/dist/{chunk-MCFMOSY6.js.map → chunk-IK2WRYVA.js.map} +0 -0
- /package/dist/{chunk-MSMSMXST.js.map → chunk-JKAX5K7C.js.map} +0 -0
- /package/dist/{chunk-AXS2XECN.js.map → chunk-JO3NPEK6.js.map} +0 -0
- /package/dist/{chunk-NLGPA3VY.js.map → chunk-JOFGJYIT.js.map} +0 -0
- /package/dist/{chunk-PPVWJQTF.js.map → chunk-OT4RV6WY.js.map} +0 -0
- /package/dist/{chunk-O2XT6VUS.js.map → chunk-VZX3SSFL.js.map} +0 -0
- /package/dist/{chunk-ELED5PW4.js.map → chunk-XG7FLAKQ.js.map} +0 -0
- /package/dist/{chunk-YIPZT3XR.js.map → chunk-ZIQMVAGA.js.map} +0 -0
|
@@ -1026,26 +1026,34 @@ const FINANCIAL_CARD_CONFIGS: Record<string, FinancialCardConfig> = {
|
|
|
1026
1026
|
balance_sheet: { label: 'Balance sheet', icon: () => <BankIcon size={24} /> },
|
|
1027
1027
|
cash_flow: { label: 'Cash flow', icon: () => <CoinsExchangeCurrencyIcon size={24} /> },
|
|
1028
1028
|
}
|
|
1029
|
-
|
|
1029
|
+
/** Expand a per-family config map into registry entries — ONE loop shell
|
|
1030
|
+
* for every card family (financial, github, program, roadmap); adding a
|
|
1031
|
+
* family is a config map + one `registryEntries(...)` call. */
|
|
1032
|
+
function registryEntries<C>(
|
|
1033
|
+
configs: Record<string, C>,
|
|
1034
|
+
build: (cfg: C) => ChatCardRegistryEntry,
|
|
1035
|
+
): Record<string, ChatCardRegistryEntry> {
|
|
1030
1036
|
const out: Record<string, ChatCardRegistryEntry> = {}
|
|
1031
|
-
for (const [docType, cfg] of Object.entries(
|
|
1032
|
-
out[docType] = {
|
|
1033
|
-
mode: 'no-fetch',
|
|
1034
|
-
label: cfg.label,
|
|
1035
|
-
bareInline: true,
|
|
1036
|
-
render: (chatRef, opts) => (
|
|
1037
|
-
<GenericFinancialChatCard
|
|
1038
|
-
chatRef={chatRef}
|
|
1039
|
-
icon={cfg.icon()}
|
|
1040
|
-
isNewTab={opts.isNewTab}
|
|
1041
|
-
discuss={opts.discuss}
|
|
1042
|
-
/>
|
|
1043
|
-
),
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1037
|
+
for (const [docType, cfg] of Object.entries(configs)) out[docType] = build(cfg)
|
|
1046
1038
|
return out
|
|
1047
1039
|
}
|
|
1048
1040
|
|
|
1041
|
+
function financialRegistryEntries(): Record<string, ChatCardRegistryEntry> {
|
|
1042
|
+
return registryEntries(FINANCIAL_CARD_CONFIGS, (cfg) => ({
|
|
1043
|
+
mode: 'no-fetch',
|
|
1044
|
+
label: cfg.label,
|
|
1045
|
+
bareInline: true,
|
|
1046
|
+
render: (chatRef, opts) => (
|
|
1047
|
+
<GenericFinancialChatCard
|
|
1048
|
+
chatRef={chatRef}
|
|
1049
|
+
icon={cfg.icon()}
|
|
1050
|
+
isNewTab={opts.isNewTab}
|
|
1051
|
+
discuss={opts.discuss}
|
|
1052
|
+
/>
|
|
1053
|
+
),
|
|
1054
|
+
}))
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1049
1057
|
interface GitHubCardConfig {
|
|
1050
1058
|
label: string
|
|
1051
1059
|
kind: GitHubActivityKind
|
|
@@ -1059,19 +1067,15 @@ const GITHUB_CARD_CONFIGS: Record<string, GitHubCardConfig> = {
|
|
|
1059
1067
|
github_pr_review_public: { label: 'GitHub review (public)', kind: 'pr_review' },
|
|
1060
1068
|
}
|
|
1061
1069
|
function githubRegistryEntries(): Record<string, ChatCardRegistryEntry> {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
),
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
return out
|
|
1070
|
+
return registryEntries(GITHUB_CARD_CONFIGS, (cfg) => ({
|
|
1071
|
+
mode: 'no-fetch',
|
|
1072
|
+
label: cfg.label,
|
|
1073
|
+
bareInline: true,
|
|
1074
|
+
render: (chatRef, opts) => (
|
|
1075
|
+
<GitHubChatCard chatRef={chatRef} kind={cfg.kind} isNewTab={opts.isNewTab}
|
|
1076
|
+
discuss={opts.discuss} />
|
|
1077
|
+
),
|
|
1078
|
+
}))
|
|
1075
1079
|
}
|
|
1076
1080
|
|
|
1077
1081
|
type ProgramConfigKey = 'podcast' | 'webinar' | 'event'
|
|
@@ -1086,28 +1090,24 @@ const PROGRAM_CARD_CONFIGS: Record<string, ProgramCardConfig> = {
|
|
|
1086
1090
|
event: { label: 'Event', configKey: 'event', contentRefType: 'event' },
|
|
1087
1091
|
}
|
|
1088
1092
|
function programRegistryEntries(): Record<string, ChatCardRegistryEntry> {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
),
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
return out
|
|
1093
|
+
return registryEntries(PROGRAM_CARD_CONFIGS, (cfg) => ({
|
|
1094
|
+
mode: 'fetch',
|
|
1095
|
+
label: cfg.label,
|
|
1096
|
+
contentRefType: cfg.contentRefType,
|
|
1097
|
+
bareInline: true,
|
|
1098
|
+
skeleton: () => <ProgramCardSkeleton size="sm" />,
|
|
1099
|
+
render: (item, chatRef, opts) => (
|
|
1100
|
+
<ProgramChatCard
|
|
1101
|
+
item={item}
|
|
1102
|
+
chatRef={chatRef}
|
|
1103
|
+
isNewTab={opts.isNewTab}
|
|
1104
|
+
discuss={opts.discuss}
|
|
1105
|
+
configKey={cfg.configKey}
|
|
1106
|
+
label={cfg.configKey.charAt(0).toUpperCase() + cfg.configKey.slice(1)}
|
|
1107
|
+
ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}
|
|
1108
|
+
/>
|
|
1109
|
+
),
|
|
1110
|
+
}))
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
1113
|
type RoadmapCardType = 'roadmap_item' | 'delivery_item' | 'internal_task'
|
|
@@ -1135,27 +1135,23 @@ const ROADMAP_CARD_CONFIGS: Record<string, RoadmapEntryConfig> = {
|
|
|
1135
1135
|
},
|
|
1136
1136
|
}
|
|
1137
1137
|
function roadmapRegistryEntries(): Record<string, ChatCardRegistryEntry> {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
),
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
return out
|
|
1138
|
+
return registryEntries(ROADMAP_CARD_CONFIGS, (cfg) => ({
|
|
1139
|
+
mode: 'fetch',
|
|
1140
|
+
label: cfg.label,
|
|
1141
|
+
contentRefType: cfg.contentRefType,
|
|
1142
|
+
bareInline: true,
|
|
1143
|
+
...(cfg.noComposedHref ? { noComposedHref: true } : {}),
|
|
1144
|
+
skeleton: () => <RoadmapCardSkeleton size="sm" />,
|
|
1145
|
+
render: (item, chatRef, opts) => (
|
|
1146
|
+
<RoadmapChatCard
|
|
1147
|
+
item={item}
|
|
1148
|
+
chatRef={chatRef}
|
|
1149
|
+
isNewTab={opts.isNewTab}
|
|
1150
|
+
discuss={opts.discuss}
|
|
1151
|
+
cardType={cfg.cardType}
|
|
1152
|
+
/>
|
|
1153
|
+
),
|
|
1154
|
+
}))
|
|
1159
1155
|
}
|
|
1160
1156
|
|
|
1161
1157
|
const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
|
|
@@ -1668,6 +1664,23 @@ export function ChatCardLoader({
|
|
|
1668
1664
|
{children}
|
|
1669
1665
|
</ChatCardNavWrap>
|
|
1670
1666
|
)
|
|
1667
|
+
// ONE render tail for both modes — bare-inline cards skip the discuss
|
|
1668
|
+
// wrapper; everything else gets the ChatCardWithDiscuss chrome. Kept
|
|
1669
|
+
// here (not per-branch) so the two paths can never drift on the
|
|
1670
|
+
// discuss-menu wiring.
|
|
1671
|
+
const finish = (node: React.ReactNode) =>
|
|
1672
|
+
entry.bareInline ? (
|
|
1673
|
+
<>{navWrap(node)}</>
|
|
1674
|
+
) : (
|
|
1675
|
+
<ChatCardWithDiscuss
|
|
1676
|
+
chatRef={finalChatRef}
|
|
1677
|
+
onDiscuss={onDiscuss}
|
|
1678
|
+
onDisplay={onDisplay}
|
|
1679
|
+
displayAction={entry.displayAction}
|
|
1680
|
+
>
|
|
1681
|
+
{navWrap(node)}
|
|
1682
|
+
</ChatCardWithDiscuss>
|
|
1683
|
+
)
|
|
1671
1684
|
if (entry.mode === 'no-fetch') {
|
|
1672
1685
|
// Synthetic-ref gate. `chat-message-enhanced.tsx` builds a minimal
|
|
1673
1686
|
// `{ type, id, title: cardId, url: null }` ChatRef when the LLM
|
|
@@ -1690,19 +1703,7 @@ export function ChatCardLoader({
|
|
|
1690
1703
|
// Fetch-mode types already handle this gracefully: a synthetic
|
|
1691
1704
|
// id leads to a fetch miss → `!item` → null at line ~1034.
|
|
1692
1705
|
if (!finalChatRef.sourceRepo) return null
|
|
1693
|
-
|
|
1694
|
-
return navWrap(entry.render(finalChatRef, renderOpts))
|
|
1695
|
-
}
|
|
1696
|
-
return (
|
|
1697
|
-
<ChatCardWithDiscuss
|
|
1698
|
-
chatRef={finalChatRef}
|
|
1699
|
-
onDiscuss={onDiscuss}
|
|
1700
|
-
onDisplay={onDisplay}
|
|
1701
|
-
displayAction={entry.displayAction}
|
|
1702
|
-
>
|
|
1703
|
-
{navWrap(entry.render(finalChatRef, renderOpts))}
|
|
1704
|
-
</ChatCardWithDiscuss>
|
|
1705
|
-
)
|
|
1706
|
+
return finish(entry.render(finalChatRef, renderOpts))
|
|
1706
1707
|
}
|
|
1707
1708
|
if (isLoading) return <>{entry.skeleton()}</>
|
|
1708
1709
|
if (!item) {
|
|
@@ -1742,19 +1743,7 @@ export function ChatCardLoader({
|
|
|
1742
1743
|
}
|
|
1743
1744
|
return null
|
|
1744
1745
|
}
|
|
1745
|
-
|
|
1746
|
-
return <>{navWrap(entry.render(item, finalChatRef, renderOpts))}</>
|
|
1747
|
-
}
|
|
1748
|
-
return (
|
|
1749
|
-
<ChatCardWithDiscuss
|
|
1750
|
-
chatRef={finalChatRef}
|
|
1751
|
-
onDiscuss={onDiscuss}
|
|
1752
|
-
onDisplay={onDisplay}
|
|
1753
|
-
displayAction={entry.displayAction}
|
|
1754
|
-
>
|
|
1755
|
-
{navWrap(entry.render(item, finalChatRef, renderOpts))}
|
|
1756
|
-
</ChatCardWithDiscuss>
|
|
1757
|
-
)
|
|
1746
|
+
return finish(entry.render(item, finalChatRef, renderOpts))
|
|
1758
1747
|
}
|
|
1759
1748
|
|
|
1760
1749
|
// =============================================================================
|
|
@@ -254,21 +254,21 @@ export function useSseChatAdapter(
|
|
|
254
254
|
|
|
255
255
|
// Approve/reject pass the transport's boolean outcome through so batch
|
|
256
256
|
// approve-all loops can mark a FAILED row (expired proposal, network
|
|
257
|
-
// error) instead of leaving its execution loader spinning.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
257
|
+
// error) instead of leaving its execution loader spinning. ONE factory
|
|
258
|
+
// — the two handlers differ only in the action literal.
|
|
259
|
+
const cardResolve = useCallback(
|
|
260
|
+
(action: 'approve' | 'reject') =>
|
|
261
|
+
(reqId?: string): void | Promise<boolean> => {
|
|
262
|
+
if (!reqId) return
|
|
263
|
+
return sendMessageRef.current('', {
|
|
264
|
+
hidden: true,
|
|
265
|
+
approvalAction: { proposalId: reqId, action },
|
|
266
|
+
})
|
|
267
|
+
},
|
|
268
|
+
[],
|
|
269
|
+
)
|
|
270
|
+
const cardApprove = useMemo(() => cardResolve('approve'), [cardResolve])
|
|
271
|
+
const cardReject = useMemo(() => cardResolve('reject'), [cardResolve])
|
|
272
272
|
|
|
273
273
|
const createReducerOptions = useCallback(
|
|
274
274
|
(): ChatStreamReducerOptions => ({
|
|
@@ -1978,6 +1978,12 @@ export function createChatStreamReducer(
|
|
|
1978
1978
|
setMessagesInternal(
|
|
1979
1979
|
projectApprovalResolutionToMessages(messages, requestId, status, resolvedByName),
|
|
1980
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)
|
|
1981
1987
|
return segments
|
|
1982
1988
|
},
|
|
1983
1989
|
getPendingEscalated() {
|
|
@@ -134,28 +134,30 @@ 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
|
+
|
|
137
141
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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.
|
|
144
148
|
*/
|
|
145
|
-
|
|
149
|
+
function mapSegments(
|
|
146
150
|
prev: UnifiedChatMessage[],
|
|
147
|
-
|
|
148
|
-
status: ChatApprovalStatus,
|
|
151
|
+
fn: (s: MessageSegment) => MessageSegment,
|
|
149
152
|
): UnifiedChatMessage[] {
|
|
150
153
|
let changed = false
|
|
151
154
|
const next = prev.map((m) => {
|
|
152
155
|
if (m.role !== 'assistant' || !m.segments) return m
|
|
153
156
|
let msgChanged = false
|
|
154
157
|
const segs = m.segments.map((s) => {
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
return { ...s, status }
|
|
158
|
+
const out = fn(s)
|
|
159
|
+
if (out !== s) msgChanged = true
|
|
160
|
+
return out
|
|
159
161
|
})
|
|
160
162
|
if (!msgChanged) return m
|
|
161
163
|
changed = true
|
|
@@ -164,6 +166,107 @@ export function projectBatchStatusToMessages(
|
|
|
164
166
|
return changed ? next : prev
|
|
165
167
|
}
|
|
166
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
|
+
|
|
167
270
|
/**
|
|
168
271
|
* Mark ONE batch row's confirm as FAILED (expired proposal, network
|
|
169
272
|
* error): tick its execution icon to the failure cross so the row's
|
|
@@ -174,36 +277,17 @@ export function projectBatchRowFailureToMessages(
|
|
|
174
277
|
prev: UnifiedChatMessage[],
|
|
175
278
|
rowRequestId: string,
|
|
176
279
|
): UnifiedChatMessage[] {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
const existing = s.data.executions?.[rowRequestId]
|
|
189
|
-
if (existing?.status === 'done') return s
|
|
190
|
-
msgChanged = true
|
|
191
|
-
return {
|
|
192
|
-
...s,
|
|
193
|
-
data: {
|
|
194
|
-
...s.data,
|
|
195
|
-
executions: {
|
|
196
|
-
...(s.data.executions ?? {}),
|
|
197
|
-
[rowRequestId]: { status: 'done' as const, success: false },
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
}
|
|
201
|
-
})
|
|
202
|
-
if (!msgChanged) return m
|
|
203
|
-
changed = true
|
|
204
|
-
return { ...m, segments: segs }
|
|
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 })
|
|
205
290
|
})
|
|
206
|
-
return changed ? next : prev
|
|
207
291
|
}
|
|
208
292
|
|
|
209
293
|
/**
|
|
@@ -314,23 +398,16 @@ export function mergeToolExecutionIfPresent(
|
|
|
314
398
|
seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId)
|
|
315
399
|
) {
|
|
316
400
|
const prevExec: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
|
|
317
|
-
//
|
|
318
|
-
// redelivery of
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const nextExec: ApprovalBatchExecutionState =
|
|
324
|
-
toolData.type === 'EXECUTED_TOOL'
|
|
325
|
-
? { status: 'done', result: toolData.result, success: toolData.success }
|
|
326
|
-
: { 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
|
|
327
407
|
// Value-level no-op (replayed duplicate) → prior references.
|
|
328
408
|
if (sameExecutionState(prevExec, nextExec)) return prev
|
|
329
409
|
const nextSegments = [...message.segments]
|
|
330
|
-
nextSegments[j] =
|
|
331
|
-
...seg,
|
|
332
|
-
data: { ...seg.data, executions: { ...(seg.data.executions ?? {}), [execId]: nextExec } },
|
|
333
|
-
}
|
|
410
|
+
nextSegments[j] = withBatchExecution(seg, execId, nextExec)
|
|
334
411
|
const next = [...prev]
|
|
335
412
|
next[i] = { ...message, segments: nextSegments }
|
|
336
413
|
return next
|
|
@@ -376,10 +453,10 @@ export function mergeToolExecutionIfPresent(
|
|
|
376
453
|
}
|
|
377
454
|
|
|
378
455
|
/**
|
|
379
|
-
*
|
|
380
|
-
* `
|
|
381
|
-
*
|
|
382
|
-
*
|
|
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`.
|
|
383
460
|
*/
|
|
384
461
|
export function projectApprovalResolutionToMessages(
|
|
385
462
|
prev: UnifiedChatMessage[],
|
|
@@ -387,60 +464,7 @@ export function projectApprovalResolutionToMessages(
|
|
|
387
464
|
status: ChatApprovalStatus,
|
|
388
465
|
resolvedByName?: string | null,
|
|
389
466
|
): UnifiedChatMessage[] {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
let msgChanged = false
|
|
394
|
-
const segs = m.segments.map((s) => {
|
|
395
|
-
if (s.type === 'approval_request' && s.data.requestId === requestId && s.status !== status) {
|
|
396
|
-
msgChanged = true
|
|
397
|
-
return { ...s, status }
|
|
398
|
-
}
|
|
399
|
-
if (s.type === 'approval_batch') {
|
|
400
|
-
const isAnchor = s.data.approvalRequestId === requestId
|
|
401
|
-
// Wire-native chat batches: each ROW is its own proposal
|
|
402
|
-
// (`toolExecutionRequestId` = proposalId) resolving through its
|
|
403
|
-
// own per-proposal confirm — a resolution for a row ticks that
|
|
404
|
-
// row's execution icon (check on approved, cross on rejected/
|
|
405
|
-
// failed) without touching the batch status (flipped
|
|
406
|
-
// optimistically at click time via
|
|
407
|
-
// `projectBatchStatusToMessages`). The anchor id is the
|
|
408
|
-
// server-minted `batch:<first proposalId>` — NEVER a row id
|
|
409
|
-
// (see ApprovalBatchFrame.batchId) — so `isAnchor` flips the
|
|
410
|
-
// batch status only.
|
|
411
|
-
const hasRow = s.data.toolCalls?.some((c) => c.toolExecutionRequestId === requestId)
|
|
412
|
-
if (!isAnchor && !hasRow) return s
|
|
413
|
-
const nextResolvedBy = resolvedByName ?? s.resolvedByName
|
|
414
|
-
const nextExecutions = hasRow
|
|
415
|
-
? {
|
|
416
|
-
...(s.data.executions ?? {}),
|
|
417
|
-
[requestId]: { status: 'done' as const, success: status === 'approved' },
|
|
418
|
-
}
|
|
419
|
-
: s.data.executions
|
|
420
|
-
const nextStatus = isAnchor ? status : s.status
|
|
421
|
-
if (
|
|
422
|
-
s.status === nextStatus &&
|
|
423
|
-
nextResolvedBy === s.resolvedByName &&
|
|
424
|
-
nextExecutions === s.data.executions
|
|
425
|
-
) {
|
|
426
|
-
return s
|
|
427
|
-
}
|
|
428
|
-
msgChanged = true
|
|
429
|
-
return {
|
|
430
|
-
...s,
|
|
431
|
-
status: nextStatus,
|
|
432
|
-
resolvedByName: nextResolvedBy,
|
|
433
|
-
data: {
|
|
434
|
-
...s.data,
|
|
435
|
-
...(nextExecutions ? { executions: nextExecutions } : {}),
|
|
436
|
-
},
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return s
|
|
440
|
-
})
|
|
441
|
-
if (!msgChanged) return m
|
|
442
|
-
changed = true
|
|
443
|
-
return { ...m, segments: segs }
|
|
444
|
-
})
|
|
445
|
-
return changed ? next : prev
|
|
467
|
+
return mapSegments(prev, (s) =>
|
|
468
|
+
applyApprovalStatusToSegment(s, requestId, status, resolvedByName),
|
|
469
|
+
)
|
|
446
470
|
}
|
|
@@ -23,6 +23,11 @@ import type {
|
|
|
23
23
|
ChatApprovalStatus,
|
|
24
24
|
ExecutingToolState,
|
|
25
25
|
} from '../types'
|
|
26
|
+
import {
|
|
27
|
+
applyApprovalStatusToSegment,
|
|
28
|
+
nextBatchExecution,
|
|
29
|
+
withBatchExecution,
|
|
30
|
+
} from '../stream/message-mutations'
|
|
26
31
|
|
|
27
32
|
export interface AccumulatorCallbacks {
|
|
28
33
|
/** See `ApprovalResolutionHandler` (message.types) — the SSOT for the
|
|
@@ -247,25 +252,13 @@ export class MessageSegmentAccumulator {
|
|
|
247
252
|
if (!hasCall) return seg
|
|
248
253
|
|
|
249
254
|
const prev: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return seg
|
|
255
|
-
}
|
|
256
|
-
const next: ApprovalBatchExecutionState =
|
|
257
|
-
toolData.type === 'EXECUTED_TOOL'
|
|
258
|
-
? { status: 'done', result: toolData.result, success: toolData.success }
|
|
259
|
-
: { status: 'executing', result: prev?.result, success: prev?.success }
|
|
260
|
-
|
|
255
|
+
// Shared rule (`nextBatchExecution` from message-mutations): the
|
|
256
|
+
// never-downgrade guard + EXECUTED/EXECUTING ternary — one
|
|
257
|
+
// implementation with the message-array projection.
|
|
258
|
+
const next = nextBatchExecution(prev, toolData)
|
|
261
259
|
matched = true
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
-
data: {
|
|
265
|
-
...seg.data,
|
|
266
|
-
executions: { ...(seg.data.executions ?? {}), [execId]: next },
|
|
267
|
-
},
|
|
268
|
-
}
|
|
260
|
+
if (next === null) return seg
|
|
261
|
+
return withBatchExecution(seg, execId, next)
|
|
269
262
|
})
|
|
270
263
|
return matched
|
|
271
264
|
}
|
|
@@ -432,15 +425,14 @@ export class MessageSegmentAccumulator {
|
|
|
432
425
|
* resolved card shows "by {name}"; omit it to leave any existing value untouched.
|
|
433
426
|
*/
|
|
434
427
|
updateApprovalStatus(requestId: string, status: ChatApprovalStatus, resolvedByName?: string | null): MessageSegment[] {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
})
|
|
428
|
+
// ONE rule, two containers: `applyApprovalStatusToSegment` is the
|
|
429
|
+
// same predicate the message-array projection uses
|
|
430
|
+
// (`projectApprovalResolutionToMessages`) — anchor status flip AND
|
|
431
|
+
// batch-row execution tick, so this flat-list path can never drift
|
|
432
|
+
// from the projection again (it previously missed the row case).
|
|
433
|
+
this.segments = this.segments.map((segment) =>
|
|
434
|
+
applyApprovalStatusToSegment(segment, requestId, status, resolvedByName),
|
|
435
|
+
)
|
|
444
436
|
return this.getSegments()
|
|
445
437
|
}
|
|
446
438
|
|