@flamingo-stack/openframe-frontend-core 0.0.850 → 0.0.852

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.
Files changed (154) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{case-study-card-X2HKPKT7.cjs → case-study-card-WP5Z234Q.cjs} +5 -5
  3. package/dist/chat-protocol/events.d.ts +4 -1
  4. package/dist/chat-protocol/events.d.ts.map +1 -1
  5. package/dist/chat-protocol/index.cjs +90 -40
  6. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  7. package/dist/chat-protocol/nats-decoder.js +5 -1
  8. package/dist/chat-protocol/policy-review.d.ts +18 -0
  9. package/dist/chat-protocol/policy-review.d.ts.map +1 -0
  10. package/dist/chat-protocol/policy-review.js +55 -0
  11. package/dist/{chunk-BU2PSRAS.cjs → chunk-2W22LY3W.cjs} +11 -11
  12. package/dist/{chunk-ZDSC3GFX.cjs → chunk-3SJVF3HW.cjs} +6 -6
  13. package/dist/{chunk-GAYLAA3L.cjs → chunk-3ZQESOPJ.cjs} +79 -79
  14. package/dist/{chunk-IPGGSFR7.cjs → chunk-477P6AQH.cjs} +6 -6
  15. package/dist/{chunk-5CBI2ZYV.cjs → chunk-5RMBLOJ5.cjs} +12 -12
  16. package/dist/{chunk-36DUCHI7.cjs → chunk-6GWRPW6B.cjs} +4 -4
  17. package/dist/{chunk-SOSIXRQ3.cjs → chunk-AZEO7HBB.cjs} +2 -2
  18. package/dist/{chunk-FRG724DU.cjs → chunk-BNHTBLAG.cjs} +17 -17
  19. package/dist/{chunk-VRWBJL7B.cjs → chunk-CHGIACIP.cjs} +91 -91
  20. package/dist/{chunk-CAGNWDVJ.cjs → chunk-COJG2ATY.cjs} +45 -45
  21. package/dist/{chunk-XKOL5CNU.cjs → chunk-D3H6AI7O.cjs} +956 -1077
  22. package/dist/{chunk-ALRYVZ3E.cjs → chunk-DLTPSP5F.cjs} +2 -2
  23. package/dist/{chunk-3W4ASAXN.cjs → chunk-EIACEY2L.cjs} +4 -4
  24. package/dist/{chunk-TIVPOVIM.cjs → chunk-ETZFBB7O.cjs} +2 -2
  25. package/dist/{chunk-GRCOEICK.cjs → chunk-G7GF4PWA.cjs} +6 -6
  26. package/dist/{chunk-7ICHJVAT.cjs → chunk-GEZ3UXCA.cjs} +25 -25
  27. package/dist/chunk-GGUFVIJ6.cjs +28 -0
  28. package/dist/{chunk-ZGEWT3SF.cjs → chunk-HXURNKXI.cjs} +3 -3
  29. package/dist/{chunk-TX4NTH22.cjs → chunk-IC4IJJB2.cjs} +102 -102
  30. package/dist/{chunk-BUEEQD3G.cjs → chunk-IGVAPIEW.cjs} +2 -2
  31. package/dist/{chunk-IG6ROHPS.cjs → chunk-IO6NN3SL.cjs} +54 -54
  32. package/dist/{chunk-5PIR77V5.cjs → chunk-KHOB3TES.cjs} +4 -4
  33. package/dist/{chunk-JQCQCE6U.cjs → chunk-KKFDRCQF.cjs} +2 -2
  34. package/dist/{chunk-6AJYGXXS.cjs → chunk-KNN2GCMW.cjs} +21 -21
  35. package/dist/{chunk-YJZ7R3W5.cjs → chunk-KXFIYO4O.cjs} +8 -8
  36. package/dist/{chunk-FXLROQUT.cjs → chunk-LFEURIZN.cjs} +590 -607
  37. package/dist/{chunk-JWIITCVB.cjs → chunk-MFDRCTM3.cjs} +47 -47
  38. package/dist/{chunk-OHHDX3Z4.cjs → chunk-MXY2OGEV.cjs} +42 -42
  39. package/dist/{chunk-533QK4R6.cjs → chunk-NMWLXHFH.cjs} +130 -130
  40. package/dist/{chunk-MDLT6D46.cjs → chunk-O3EKOXFZ.cjs} +20 -20
  41. package/dist/{chunk-HLJXCOMA.cjs → chunk-OH5TXFRR.cjs} +4 -4
  42. package/dist/{chunk-SA2NNWIJ.cjs → chunk-P63CRPWI.cjs} +19 -64
  43. package/dist/{chunk-R5JPZ6TY.cjs → chunk-QOW2GP3V.cjs} +21 -21
  44. package/dist/{chunk-JBTH3O2C.cjs → chunk-SSKMGH5O.cjs} +4 -4
  45. package/dist/{chunk-4YBS4HIO.cjs → chunk-U767P4JU.cjs} +455 -38
  46. package/dist/{chunk-FNQWL4W4.cjs → chunk-UO2EJGRJ.cjs} +45 -45
  47. package/dist/{chunk-IHY7E4EX.cjs → chunk-USPUZO53.cjs} +4 -4
  48. package/dist/{chunk-IN3LA2FU.cjs → chunk-UV45EPXN.cjs} +2 -2
  49. package/dist/{chunk-S3QXLD4X.cjs → chunk-VCFQHPOO.cjs} +37 -3
  50. package/dist/{chunk-2QZUTNL3.cjs → chunk-VUHHLRAY.cjs} +92 -92
  51. package/dist/{chunk-QJER2ZNA.cjs → chunk-WFAZMASF.cjs} +3 -3
  52. package/dist/{chunk-LGYPCUHO.cjs → chunk-X3WIOMEE.cjs} +36 -36
  53. package/dist/{cloud-tenants-TVB3DT3J.cjs → cloud-tenants-O33HCUYB.cjs} +50 -50
  54. package/dist/components/case-studies/index.cjs +69 -69
  55. package/dist/components/chat/approval-batch-message.d.ts +5 -0
  56. package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
  57. package/dist/components/chat/approval-batch-message.js +36 -10
  58. package/dist/components/chat/index.cjs +45 -45
  59. package/dist/components/chat/policy-review.d.ts +27 -0
  60. package/dist/components/chat/policy-review.d.ts.map +1 -0
  61. package/dist/components/chat/policy-review.js +67 -0
  62. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
  63. package/dist/components/chat/stream/message-mutations.js +4 -1
  64. package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
  65. package/dist/components/chat/tool-execution-display.js +16 -5
  66. package/dist/components/chat/types/message.types.d.ts +40 -2
  67. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  68. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  69. package/dist/components/chat/utils/extract-incomplete-message-state.js +2 -0
  70. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  71. package/dist/components/chat/utils/message-segment-accumulator.js +3 -0
  72. package/dist/components/chat/utils/process-historical-messages.d.ts +2 -2
  73. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  74. package/dist/components/chat/utils/process-historical-messages.js +7 -1
  75. package/dist/components/contact/index.cjs +51 -51
  76. package/dist/components/docs/index.cjs +54 -54
  77. package/dist/components/embeds/index.cjs +51 -51
  78. package/dist/components/faq/index.cjs +52 -52
  79. package/dist/components/features/approval-level.d.ts +6 -12
  80. package/dist/components/features/approval-level.d.ts.map +1 -1
  81. package/dist/components/features/approval-level.js +45 -55
  82. package/dist/components/features/index.cjs +55 -54
  83. package/dist/components/features/notifications/approval-request-notification-tile.d.ts.map +1 -1
  84. package/dist/components/features/notifications/approval-request-notification-tile.js +71 -48
  85. package/dist/components/features/notifications/types.d.ts +2 -0
  86. package/dist/components/features/notifications/types.d.ts.map +1 -1
  87. package/dist/components/features/notifications/types.js +3 -1
  88. package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
  89. package/dist/components/features/policy-configuration-panel.js +75 -64
  90. package/dist/components/help-center-pages/index.cjs +156 -156
  91. package/dist/components/icons-v2-generated/brand-logos/index.d.ts +1 -0
  92. package/dist/components/icons-v2-generated/brand-logos/index.d.ts.map +1 -1
  93. package/dist/components/icons-v2-generated/brand-logos/index.js +2 -0
  94. package/dist/components/icons-v2-generated/brand-logos/stripe-icon.d.ts +8 -0
  95. package/dist/components/icons-v2-generated/brand-logos/stripe-icon.d.ts.map +1 -0
  96. package/dist/components/icons-v2-generated/brand-logos/stripe-icon.js +24 -0
  97. package/dist/components/icons-v2-generated/icon-loaders.d.ts.map +1 -1
  98. package/dist/components/icons-v2-generated/icon-loaders.js +1 -0
  99. package/dist/components/icons-v2-generated/icon-names.js +1 -1
  100. package/dist/components/icons-v2-generated/index.cjs +33 -29
  101. package/dist/components/index.cjs +122 -123
  102. package/dist/components/meeting-scheduler/index.cjs +113 -112
  103. package/dist/components/navigation/index.cjs +54 -54
  104. package/dist/components/onboarding-guides/index.cjs +53 -53
  105. package/dist/components/product-demo/index.cjs +61 -61
  106. package/dist/components/related-content/index.cjs +9 -9
  107. package/dist/components/tickets/index.cjs +57 -57
  108. package/dist/components/ui/file-manager/index.cjs +8 -9
  109. package/dist/components/ui/index.cjs +55 -56
  110. package/dist/components/ui/radio-group.d.ts.map +1 -1
  111. package/dist/components/ui/radio-group.js +8 -2
  112. package/dist/components/ui/tab-selector.d.ts +7 -1
  113. package/dist/components/ui/tab-selector.d.ts.map +1 -1
  114. package/dist/components/ui/tab-selector.js +22 -3
  115. package/dist/{devices-NGCTSXIU.cjs → devices-2RGGMC2B.cjs} +51 -51
  116. package/dist/{embeddable-chat-322NDFP5.cjs → embeddable-chat-SJEEJI4N.cjs} +122 -122
  117. package/dist/hooks/index.cjs +9 -9
  118. package/dist/{icon-loaders-SUPEKMBJ.cjs → icon-loaders-CTH6E7OP.cjs} +1 -0
  119. package/dist/index.cjs +61 -62
  120. package/dist/{knowledge-75PO5DYS.cjs → knowledge-5TJIXVMD.cjs} +52 -52
  121. package/dist/{logs-PZESOAIG.cjs → logs-HGQOTRKZ.cjs} +57 -57
  122. package/dist/{policies-6XNMBFWO.cjs → policies-GWFSLVSM.cjs} +54 -54
  123. package/dist/{remote-session-Q2FIBCGT.cjs → remote-session-SG44CNOK.cjs} +36 -36
  124. package/dist/{scripts-B3JW6TPF.cjs → scripts-PGJNAZF4.cjs} +57 -57
  125. package/dist/{software-update-NWP542J7.cjs → software-update-KKLSYHDI.cjs} +54 -54
  126. package/dist/stripe-icon-YKVABTNI.cjs +7 -0
  127. package/dist/{tickets-board-7FLGQEHF.cjs → tickets-board-SQY2Y6QV.cjs} +39 -38
  128. package/dist/utils/index.cjs +32 -1
  129. package/package.json +1 -1
  130. package/src/chat-protocol/events.ts +4 -1
  131. package/src/chat-protocol/nats-decoder.ts +4 -0
  132. package/src/chat-protocol/policy-review.ts +71 -0
  133. package/src/components/chat/approval-batch-message.tsx +47 -10
  134. package/src/components/chat/policy-review.tsx +90 -0
  135. package/src/components/chat/stream/message-mutations.ts +4 -0
  136. package/src/components/chat/tool-execution-display.tsx +23 -4
  137. package/src/components/chat/types/message.types.ts +42 -2
  138. package/src/components/chat/utils/extract-incomplete-message-state.ts +2 -0
  139. package/src/components/chat/utils/message-segment-accumulator.ts +3 -0
  140. package/src/components/chat/utils/process-historical-messages.ts +14 -3
  141. package/src/components/features/approval-level.tsx +74 -78
  142. package/src/components/features/notifications/approval-request-notification-tile.tsx +75 -50
  143. package/src/components/features/notifications/types.ts +4 -0
  144. package/src/components/features/policy-configuration-panel.tsx +100 -83
  145. package/src/components/icons-v2/icon-colors.json +3 -0
  146. package/src/components/icons-v2-generated/brand-logos/index.ts +1 -0
  147. package/src/components/icons-v2-generated/brand-logos/stripe-icon.tsx +27 -0
  148. package/src/components/icons-v2-generated/icon-loaders.ts +1 -0
  149. package/src/components/icons-v2-generated/icon-names.ts +1 -1
  150. package/src/components/ui/radio-group.tsx +20 -2
  151. package/src/components/ui/tab-selector.tsx +26 -3
  152. package/src/styles/ods-colors.css +1 -0
  153. package/dist/chunk-N3MBQRCQ.cjs +0 -41
  154. /package/dist/{chunk-46547WQW.cjs → chunk-7HH4Y27U.cjs} +0 -0
