@flamingo-stack/openframe-frontend-core 0.0.850 → 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.
Files changed (114) hide show
  1. package/dist/chat-protocol/events.d.ts +4 -1
  2. package/dist/chat-protocol/events.d.ts.map +1 -1
  3. package/dist/chat-protocol/index.cjs +90 -40
  4. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  5. package/dist/chat-protocol/nats-decoder.js +5 -1
  6. package/dist/chat-protocol/policy-review.d.ts +18 -0
  7. package/dist/chat-protocol/policy-review.d.ts.map +1 -0
  8. package/dist/chat-protocol/policy-review.js +55 -0
  9. package/dist/{chunk-ZDSC3GFX.cjs → chunk-2NGW73T2.cjs} +3 -3
  10. package/dist/{chunk-2QZUTNL3.cjs → chunk-2WZHNMQY.cjs} +67 -67
  11. package/dist/{chunk-R5JPZ6TY.cjs → chunk-5SEJEBAB.cjs} +8 -8
  12. package/dist/{chunk-TX4NTH22.cjs → chunk-B6D4WWKD.cjs} +36 -36
  13. package/dist/{chunk-FXLROQUT.cjs → chunk-CDW6JTAQ.cjs} +442 -459
  14. package/dist/{chunk-5PIR77V5.cjs → chunk-DN227KZH.cjs} +4 -4
  15. package/dist/{chunk-QJER2ZNA.cjs → chunk-FGSN5UNY.cjs} +3 -3
  16. package/dist/{chunk-ZGEWT3SF.cjs → chunk-HXURNKXI.cjs} +3 -3
  17. package/dist/{chunk-FRG724DU.cjs → chunk-KRTTH3ET.cjs} +4 -4
  18. package/dist/{chunk-YJZ7R3W5.cjs → chunk-KXFIYO4O.cjs} +8 -8
  19. package/dist/{chunk-4YBS4HIO.cjs → chunk-LM7EL3UQ.cjs} +444 -27
  20. package/dist/{chunk-LGYPCUHO.cjs → chunk-NQEPLUUW.cjs} +21 -21
  21. package/dist/{chunk-OHHDX3Z4.cjs → chunk-ODMZSAIY.cjs} +36 -36
  22. package/dist/{chunk-VRWBJL7B.cjs → chunk-OUP7L3U5.cjs} +50 -50
  23. package/dist/{chunk-5CBI2ZYV.cjs → chunk-PJNEBSP5.cjs} +5 -5
  24. package/dist/{chunk-6AJYGXXS.cjs → chunk-PPCRLSVR.cjs} +11 -11
  25. package/dist/{chunk-SA2NNWIJ.cjs → chunk-QCSA7GIX.cjs} +17 -62
  26. package/dist/{chunk-JWIITCVB.cjs → chunk-SPKAXETT.cjs} +42 -42
  27. package/dist/{chunk-CAGNWDVJ.cjs → chunk-U4RTWH5D.cjs} +42 -42
  28. package/dist/{chunk-IHY7E4EX.cjs → chunk-UFGPLXH6.cjs} +4 -4
  29. package/dist/{chunk-7ICHJVAT.cjs → chunk-UQK6J3ZS.cjs} +13 -13
  30. package/dist/{chunk-S3QXLD4X.cjs → chunk-VCFQHPOO.cjs} +37 -3
  31. package/dist/{chunk-IPGGSFR7.cjs → chunk-VU6N3NZS.cjs} +6 -6
  32. package/dist/{chunk-GAYLAA3L.cjs → chunk-VXIZCUNZ.cjs} +61 -61
  33. package/dist/{chunk-3W4ASAXN.cjs → chunk-WYROOMUZ.cjs} +2 -2
  34. package/dist/{chunk-XKOL5CNU.cjs → chunk-ZEPEFKHA.cjs} +811 -932
  35. package/dist/{cloud-tenants-TVB3DT3J.cjs → cloud-tenants-XH6GV4EI.cjs} +12 -13
  36. package/dist/components/case-studies/index.cjs +24 -25
  37. package/dist/components/chat/approval-batch-message.d.ts +5 -0
  38. package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
  39. package/dist/components/chat/approval-batch-message.js +36 -10
  40. package/dist/components/chat/index.cjs +8 -9
  41. package/dist/components/chat/policy-review.d.ts +27 -0
  42. package/dist/components/chat/policy-review.d.ts.map +1 -0
  43. package/dist/components/chat/policy-review.js +67 -0
  44. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
  45. package/dist/components/chat/stream/message-mutations.js +4 -1
  46. package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
  47. package/dist/components/chat/tool-execution-display.js +16 -5
  48. package/dist/components/chat/types/message.types.d.ts +40 -2
  49. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  50. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  51. package/dist/components/chat/utils/extract-incomplete-message-state.js +2 -0
  52. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  53. package/dist/components/chat/utils/message-segment-accumulator.js +3 -0
  54. package/dist/components/chat/utils/process-historical-messages.d.ts +2 -2
  55. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  56. package/dist/components/chat/utils/process-historical-messages.js +7 -1
  57. package/dist/components/contact/index.cjs +13 -14
  58. package/dist/components/docs/index.cjs +16 -17
  59. package/dist/components/embeds/index.cjs +13 -14
  60. package/dist/components/faq/index.cjs +14 -15
  61. package/dist/components/features/approval-level.d.ts +6 -12
  62. package/dist/components/features/approval-level.d.ts.map +1 -1
  63. package/dist/components/features/approval-level.js +45 -55
  64. package/dist/components/features/index.cjs +16 -16
  65. package/dist/components/features/notifications/approval-request-notification-tile.d.ts.map +1 -1
  66. package/dist/components/features/notifications/approval-request-notification-tile.js +71 -48
  67. package/dist/components/features/notifications/types.d.ts +2 -0
  68. package/dist/components/features/notifications/types.d.ts.map +1 -1
  69. package/dist/components/features/notifications/types.js +3 -1
  70. package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
  71. package/dist/components/features/policy-configuration-panel.js +75 -64
  72. package/dist/components/help-center-pages/index.cjs +78 -79
  73. package/dist/components/index.cjs +57 -59
  74. package/dist/components/meeting-scheduler/index.cjs +61 -61
  75. package/dist/components/navigation/index.cjs +15 -16
  76. package/dist/components/onboarding-guides/index.cjs +14 -15
  77. package/dist/components/product-demo/index.cjs +23 -24
  78. package/dist/components/tickets/index.cjs +17 -18
  79. package/dist/components/ui/file-manager/index.cjs +8 -9
  80. package/dist/components/ui/index.cjs +16 -18
  81. package/dist/components/ui/radio-group.d.ts.map +1 -1
  82. package/dist/components/ui/radio-group.js +8 -2
  83. package/dist/components/ui/tab-selector.d.ts +7 -1
  84. package/dist/components/ui/tab-selector.d.ts.map +1 -1
  85. package/dist/components/ui/tab-selector.js +22 -3
  86. package/dist/{devices-NGCTSXIU.cjs → devices-7PT6MPJI.cjs} +13 -14
  87. package/dist/{embeddable-chat-322NDFP5.cjs → embeddable-chat-5NY76K7K.cjs} +84 -85
  88. package/dist/index.cjs +23 -25
  89. package/dist/{knowledge-75PO5DYS.cjs → knowledge-J6N7IEVI.cjs} +13 -14
  90. package/dist/{logs-PZESOAIG.cjs → logs-D6KQP2BX.cjs} +19 -20
  91. package/dist/{policies-6XNMBFWO.cjs → policies-J7MCOL3C.cjs} +16 -17
  92. package/dist/{remote-session-Q2FIBCGT.cjs → remote-session-PXHA5KFK.cjs} +8 -9
  93. package/dist/{scripts-B3JW6TPF.cjs → scripts-VURXIHVS.cjs} +19 -20
  94. package/dist/{software-update-NWP542J7.cjs → software-update-6SHUBDIE.cjs} +16 -17
  95. package/dist/{tickets-board-7FLGQEHF.cjs → tickets-board-NLCN7WNX.cjs} +9 -9
  96. package/package.json +1 -1
  97. package/src/chat-protocol/events.ts +4 -1
  98. package/src/chat-protocol/nats-decoder.ts +4 -0
  99. package/src/chat-protocol/policy-review.ts +71 -0
  100. package/src/components/chat/approval-batch-message.tsx +47 -10
  101. package/src/components/chat/policy-review.tsx +90 -0
  102. package/src/components/chat/stream/message-mutations.ts +4 -0
  103. package/src/components/chat/tool-execution-display.tsx +23 -4
  104. package/src/components/chat/types/message.types.ts +42 -2
  105. package/src/components/chat/utils/extract-incomplete-message-state.ts +2 -0
  106. package/src/components/chat/utils/message-segment-accumulator.ts +3 -0
  107. package/src/components/chat/utils/process-historical-messages.ts +14 -3
  108. package/src/components/features/approval-level.tsx +74 -78
  109. package/src/components/features/notifications/approval-request-notification-tile.tsx +75 -50
  110. package/src/components/features/notifications/types.ts +4 -0
  111. package/src/components/features/policy-configuration-panel.tsx +100 -83
  112. package/src/components/ui/radio-group.tsx +20 -2
  113. package/src/components/ui/tab-selector.tsx +26 -3
  114. package/dist/chunk-N3MBQRCQ.cjs +0 -41
