diff --git a/packages/ui/src/components/exec-approval-item.tsx b/packages/ui/src/components/exec-approval-item.tsx new file mode 100644 index 00000000..4487e0f3 --- /dev/null +++ b/packages/ui/src/components/exec-approval-item.tsx @@ -0,0 +1,144 @@ +"use client" + +import { memo, useState, useEffect, useCallback } from "react" +import { HugeiconsIcon } from "@hugeicons/react" +import { + Tick01Icon, + TickDouble01Icon, + Cancel01Icon, + CommandLineIcon, +} from "@hugeicons/core-free-icons" +import { cn } from "@multica/ui/lib/utils" +import { Button } from "@multica/ui/components/ui/button" + +export interface ExecApprovalItemProps { + command: string + cwd?: string + riskLevel: "safe" | "needs-review" | "dangerous" + riskReasons: string[] + expiresAtMs: number + onDecision: (decision: "allow-once" | "allow-always" | "deny") => void +} + +function useCountdown(expiresAtMs: number): number { + const [remaining, setRemaining] = useState(() => + Math.max(0, Math.ceil((expiresAtMs - Date.now()) / 1000)), + ) + + useEffect(() => { + const id = setInterval(() => { + const next = Math.max(0, Math.ceil((expiresAtMs - Date.now()) / 1000)) + setRemaining(next) + if (next <= 0) clearInterval(id) + }, 1000) + return () => clearInterval(id) + }, [expiresAtMs]) + + return remaining +} + +export const ExecApprovalItem = memo(function ExecApprovalItem({ + command, + cwd, + riskLevel, + riskReasons, + expiresAtMs, + onDecision, +}: ExecApprovalItemProps) { + const remaining = useCountdown(expiresAtMs) + const [decided, setDecided] = useState(false) + + const handleDecision = useCallback( + (decision: "allow-once" | "allow-always" | "deny") => { + if (decided) return + setDecided(true) + onDecision(decision) + }, + [decided, onDecision], + ) + + const riskLabel = + riskLevel === "dangerous" + ? "Dangerous command" + : riskLevel === "needs-review" + ? "Needs review" + : "Command approval" + + return ( +
{reason}
+ ))} ++ {decided ? "Decision sent" : "Expired"} +
+ )} +