// @ts-nocheck - GitHub issues view with issue selection and feature creation flow import { useState, useCallback, useMemo } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { CircleDot, RefreshCw, SearchX } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; import { getElectronAPI, GitHubIssue, IssueValidationResult } from '@/lib/electron'; import { useAppStore } from '@/store/app-store'; import { Button } from '@/components/ui/button'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { LoadingState } from '@/components/ui/loading-state'; import { ErrorState } from '@/components/ui/error-state'; import { cn, pathsEqual, generateUUID } from '@/lib/utils'; import { useIsMobile } from '@/hooks/use-media-query'; import { toast } from 'sonner'; import { queryKeys } from '@/lib/query-keys'; import { useGithubIssues, useIssueValidation, useIssuesFilter } from './github-issues-view/hooks'; import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components'; import { ValidationDialog } from './github-issues-view/dialogs'; import { AddFeatureDialog } from './board-view/dialogs'; import { formatDate, getFeaturePriority } from './github-issues-view/utils'; import { resolveModelString } from '@automaker/model-resolver'; import { useModelOverride } from '@/components/shared'; import type { ValidateIssueOptions, IssuesFilterState, IssuesStateFilter, } from './github-issues-view/types'; import { DEFAULT_ISSUES_FILTER_STATE } from './github-issues-view/types'; const logger = createLogger('GitHubIssuesView'); 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); // Add Feature dialog state const [showAddFeatureDialog, setShowAddFeatureDialog] = useState(false); const [createFeatureIssue, setCreateFeatureIssue] = useState(null); // Filter state const [filterState, setFilterState] = useState(DEFAULT_ISSUES_FILTER_STATE); const { currentProject, getCurrentWorktree, worktreesByProject, defaultSkipTests } = useAppStore(); const queryClient = useQueryClient(); // Model override for validation const validationModelOverride = useModelOverride({ phase: 'validationModel' }); const isMobile = useIsMobile(); const { openIssues, closedIssues, loading, refreshing, error, refresh } = useGithubIssues(); const { validatingIssues, cachedValidations, handleValidateIssue, handleViewCachedValidation } = useIssueValidation({ selectedIssue, showValidationDialog, onValidationResultChange: setValidationResult, onShowValidationDialogChange: setShowValidationDialog, }); // Combine all issues for filtering const allIssues = useMemo(() => [...openIssues, ...closedIssues], [openIssues, closedIssues]); // Apply filter to issues - now returns matched issues directly for better performance const filterResult = useIssuesFilter(allIssues, filterState, cachedValidations); // Separate filtered issues by state - this is O(n) but now only done once // since filterResult.matchedIssues already contains the filtered issues const { filteredOpenIssues, filteredClosedIssues } = useMemo(() => { const open: typeof openIssues = []; const closed: typeof closedIssues = []; for (const issue of filterResult.matchedIssues) { if (issue.state.toLowerCase() === 'open') { open.push(issue); } else { closed.push(issue); } } return { filteredOpenIssues: open, filteredClosedIssues: closed }; }, [filterResult.matchedIssues]); // Filter state change handlers const handleStateFilterChange = useCallback((stateFilter: IssuesStateFilter) => { setFilterState((prev) => ({ ...prev, stateFilter })); }, []); const handleLabelsChange = useCallback((selectedLabels: string[]) => { setFilterState((prev) => ({ ...prev, selectedLabels })); }, []); // Clear all filters to default state const handleClearFilters = useCallback(() => { setFilterState(DEFAULT_ISSUES_FILTER_STATE); }, []); // 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); }, []); // Build a prefilled description from a GitHub issue for the feature dialog const buildIssueDescription = useCallback( (issue: GitHubIssue) => { const parts = [ `**From GitHub Issue #${issue.number}**`, '', issue.body || 'No description provided.', ]; // Include labels if present if (issue.labels.length > 0) { parts.push('', `**Labels:** ${issue.labels.map((l) => l.name).join(', ')}`); } // Include linked PRs info if present if (issue.linkedPRs && issue.linkedPRs.length > 0) { parts.push( '', '**Linked Pull Requests:**', ...issue.linkedPRs.map((pr) => `- #${pr.number}: ${pr.title} (${pr.state})`) ); } // Include cached validation analysis if available const cached = cachedValidations.get(issue.number); if (cached?.result) { const validation = cached.result; parts.push('', '---', '', '**AI Validation Analysis:**', validation.reasoning); if (validation.suggestedFix) { parts.push('', `**Suggested Approach:**`, validation.suggestedFix); } if (validation.relatedFiles?.length) { parts.push('', '**Related Files:**', ...validation.relatedFiles.map((f) => `- \`${f}\``)); } } return parts.join('\n'); }, [cachedValidations] ); // Memoize the prefilled description to avoid recomputing on every render const prefilledDescription = useMemo( () => (createFeatureIssue ? buildIssueDescription(createFeatureIssue) : undefined), [createFeatureIssue, buildIssueDescription] ); // Open the Add Feature dialog with pre-filled data from a GitHub issue const handleCreateFeature = useCallback((issue: GitHubIssue) => { setCreateFeatureIssue(issue); setShowAddFeatureDialog(true); }, []); // Handle feature creation from the AddFeatureDialog const handleAddFeatureFromIssue = useCallback( async (featureData: { title: string; category: string; description: string; priority: number; model: string; thinkingLevel: string; reasoningEffort: string; skipTests: boolean; branchName: string; planningMode: string; requirePlanApproval: boolean; excludedPipelineSteps?: string[]; workMode: string; imagePaths?: Array<{ id: string; path: string; description?: string }>; textFilePaths?: Array<{ id: string; path: string; description?: string }>; }) => { if (!currentProject?.path) { toast.error('No project selected'); return; } try { const api = getElectronAPI(); if (api.features?.create) { const feature = { id: `issue-${createFeatureIssue?.number || 'new'}-${generateUUID()}`, title: featureData.title, description: featureData.description, category: featureData.category, status: 'backlog' as const, passes: false, priority: featureData.priority, model: featureData.model, thinkingLevel: featureData.thinkingLevel, reasoningEffort: featureData.reasoningEffort, providerId: featureData.providerId, skipTests: featureData.skipTests, branchName: featureData.workMode === 'current' ? currentBranch : featureData.branchName, planningMode: featureData.planningMode, requirePlanApproval: featureData.requirePlanApproval, dependencies: [], excludedPipelineSteps: featureData.excludedPipelineSteps, ...(featureData.imagePaths?.length ? { imagePaths: featureData.imagePaths } : {}), ...(featureData.textFilePaths?.length ? { textFilePaths: featureData.textFilePaths } : {}), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; const result = await api.features.create(currentProject.path, feature); if (result.success) { queryClient.invalidateQueries({ queryKey: queryKeys.features.all(currentProject.path), }); toast.success( `Created feature: ${featureData.title || featureData.description.slice(0, 50)}` ); setShowAddFeatureDialog(false); setCreateFeatureIssue(null); } else { toast.error(result.error || 'Failed to create feature'); } } } catch (err) { logger.error('Create feature from issue error:', err); toast.error(err instanceof Error ? err.message : 'Failed to create feature'); } }, [currentProject?.path, currentBranch, queryClient, createFeatureIssue] ); 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 parts = [ `**From GitHub Issue #${issue.number}**`, '', issue.body || 'No description provided.', '', '---', '', '**AI Validation Analysis:**', validation.reasoning, ]; if (validation.suggestedFix) { parts.push('', `**Suggested Approach:**`, validation.suggestedFix); } if (validation.relatedFiles?.length) { parts.push( '', '**Related Files:**', ...validation.relatedFiles.map((f) => `- \`${f}\``) ); } const description = parts.join('\n'); const feature = { id: `issue-${issue.number}-${generateUUID()}`, title: issue.title, description, category: 'From GitHub', status: 'backlog' as const, passes: false, priority: getFeaturePriority(validation.estimatedComplexity), model: resolveModelString('opus'), thinkingLevel: 'none' as const, branchName: currentBranch, planningMode: 'skip' as const, requirePlanApproval: false, dependencies: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; const result = await api.features.create(currentProject.path, feature); if (result.success) { // Invalidate React Query cache to sync UI queryClient.invalidateQueries({ queryKey: queryKeys.features.all(currentProject.path), }); toast.success(`Created task: ${issue.title}`); } else { toast.error(result.error || 'Failed to create task'); } } } catch (err) { logger.error('Convert to task error:', err); toast.error(err instanceof Error ? err.message : 'Failed to create task'); } }, [currentProject?.path, currentBranch, queryClient] ); if (loading) { return ; } if (error) { return ; } const totalIssues = filteredOpenIssues.length + filteredClosedIssues.length; const totalUnfilteredIssues = openIssues.length + closedIssues.length; const isFilteredEmpty = totalIssues === 0 && totalUnfilteredIssues > 0 && filterResult.hasActiveFilter; return (
{/* Issues List - hidden on mobile when an issue is selected */}
{/* Header */} {/* Issues List */}
{totalIssues === 0 ? (
{isFilteredEmpty ? ( ) : ( )}

{isFilteredEmpty ? 'No Matching Issues' : 'No Issues'}

{isFilteredEmpty ? 'No issues match your current filters.' : 'This repository has no issues yet.'}

{isFilteredEmpty && ( )}
) : (
{/* Open Issues */} {filteredOpenIssues.map((issue) => ( setSelectedIssue(issue)} onOpenExternal={() => handleOpenInGitHub(issue.url)} formatDate={formatDate} cachedValidation={cachedValidations.get(issue.number)} isValidating={validatingIssues.has(issue.number)} /> ))} {/* Closed Issues Section */} {filteredClosedIssues.length > 0 && ( <>
Closed Issues ({filteredClosedIssues.length})
{filteredClosedIssues.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); }} onCreateFeature={handleCreateFeature} formatDate={formatDate} modelOverride={validationModelOverride} isMobile={isMobile} /> )} {/* Validation Dialog */} {/* Add Feature Dialog - opened from issue detail panel */} { setShowAddFeatureDialog(open); if (!open) { setCreateFeatureIssue(null); } }} onAdd={handleAddFeatureFromIssue} categorySuggestions={['From GitHub']} branchSuggestions={[]} defaultSkipTests={defaultSkipTests} defaultBranch={currentBranch} currentBranch={currentBranch || undefined} isMaximized={false} projectPath={currentProject?.path} prefilledTitle={createFeatureIssue?.title} prefilledDescription={prefilledDescription} prefilledCategory="From GitHub" /> {/* 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) { logger.info('Revalidating with options:', { commentsCount: pendingRevalidateOptions.comments?.length ?? 0, linkedPRsCount: pendingRevalidateOptions.linkedPRs?.length ?? 0, }); handleValidateIssue(selectedIssue, { ...pendingRevalidateOptions, forceRevalidate: true, }); } }} />
); }