import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Label } from '@radix-ui/react-label'; import { Textarea } from '@/components/ui/textarea.tsx'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import BranchSelector from '@/components/tasks/BranchSelector'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { attemptsApi } from '@/lib/api.ts'; import { useTranslation } from 'react-i18next'; import { TaskWithAttemptStatus, Workspace } from 'shared/types'; import { Loader2 } from 'lucide-react'; import NiceModal, { useModal } from '@ebay/nice-modal-react'; import { useAuth, useRepoBranches } from '@/hooks'; import { GhCliHelpInstructions, GhCliSetupDialog, mapGhCliErrorToUi, } from '@/components/dialogs/auth/GhCliSetupDialog'; import type { GhCliSupportContent, GhCliSupportVariant, } from '@/components/dialogs/auth/GhCliSetupDialog'; import type { GhCliSetupError } from 'shared/types'; import { useUserSystem } from '@/components/ConfigProvider'; import { defineModal } from '@/lib/modals'; interface CreatePRDialogProps { attempt: Workspace; task: TaskWithAttemptStatus; repoId: string; targetBranch?: string; } export type CreatePRDialogResult = { success: boolean; error?: string; }; const CreatePRDialogImpl = NiceModal.create( ({ attempt, task, repoId, targetBranch }) => { const modal = useModal(); const { t } = useTranslation('tasks'); const { isLoaded } = useAuth(); const { environment, config } = useUserSystem(); const [prTitle, setPrTitle] = useState(''); const [prBody, setPrBody] = useState(''); const [prBaseBranch, setPrBaseBranch] = useState(''); const [creatingPR, setCreatingPR] = useState(false); const [error, setError] = useState(null); const [ghCliHelp, setGhCliHelp] = useState( null ); const [isDraft, setIsDraft] = useState(false); const [autoGenerateDescription, setAutoGenerateDescription] = useState( config?.pr_auto_description_enabled ?? false ); const { data: branches = [], isLoading: branchesLoading } = useRepoBranches( repoId, { enabled: modal.visible && !!repoId } ); const getGhCliHelpTitle = (variant: GhCliSupportVariant) => variant === 'homebrew' ? 'Homebrew is required for automatic setup' : 'GitHub CLI needs manual setup'; // Initialize form when dialog opens useEffect(() => { if (!modal.visible || !isLoaded) { return; } setPrTitle(`${task.title} (vibe-kanban)`); setPrBody(task.description || ''); setError(null); setGhCliHelp(null); }, [modal.visible, isLoaded, task]); // Set default base branch when branches are loaded useEffect(() => { if (branches.length > 0 && !prBaseBranch) { // First priority: use the target branch from attempt config if (targetBranch && branches.some((b) => b.name === targetBranch)) { setPrBaseBranch(targetBranch); return; } // Fallback: use the current branch const currentBranch = branches.find((b) => b.is_current); if (currentBranch) { setPrBaseBranch(currentBranch.name); } } }, [branches, prBaseBranch, targetBranch]); const isMacEnvironment = useMemo( () => environment?.os_type?.toLowerCase().includes('mac'), [environment?.os_type] ); const handleConfirmCreatePR = useCallback(async () => { if (!repoId || !attempt.id) return; setError(null); setGhCliHelp(null); setCreatingPR(true); const handleGhCliSetupOutcome = ( setupResult: GhCliSetupError | null, fallbackMessage: string ) => { if (setupResult === null) { setError(null); setGhCliHelp(null); setCreatingPR(false); modal.hide(); return; } const ui = mapGhCliErrorToUi(setupResult, fallbackMessage, t); if (ui.variant) { setGhCliHelp(ui); setError(null); return; } setGhCliHelp(null); setError(ui.message); }; const result = await attemptsApi.createPR(attempt.id, { title: prTitle, body: prBody || null, target_branch: prBaseBranch || null, draft: isDraft, auto_generate_description: autoGenerateDescription, repo_id: repoId, }); if (result.success) { setPrTitle(''); setPrBody(''); setPrBaseBranch(''); setIsDraft(false); setAutoGenerateDescription( config?.pr_auto_description_enabled ?? false ); setCreatingPR(false); modal.resolve({ success: true } as CreatePRDialogResult); modal.hide(); return; } setCreatingPR(false); const defaultGhCliErrorMessage = result.message || 'Failed to run GitHub CLI setup.'; const showGhCliSetupDialog = async () => { const setupResult = await GhCliSetupDialog.show({ attemptId: attempt.id, }); handleGhCliSetupOutcome(setupResult, defaultGhCliErrorMessage); }; if (result.error) { if ( result.error.type === 'cli_not_installed' || result.error.type === 'cli_not_logged_in' ) { // Only show setup dialog for GitHub CLI on Mac if (result.error.provider === 'git_hub' && isMacEnvironment) { await showGhCliSetupDialog(); } else { const providerName = result.error.provider === 'git_hub' ? 'GitHub' : result.error.provider === 'azure_dev_ops' ? 'Azure DevOps' : 'Git host'; const action = result.error.type === 'cli_not_installed' ? 'not installed' : 'not logged in'; setError(`${providerName} CLI is ${action}`); setGhCliHelp(null); } return; } else if ( result.error.type === 'git_cli_not_installed' || result.error.type === 'git_cli_not_logged_in' ) { const gitCliErrorKey = result.error.type === 'git_cli_not_logged_in' ? 'createPrDialog.errors.gitCliNotLoggedIn' : 'createPrDialog.errors.gitCliNotInstalled'; setError(result.message || t(gitCliErrorKey)); setGhCliHelp(null); return; } else if (result.error.type === 'target_branch_not_found') { setError( t('createPrDialog.errors.targetBranchNotFound', { branch: result.error.branch, }) ); setGhCliHelp(null); return; } } if (result.message) { setError(result.message); setGhCliHelp(null); } else { setError(t('createPrDialog.errors.failedToCreate')); setGhCliHelp(null); } }, [ attempt, repoId, prBaseBranch, prBody, prTitle, isDraft, autoGenerateDescription, config?.pr_auto_description_enabled, modal, isMacEnvironment, t, ]); const handleCancelCreatePR = useCallback(() => { // Return error if one was set, otherwise just canceled const result: CreatePRDialogResult = error ? { success: false, error } : { success: false }; modal.resolve(result); modal.hide(); // Reset form to empty state setPrTitle(''); setPrBody(''); setPrBaseBranch(''); setIsDraft(false); setAutoGenerateDescription(config?.pr_auto_description_enabled ?? false); }, [modal, config?.pr_auto_description_enabled, error]); return ( <> handleCancelCreatePR()} > {t('createPrDialog.title')} {t('createPrDialog.description')} {!isLoaded ? (
) : (
setPrTitle(e.target.value)} placeholder={t('createPrDialog.titlePlaceholder')} disabled={autoGenerateDescription} className={ autoGenerateDescription ? 'opacity-50 cursor-not-allowed' : '' } />