"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 ;
case "failed":
return ;
case "in_progress":
return (
);
case "merged":
return ;
case "closed":
return ;
default:
return ;
}
}