Skip to content
Source
Sign inSign up
Open History to see the last commit

PendingApprovalCard.tsx

108 lines4.0 KB

1import { Button } from "../../components/ui/Button";
2import type {
3 ProviderApprovalDecision,
4 ProviderApprovalOption,
5 RuntimeRequestId,
6} from "@t3tools/contracts";
7import {
8 FULL_ACCESS_OVERRIDE_QUESTION,
9 fullAccessOverrideExplanation,
10} from "@t3tools/client-runtime/state/thread-requests";
11import { useState } from "react";
12import { Pressable, View } from "react-native";
13​
14import { AppText as Text } from "../../components/ui/AppText";
15import type { PendingApproval } from "../../lib/threadActivity";
16​
17export interface PendingApprovalCardProps {
18 readonly canOperateThread: boolean;
19 readonly approval: PendingApproval;
20 readonly respondingApprovalId: RuntimeRequestId | null;
21 readonly onRespond: (
22 requestId: RuntimeRequestId,
23 decision: ProviderApprovalDecision,
24 ) => Promise<unknown>;
26​
27const DEFAULT_APPROVAL_OPTIONS: ReadonlyArray<ProviderApprovalOption> = [
28 { decision: "accept", label: "Allow once" },
29 { decision: "acceptForSession", label: "Allow session" },
30 { decision: "decline", label: "Decline" },
31];
32​
33export function PendingApprovalCard(props: PendingApprovalCardProps) {
34 const options: ReadonlyArray<ProviderApprovalOption> =
35 props.approval.options ?? DEFAULT_APPROVAL_OPTIONS;
36 const warning = options.find((option) => option.warning)?.warning;
37 const canRespond = props.approval.responseCapability === "live";
38 const disabled = !canRespond || props.respondingApprovalId === props.approval.requestId;
39 const [explanationOpen, setExplanationOpen] = useState(false);
40 return (
41 <View className="gap-2.5 rounded-[20px] border border-border bg-card-alt p-4">
42 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-foreground-secondary">
43 Approval needed
44 </Text>
45 <Text className="font-t3-bold text-lg text-foreground">
46 {props.approval.appName ?? props.approval.requestKind}
47 </Text>
48 {props.approval.detail ? (
49 <Text className="font-sans text-sm leading-normal text-foreground-secondary">
50 {props.approval.detail}
51 </Text>
52 ) : null}
53 {props.approval.reason ? (
54 <Text className="font-sans text-xs leading-normal text-warning-foreground">
55 {props.approval.reason}
56 </Text>
57 ) : null}
58 {props.approval.fullAccessOverride ? (
59 <Pressable
60 accessibilityRole="button"
61 accessibilityState={{ expanded: explanationOpen }}
62 onPress={() => setExplanationOpen((open) => !open)}
63 >
64 <Text className="font-t3-bold text-xs text-foreground-secondary underline">
65 {FULL_ACCESS_OVERRIDE_QUESTION}
66 </Text>
67 {explanationOpen ? (
68 <Text className="mt-1 font-sans text-xs leading-normal text-foreground-muted">
69 {fullAccessOverrideExplanation(props.approval.fullAccessOverride)}
70 </Text>
71 ) : null}
72 </Pressable>
73 ) : null}
74 {!canRespond ? (
75 <Text className="font-sans text-sm leading-5 text-foreground-muted">
76 The provider process for this request is no longer available. Interrupt or restart the run
77 to continue.
78 </Text>
79 ) : null}
80 {warning ? (
81 <Text className="font-sans text-xs leading-normal text-warning-foreground">{warning}</Text>
82 ) : null}
83 <View className="gap-2">
84 {options.map((option) => (
85 <Button
86 key={option.decision}
87 label={option.label}
88 tone={
89 option.decision === "accept"
90 ? "primary"
91 : option.decision === "decline"
92 ? "danger"
93 : "secondary"
94 }
95 disabled={disabled || !props.canOperateThread}
96 onPress={() => void props.onRespond(props.approval.requestId, option.decision)}
97 fullWidth
98 />
99 ))}
100 </View>
101 {!props.canOperateThread ? (
102 <Text className="font-sans text-xs text-foreground-muted">
103 This connection cannot respond to approvals.
104 </Text>
105 ) : null}
106 </View>
107 );