import { useCallback, useMemo, useState } from "react"; import { GitPullRequest, ExternalLink, RefreshCw, Plus, MessageSquare, CircleDot, XCircle, GitMerge } from "lucide-react"; import { getErrorMessage } from "@fusion/core"; import { refreshPrStatus, type PrCheckStatus, type PrInfo, type PrRefreshResponse } from "../api"; import { usePrChecksStream } from "../hooks/usePrChecksStream"; import { PrChecksList } from "./PrChecksList"; import type { ToastType } from "../hooks/useToast"; import "./PrPanel.css"; interface PrPanelProps { taskId: string; projectId?: string; prInfo?: PrInfo; automationStatus?: string | null; autoMerge?: boolean; isManualPrFlow?: boolean; prAuthAvailable: boolean; onPrUpdated: (prInfo: PrInfo) => void; onRequestCreatePr?: () => void; addToast: (message: string, type?: ToastType) => void; } const STATUS_ICONS: Record = { open: , closed: , merged: , }; type PrCheckState = PrCheckStatus["state"]; const PASSING_STATES = new Set(["success", "neutral", "skipped"]); const FAILING_STATES = new Set(["failure", "error", "cancelled", "timed_out", "action_required", "startup_failure"]); const PENDING_STATES = new Set(["pending", "stale"]); function getReviewTone(reviewDecision: PrRefreshResponse["reviewDecision"]): "success" | "error" | "warning" | "muted" { if (reviewDecision === "APPROVED") return "success"; if (reviewDecision === "CHANGES_REQUESTED") return "error"; if (reviewDecision === "REVIEW_REQUIRED") return "warning"; return "muted"; } export function PrPanel({ taskId, projectId, prInfo, automationStatus, autoMerge = false, isManualPrFlow = false, prAuthAvailable, onPrUpdated, onRequestCreatePr, addToast, }: PrPanelProps) { const [isRefreshing, setIsRefreshing] = useState(false); const [refreshState, setRefreshState] = useState(null); const handleRefresh = useCallback(async () => { if (!prInfo) return; setIsRefreshing(true); try { const updated = await refreshPrStatus(taskId, projectId); setRefreshState(updated); onPrUpdated(updated.prInfo); addToast("PR status refreshed", "success"); } catch (err) { addToast(getErrorMessage(err) || "Failed to refresh PR", "error"); } finally { setIsRefreshing(false); } }, [taskId, projectId, prInfo, onPrUpdated, addToast]); if (!prInfo) { if (automationStatus === "creating-pr") { return (

Pull Request

fn is creating a pull request automatically for this task.
); } if (autoMerge) { return (

Pull Request

Auto-merge will handle this task automatically.
); } const createDisabled = !prAuthAvailable || !onRequestCreatePr; return (

Pull Request

{isManualPrFlow &&
Use the footer action to run PR-first completion for this task.
} {(!prAuthAvailable || !onRequestCreatePr) && (
Run gh auth login to enable PR creation.
)}
); } const statusIcon = STATUS_ICONS[prInfo.status] ?? ; const blockingReasons = refreshState?.blockingReasons ?? []; const checks = refreshState?.checks; const reviewDecision = refreshState?.reviewDecision ?? null; const checkSummary = useMemo(() => { if (!checks) return "unknown" as const; if (checks.some((check) => FAILING_STATES.has(check.state))) return "failure" as const; if (checks.some((check) => PENDING_STATES.has(check.state))) return "pending" as const; if (checks.some((check) => PASSING_STATES.has(check.state))) return "success" as const; return "unknown" as const; }, [checks]); const streamChecks = usePrChecksStream({ taskId, projectId, prNumber: prInfo.number, enabled: prInfo.status !== "merged" && prInfo.status !== "closed", initialChecks: checks ?? [], initialRollup: checkSummary, initialLastCheckedAt: prInfo.lastCheckedAt, }); return (

Pull Request

{statusIcon} {prInfo.status} #{prInfo.number}
{prInfo.title}
{prInfo.headBranch} {prInfo.baseBranch}
{prInfo.status !== "merged" && prInfo.status !== "closed" ? ( { void streamChecks.refresh(); }} /> ) : null}
Review
{reviewDecision ? ( {reviewDecision} ) : ( No reviews yet )}
{automationStatus === "merging-pr" &&
fn is merging this pull request automatically.
} {automationStatus === "awaiting-pr-checks" && (
{blockingReasons.length > 0 ? `Waiting for: ${blockingReasons.join("; ")}` : "Waiting for required checks or review feedback before auto-merge."}
)} {prInfo.status === "merged" && (
This PR is merged. fn will finish local cleanup and move the task to Done.
)}
{prInfo.commentCount} {prInfo.lastCommentAt ? Last: {new Date(prInfo.lastCommentAt).toLocaleString()} : null} View on GitHub
); }