import { useState, useCallback, useMemo } from 'react'; import { CircleDot, RefreshCw } from 'lucide-react'; import { getElectronAPI, GitHubIssue, IssueValidationResult } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { LoadingState } from '@/components/ui/loading-state'; import { ErrorState } from '@/components/ui/error-state'; import { cn, pathsEqual } from '@/lib/utils'; import { toast } from 'sonner'; import { useGithubIssues, useIssueValidation } from './github-issues-view/hooks'; import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components'; import { ValidationDialog } from './github-issues-view/dialogs'; import { formatDate, getFeaturePriority } from './github-issues-view/utils'; import type { ValidateIssueOptions } from './github-issues-view/types'; export function GitHubIssuesView() { const [selectedIssue, setSelectedIssue] = useState(null); const [validationResult, setValidationResult] = useState(null); const [showValidationDialog, setShowValidationDialog] = useState(false); const [showRevalidateConfirm, setShowRevalidateConfirm] = useState(false); const [pendingRevalidateOptions, setPendingRevalidateOptions] = useState(null); const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } = useAppStore(); const { openIssues, closedIssues, loading, refreshing, error, refresh } = useGithubIssues(); const { validatingIssues, cachedValidations, handleValidateIssue, handleViewCachedValidation } = useIssueValidation({ selectedIssue, showValidationDialog, onValidationResultChange: setValidationResult, onShowValidationDialogChange: setShowValidationDialog, }); // Get default AI profile for task creation const defaultProfile = useMemo(() => { if (!defaultAIProfileId) return null; return aiProfiles.find((p) => p.id === defaultAIProfileId) ?? null; }, [defaultAIProfileId, aiProfiles]); // Get current branch from selected worktree const currentBranch = useMemo(() => { if (!currentProject?.path) return ''; const currentWorktreeInfo = getCurrentWorktree(currentProject.path); const worktrees = worktreesByProject[currentProject.path] ?? []; const currentWorktreePath = currentWorktreeInfo?.path ?? null; const selectedWorktree = currentWorktreePath === null ? worktrees.find((w) => w.isMain) : worktrees.find((w) => !w.isMain && pathsEqual(w.path, currentWorktreePath)); return selectedWorktree?.branch || worktrees.find((w) => w.isMain)?.branch || ''; }, [currentProject?.path, getCurrentWorktree, worktreesByProject]); const handleOpenInGitHub = useCallback((url: string) => { const api = getElectronAPI(); api.openExternalLink(url); }, []); const handleConvertToTask = useCallback( async (issue: GitHubIssue, validation: IssueValidationResult) => { if (!currentProject?.path) { toast.error('No project selected'); return; } try { const api = getElectronAPI(); if (api.features?.create) { // Build description from issue body + validation info const description = [ `**From GitHub Issue #${issue.number}**`, '', issue.body || 'No description provided.', '', '---', '', '**AI Validation Analysis:**', validation.reasoning, validation.suggestedFix ? `\n**Suggested Approach:**\n${validation.suggestedFix}` : '', validation.relatedFiles?.length ? `\n**Related Files:**\n${validation.relatedFiles.map((f) => `- \`${f}\``).join('\n')}` : '', ] .filter(Boolean) .join('\n'); const feature = { id: `issue-${issue.number}-${crypto.randomUUID()}`, title: issue.title, description, category: 'From GitHub', status: 'backlog' as const, passes: false, priority: getFeaturePriority(validation.estimatedComplexity), model: defaultProfile?.model ?? 'opus', thinkingLevel: defaultProfile?.thinkingLevel ?? 'none', branchName: currentBranch, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; const result = await api.features.create(currentProject.path, feature); if (result.success) { toast.success(`Created task: ${issue.title}`); } else { toast.error(result.error || 'Failed to create task'); } } } catch (err) { console.error('[GitHubIssuesView] Convert to task error:', err); toast.error(err instanceof Error ? err.message : 'Failed to create task'); } }, [currentProject?.path, defaultProfile, currentBranch] ); if (loading) { return ; } if (error) { return ; } const totalIssues = openIssues.length + closedIssues.length; return (
{/* Issues List */}
{/* Header */} {/* Issues List */}
{totalIssues === 0 ? (

No Issues

This repository has no issues yet.

) : (
{/* Open Issues */} {openIssues.map((issue) => ( setSelectedIssue(issue)} onOpenExternal={() => handleOpenInGitHub(issue.url)} formatDate={formatDate} cachedValidation={cachedValidations.get(issue.number)} isValidating={validatingIssues.has(issue.number)} /> ))} {/* Closed Issues Section */} {closedIssues.length > 0 && ( <>
Closed Issues ({closedIssues.length})
{closedIssues.map((issue) => ( setSelectedIssue(issue)} onOpenExternal={() => handleOpenInGitHub(issue.url)} formatDate={formatDate} cachedValidation={cachedValidations.get(issue.number)} isValidating={validatingIssues.has(issue.number)} /> ))} )}
)}
{/* Issue Detail Panel */} {selectedIssue && ( setSelectedIssue(null)} onShowRevalidateConfirm={(options) => { setPendingRevalidateOptions(options); setShowRevalidateConfirm(true); }} formatDate={formatDate} /> )} {/* Validation Dialog */} {/* Revalidate Confirmation Dialog */} { setShowRevalidateConfirm(open); if (!open) { setPendingRevalidateOptions(null); } }} title="Re-validate Issue" description={`Are you sure you want to re-validate issue #${selectedIssue?.number}? This will run a new AI analysis and replace the existing validation result.`} icon={RefreshCw} iconClassName="text-primary" confirmText="Re-validate" onConfirm={() => { if (selectedIssue && pendingRevalidateOptions) { console.log('[GitHubIssuesView] Revalidating with options:', { commentsCount: pendingRevalidateOptions.comments?.length ?? 0, linkedPRsCount: pendingRevalidateOptions.linkedPRs?.length ?? 0, }); handleValidateIssue(selectedIssue, pendingRevalidateOptions); } }} />
); }