@flamingo-stack/openframe-frontend-core 0.0.389 → 0.0.390-snapshot.20260709094846

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 (125) hide show
  1. package/dist/{chunk-3JQUNKWY.cjs → chunk-33PP6VR3.cjs} +15 -15
  2. package/dist/{chunk-3JQUNKWY.cjs.map → chunk-33PP6VR3.cjs.map} +1 -1
  3. package/dist/{chunk-CQXHHVMR.js → chunk-4HV6RIDE.js} +2 -2
  4. package/dist/{chunk-LFLFQ422.js → chunk-BY337N4T.js} +2 -2
  5. package/dist/{chunk-M6ODI2F5.js → chunk-CAEKZPSH.js} +2 -2
  6. package/dist/{chunk-JSFHTIKA.js → chunk-CPWR2BJG.js} +2 -2
  7. package/dist/{chunk-Q4YGG3CL.cjs → chunk-DAYWQZIW.cjs} +31 -31
  8. package/dist/{chunk-Q4YGG3CL.cjs.map → chunk-DAYWQZIW.cjs.map} +1 -1
  9. package/dist/{chunk-44Q3IXZM.js → chunk-GM5M4L7O.js} +2 -2
  10. package/dist/{chunk-OBKCQ7LA.cjs → chunk-GWJSQEQO.cjs} +62 -62
  11. package/dist/{chunk-OBKCQ7LA.cjs.map → chunk-GWJSQEQO.cjs.map} +1 -1
  12. package/dist/{chunk-BJPQJNSC.js → chunk-HPTHEA2A.js} +4 -4
  13. package/dist/{chunk-WIMXB5EU.js → chunk-KWLSVTDN.js} +4 -4
  14. package/dist/{chunk-7MKU2EB4.cjs → chunk-LC4VIFII.cjs} +26 -26
  15. package/dist/{chunk-7MKU2EB4.cjs.map → chunk-LC4VIFII.cjs.map} +1 -1
  16. package/dist/{chunk-KA4WXCN3.js → chunk-QHKU6473.js} +4 -4
  17. package/dist/{chunk-MPOP47ME.cjs → chunk-QQQW4DUI.cjs} +12 -12
  18. package/dist/{chunk-MPOP47ME.cjs.map → chunk-QQQW4DUI.cjs.map} +1 -1
  19. package/dist/{chunk-NULFIYK2.js → chunk-QXD4OZCN.js} +2 -2
  20. package/dist/{chunk-ZFZAHLI4.cjs → chunk-R62LSFOU.cjs} +7 -7
  21. package/dist/{chunk-ZFZAHLI4.cjs.map → chunk-R62LSFOU.cjs.map} +1 -1
  22. package/dist/{chunk-4OLIDOLO.js → chunk-SJYXTTUB.js} +2 -2
  23. package/dist/{chunk-EHTE3CTT.cjs → chunk-TXCE75UB.cjs} +37 -37
  24. package/dist/{chunk-EHTE3CTT.cjs.map → chunk-TXCE75UB.cjs.map} +1 -1
  25. package/dist/{chunk-2JCSORCJ.cjs → chunk-USUNV54R.cjs} +26 -26
  26. package/dist/{chunk-2JCSORCJ.cjs.map → chunk-USUNV54R.cjs.map} +1 -1
  27. package/dist/{chunk-IDWKP5H5.js → chunk-VIZUVGIY.js} +1936 -1852
  28. package/dist/chunk-VIZUVGIY.js.map +1 -0
  29. package/dist/{chunk-VXJBE5NX.cjs → chunk-VY3N2RPS.cjs} +59 -59
  30. package/dist/{chunk-VXJBE5NX.cjs.map → chunk-VY3N2RPS.cjs.map} +1 -1
  31. package/dist/{chunk-GNB5ERZP.cjs → chunk-Y4S6X7IV.cjs} +1574 -1490
  32. package/dist/chunk-Y4S6X7IV.cjs.map +1 -0
  33. package/dist/{chunk-MIEODAAV.cjs → chunk-YKQE7DDT.cjs} +9 -9
  34. package/dist/{chunk-MIEODAAV.cjs.map → chunk-YKQE7DDT.cjs.map} +1 -1
  35. package/dist/{chunk-XGOX5UQ2.js → chunk-YU2RPRI4.js} +4 -4
  36. package/dist/{chunk-TQZLC3BQ.cjs → chunk-ZHYT2HYT.cjs} +5 -5
  37. package/dist/{chunk-TQZLC3BQ.cjs.map → chunk-ZHYT2HYT.cjs.map} +1 -1
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/approval-batch-message.d.ts +13 -4
  41. package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
  42. package/dist/components/chat/approval-request-message.d.ts.map +1 -1
  43. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  44. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  45. package/dist/components/chat/index.cjs +4 -2
  46. package/dist/components/chat/index.cjs.map +1 -1
  47. package/dist/components/chat/index.js +3 -1
  48. package/dist/components/chat/types/chat.types.d.ts +13 -0
  49. package/dist/components/chat/types/chat.types.d.ts.map +1 -1
  50. package/dist/components/chat/types/component.types.d.ts +21 -3
  51. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  52. package/dist/components/chat/types/message.types.d.ts +3 -0
  53. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  54. package/dist/components/contact/index.cjs +3 -3
  55. package/dist/components/contact/index.js +2 -2
  56. package/dist/components/docs/index.cjs +5 -5
  57. package/dist/components/docs/index.js +4 -4
  58. package/dist/components/embeds/index.cjs +3 -3
  59. package/dist/components/embeds/index.js +2 -2
  60. package/dist/components/faq/index.cjs +3 -3
  61. package/dist/components/faq/index.js +2 -2
  62. package/dist/components/features/auth/accept-invitation-form.d.ts +2 -2
  63. package/dist/components/features/auth/accept-invitation-form.d.ts.map +1 -1
  64. package/dist/components/features/index.cjs +2 -2
  65. package/dist/components/features/index.js +1 -1
  66. package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
  67. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts +5 -2
  68. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -1
  69. package/dist/components/help-center-pages/index.cjs +20 -20
  70. package/dist/components/help-center-pages/index.js +11 -11
  71. package/dist/components/index.cjs +122 -120
  72. package/dist/components/index.cjs.map +1 -1
  73. package/dist/components/index.js +11 -9
  74. package/dist/components/index.js.map +1 -1
  75. package/dist/components/navigation/app-header.d.ts.map +1 -1
  76. package/dist/components/navigation/app-layout-context.d.ts +34 -0
  77. package/dist/components/navigation/app-layout-context.d.ts.map +1 -0
  78. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  79. package/dist/components/navigation/app-layout.d.ts +2 -21
  80. package/dist/components/navigation/app-layout.d.ts.map +1 -1
  81. package/dist/components/navigation/index.cjs +2 -2
  82. package/dist/components/navigation/index.js +1 -1
  83. package/dist/components/onboarding-guides/index.cjs +5 -5
  84. package/dist/components/onboarding-guides/index.js +4 -4
  85. package/dist/components/related-content/index.cjs +3 -3
  86. package/dist/components/related-content/index.js +2 -2
  87. package/dist/components/tickets/index.cjs +5 -5
  88. package/dist/components/tickets/index.js +4 -4
  89. package/dist/components/ui/index.cjs +4 -2
  90. package/dist/components/ui/index.cjs.map +1 -1
  91. package/dist/components/ui/index.js +3 -1
  92. package/dist/index.cjs +4 -2
  93. package/dist/index.cjs.map +1 -1
  94. package/dist/index.js +3 -1
  95. package/dist/utils/index.cjs.map +1 -1
  96. package/dist/utils/index.js.map +1 -1
  97. package/package.json +1 -1
  98. package/src/components/chat/approval-batch-message.tsx +104 -88
  99. package/src/components/chat/approval-request-message.tsx +57 -3
  100. package/src/components/chat/chat-message-enhanced.tsx +5 -1
  101. package/src/components/chat/chat-message-list.tsx +3 -0
  102. package/src/components/chat/types/chat.types.ts +17 -0
  103. package/src/components/chat/types/component.types.ts +21 -3
  104. package/src/components/chat/types/message.types.ts +3 -0
  105. package/src/components/features/auth/accept-invitation-form.tsx +4 -4
  106. package/src/components/features/notifications/notification-drawer.tsx +72 -43
  107. package/src/components/features/time-tracker/time-tracker-header-button.tsx +46 -6
  108. package/src/components/navigation/app-header.tsx +13 -9
  109. package/src/components/navigation/app-layout-context.ts +46 -0
  110. package/src/components/navigation/app-layout-drawer.tsx +21 -11
  111. package/src/components/navigation/app-layout.tsx +17 -39
  112. package/src/stories/ApprovalBatchMessageClient.stories.tsx +20 -62
  113. package/dist/chunk-GNB5ERZP.cjs.map +0 -1
  114. package/dist/chunk-IDWKP5H5.js.map +0 -1
  115. /package/dist/{chunk-CQXHHVMR.js.map → chunk-4HV6RIDE.js.map} +0 -0
  116. /package/dist/{chunk-LFLFQ422.js.map → chunk-BY337N4T.js.map} +0 -0
  117. /package/dist/{chunk-M6ODI2F5.js.map → chunk-CAEKZPSH.js.map} +0 -0
  118. /package/dist/{chunk-JSFHTIKA.js.map → chunk-CPWR2BJG.js.map} +0 -0
  119. /package/dist/{chunk-44Q3IXZM.js.map → chunk-GM5M4L7O.js.map} +0 -0
  120. /package/dist/{chunk-BJPQJNSC.js.map → chunk-HPTHEA2A.js.map} +0 -0
  121. /package/dist/{chunk-WIMXB5EU.js.map → chunk-KWLSVTDN.js.map} +0 -0
  122. /package/dist/{chunk-KA4WXCN3.js.map → chunk-QHKU6473.js.map} +0 -0
  123. /package/dist/{chunk-NULFIYK2.js.map → chunk-QXD4OZCN.js.map} +0 -0
  124. /package/dist/{chunk-4OLIDOLO.js.map → chunk-SJYXTTUB.js.map} +0 -0
  125. /package/dist/{chunk-XGOX5UQ2.js.map → chunk-YU2RPRI4.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.389",
3
+ "version": "0.0.390-snapshot.20260709094846",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,8 +9,9 @@ import { ToolIcon } from "../tool-icon"
9
9
  import { CheckCircleIcon, DotsLoaderIcon, XmarkCircleIcon, XmarkIcon } from "../icons-v2-generated"
10
10
  import { ExpandChevron } from "./expand-chevron"
11
11
  import { useCollapsible } from "./hooks/use-collapsible"
12
- import { ArgRow, CommandBlock, ResultBlock } from "./tool-call-blocks"
12
+ import { ArgRow, ResultBlock } from "./tool-call-blocks"
13
13
  import type {
14
+ ApprovalBlockVariant,
14
15
  AssistantType,
15
16
  ApprovalBatchExecutionState,
16
17
  ApprovalBatchSegment,
@@ -18,7 +19,6 @@ import type {
18
19
  } from "./types"
19
20
  import {
20
21
  COMMAND_BODY_ARG_KEYS,
21
- getCommandBody,
22
22
  getCommandText,
23
23
  } from "./utils/tool-call-helpers"
24
24
 
@@ -42,11 +42,20 @@ export interface ApprovalBatchMessageProps extends React.HTMLAttributes<HTMLDivE
42
42
  */
43
43
  showExecutionStatus?: boolean
44
44
  /**
45
- * Chat identity, which drives the styling variant. `'fae'` = client
46
- * (frameless outer, bordered command box, no tool icon, no divider,
47
- * full-text status pill); `'mingo'`/undefined = admin (original layout).
45
+ * Chat identity. Kept for prop parity with the message renderers; does NOT
46
+ * drive the styling anymore use `variant` instead. An admin can be
47
+ * looking at a Fae dialog (tickets dialog client tab) and must still see
48
+ * the full admin block.
48
49
  */
49
50
  assistantType?: AssistantType
51
+ /**
52
+ * Viewer variant. `'admin'` (default) = full block with command preview,
53
+ * expandable args/result and tool icon. `'client'` = end-client (Fae
54
+ * desktop app) card that shows ONLY the BE-generated title(s) plus the
55
+ * Approve/Reject buttons or the full-text resolved pill ("Approved by
56
+ * {name}") — commands and scripts are never rendered.
57
+ */
58
+ variant?: ApprovalBlockVariant
50
59
  /**
51
60
  * Render the footer Approve/Reject buttons (or the resolved-status tag).
52
61
  * Turn off when the host owns the actions row — e.g. the approval
@@ -119,45 +128,29 @@ interface ToolCallRowProps {
119
128
  batchStatus: ApprovalBatchSegment["status"]
120
129
  execution: ApprovalBatchExecutionState | undefined
121
130
  showExecutionStatus: boolean
122
- isClient: boolean
123
131
  }
124
132
 
125
- function ToolCallRow({ call, expanded, onToggle, batchStatus, execution, showExecutionStatus, isClient }: ToolCallRowProps) {
133
+ // ADMIN-only row: command preview header, expandable args/result. The client
134
+ // variant never renders tool calls — see the `variant === 'client'` branch of
135
+ // `<ApprovalBatchMessage>`.
136
+ function ToolCallRow({ call, expanded, onToggle, batchStatus, execution, showExecutionStatus }: ToolCallRowProps) {
126
137
  const command = getCommandText(call)
127
138
  const args = getArgEntries(call)
128
139
  const toolType = (call.toolType as ToolType) || ("OPENFRAME" as ToolType)
129
140
  const { innerRef, containerStyle } = useCollapsible({ expanded })
130
141
  const result = execution?.status === "done" ? execution.result : undefined
131
- // CLIENT (Fae): the collapsed line shows the human-readable `toolExplanation`
132
- // and the raw command moves into the expanded body (Figma 1972-6100). Falls
133
- // back to the command when no explanation is present. ADMIN is unchanged.
134
- const explanation = call.toolExplanation?.trim()
135
- const headerText = isClient && explanation ? explanation : command
136
- const commandBody = isClient ? getCommandBody(call.toolCallArguments) : undefined
137
- const hasExpandableBody = !!commandBody || args.length > 0 || (typeof result === "string" && result.length > 0)
142
+ const hasExpandableBody = args.length > 0 || (typeof result === "string" && result.length > 0)
138
143
 
139
144
  return (
140
- <div
141
- className={cn(
142
- "bg-ods-card flex flex-col items-start w-full",
143
- // ADMIN: rows share one outer card, separated by a bottom border.
144
- // CLIENT (Fae): each command is its own self-contained bordered box
145
- // (Figma 1092-2804 / 1972-7925) — the outer component is frameless.
146
- isClient
147
- ? "border border-ods-border rounded-[6px] overflow-hidden"
148
- : "border-b border-ods-border last:border-b-0",
149
- )}
150
- >
145
+ <div className="bg-ods-card flex flex-col items-start w-full border-b border-ods-border last:border-b-0">
151
146
  <button
152
147
  type="button"
153
148
  onClick={onToggle}
154
149
  className="flex gap-[var(--spacing-system-xsf)] items-start w-full p-[var(--spacing-system-sf)] cursor-pointer text-left"
155
150
  >
156
- {!isClient && (
157
- <div className="flex items-center justify-center shrink-0 w-5 h-5">
158
- <ToolIcon toolType={toolType} size={16} />
159
- </div>
160
- )}
151
+ <div className="flex items-center justify-center shrink-0 w-5 h-5">
152
+ <ToolIcon toolType={toolType} size={16} />
153
+ </div>
161
154
  <div
162
155
  className={cn(
163
156
  "flex-1 min-w-0 text-h6",
@@ -166,7 +159,7 @@ function ToolCallRow({ call, expanded, onToggle, batchStatus, execution, showExe
166
159
  : "text-ods-text-secondary line-clamp-2 max-h-10 break-all",
167
160
  )}
168
161
  >
169
- {headerText}
162
+ {command}
170
163
  </div>
171
164
  {showExecutionStatus && (
172
165
  <div className="flex items-center justify-center shrink-0 w-5 h-5">
@@ -182,12 +175,6 @@ function ToolCallRow({ call, expanded, onToggle, batchStatus, execution, showExe
182
175
  <div ref={innerRef}>
183
176
  {hasExpandableBody && (
184
177
  <div className="flex flex-col gap-0 items-start w-full text-h6 px-[var(--spacing-system-sf)] pb-[var(--spacing-system-sf)] bg-ods-card">
185
- {commandBody && (
186
- <CommandBlock
187
- command={commandBody}
188
- className={args.length > 0 || result ? "mb-[var(--spacing-system-xsf)]" : undefined}
189
- />
190
- )}
191
178
  {args.map(([key, value]) => (
192
179
  <ArgRow key={key} argKey={key} value={value} />
193
180
  ))}
@@ -206,17 +193,14 @@ function ToolCallRow({ call, expanded, onToggle, batchStatus, execution, showExe
206
193
  }
207
194
 
208
195
  const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProps>(
209
- ({ className, data, onApprove, onReject, status = "pending", maxBodyHeight, resolvedByName, showExecutionStatus = true, assistantType, showFooterActions = true, ...props }, ref) => {
196
+ ({ className, data, onApprove, onReject, status = "pending", maxBodyHeight, resolvedByName, showExecutionStatus = true, assistantType: _assistantType, variant = "admin", showFooterActions = true, ...props }, ref) => {
210
197
  const [expandedId, setExpandedId] = useState<string | null>(null)
211
198
  const [isProcessing, setIsProcessing] = useState(false)
212
- const isClient = assistantType === "fae"
199
+ const isClient = variant === "client"
213
200
 
214
- // CLIENT (Fae) promotes each tool's `toolExplanation` to the command row's
215
- // header, so the footer must NOT repeat it as bullets. ADMIN keeps the
216
- // explanation bullets in the footer.
217
- const explanations = isClient
218
- ? []
219
- : data.toolCalls.map((c) => c.toolExplanation?.trim()).filter((s): s is string => !!s)
201
+ const explanations = data.toolCalls
202
+ .map((c) => c.toolExplanation?.trim())
203
+ .filter((s): s is string => !!s)
220
204
 
221
205
  const handleApprove = async () => {
222
206
  setIsProcessing(true)
@@ -236,6 +220,76 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
236
220
  }
237
221
  }
238
222
 
223
+ const actionButtons = (
224
+ <>
225
+ <Button
226
+ size="small-legacy"
227
+ variant="accent"
228
+ onClick={handleApprove}
229
+ disabled={isProcessing}
230
+ className={cn(
231
+ "bg-ods-accent hover:bg-ods-accent/90",
232
+ "text-h5 text-ods-bg",
233
+ "px-[var(--spacing-system-xsf)] h-8",
234
+ )}
235
+ >
236
+ Approve
237
+ </Button>
238
+ <Button
239
+ size="small-legacy"
240
+ variant="outline"
241
+ onClick={handleReject}
242
+ disabled={isProcessing}
243
+ className={cn(
244
+ "bg-ods-card border-ods-border",
245
+ "text-h5 text-ods-text-primary",
246
+ "hover:bg-ods-bg px-[var(--spacing-system-xsf)] h-8",
247
+ )}
248
+ >
249
+ Reject
250
+ </Button>
251
+ </>
252
+ )
253
+
254
+ // CLIENT (Fae end-user) card — Figma 203-11947 "fae-approval-block".
255
+ // One bordered card: BE-generated title(s) + Approve/Reject buttons or the
256
+ // full-text resolved pill ("Approved by {name}"). No commands, scripts,
257
+ // expansion or execution icons — the end client must not see them.
258
+ if (isClient) {
259
+ const titles = data.toolCalls
260
+ .map((c) => c.toolExplanation?.trim() || c.toolTitle?.trim())
261
+ .filter((s): s is string => !!s)
262
+ return (
263
+ <div
264
+ ref={ref}
265
+ className={cn(
266
+ "bg-ods-card border border-ods-border rounded-md p-[var(--spacing-system-mf)] mb-[var(--spacing-system-xsf)] flex flex-col gap-[var(--spacing-system-mf)]",
267
+ className,
268
+ )}
269
+ {...props}
270
+ >
271
+ {titles.length > 0 ? (
272
+ titles.map((title, i) => (
273
+ <p key={i} className="text-h4 text-ods-text-primary whitespace-pre-line break-words w-full">
274
+ {title}
275
+ </p>
276
+ ))
277
+ ) : (
278
+ <p className="text-h4 text-ods-text-primary w-full">Approval required</p>
279
+ )}
280
+ {showFooterActions && (status === "pending" ? (
281
+ <div className="flex gap-[var(--spacing-system-mf)] items-center w-full">
282
+ {actionButtons}
283
+ </div>
284
+ ) : (
285
+ <div className="flex w-full">
286
+ <ApprovalStatusTag status={status} resolvedByName={resolvedByName} inlineResolver />
287
+ </div>
288
+ ))}
289
+ </div>
290
+ )
291
+ }
292
+
239
293
  const showFooterBlock = explanations.length > 0 || showFooterActions
240
294
 
241
295
  return (
@@ -243,11 +297,7 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
243
297
  ref={ref}
244
298
  className={cn(
245
299
  "flex flex-col mb-[var(--spacing-system-xsf)]",
246
- // ADMIN: one framed card. CLIENT (Fae): frameless container — the
247
- // bordered command box(es) + button/status row stacked 16px apart.
248
- isClient
249
- ? "gap-[var(--spacing-system-mf)]"
250
- : "bg-ods-card border border-ods-border rounded-md overflow-hidden",
300
+ "bg-ods-card border border-ods-border rounded-md overflow-hidden",
251
301
  className,
252
302
  )}
253
303
  {...props}
@@ -255,7 +305,6 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
255
305
  <div
256
306
  className={cn(
257
307
  "flex flex-col",
258
- isClient && "gap-[var(--spacing-system-mf)]",
259
308
  maxBodyHeight != null && "overflow-y-auto overscroll-contain",
260
309
  )}
261
310
  style={maxBodyHeight != null ? { maxHeight: maxBodyHeight } : undefined}
@@ -273,20 +322,12 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
273
322
  batchStatus={status}
274
323
  execution={data.executions?.[call.toolExecutionRequestId]}
275
324
  showExecutionStatus={showExecutionStatus}
276
- isClient={isClient}
277
325
  />
278
326
  ))}
279
327
  </div>
280
328
 
281
329
  {showFooterBlock && (
282
- <div
283
- className={cn(
284
- "flex flex-col gap-[var(--spacing-system-xsf)] items-start justify-center",
285
- // ADMIN: padded footer inside the card, divided from the list.
286
- // CLIENT (Fae): buttons/status sit flush outside the box.
287
- !isClient && "bg-ods-card border-t border-ods-border p-[var(--spacing-system-sf)]",
288
- )}
289
- >
330
+ <div className="flex flex-col gap-[var(--spacing-system-xsf)] items-start justify-center bg-ods-card border-t border-ods-border p-[var(--spacing-system-sf)]">
290
331
  {explanations.length > 0 && (
291
332
  <ul className="list-disc pl-5 text-h6 text-ods-text-primary w-full">
292
333
  {explanations.map((expl, i) => (
@@ -297,37 +338,12 @@ const ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProp
297
338
 
298
339
  {showFooterActions && (status === "pending" ? (
299
340
  <div className="flex gap-[var(--spacing-system-xsf)] items-center">
300
- <Button
301
- size="small-legacy"
302
- variant="accent"
303
- onClick={handleApprove}
304
- disabled={isProcessing}
305
- className={cn(
306
- "bg-ods-accent hover:bg-ods-accent/90",
307
- "text-h5 text-ods-bg",
308
- "px-[var(--spacing-system-xsf)] h-8",
309
- )}
310
- >
311
- Approve
312
- </Button>
313
- <Button
314
- size="small-legacy"
315
- variant="outline"
316
- onClick={handleReject}
317
- disabled={isProcessing}
318
- className={cn(
319
- "bg-ods-card border-ods-border",
320
- "text-h5 text-ods-text-primary",
321
- "hover:bg-ods-bg px-[var(--spacing-system-xsf)] h-8",
322
- )}
323
- >
324
- Reject
325
- </Button>
341
+ {actionButtons}
326
342
  </div>
327
343
  ) : (
328
344
  <div className="flex items-center gap-[var(--spacing-system-xsf)]">
329
- <ApprovalStatusTag status={status} resolvedByName={resolvedByName} inlineResolver={isClient} />
330
- {!isClient && resolvedByName && (
345
+ <ApprovalStatusTag status={status} resolvedByName={resolvedByName} />
346
+ {resolvedByName && (
331
347
  <span className="text-h6 text-ods-text-secondary">by {resolvedByName}</span>
332
348
  )}
333
349
  </div>
@@ -5,6 +5,7 @@ import { cn } from "../../utils/cn"
5
5
  import { Button } from "../ui/button"
6
6
  import { Tag } from "../ui/tag"
7
7
  import { Ban, CheckCircle, XCircle } from "lucide-react"
8
+ import { ApprovalStatusTag } from "./approval-batch-message"
8
9
  import type { ApprovalRequestMessageProps } from "./types"
9
10
  import type { ApprovalRequestField } from "./types/message.types"
10
11
 
@@ -78,9 +79,8 @@ function ApprovalCardBody({
78
79
 
79
80
  const ApprovalRequestMessage = forwardRef<HTMLDivElement, ApprovalRequestMessageProps>(
80
81
  // `assistantType` is accepted for prop-parity with the batch card (so hosts
81
- // can forward it uniformly); the legacy single-command card has no
82
- // icon/divider to vary, so it renders identically for admin and client today.
83
- ({ className, data, onApprove, onReject, status = 'pending', assistantType: _assistantType, ...props }, ref) => {
82
+ // can forward it uniformly); the viewer variant is driven by `variant`.
83
+ ({ className, data, onApprove, onReject, status = 'pending', assistantType: _assistantType, variant = 'admin', resolvedByName, ...props }, ref) => {
84
84
  const [isProcessing, setIsProcessing] = useState(false)
85
85
 
86
86
  const handleApprove = async () => {
@@ -101,6 +101,60 @@ const ApprovalRequestMessage = forwardRef<HTMLDivElement, ApprovalRequestMessage
101
101
  }
102
102
  }
103
103
 
104
+ // CLIENT (Fae end-user) card — Figma 203-11947 "fae-approval-block".
105
+ // Shows ONLY the BE-generated title (`explanation`) plus the actions row
106
+ // or the full-text resolved pill; the raw command is never rendered.
107
+ if (variant === 'client') {
108
+ return (
109
+ <div
110
+ ref={ref}
111
+ className={cn(
112
+ "bg-ods-card border border-ods-border rounded-md p-[var(--spacing-system-mf)] mb-[var(--spacing-system-xsf)] flex flex-col gap-[var(--spacing-system-mf)]",
113
+ className
114
+ )}
115
+ {...props}
116
+ >
117
+ <p className="text-h4 text-ods-text-primary whitespace-pre-line break-words w-full">
118
+ {data.explanation?.trim() || "Approval required"}
119
+ </p>
120
+ {status === 'pending' ? (
121
+ <div className="flex gap-[var(--spacing-system-mf)] items-center w-full">
122
+ <Button
123
+ size="small-legacy"
124
+ variant="accent"
125
+ onClick={handleApprove}
126
+ disabled={isProcessing}
127
+ className={cn(
128
+ "bg-ods-accent hover:bg-ods-accent/90",
129
+ "text-h5 text-ods-bg",
130
+ "px-[var(--spacing-system-xsf)] h-8"
131
+ )}
132
+ >
133
+ Approve
134
+ </Button>
135
+ <Button
136
+ size="small-legacy"
137
+ variant="outline"
138
+ onClick={handleReject}
139
+ disabled={isProcessing}
140
+ className={cn(
141
+ "bg-ods-card border-ods-border",
142
+ "text-h5 text-ods-text-primary",
143
+ "hover:bg-ods-bg px-[var(--spacing-system-xsf)] h-8"
144
+ )}
145
+ >
146
+ Reject
147
+ </Button>
148
+ </div>
149
+ ) : (
150
+ <div className="flex w-full">
151
+ <ApprovalStatusTag status={status} resolvedByName={resolvedByName} inlineResolver />
152
+ </div>
153
+ )}
154
+ </div>
155
+ )
156
+ }
157
+
104
158
  return (
105
159
  <div
106
160
  ref={ref}
@@ -43,7 +43,7 @@ function normalizeContent(content: MessageContent): MessageSegment[] {
43
43
  }
44
44
 
45
45
  const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>(
46
- ({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderContextItem, renderMention, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
46
+ ({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, approvalVariant, authorType: authorTypeProp, assistantIcon, chatRefs, contextItems, resolveContextIcon, renderContextItem, renderMention, renderEntityCard, NavLinkAnchor, ...props }, ref) => {
47
47
  const isUser = role === 'user'
48
48
  const isError = role === 'error'
49
49
  const authorType = authorTypeProp ?? (isUser ? 'user' : assistantType === 'mingo' ? 'mingo' : 'fae')
@@ -546,9 +546,11 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
546
546
  key={index}
547
547
  data={segment.data}
548
548
  status={segment.status}
549
+ resolvedByName={segment.resolvedByName}
549
550
  onApprove={segment.onApprove}
550
551
  onReject={segment.onReject}
551
552
  assistantType={assistantType}
553
+ variant={approvalVariant}
552
554
  />
553
555
  )
554
556
  } else if (segment.type === 'approval_batch') {
@@ -561,6 +563,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
561
563
  onApprove={segment.onApprove}
562
564
  onReject={segment.onReject}
563
565
  assistantType={assistantType}
566
+ variant={approvalVariant}
564
567
  />
565
568
  )
566
569
  } else if (segment.type === 'error') {
@@ -620,6 +623,7 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
620
623
  prevProps.timestamp?.getTime() === nextProps.timestamp?.getTime() &&
621
624
  prevProps.showAvatar === nextProps.showAvatar &&
622
625
  prevProps.assistantType === nextProps.assistantType &&
626
+ prevProps.approvalVariant === nextProps.approvalVariant &&
623
627
  prevProps.authorType === nextProps.authorType &&
624
628
  prevProps.assistantIcon === nextProps.assistantIcon &&
625
629
  prevProps.className === nextProps.className &&
@@ -100,6 +100,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
100
100
  fullWidth = false,
101
101
  contentClassName,
102
102
  assistantType,
103
+ approvalVariant,
103
104
  assistantIcon,
104
105
  pendingApprovals,
105
106
  hasNextPage,
@@ -535,6 +536,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
535
536
  avatar={showAvatars ? message.avatar : null}
536
537
  showAvatar={showAvatars}
537
538
  assistantType={message.assistantType || assistantType}
539
+ approvalVariant={approvalVariant}
538
540
  authorType={message.authorType}
539
541
  assistantIcon={message.role !== 'user' ? assistantIcon : undefined}
540
542
  chatRefs={message.chatRefs}
@@ -590,6 +592,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
590
592
  timestamp={new Date()}
591
593
  showAvatar={showAvatars}
592
594
  assistantType={assistantType}
595
+ approvalVariant={approvalVariant}
593
596
  assistantIcon={assistantIcon}
594
597
  NavLinkAnchor={NavLinkAnchor}
595
598
  className="py-3"
@@ -64,6 +64,23 @@ export const APPROVAL_STATUS = {
64
64
 
65
65
  export type ChatApprovalStatus = typeof APPROVAL_STATUS[keyof typeof APPROVAL_STATUS]
66
66
 
67
+ // ========== Approval Block Variant Definitions ==========
68
+
69
+ /**
70
+ * Who is LOOKING at an approval block — not which assistant produced it.
71
+ * `admin` (default) shows the full block: command/script preview, expandable
72
+ * args/result, tool icon. `client` (end-user Fae desktop app) shows only the
73
+ * BE-generated title plus Approve/Reject or the resolved-status pill — raw
74
+ * commands and scripts must never reach the end client. Admin surfaces that
75
+ * render a Fae dialog (tickets dialog client tab) keep the admin variant.
76
+ */
77
+ export const APPROVAL_BLOCK_VARIANT = {
78
+ ADMIN: 'admin',
79
+ CLIENT: 'client',
80
+ } as const
81
+
82
+ export type ApprovalBlockVariant = typeof APPROVAL_BLOCK_VARIANT[keyof typeof APPROVAL_BLOCK_VARIANT]
83
+
67
84
  // ========== Connection Status Definitions ==========
68
85
 
69
86
  export const CONNECTION_STATUS = {
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { ComponentType, HTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react'
6
- import type { AssistantType, AuthorType, ChatApprovalStatus, ConnectionStatus } from './chat.types'
6
+ import type { ApprovalBlockVariant, AssistantType, AuthorType, ChatApprovalStatus, ConnectionStatus } from './chat.types'
7
7
  import type { ApprovalRequestData, Message, MessageSegment, ToolExecutionData } from './message.types'
8
8
  import type { ChatRef } from '../chat-ref.types'
9
9
  import type { ChatContextItem } from './context-item.types'
@@ -119,6 +119,11 @@ export interface ChatMessageEnhancedProps extends Omit<HTMLAttributes<HTMLDivEle
119
119
  name?: string
120
120
  assistantType?: AssistantType
121
121
  authorType?: AuthorType
122
+ /** Viewer variant for approval blocks in this message's segments.
123
+ * `'admin'` (default) = full command block; `'client'` = end-client
124
+ * (Fae desktop app) title-only card. Forwarded to
125
+ * ApprovalRequestMessage / ApprovalBatchMessage. */
126
+ approvalVariant?: ApprovalBlockVariant
122
127
  assistantIcon?: React.ReactNode
123
128
  avatar?: string | null
124
129
  timestamp?: Date
@@ -229,6 +234,12 @@ export interface ChatMessageListProps extends HTMLAttributes<HTMLDivElement> {
229
234
  * override (custom max-w value, etc.). */
230
235
  contentClassName?: string
231
236
  assistantType?: AssistantType
237
+ /** Viewer variant for approval blocks in every rendered message (incl. the
238
+ * sticky pending-approvals footer). `'admin'` (default) = full command
239
+ * block; `'client'` = end-client (Fae desktop app) title-only card. Set to
240
+ * `'client'` ONLY on true end-client surfaces — admin views of a Fae
241
+ * dialog (tickets dialog client tab) keep the default. */
242
+ approvalVariant?: ApprovalBlockVariant
232
243
  assistantIcon?: React.ReactNode
233
244
  pendingApprovals?: MessageSegment[]
234
245
  onApprove?: (requestId?: string) => void | Promise<void>
@@ -488,9 +499,16 @@ export interface ApprovalRequestMessageProps extends HTMLAttributes<HTMLDivEleme
488
499
  onReject?: (requestId?: string) => void | Promise<void>
489
500
  status?: ChatApprovalStatus
490
501
  disabled?: boolean
491
- /** Chat identity; drives the CLIENT (Fae) styling. Accepted for parity with
492
- * the batch card. `'fae'` = client, `'mingo'`/undefined = admin. */
502
+ /** Chat identity. Accepted for parity with the batch card; does NOT drive
503
+ * the styling use `variant`. */
493
504
  assistantType?: AssistantType
505
+ /** Viewer variant. `'admin'` (default) = full card with the raw command;
506
+ * `'client'` = end-client (Fae desktop app) card with only the
507
+ * BE-generated title (`explanation`) + actions/status pill. */
508
+ variant?: ApprovalBlockVariant
509
+ /** Display name of the user who resolved the request; baked into the
510
+ * client variant's full-text status pill ("Approved by {name}"). */
511
+ resolvedByName?: string | null
494
512
  }
495
513
 
496
514
  // ========== Error Message Display Props ==========
@@ -171,6 +171,9 @@ export type ApprovalRequestSegment = {
171
171
  type: 'approval_request'
172
172
  data: ApprovalRequestData & { approvalType?: string }
173
173
  status?: ChatApprovalStatus
174
+ /** Display name of the user who resolved the request; baked into the client
175
+ * variant's full-text status pill ("Approved by {name}"). */
176
+ resolvedByName?: string | null
174
177
  onApprove?: (requestId?: string) => void | Promise<void>
175
178
  onReject?: (requestId?: string) => void | Promise<void>
176
179
  }
@@ -7,8 +7,8 @@ import type { AuthSsoProvider } from './sso-providers'
7
7
  import { SsoProviderButtons } from './sso-providers'
8
8
 
9
9
  export interface AcceptInvitationFormProps {
10
- /** Invited email — shown read-only (prefilled from the invitation link). */
11
- email: string
10
+ /** Invited email — shown read-only. The field is hidden when empty/absent. */
11
+ email?: string
12
12
  /** SSO providers offered to accept the invitation. */
13
13
  ssoProviders: AuthSsoProvider[]
14
14
  onSsoClick: (provider: AuthSsoProvider) => void
@@ -53,8 +53,8 @@ export function AcceptInvitationForm({
53
53
  <p className="text-h4 text-ods-text-secondary">{subtitle}</p>
54
54
  </div>
55
55
 
56
- {/* Invited email — read-only */}
57
- <Input type="email" label={emailLabel} value={email} disabled readOnly />
56
+ {/* Invited email — read-only; hidden when the backend didn't provide it */}
57
+ {email && <Input type="email" label={emailLabel} value={email} disabled />}
58
58
 
59
59
  {/* SSO providers */}
60
60
  <SsoProviderButtons