import { Clock, EyeOff, ShieldX, UserCog } from "lucide-react"; import { describeIssueWriteDenial, type IssueWriteDenialCode, type IssueWriteDenialContext, type IssueWriteDenialTone, } from "@paperclipai/shared"; import { cn } from "../lib/utils"; /** * Renders actionable copy for a denied issue write (the open cross-task write design (failure UX)). * * Cross-issue writes are open by default, so the walls that remain are rare — * which is exactly why hitting one has to be self-explaining. A real incident * burned a full detour discovering a workaround behind an opaque 403; this notice always * states the boundary that fired, who *can* act, and the sanctioned path. * * Copy comes from the shared `describeIssueWriteDenial` contract, so the words a * human reads here match the API error body an agent reads. */ const TONE_ICONS: Record = { boundary: ShieldX, lock: Clock, cap: Clock, attribution: UserCog, }; const TONE_CLASSES: Record = { // A real authorization wall — the strongest signal. boundary: { surface: "border-red-300/70 bg-red-50/90 text-red-950 dark:border-red-500/40 dark:bg-red-500/10 dark:text-red-100", icon: "text-red-600 dark:text-red-300", action: "text-red-800 dark:text-red-200", }, // Run-lifecycle machinery that clears on its own; not a permission problem. lock: { surface: "border-amber-300/70 bg-amber-50/90 text-amber-950 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100", icon: "text-amber-600 dark:text-amber-300", action: "text-amber-800 dark:text-amber-200", }, // A rate backstop — the write was allowed, the budget was not. cap: { surface: "border-amber-300/70 bg-amber-50/90 text-amber-950 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100", icon: "text-amber-600 dark:text-amber-300", action: "text-amber-800 dark:text-amber-200", }, // Rejected attribution: the write is fine, the claimed identity is not. // Neutral surface, but the action lines stay near-foreground — on a muted // background `text-muted-foreground` drops below comfortable contrast. attribution: { surface: "border-border bg-muted/60 text-foreground dark:border-border dark:bg-muted/30", icon: "text-muted-foreground", action: "text-foreground/80", }, }; /** Codes whose primary failure is that the target is simply not visible. */ const VISIBILITY_CODES: ReadonlySet = new Set([ "issue_write_not_visible", ]); export function IssueWriteDenialNotice({ code, context, className, }: { code: IssueWriteDenialCode; context?: IssueWriteDenialContext; className?: string; }) { const copy = describeIssueWriteDenial(code, context ?? {}); const tone = TONE_CLASSES[copy.tone]; const Icon = VISIBILITY_CODES.has(code) ? EyeOff : TONE_ICONS[copy.tone]; return (
); }