import type { ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts"; import { resolveThreadPullRequestChains, visibleThreadPullRequests, } from "@t3tools/shared/threadPullRequests"; import { GitPullRequestArrow, LayersIcon, LinkIcon, MoreHorizontalIcon, PlusIcon, } from "lucide-react"; import { useCallback, useMemo } from "react"; import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; import { useOpenPrLink } from "~/lib/openPullRequestLink"; import { cn } from "~/lib/utils"; import { useServerConfigs, useThreadShell } from "~/state/entities"; import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState"; import { threadEnvironment } from "~/state/threads"; import { useAtomCommand } from "~/state/use-atom-command"; import { formatRelativeTimeLabel } from "~/timestampFormat"; import { Button } from "../ui/button"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { ScrollArea } from "../ui/scroll-area"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { openLinkPullRequestDialog } from "./LinkPullRequestDialog"; import { pullRequestListLines, type PullRequestListLine } from "./pullRequestListLines"; import { PullRequestActorAvatar, PullRequestDiffStat, PullRequestApprovalGlyph, PullRequestStateGlyph, pullRequestChecksStatePresentation, } from "./pullRequestPresentation"; const SOURCE_LABELS: Record = { manual: "Linked by you", created: "Created from this thread", agent: "Linked by the agent", stack: "Found in the stack", "stack-dismissed": "Dismissed", }; function ChecksGlyph({ state, }: { state: NonNullable["checksState"] & string; }) { const presentation = pullRequestChecksStatePresentation(state); return ( }> {presentation.label} ); } function LinkRow({ line, threadRef, onUnlink, }: { line: PullRequestListLine; threadRef: ScopedThreadRef; onUnlink: (link: ThreadPullRequestLink) => void; }) { const openPrLink = useOpenPrLink(threadRef); const { link, depth, stack } = line; const snapshot = link.snapshot; return (
{depth > 0 ? : null} {snapshot === null ? ( ) : ( )} openPrLink(event, link.url, threadRef)} className="min-w-0 flex-1" > } > #{link.number} {SOURCE_LABELS[link.source]} · {formatRelativeTimeLabel(link.linkedAt)} {snapshot?.title ?? link.repository} {/* Match the full PR list: review verdict, checks, then diff counts. Each is absent rather than neutral when the host said nothing, so a row without them reads as unknown, not as fine. */} {snapshot?.state === "open" && (snapshot.reviewDecision === "approved" || snapshot.reviewDecision === "changes-requested") ? ( snapshot.reviewDecision === "approved" ? ( ) : ( Changes requested ) ) : null} {snapshot?.state === "open" && snapshot.mergeability === "conflicting" ? ( Conflicts ) : null} {snapshot?.checksState ? : null} {stack ? ( } > {stack.kind === "native" ? ( ) : ( )} {stack.size} {stack.kind === "native" ? `GitHub stack of ${stack.size}: merging a layer lands the ones below it.` : `${stack.size} pull requests chained by base branch.`} ) : null} {snapshot?.author ? ( {snapshot.author.login} ) : null} {snapshot !== null ? `${snapshot.headBranch} → ${snapshot.baseBranch}` : `${link.host}/${link.repository}`} {snapshot?.updatedAt ? ( {formatRelativeTimeLabel(snapshot.updatedAt)} ) : null} } /> void writeTextToClipboard(link.url, "link")}>Copy link openPrLink(event, link.url, threadRef)}>Open onUnlink(link)}> {link.source === "stack" ? "Dismiss from thread" : "Unlink from thread"}
); } export function ThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThreadRef }) { const configs = useServerConfigs(); if (configs.get(threadRef.environmentId)?.environment.capabilities.threadPullRequests !== true) { return ( ); } return ; } function EnabledThreadPullRequestsPanel({ threadRef }: { threadRef: ScopedThreadRef }) { const thread = useThreadShell(threadRef); const openLinkDialog = useCallback(() => openLinkPullRequestDialog(threadRef), [threadRef]); const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: true }); const links = useMemo(() => visibleThreadPullRequests(thread?.pullRequests ?? []), [thread]); const lines = useMemo(() => pullRequestListLines(resolveThreadPullRequestChains(links)), [links]); const handleUnlink = useCallback( (link: ThreadPullRequestLink) => { void unlink({ environmentId: threadRef.environmentId, input: { threadId: threadRef.threadId, host: link.host, repository: link.repository, number: link.number, }, }); }, [threadRef, unlink], ); const openCount = useMemo( () => links.filter((link) => link.snapshot === null || link.snapshot.state === "open").length, [links], ); const lastSynced = useMemo(() => { let latest: string | null = null; for (const link of links) { const at = link.snapshot?.syncedAt; if (at !== undefined && (latest === null || at > latest)) latest = at; } return latest; }, [links]); if (links.length === 0) { return (

No linked pull requests

Pull requests the agent opens from this thread land here. Link one yourself from a URL or a number.

); } return (
{lines.map((line) => ( ))}
); }