"use client"; import { useRef } from "react"; import { IconCircleCheckFilled, IconCircleXFilled, IconChecklist, IconLoader2, IconPointFilled, } from "@tabler/icons-react"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@kandev/ui/hover-card"; import { useTaskPR } from "@/hooks/domains/github/use-task-pr"; import { usePRFeedbackBackgroundSync } from "@/hooks/domains/github/use-pr-ci-popover"; import { PRCIPopover } from "@/components/github/pr-ci-popover"; import { isPRAwaitingReview, isPRReadyToMerge } from "@/components/github/pr-task-icon"; import type { TaskPR } from "@/lib/types/github"; const HOVER_OPEN_DELAY_MS = 150; const HOVER_CLOSE_DELAY_MS = 150; type ChipStatus = "passed" | "failed" | "in_progress" | "merged" | "closed" | "neutral"; function chipStatus(pr: TaskPR): ChipStatus { if (pr.state === "merged") return "merged"; if (pr.state === "closed") return "closed"; if (pr.review_state === "changes_requested" || pr.checks_state === "failure") return "failed"; // Pending checks / pending review must beat checks_state === "success" so a // PR with all checks green but reviewers still outstanding renders as // in-progress, not passed. Without this order, the chip flips to green the // moment CI finishes and ignores the human gate. isPRAwaitingReview also // covers approved PRs where branch protection requires more reviewers. if (pr.checks_state === "pending" || pr.review_state === "pending") return "in_progress"; // Mirror getPRStatusColor priority: ready-to-merge beats awaiting-review so // the chip and icon never disagree on a (theoretical) clean+approved+pending PR. if (isPRAwaitingReview(pr) && !isPRReadyToMerge(pr)) return "in_progress"; if (pr.checks_state === "success") return "passed"; return "neutral"; } /** * Compact CI indicator for the chat status bar — a "CI" prefix icon plus a * status glyph that mirrors the popover's bucket colors: * passed → green check * failed → red X * in progress → yellow spinner * merged → purple dot * neutral → muted dot * * Hovering opens the full PRCIPopover anchored to the top edge so the card * expands upward (the chip sits just above the chat input). * * Returns null when the task has no PR yet. */ export function PRStatusChip({ taskId }: { taskId: string | null }) { const { pr } = useTaskPR(taskId); // Subscribe at the chip level so the cache warms even when the // top-bar PR button isn't mounted (e.g. small viewport that hides it). usePRFeedbackBackgroundSync(pr); if (!pr) return null; return ; } function PRStatusChipInner({ pr }: { pr: TaskPR }) { const status = chipStatus(pr); const triggerRef = useRef(null); return ( { // Radix HoverCard treats the trigger as outside the content's // bounding box, so a click on the chip would auto-close the // popover. Filter out trigger clicks so clicking the chip is // a no-op while the popover stays open via hover. if (triggerRef.current && triggerRef.current.contains(e.target as Node)) { e.preventDefault(); } }} > ); } function ChipStatusGlyph({ status }: { status: ChipStatus }) { switch (status) { case "passed": return