diff --git a/apps/ui/src/components/views/github-issues-view.tsx b/apps/ui/src/components/views/github-issues-view.tsx
index e8e5536b8..7bbd03431 100644
--- a/apps/ui/src/components/views/github-issues-view.tsx
+++ b/apps/ui/src/components/views/github-issues-view.tsx
@@ -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(null);
const [showValidationDialog, setShowValidationDialog] = useState(false);
const [showRevalidateConfirm, setShowRevalidateConfirm] = useState(false);
+ const [showImportDialog, setShowImportDialog] = useState(false);
const [pendingRevalidateOptions, setPendingRevalidateOptions] =
useState(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>(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]
+ );
+
if (loading) {
return ;
}
@@ -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 */}
+
);
}
diff --git a/apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx b/apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx
index 5529b30c6..8a445cb20 100644
--- a/apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx
+++ b/apps/ui/src/components/views/github-issues-view/components/issues-list-header.tsx
@@ -1,5 +1,7 @@
-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 {
@@ -7,6 +9,9 @@ interface IssuesListHeaderProps {
closedCount: number;
refreshing: boolean;
onRefresh: () => void;
+ autoValidate: boolean;
+ onAutoValidateChange: (enabled: boolean) => void;
+ onImportClick: () => void;
}
export function IssuesListHeader({
@@ -14,6 +19,9 @@ export function IssuesListHeader({
closedCount,
refreshing,
onRefresh,
+ autoValidate,
+ onAutoValidateChange,
+ onImportClick,
}: IssuesListHeaderProps) {
const totalIssues = openCount + closedCount;
@@ -30,9 +38,55 @@ export function IssuesListHeader({
-
+
+ {/* Auto-validate toggle */}
+
+
+
+
+
+
+
+
+
+ Auto-validate: {autoValidate ? 'ON' : 'OFF'}
+
+ {autoValidate
+ ? 'Issues are validated automatically when loaded'
+ : 'Click validate button for each issue'}
+
+
+
+
+
+ {/* Import button */}
+
+
+
+
+
+
+ Import issues as tasks
+
+
+
+
+ {/* Refresh button */}
+
+
);
}
diff --git a/apps/ui/src/components/views/github-issues-view/dialogs/import-issues-dialog.tsx b/apps/ui/src/components/views/github-issues-view/dialogs/import-issues-dialog.tsx
new file mode 100644
index 000000000..a334a5908
--- /dev/null
+++ b/apps/ui/src/components/views/github-issues-view/dialogs/import-issues-dialog.tsx
@@ -0,0 +1,281 @@
+import { useState, useMemo } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Badge } from '@/components/ui/badge';
+import { CheckCircle2, XCircle, AlertCircle, Import, Loader2 } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import type { GitHubIssue, StoredValidation, IssueValidationVerdict } from '@/lib/electron';
+
+interface ImportIssuesDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ issues: GitHubIssue[];
+ cachedValidations: Map;
+ validatingIssues: Set;
+ onImport: (issues: GitHubIssue[]) => Promise;
+}
+
+const verdictConfig: Record<
+ IssueValidationVerdict,
+ { label: string; color: string; bgColor: string; icon: typeof CheckCircle2 }
+> = {
+ valid: {
+ label: 'Valid',
+ color: 'text-green-500',
+ bgColor: 'bg-green-500/10',
+ icon: CheckCircle2,
+ },
+ invalid: {
+ label: 'Invalid',
+ color: 'text-red-500',
+ bgColor: 'bg-red-500/10',
+ icon: XCircle,
+ },
+ needs_clarification: {
+ label: 'Needs Clarification',
+ color: 'text-yellow-500',
+ bgColor: 'bg-yellow-500/10',
+ icon: AlertCircle,
+ },
+};
+
+export function ImportIssuesDialog({
+ open,
+ onOpenChange,
+ issues,
+ cachedValidations,
+ validatingIssues,
+ onImport,
+}: ImportIssuesDialogProps) {
+ const [selectedIssues, setSelectedIssues] = useState>(new Set());
+ const [importing, setImporting] = useState(false);
+
+ // Filter issues that can be imported (have valid validation)
+ const importableIssues = useMemo(() => {
+ return issues.filter((issue) => {
+ const validation = cachedValidations.get(issue.number);
+ return validation?.result.verdict === 'valid';
+ });
+ }, [issues, cachedValidations]);
+
+ // Count by status
+ const statusCounts = useMemo(() => {
+ let valid = 0;
+ let invalid = 0;
+ let needsClarification = 0;
+ let notValidated = 0;
+
+ for (const issue of issues) {
+ const validation = cachedValidations.get(issue.number);
+ if (!validation) {
+ notValidated++;
+ } else {
+ switch (validation.result.verdict) {
+ case 'valid':
+ valid++;
+ break;
+ case 'invalid':
+ invalid++;
+ break;
+ case 'needs_clarification':
+ needsClarification++;
+ break;
+ }
+ }
+ }
+
+ return { valid, invalid, needsClarification, notValidated };
+ }, [issues, cachedValidations]);
+
+ const handleSelectAll = () => {
+ if (selectedIssues.size === importableIssues.length) {
+ setSelectedIssues(new Set());
+ } else {
+ setSelectedIssues(new Set(importableIssues.map((i) => i.number)));
+ }
+ };
+
+ const handleToggleIssue = (issueNumber: number) => {
+ const newSelected = new Set(selectedIssues);
+ if (newSelected.has(issueNumber)) {
+ newSelected.delete(issueNumber);
+ } else {
+ newSelected.add(issueNumber);
+ }
+ setSelectedIssues(newSelected);
+ };
+
+ const handleImport = async () => {
+ const issuesToImport = issues.filter((i) => selectedIssues.has(i.number));
+ if (issuesToImport.length === 0) return;
+
+ setImporting(true);
+ try {
+ await onImport(issuesToImport);
+ setSelectedIssues(new Set());
+ onOpenChange(false);
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ // Reset selection when dialog opens
+ const handleOpenChange = (newOpen: boolean) => {
+ if (!newOpen) {
+ setSelectedIssues(new Set());
+ }
+ onOpenChange(newOpen);
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/ui/src/components/views/github-issues-view/dialogs/index.ts b/apps/ui/src/components/views/github-issues-view/dialogs/index.ts
index 886b09b23..4c2bf73eb 100644
--- a/apps/ui/src/components/views/github-issues-view/dialogs/index.ts
+++ b/apps/ui/src/components/views/github-issues-view/dialogs/index.ts
@@ -1 +1,2 @@
export { ValidationDialog } from './validation-dialog';
+export { ImportIssuesDialog } from './import-issues-dialog';
diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts
index 78d6e65cf..34604123d 100644
--- a/apps/ui/src/store/app-store.ts
+++ b/apps/ui/src/store/app-store.ts
@@ -557,6 +557,9 @@ export interface AppState {
// Validation Model Settings
validationModel: ModelAlias; // Model used for GitHub issue validation (default: opus)
+ // GitHub Auto-Validate Settings
+ githubAutoValidate: boolean; // When true, automatically validate issues when viewing GitHub Issues tab
+
// Phase Model Settings - per-phase AI model configuration
phaseModels: PhaseModelConfig;
favoriteModels: string[];
@@ -924,6 +927,9 @@ export interface AppActions {
// Validation Model actions
setValidationModel: (model: ModelAlias) => void;
+ // GitHub Auto-Validate actions
+ setGithubAutoValidate: (enabled: boolean) => void;
+
// Phase Model actions
setPhaseModel: (phase: PhaseModelKey, entry: PhaseModelEntry) => Promise;
setPhaseModels: (models: Partial) => Promise;
@@ -1180,6 +1186,7 @@ const initialState: AppState = {
muteDoneSound: false, // Default to sound enabled (not muted)
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
validationModel: 'opus', // Default to opus for GitHub issue validation
+ githubAutoValidate: false, // Default to disabled (manual validation only)
phaseModels: DEFAULT_PHASE_MODELS, // Phase-specific model configuration
favoriteModels: [],
enabledCursorModels: getAllCursorModelIds(), // All Cursor models enabled by default
@@ -1843,6 +1850,9 @@ export const useAppStore = create()((set, get) => ({
// Validation Model actions
setValidationModel: (model) => set({ validationModel: model }),
+ // GitHub Auto-Validate actions
+ setGithubAutoValidate: (enabled) => set({ githubAutoValidate: enabled }),
+
// Phase Model actions
setPhaseModel: async (phase, entry) => {
set((state) => ({