@@ -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
  }
@@ -5,7 +5,9 @@ import { cn } from '../../../utils/cn';
5
5
  import { ApprovalBatchMessage, ApprovalStatusTag } from '../../chat/approval-batch-message';
6
6
  import { ExpandChevron } from '../../chat/expand-chevron';
7
7
  import { useCollapsible } from '../../chat/hooks/use-collapsible';
8
+ import { PolicyEscalationAlert, isPolicyEscalated } from '../../chat/policy-review';
8
9
  import type { ChatApprovalStatus } from '../../chat/types';
10
+ import { AlertCircleIcon } from '../../icons-v2-generated';
9
11
  import { Button } from '../../ui/button';
10
12
  import {
11
13
  APPROVAL_BUTTON_CLASS,
@@ -85,6 +87,10 @@ export function ApprovalRequestNotificationTile({
85
87
  type: notification.type ?? 'Approval Required',
86
88
  };
87
89
 
90
+ // What Mingo escalated is read with the commands folded away, in the chat block's order (Figma 1375-114530): the
91
+ // "!" beside the chevron, the "Mingo Escalated" band before the buttons, Approve in the warning colour.
92
+ const escalatedCalls = batchData.toolCalls.filter(call => isPolicyEscalated(call.policyReview));
93
+ const escalated = escalatedCalls.length > 0;
88
94
  const commandWord = batchData.toolCalls.length > 1 ? 'commands' : 'command';
89
95
  const toggleLabel = `${expanded ? 'Hide' : 'Show'} ${commandWord} section`;
90
96
 
@@ -119,57 +125,75 @@ export function ApprovalRequestNotificationTile({
119
125
  timeLabel={timeLabel}
120
126
  paused={pinned || processing}
121
127
  actions={
122
- <div className="flex w-full items-center gap-[var(--spacing-system-xs)]">
123
- {status === 'pending' ? (
124
- <>
125
- <Button
126
- variant="accent"
127
- size="small"
128
- onClick={() => resolve('approved', onApprove)}
129
- disabled={processing}
130
- data-pressed={approvePressed || undefined}
131
- className={cn(
132
- 'transition-[filter,transform] duration-200 motion-reduce:transition-none',
133
- lockscreen ? LOCKSCREEN_APPROVAL_BUTTON_CLASS : APPROVAL_BUTTON_CLASS,
134
- approvePressed && 'scale-[0.98] brightness-75',
128
+ <>
129
+ {!lockscreen &&
130
+ escalatedCalls.map(call => (
131
+ // Edge to edge, as under the chat's command row: the tile's padding cancelled (`w-auto`, so the stretched
132
+ // flex item grows into it instead of keeping the Alert's `w-full`), a rule above and below.
133
+ <PolicyEscalationAlert
134
+ key={call.toolExecutionRequestId}
135
+ review={call.policyReview}
136
+ className="-mx-[var(--spacing-system-s)] w-auto border-y border-ods-border"
137
+ />
138
+ ))}
139
+ <div className="flex w-full items-center gap-[var(--spacing-system-xs)]">
140
+ {status === 'pending' ? (
141
+ <>
142
+ <Button
143
+ variant={escalated ? 'warning' : 'accent'}
144
+ size="small"
145
+ onClick={() => resolve('approved', onApprove)}
146
+ disabled={processing}
147
+ data-pressed={approvePressed || undefined}
148
+ className={cn(
149
+ 'transition-[filter,transform] duration-200 motion-reduce:transition-none',
150
+ lockscreen ? LOCKSCREEN_APPROVAL_BUTTON_CLASS : APPROVAL_BUTTON_CLASS,
151
+ approvePressed && 'scale-[0.98] brightness-75',
152
+ )}
153
+ >
154
+ Approve
155
+ </Button>
156
+ <Button
157
+ variant="outline"
158
+ size="small"
159
+ onClick={() => resolve('rejected', onReject)}
160
+ disabled={processing}
161
+ className={lockscreen ? LOCKSCREEN_APPROVAL_BUTTON_CLASS : APPROVAL_BUTTON_CLASS}
162
+ >
163
+ Reject
164
+ </Button>
165
+ </>
166
+ ) : (
167
+ <span className="flex min-w-0 items-center gap-[var(--spacing-system-xsf)]">
168
+ <ApprovalStatusTag status={status} />
169
+ {approval.resolvedByName ? (
170
+ <span className="truncate text-ods-text-secondary text-h6">by {approval.resolvedByName}</span>
171
+ ) : null}
172
+ </span>
173
+ )}
174
+ {!lockscreen && (
175
+ <span className="ml-auto flex shrink-0 items-center gap-[var(--spacing-system-xs)]">
176
+ {escalated && !expanded && (
177
+ // Decorative: the band above says it in words. Open, each command row draws its own.
178
+ <AlertCircleIcon className="size-4 text-ods-warning" aria-hidden data-escalation-mark />
135
179
  )}
136
- >
137
- Approve
138
- </Button>
139
- <Button
140
- variant="outline"
141
- size="small"
142
- onClick={() => resolve('rejected', onReject)}
143
- disabled={processing}
144
- className={lockscreen ? LOCKSCREEN_APPROVAL_BUTTON_CLASS : APPROVAL_BUTTON_CLASS}
145
- >
146
- Reject
147
- </Button>
148
- </>
149
- ) : (
150
- <span className="flex min-w-0 items-center gap-[var(--spacing-system-xsf)]">
151
- <ApprovalStatusTag status={status} />
152
- {approval.resolvedByName ? (
153
- <span className="truncate text-ods-text-secondary text-h6">by {approval.resolvedByName}</span>
154
- ) : null}
155
- </span>
156
- )}
157
- {!lockscreen && (
158
- <Button
159
- variant="outline"
160
- size="small"
161
- onClick={() => {
162
- setExpanded(prev => !prev);
163
- setPinned(true);
164
- }}
165
- aria-expanded={expanded}
166
- aria-label={toggleLabel}
167
- className={APPROVAL_TOGGLE_BUTTON_CLASS}
168
- >
169
- <ExpandChevron expanded={expanded} />
170
- </Button>
171
- )}
172
- </div>
180
+ <Button
181
+ variant="outline"
182
+ size="small"
183
+ onClick={() => {
184
+ setExpanded(prev => !prev);
185
+ setPinned(true);
186
+ }}
187
+ aria-expanded={expanded}
188
+ aria-label={toggleLabel}
189
+ className={APPROVAL_TOGGLE_BUTTON_CLASS}
190
+ >
191
+ <ExpandChevron expanded={expanded} />
192
+ </Button>
193
+ </span>
194
+ )}
195
+ </div>
196
+ </>
173
197
  }
174
198
  >
175
199
  {!lockscreen && (
@@ -182,6 +206,7 @@ export function ApprovalRequestNotificationTile({
182
206
  data={batchData}
183
207
  showExecutionStatus={false}
184
208
  showFooterActions={false}
209
+ showEscalationAlerts={false}
185
210
  maxBodyHeight="50vh"
186
211
  className="mb-0 rounded-none border-0"
187
212
  />
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
+ import { policyReviewField } from '../../../chat-protocol/policy-review';
2
3
  import type { NotificationSeverity } from '../../../utils/notifications';
3
4
  import { APPROVAL_STATUS, type ApprovalBatchData, type ChatApprovalStatus } from '../../chat/types';
4
5
 
@@ -73,6 +74,8 @@ export interface ApprovalToolCallMeta {
73
74
  requiresApproval?: boolean;
74
75
  approvalType?: string | null;
75
76
  toolCallArguments?: Record<string, unknown> | null;
77
+ /** The guardrail review, as the wire sends it: normalized into the batch by `approvalMetaToBatchData`. */
78
+ policyReview?: unknown;
76
79
  }
77
80
 
78
81
  /** Approval payload stashed in `notification.meta` for an approval-request notification. */
@@ -138,6 +141,7 @@ export function approvalMetaToBatchData(approval: ApprovalNotificationMeta): App
138
141
  requiresApproval: tc.requiresApproval ?? true,
139
142
  approvalType: tc.approvalType ?? null,
140
143
  toolCallArguments: tc.toolCallArguments ?? null,
144
+ ...policyReviewField(tc.policyReview),
141
145
  })),
142
146
  };
143
147
  }