@flamingo-stack/openframe-frontend-core 0.0.849 → 0.0.851
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/build/export-map.json +1 -1
- package/dist/chat-protocol/events.d.ts +4 -1
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +90 -40
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/chat-protocol/nats-decoder.js +5 -1
- package/dist/chat-protocol/policy-review.d.ts +18 -0
- package/dist/chat-protocol/policy-review.d.ts.map +1 -0
- package/dist/chat-protocol/policy-review.js +55 -0
- package/dist/{chunk-C3P7IUTU.cjs → chunk-2NGW73T2.cjs} +5 -4
- package/dist/{chunk-SHQY4GOE.cjs → chunk-2WZHNMQY.cjs} +67 -67
- package/dist/{chunk-R5JPZ6TY.cjs → chunk-5SEJEBAB.cjs} +8 -8
- package/dist/{chunk-TX4NTH22.cjs → chunk-B6D4WWKD.cjs} +36 -36
- package/dist/{chunk-FXLROQUT.cjs → chunk-CDW6JTAQ.cjs} +442 -459
- package/dist/{chunk-5PIR77V5.cjs → chunk-DN227KZH.cjs} +4 -4
- package/dist/{chunk-MC3L525K.cjs → chunk-FGSN5UNY.cjs} +3 -3
- package/dist/{chunk-ZGEWT3SF.cjs → chunk-HXURNKXI.cjs} +3 -3
- package/dist/{chunk-FRG724DU.cjs → chunk-KRTTH3ET.cjs} +4 -4
- package/dist/{chunk-YJZ7R3W5.cjs → chunk-KXFIYO4O.cjs} +8 -8
- package/dist/{chunk-4YBS4HIO.cjs → chunk-LM7EL3UQ.cjs} +444 -27
- package/dist/{chunk-YYHQY3K6.cjs → chunk-NQEPLUUW.cjs} +21 -21
- package/dist/{chunk-KXBZCUDW.cjs → chunk-ODMZSAIY.cjs} +36 -36
- package/dist/{chunk-36XWCVSM.cjs → chunk-OUP7L3U5.cjs} +50 -50
- package/dist/{chunk-5CBI2ZYV.cjs → chunk-PJNEBSP5.cjs} +5 -5
- package/dist/{chunk-6AJYGXXS.cjs → chunk-PPCRLSVR.cjs} +11 -11
- package/dist/{chunk-SA2NNWIJ.cjs → chunk-QCSA7GIX.cjs} +17 -62
- package/dist/{chunk-PV2XQJ2R.cjs → chunk-SPKAXETT.cjs} +42 -42
- package/dist/{chunk-IKOL6NJY.cjs → chunk-U4RTWH5D.cjs} +53 -43
- package/dist/{chunk-IHY7E4EX.cjs → chunk-UFGPLXH6.cjs} +4 -4
- package/dist/{chunk-TEKN6TJ5.cjs → chunk-UQK6J3ZS.cjs} +13 -13
- package/dist/{chunk-S3QXLD4X.cjs → chunk-VCFQHPOO.cjs} +37 -3
- package/dist/{chunk-Z5UCDEKZ.cjs → chunk-VU6N3NZS.cjs} +6 -6
- package/dist/{chunk-ZY34R37A.cjs → chunk-VXIZCUNZ.cjs} +61 -61
- package/dist/{chunk-3W4ASAXN.cjs → chunk-WYROOMUZ.cjs} +2 -2
- package/dist/{chunk-YLETC7LP.cjs → chunk-ZEPEFKHA.cjs} +893 -973
- package/dist/{cloud-tenants-HKUUEUAY.cjs → cloud-tenants-XH6GV4EI.cjs} +12 -13
- package/dist/components/case-studies/index.cjs +24 -25
- package/dist/components/chat/approval-batch-message.d.ts +5 -0
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
- package/dist/components/chat/approval-batch-message.js +36 -10
- package/dist/components/chat/index.cjs +8 -9
- package/dist/components/chat/policy-review.d.ts +27 -0
- package/dist/components/chat/policy-review.d.ts.map +1 -0
- package/dist/components/chat/policy-review.js +67 -0
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
- package/dist/components/chat/stream/message-mutations.js +4 -1
- package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
- package/dist/components/chat/tool-execution-display.js +16 -5
- package/dist/components/chat/types/message.types.d.ts +40 -2
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.js +2 -0
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.js +3 -0
- package/dist/components/chat/utils/process-historical-messages.d.ts +2 -2
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.js +7 -1
- package/dist/components/contact/index.cjs +13 -14
- package/dist/components/docs/index.cjs +16 -17
- package/dist/components/embeds/embed-viewer-frame.d.ts +2 -0
- package/dist/components/embeds/embed-viewer-frame.d.ts.map +1 -1
- package/dist/components/embeds/embed-viewer-frame.js +12 -2
- package/dist/components/embeds/index.cjs +13 -14
- package/dist/components/embeds/pdf-viewer.d.ts.map +1 -1
- package/dist/components/embeds/pdf-viewer.js +2 -1
- package/dist/components/faq/index.cjs +14 -15
- package/dist/components/features/approval-level.d.ts +6 -12
- package/dist/components/features/approval-level.d.ts.map +1 -1
- package/dist/components/features/approval-level.js +45 -55
- package/dist/components/features/index.cjs +17 -15
- package/dist/components/features/notifications/approval-request-notification-tile.d.ts.map +1 -1
- package/dist/components/features/notifications/approval-request-notification-tile.js +71 -48
- package/dist/components/features/notifications/types.d.ts +2 -0
- package/dist/components/features/notifications/types.d.ts.map +1 -1
- package/dist/components/features/notifications/types.js +3 -1
- package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
- package/dist/components/features/policy-configuration-panel.js +75 -64
- package/dist/components/features/software/index.d.ts +1 -1
- package/dist/components/features/software/index.d.ts.map +1 -1
- package/dist/components/features/software/index.js +2 -0
- package/dist/components/features/software/software-action-copy.d.ts.map +1 -1
- package/dist/components/features/software/software-action-copy.js +6 -12
- package/dist/components/features/software/software-action-form.d.ts +7 -1
- package/dist/components/features/software/software-action-form.d.ts.map +1 -1
- package/dist/components/features/software/software-action-form.js +11 -3
- package/dist/components/features/software/update-settings-fields.d.ts +12 -1
- package/dist/components/features/software/update-settings-fields.d.ts.map +1 -1
- package/dist/components/features/software/update-settings-fields.js +57 -29
- package/dist/components/help-center-pages/index.cjs +78 -79
- package/dist/components/index.cjs +58 -58
- package/dist/components/meeting-scheduler/index.cjs +61 -61
- package/dist/components/navigation/index.cjs +15 -16
- package/dist/components/onboarding-guides/index.cjs +14 -15
- package/dist/components/product-demo/index.cjs +23 -24
- package/dist/components/tickets/index.cjs +17 -18
- package/dist/components/ui/file-manager/index.cjs +8 -9
- package/dist/components/ui/index.cjs +16 -18
- package/dist/components/ui/page-actions.d.ts.map +1 -1
- package/dist/components/ui/page-actions.js +11 -1
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group.js +8 -2
- package/dist/components/ui/tab-selector.d.ts +7 -1
- package/dist/components/ui/tab-selector.d.ts.map +1 -1
- package/dist/components/ui/tab-selector.js +22 -3
- package/dist/{devices-P7M4Z7VP.cjs → devices-7PT6MPJI.cjs} +13 -14
- package/dist/{embeddable-chat-322NDFP5.cjs → embeddable-chat-5NY76K7K.cjs} +84 -85
- package/dist/index.cjs +24 -24
- package/dist/{knowledge-TC7JLOWI.cjs → knowledge-J6N7IEVI.cjs} +13 -14
- package/dist/{logs-GPOYCP4B.cjs → logs-D6KQP2BX.cjs} +19 -20
- package/dist/{policies-ZUHOUMDD.cjs → policies-J7MCOL3C.cjs} +16 -17
- package/dist/{remote-session-NT4NG7EF.cjs → remote-session-PXHA5KFK.cjs} +8 -9
- package/dist/{scripts-FOEVDGTH.cjs → scripts-VURXIHVS.cjs} +19 -20
- package/dist/{software-update-XGTMNFST.cjs → software-update-6SHUBDIE.cjs} +16 -17
- package/dist/{tickets-board-7FLGQEHF.cjs → tickets-board-NLCN7WNX.cjs} +9 -9
- package/package.json +1 -1
- package/src/chat-protocol/events.ts +4 -1
- package/src/chat-protocol/nats-decoder.ts +4 -0
- package/src/chat-protocol/policy-review.ts +71 -0
- package/src/components/chat/approval-batch-message.tsx +47 -10
- package/src/components/chat/policy-review.tsx +90 -0
- package/src/components/chat/stream/message-mutations.ts +4 -0
- package/src/components/chat/tool-execution-display.tsx +23 -4
- package/src/components/chat/types/message.types.ts +42 -2
- package/src/components/chat/utils/extract-incomplete-message-state.ts +2 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +3 -0
- package/src/components/chat/utils/process-historical-messages.ts +14 -3
- package/src/components/embeds/embed-viewer-frame.tsx +10 -2
- package/src/components/embeds/pdf-viewer.tsx +4 -1
- package/src/components/features/approval-level.tsx +74 -78
- package/src/components/features/notifications/approval-request-notification-tile.tsx +75 -50
- package/src/components/features/notifications/types.ts +4 -0
- package/src/components/features/policy-configuration-panel.tsx +100 -83
- package/src/components/features/software/index.ts +1 -0
- package/src/components/features/software/software-action-copy.ts +19 -12
- package/src/components/features/software/software-action-form.tsx +16 -3
- package/src/components/features/software/update-settings-fields.tsx +104 -32
- package/src/components/ui/page-actions.tsx +19 -2
- package/src/components/ui/radio-group.tsx +20 -2
- package/src/components/ui/tab-selector.tsx +26 -3
- package/dist/chunk-N3MBQRCQ.cjs +0 -41
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { PolicyReviewData, PolicyReviewRuleData } from '../components/chat/types/message.types';
|
|
2
|
+
import type { ApprovalLevel } from '../types/permissions';
|
|
3
|
+
import { isRecord } from './wire-narrow';
|
|
4
|
+
|
|
5
|
+
const APPROVAL_LEVELS: ReadonlySet<string> = new Set<ApprovalLevel>(['ALLOW', 'ASK_USER', 'ASK_TECHNICIAN', 'DENY']);
|
|
6
|
+
|
|
7
|
+
function text(value: unknown): string | undefined {
|
|
8
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function level(value: unknown): ApprovalLevel | undefined {
|
|
12
|
+
return typeof value === 'string' && APPROVAL_LEVELS.has(value) ? (value as ApprovalLevel) : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rules(raw: unknown): PolicyReviewRuleData[] {
|
|
16
|
+
if (!Array.isArray(raw)) return [];
|
|
17
|
+
return raw.filter(isRecord).flatMap(rule => {
|
|
18
|
+
const code = text(rule.code);
|
|
19
|
+
const label = text(rule.label) ?? code;
|
|
20
|
+
return label ? [{ code: code ?? label, label }] : [];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function codes(raw: unknown): string[] {
|
|
25
|
+
if (!Array.isArray(raw)) return [];
|
|
26
|
+
return raw.map(text).filter((code): code is string => !!code);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `{ policyReview }` when the wire carries one, else `{}`: the key is additive, so a row without a review keeps its
|
|
30
|
+
* exact shape (history, the live stream and their golden corpora). */
|
|
31
|
+
export function policyReviewField(raw: unknown): { policyReview?: PolicyReviewData } {
|
|
32
|
+
const policyReview = normalizePolicyReview(raw);
|
|
33
|
+
return policyReview ? { policyReview } : {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The first review of the ones given, as an additive field (see {@link policyReviewField}). */
|
|
37
|
+
export function keptPolicyReview(...reviews: Array<PolicyReviewData | undefined>): {
|
|
38
|
+
policyReview?: PolicyReviewData;
|
|
39
|
+
} {
|
|
40
|
+
const policyReview = reviews.find(review => review !== undefined);
|
|
41
|
+
return policyReview ? { policyReview } : {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The wire's `policyReview` (NATS chunk or history row) as the chat reads it, or `undefined` when there is none.
|
|
46
|
+
* Rules without a name and blank operation codes are dropped; an approval level this build does not know is dropped
|
|
47
|
+
* alone, not the review; a review with nothing left in it is `undefined`. `decision` and `source` stay open strings (the backend is folding the two deny kinds into
|
|
48
|
+
* one `DENY`).
|
|
49
|
+
*/
|
|
50
|
+
export function normalizePolicyReview(raw: unknown): PolicyReviewData | undefined {
|
|
51
|
+
if (!isRecord(raw)) return undefined;
|
|
52
|
+
const source = text(raw.source);
|
|
53
|
+
const decision = text(raw.decision);
|
|
54
|
+
const approvalLevel = level(raw.approvalLevel);
|
|
55
|
+
const policyLevel = level(raw.policyLevel);
|
|
56
|
+
const reason = text(raw.reason);
|
|
57
|
+
const operationCodes = codes(raw.operationCodes);
|
|
58
|
+
const matchedRules = rules(raw.matchedRules);
|
|
59
|
+
if (!decision && !approvalLevel && !policyLevel && !reason && !operationCodes.length && !matchedRules.length) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...(source ? { source } : {}),
|
|
64
|
+
...(decision ? { decision } : {}),
|
|
65
|
+
...(approvalLevel ? { approvalLevel } : {}),
|
|
66
|
+
...(policyLevel ? { policyLevel } : {}),
|
|
67
|
+
operationCodes,
|
|
68
|
+
matchedRules,
|
|
69
|
+
...(reason ? { reason } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { forwardRef, useState } from 'react';
|
|
4
4
|
import type { HTMLAttributes } from 'react';
|
|
5
5
|
import { cn } from '../../utils/cn';
|
|
6
|
-
import { CheckCircleIcon, DotsLoaderIcon, XmarkCircleIcon } from '../icons-v2-generated';
|
|
6
|
+
import { AlertCircleIcon, CheckCircleIcon, DotsLoaderIcon, XmarkCircleIcon } from '../icons-v2-generated';
|
|
7
7
|
import { MediaSkeleton, TextLineSkeleton } from '../loading/unified-skeleton';
|
|
8
8
|
import type { ToolType } from '../platform';
|
|
9
9
|
import { ToolIcon } from '../tool-icon';
|
|
@@ -13,6 +13,7 @@ import { StatusLine } from '../ui/status-line';
|
|
|
13
13
|
import { CHAT_BLOCK_CLASS, CHAT_BLOCK_FRAME_CLASS } from './chat-block-frame';
|
|
14
14
|
import { ExpandChevron } from './expand-chevron';
|
|
15
15
|
import { useCollapsible } from './hooks/use-collapsible';
|
|
16
|
+
import { PolicyEscalationAlert, PolicyReviewRows, hasPolicyReviewRows, isPolicyEscalated } from './policy-review';
|
|
16
17
|
import { ArgRow, ResultBlock } from './tool-call-blocks';
|
|
17
18
|
import type {
|
|
18
19
|
ApprovalBlockVariant,
|
|
@@ -65,6 +66,11 @@ export interface ApprovalBatchMessageProps extends HTMLAttributes<HTMLDivElement
|
|
|
65
66
|
* notification tile. Explanation bullets still render.
|
|
66
67
|
*/
|
|
67
68
|
showFooterActions?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Draw the "Mingo Escalated" band under each escalated command. Turn off when the host draws it itself — the
|
|
71
|
+
* approval notification tile shows it while its commands are folded away. Defaults to on.
|
|
72
|
+
*/
|
|
73
|
+
showEscalationAlerts?: boolean;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
const COMMAND_BODY_KEYS = new Set<string>(COMMAND_BODY_ARG_KEYS);
|
|
@@ -106,7 +112,7 @@ function getArgEntries(call: PendingToolCallData): Array<[string, unknown]> {
|
|
|
106
112
|
|
|
107
113
|
/**
|
|
108
114
|
* Status icon for one tool call inside an approved/done batch.
|
|
109
|
-
* - pending batch → null (chevron-only row, no status icon)
|
|
115
|
+
* - pending batch → null (chevron-only row, no status icon); a warning circle when Mingo escalated the call
|
|
110
116
|
* - approved, no exec → DotsLoaderIcon (queued / waiting for backend)
|
|
111
117
|
* - executing → DotsLoaderIcon
|
|
112
118
|
* - done + success → green check
|
|
@@ -115,10 +121,15 @@ function getArgEntries(call: PendingToolCallData): Array<[string, unknown]> {
|
|
|
115
121
|
function ExecutionStatusIcon({
|
|
116
122
|
batchStatus,
|
|
117
123
|
execution,
|
|
124
|
+
escalated,
|
|
118
125
|
}: {
|
|
119
126
|
batchStatus: ApprovalBatchSegment['status'];
|
|
120
127
|
execution: ApprovalBatchExecutionState | undefined;
|
|
128
|
+
escalated: boolean;
|
|
121
129
|
}) {
|
|
130
|
+
if (batchStatus === 'pending' && escalated)
|
|
131
|
+
// Decorative: the "Mingo Escalated" band under the row says it in words.
|
|
132
|
+
return <AlertCircleIcon className="h-4 w-4 text-ods-warning" aria-hidden />;
|
|
122
133
|
if (batchStatus !== 'approved') return null;
|
|
123
134
|
if (!execution || execution.status === 'executing')
|
|
124
135
|
return <DotsLoaderIcon size={16} className="text-ods-text-secondary" />;
|
|
@@ -133,18 +144,30 @@ interface ToolCallRowProps {
|
|
|
133
144
|
batchStatus: ApprovalBatchSegment['status'];
|
|
134
145
|
execution: ApprovalBatchExecutionState | undefined;
|
|
135
146
|
showExecutionStatus: boolean;
|
|
147
|
+
showEscalationAlert: boolean;
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
// ADMIN-only row: command preview header, expandable args/result. The client
|
|
139
151
|
// variant never renders tool calls — see the `variant === 'client'` branch of
|
|
140
152
|
// `<ApprovalBatchMessage>`.
|
|
141
|
-
function ToolCallRow({
|
|
153
|
+
function ToolCallRow({
|
|
154
|
+
call,
|
|
155
|
+
expanded,
|
|
156
|
+
onToggle,
|
|
157
|
+
batchStatus,
|
|
158
|
+
execution,
|
|
159
|
+
showExecutionStatus,
|
|
160
|
+
showEscalationAlert,
|
|
161
|
+
}: ToolCallRowProps) {
|
|
142
162
|
const command = getCommandText(call);
|
|
143
163
|
const args = getArgEntries(call);
|
|
144
164
|
const toolType = (call.toolType as ToolType) || 'OPENFRAME';
|
|
145
165
|
const { innerRef, containerStyle } = useCollapsible({ expanded });
|
|
146
166
|
const result = execution?.status === 'done' ? execution.result : undefined;
|
|
147
|
-
const
|
|
167
|
+
const review = call.policyReview;
|
|
168
|
+
const escalated = isPolicyEscalated(review);
|
|
169
|
+
const reviewRows = hasPolicyReviewRows(review);
|
|
170
|
+
const hasExpandableBody = args.length > 0 || reviewRows || (typeof result === 'string' && result.length > 0);
|
|
148
171
|
|
|
149
172
|
return (
|
|
150
173
|
<div className="flex w-full flex-col items-start border-b border-ods-border bg-ods-card last:border-b-0">
|
|
@@ -165,9 +188,10 @@ function ToolCallRow({ call, expanded, onToggle, batchStatus, execution, showExe
|
|
|
165
188
|
>
|
|
166
189
|
{command}
|
|
167
190
|
</div>
|
|
168
|
-
{
|
|
191
|
+
{/* The escalation mark is the review's, not the run's: it shows where the run status is off too. */}
|
|
192
|
+
{(showExecutionStatus || (batchStatus === 'pending' && escalated)) && (
|
|
169
193
|
<div className="flex h-5 w-5 shrink-0 items-center justify-center">
|
|
170
|
-
<ExecutionStatusIcon batchStatus={batchStatus} execution={execution} />
|
|
194
|
+
<ExecutionStatusIcon batchStatus={batchStatus} execution={execution} escalated={escalated} />
|
|
171
195
|
</div>
|
|
172
196
|
)}
|
|
173
197
|
<div className="flex h-5 w-5 shrink-0 items-center justify-center">
|
|
@@ -182,16 +206,25 @@ function ToolCallRow({ call, expanded, onToggle, batchStatus, execution, showExe
|
|
|
182
206
|
{args.map(([key, value]) => (
|
|
183
207
|
<ArgRow key={key} argKey={key} value={value} />
|
|
184
208
|
))}
|
|
209
|
+
{reviewRows && (
|
|
210
|
+
<PolicyReviewRows
|
|
211
|
+
review={review}
|
|
212
|
+
className={args.length > 0 ? 'mt-[var(--spacing-system-xsf)]' : undefined}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
185
215
|
{result && (
|
|
186
216
|
<ResultBlock
|
|
187
217
|
result={result}
|
|
188
|
-
className={args.length > 0 ? 'mt-[var(--spacing-system-xsf)]' : undefined}
|
|
218
|
+
className={args.length > 0 || reviewRows ? 'mt-[var(--spacing-system-xsf)]' : undefined}
|
|
189
219
|
/>
|
|
190
220
|
)}
|
|
191
221
|
</div>
|
|
192
222
|
)}
|
|
193
223
|
</div>
|
|
194
224
|
</div>
|
|
225
|
+
|
|
226
|
+
{/* Outside the collapsible part: an escalation is read before anyone opens the command. */}
|
|
227
|
+
{showEscalationAlert && <PolicyEscalationAlert review={review} className="border-t border-ods-border" />}
|
|
195
228
|
</div>
|
|
196
229
|
);
|
|
197
230
|
}
|
|
@@ -300,6 +333,7 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
|
|
|
300
333
|
assistantType: _assistantType,
|
|
301
334
|
variant = 'admin',
|
|
302
335
|
showFooterActions = true,
|
|
336
|
+
showEscalationAlerts = true,
|
|
303
337
|
...props
|
|
304
338
|
},
|
|
305
339
|
ref,
|
|
@@ -323,6 +357,8 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
|
|
|
323
357
|
}
|
|
324
358
|
|
|
325
359
|
const explanations = data.toolCalls.map(c => c.toolExplanation?.trim()).filter((s): s is string => !!s);
|
|
360
|
+
// An approval Mingo escalated is approved in the warning colour (Figma 1374-105483), the admin view only.
|
|
361
|
+
const escalated = !isClient && data.toolCalls.some(c => isPolicyEscalated(c.policyReview));
|
|
326
362
|
|
|
327
363
|
const handleApprove = async () => {
|
|
328
364
|
setIsProcessing(true);
|
|
@@ -346,12 +382,12 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
|
|
|
346
382
|
<>
|
|
347
383
|
<Button
|
|
348
384
|
size="small-legacy"
|
|
349
|
-
variant=
|
|
385
|
+
variant={escalated ? 'warning' : 'accent'}
|
|
350
386
|
onClick={handleApprove}
|
|
351
387
|
disabled={isProcessing}
|
|
352
388
|
className={cn(
|
|
353
|
-
'bg-ods-accent hover:bg-ods-accent/90',
|
|
354
|
-
'text-
|
|
389
|
+
!escalated && 'bg-ods-accent text-ods-bg hover:bg-ods-accent/90',
|
|
390
|
+
'text-h5',
|
|
355
391
|
'h-8 px-[var(--spacing-system-xsf)]',
|
|
356
392
|
)}
|
|
357
393
|
>
|
|
@@ -430,6 +466,7 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
|
|
|
430
466
|
setExpandedId(prev => (prev === call.toolExecutionRequestId ? null : call.toolExecutionRequestId))
|
|
431
467
|
}
|
|
432
468
|
batchStatus={status}
|
|
469
|
+
showEscalationAlert={showEscalationAlerts}
|
|
433
470
|
execution={data.executions?.[call.toolExecutionRequestId]}
|
|
434
471
|
showExecutionStatus={showExecutionStatus}
|
|
435
472
|
/>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
4
|
+
import { APPROVAL_LEVEL_META } from '../features/approval-level';
|
|
5
|
+
import { AlertTriangleIcon } from '../icons-v2-generated';
|
|
6
|
+
import { Alert } from '../ui/alert';
|
|
7
|
+
import { ArgRow } from './tool-call-blocks';
|
|
8
|
+
import type { PolicyReviewData } from './types';
|
|
9
|
+
|
|
10
|
+
/** The decisions in which a matched guardrail rule, not the policy, set the level (`DENY` and the two it replaces). */
|
|
11
|
+
const RULE_DECISIONS: ReadonlySet<string> = new Set(['DENY', 'SOFT_DENY', 'HARD_DENY']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether Mingo escalated the call: a guardrail rule, not the policy, set its level. The decision says so; matched rules
|
|
15
|
+
* stand in only when a backend sends no decision. The one test behind the alert, the row's warning icon and the warning
|
|
16
|
+
* Approve.
|
|
17
|
+
*/
|
|
18
|
+
export function isPolicyEscalated(review: PolicyReviewData | undefined): boolean {
|
|
19
|
+
if (!review) return false;
|
|
20
|
+
return review.decision ? RULE_DECISIONS.has(review.decision) : review.matchedRules.length > 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The policy's level as the settings page names it. A call no rule of the policy matched gets the fallback, which the
|
|
25
|
+
* settings call "Unmatched Actions".
|
|
26
|
+
*/
|
|
27
|
+
function policyLevelText(review: PolicyReviewData): string | undefined {
|
|
28
|
+
// The level in force is the policy's only when no rule raised it.
|
|
29
|
+
const level = review.policyLevel ?? (isPolicyEscalated(review) ? undefined : review.approvalLevel);
|
|
30
|
+
if (!level) return undefined;
|
|
31
|
+
const label = APPROVAL_LEVEL_META[level].label;
|
|
32
|
+
return review.decision === 'FALLBACK' ? `${label} (Unmatched Actions)` : label;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether {@link PolicyReviewRows} draws anything: a body opens only onto rows it has. */
|
|
36
|
+
export function hasPolicyReviewRows(review: PolicyReviewData | undefined): review is PolicyReviewData {
|
|
37
|
+
return !!review && (!!policyLevelText(review) || review.operationCodes.length > 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The review in the command's body, under its arguments (Figma 1374-105458): the policy's level and the operations the
|
|
42
|
+
* review saw, as `key: value` rows like the arguments above them. The matched rules are the policy's own, so they get
|
|
43
|
+
* no row of their own; an escalation says why in its band.
|
|
44
|
+
*/
|
|
45
|
+
export function PolicyReviewRows({ review, className }: { review: PolicyReviewData; className?: string }) {
|
|
46
|
+
const level = policyLevelText(review);
|
|
47
|
+
if (!hasPolicyReviewRows(review)) return null;
|
|
48
|
+
return (
|
|
49
|
+
// A plain block: the rows stack on their own, and a nested flex column costs WebKit a layout pass (WEBVIEW-001).
|
|
50
|
+
<div className={cn('w-full', className)}>
|
|
51
|
+
{level && <ArgRow argKey="Guardrail Policy" value={level} />}
|
|
52
|
+
{review.operationCodes.length > 0 && <ArgRow argKey="Operations" value={review.operationCodes.join(', ')} />}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* "Mingo Escalated": the band under an escalated command, with the review's reason (Figma 1374-105482). Draws nothing
|
|
59
|
+
* for a call that was not escalated. Without a reason it names the rules instead.
|
|
60
|
+
*/
|
|
61
|
+
export function PolicyEscalationAlert({
|
|
62
|
+
review,
|
|
63
|
+
className,
|
|
64
|
+
}: {
|
|
65
|
+
review: PolicyReviewData | undefined;
|
|
66
|
+
className?: string;
|
|
67
|
+
}) {
|
|
68
|
+
if (!review || !isPolicyEscalated(review)) return null;
|
|
69
|
+
const description = review.reason ?? review.matchedRules.map(rule => rule.label).join(', ');
|
|
70
|
+
return (
|
|
71
|
+
// A note, not an `alert`: it is part of the thread, read in place, and a live region would interrupt the reader
|
|
72
|
+
// for every escalated call on each replay of the history.
|
|
73
|
+
<Alert
|
|
74
|
+
role="note"
|
|
75
|
+
variant="warning"
|
|
76
|
+
className={cn(
|
|
77
|
+
'flex items-center gap-[var(--spacing-system-m)] rounded-none border-0 p-[var(--spacing-system-m)]',
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<span className="flex shrink-0" aria-hidden>
|
|
82
|
+
<AlertTriangleIcon size={20} />
|
|
83
|
+
</span>
|
|
84
|
+
<div className="grid min-w-0 flex-1 grid-cols-1 gap-[var(--spacing-system-xxs)]">
|
|
85
|
+
<p className="break-words text-h3">Mingo Escalated</p>
|
|
86
|
+
{description && <p className="break-words text-h6">{description}</p>}
|
|
87
|
+
</div>
|
|
88
|
+
</Alert>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Framework-free: no React, no timers.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { keptPolicyReview } from '../../../chat-protocol/policy-review';
|
|
18
19
|
import type {
|
|
19
20
|
ApprovalBatchExecutionState,
|
|
20
21
|
ChatApprovalStatus,
|
|
@@ -381,6 +382,7 @@ function sameToolData(a: ToolExecutionData, b: ToolExecutionData): boolean {
|
|
|
381
382
|
a.integratedToolType === b.integratedToolType &&
|
|
382
383
|
a.toolFunction === b.toolFunction &&
|
|
383
384
|
a.toolTitle === b.toolTitle &&
|
|
385
|
+
(a.policyReview === b.policyReview || JSON.stringify(a.policyReview) === JSON.stringify(b.policyReview)) &&
|
|
384
386
|
a.result === b.result &&
|
|
385
387
|
a.success === b.success &&
|
|
386
388
|
a.toolExecutionRequestId === b.toolExecutionRequestId &&
|
|
@@ -479,6 +481,8 @@ export function mergeToolExecutionIfPresent(
|
|
|
479
481
|
const mergedData: ToolExecutionData = {
|
|
480
482
|
...toolData,
|
|
481
483
|
toolTitle: toolData.toolTitle ?? seg.data.toolTitle,
|
|
484
|
+
// The review rides the EXECUTING chunk only: the EXECUTED one keeps it.
|
|
485
|
+
...keptPolicyReview(toolData.policyReview, seg.data.policyReview),
|
|
482
486
|
parameters: toolData.parameters || seg.data.parameters,
|
|
483
487
|
};
|
|
484
488
|
// Value-level no-op (replayed duplicate) → prior references.
|
|
@@ -11,6 +11,7 @@ import { StatusLine, type StatusLineTone } from '../ui/status-line';
|
|
|
11
11
|
import { CHAT_BLOCK_FRAME_CLASS } from './chat-block-frame';
|
|
12
12
|
import { ExpandChevron } from './expand-chevron';
|
|
13
13
|
import { useCollapsible } from './hooks/use-collapsible';
|
|
14
|
+
import { PolicyEscalationAlert, PolicyReviewRows, hasPolicyReviewRows, isPolicyEscalated } from './policy-review';
|
|
14
15
|
import { ArgRow, ResultBlock } from './tool-call-blocks';
|
|
15
16
|
import type { ToolExecutionDisplayProps } from './types';
|
|
16
17
|
import { COMMAND_BODY_ARG_KEYS } from './utils/tool-call-helpers';
|
|
@@ -56,6 +57,9 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
|
|
|
56
57
|
}, [message.parameters, variant]);
|
|
57
58
|
|
|
58
59
|
const hasResult = isExecuted && typeof message.result === 'string' && message.result.length > 0;
|
|
60
|
+
// Admin-only detail, like the command: the end-client card is the static row below, which draws no body.
|
|
61
|
+
const review = message.policyReview;
|
|
62
|
+
const reviewRows = hasPolicyReviewRows(review);
|
|
59
63
|
|
|
60
64
|
// A remote (MCP) read-only tool deliberately publishes NO parameters and NO
|
|
61
65
|
// result — the backend hides both, and sends a human title instead. There is
|
|
@@ -67,9 +71,15 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
|
|
|
67
71
|
// remote rather than one of ours — and guarded on there being a title to
|
|
68
72
|
// show, so a title-less row keeps the old behaviour rather than collapsing
|
|
69
73
|
// to a blank line.
|
|
70
|
-
const isConciseRemoteRead =
|
|
74
|
+
const isConciseRemoteRead =
|
|
75
|
+
!message.integratedToolType &&
|
|
76
|
+
!!previewText &&
|
|
77
|
+
argEntries.length === 0 &&
|
|
78
|
+
!hasResult &&
|
|
79
|
+
!reviewRows &&
|
|
80
|
+
!isPolicyEscalated(review);
|
|
71
81
|
const isStaticRow = isClientView || isConciseRemoteRead;
|
|
72
|
-
const hasBody = argEntries.length > 0 || hasResult || isExecuting;
|
|
82
|
+
const hasBody = argEntries.length > 0 || reviewRows || hasResult || isExecuting;
|
|
73
83
|
// A run with no arguments and no result has nothing behind a chevron either:
|
|
74
84
|
// its header stays, with no chevron and no click target, so the row never
|
|
75
85
|
// opens onto an empty box. (A run still executing has a body: its live
|
|
@@ -166,17 +176,23 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
|
|
|
166
176
|
{argEntries.map(([key, value]) => (
|
|
167
177
|
<ArgRow key={key} argKey={key} value={value} />
|
|
168
178
|
))}
|
|
179
|
+
{reviewRows && (
|
|
180
|
+
<PolicyReviewRows
|
|
181
|
+
review={review}
|
|
182
|
+
className={argEntries.length > 0 ? 'mt-[var(--spacing-system-xsf)]' : undefined}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
169
185
|
{hasResult && (
|
|
170
186
|
<ResultBlock
|
|
171
187
|
result={message.result}
|
|
172
|
-
className={argEntries.length > 0 ? 'mt-[var(--spacing-system-xsf)]' : undefined}
|
|
188
|
+
className={argEntries.length > 0 || reviewRows ? 'mt-[var(--spacing-system-xsf)]' : undefined}
|
|
173
189
|
/>
|
|
174
190
|
)}
|
|
175
191
|
{isExecuting && (
|
|
176
192
|
<div
|
|
177
193
|
className={cn(
|
|
178
194
|
'flex w-full flex-col items-start gap-[var(--spacing-system-xxs)]',
|
|
179
|
-
argEntries.length > 0 && 'mt-[var(--spacing-system-xsf)]',
|
|
195
|
+
(argEntries.length > 0 || reviewRows) && 'mt-[var(--spacing-system-xsf)]',
|
|
180
196
|
)}
|
|
181
197
|
>
|
|
182
198
|
<span className="text-ods-text-secondary">Result:</span>
|
|
@@ -187,6 +203,9 @@ const ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProp
|
|
|
187
203
|
)}
|
|
188
204
|
</div>
|
|
189
205
|
</div>
|
|
206
|
+
|
|
207
|
+
{/* A run Mingo escalated (approved, then run) keeps saying so, outside the collapsible part. */}
|
|
208
|
+
<PolicyEscalationAlert review={review} className="border-t border-ods-border" />
|
|
190
209
|
</>
|
|
191
210
|
)}
|
|
192
211
|
</div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Contains all message structures, segments, and content types
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { ApprovalLevel } from '../../../types/permissions';
|
|
6
7
|
import type { ChatRef } from '../chat-ref.types';
|
|
7
8
|
import type { QuickActionIconSpec } from '../quick-action-chip';
|
|
8
9
|
import type { AssistantType, AuthorType, ChatApprovalStatus, MessageOwner } from './chat.types';
|
|
@@ -55,6 +56,36 @@ export const SCROLL_ANCHOR = { TOP: 'top', BOTTOM: 'bottom' } as const;
|
|
|
55
56
|
|
|
56
57
|
export type ScrollAnchor = (typeof SCROLL_ANCHOR)[keyof typeof SCROLL_ANCHOR];
|
|
57
58
|
|
|
59
|
+
// ========== Policy Review Types ==========
|
|
60
|
+
|
|
61
|
+
/** One guardrail rule the command review matched (ai-agent `PolicyReviewRule`). */
|
|
62
|
+
export interface PolicyReviewRuleData {
|
|
63
|
+
code: string;
|
|
64
|
+
/** The rule's name when it matched; the history keeps the name it had then. */
|
|
65
|
+
label: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Why the security policy gave a tool call its approval level (ai-agent `PolicyReview`). Sent only while
|
|
70
|
+
* the semantic guardrail is on, on approval tool calls and on `EXECUTING_TOOL`. Enum values stay open
|
|
71
|
+
* strings: the backend is narrowing `decision` (`SOFT_DENY` / `HARD_DENY` become `DENY`), and a value
|
|
72
|
+
* this build does not know must not drop the review. Normalized by `chat-protocol/policy-review`.
|
|
73
|
+
*/
|
|
74
|
+
export interface PolicyReviewData {
|
|
75
|
+
/** `CLASSIFIER` (the AI review) or `REGEX`. */
|
|
76
|
+
source?: string;
|
|
77
|
+
/** `POLICY`, `FALLBACK` (no rule of the policy matched), or a deny by a matched guardrail rule. */
|
|
78
|
+
decision?: string;
|
|
79
|
+
/** The level in force for this call. */
|
|
80
|
+
approvalLevel?: ApprovalLevel;
|
|
81
|
+
/** The level the organization's policy sets; differs from `approvalLevel` when a rule raised it. */
|
|
82
|
+
policyLevel?: ApprovalLevel;
|
|
83
|
+
operationCodes: string[];
|
|
84
|
+
matchedRules: PolicyReviewRuleData[];
|
|
85
|
+
/** The review's one-line reason; absent from a regex review. */
|
|
86
|
+
reason?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
// ========== Tool Execution Types ==========
|
|
59
90
|
|
|
60
91
|
export interface ToolExecutionData {
|
|
@@ -69,6 +100,9 @@ export interface ToolExecutionData {
|
|
|
69
100
|
* the accumulator restores it onto the merged `EXECUTED_TOOL` segment.
|
|
70
101
|
*/
|
|
71
102
|
toolExplanation?: string;
|
|
103
|
+
/** The security policy's review of this call. Only sent on `EXECUTING_TOOL`, restored onto `EXECUTED_TOOL` like
|
|
104
|
+
* `toolExplanation`. */
|
|
105
|
+
policyReview?: PolicyReviewData;
|
|
72
106
|
parameters?: Record<string, unknown>;
|
|
73
107
|
result?: string;
|
|
74
108
|
success?: boolean;
|
|
@@ -100,6 +134,8 @@ export interface ExecutingToolState {
|
|
|
100
134
|
toolTitle?: string;
|
|
101
135
|
/** Mirrors {@link ToolExecutionData.toolExplanation}; absent on `EXECUTED_TOOL`. */
|
|
102
136
|
toolExplanation?: string;
|
|
137
|
+
/** Mirrors {@link ToolExecutionData.policyReview}; absent on `EXECUTED_TOOL`. */
|
|
138
|
+
policyReview?: PolicyReviewData;
|
|
103
139
|
parameters?: Record<string, unknown>;
|
|
104
140
|
}
|
|
105
141
|
|
|
@@ -211,6 +247,8 @@ export interface PendingToolCallData {
|
|
|
211
247
|
requiresApproval: boolean;
|
|
212
248
|
approvalType?: string | null;
|
|
213
249
|
toolCallArguments?: Record<string, unknown> | null;
|
|
250
|
+
/** Why the policy gave this call its approval level; absent while the semantic guardrail is off. */
|
|
251
|
+
policyReview?: PolicyReviewData;
|
|
214
252
|
}
|
|
215
253
|
|
|
216
254
|
/**
|
|
@@ -517,6 +555,8 @@ export interface ExecutingToolMessageData extends MessageDataBase {
|
|
|
517
555
|
title?: string;
|
|
518
556
|
/** Backend-issued human-readable explanation (what/why) of the tool call. */
|
|
519
557
|
toolExplanation?: string;
|
|
558
|
+
/** WIRE shape of the policy review: normalized by `normalizePolicyReview` on the way in. */
|
|
559
|
+
policyReview?: unknown;
|
|
520
560
|
parameters?: Record<string, unknown>;
|
|
521
561
|
toolExecutionRequestId?: string;
|
|
522
562
|
}
|
|
@@ -539,8 +579,8 @@ export interface ApprovalRequestMessageData extends MessageDataBase {
|
|
|
539
579
|
approvalType?: string;
|
|
540
580
|
command?: string;
|
|
541
581
|
explanation?: string;
|
|
542
|
-
/** Present when the approval is a batch of tool calls (new format). */
|
|
543
|
-
toolCalls?: PendingToolCallData
|
|
582
|
+
/** Present when the approval is a batch of tool calls (new format). WIRE shape: the review is normalized on replay. */
|
|
583
|
+
toolCalls?: Array<Omit<PendingToolCallData, 'policyReview'> & { policyReview?: unknown }>;
|
|
544
584
|
}
|
|
545
585
|
|
|
546
586
|
export interface ApprovalResultMessageData extends MessageDataBase {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Used to continue building messages across realtime connections
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { keptPolicyReview } from '../../../chat-protocol/policy-review';
|
|
6
7
|
import type { InitializeExtras } from '../stream/chat-stream-reducer';
|
|
7
8
|
import type { ProcessedMessage, MessageSegment, PendingApproval, ExecutingToolState } from '../types';
|
|
8
9
|
|
|
@@ -51,6 +52,7 @@ export function extractIncompleteMessageState(
|
|
|
51
52
|
toolFunction: segment.data.toolFunction,
|
|
52
53
|
toolTitle: segment.data.toolTitle,
|
|
53
54
|
toolExplanation: segment.data.toolExplanation,
|
|
55
|
+
...keptPolicyReview(segment.data.policyReview),
|
|
54
56
|
parameters: segment.data.parameters,
|
|
55
57
|
});
|
|
56
58
|
hasIncompleteState = true;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Creating approval segments when results arrive
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { keptPolicyReview } from '../../../chat-protocol/policy-review';
|
|
11
12
|
import { applyApprovalStatusToSegment, nextBatchExecution, withBatchExecution } from '../stream/message-mutations';
|
|
12
13
|
import type {
|
|
13
14
|
AskOptionData,
|
|
@@ -197,6 +198,7 @@ export class MessageSegmentAccumulator {
|
|
|
197
198
|
toolFunction: toolData.toolFunction,
|
|
198
199
|
toolTitle: toolData.toolTitle,
|
|
199
200
|
toolExplanation: toolData.toolExplanation,
|
|
201
|
+
...keptPolicyReview(toolData.policyReview),
|
|
200
202
|
parameters: toolData.parameters,
|
|
201
203
|
});
|
|
202
204
|
this.segments.push(segment);
|
|
@@ -225,6 +227,7 @@ export class MessageSegmentAccumulator {
|
|
|
225
227
|
toolTitle: toolData.toolTitle ?? existingExecuting?.data.toolTitle ?? executingTool?.toolTitle,
|
|
226
228
|
toolExplanation:
|
|
227
229
|
toolData.toolExplanation ?? existingExecuting?.data.toolExplanation ?? executingTool?.toolExplanation,
|
|
230
|
+
...keptPolicyReview(toolData.policyReview, existingExecuting?.data.policyReview, executingTool?.policyReview),
|
|
228
231
|
parameters: toolData.parameters || executingTool?.parameters,
|
|
229
232
|
},
|
|
230
233
|
};
|
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
// One normalizer for ask rows, shared with the live decoder — history and the
|
|
35
35
|
// stream must agree on which options are usable.
|
|
36
36
|
import { normalizeAskOptions } from '../../../chat-protocol/nats-decoder';
|
|
37
|
+
// The same review normalizer as the live decoder, for the same reason.
|
|
38
|
+
import { policyReviewField } from '../../../chat-protocol/policy-review';
|
|
37
39
|
import {
|
|
38
40
|
attachmentsEvent,
|
|
39
41
|
mergeSourceMetadata,
|
|
@@ -57,6 +59,14 @@ import { approvalDisplaysInline } from './approval-display';
|
|
|
57
59
|
import { type MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator';
|
|
58
60
|
import { getCommandText } from './tool-call-helpers';
|
|
59
61
|
|
|
62
|
+
/** A history tool call with its wire `policyReview` normalized (or dropped), as the live decoder does it. */
|
|
63
|
+
function withNormalizedReview(
|
|
64
|
+
call: Omit<PendingToolCallData, 'policyReview'> & { policyReview?: unknown },
|
|
65
|
+
): PendingToolCallData {
|
|
66
|
+
const { policyReview: raw, ...rest } = call;
|
|
67
|
+
return { ...rest, ...policyReviewField(raw) };
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
function getOwnerDisplayName(owner?: MessageOwner): string {
|
|
61
71
|
if (owner?.type === OWNER_TYPE.ADMIN && owner.user) {
|
|
62
72
|
const { firstName, lastName } = owner.user;
|
|
@@ -106,8 +116,8 @@ function pushStandaloneMessages(
|
|
|
106
116
|
* the resolver's name as `displayName`);
|
|
107
117
|
* - `approvalType` defaults to 'CLIENT' (legacy history parity;
|
|
108
118
|
* realtime defaults APPROVAL_REQUEST to 'USER');
|
|
109
|
-
* - batch `toolCalls` pass through
|
|
110
|
-
*
|
|
119
|
+
* - batch `toolCalls` pass through as persisted, except `policyReview`,
|
|
120
|
+
* normalized as the live decoder does it.
|
|
111
121
|
*
|
|
112
122
|
* Returns `null` for rows the assistant-turn path doesn't decode (SYSTEM
|
|
113
123
|
* is handled by the envelope's standalone path; user TEXT rows are handled
|
|
@@ -166,6 +176,7 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
|
|
|
166
176
|
// EXECUTING chunk only, so a history replay that drops it renders
|
|
167
177
|
// an empty tool card even though the live stream showed one.
|
|
168
178
|
toolExplanation: typeof data.toolExplanation === 'string' ? data.toolExplanation : undefined,
|
|
179
|
+
...policyReviewField(data.policyReview),
|
|
169
180
|
parameters: data.parameters,
|
|
170
181
|
toolExecutionRequestId: data.toolExecutionRequestId,
|
|
171
182
|
},
|
|
@@ -199,7 +210,7 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
|
|
|
199
210
|
approvalType: data.approvalType || 'CLIENT',
|
|
200
211
|
command: data.command || '',
|
|
201
212
|
explanation: data.explanation,
|
|
202
|
-
...(Array.isArray(data.toolCalls) ? { toolCalls: data.toolCalls } : {}),
|
|
213
|
+
...(Array.isArray(data.toolCalls) ? { toolCalls: data.toolCalls.map(withNormalizedReview) } : {}),
|
|
203
214
|
};
|
|
204
215
|
}
|
|
205
216
|
return null;
|
|
@@ -72,6 +72,9 @@ export interface EmbedViewerFrameProps {
|
|
|
72
72
|
loadingActionCount?: number;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** The header's loading form draws one button by default: most viewers have one action (the PDF viewer passes two). */
|
|
76
|
+
export const EMBED_VIEWER_SKELETON_ACTIONS = 1;
|
|
77
|
+
|
|
75
78
|
export function EmbedViewerFrame({
|
|
76
79
|
icon,
|
|
77
80
|
title,
|
|
@@ -89,7 +92,7 @@ export function EmbedViewerFrame({
|
|
|
89
92
|
sandbox,
|
|
90
93
|
className,
|
|
91
94
|
fullscreenControl,
|
|
92
|
-
loadingActionCount =
|
|
95
|
+
loadingActionCount = EMBED_VIEWER_SKELETON_ACTIONS,
|
|
93
96
|
}: EmbedViewerFrameProps) {
|
|
94
97
|
// ONE switch for the whole frame (header included): a page's loading scope, or the standard `loading` prop. The body
|
|
95
98
|
// alone also waits while the viewer resolves where its frame points.
|
|
@@ -157,7 +160,12 @@ export function EmbedViewerFrame({
|
|
|
157
160
|
<div className="flex w-full items-center gap-2 content-sm:w-auto">
|
|
158
161
|
{Array.from({ length: loadingActionCount }, (_, i) => (
|
|
159
162
|
// biome-ignore lint/suspicious/noArrayIndexKey: loading buttons never reorder
|
|
160
|
-
<InteractiveSkeleton.Button
|
|
163
|
+
<InteractiveSkeleton.Button
|
|
164
|
+
key={i}
|
|
165
|
+
data-loading-item
|
|
166
|
+
size="small-legacy"
|
|
167
|
+
className="flex-1 content-sm:flex-initial"
|
|
168
|
+
/>
|
|
161
169
|
))}
|
|
162
170
|
</div>
|
|
163
171
|
) : fullscreenControl && src ? (
|
|
@@ -16,6 +16,9 @@ export interface PdfViewerProps {
|
|
|
16
16
|
loading?: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** The header's loading form: Preview and Download, the viewer's two actions. */
|
|
20
|
+
const PDF_VIEWER_SKELETON_ACTIONS = 2;
|
|
21
|
+
|
|
19
22
|
export function PdfViewer({ src, fileName, onPreview, onDownload, height, loading = false }: PdfViewerProps) {
|
|
20
23
|
const displayName = fileName || 'PDF Document';
|
|
21
24
|
// In a loading scope the document is being read: the frame draws its loading form, never "not available".
|
|
@@ -62,7 +65,7 @@ export function PdfViewer({ src, fileName, onPreview, onDownload, height, loadin
|
|
|
62
65
|
</div>
|
|
63
66
|
}
|
|
64
67
|
src={src}
|
|
65
|
-
loadingActionCount={
|
|
68
|
+
loadingActionCount={PDF_VIEWER_SKELETON_ACTIONS}
|
|
66
69
|
loading={loading}
|
|
67
70
|
height={height}
|
|
68
71
|
/>
|