import type { IssueReviewAttention, IssueReviewAttentionPath, IssueStatus } from "@paperclipai/shared"; import { Activity, AlertTriangle, Bell, Clock, HelpCircle, LifeBuoy, ShieldCheck, UserCheck, Users, type LucideIcon, } from "lucide-react"; import { cn, relativeTime } from "../lib/utils"; import { StatusGlyph } from "./StatusGlyph"; import { StalledReviewActions } from "./StalledReviewActions"; /** Minimal shape the panel needs — the full `Issue` satisfies it. */ export interface ReviewPanelIssue { id: string; companyId: string; status: IssueStatus; reviewAttention?: IssueReviewAttention; } const PATH_ICON: Record = { execution_participant: Users, interaction: HelpCircle, approval: ShieldCheck, monitor: Clock, human_reviewer: UserCheck, active_run: Activity, queued_wake: Bell, recovery: LifeBuoy, }; /** * Persistent review panel pinned above the thread whenever an issue is * `in_review` (PAP-16080 §4.4). Driven by `issue.reviewAttention` (P2): * * - **covered** — names WHAT is being reviewed (each maintained path), WHO * decides it, and since when, plus what each outcome does. Keeps a stalled * review from ever being the *only* thing an operator sees, and surfaces the * responder so a covered review reads as "someone has this". * - **stalled** — the amber "nobody is reviewing this" notice with the three * escape actions (approve / request changes / send back), so an agent-owned * review can never become an invisible zombie (the PAP-14994 failure). * * Renders nothing when the issue is not in review, or when `reviewAttention` is * absent (older payloads) — the thread simply shows as it does today. */ export function IssueReviewPanel({ issue }: { issue: ReviewPanelIssue }) { const reviewAttention = issue.reviewAttention; if (issue.status !== "in_review" || !reviewAttention) return null; if (reviewAttention.state === "stalled") { return ; } if (reviewAttention.state === "covered") { return ; } return null; } function CoveredReviewPanel({ reviewAttention }: { reviewAttention: IssueReviewAttention }) { return (

In review

{reviewAttention.reason ?? "This issue has a maintained review path."}

    {reviewAttention.paths.map((path, index) => ( ))}

Approving marks this issue done. Requesting changes or sending it back returns it to the assignee.

); } function ReviewPathRow({ path }: { path: IssueReviewAttentionPath }) { const Icon = PATH_ICON[path.kind] ?? Activity; return (
  • {path.label} {path.responder && ( <> {path.responder} )} {path.since && ( <> {relativeTime(path.since)} )}
  • ); } function PathDot() { return ( · ); } function StalledReviewPanel({ issue, reason, }: { issue: ReviewPanelIssue; reason: string | null; }) { return (

    Nobody is reviewing this

    {reason ?? "No reviewer, interaction, approval, or monitor exists — the review has no owner."}

    ); }