import type { GitResolvePullRequestResult } from "@t3tools/contracts"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useDebouncedValue } from "@tanstack/react-pacer"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { gitPreparePullRequestThreadMutationOptions, gitResolvePullRequestQueryOptions, } from "~/lib/gitReactQuery"; import { cn } from "~/lib/utils"; import { parsePullRequestReference } from "~/pullRequestReference"; import { Button } from "./ui/button"; import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPanel, DialogPopup, DialogTitle, } from "./ui/dialog"; import { Input } from "./ui/input"; import { Spinner } from "./ui/spinner"; interface PullRequestThreadDialogProps { open: boolean; cwd: string | null; initialReference: string | null; onOpenChange: (open: boolean) => void; onPrepared: (input: { branch: string; worktreePath: string | null }) => Promise | void; } export function PullRequestThreadDialog({ open, cwd, initialReference, onOpenChange, onPrepared, }: PullRequestThreadDialogProps) { const queryClient = useQueryClient(); const referenceInputRef = useRef(null); const [reference, setReference] = useState(initialReference ?? ""); const [referenceDirty, setReferenceDirty] = useState(false); const [preparingMode, setPreparingMode] = useState<"local" | "worktree" | null>(null); const [debouncedReference, referenceDebouncer] = useDebouncedValue( reference, { wait: 450 }, (debouncerState) => ({ isPending: debouncerState.isPending }), ); useEffect(() => { if (!open) return; setReference(initialReference ?? ""); setReferenceDirty(false); setPreparingMode(null); }, [initialReference, open]); useEffect(() => { if (!open) return; const frame = window.requestAnimationFrame(() => { referenceInputRef.current?.focus(); referenceInputRef.current?.select(); }); return () => { window.cancelAnimationFrame(frame); }; }, [open]); const parsedReference = parsePullRequestReference(reference); const parsedDebouncedReference = parsePullRequestReference(debouncedReference); const resolvePullRequestQuery = useQuery( gitResolvePullRequestQueryOptions({ cwd, reference: open ? parsedDebouncedReference : null, }), ); const cachedPullRequest = useMemo(() => { if (!cwd || !parsedReference) { return null; } const cached = queryClient.getQueryData([ "git", "pull-request", cwd, parsedReference, ]); return cached?.pullRequest ?? null; }, [cwd, parsedReference, queryClient]); const preparePullRequestThreadMutation = useMutation( gitPreparePullRequestThreadMutationOptions({ cwd, queryClient }), ); const liveResolvedPullRequest = parsedReference !== null && parsedReference === parsedDebouncedReference ? (resolvePullRequestQuery.data?.pullRequest ?? null) : null; const resolvedPullRequest = liveResolvedPullRequest ?? cachedPullRequest; const isResolving = open && parsedReference !== null && resolvedPullRequest === null && (referenceDebouncer.state.isPending || parsedReference !== parsedDebouncedReference || resolvePullRequestQuery.isPending || resolvePullRequestQuery.isFetching); const statusTone = useMemo(() => { switch (resolvedPullRequest?.state) { case "merged": return "text-violet-600 dark:text-violet-300/90"; case "closed": return "text-zinc-500 dark:text-zinc-400/80"; case "open": return "text-emerald-600 dark:text-emerald-300/90"; default: return "text-muted-foreground"; } }, [resolvedPullRequest?.state]); const handleConfirm = useCallback( async (mode: "local" | "worktree") => { if (!parsedReference) { setReferenceDirty(true); return; } if (!parsedReference || !resolvedPullRequest || !cwd) { return; } setPreparingMode(mode); try { const result = await preparePullRequestThreadMutation.mutateAsync({ reference: parsedReference, mode, }); await onPrepared({ branch: result.branch, worktreePath: result.worktreePath, }); onOpenChange(false); } finally { setPreparingMode(null); } }, [ cwd, onOpenChange, onPrepared, parsedReference, preparePullRequestThreadMutation, resolvedPullRequest, ], ); const validationMessage = !referenceDirty ? null : reference.trim().length === 0 ? "Paste a GitHub pull request URL or enter 123 / #123." : parsedReference === null ? "Use a GitHub pull request URL, 123, or #123." : null; const errorMessage = validationMessage ?? (resolvedPullRequest === null && resolvePullRequestQuery.isError ? resolvePullRequestQuery.error instanceof Error ? resolvePullRequestQuery.error.message : "Failed to resolve pull request." : preparePullRequestThreadMutation.error instanceof Error ? preparePullRequestThreadMutation.error.message : preparePullRequestThreadMutation.error ? "Failed to prepare pull request thread." : null); return ( { if (!preparePullRequestThreadMutation.isPending) { onOpenChange(nextOpen); } }} > Checkout Pull Request Resolve a GitHub pull request, then create the draft thread in the main repo or in a dedicated worktree. {resolvedPullRequest ? (

{resolvedPullRequest.title}

#{resolvedPullRequest.number} ยท {resolvedPullRequest.headBranch} to{" "} {resolvedPullRequest.baseBranch}

{resolvedPullRequest.state}
) : null} {isResolving ? (
Resolving pull request...
) : null} {errorMessage ?

{errorMessage}

: null}
); }