-
Notifications
You must be signed in to change notification settings - Fork 578
feat: Add auto-validate toggle and import dialog for GitHub Issues #446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| // @ts-nocheck | ||
| import { useState, useCallback, useMemo } from 'react'; | ||
| import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; | ||
| import { createLogger } from '@automaker/utils/logger'; | ||
| import { CircleDot, RefreshCw } from 'lucide-react'; | ||
| import { getElectronAPI, GitHubIssue, IssueValidationResult } from '@/lib/electron'; | ||
|
|
@@ -11,7 +11,7 @@ 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 { ValidationDialog, ImportIssuesDialog } 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'; | ||
|
|
@@ -23,11 +23,19 @@ export function GitHubIssuesView() { | |
| const [validationResult, setValidationResult] = useState<IssueValidationResult | null>(null); | ||
| const [showValidationDialog, setShowValidationDialog] = useState(false); | ||
| const [showRevalidateConfirm, setShowRevalidateConfirm] = useState(false); | ||
| const [showImportDialog, setShowImportDialog] = useState(false); | ||
| const [pendingRevalidateOptions, setPendingRevalidateOptions] = | ||
| useState<ValidateIssueOptions | null>(null); | ||
|
|
||
| const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } = | ||
| useAppStore(); | ||
| const { | ||
| currentProject, | ||
| defaultAIProfileId, | ||
| aiProfiles, | ||
| getCurrentWorktree, | ||
| worktreesByProject, | ||
| githubAutoValidate, | ||
| setGithubAutoValidate, | ||
| } = useAppStore(); | ||
|
|
||
| // Model override for validation | ||
| const validationModelOverride = useModelOverride({ phase: 'validationModel' }); | ||
|
|
@@ -45,6 +53,54 @@ export function GitHubIssuesView() { | |
| onShowValidationDialogChange: setShowValidationDialog, | ||
| }); | ||
|
|
||
| // Track which issues we've already triggered auto-validate for (to avoid duplicates) | ||
| const autoValidatedIssuesRef = useRef<Set<number>>(new Set()); | ||
|
|
||
| // Auto-validate issues when enabled and issues are loaded | ||
| useEffect(() => { | ||
| if (!githubAutoValidate || loading || openIssues.length === 0) return; | ||
|
|
||
| // Find issues that need validation (not cached and not currently validating) | ||
| const issuesToValidate = openIssues.filter((issue) => { | ||
| // Skip if already auto-validated in this session | ||
| if (autoValidatedIssuesRef.current.has(issue.number)) return false; | ||
| // Skip if already has cached validation | ||
| if (cachedValidations.has(issue.number)) return false; | ||
| // Skip if currently validating | ||
| if (validatingIssues.has(issue.number)) return false; | ||
| return true; | ||
| }); | ||
|
|
||
| // Validate up to 3 issues at a time to avoid overwhelming the system | ||
| const batchSize = 3; | ||
| const batch = issuesToValidate.slice(0, batchSize); | ||
|
|
||
| for (const issue of batch) { | ||
| autoValidatedIssuesRef.current.add(issue.number); | ||
| handleValidateIssue(issue); | ||
| } | ||
|
|
||
| if (batch.length > 0) { | ||
| logger.info( | ||
| `Auto-validating ${batch.length} issues (${issuesToValidate.length - batch.length} remaining)` | ||
| ); | ||
| } | ||
| }, [ | ||
| githubAutoValidate, | ||
| loading, | ||
| openIssues, | ||
| cachedValidations, | ||
| validatingIssues, | ||
| handleValidateIssue, | ||
| ]); | ||
|
|
||
| // Reset auto-validated tracking when auto-validate is toggled off | ||
| useEffect(() => { | ||
| if (!githubAutoValidate) { | ||
| autoValidatedIssuesRef.current.clear(); | ||
| } | ||
| }, [githubAutoValidate]); | ||
|
|
||
| // Get default AI profile for task creation | ||
| const defaultProfile = useMemo(() => { | ||
| if (!defaultAIProfileId) return null; | ||
|
|
@@ -132,6 +188,42 @@ export function GitHubIssuesView() { | |
| [currentProject?.path, defaultProfile, currentBranch] | ||
| ); | ||
|
|
||
| // Handle bulk import of issues as tasks | ||
| const handleImportIssues = useCallback( | ||
| async (issues: GitHubIssue[]) => { | ||
| if (!currentProject?.path) { | ||
| toast.error('No project selected'); | ||
| return; | ||
| } | ||
|
|
||
| let successCount = 0; | ||
| let errorCount = 0; | ||
|
|
||
| for (const issue of issues) { | ||
| const validation = cachedValidations.get(issue.number); | ||
| if (!validation?.result) { | ||
| errorCount++; | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| await handleConvertToTask(issue, validation.result); | ||
| successCount++; | ||
| } catch { | ||
| errorCount++; | ||
| } | ||
| } | ||
|
|
||
| if (successCount > 0) { | ||
| toast.success(`Imported ${successCount} issue${successCount !== 1 ? 's' : ''} as tasks`); | ||
| } | ||
| if (errorCount > 0) { | ||
| toast.error(`Failed to import ${errorCount} issue${errorCount !== 1 ? 's' : ''}`); | ||
| } | ||
| }, | ||
| [currentProject?.path, cachedValidations, handleConvertToTask] | ||
| ); | ||
|
Comment on lines
+191
to
+225
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐛 Suggested fixFirst, modify const handleConvertToTask = useCallback(
- async (issue: GitHubIssue, validation: IssueValidationResult) => {
+ async (issue: GitHubIssue, validation: IssueValidationResult): Promise<boolean> => {
if (!currentProject?.path) {
toast.error('No project selected');
- return;
+ return false;
}
try {
const api = getElectronAPI();
if (api.features?.create) {
// ... build description and feature ...
const result = await api.features.create(currentProject.path, feature);
if (result.success) {
toast.success(`Created task: ${issue.title}`);
+ return true;
} else {
toast.error(result.error || 'Failed to create task');
+ return false;
}
}
+ return false;
} catch (err) {
logger.error('Convert to task error:', err);
toast.error(err instanceof Error ? err.message : 'Failed to create task');
+ return false;
}
},
[currentProject?.path, defaultProfile, currentBranch]
);Then update try {
- await handleConvertToTask(issue, validation.result);
- successCount++;
+ const success = await handleConvertToTask(issue, validation.result);
+ if (success) {
+ successCount++;
+ } else {
+ errorCount++;
+ }
} catch {
errorCount++;
}🤖 Prompt for AI Agents |
||
|
|
||
| if (loading) { | ||
| return <LoadingState />; | ||
| } | ||
|
|
@@ -157,6 +249,9 @@ export function GitHubIssuesView() { | |
| closedCount={closedIssues.length} | ||
| refreshing={refreshing} | ||
| onRefresh={refresh} | ||
| autoValidate={githubAutoValidate} | ||
| onAutoValidateChange={setGithubAutoValidate} | ||
| onImportClick={() => setShowImportDialog(true)} | ||
| /> | ||
|
|
||
| {/* Issues List */} | ||
|
|
@@ -265,6 +360,16 @@ export function GitHubIssuesView() { | |
| } | ||
| }} | ||
| /> | ||
|
|
||
| {/* Import Issues Dialog */} | ||
| <ImportIssuesDialog | ||
| open={showImportDialog} | ||
| onOpenChange={setShowImportDialog} | ||
| issues={openIssues} | ||
| cachedValidations={cachedValidations} | ||
| validatingIssues={validatingIssues} | ||
| onImport={handleImportIssues} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,27 @@ | ||
| import { CircleDot, RefreshCw } from 'lucide-react'; | ||
| import { CircleDot, RefreshCw, Zap, Import } from 'lucide-react'; | ||
| import { Button } from '@/components/ui/button'; | ||
| import { Switch } from '@/components/ui/switch'; | ||
| import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; | ||
| import { cn } from '@/lib/utils'; | ||
|
|
||
| interface IssuesListHeaderProps { | ||
| openCount: number; | ||
| closedCount: number; | ||
| refreshing: boolean; | ||
| onRefresh: () => void; | ||
| autoValidate: boolean; | ||
| onAutoValidateChange: (enabled: boolean) => void; | ||
| onImportClick: () => void; | ||
| } | ||
|
|
||
| export function IssuesListHeader({ | ||
| openCount, | ||
| closedCount, | ||
| refreshing, | ||
| onRefresh, | ||
| autoValidate, | ||
| onAutoValidateChange, | ||
| onImportClick, | ||
| }: IssuesListHeaderProps) { | ||
| const totalIssues = openCount + closedCount; | ||
|
|
||
|
|
@@ -30,9 +38,55 @@ export function IssuesListHeader({ | |
| </p> | ||
| </div> | ||
| </div> | ||
| <Button variant="outline" size="sm" onClick={onRefresh} disabled={refreshing}> | ||
| <RefreshCw className={cn('h-4 w-4', refreshing && 'animate-spin')} /> | ||
| </Button> | ||
| <div className="flex items-center gap-3"> | ||
| {/* Auto-validate toggle */} | ||
| <TooltipProvider> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <div className="flex items-center gap-2"> | ||
| <Zap | ||
| className={cn( | ||
| 'h-4 w-4', | ||
| autoValidate ? 'text-yellow-500' : 'text-muted-foreground' | ||
| )} | ||
| /> | ||
| <Switch | ||
| checked={autoValidate} | ||
| onCheckedChange={onAutoValidateChange} | ||
| aria-label="Auto-validate issues" | ||
| /> | ||
| </div> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| <p>Auto-validate: {autoValidate ? 'ON' : 'OFF'}</p> | ||
| <p className="text-xs text-muted-foreground"> | ||
| {autoValidate | ||
| ? 'Issues are validated automatically when loaded' | ||
| : 'Click validate button for each issue'} | ||
| </p> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| </TooltipProvider> | ||
|
|
||
| {/* Import button */} | ||
| <TooltipProvider> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button variant="outline" size="sm" onClick={onImportClick}> | ||
| <Import className="h-4 w-4" /> | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| <p>Import issues as tasks</p> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| </TooltipProvider> | ||
|
|
||
| {/* Refresh button */} | ||
| <Button variant="outline" size="sm" onClick={onRefresh} disabled={refreshing}> | ||
| <RefreshCw className={cn('h-4 w-4', refreshing && 'animate-spin')} /> | ||
| </Button> | ||
| </div> | ||
|
Comment on lines
+41
to
+89
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a best practice to wrap your application (or a section of it) in a single |
||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a better user experience with bulk imports, you can process the issue imports in parallel instead of sequentially. Using
Promise.allSettledwill execute all import operations concurrently, which can significantly speed up the process when importing multiple issues. This also allows for more robust error handling by collecting all results, whether fulfilled or rejected.