import { useState, useEffect, useRef } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { GitPullRequest, Loader2, ExternalLink } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { toast } from "sonner"; interface WorktreeInfo { path: string; branch: string; isMain: boolean; hasChanges?: boolean; changedFilesCount?: number; } interface CreatePRDialogProps { open: boolean; onOpenChange: (open: boolean) => void; worktree: WorktreeInfo | null; projectPath: string | null; onCreated: (prUrl?: string) => void; } export function CreatePRDialog({ open, onOpenChange, worktree, projectPath, onCreated, }: CreatePRDialogProps) { const [title, setTitle] = useState(""); const [body, setBody] = useState(""); const [baseBranch, setBaseBranch] = useState("main"); const [commitMessage, setCommitMessage] = useState(""); const [isDraft, setIsDraft] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [prUrl, setPrUrl] = useState(null); const [browserUrl, setBrowserUrl] = useState(null); const [showBrowserFallback, setShowBrowserFallback] = useState(false); // Track whether an operation completed that warrants a refresh const operationCompletedRef = useRef(false); // Reset state when dialog opens or worktree changes useEffect(() => { if (open) { // Reset form fields setTitle(""); setBody(""); setCommitMessage(""); setBaseBranch("main"); setIsDraft(false); setError(null); // Also reset result states when opening for a new worktree // This prevents showing stale PR URLs from previous worktrees setPrUrl(null); setBrowserUrl(null); setShowBrowserFallback(false); // Reset operation tracking operationCompletedRef.current = false; } else { // Reset everything when dialog closes setTitle(""); setBody(""); setCommitMessage(""); setBaseBranch("main"); setIsDraft(false); setError(null); setPrUrl(null); setBrowserUrl(null); setShowBrowserFallback(false); operationCompletedRef.current = false; } }, [open, worktree?.path]); const handleCreate = async () => { if (!worktree) return; setIsLoading(true); setError(null); try { const api = getElectronAPI(); if (!api?.worktree?.createPR) { setError("Worktree API not available"); return; } const result = await api.worktree.createPR(worktree.path, { projectPath: projectPath || undefined, commitMessage: commitMessage || undefined, prTitle: title || worktree.branch, prBody: body || `Changes from branch ${worktree.branch}`, baseBranch, draft: isDraft, }); if (result.success && result.result) { if (result.result.prCreated && result.result.prUrl) { setPrUrl(result.result.prUrl); // Mark operation as completed for refresh on close operationCompletedRef.current = true; // Show different message based on whether PR already existed if (result.result.prAlreadyExisted) { toast.success("Pull request found!", { description: `PR already exists for ${result.result.branch}`, action: { label: "View PR", onClick: () => window.open(result.result!.prUrl!, "_blank"), }, }); } else { toast.success("Pull request created!", { description: `PR created from ${result.result.branch}`, action: { label: "View PR", onClick: () => window.open(result.result!.prUrl!, "_blank"), }, }); } // Don't call onCreated() here - keep dialog open to show success message // onCreated() will be called when user closes the dialog } else { // Branch was pushed successfully const prError = result.result.prError; const hasBrowserUrl = !!result.result.browserUrl; // Check if we should show browser fallback if (!result.result.prCreated && hasBrowserUrl) { // If gh CLI is not available, show browser fallback UI if (prError === "gh_cli_not_available" || !result.result.ghCliAvailable) { setBrowserUrl(result.result.browserUrl ?? null); setShowBrowserFallback(true); // Mark operation as completed - branch was pushed successfully operationCompletedRef.current = true; toast.success("Branch pushed", { description: result.result.committed ? `Commit ${result.result.commitHash} pushed to ${result.result.branch}` : `Branch ${result.result.branch} pushed`, }); // Don't call onCreated() here - we want to keep the dialog open to show the browser URL setIsLoading(false); return; // Don't close dialog, show browser fallback UI } // gh CLI is available but failed - show error with browser option if (prError) { // Parse common gh CLI errors for better messages let errorMessage = prError; if (prError.includes("No commits between")) { errorMessage = "No new commits to create PR. Make sure your branch has changes compared to the base branch."; } else if (prError.includes("already exists")) { errorMessage = "A pull request already exists for this branch."; } else if (prError.includes("not logged in") || prError.includes("auth")) { errorMessage = "GitHub CLI not authenticated. Run 'gh auth login' in terminal."; } // Show error but also provide browser option setBrowserUrl(result.result.browserUrl ?? null); setShowBrowserFallback(true); // Mark operation as completed - branch was pushed even though PR creation failed operationCompletedRef.current = true; toast.error("PR creation failed", { description: errorMessage, duration: 8000, }); // Don't call onCreated() here - we want to keep the dialog open to show the browser URL setIsLoading(false); return; } } // Show success toast for push toast.success("Branch pushed", { description: result.result.committed ? `Commit ${result.result.commitHash} pushed to ${result.result.branch}` : `Branch ${result.result.branch} pushed`, }); // No browser URL available, just close if (!result.result.prCreated) { if (!hasBrowserUrl) { toast.info("PR not created", { description: "Could not determine repository URL. GitHub CLI (gh) may not be installed or authenticated.", duration: 8000, }); } } onCreated(); onOpenChange(false); } } else { setError(result.error || "Failed to create pull request"); } } catch (err) { setError(err instanceof Error ? err.message : "Failed to create PR"); } finally { setIsLoading(false); } }; const handleClose = () => { // Only call onCreated() if an actual operation completed // This prevents unnecessary refreshes when user cancels if (operationCompletedRef.current) { // Pass the PR URL if one was created onCreated(prUrl || undefined); } onOpenChange(false); // State reset is handled by useEffect when open becomes false }; if (!worktree) return null; const shouldShowBrowserFallback = showBrowserFallback && browserUrl; return ( Create Pull Request Push changes and create a pull request from{" "} {worktree.branch} {prUrl ? (

Pull Request Created!

Your PR is ready for review

) : shouldShowBrowserFallback ? (

Branch Pushed!

Your changes have been pushed to GitHub.
Click below to create a pull request in your browser.

{browserUrl}

Tip: Install the GitHub CLI (gh) to create PRs directly from the app

) : ( <>
{worktree.hasChanges && (
setCommitMessage(e.target.value)} className="font-mono text-sm" />

{worktree.changedFilesCount} uncommitted file(s) will be committed

)}
setTitle(e.target.value)} />