@flamingo-stack/openframe-frontend-core 0.0.532 → 0.0.533
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/confirm-tool.d.ts +50 -0
- package/dist/chat-protocol/confirm-tool.d.ts.map +1 -0
- package/dist/chat-protocol/decode.d.ts +1 -9
- package/dist/chat-protocol/decode.d.ts.map +1 -1
- package/dist/chat-protocol/events.d.ts +35 -0
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +72 -3
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.d.ts +1 -0
- package/dist/chat-protocol/index.d.ts.map +1 -1
- package/dist/chat-protocol/index.js +67 -4
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/leading-frames.d.ts +38 -0
- package/dist/chat-protocol/leading-frames.d.ts.map +1 -0
- package/dist/chat-protocol/nats-decoder.d.ts +41 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/{chunk-QEMI34VA.cjs → chunk-2MYDGLJS.cjs} +87 -87
- package/dist/{chunk-QEMI34VA.cjs.map → chunk-2MYDGLJS.cjs.map} +1 -1
- package/dist/{chunk-TQ7EPKKO.js → chunk-2TUOBFPK.js} +2 -2
- package/dist/{chunk-XBYEPSNZ.js → chunk-2XGHEJZK.js} +4 -4
- package/dist/{chunk-OAEJWP7H.js → chunk-46FKXLNE.js} +2 -2
- package/dist/{chunk-ECR37JHH.js → chunk-4QKXNWH5.js} +2 -2
- package/dist/{chunk-GWFC3NE5.cjs → chunk-4ZRHVOEB.cjs} +31 -31
- package/dist/{chunk-GWFC3NE5.cjs.map → chunk-4ZRHVOEB.cjs.map} +1 -1
- package/dist/{chunk-AZBS4PZG.js → chunk-5U67WTAS.js} +2 -2
- package/dist/{chunk-3G7FDE5E.js → chunk-6SMLYZLH.js} +4 -4
- package/dist/{chunk-7SGBF2I2.js → chunk-6W3ULZ4X.js} +6 -6
- package/dist/{chunk-ALXLWF75.js → chunk-6YXN5RHW.js} +2 -2
- package/dist/{chunk-2QP67D2L.cjs → chunk-DFQLUK75.cjs} +3 -3
- package/dist/{chunk-2QP67D2L.cjs.map → chunk-DFQLUK75.cjs.map} +1 -1
- package/dist/{chunk-ETF2QXXU.cjs → chunk-ENPGPZIB.cjs} +61 -61
- package/dist/{chunk-ETF2QXXU.cjs.map → chunk-ENPGPZIB.cjs.map} +1 -1
- package/dist/{chunk-HJDCDOJM.cjs → chunk-GNOK5J34.cjs} +37 -37
- package/dist/{chunk-HJDCDOJM.cjs.map → chunk-GNOK5J34.cjs.map} +1 -1
- package/dist/{chunk-4GL2XJ5C.cjs → chunk-J4BE4FIR.cjs} +4 -4
- package/dist/{chunk-4GL2XJ5C.cjs.map → chunk-J4BE4FIR.cjs.map} +1 -1
- package/dist/{chunk-HXHA3DCI.cjs → chunk-JDW2IL7A.cjs} +11 -11
- package/dist/{chunk-HXHA3DCI.cjs.map → chunk-JDW2IL7A.cjs.map} +1 -1
- package/dist/{chunk-TMZS63G2.cjs → chunk-KMB66NYX.cjs} +7 -7
- package/dist/{chunk-TMZS63G2.cjs.map → chunk-KMB66NYX.cjs.map} +1 -1
- package/dist/{chunk-5W3RCZIK.js → chunk-LE3BJ5H2.js} +2 -2
- package/dist/{chunk-WINT757E.js → chunk-LIUARTOZ.js} +206 -64
- package/dist/chunk-LIUARTOZ.js.map +1 -0
- package/dist/{chunk-65NR333L.js → chunk-MVGL5NNJ.js} +2 -2
- package/dist/{chunk-LYWSHNK4.cjs → chunk-NAV4FRNL.cjs} +26 -26
- package/dist/{chunk-LYWSHNK4.cjs.map → chunk-NAV4FRNL.cjs.map} +1 -1
- package/dist/{chunk-2MBS3YEY.cjs → chunk-OLOKQQAP.cjs} +5 -5
- package/dist/{chunk-2MBS3YEY.cjs.map → chunk-OLOKQQAP.cjs.map} +1 -1
- package/dist/{chunk-QYNLVIGQ.cjs → chunk-QZ2NAPLF.cjs} +9 -9
- package/dist/{chunk-QYNLVIGQ.cjs.map → chunk-QZ2NAPLF.cjs.map} +1 -1
- package/dist/{chunk-L6EKEYF7.cjs → chunk-RKITTO5V.cjs} +14 -14
- package/dist/{chunk-L6EKEYF7.cjs.map → chunk-RKITTO5V.cjs.map} +1 -1
- package/dist/{chunk-UP4RA54U.js → chunk-U555TMPC.js} +5 -5
- package/dist/{chunk-GVLA2BPW.cjs → chunk-UPHN2VRZ.cjs} +259 -117
- package/dist/chunk-UPHN2VRZ.cjs.map +1 -0
- package/dist/{chunk-IZOGFZ7N.js → chunk-XN4JKIJY.js} +2 -2
- package/dist/{chunk-KYQZBQYV.js → chunk-YLB6XXO6.js} +2 -2
- package/dist/{chunk-JZ5XDZYD.cjs → chunk-Z3NOI7X7.cjs} +26 -26
- package/dist/{chunk-JZ5XDZYD.cjs.map → chunk-Z3NOI7X7.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/embeddable-chat.d.ts +13 -0
- package/dist/components/chat/embeddable-chat.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 +10 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +9 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +9 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +15 -0
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/network.types.d.ts +4 -0
- package/dist/components/chat/types/network.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +7 -1
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/approval-display.d.ts +49 -0
- package/dist/components/chat/utils/approval-display.d.ts.map +1 -0
- package/dist/components/chat/utils/index.d.ts +1 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +25 -3
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- 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 +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +140 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +19 -11
- package/dist/components/index.js.map +1 -1
- 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 +10 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +9 -1
- package/dist/index.cjs +10 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +70 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +108 -1
- package/src/chat-protocol/confirm-tool.ts +67 -0
- package/src/chat-protocol/decode.ts +7 -153
- package/src/chat-protocol/events.ts +40 -0
- package/src/chat-protocol/index.ts +3 -0
- package/src/chat-protocol/leading-frames.ts +175 -0
- package/src/chat-protocol/nats-decoder.ts +111 -3
- package/src/components/chat/embeddable-chat.tsx +18 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +7 -16
- package/src/components/chat/stream/chat-stream-reducer.ts +49 -3
- package/src/components/chat/types/message.types.ts +15 -0
- package/src/components/chat/types/network.types.ts +4 -0
- package/src/components/chat/types/processing.types.ts +12 -1
- package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +92 -0
- package/src/components/chat/utils/approval-display.ts +70 -0
- package/src/components/chat/utils/index.ts +9 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +62 -32
- package/src/components/chat/utils/process-historical-messages.ts +40 -6
- package/dist/chunk-GVLA2BPW.cjs.map +0 -1
- package/dist/chunk-WINT757E.js.map +0 -1
- /package/dist/{chunk-TQ7EPKKO.js.map → chunk-2TUOBFPK.js.map} +0 -0
- /package/dist/{chunk-XBYEPSNZ.js.map → chunk-2XGHEJZK.js.map} +0 -0
- /package/dist/{chunk-OAEJWP7H.js.map → chunk-46FKXLNE.js.map} +0 -0
- /package/dist/{chunk-ECR37JHH.js.map → chunk-4QKXNWH5.js.map} +0 -0
- /package/dist/{chunk-AZBS4PZG.js.map → chunk-5U67WTAS.js.map} +0 -0
- /package/dist/{chunk-3G7FDE5E.js.map → chunk-6SMLYZLH.js.map} +0 -0
- /package/dist/{chunk-7SGBF2I2.js.map → chunk-6W3ULZ4X.js.map} +0 -0
- /package/dist/{chunk-ALXLWF75.js.map → chunk-6YXN5RHW.js.map} +0 -0
- /package/dist/{chunk-5W3RCZIK.js.map → chunk-LE3BJ5H2.js.map} +0 -0
- /package/dist/{chunk-65NR333L.js.map → chunk-MVGL5NNJ.js.map} +0 -0
- /package/dist/{chunk-UP4RA54U.js.map → chunk-U555TMPC.js.map} +0 -0
- /package/dist/{chunk-IZOGFZ7N.js.map → chunk-XN4JKIJY.js.map} +0 -0
- /package/dist/{chunk-KYQZBQYV.js.map → chunk-YLB6XXO6.js.map} +0 -0
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect } from 'vitest'
|
|
12
12
|
import { processHistoricalMessagesWithErrors } from '../process-historical-messages'
|
|
13
|
+
import { decodeNatsChunk } from '../../../../chat-protocol/nats-decoder'
|
|
13
14
|
import { createChatStreamReducer } from '../../stream/chat-stream-reducer'
|
|
14
15
|
import type { HistoricalMessage, MessageSegment } from '../../types'
|
|
15
16
|
|
|
@@ -156,3 +157,94 @@ describe('displayApprovalTypes — history vs realtime default divergence', () =
|
|
|
156
157
|
expect(live.escalated).toHaveLength(0)
|
|
157
158
|
})
|
|
158
159
|
})
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A Product Guide card must survive a reload UNCHANGED.
|
|
163
|
+
*
|
|
164
|
+
* It arrives as a hub frame the agent re-streams into the dialog, so it is
|
|
165
|
+
* typed by TOOL and resolves through the hub's confirm route — the tier gate
|
|
166
|
+
* these tests are about simply does not apply to it. When the rule lived only
|
|
167
|
+
* in the realtime kernel, the same card rendered inline while streaming and
|
|
168
|
+
* then, on the next page load, was tier-gated: lifted out of its turn into the
|
|
169
|
+
* consumer's sticky pending-approvals envelope, stripped of the `fields` that
|
|
170
|
+
* are its entire body, and stripped of the `origin` that routes its buttons —
|
|
171
|
+
* so clicking Approve hit the agent's endpoint and 404'd.
|
|
172
|
+
*/
|
|
173
|
+
describe('Product Guide approvals — history and realtime agree', () => {
|
|
174
|
+
const guideProposalMessage = (id: string): HistoricalMessage => ({
|
|
175
|
+
id,
|
|
176
|
+
createdAt: '2026-07-17T12:00:00Z',
|
|
177
|
+
owner: { type: 'ASSISTANT' },
|
|
178
|
+
messageData: [
|
|
179
|
+
{
|
|
180
|
+
type: 'GUIDE',
|
|
181
|
+
payload: {
|
|
182
|
+
kind: 'approval_request',
|
|
183
|
+
proposalId: 'prop-1',
|
|
184
|
+
toolName: 'create_ticket',
|
|
185
|
+
title: 'Open a support ticket',
|
|
186
|
+
fields: [{ label: 'Subject', value: 'Agent will not enroll' }],
|
|
187
|
+
},
|
|
188
|
+
} as any,
|
|
189
|
+
],
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// The narrowest possible tier list: it excludes the guide's tool name, so a
|
|
193
|
+
// card that still renders proves the gate was bypassed, not merely satisfied.
|
|
194
|
+
const NARROW_TIERS = ['CLIENT']
|
|
195
|
+
|
|
196
|
+
it('renders inline on reload despite a tier list that excludes it', () => {
|
|
197
|
+
const { messages, escalatedApprovals } = processHistoricalMessagesWithErrors(
|
|
198
|
+
[guideProposalMessage('m1')],
|
|
199
|
+
{ displayApprovalTypes: NARROW_TIERS },
|
|
200
|
+
)
|
|
201
|
+
const rendered = messages.flatMap((m) => approvalSegments(m.content))
|
|
202
|
+
expect(rendered).toHaveLength(1)
|
|
203
|
+
expect(escalatedApprovals.size).toBe(0)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('keeps the card in its own turn, not in the sticky pending-approvals envelope', () => {
|
|
207
|
+
const { messages } = processHistoricalMessagesWithErrors([guideProposalMessage('m1')], {
|
|
208
|
+
displayApprovalTypes: NARROW_TIERS,
|
|
209
|
+
})
|
|
210
|
+
expect(messages.some((m) => String(m.id).startsWith('pending-approvals-'))).toBe(false)
|
|
211
|
+
const turn = messages.find((m) => m.id === 'm1')
|
|
212
|
+
expect(approvalSegments(turn?.content)).toHaveLength(1)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('carries the same fields and origin as the live stream', () => {
|
|
216
|
+
/** The single card's `data`, narrowed off the segment union. */
|
|
217
|
+
const cardData = (segments: MessageSegment[]) => {
|
|
218
|
+
const card = segments.find((s) => s.type === 'approval_request')
|
|
219
|
+
return card?.type === 'approval_request' ? card.data : undefined
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { messages } = processHistoricalMessagesWithErrors([guideProposalMessage('m1')], {
|
|
223
|
+
displayApprovalTypes: NARROW_TIERS,
|
|
224
|
+
})
|
|
225
|
+
const replayed = cardData(messages.flatMap((m) => approvalSegments(m.content)))
|
|
226
|
+
|
|
227
|
+
const live = createChatStreamReducer({ transport: 'nats', displayApprovalTypes: NARROW_TIERS })
|
|
228
|
+
live.apply({ type: 'turn-start' })
|
|
229
|
+
live.apply(
|
|
230
|
+
decodeNatsChunk({
|
|
231
|
+
type: 'GUIDE',
|
|
232
|
+
payload: {
|
|
233
|
+
kind: 'approval_request',
|
|
234
|
+
proposalId: 'prop-1',
|
|
235
|
+
toolName: 'create_ticket',
|
|
236
|
+
title: 'Open a support ticket',
|
|
237
|
+
fields: [{ label: 'Subject', value: 'Agent will not enroll' }],
|
|
238
|
+
},
|
|
239
|
+
})!,
|
|
240
|
+
)
|
|
241
|
+
const streamed = cardData(live.state.messages.flatMap((m) => approvalSegments(m.segments)))
|
|
242
|
+
|
|
243
|
+
// Same card either way — down to the rows that ARE its body, and the marker
|
|
244
|
+
// that decides which backend its buttons talk to.
|
|
245
|
+
expect(replayed).toEqual(streamed)
|
|
246
|
+
expect(replayed?.origin).toBe('guide')
|
|
247
|
+
expect(replayed?.approvalType).toBe('create_ticket')
|
|
248
|
+
expect(replayed?.fields).toEqual([{ label: 'Subject', value: 'Agent will not enroll' }])
|
|
249
|
+
})
|
|
250
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE an approval card renders — the one rule, read by every path that turns
|
|
3
|
+
* an approval into segments.
|
|
4
|
+
*
|
|
5
|
+
* There are three such paths and they must agree: the NATS kernel (live chunks),
|
|
6
|
+
* the SSE kernel, and the history replay that rebuilds a dialog after a reload.
|
|
7
|
+
* When the rule lived only in the live kernel, a Product Guide card rendered
|
|
8
|
+
* inline while streaming and then moved into the escalation treatment on the
|
|
9
|
+
* next page load — same card, two behaviors, and its buttons pointed at the
|
|
10
|
+
* wrong backend once it moved.
|
|
11
|
+
*
|
|
12
|
+
* The rule itself: a card is gated on approval TIER (`displayApprovalTypes`,
|
|
13
|
+
* the agent's own privilege model) UNLESS it came from the Product Guide, whose
|
|
14
|
+
* proposals are typed by TOOL and are resolved through the hub's confirm route.
|
|
15
|
+
* A hub proposal has no tier to gate on, so tier-gating one means escalating it
|
|
16
|
+
* to a human who has no way to act on it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { GuideOrigin } from '../../../chat-protocol/events'
|
|
20
|
+
import { GUIDE_ORIGIN, isGuideOrigin } from '../../../chat-protocol/events'
|
|
21
|
+
import type { MessageSegment } from '../types/message.types'
|
|
22
|
+
|
|
23
|
+
/** Anything carrying the optional `origin` marker: a stream event, or the
|
|
24
|
+
* `data` of a rendered segment. */
|
|
25
|
+
interface OriginBearing {
|
|
26
|
+
origin?: GuideOrigin | string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when this approval was minted by the Product Guide (the hub) rather
|
|
30
|
+
* than by the agent's own tool-approval tiering. */
|
|
31
|
+
export function isGuideApproval(source: OriginBearing | null | undefined): boolean {
|
|
32
|
+
return isGuideOrigin(source)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The marker to stamp onto a built segment, or undefined for agent approvals.
|
|
36
|
+
* Keeps `origin` out of segment data unless it means something. */
|
|
37
|
+
export function guideApprovalOrigin(
|
|
38
|
+
source: OriginBearing | null | undefined,
|
|
39
|
+
): GuideOrigin | undefined {
|
|
40
|
+
return isGuideApproval(source) ? GUIDE_ORIGIN : undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Does this approval render inline in its turn (true), or take the consumer's
|
|
45
|
+
* escalation treatment (false)?
|
|
46
|
+
*
|
|
47
|
+
* `displayApprovalTypes` undefined means "no tier gate configured" and every
|
|
48
|
+
* agent approval displays — the pre-existing history-replay default, preserved
|
|
49
|
+
* here so both kernels can share one predicate.
|
|
50
|
+
*/
|
|
51
|
+
export function approvalDisplaysInline(
|
|
52
|
+
source: OriginBearing | null | undefined,
|
|
53
|
+
approvalType: string,
|
|
54
|
+
displayApprovalTypes?: readonly string[],
|
|
55
|
+
): boolean {
|
|
56
|
+
if (isGuideApproval(source)) return true
|
|
57
|
+
return !displayApprovalTypes || displayApprovalTypes.includes(approvalType)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* True for a rendered segment that holds a Product Guide card (single or batch).
|
|
62
|
+
*
|
|
63
|
+
* Consumers use it to keep such a card in the message flow, where the hub's own
|
|
64
|
+
* chat puts it, instead of lifting it into their sticky pending-approvals
|
|
65
|
+
* footer — and to route its buttons to the hub rather than to the agent.
|
|
66
|
+
*/
|
|
67
|
+
export function isGuideApprovalSegment(segment: MessageSegment): boolean {
|
|
68
|
+
if (segment.type !== 'approval_request' && segment.type !== 'approval_batch') return false
|
|
69
|
+
return isGuideApproval(segment.data)
|
|
70
|
+
}
|
|
@@ -14,6 +14,15 @@ export {
|
|
|
14
14
|
type AccumulatorCallbacks,
|
|
15
15
|
} from './message-segment-accumulator'
|
|
16
16
|
|
|
17
|
+
// Where an approval card renders — one rule for both live kernels, the history
|
|
18
|
+
// replay, and hosts that sort cards between their own footer and the flow.
|
|
19
|
+
export {
|
|
20
|
+
isGuideApproval,
|
|
21
|
+
guideApprovalOrigin,
|
|
22
|
+
approvalDisplaysInline,
|
|
23
|
+
isGuideApprovalSegment,
|
|
24
|
+
} from './approval-display'
|
|
25
|
+
|
|
17
26
|
// Historical message processing
|
|
18
27
|
export {
|
|
19
28
|
processHistoricalMessages,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
AskOptionData,
|
|
13
13
|
MessageSegment,
|
|
14
14
|
ToolExecutionSegment,
|
|
15
|
+
ApprovalRequestField,
|
|
15
16
|
ApprovalRequestSegment,
|
|
16
17
|
ApprovalBatchSegment,
|
|
17
18
|
ApprovalBatchExecutionState,
|
|
@@ -324,23 +325,55 @@ export class MessageSegmentAccumulator {
|
|
|
324
325
|
command: string,
|
|
325
326
|
explanation: string | undefined,
|
|
326
327
|
approvalType: string,
|
|
327
|
-
status: ChatApprovalStatus = 'pending'
|
|
328
|
+
status: ChatApprovalStatus = 'pending',
|
|
329
|
+
/** Structured label/value rows. The card prefers them over `explanation`
|
|
330
|
+
* (see `ApprovalRequestData.fields`). Optional because the agent's own
|
|
331
|
+
* approvals carry prose; a Product Guide card is almost entirely fields —
|
|
332
|
+
* dropping them here left it as a bare title. */
|
|
333
|
+
fields?: ApprovalRequestField[],
|
|
334
|
+
/** Where the card came from; `'guide'` keeps it inline (see
|
|
335
|
+
* `ApprovalRequestData.origin`). */
|
|
336
|
+
origin?: 'guide'
|
|
328
337
|
): MessageSegment[] {
|
|
329
|
-
|
|
338
|
+
this.segments.push(
|
|
339
|
+
this.buildApprovalRequestSegment(
|
|
340
|
+
requestId,
|
|
341
|
+
{ command, explanation, approvalType, fields, origin },
|
|
342
|
+
status,
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
return this.getSegments()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Build one approval-request segment — THE constructor for this segment type.
|
|
350
|
+
*
|
|
351
|
+
* Three paths produce these: `addApprovalRequest` (live stream and replay),
|
|
352
|
+
* `flushPendingApprovals` (tracked-but-unresolved after a history replay) and
|
|
353
|
+
* `processApprovalResult` (a result arriving for a tracked request). They used
|
|
354
|
+
* to hand-write the object each, which is how `fields` and `origin` reached
|
|
355
|
+
* the card down one path and not the others — the same proposal rendering as a
|
|
356
|
+
* bare title, or losing the marker that routes its buttons to the hub.
|
|
357
|
+
*/
|
|
358
|
+
private buildApprovalRequestSegment(
|
|
359
|
+
requestId: string,
|
|
360
|
+
approval: PendingApproval,
|
|
361
|
+
status: ChatApprovalStatus,
|
|
362
|
+
): ApprovalRequestSegment {
|
|
363
|
+
return {
|
|
330
364
|
type: 'approval_request',
|
|
331
365
|
data: {
|
|
332
|
-
command,
|
|
333
|
-
explanation,
|
|
366
|
+
command: approval.command,
|
|
367
|
+
explanation: approval.explanation,
|
|
334
368
|
requestId,
|
|
335
|
-
approvalType,
|
|
369
|
+
approvalType: approval.approvalType,
|
|
370
|
+
...(approval.fields && approval.fields.length > 0 ? { fields: approval.fields } : {}),
|
|
371
|
+
...(approval.origin ? { origin: approval.origin } : {}),
|
|
336
372
|
},
|
|
337
373
|
status,
|
|
338
374
|
onApprove: this.callbacks.onApprove,
|
|
339
375
|
onReject: this.callbacks.onReject,
|
|
340
376
|
}
|
|
341
|
-
|
|
342
|
-
this.segments.push(segment)
|
|
343
|
-
return this.getSegments()
|
|
344
377
|
}
|
|
345
378
|
|
|
346
379
|
/**
|
|
@@ -363,6 +396,9 @@ export class MessageSegmentAccumulator {
|
|
|
363
396
|
status: ChatApprovalStatus = 'pending',
|
|
364
397
|
executions?: Record<string, ApprovalBatchExecutionState>,
|
|
365
398
|
resolvedByName?: string | null,
|
|
399
|
+
/** Where the batch came from; `'guide'` keeps it inline and routes it to
|
|
400
|
+
* the hub, exactly as for a single card (see `ApprovalBatchData.origin`). */
|
|
401
|
+
origin?: 'guide',
|
|
366
402
|
): MessageSegment[] {
|
|
367
403
|
const existingIndex = this.segments.findIndex(
|
|
368
404
|
(s): s is ApprovalBatchSegment =>
|
|
@@ -372,6 +408,9 @@ export class MessageSegmentAccumulator {
|
|
|
372
408
|
if (existingIndex !== -1) {
|
|
373
409
|
const existing = this.segments[existingIndex] as ApprovalBatchSegment
|
|
374
410
|
const mergedExecutions = executions ?? existing.data.executions
|
|
411
|
+
// An upsert must not strip the marker: the flip to `approved` comes from
|
|
412
|
+
// a later event that carries no origin of its own.
|
|
413
|
+
const mergedOrigin = origin ?? existing.data.origin
|
|
375
414
|
this.segments[existingIndex] = {
|
|
376
415
|
...existing,
|
|
377
416
|
data: {
|
|
@@ -379,6 +418,7 @@ export class MessageSegmentAccumulator {
|
|
|
379
418
|
approvalType,
|
|
380
419
|
toolCalls,
|
|
381
420
|
...(mergedExecutions ? { executions: mergedExecutions } : {}),
|
|
421
|
+
...(mergedOrigin ? { origin: mergedOrigin } : {}),
|
|
382
422
|
},
|
|
383
423
|
status,
|
|
384
424
|
resolvedByName: resolvedByName ?? existing.resolvedByName,
|
|
@@ -395,6 +435,7 @@ export class MessageSegmentAccumulator {
|
|
|
395
435
|
approvalType,
|
|
396
436
|
toolCalls,
|
|
397
437
|
...(executions ? { executions } : {}),
|
|
438
|
+
...(origin ? { origin } : {}),
|
|
398
439
|
},
|
|
399
440
|
status,
|
|
400
441
|
resolvedByName,
|
|
@@ -417,20 +458,17 @@ export class MessageSegmentAccumulator {
|
|
|
417
458
|
): { segment: ApprovalRequestSegment; pendingData: PendingApproval | null } | null {
|
|
418
459
|
const pendingApproval = this.pendingApprovals.get(requestId)
|
|
419
460
|
const status: ChatApprovalStatus = approved ? 'approved' : 'rejected'
|
|
420
|
-
|
|
421
|
-
const segment
|
|
422
|
-
|
|
423
|
-
|
|
461
|
+
|
|
462
|
+
const segment = this.buildApprovalRequestSegment(
|
|
463
|
+
requestId,
|
|
464
|
+
{
|
|
465
|
+
...pendingApproval,
|
|
424
466
|
command: pendingApproval?.command || '',
|
|
425
|
-
explanation: pendingApproval?.explanation,
|
|
426
|
-
requestId,
|
|
427
467
|
approvalType: pendingApproval?.approvalType || approvalType,
|
|
428
468
|
},
|
|
429
469
|
status,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
470
|
+
)
|
|
471
|
+
|
|
434
472
|
this.segments.push(segment)
|
|
435
473
|
|
|
436
474
|
if (pendingApproval) {
|
|
@@ -538,22 +576,11 @@ export class MessageSegmentAccumulator {
|
|
|
538
576
|
*/
|
|
539
577
|
flushPendingApprovals(): ApprovalRequestSegment[] {
|
|
540
578
|
const segments: ApprovalRequestSegment[] = []
|
|
541
|
-
|
|
579
|
+
|
|
542
580
|
this.pendingApprovals.forEach((approval, requestId) => {
|
|
543
|
-
segments.push(
|
|
544
|
-
type: 'approval_request',
|
|
545
|
-
data: {
|
|
546
|
-
command: approval.command,
|
|
547
|
-
explanation: approval.explanation,
|
|
548
|
-
requestId,
|
|
549
|
-
approvalType: approval.approvalType,
|
|
550
|
-
},
|
|
551
|
-
status: 'pending',
|
|
552
|
-
onApprove: this.callbacks.onApprove,
|
|
553
|
-
onReject: this.callbacks.onReject,
|
|
554
|
-
})
|
|
581
|
+
segments.push(this.buildApprovalRequestSegment(requestId, approval, 'pending'))
|
|
555
582
|
})
|
|
556
|
-
|
|
583
|
+
|
|
557
584
|
return segments
|
|
558
585
|
}
|
|
559
586
|
|
|
@@ -627,6 +654,8 @@ export class MessageSegmentAccumulator {
|
|
|
627
654
|
data.explanation,
|
|
628
655
|
data.approvalType || '',
|
|
629
656
|
status,
|
|
657
|
+
data.fields,
|
|
658
|
+
data.origin,
|
|
630
659
|
)
|
|
631
660
|
break
|
|
632
661
|
}
|
|
@@ -639,6 +668,7 @@ export class MessageSegmentAccumulator {
|
|
|
639
668
|
status,
|
|
640
669
|
data.executions,
|
|
641
670
|
resolvedByName,
|
|
671
|
+
data.origin,
|
|
642
672
|
)
|
|
643
673
|
break
|
|
644
674
|
}
|
|
@@ -40,7 +40,8 @@ import {
|
|
|
40
40
|
import { ESCALATION_STATE, escalationResolvedStatus, type ChatStreamEvent } from '../../../chat-protocol/events'
|
|
41
41
|
// One normalizer for ask rows, shared with the live decoder — history and the
|
|
42
42
|
// stream must agree on which options are usable.
|
|
43
|
-
import { normalizeAskOptions } from '../../../chat-protocol/nats-decoder'
|
|
43
|
+
import { guideFrameEvent, normalizeAskOptions } from '../../../chat-protocol/nats-decoder'
|
|
44
|
+
import { approvalDisplaysInline, guideApprovalOrigin } from './approval-display'
|
|
44
45
|
import { MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator'
|
|
45
46
|
import { applyApprovalStatusToSegment } from '../stream/message-mutations'
|
|
46
47
|
import { getCommandText } from './tool-call-helpers'
|
|
@@ -115,11 +116,21 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
|
|
|
115
116
|
}
|
|
116
117
|
return null
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
// Two persisted shapes, mirroring the live `GUIDE` chunk: the answer body
|
|
120
|
+
// (`text`) and a Product Guide frame the agent re-streamed (`payload`,
|
|
121
|
+
// persisted so a card survives a reload). Decoded through the SAME
|
|
122
|
+
// `guideFrameEvent` the live path uses — a second mapping here would let
|
|
123
|
+
// history and realtime disagree about the same bytes.
|
|
124
|
+
case MESSAGE_TYPE.GUIDE: {
|
|
119
125
|
if ('text' in data && data.text) {
|
|
120
126
|
return { type: 'guide-delta', text: data.text }
|
|
121
127
|
}
|
|
128
|
+
const payload = 'payload' in data ? data.payload : undefined
|
|
129
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
130
|
+
return guideFrameEvent(payload as Record<string, unknown>)
|
|
131
|
+
}
|
|
122
132
|
return null
|
|
133
|
+
}
|
|
123
134
|
|
|
124
135
|
// Same completeness gate as the live decoder (`decodeNatsChunk`): a
|
|
125
136
|
// persisted row without a question or without options is not a card the
|
|
@@ -361,12 +372,24 @@ function applyHistoryEvent(
|
|
|
361
372
|
const approvalType = event.approvalType || 'CLIENT'
|
|
362
373
|
const toolCalls = event.toolCalls as PendingToolCallData[] | undefined
|
|
363
374
|
const isBatch = !!toolCalls && toolCalls.length > 0
|
|
375
|
+
// Same rule the live kernels use — a card must not change where it
|
|
376
|
+
// renders (or which backend its buttons hit) just because the page was
|
|
377
|
+
// reloaded and it came back through history instead of the stream.
|
|
378
|
+
const guideOrigin = guideApprovalOrigin(event)
|
|
364
379
|
|
|
365
|
-
if (
|
|
380
|
+
if (approvalDisplaysInline(event, approvalType, displayApprovalTypes)) {
|
|
366
381
|
if (isBatch) {
|
|
367
382
|
const status = (approvalStatuses[event.requestId] as ChatApprovalStatus) || 'pending'
|
|
368
383
|
if (batchApprovalsEnabled) {
|
|
369
|
-
accumulator.addApprovalBatch(
|
|
384
|
+
accumulator.addApprovalBatch(
|
|
385
|
+
event.requestId,
|
|
386
|
+
approvalType,
|
|
387
|
+
toolCalls!,
|
|
388
|
+
status,
|
|
389
|
+
undefined,
|
|
390
|
+
undefined,
|
|
391
|
+
guideOrigin,
|
|
392
|
+
)
|
|
370
393
|
} else {
|
|
371
394
|
// Flag OFF — unfold batch into N legacy approval cards (same id).
|
|
372
395
|
for (const call of toolCalls!) {
|
|
@@ -377,6 +400,8 @@ function applyHistoryEvent(
|
|
|
377
400
|
call.toolExplanation,
|
|
378
401
|
approvalType,
|
|
379
402
|
status,
|
|
403
|
+
undefined,
|
|
404
|
+
guideOrigin,
|
|
380
405
|
)
|
|
381
406
|
}
|
|
382
407
|
}
|
|
@@ -389,19 +414,28 @@ function applyHistoryEvent(
|
|
|
389
414
|
// sticky card on every history re-process. Mirror the batch path
|
|
390
415
|
// and honor `approvalStatuses`.
|
|
391
416
|
const resolvedStatus = approvalStatuses[event.requestId] as ChatApprovalStatus | undefined
|
|
392
|
-
|
|
417
|
+
const isResolved = resolvedStatus === 'approved' || resolvedStatus === 'rejected'
|
|
418
|
+
// A guide card is added inline even while pending. The tracked path
|
|
419
|
+
// below ends in `flushPendingApprovals`, whose segments the consumer
|
|
420
|
+
// lifts into a sticky footer — that is the treatment for the
|
|
421
|
+
// consumer's OWN approvals, and it is not how the hub's chat renders
|
|
422
|
+
// a proposal (nor where its preamble expects it).
|
|
423
|
+
if (guideOrigin || isResolved) {
|
|
393
424
|
accumulator.addApprovalRequest(
|
|
394
425
|
event.requestId,
|
|
395
426
|
event.command || '',
|
|
396
427
|
event.explanation,
|
|
397
428
|
approvalType,
|
|
398
|
-
resolvedStatus,
|
|
429
|
+
resolvedStatus ?? 'pending',
|
|
430
|
+
event.fields,
|
|
431
|
+
guideOrigin,
|
|
399
432
|
)
|
|
400
433
|
} else {
|
|
401
434
|
accumulator.trackApprovalRequest(event.requestId, {
|
|
402
435
|
command: event.command || '',
|
|
403
436
|
explanation: event.explanation,
|
|
404
437
|
approvalType,
|
|
438
|
+
fields: event.fields,
|
|
405
439
|
})
|
|
406
440
|
}
|
|
407
441
|
}
|