"use client"; import { useState, useEffect } 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; onCreated: () => void; } export function CreatePRDialog({ open, onOpenChange, worktree, 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); // Reset state when dialog opens or worktree changes useEffect(() => { if (open) { // Only reset form fields, not the result states (prUrl, browserUrl, showBrowserFallback) // These are set by the API response and should persist until dialog closes setTitle(""); setBody(""); setCommitMessage(""); setBaseBranch("main"); setIsDraft(false); setError(null); } else { // Reset everything when dialog closes setTitle(""); setBody(""); setCommitMessage(""); setBaseBranch("main"); setIsDraft(false); setError(null); setPrUrl(null); setBrowserUrl(null); setShowBrowserFallback(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, { 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); toast.success("Pull request created!", { description: `PR created from ${result.result.branch}`, action: { label: "View PR", onClick: () => window.open(result.result!.prUrl!, "_blank"), }, }); onCreated(); } 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); 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); 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 = () => { onOpenChange(false); // Reset state after dialog closes setTimeout(() => { setTitle(""); setBody(""); setCommitMessage(""); setBaseBranch("main"); setIsDraft(false); setError(null); setPrUrl(null); setBrowserUrl(null); setShowBrowserFallback(false); }, 200); }; 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)} />