1import { RequestActionButton } from "./RequestActionButton";
2import type {
3 ProviderApprovalDecision,
4 ProviderApprovalOption,
5 RuntimeRequestId,
6} from "@t3tools/contracts";
7import { View } from "react-native";
8
9import { AppText as Text } from "../../components/AppText";
10import type { PendingApproval } from "../../lib/threadActivity";
11
12export 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
22const DEFAULT_APPROVAL_OPTIONS: ReadonlyArray<ProviderApprovalOption> = [
23 { decision: "accept", label: "Allow once" },
24 { decision: "acceptForSession", label: "Allow session" },
25 { decision: "decline", label: "Decline" },
26];
27
28export 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}