import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { dequal } from "dequal"; import { ChevronLeftIcon, ChevronRightIcon, ShieldCheckIcon } from "lucide-react"; import { OpenGeniApiError, toolReviewAction, toolReviewFields, toolReviewDetails, type ToolActionReview, } from "@opengeni/sdk"; import type { PendingApproval } from "../approvals"; import { cn } from "../lib/cn"; import { useErrorMessage } from "../lib/error-message"; import { toolDisplayName } from "../timeline/tool-display-name"; import { builtInToolStep } from "../timeline/tool-steps"; import { ToolActionReviewCard, ToolActionReviewDetails, ToolReviewFrame, DeclineButton, UNRECOVERABLE_TEXT, type ToolReviewDetailsLoader, } from "./tool-action-review"; import { useOpenGeniText, type OpenGeniText } from "../messages/runtime"; export type ApprovalSurfaceMessages = { title: string; description: string; approve: string; reject: string; approving: string; rejecting: string; formatToolName: (name: string) => string; showDetails?: string | undefined; hideDetails?: string | undefined; }; export const defaultApprovalSurfaceMessages = (t: OpenGeniText): ApprovalSurfaceMessages => ({ title: t("approvalSurface.approvalNeeded"), description: t("approvalSurface.reviewTheActionBeforeIt"), approve: t("approvalSurface.approveAction"), reject: t("approvalSurface.decline"), approving: t("approvalSurface.approving"), rejecting: t("approvalSurface.declining"), formatToolName: (name) => name.includes("__") || /^[a-f0-9]{64}$/.test(name) || builtInToolStep(name) ? toolDisplayName(t, name, undefined, "running") : humanToolName(name.replaceAll("_", " ").replaceAll(".", " › ")), showDetails: t("approvalSurface.viewFullDetails"), hideDetails: t("approvalSurface.backToReview"), }); /** "batch modify messages" → "Modify messages": how a call is grouped is not what it does. */ function humanToolName(name: string): string { const phrase = name.replace(/^batch\s+/i, "").trim(); return phrase ? phrase.charAt(0).toUpperCase() + phrase.slice(1) : name; } export type ApprovalSurfaceProps = { approvals: PendingApproval[]; onApprove: (approval: PendingApproval) => void | Promise; onReject: (approval: PendingApproval) => void | Promise; responding?: boolean | undefined; error?: string | Error | null | undefined; messages?: Partial | undefined; renderApproval?: ((approval: PendingApproval) => ReactNode) | undefined; /** Authenticated server facts; hosts need not import any web-app code. */ loadReview?: ((approval: PendingApproval) => Promise) | undefined; loadDetails?: ToolReviewDetailsLoader | undefined; onViewDetails?: ((review: ToolActionReview, path: string) => void) | undefined; selectedApprovalId?: string | null | undefined; onSelectedApprovalChange?: ((id: string) => void) | undefined; className?: string | undefined; }; export type ApprovalField = { key: string; label: string; value: string }; /** @deprecated Prefer the versioned server review; retained for small custom renderers. */ export function approvalArgumentFields(value: unknown): ApprovalField[] | null { const projected = toolReviewFields(value); return projected.fields.length ? projected.fields.map((field) => ({ key: field.path.slice(1), label: field.label, value: field.preview, })) : null; } /** Hosts provide decisions; this surface never mutates the approved payload. */ export function ApprovalSurface({ approvals, onApprove, onReject, responding, error, messages: overrides, renderApproval, loadReview, loadDetails, onViewDetails, selectedApprovalId, onSelectedApprovalChange, className, }: ApprovalSurfaceProps) { const t = useOpenGeniText(); const messages = { ...defaultApprovalSurfaceMessages(t), ...overrides }; const formatError = useErrorMessage(); const [pending, setPending] = useState<{ id: string; decision: "approve" | "reject"; } | null>(null); const pendingRef = useRef(null); const [decisionError, setDecisionError] = useState<{ cause: unknown } | null>(null); const [localSelectedId, setLocalSelectedId] = useState(null); const selectedId = selectedApprovalId === undefined ? localSelectedId : selectedApprovalId; const setSelectedId = (id: string) => { setLocalSelectedId(id); onSelectedApprovalChange?.(id); }; const surface = useRef(null); useEffect(() => { if (pending && !approvals.some((approval) => approval.id === pending.id)) { pendingRef.current = null; setPending(null); requestAnimationFrame(() => surface.current?.querySelector("h3")?.focus()); } }, [approvals, pending]); if (!approvals.length) return null; const selectedIndex = Math.max( 0, approvals.findIndex((approval) => approval.id === selectedId), ); const selected = approvals[selectedIndex]!; const decide = async (approval: PendingApproval, decision: "approve" | "reject") => { if (responding || pendingRef.current) return; pendingRef.current = approval.id; setPending({ id: approval.id, decision }); setDecisionError(null); try { await (decision === "approve" ? onApprove(approval) : onReject(approval)); } catch (cause) { if (pendingRef.current === approval.id) { pendingRef.current = null; setPending(null); setDecisionError({ cause }); } } }; const errorText = decisionError ? formatError(decisionError.cause) : error instanceof Error ? formatError(error) : error; return (
{approvals.length > 1 && ( )}
{[selected].map((approval) => ( void decide(approval, "approve")} onReject={() => void decide(approval, "reject")} /> ))}
{errorText && (

{errorText}

)}
); } function ApprovalRequest({ approval, loadReview, loadDetails, messages, custom, onViewDetails, ...actions }: { approval: PendingApproval; loadReview: ApprovalSurfaceProps["loadReview"]; messages: ApprovalSurfaceMessages; loadDetails: ToolReviewDetailsLoader | undefined; custom: ReactNode; onViewDetails: ApprovalSurfaceProps["onViewDetails"]; submitting: "approve" | "reject" | null; disabled: boolean; onApprove: () => void; onReject: () => void; }) { const t = useOpenGeniText(); const [retry, setRetry] = useState(0); // Event projection recreates the same approval on unrelated timeline updates. // Keep one request for equal facts, but never reuse it across changed facts, // an explicit retry, or a loader change (the host's client/access boundary). const requestRef = useRef({ approval, loadReview, retry }); if ( requestRef.current.loadReview !== loadReview || requestRef.current.retry !== retry || !dequal(requestRef.current.approval, approval) ) { requestRef.current = { approval, loadReview, retry }; } const request = requestRef.current; const [result, setResult] = useState<{ request: typeof request; review?: ToolActionReview; failed?: boolean; legacy?: boolean; denied?: boolean; } | null>(null); // Hide stale facts during render, before passive effect cleanup can run. const current = result?.request === request ? result : null; const loaded = current?.review; const failed = current?.failed; const legacy = current?.legacy; const denied = current?.denied; const [detailPath, setDetailPath] = useState(null); useEffect(() => { let active = true; if (request.loadReview) void request.loadReview(request.approval).then( (review) => { if (active) { setResult( review.id !== request.approval.id ? { request, failed: true } : { request, review }, ); } }, (error) => { if (active) { // 404: no saved review (older or non-connector actions); use the // approval's own arguments. 401/403: this person cannot review it. if (error instanceof OpenGeniApiError && error.status === 404) setResult({ request, legacy: true }); else if ( error instanceof OpenGeniApiError && (error.status === 401 || error.status === 403) ) setResult({ request, failed: true, denied: true }); else setResult({ request, failed: true }); } }, ); return () => { active = false; }; }, [request]); const stableApproval = request.approval; const { formatToolName, approve: approveLabel } = messages; const fallback = useMemo( () => ({ version: 1, id: stableApproval.id, actionDigest: "", revision: "legacy", status: "pending", ...toolReviewAction(stableApproval.name, stableApproval.arguments, { kind: "generic", title: stableApproval.display?.title ?? formatToolName(stableApproval.display?.toolName ?? stableApproval.name), }), ...(stableApproval.display?.accountLabel ? { accountLabel: stableApproval.display.accountLabel } : {}), ...toolReviewFields(stableApproval.arguments), reason: "", createdAt: "", updatedAt: "", approveLabel, availableActions: stableApproval.arguments === undefined ? [] : ["approve", "reject"], detailsAvailable: stableApproval.arguments !== undefined, }), [stableApproval, formatToolName, approveLabel], ); const loadLegacyDetails = useCallback( async (value, path, offset) => ({ version: 1, id: value.id, actionDigest: value.actionDigest, ...toolReviewDetails(stableApproval.arguments, undefined, path, offset), }), [stableApproval], ); if (loadReview && !loaded && !failed && !legacy) return (