| 1 | import { RequestActionButton } from "./RequestActionButton"; |
| 2 | import type { |
| 3 | ProviderApprovalDecision, |
| 4 | ProviderApprovalOption, |
| 5 | RuntimeRequestId, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import { View } from "react-native"; |
| 8 | |
| 9 | import { AppText as Text } from "../../components/AppText"; |
| 10 | import type { PendingApproval } from "../../lib/threadActivity"; |
| 11 | |
| 12 | export interface PendingApprovalCardProps { |
| 13 | readonly canOperateThread: boolean; |
| 14 | readonly approval: PendingApproval; |
| 15 | readonly respondingApprovalId: RuntimeRequestId | null; |
| 16 | readonly onRespond: ( |
| 17 | requestId: RuntimeRequestId, |
| 18 | decision: ProviderApprovalDecision, |
| 19 | ) => Promise<unknown>; |
| 20 | } |
| 21 | |
| 22 | const DEFAULT_APPROVAL_OPTIONS: ReadonlyArray<ProviderApprovalOption> = [ |
| 23 | { decision: "accept", label: "Allow once" }, |
| 24 | { decision: "acceptForSession", label: "Allow session" }, |
| 25 | { decision: "decline", label: "Decline" }, |
| 26 | ]; |
| 27 | |
| 28 | export function PendingApprovalCard(props: PendingApprovalCardProps) { |
| 29 | const options: ReadonlyArray<ProviderApprovalOption> = |
| 30 | props.approval.options ?? DEFAULT_APPROVAL_OPTIONS; |
| 31 | const warning = options.find((option) => option.warning)?.warning; |
| 32 | // Opaque for the same reason as PendingUserInputCard: nothing blurs the feed |
| 33 | // behind this card, so a translucent surface bleeds messages through it. |
| 34 | const canRespond = props.approval.responseCapability === "live"; |
| 35 | const disabled = !canRespond || props.respondingApprovalId === props.approval.requestId; |
| 36 | return ( |
| 37 | <View className="gap-2.5 rounded-[20px] border border-border bg-card-alt p-4"> |
| 38 | <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-foreground-secondary"> |
| 39 | Approval needed |
| 40 | </Text> |
| 41 | <Text className="font-t3-bold text-lg text-foreground"> |
| 42 | {props.approval.appName ?? props.approval.requestKind} |
| 43 | </Text> |
| 44 | {props.approval.detail ? ( |
| 45 | <Text className="font-sans text-sm leading-normal text-foreground-secondary"> |
| 46 | {props.approval.detail} |
| 47 | </Text> |
| 48 | ) : null} |
| 49 | {!canRespond ? ( |
| 50 | <Text className="font-sans text-sm leading-5 text-foreground-muted"> |
| 51 | The provider process for this request is no longer available. Interrupt or restart the run |
| 52 | to continue. |
| 53 | </Text> |
| 54 | ) : null} |
| 55 | {warning ? ( |
| 56 | <Text className="font-sans text-xs leading-normal text-warning-foreground">{warning}</Text> |
| 57 | ) : null} |
| 58 | <View className="flex-row flex-wrap gap-2.5"> |
| 59 | {options.map((option) => ( |
| 60 | <RequestActionButton |
| 61 | key={option.decision} |
| 62 | label={option.label} |
| 63 | tone={ |
| 64 | option.decision === "accept" |
| 65 | ? "primary" |
| 66 | : option.decision === "decline" |
| 67 | ? "danger" |
| 68 | : "secondary" |
| 69 | } |
| 70 | disabled={disabled || !props.canOperateThread} |
| 71 | onPress={() => void props.onRespond(props.approval.requestId, option.decision)} |
| 72 | /> |
| 73 | ))} |
| 74 | </View> |
| 75 | {!props.canOperateThread ? ( |
| 76 | <Text className="font-sans text-xs text-foreground-muted"> |
| 77 | This connection cannot respond to approvals. |
| 78 | </Text> |
| 79 | ) : null} |
| 80 | </View> |
| 81 | ); |
| 82 | } |