import { useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { Link } from "react-router-dom"; import { AlertCircle, ArrowRight, ExternalLink, Inbox, KeyRound, Link2, Loader2, } from "lucide-react"; import type { CompanySecretProviderConfig, SecretProposalView } from "@paperclipai/shared"; import { secretsApi } from "../../api/secrets"; import { queryKeys } from "../../lib/queryKeys"; import { cn } from "../../lib/utils"; import { EmptyState } from "../../components/EmptyState"; import { SecretPathName } from "./SecretPathName"; import { AgentRefChip, DeliveryBadge, FingerprintChip, ProposalActions, ProposalJustification, ProposedBadge, bindingEnvKey, bindingSecretLabel, useProposalReview, } from "./proposal-review"; /** ISO expiry → "expires in 12d" / "expires in 5h" / "expired". */ function expiryLabel(expiresAt: string): { text: string; urgent: boolean } { const ms = new Date(expiresAt).getTime() - Date.now(); if (Number.isNaN(ms)) return { text: "no expiry", urgent: false }; if (ms <= 0) return { text: "expired", urgent: true }; const hours = Math.floor(ms / 3_600_000); if (hours < 24) return { text: `expires in ${hours}h`, urgent: true }; const days = Math.floor(hours / 24); return { text: `expires in ${days}d`, urgent: days <= 2 }; } function ProposalRow({ proposal, onApprove, onReject, disabled, }: { proposal: SecretProposalView; onApprove: (p: SecretProposalView) => void; onReject: (p: SecretProposalView) => void; disabled: boolean; }) { const isSecret = proposal.kind === "secret"; const expiry = expiryLabel(proposal.expiresAt); const secret = bindingSecretLabel(proposal); const envKey = bindingEnvKey(proposal); return (
{/* Headline */}
{isSecret ? ( ) : ( {proposal.target ? ( ) : ( agent )} {envKey || proposal.configPath} {secret.name} {secret.pending ? : null} )}
{/* Provenance meta */}
by {proposal.originIssue ? ( <> {proposal.originIssue.key} ) : null} {isSecret ? ( <> ) : null} {expiry.text}
{/* Justification (agent-authored — framed as an untrusted claim). */}
); } export function ProposalsTab({ companyId, providerConfigs, }: { companyId: string; providerConfigs: CompanySecretProviderConfig[]; }) { const proposalsQuery = useQuery({ queryKey: queryKeys.secrets.proposals(companyId, "pending"), queryFn: () => secretsApi.listProposals(companyId, "pending"), enabled: Boolean(companyId), }); const review = useProposalReview(companyId, providerConfigs); const proposals = proposalsQuery.data ?? EMPTY_PROPOSALS; // Secret proposals first, then bindings — an approver usually lands the // secret before (or alongside) the bindings that depend on it. const sorted = useMemo( () => [...proposals].sort((a, b) => { if (a.kind !== b.kind) return a.kind === "secret" ? -1 : 1; return a.createdAt.localeCompare(b.createdAt); }), [proposals], ); if (proposalsQuery.isError) { return (
Couldn’t load proposals. Try again.
); } if (proposalsQuery.isPending) { return (
Loading proposals…
); } if (sorted.length === 0) { return ( ); } return (

Agents propose credentials and access bindings; you approve or reject them here. Proposed values are never shown — only a fingerprint and length.

{sorted.map((proposal) => ( ))} {review.dialogs}
); } const EMPTY_PROPOSALS: SecretProposalView[] = [];