@flamingo-stack/openframe-frontend-core 0.0.390 → 0.0.391-snapshot.20260709132828

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 (141) hide show
  1. package/dist/{chunk-RAN67OX4.js → chunk-2KW2GOFW.js} +2394 -2203
  2. package/dist/chunk-2KW2GOFW.js.map +1 -0
  3. package/dist/{chunk-3DTVUZUS.cjs → chunk-2Q2PIMSX.cjs} +5 -5
  4. package/dist/{chunk-3DTVUZUS.cjs.map → chunk-2Q2PIMSX.cjs.map} +1 -1
  5. package/dist/{chunk-W4T6Z27I.cjs → chunk-2RCCOZHC.cjs} +15 -15
  6. package/dist/{chunk-W4T6Z27I.cjs.map → chunk-2RCCOZHC.cjs.map} +1 -1
  7. package/dist/{chunk-XR4SQ32H.cjs → chunk-3YBDFKBZ.cjs} +1953 -1762
  8. package/dist/chunk-3YBDFKBZ.cjs.map +1 -0
  9. package/dist/{chunk-PQY63H3D.js → chunk-45IP2W4W.js} +2 -2
  10. package/dist/{chunk-ZT56IO5B.js → chunk-4L4JG5GB.js} +2 -2
  11. package/dist/{chunk-DI55URRP.js → chunk-4ZCXV6GB.js} +4 -4
  12. package/dist/{chunk-PY376F37.cjs → chunk-5PF4BSCV.cjs} +12 -12
  13. package/dist/{chunk-PY376F37.cjs.map → chunk-5PF4BSCV.cjs.map} +1 -1
  14. package/dist/{chunk-ZPMRWBWF.js → chunk-AGZMMTRL.js} +4 -4
  15. package/dist/{chunk-NIBFE2DV.cjs → chunk-ANLFC67D.cjs} +37 -37
  16. package/dist/{chunk-NIBFE2DV.cjs.map → chunk-ANLFC67D.cjs.map} +1 -1
  17. package/dist/{chunk-IDDSEQBZ.js → chunk-AQUU74K4.js} +4 -4
  18. package/dist/{chunk-3BNQCRMN.js → chunk-CSHLDG7J.js} +2 -2
  19. package/dist/{chunk-L5HOLKHH.cjs → chunk-DHJVMIKE.cjs} +26 -26
  20. package/dist/{chunk-L5HOLKHH.cjs.map → chunk-DHJVMIKE.cjs.map} +1 -1
  21. package/dist/{chunk-HWH2ZTP3.cjs → chunk-FGZ23YLT.cjs} +62 -62
  22. package/dist/{chunk-HWH2ZTP3.cjs.map → chunk-FGZ23YLT.cjs.map} +1 -1
  23. package/dist/{chunk-EFY7ALPU.cjs → chunk-GCUAXBR6.cjs} +9 -9
  24. package/dist/{chunk-EFY7ALPU.cjs.map → chunk-GCUAXBR6.cjs.map} +1 -1
  25. package/dist/{chunk-LTGZ46D4.js → chunk-HXFHEKGD.js} +4 -4
  26. package/dist/{chunk-IBEJAPAU.js → chunk-J6QEZIRO.js} +2 -2
  27. package/dist/{chunk-XG4FQFMH.cjs → chunk-NYAZENBC.cjs} +7 -7
  28. package/dist/{chunk-XG4FQFMH.cjs.map → chunk-NYAZENBC.cjs.map} +1 -1
  29. package/dist/{chunk-DAMGTJ7E.js → chunk-QEN2T5AV.js} +2 -2
  30. package/dist/{chunk-MSKUU2C6.js → chunk-R2VRTPLY.js} +2 -2
  31. package/dist/{chunk-33IT4ZAY.js → chunk-SGKMMZBD.js} +2 -2
  32. package/dist/{chunk-TOBDMXND.cjs → chunk-TTL3ILIM.cjs} +31 -31
  33. package/dist/{chunk-TOBDMXND.cjs.map → chunk-TTL3ILIM.cjs.map} +1 -1
  34. package/dist/{chunk-RABSUY3W.cjs → chunk-UCXBVKSM.cjs} +26 -26
  35. package/dist/{chunk-RABSUY3W.cjs.map → chunk-UCXBVKSM.cjs.map} +1 -1
  36. package/dist/{chunk-EIH6WUAP.cjs → chunk-YV4UMWDP.cjs} +59 -59
  37. package/dist/{chunk-EIH6WUAP.cjs.map → chunk-YV4UMWDP.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/auth-shell.d.ts +1 -3
  63. package/dist/components/features/auth/auth-shell.d.ts.map +1 -1
  64. package/dist/components/features/auth/complete-account-form.d.ts +43 -0
  65. package/dist/components/features/auth/complete-account-form.d.ts.map +1 -0
  66. package/dist/components/features/auth/index.d.ts +1 -1
  67. package/dist/components/features/auth/index.d.ts.map +1 -1
  68. package/dist/components/features/index.cjs +2 -2
  69. package/dist/components/features/index.cjs.map +1 -1
  70. package/dist/components/features/index.js +3 -3
  71. package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
  72. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts +5 -2
  73. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -1
  74. package/dist/components/help-center-pages/index.cjs +20 -20
  75. package/dist/components/help-center-pages/index.js +11 -11
  76. package/dist/components/index.cjs +122 -120
  77. package/dist/components/index.cjs.map +1 -1
  78. package/dist/components/index.js +13 -11
  79. package/dist/components/index.js.map +1 -1
  80. package/dist/components/navigation/app-header.d.ts.map +1 -1
  81. package/dist/components/navigation/app-layout-context.d.ts +34 -0
  82. package/dist/components/navigation/app-layout-context.d.ts.map +1 -0
  83. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  84. package/dist/components/navigation/app-layout.d.ts +2 -21
  85. package/dist/components/navigation/app-layout.d.ts.map +1 -1
  86. package/dist/components/navigation/index.cjs +2 -2
  87. package/dist/components/navigation/index.js +1 -1
  88. package/dist/components/onboarding-guides/index.cjs +5 -5
  89. package/dist/components/onboarding-guides/index.js +4 -4
  90. package/dist/components/related-content/index.cjs +3 -3
  91. package/dist/components/related-content/index.js +2 -2
  92. package/dist/components/tickets/index.cjs +5 -5
  93. package/dist/components/tickets/index.js +4 -4
  94. package/dist/components/ui/index.cjs +4 -2
  95. package/dist/components/ui/index.cjs.map +1 -1
  96. package/dist/components/ui/index.js +3 -1
  97. package/dist/index.cjs +4 -2
  98. package/dist/index.cjs.map +1 -1
  99. package/dist/index.js +5 -3
  100. package/dist/utils/index.cjs.map +1 -1
  101. package/dist/utils/index.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/components/chat/approval-batch-message.tsx +104 -88
  104. package/src/components/chat/approval-request-message.tsx +57 -3
  105. package/src/components/chat/chat-message-enhanced.tsx +16 -2
  106. package/src/components/chat/chat-message-list.tsx +3 -0
  107. package/src/components/chat/types/chat.types.ts +17 -0
  108. package/src/components/chat/types/component.types.ts +21 -3
  109. package/src/components/chat/types/message.types.ts +3 -0
  110. package/src/components/features/auth/auth-shell.tsx +2 -11
  111. package/src/components/features/auth/complete-account-form.tsx +197 -0
  112. package/src/components/features/auth/index.ts +1 -1
  113. package/src/components/features/notifications/notification-drawer.tsx +72 -43
  114. package/src/components/features/time-tracker/time-tracker-header-button.tsx +46 -6
  115. package/src/components/navigation/app-header.tsx +13 -9
  116. package/src/components/navigation/app-layout-context.ts +46 -0
  117. package/src/components/navigation/app-layout-drawer.tsx +21 -11
  118. package/src/components/navigation/app-layout.tsx +17 -39
  119. package/src/stories/ApprovalBatchMessageClient.stories.tsx +20 -62
  120. package/src/stories/auth/CompleteAccount.stories.tsx +103 -0
  121. package/src/stories/auth/CreateOrganization.stories.tsx +0 -6
  122. package/src/stories/auth/Login.stories.tsx +0 -6
  123. package/src/stories/auth/PasswordReset.stories.tsx +0 -6
  124. package/src/styles/app-globals.css +30 -17
  125. package/dist/chunk-RAN67OX4.js.map +0 -1
  126. package/dist/chunk-XR4SQ32H.cjs.map +0 -1
  127. package/dist/components/features/auth/accept-invitation-form.d.ts +0 -23
  128. package/dist/components/features/auth/accept-invitation-form.d.ts.map +0 -1
  129. package/src/components/features/auth/accept-invitation-form.tsx +0 -71
  130. package/src/stories/auth/AcceptInvitation.stories.tsx +0 -54
  131. /package/dist/{chunk-PQY63H3D.js.map → chunk-45IP2W4W.js.map} +0 -0
  132. /package/dist/{chunk-ZT56IO5B.js.map → chunk-4L4JG5GB.js.map} +0 -0
  133. /package/dist/{chunk-DI55URRP.js.map → chunk-4ZCXV6GB.js.map} +0 -0
  134. /package/dist/{chunk-ZPMRWBWF.js.map → chunk-AGZMMTRL.js.map} +0 -0
  135. /package/dist/{chunk-IDDSEQBZ.js.map → chunk-AQUU74K4.js.map} +0 -0
  136. /package/dist/{chunk-3BNQCRMN.js.map → chunk-CSHLDG7J.js.map} +0 -0
  137. /package/dist/{chunk-LTGZ46D4.js.map → chunk-HXFHEKGD.js.map} +0 -0
  138. /package/dist/{chunk-IBEJAPAU.js.map → chunk-J6QEZIRO.js.map} +0 -0
  139. /package/dist/{chunk-DAMGTJ7E.js.map → chunk-QEN2T5AV.js.map} +0 -0
  140. /package/dist/{chunk-MSKUU2C6.js.map → chunk-R2VRTPLY.js.map} +0 -0
  141. /package/dist/{chunk-33IT4ZAY.js.map → chunk-SGKMMZBD.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.390",
3
+ "version": "0.0.391-snapshot.20260709132828",
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}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { forwardRef, memo, useMemo, useRef } from "react"
4
4
  import { cn } from "../../utils/cn"
5
+ import { isToday } from "../../utils/date-utils"
5
6
  import { SquareAvatar } from "../ui/square-avatar"
6
7
  import { ToolExecutionDisplay } from "./tool-execution-display"
7
8
  import { ApprovalRequestMessage } from "./approval-request-message"
@@ -35,6 +36,15 @@ const MENTION_MARKER_REGEX = /(^|[^\w@])@[a-z]+:([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/
35
36
  */
36
37
  const CARD_MARKER_REGEX = /\[card:\/\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)[\])]/g
37
38
 
39
+ /** Timestamp label: today's messages show time only ("2:47 PM"),
40
+ * older messages prepend the date ("05/05/2026 2:47 PM"). */
41
+ function formatMessageTimestamp(timestamp: Date): string {
42
+ const time = timestamp.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
43
+ if (isToday(timestamp)) return time
44
+ const date = timestamp.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' })
45
+ return `${date} ${time}`
46
+ }
47
+
38
48
  function normalizeContent(content: MessageContent): MessageSegment[] {
39
49
  if (typeof content === 'string') {
40
50
  return content ? [{ type: 'text', text: content }] : []
@@ -43,7 +53,7 @@ function normalizeContent(content: MessageContent): MessageSegment[] {
43
53
  }
44
54
 
45
55
  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) => {
56
+ ({ 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
57
  const isUser = role === 'user'
48
58
  const isError = role === 'error'
49
59
  const authorType = authorTypeProp ?? (isUser ? 'user' : assistantType === 'mingo' ? 'mingo' : 'fae')
@@ -469,7 +479,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
469
479
  </span>
470
480
  {timestamp && (
471
481
  <span className="font-sans text-heading-5 font-medium text-ods-text-secondary shrink-0 whitespace-nowrap">
472
- {timestamp.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
482
+ {formatMessageTimestamp(timestamp)}
473
483
  </span>
474
484
  )}
475
485
  </div>
@@ -546,9 +556,11 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
546
556
  key={index}
547
557
  data={segment.data}
548
558
  status={segment.status}
559
+ resolvedByName={segment.resolvedByName}
549
560
  onApprove={segment.onApprove}
550
561
  onReject={segment.onReject}
551
562
  assistantType={assistantType}
563
+ variant={approvalVariant}
552
564
  />
553
565
  )
554
566
  } else if (segment.type === 'approval_batch') {
@@ -561,6 +573,7 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
561
573
  onApprove={segment.onApprove}
562
574
  onReject={segment.onReject}
563
575
  assistantType={assistantType}
576
+ variant={approvalVariant}
564
577
  />
565
578
  )
566
579
  } else if (segment.type === 'error') {
@@ -620,6 +633,7 @@ const MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextPr
620
633
  prevProps.timestamp?.getTime() === nextProps.timestamp?.getTime() &&
621
634
  prevProps.showAvatar === nextProps.showAvatar &&
622
635
  prevProps.assistantType === nextProps.assistantType &&
636
+ prevProps.approvalVariant === nextProps.approvalVariant &&
623
637
  prevProps.authorType === nextProps.authorType &&
624
638
  prevProps.assistantIcon === nextProps.assistantIcon &&
625
639
  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
  }
@@ -12,8 +12,6 @@ export interface AuthShellProps {
12
12
  children: React.ReactNode
13
13
  /** Marketing panel. Defaults to <AuthBenefitsPanel />. */
14
14
  benefits?: React.ReactNode
15
- /** Tagline shown under the logo on mobile only. */
16
- mobileTagline?: React.ReactNode
17
15
  /** Pinned to the bottom-left of the form column on desktop only (e.g. "Back to Login"). */
18
16
  footer?: React.ReactNode
19
17
  className?: string
@@ -24,7 +22,7 @@ export interface AuthShellProps {
24
22
  * (form left, marketing right); tablet and mobile stack into a single centered
25
23
  * column (logo → tabs → form → benefits → powered-by). Content is top-aligned.
26
24
  */
27
- export function AuthShell({ tabs, children, benefits, mobileTagline, footer, className }: AuthShellProps) {
25
+ export function AuthShell({ tabs, children, benefits, footer, className }: AuthShellProps) {
28
26
  const benefitsNode = benefits ?? <AuthBenefitsPanel />
29
27
 
30
28
  return (
@@ -37,16 +35,9 @@ export function AuthShell({ tabs, children, benefits, mobileTagline, footer, cla
37
35
  footer && 'lg:min-h-full',
38
36
  )}
39
37
  >
40
- {/* Logo + tagline — narrow screens only (desktop shows the logo in the right column) */}
38
+ {/* Logo — narrow screens only (desktop shows the logo in the right column) */}
41
39
  <div className="flex w-full flex-col items-center gap-[var(--spacing-system-l)] lg:hidden">
42
40
  <OpenFrameWordmark />
43
- {/* Tagline is mobile-only; tablet & desktop hide it. Each line stays on one
44
- line and truncates with an ellipsis if it overflows (per the mockup). */}
45
- {mobileTagline && (
46
- <div className="w-full text-center text-h4 text-ods-text-primary [&>p]:truncate md:hidden">
47
- {mobileTagline}
48
- </div>
49
- )}
50
41
  </div>
51
42
 
52
43
  {/* Full width on mobile; fixed 320px from tablet up (matches desktop) */}