@@ -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 = !message.integratedToolType && !!previewText && argEntries.length === 0 && !hasResult;
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 verbatim (already the persisted
110
- * `PendingToolCallData` shape — no normalization pass).
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;
@@ -3,24 +3,18 @@
3
3
  import type { ComponentType } from 'react';
4
4
  import type { ApprovalLevel } from '../../types/permissions';
5
5
  import { cn } from '../../utils/cn';
6
- import { HeadsetIcon } from '../icons-v2-generated/audio-and-visual/headset-icon';
7
6
  import { BannedIcon } from '../icons-v2-generated/security/banned-icon';
7
+ import { ShieldCheckIcon } from '../icons-v2-generated/security/shield-check-icon';
8
8
  import { CheckCircleIcon } from '../icons-v2-generated/signs-and-symbols/check-circle-icon';
9
- import { UserIcon } from '../icons-v2-generated/users/user-icon';
9
+ import { UserCheckIcon } from '../icons-v2-generated/users/user-check-icon';
10
10
  import { UnifiedSkeleton } from '../loading/unified-skeleton';
11
- import { TOGGLE_GROUP_CLASS, ToggleGroup, ToggleGroupItem } from '../toggle-group';
12
11
  import { TextHold } from '../ui/layout-hold';
13
12
  import { useLoadingScope } from '../ui/loading-scope';
