/** * GitHub PRs View * * Displays pull requests using React Query for data fetching. */ import { useState, useCallback } from 'react'; import { GitPullRequest, RefreshCw, ExternalLink, GitMerge, X, MessageSquare, MoreHorizontal, Zap, ArrowLeft, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { getElectronAPI, type GitHubPR } from '@/lib/electron'; import { useAppStore, type Feature } from '@/store/app-store'; import { Button } from '@/components/ui/button'; import { Markdown } from '@/components/ui/markdown'; import { cn, generateUUID } from '@/lib/utils'; import { useIsMobile } from '@/hooks/use-media-query'; import { useGitHubPRs } from '@/hooks/queries'; import { useCreateFeature } from '@/hooks/mutations/use-feature-mutations'; import { PRCommentResolutionDialog } from '@/components/dialogs'; import { resolveModelString } from '@automaker/model-resolver'; import { toast } from 'sonner'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; export function GitHubPRsView() { const [selectedPR, setSelectedPR] = useState(null); const [commentDialogPR, setCommentDialogPR] = useState(null); const { currentProject, getEffectiveUseWorktrees } = useAppStore(); const isMobile = useIsMobile(); const { data, isLoading: loading, isFetching: refreshing, error, refetch, } = useGitHubPRs(currentProject?.path); const openPRs = data?.openPRs ?? []; const mergedPRs = data?.mergedPRs ?? []; const handleRefresh = useCallback(() => { refetch(); }, [refetch]); const handleOpenInGitHub = useCallback((url: string) => { const api = getElectronAPI(); api.openExternalLink(url); }, []); const createFeature = useCreateFeature(currentProject?.path ?? ''); const handleAutoAddressComments = useCallback( async (pr: GitHubPR) => { if (!pr.number || !currentProject?.path) { toast.error('Cannot address PR comments', { description: 'No PR number or project available.', }); return; } const featureId = `pr-${pr.number}-${generateUUID()}`; const feature: Feature = { id: featureId, title: `Address PR #${pr.number} Review Comments`, category: 'bug-fix', description: `Read the review requests on PR #${pr.number} and address any feedback the best you can.`, steps: [], status: 'backlog', model: resolveModelString('opus'), thinkingLevel: 'none', planningMode: 'skip', requirePlanApproval: false, dependencies: [], ...(pr.url ? { prUrl: pr.url } : {}), ...(pr.headRefName ? { branchName: pr.headRefName } : {}), }; try { await createFeature.mutateAsync(feature); // Start the feature immediately after creation const api = getElectronAPI(); if (api.autoMode?.runFeature) { try { await api.autoMode.runFeature( currentProject.path, featureId, getEffectiveUseWorktrees(currentProject.path) ); toast.success('Feature created and started', { description: `Addressing review comments on PR #${pr.number}`, }); } catch (runError) { toast.error('Feature created but failed to start', { description: runError instanceof Error ? runError.message : 'An error occurred while starting the feature', }); } } else { toast.error('Cannot start feature', { description: 'Feature API is not available. The feature was created but could not be started.', }); } } catch (error) { toast.error('Failed to create feature', { description: error instanceof Error ? error.message : 'An error occurred', }); } }, [currentProject, createFeature, getEffectiveUseWorktrees] ); const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); }; const getReviewStatus = (pr: GitHubPR) => { if (pr.isDraft) return { label: 'Draft', color: 'text-muted-foreground', bg: 'bg-muted' }; switch (pr.reviewDecision) { case 'APPROVED': return { label: 'Approved', color: 'text-green-500', bg: 'bg-green-500/10' }; case 'CHANGES_REQUESTED': return { label: 'Changes requested', color: 'text-orange-500', bg: 'bg-orange-500/10' }; case 'REVIEW_REQUIRED': return { label: 'Review required', color: 'text-yellow-500', bg: 'bg-yellow-500/10' }; default: return null; } }; if (loading) { return (
); } if (error) { return (

Failed to Load Pull Requests

{error instanceof Error ? error.message : 'Failed to fetch pull requests'}

); } const totalPRs = openPRs.length + mergedPRs.length; return (
{/* PR List - hidden on mobile when a PR is selected */}
{/* Header */}

Pull Requests

{totalPRs === 0 ? 'No pull requests found' : `${openPRs.length} open, ${mergedPRs.length} merged`}

{/* PR List */}
{totalPRs === 0 ? (

No Pull Requests

This repository has no pull requests yet.

) : (
{/* Open PRs */} {openPRs.map((pr) => ( setSelectedPR(pr)} onOpenExternal={() => handleOpenInGitHub(pr.url)} onManageComments={() => setCommentDialogPR(pr)} onAutoAddressComments={() => handleAutoAddressComments(pr)} formatDate={formatDate} getReviewStatus={getReviewStatus} /> ))} {/* Merged PRs Section */} {mergedPRs.length > 0 && ( <>
Merged ({mergedPRs.length})
{mergedPRs.map((pr) => ( setSelectedPR(pr)} onOpenExternal={() => handleOpenInGitHub(pr.url)} onManageComments={() => setCommentDialogPR(pr)} onAutoAddressComments={() => handleAutoAddressComments(pr)} formatDate={formatDate} getReviewStatus={getReviewStatus} /> ))} )}
)}
{/* PR Detail Panel */} {selectedPR && (() => { const reviewStatus = getReviewStatus(selectedPR); return (
{/* Detail Header */}
{isMobile && ( )} {selectedPR.state === 'MERGED' ? ( ) : ( )} #{selectedPR.number} {selectedPR.title} {selectedPR.isDraft && ( Draft )}
{!isMobile && ( )} {!isMobile && ( )}
{/* PR Detail Content */}
{/* Title */}

{selectedPR.title}

{/* Meta info */}
{selectedPR.state === 'MERGED' ? 'Merged' : selectedPR.isDraft ? 'Draft' : 'Open'} {reviewStatus && ( {reviewStatus.label} )} #{selectedPR.number} opened {formatDate(selectedPR.createdAt)} by{' '} {selectedPR.author.login}
{/* Branch info */} {selectedPR.headRefName && (
Branch: {selectedPR.headRefName}
)} {/* Labels */} {selectedPR.labels.length > 0 && (
{selectedPR.labels.map((label) => ( {label.name} ))}
)} {/* Body */} {selectedPR.body ? ( {selectedPR.body} ) : (

No description provided.

)} {/* Review Comments CTA */}
Review Comments

Manage review comments individually or let AI address all feedback automatically.

{/* Open in GitHub CTA */}

View code changes, comments, and reviews on GitHub.

); })()} {/* PR Comment Resolution Dialog */} {commentDialogPR && ( { if (!open) setCommentDialogPR(null); }} pr={commentDialogPR} /> )}
); } interface PRRowProps { pr: GitHubPR; isSelected: boolean; onClick: () => void; onOpenExternal: () => void; onManageComments: () => void; onAutoAddressComments: () => void; formatDate: (date: string) => string; getReviewStatus: (pr: GitHubPR) => { label: string; color: string; bg: string } | null; } function PRRow({ pr, isSelected, onClick, onOpenExternal, onManageComments, onAutoAddressComments, formatDate, getReviewStatus, }: PRRowProps) { const reviewStatus = getReviewStatus(pr); return (
{pr.state === 'MERGED' ? ( ) : ( )}
{pr.title} {pr.isDraft && ( Draft )}
#{pr.number} opened {formatDate(pr.createdAt)} by {pr.author.login} {pr.headRefName && ( {pr.headRefName} )}
{/* Review Status */} {reviewStatus && ( {reviewStatus.label} )} {/* Labels */} {pr.labels.map((label) => ( {label.name} ))}
{/* Actions dropdown menu */} { e.stopPropagation(); onManageComments(); }} className="text-xs text-blue-500 focus:text-blue-600" > Manage PR Comments { e.stopPropagation(); onAutoAddressComments(); }} className="text-xs text-blue-500 focus:text-blue-600" > Address PR Comments { e.stopPropagation(); onOpenExternal(); }} className="text-xs" > Open in GitHub
); }