From 83d4b5be2629cb1bb5f58d77bf2bbbbe476782c3 Mon Sep 17 00:00:00 2001 From: "rafael r. camargo" <66796237+rafaelrcamargo@users.noreply.github.com> Date: Tue, 25 Aug 2026 20:39:45 -0300 Subject: [PATCH 1/7] feat(localizations,shared,ui): add agent action approval flow --- packages/localizations/src/en-US.ts | 19 +++ packages/shared/src/types/clerk.ts | 14 ++ packages/shared/src/types/localization.ts | 18 +++ .../AgentActionApproval.tsx | 153 ++++++++++++++++++ .../AgentActionApproval/ParameterStamp.tsx | 52 ++++++ .../AgentActionApproval/TerminalCard.tsx | 25 +++ .../__tests__/AgentActionApproval.test.tsx | 70 ++++++++ .../AgentActionApproval/useCountdown.ts | 32 ++++ .../src/contexts/ClerkUIComponentsContext.tsx | 2 + packages/ui/src/elements/Divider.tsx | 22 +-- packages/ui/src/elements/contexts/index.tsx | 1 + packages/ui/src/internal/appearance.ts | 5 + packages/ui/src/internal/index.ts | 1 + packages/ui/src/lazyModules/components.ts | 7 + packages/ui/src/types.ts | 8 + 15 files changed, 420 insertions(+), 9 deletions(-) create mode 100644 packages/ui/src/components/AgentActionApproval/AgentActionApproval.tsx create mode 100644 packages/ui/src/components/AgentActionApproval/ParameterStamp.tsx create mode 100644 packages/ui/src/components/AgentActionApproval/TerminalCard.tsx create mode 100644 packages/ui/src/components/AgentActionApproval/__tests__/AgentActionApproval.test.tsx create mode 100644 packages/ui/src/components/AgentActionApproval/useCountdown.ts diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index aa0385251de..0aa86c73a94 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -978,6 +978,25 @@ export const enUS: LocalizationResource = { warning: 'Make sure that you trust {{applicationName}} ({{domainAction}}). You may be sharing sensitive data with this site or app.', }, + agentActionApproval: { + action__approve: 'Approve', + action__reject: 'Reject', + commentLabel: 'Comment (optional)', + commentPlaceholder: 'Add context for the requesting application', + expiresIn: 'Approval window closes in {{remaining}}', + requestedBy: 'Requested by {{agent}} · {{createdAt}}', + subtitle: 'Review the details before allowing this action to continue.', + terminal: { + approvedSubtitle: 'You can close this window, return to your agent, and retry the request.', + approvedTitle: 'Agent is now authorized to continue', + expiredSubtitle: + 'The approval window closed. No action was taken. You can close this window and return to your agent.', + expiredTitle: 'Approval window closed', + rejectedSubtitle: 'No action was taken. You can close this window and return to your agent.', + rejectedTitle: 'Action rejected', + }, + title: 'Approve agent action', + }, organizationList: { action__createOrganization: 'Create organization', action__invitationAccept: 'Join', diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 0f57591517d..6f645ffd289 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -2770,6 +2770,20 @@ export type OAuthConsentProps = { onDeny?: () => void; }; +/** + * Properties for the experimental agent action approval component. + * + * @interface + * + * @experimental + */ +export type AgentActionApprovalProps = { + /** + * Customization options to fully match the Clerk components to your own brand. These options serve as overrides and will be merged with the global `appearance` configuration (if one is provided). + */ + appearance?: ClerkAppearanceTheme; +}; + /** @deprecated Use OAuthConsentProps instead. */ export type __internal_OAuthConsentProps = OAuthConsentProps; diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 26b9452fc15..43a5e2c1ba0 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1374,6 +1374,24 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue<'applicationName'>; }; }; + agentActionApproval: { + title: LocalizationValue; + subtitle: LocalizationValue; + requestedBy: LocalizationValue<'agent' | 'createdAt'>; + commentLabel: LocalizationValue; + commentPlaceholder: LocalizationValue; + expiresIn: LocalizationValue<'remaining'>; + action__approve: LocalizationValue; + action__reject: LocalizationValue; + terminal: { + approvedTitle: LocalizationValue; + approvedSubtitle: LocalizationValue; + rejectedTitle: LocalizationValue; + rejectedSubtitle: LocalizationValue; + expiredTitle: LocalizationValue; + expiredSubtitle: LocalizationValue; + }; + }; unstable__errors: UnstableErrors; dates: { previous6Days: LocalizationValue<'date'>; diff --git a/packages/ui/src/components/AgentActionApproval/AgentActionApproval.tsx b/packages/ui/src/components/AgentActionApproval/AgentActionApproval.tsx new file mode 100644 index 00000000000..fcb06754e86 --- /dev/null +++ b/packages/ui/src/components/AgentActionApproval/AgentActionApproval.tsx @@ -0,0 +1,153 @@ +import { useMemo, useState } from 'react'; + +import { + Button, + Col, + Flow, + FormLabel, + Grid, + Heading, + localizationKeys, + Text, + useLocalizations, +} from '@/ui/customizables'; +import { Card } from '@/ui/elements/Card'; +import { withCardStateProvider } from '@/ui/elements/contexts'; +import { Divider } from '@/ui/elements/Divider'; +import { Header } from '@/ui/elements/Header'; +import { Textarea } from '@/ui/primitives'; + +import { ParameterStamp } from './ParameterStamp'; +import { TerminalCard, type TerminalStatus } from './TerminalCard'; +import { formatRemainingTime, useCountdown } from './useCountdown'; + +const mockAction = { + description: 'Refund the most recent charge after the customer reported a duplicate payment.', + operation: 'Create a refund', + parameters: [ + { key: 'refund_amount', label: 'Refund amount', value: '$250.00' }, + { key: 'customer_id', label: 'Customer', value: 'cus_agent_demo' }, + { key: 'payment_intent_id', label: 'Payment intent', value: 'pi_agent_demo' }, + { key: 'payment_method', label: 'Payment method', value: 'Visa ending in 4242' }, + ], + requestedBy: 'Codex', +}; + +function ApprovalScreen() { + const { t } = useLocalizations(); + const [comment, setComment] = useState(''); + const [decision, setDecision] = useState(null); + const timestamps = useMemo( + () => ({ + createdAt: Date.now() - 2 * 60 * 1_000, + expiresAt: Date.now() + 15 * 60 * 1_000, + }), + [], + ); + const { remaining, isExpired } = useCountdown(timestamps.expiresAt, decision !== null); + + if (decision) { + return ; + } + + if (isExpired) { + return ; + } + + return ( + + + + + + + + + + + + {mockAction.operation} + + ({ fontSize: theme.fontSizes.$sm })} + localizationKey={localizationKeys('agentActionApproval.requestedBy', { + agent: mockAction.requestedBy, + createdAt: formatTimestamp(timestamps.createdAt), + })} + /> + + {mockAction.description} + + + + + +