14
- import { TOGGLE_SIZE_CLASS } from '../ui/toggle';
15
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
13
+ import { TabSelector } from '../ui/tab-selector';
16
14
  import { TouchFriendlyTooltip } from '../ui/touch-friendly-tooltip';
17
15
 
18
- /**
19
- * The approval levels, in the order a choice reads: the two that need nobody
20
- * (allow, never) side by side, then the two that ask someone (a technician,
21
- * the user) side by side.
22
- */
23
- export const APPROVAL_LEVELS: readonly ApprovalLevel[] = ['ALLOW', 'DENY', 'ASK_TECHNICIAN', 'ASK_USER'];
16
+ /** The approval levels, from the freest to the strictest, as the guardrails settings order them (Figma 4365-22670). */
17
+ export const APPROVAL_LEVELS: readonly ApprovalLevel[] = ['ALLOW', 'ASK_USER', 'ASK_TECHNICIAN', 'DENY'];
24
18
 
25
19
  export interface ApprovalLevelMeta {
26
20
  /** The level's name, as every surface writes it. */
@@ -47,31 +41,48 @@ export const APPROVAL_LEVEL_META: Record<ApprovalLevel, ApprovalLevelMeta> = {
47
41
  ASK_USER: {
48
42
  label: 'Ask User',
49
43
  hint: 'Asks the person at the computer first.',
50
- Icon: UserIcon,
44
+ Icon: UserCheckIcon,
51
45
  toneClassName: 'text-ods-warning',
52
46
  },
53
47
  ASK_TECHNICIAN: {
54
48
  label: 'Ask Technician',
55
49
  hint: 'Asks a technician first. Nothing runs until they approve.',
56
- Icon: HeadsetIcon,
50
+ Icon: ShieldCheckIcon,
57
51
  toneClassName: 'text-ods-warning',
58
52
  },
59
53
  DENY: { label: 'Restrict', hint: 'Never runs.', Icon: BannedIcon, toneClassName: 'text-ods-error' },
60
54
  };
61
55
 
62
- const ICON_PX = 16;
56
+ /** A read-only level's icon (Figma 1608-49531: 24px, before the name). */
57
+ const ICON_PX = 24;
63
58
 
64
59
  /** The view's layout, read by the view and by its skeleton, so the skeleton is the view's size. */
65
60
  const READ_ONLY_ROW_CLASS = 'inline-flex shrink-0 items-center gap-[var(--spacing-system-xxs)]';
66
- const EDITABLE_ROW_CLASS = 'flex shrink-0 items-center gap-[var(--spacing-system-sf)]';
61
+ /** Editable: the name after the strip on a phone, before it from `content-md` (Figma 4365-22461 / 4365-22663). */
62
+ const EDITABLE_ROW_CLASS =
63
+ 'flex shrink-0 items-center gap-[var(--spacing-system-xxs)] content-md:gap-[var(--spacing-system-m)]';
64
+ const EDITABLE_NAME_ORDER_CLASS = 'order-last content-md:order-none';
65
+ /** A read-only level's icon box: 16px on a phone, 24 from `content-md` (Figma 555-39573 / 1608-49531). */
66
+ const READ_ONLY_ICON_BOX_CLASS = '[&>*]:!size-4 content-md:[&>*]:!size-6';
67
+ /** Read-only, the name is the row's value (h4, primary); editable, a caption in the level's colour beside the strip. */
68
+ const READ_ONLY_NAME_CLASS = 'whitespace-nowrap text-ods-text-primary text-h4';
67
69
  const LEVEL_NAME_CLASS = 'whitespace-nowrap text-h6';
68
- /** An editable level's button: the small toggle with tighter sides. */
69
- const LEVEL_BUTTON_CLASS = 'px-2';
70
+ /** An editable level's icon (Figma: 20px in the segment). */
71
+ const EDITABLE_ICON_PX = 20;
70
72
  /** The longest level name: the skeleton's name bar is exactly its width (the component's own words, unseen). */
71
73
  const LONGEST_LEVEL_LABEL = APPROVAL_LEVELS.map(level => APPROVAL_LEVEL_META[level].label).reduce((a, b) =>
72
74
  b.length > a.length ? b : a,
73
75
  );
74
76
 
77
+ /** The strip's four levels: the one in force (none while loading) in the primary colour, the others secondary (Figma 4365-22670). */
78
+ function levelItems(inForce: ApprovalLevel | undefined) {
79
+ return APPROVAL_LEVELS.map(level => {
80
+ const { label, Icon } = APPROVAL_LEVEL_META[level];
81
+ const tone = level === inForce ? 'text-ods-text-primary' : 'text-ods-text-secondary';
82
+ return { id: level, ariaLabel: label, icon: <Icon size={EDITABLE_ICON_PX} className={tone} aria-hidden /> };
83
+ });
84
+ }
85
+
75
86
  export interface ApprovalLevelViewProps {
76
87
  /** The level in force. */
77
88
  value: ApprovalLevel;
@@ -100,13 +111,11 @@ export interface ApprovalLevelViewProps {
100
111
 
101
112
  /**
102
113
  * THE way a level is shown, read-only or editable, by one prop: the level's
103
- * name in its colour, then its icon (read-only) or the four levels' icons as a
104
- * single-choice group (editable; arrow keys move, like every toggle group).
105
- * Name, icon, colour and tooltip are `APPROVAL_LEVEL_META`'s in both, so a
106
- * rule reads the same on a page that shows it and on one that lets it change.
107
- *
108
- * Editable, each icon has its level's name as its accessible name and a
109
- * tooltip with what the level means (hover or keyboard focus; a tap selects).
114
+ * icon in its colour, then its name (read-only, Figma 1608-49531), or its name
115
+ * in its colour, then the four levels' icons in a compact `TabSelector` strip,
116
+ * the one in force filled (editable, Figma 4365-22670). Name, icon and colour
117
+ * are `APPROVAL_LEVEL_META`'s in both. Editable, each icon has its level's name
118
+ * as its accessible name.
110
119
  */
111
120
  export function ApprovalLevelView({
112
121
  value,
@@ -124,18 +133,16 @@ export function ApprovalLevelView({
124
133
  return <ApprovalLevelSkeleton editable={canEdit} showLabel={showLabel} className={className} />;
125
134
  }
126
135
  const current = APPROVAL_LEVEL_META[value];
127
- const name = showLabel ? <span className={cn(LEVEL_NAME_CLASS, current.toneClassName)}>{current.label}</span> : null;
128
136
 
129
137
  if (!canEdit) {
130
138
  const icon = (
131
- <span className={cn('inline-flex shrink-0', current.toneClassName)}>
139
+ <span className={cn('inline-flex shrink-0', READ_ONLY_ICON_BOX_CLASS, current.toneClassName)}>
132
140
  <current.Icon size={ICON_PX} aria-hidden />
133
141
  {!showLabel && <span className="sr-only">{current.label}</span>}
134
142
  </span>
135
143
  );
136
144
  return (
137
145
  <span className={cn(READ_ONLY_ROW_CLASS, className)}>
138
- {name}
139
146
  {showLabel ? (
140
147
  icon
141
148
  ) : (
@@ -143,49 +150,32 @@ export function ApprovalLevelView({
143
150
  {icon}
144
151
  </TouchFriendlyTooltip>
145
152
  )}
153
+ {showLabel && <span className={READ_ONLY_NAME_CLASS}>{current.label}</span>}
146
154
  </span>
147
155
  );
148
156
  }
149
157
 
158
+ const name = showLabel ? (
159
+ <span className={cn(LEVEL_NAME_CLASS, EDITABLE_NAME_ORDER_CLASS, current.toneClassName)}>{current.label}</span>
160
+ ) : null;
161
+
150
162
  return (
151
163
  <div className={cn(EDITABLE_ROW_CLASS, className)}>
152
164
  {name}
153
- <TooltipProvider delayDuration={150}>
154
- <ToggleGroup
155
- type="single"
156
- variant="outline"
157
- size="sm"
165
+ {/* The settings design's segmented control: one strip, the level in force filled (Figma 4365-22670). */}
166
+ <div role="group" aria-labelledby={labelledBy}>
167
+ <TabSelector
168
+ variant="secondary"
169
+ size="compact"
158
170
  value={value}
159
171
  disabled={disabled}
160
- // Clicking the level in force reports '' (deselect): a rule always has a level.
161
- onValueChange={(next: string) => {
162
- if (next && next !== value) onChange?.(next as ApprovalLevel);
172
+ onValueChange={next => {
173
+ if (next !== value) onChange?.(next as ApprovalLevel);
163
174
  }}
164
- aria-labelledby={labelledBy}
165
- >
166
- {APPROVAL_LEVELS.map(level => {
167
- const { label, hint, Icon, toneClassName } = APPROVAL_LEVEL_META[level];
168
- return (
169
- // A plain tooltip (hover and keyboard focus): a tap must SELECT the level, and on
170
- // touch the name beside the group says what is in force.
171
- <Tooltip key={level}>
172
- <TooltipTrigger asChild>
173
- <ToggleGroupItem
174
- value={level}
175
- aria-label={label}
176
- className={cn(LEVEL_BUTTON_CLASS, level === value && toneClassName)}
177
- >
178
- <Icon size={ICON_PX} aria-hidden />
179
- </ToggleGroupItem>
180
- </TooltipTrigger>
181
- <TooltipContent side="top" className="max-w-xs">
182
- <b>{label}</b>: {hint}
183
- </TooltipContent>
184
- </Tooltip>
185
- );
186
- })}
187
- </ToggleGroup>
188
- </TooltipProvider>
175
+ items={levelItems(value)}
176
+ className="w-auto"
177
+ />
178
+ </div>
189
179
  </div>
190
180
  );
191
181
  }
@@ -200,35 +190,41 @@ interface ApprovalLevelSkeletonProps {
200
190
 
201
191
  /**
202
192
  * `ApprovalLevelView`'s own skeleton (a level chip, not a whole view): a level while it is read: the view's own row, the name a bar at its type step (the longest name's width), and the
203
- * read-only icon's box or the editable group's four small toggle boxes. Inert: nothing in it can be picked.
193
+ * read-only icon's box or the editable strip itself, disabled. Inert: nothing in it can be picked.
204
194
  */
205
195
  function ApprovalLevelSkeleton({ editable = false, showLabel = true, className }: ApprovalLevelSkeletonProps) {
206
- const name = showLabel ? (
207
- <span className={LEVEL_NAME_CLASS}>
208
- <TextHold>{LONGEST_LEVEL_LABEL}</TextHold>
209
- </span>
210
- ) : null;
211
196
  if (!editable) {
212
197
  return (
213
198
  <span aria-hidden className={cn(READ_ONLY_ROW_CLASS, className)}>
214
- {name}
215
- <UnifiedSkeleton as="span" className="inline-block shrink-0 rounded-sm" width={ICON_PX} height={ICON_PX} />
199
+ <span className={cn('inline-flex shrink-0', READ_ONLY_ICON_BOX_CLASS)}>
200
+ <UnifiedSkeleton as="span" className="inline-block shrink-0 rounded-sm" width={ICON_PX} height={ICON_PX} />
201
+ </span>
202
+ {showLabel && (
203
+ <span className={READ_ONLY_NAME_CLASS}>
204
+ <TextHold>{LONGEST_LEVEL_LABEL}</TextHold>
205
+ </span>
206
+ )}
216
207
  </span>
217
208
  );
218
209
  }
210
+ const name = showLabel ? (
211
+ <span className={cn(LEVEL_NAME_CLASS, EDITABLE_NAME_ORDER_CLASS)}>
212
+ <TextHold>{LONGEST_LEVEL_LABEL}</TextHold>
213
+ </span>
214
+ ) : null;
219
215
  return (
220
216
  <div aria-hidden inert className={cn(EDITABLE_ROW_CLASS, className)}>
221
217
  {name}
222
- <div className={TOGGLE_GROUP_CLASS}>
223
- {APPROVAL_LEVELS.map(level => (
224
- <UnifiedSkeleton
225
- key={level}
226
- // The small toggle's height and its tighter sides around one icon (`box-content`: the padding adds).
227
- className={cn(TOGGLE_SIZE_CLASS.sm, LEVEL_BUTTON_CLASS, 'box-content shrink-0 rounded-md')}
228
- width={ICON_PX}
229
- />
230
- ))}
231
- </div>
218
+ {/* The real strip, inert, with no level in force yet. */}
219
+ <TabSelector
220
+ variant="secondary"
221
+ size="compact"
222
+ value=""
223
+ disabled
224
+ onValueChange={() => {}}
225
+ items={levelItems(undefined)}
226
+ className="w-auto"
227
+ />
232
228
  </div>
233
229
  );
234
230
  }