Files
automaker/apps/ui/src/components/views/github-issues-view.tsx
DhanushSantosh 96f154d440 refactor: enhance type safety and error handling in navigation and project creation
- Updated navigation functions to cast route paths correctly, improving type safety.
- Added error handling for the templates API in project creation hooks to ensure robustness.
- Refactored task progress panel to improve type handling for feature data.
- Introduced type checks and default values in various components to enhance overall stability.

These changes improve the reliability and maintainability of the application, ensuring better user experience and code quality.
2026-01-07 19:09:25 +05:30

271 lines
10 KiB
TypeScript

// @ts-nocheck
import { useState, useCallback, useMemo } from 'react';
import { createLogger } from '@automaker/utils/logger';
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 { useModelOverride } from '@/components/shared';
import type { ValidateIssueOptions } from './github-issues-view/types';
const logger = createLogger('GitHubIssuesView');
export function GitHubIssuesView() {
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
const [validationResult, setValidationResult] = useState<IssueValidationResult | null>(null);
const [showValidationDialog, setShowValidationDialog] = useState(false);
const [showRevalidateConfirm, setShowRevalidateConfirm] = useState(false);
const [pendingRevalidateOptions, setPendingRevalidateOptions] =
useState<ValidateIssueOptions | null>(null);
const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } =
useAppStore();
// Model override for validation
const validationModelOverride = useModelOverride({ phase: 'validationModel' });
// Extract model string for API calls (backward compatibility)
const validationModelString = validationModelOverride.effectiveModel;
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');
// Use profile default model
const featureModel = defaultProfile?.model ?? 'opus';
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: featureModel,
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) {
logger.error('Convert to task error:', err);
toast.error(err instanceof Error ? err.message : 'Failed to create task');
}
},
[currentProject?.path, defaultProfile, currentBranch]
);
if (loading) {
return <LoadingState />;
}
if (error) {
return <ErrorState error={error} title="Failed to Load Issues" onRetry={refresh} />;
}
const totalIssues = openIssues.length + closedIssues.length;
return (
<div className="flex-1 flex overflow-hidden">
{/* Issues List */}
<div
className={cn(
'flex flex-col overflow-hidden border-r border-border',
selectedIssue ? 'w-80' : 'flex-1'
)}
>
{/* Header */}
<IssuesListHeader
openCount={openIssues.length}
closedCount={closedIssues.length}
refreshing={refreshing}
onRefresh={refresh}
/>
{/* Issues List */}
<div className="flex-1 overflow-auto">
{totalIssues === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center p-6">
<div className="p-4 rounded-full bg-muted/50 mb-4">
<CircleDot className="h-8 w-8 text-muted-foreground" />
</div>
<h2 className="text-base font-medium mb-2">No Issues</h2>
<p className="text-sm text-muted-foreground">This repository has no issues yet.</p>
</div>
) : (
<div className="divide-y divide-border">
{/* Open Issues */}
{openIssues.map((issue) => (
<IssueRow
key={issue.number}
issue={issue}
isSelected={selectedIssue?.number === issue.number}
onClick={() => setSelectedIssue(issue)}
onOpenExternal={() => handleOpenInGitHub(issue.url)}
formatDate={formatDate}
cachedValidation={cachedValidations.get(issue.number)}
isValidating={validatingIssues.has(issue.number)}
/>
))}
{/* Closed Issues Section */}
{closedIssues.length > 0 && (
<>
<div className="px-4 py-2 bg-muted/30 text-xs font-medium text-muted-foreground">
Closed Issues ({closedIssues.length})
</div>
{closedIssues.map((issue) => (
<IssueRow
key={issue.number}
issue={issue}
isSelected={selectedIssue?.number === issue.number}
onClick={() => setSelectedIssue(issue)}
onOpenExternal={() => handleOpenInGitHub(issue.url)}
formatDate={formatDate}
cachedValidation={cachedValidations.get(issue.number)}
isValidating={validatingIssues.has(issue.number)}
/>
))}
</>
)}
</div>
)}
</div>
</div>
{/* Issue Detail Panel */}
{selectedIssue && (
<IssueDetailPanel
issue={selectedIssue}
validatingIssues={validatingIssues}
cachedValidations={cachedValidations}
onValidateIssue={handleValidateIssue}
onViewCachedValidation={handleViewCachedValidation}
onOpenInGitHub={handleOpenInGitHub}
onClose={() => setSelectedIssue(null)}
onShowRevalidateConfirm={(options) => {
setPendingRevalidateOptions(options);
setShowRevalidateConfirm(true);
}}
formatDate={formatDate}
modelOverride={validationModelOverride}
/>
)}
{/* Validation Dialog */}
<ValidationDialog
open={showValidationDialog}
onOpenChange={setShowValidationDialog}
issue={selectedIssue}
validationResult={validationResult}
onConvertToTask={handleConvertToTask}
/>
{/* Revalidate Confirmation Dialog */}
<ConfirmDialog
open={showRevalidateConfirm}
onOpenChange={(open) => {
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,
});
}
}}
/>
</div>
);
}