"use client"; import { Check, ChevronRight } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { createContext, type ReactNode, useContext, useLayoutEffect, useRef, useState, } from "react"; import { EASE_OUT, SPRING_LAYOUT } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type InboxRisk = "low" | "medium" | "high"; export type InboxStatus = "pending" | "approved" | "denied" | "expired"; export type AgentInboxVariant = "card" | "quiet"; const AgentInboxVariantContext = createContext("card"); interface InboxActionButtonProps { variant?: "ghost" | "primary"; onClick?: () => void; children?: ReactNode; } /** Self-contained ghost/primary button for `InboxItem`'s approve/deny row — deliberately not imported from `agent-thread` so this block distributes independently. */ function InboxActionButton({ variant = "ghost", onClick, children }: InboxActionButtonProps) { return ( ); } interface InboxCountBadgeProps { count: number; } /** Small pill in `AgentInbox`'s header — the digits crossfade with a short y-shift on change, matching `ThreadBranchSwitcher`'s readout. */ function InboxCountBadge({ count }: InboxCountBadgeProps) { const reduce = useReducedMotion() ?? false; return ( {count} ); } export interface AgentInboxProps { /** Card keeps the approval-queue shell; quiet becomes a flat semantic list. */ variant?: AgentInboxVariant; title?: ReactNode; /** Pending-count badge shown next to the title; omit to hide it entirely. */ count?: number; /** Trailing slot for the caller's own controls, e.g. a "Clear" button. */ action?: ReactNode; className?: string; /** `InboxItem`s (or any other row, e.g. a standalone `ActionReceipt`) — hairline-separated top to bottom. */ children?: ReactNode; } /** * Approval-queue container — a hairline-ringed card (the same CSS-variable * box-shadow technique as `ThreadCard`) with a header row (title, optional * count badge, trailing action) above a stack of rows. Rows are rendered * directly as `children` and separated by a hairline via a descendant * selector, so any row shape (an `InboxItem`, a bare `ActionReceipt`) drops in * cleanly without the container needing to know its type. */ export function AgentInbox({ variant = "card", title, count, action, className, children, }: AgentInboxProps) { const quiet = variant === "quiet"; return (
{!quiet || title || count !== undefined || action ? (
{title ? {title} : null} {count !== undefined ? : null} {action ?
{action}
: null}
) : null}
{children}
); } export interface InboxRiskBadgeProps { risk: InboxRisk; className?: string; } /** Uppercase risk pill — low is neutral, medium is amber, high is red, matching the library's warning/danger hues. */ export function InboxRiskBadge({ risk, className }: InboxRiskBadgeProps) { return ( {risk} ); } export interface InboxItemProps { /** Matches the parent AgentInbox variant; quiet renders a compact flat row. */ variant?: AgentInboxVariant; /** 16px leading icon, e.g. ``. */ icon?: ReactNode; /** Requesting agent, e.g. "deploy-agent". */ source?: ReactNode; title: ReactNode; description?: ReactNode; risk?: InboxRisk; /** Trailing countdown readout, e.g. "expires in 2h". */ expires?: ReactNode; status?: InboxStatus; onApprove?: () => void; onDeny?: () => void; approveLabel?: ReactNode; denyLabel?: ReactNode; /** Replaces the approve/deny row once `status` is no longer "pending". */ resolution?: ReactNode; className?: string; /** Optional detail region (e.g. a command preview or an `ActionReceipt`) revealed by a "Details" toggle. */ children?: ReactNode; } /** * One queued approval request. While `pending`, a ghost Deny and a primary * Approve button sit at the trailing edge; once resolved, `resolution` * replaces them and the rest of the row's content dims to `opacity-70` (the * resolution line stays at full opacity). The optional `children` region is * measured with a `ResizeObserver` and tweened open/closed (`EASE_OUT`, * 0.25s), shown instantly under `useReducedMotion()` — the same idiom as * `ThreadCollapse`. The root is a `motion.div` with `layout` so the height * change on resolution settles smoothly instead of snapping. */ export function InboxItem({ variant: variantProp, icon, source, title, description, risk, expires, status = "pending", onApprove, onDeny, approveLabel = "Approve", denyLabel = "Deny", resolution, className, children, }: InboxItemProps) { const reduce = useReducedMotion() ?? false; const inheritedVariant = useContext(AgentInboxVariantContext); const variant = variantProp ?? inheritedVariant; const [detailsOpen, setDetailsOpen] = useState(false); const contentRef = useRef(null); const [contentHeight, setContentHeight] = useState(0); const pending = status === "pending"; const hasDetails = children !== undefined && children !== null; useLayoutEffect(() => { const node = contentRef.current; if (!node) return; const update = () => setContentHeight(node.offsetHeight); update(); const observer = new ResizeObserver(update); observer.observe(node); return () => observer.disconnect(); }, []); if (variant === "quiet") { return (
{icon ? ( {icon} ) : null} {title}
{source ?
{source}
: null} {description ? (
{description}
) : null}
{risk ? : null} {expires ? ( {expires} ) : null} {pending ? ( <> {denyLabel} {approveLabel} ) : resolution !== undefined ? ( {resolution} ) : status === "approved" ? ( ) : null}
{hasDetails ? (
{detailsOpen ?
{children}
: null}
) : null}
); } return (
{icon ? ( {icon} ) : null} {source ? {source} : null} {risk ? : null} {expires ? ( {expires} ) : null}
{title}
{description ?
{description}
: null} {hasDetails ? ( <>
{children}
) : null}
{pending ? (
{denyLabel} {approveLabel}
) : resolution !== undefined ? (
{resolution}
) : null}
); } export interface ActionReceiptProps { /** 16px icon, defaults to a `Check` in the library's success green. */ icon?: ReactNode; title: ReactNode; /** Sub-line describing what was acted on, e.g. "db: production/users, sessions". */ scope?: ReactNode; added?: number; removed?: number; timestamp?: ReactNode; onUndo?: () => void; undoLabel?: ReactNode; className?: string; /** Optional extra content, e.g. a list of affected file rows. */ children?: ReactNode; } /** * Operation receipt — the record of a completed action, usable standalone or * dropped into an `InboxItem`'s detail region (e.g. a "planned changes" * preview before approval, or a completion summary after). A hairline-ringed * surface (the `ThreadCard` box-shadow technique) rather than `AgentInbox`'s * full card chrome, since it's meant to nest inside other rows. */ export function ActionReceipt({ icon, title, scope, added, removed, timestamp, onUndo, undoLabel = "Undo", className, children, }: ActionReceiptProps) { return (
{icon ?? } {title} {timestamp ? ( {timestamp} ) : null}
{scope ?
{scope}
: null} {added !== undefined || removed !== undefined ? (
{added !== undefined ? ( +{added} ) : null} {removed !== undefined ? ( −{removed} ) : null}
) : null} {children} {onUndo ? (
) : null}
); }