From 03ccce5cc1c5c1e8713707c9a45cdbce8a2cab5d Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Fri, 19 Dec 2025 08:00:17 +0100 Subject: [PATCH] feat: add required GitHub setup flow after Auto Claude initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures users properly configure GitHub before using Auto Claude, which is necessary for the branch-based workflow to function correctly. Changes: - Add GitHubSetupModal component with 3-step flow: 1. GitHub OAuth authentication (via gh CLI) 2. Auto-detect repository from git remote 3. Select base branch for task worktrees (with recommended default) - Add IPC handlers for detectGitHubRepo and getGitHubBranches - Integrate modal into App.tsx to show after Auto Claude init - Update ElectronAPI types and browser mocks The flow now is: 1. User adds project 2. Git must be initialized (GitSetupModal if not) 3. Auto Claude initialized (creates .auto-claude folder) 4. GitHub setup required (new GitHubSetupModal) 5. Project ready for task creation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../ipc-handlers/github/oauth-handlers.ts | 89 ++++ .../src/preload/api/modules/github-api.ts | 11 + auto-claude-ui/src/renderer/App.tsx | 63 ++- .../renderer/components/GitHubSetupModal.tsx | 426 ++++++++++++++++++ .../renderer/lib/mocks/integration-mock.ts | 10 + auto-claude-ui/src/shared/constants/ipc.ts | 2 + auto-claude-ui/src/shared/types/ipc.ts | 2 + 7 files changed, 601 insertions(+), 2 deletions(-) create mode 100644 auto-claude-ui/src/renderer/components/GitHubSetupModal.tsx diff --git a/auto-claude-ui/src/main/ipc-handlers/github/oauth-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/github/oauth-handlers.ts index 2b451478..47b5e3d6 100644 --- a/auto-claude-ui/src/main/ipc-handlers/github/oauth-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/github/oauth-handlers.ts @@ -296,6 +296,93 @@ export function registerListUserRepos(): void { ); } +/** + * Detect GitHub repository from git remote origin + */ +export function registerDetectGitHubRepo(): void { + ipcMain.handle( + IPC_CHANNELS.GITHUB_DETECT_REPO, + async (_event: Electron.IpcMainInvokeEvent, projectPath: string): Promise> => { + debugLog('detectGitHubRepo handler called', { projectPath }); + try { + // Get the remote URL + debugLog('Running: git remote get-url origin'); + const remoteUrl = execSync('git remote get-url origin', { + encoding: 'utf-8', + cwd: projectPath, + stdio: 'pipe' + }).trim(); + + debugLog('Remote URL:', remoteUrl); + + // Parse GitHub repo from URL + // Formats: + // - https://github.com/owner/repo.git + // - git@github.com:owner/repo.git + // - https://github.com/owner/repo + const match = remoteUrl.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/); + if (match) { + const repo = match[1]; + debugLog('Detected repo:', repo); + return { + success: true, + data: repo + }; + } + + debugLog('Could not parse GitHub repo from URL'); + return { + success: false, + error: 'Remote URL is not a GitHub repository' + }; + } catch (error) { + debugLog('Failed to detect repo:', error instanceof Error ? error.message : error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to detect GitHub repository' + }; + } + } + ); +} + +/** + * Get branches from GitHub repository + */ +export function registerGetGitHubBranches(): void { + ipcMain.handle( + IPC_CHANNELS.GITHUB_GET_BRANCHES, + async (_event: Electron.IpcMainInvokeEvent, repo: string, _token: string): Promise> => { + debugLog('getGitHubBranches handler called', { repo }); + try { + // Use gh CLI to list branches (uses authenticated session) + debugLog(`Running: gh api repos/${repo}/branches --jq '.[].name'`); + const output = execSync( + `gh api repos/${repo}/branches --paginate --jq '.[].name'`, + { + encoding: 'utf-8', + stdio: 'pipe' + } + ); + + const branches = output.trim().split('\n').filter(b => b.length > 0); + debugLog('Found branches:', branches.length); + + return { + success: true, + data: branches + }; + } catch (error) { + debugLog('Failed to get branches:', error instanceof Error ? error.message : error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get branches' + }; + } + } + ); +} + /** * Register all GitHub OAuth handlers */ @@ -307,5 +394,7 @@ export function registerGithubOAuthHandlers(): void { registerGetGhToken(); registerGetGhUser(); registerListUserRepos(); + registerDetectGitHubRepo(); + registerGetGitHubBranches(); debugLog('GitHub OAuth handlers registered'); } diff --git a/auto-claude-ui/src/preload/api/modules/github-api.ts b/auto-claude-ui/src/preload/api/modules/github-api.ts index 6d680e49..1066f61b 100644 --- a/auto-claude-ui/src/preload/api/modules/github-api.ts +++ b/auto-claude-ui/src/preload/api/modules/github-api.ts @@ -41,6 +41,10 @@ export interface GitHubAPI { getGitHubUser: () => Promise>; listGitHubUserRepos: () => Promise }>>; + // Repository detection + detectGitHubRepo: (projectPath: string) => Promise>; + getGitHubBranches: (repo: string, token: string) => Promise>; + // Event Listeners onGitHubInvestigationProgress: ( callback: (projectId: string, status: GitHubInvestigationStatus) => void @@ -109,6 +113,13 @@ export const createGitHubAPI = (): GitHubAPI => ({ listGitHubUserRepos: (): Promise }>> => invokeIpc(IPC_CHANNELS.GITHUB_LIST_USER_REPOS), + // Repository detection + detectGitHubRepo: (projectPath: string): Promise> => + invokeIpc(IPC_CHANNELS.GITHUB_DETECT_REPO, projectPath), + + getGitHubBranches: (repo: string, token: string): Promise> => + invokeIpc(IPC_CHANNELS.GITHUB_GET_BRANCHES, repo, token), + // Event Listeners onGitHubInvestigationProgress: ( callback: (projectId: string, status: GitHubInvestigationStatus) => void diff --git a/auto-claude-ui/src/renderer/App.tsx b/auto-claude-ui/src/renderer/App.tsx index c1d0422f..850fc45e 100644 --- a/auto-claude-ui/src/renderer/App.tsx +++ b/auto-claude-ui/src/renderer/App.tsx @@ -37,6 +37,7 @@ import { OnboardingWizard } from './components/onboarding'; import { AppUpdateNotification } from './components/AppUpdateNotification'; import { UsageIndicator } from './components/UsageIndicator'; import { ProactiveSwapListener } from './components/ProactiveSwapListener'; +import { GitHubSetupModal } from './components/GitHubSetupModal'; import { useProjectStore, loadProjects, addProject, initializeProject } from './stores/project-store'; import { useTaskStore, loadTasks } from './stores/task-store'; import { useSettingsStore, loadSettings } from './stores/settings-store'; @@ -70,6 +71,10 @@ export function App() { const [isInitializing, setIsInitializing] = useState(false); const [skippedInitProjectId, setSkippedInitProjectId] = useState(null); + // GitHub setup state (shown after Auto Claude init) + const [showGitHubSetup, setShowGitHubSetup] = useState(false); + const [gitHubSetupProject, setGitHubSetupProject] = useState(null); + // Get selected project const selectedProject = projects.find((p) => p.id === selectedProjectId); @@ -243,16 +248,59 @@ export function App() { try { const result = await initializeProject(projectId); if (result?.success) { - // Clear pendingProject FIRST before closing dialog - // This prevents onOpenChange from triggering skip logic + // Get the updated project from store + const updatedProject = useProjectStore.getState().projects.find(p => p.id === projectId); + + // Clear init dialog state setPendingProject(null); setShowInitDialog(false); + + // Show GitHub setup modal + if (updatedProject) { + setGitHubSetupProject(updatedProject); + setShowGitHubSetup(true); + } } } finally { setIsInitializing(false); } }; + const handleGitHubSetupComplete = async (settings: { + githubToken: string; + githubRepo: string; + mainBranch: string; + }) => { + if (!gitHubSetupProject) return; + + try { + // Update project env config with GitHub settings + await window.electronAPI.updateProjectEnv(gitHubSetupProject.id, { + githubEnabled: true, + githubToken: settings.githubToken, + githubRepo: settings.githubRepo + }); + + // Update project settings with mainBranch + await window.electronAPI.updateProjectSettings(gitHubSetupProject.id, { + mainBranch: settings.mainBranch + }); + + // Refresh projects to get updated data + await loadProjects(); + } catch (error) { + console.error('Failed to save GitHub settings:', error); + } + + setShowGitHubSetup(false); + setGitHubSetupProject(null); + }; + + const handleGitHubSetupSkip = () => { + setShowGitHubSetup(false); + setGitHubSetupProject(null); + }; + const handleSkipInit = () => { if (pendingProject) { setSkippedInitProjectId(pendingProject.id); @@ -486,6 +534,17 @@ export function App() { + {/* GitHub Setup Modal - shows after Auto Claude init to configure GitHub */} + {gitHubSetupProject && ( + + )} + {/* Rate Limit Modal - shows when Claude Code hits usage limits (terminal) */} diff --git a/auto-claude-ui/src/renderer/components/GitHubSetupModal.tsx b/auto-claude-ui/src/renderer/components/GitHubSetupModal.tsx new file mode 100644 index 00000000..4182b378 --- /dev/null +++ b/auto-claude-ui/src/renderer/components/GitHubSetupModal.tsx @@ -0,0 +1,426 @@ +import { useState, useEffect } from 'react'; +import { + Github, + GitBranch, + Loader2, + CheckCircle2, + AlertCircle, + ChevronRight, + Sparkles +} from 'lucide-react'; +import { Button } from './ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from './ui/dialog'; +import { Label } from './ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from './ui/select'; +import { GitHubOAuthFlow } from './project-settings/GitHubOAuthFlow'; +import type { Project, ProjectSettings } from '../../shared/types'; + +interface GitHubSetupModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + project: Project; + onComplete: (settings: { githubToken: string; githubRepo: string; mainBranch: string }) => void; + onSkip?: () => void; +} + +type SetupStep = 'auth' | 'repo' | 'branch' | 'complete'; + +/** + * GitHub Setup Modal - Required setup flow after Auto Claude initialization + * + * Flow: + * 1. Authenticate with GitHub (via gh CLI OAuth) + * 2. Detect/confirm repository + * 3. Select base branch for tasks (with recommended default) + */ +export function GitHubSetupModal({ + open, + onOpenChange, + project, + onComplete, + onSkip +}: GitHubSetupModalProps) { + const [step, setStep] = useState('auth'); + const [githubToken, setGithubToken] = useState(null); + const [githubRepo, setGithubRepo] = useState(null); + const [detectedRepo, setDetectedRepo] = useState(null); + const [branches, setBranches] = useState([]); + const [selectedBranch, setSelectedBranch] = useState(null); + const [recommendedBranch, setRecommendedBranch] = useState(null); + const [isLoadingBranches, setIsLoadingBranches] = useState(false); + const [isLoadingRepo, setIsLoadingRepo] = useState(false); + const [error, setError] = useState(null); + + // Reset state when modal opens + useEffect(() => { + if (open) { + setStep('auth'); + setGithubToken(null); + setGithubRepo(null); + setDetectedRepo(null); + setBranches([]); + setSelectedBranch(null); + setRecommendedBranch(null); + setError(null); + } + }, [open]); + + // Detect repository from git remote when auth succeeds + const detectRepository = async () => { + setIsLoadingRepo(true); + setError(null); + + try { + // Try to detect repo from git remote + const result = await window.electronAPI.detectGitHubRepo(project.path); + if (result.success && result.data) { + setDetectedRepo(result.data); + setGithubRepo(result.data); + setStep('branch'); + // Immediately load branches + await loadBranches(result.data); + } else { + // No remote detected, show repo input step + setStep('repo'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to detect repository'); + setStep('repo'); + } finally { + setIsLoadingRepo(false); + } + }; + + // Load branches from GitHub + const loadBranches = async (repo: string) => { + setIsLoadingBranches(true); + setError(null); + + try { + // Get branches from GitHub API + const result = await window.electronAPI.getGitHubBranches(repo, githubToken!); + if (result.success && result.data) { + setBranches(result.data); + + // Detect recommended branch (main > master > develop > first) + const recommended = detectRecommendedBranch(result.data); + setRecommendedBranch(recommended); + setSelectedBranch(recommended); + } else { + setError(result.error || 'Failed to load branches'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load branches'); + } finally { + setIsLoadingBranches(false); + } + }; + + // Detect recommended branch from list + const detectRecommendedBranch = (branchList: string[]): string | null => { + const priorities = ['main', 'master', 'develop', 'dev']; + for (const priority of priorities) { + if (branchList.includes(priority)) { + return priority; + } + } + return branchList[0] || null; + }; + + // Handle OAuth success + const handleAuthSuccess = async (token: string) => { + setGithubToken(token); + // Move to repo detection + await detectRepository(); + }; + + // Handle branch selection complete + const handleComplete = () => { + if (githubToken && githubRepo && selectedBranch) { + onComplete({ + githubToken, + githubRepo, + mainBranch: selectedBranch + }); + } + }; + + // Render step content + const renderStepContent = () => { + switch (step) { + case 'auth': + return ( + <> + + + + Connect to GitHub + + + Auto Claude requires GitHub to manage your code branches and keep tasks up to date. + + + +
+ +
+ + ); + + case 'repo': + return ( + <> + + + + Repository Not Detected + + + We couldn't detect a GitHub repository for this project. Please ensure your project has a GitHub remote configured. + + + +
+
+
+ +
+

No GitHub remote found

+

+ To use Auto Claude, your project needs to be connected to a GitHub repository. +

+
+ git remote add origin https://github.com/owner/repo.git +
+
+
+
+ + {error && ( +
+ {error} +
+ )} +
+ + + {onSkip && ( + + )} + + + + ); + + case 'branch': + return ( + <> + + + + Select Base Branch + + + Choose which branch Auto Claude should use as the base for creating task branches. + + + +
+ {/* Show detected repo */} + {detectedRepo && ( +
+ + Repository: + + {detectedRepo} + + +
+ )} + + {/* Branch selector */} +
+ + +

+ All tasks will be created from branches like{' '} + auto-claude/task-name + {selectedBranch && ( + <> based on {selectedBranch} + )} +

+
+ + {/* Info about branch selection */} +
+
+ +
+

Why select a branch?

+

+ Auto Claude creates isolated workspaces for each task. Selecting the right base branch ensures + your tasks start with the latest code from your main development line. +

+
+
+
+ + {error && ( +
+ {error} +
+ )} +
+ + + {onSkip && ( + + )} + + + + ); + + case 'complete': + return ( + <> + + + + Setup Complete + + + +
+
+ +
+

+ Auto Claude is ready to use! You can now create tasks that will be + automatically based on {selectedBranch}. +

+
+ + ); + } + }; + + // Progress indicator + const renderProgress = () => { + const steps: { key: SetupStep; label: string }[] = [ + { key: 'auth', label: 'Connect' }, + { key: 'branch', label: 'Configure' }, + ]; + + // Don't show progress on complete step + if (step === 'complete') return null; + + const currentIndex = step === 'auth' ? 0 : step === 'repo' ? 0 : 1; + + return ( +
+ {steps.map((s, index) => ( +
+
+ {index < currentIndex ? ( + + ) : ( + index + 1 + )} +
+ + {s.label} + + {index < steps.length - 1 && ( + + )} +
+ ))} +
+ ); + }; + + return ( + + + {renderProgress()} + {renderStepContent()} + + + ); +} diff --git a/auto-claude-ui/src/renderer/lib/mocks/integration-mock.ts b/auto-claude-ui/src/renderer/lib/mocks/integration-mock.ts index 6a5a297d..fcc49111 100644 --- a/auto-claude-ui/src/renderer/lib/mocks/integration-mock.ts +++ b/auto-claude-ui/src/renderer/lib/mocks/integration-mock.ts @@ -178,5 +178,15 @@ export const integrationMock = { { fullName: 'user/private-repo', description: 'A private repository', isPrivate: true } ] } + }), + + detectGitHubRepo: async () => ({ + success: true, + data: 'user/example-repo' + }), + + getGitHubBranches: async () => ({ + success: true, + data: ['main', 'develop', 'feature/example'] }) }; diff --git a/auto-claude-ui/src/shared/constants/ipc.ts b/auto-claude-ui/src/shared/constants/ipc.ts index dfb3ebda..763152de 100644 --- a/auto-claude-ui/src/shared/constants/ipc.ts +++ b/auto-claude-ui/src/shared/constants/ipc.ts @@ -189,6 +189,8 @@ export const IPC_CHANNELS = { GITHUB_GET_TOKEN: 'github:getToken', GITHUB_GET_USER: 'github:getUser', GITHUB_LIST_USER_REPOS: 'github:listUserRepos', + GITHUB_DETECT_REPO: 'github:detectRepo', + GITHUB_GET_BRANCHES: 'github:getBranches', // GitHub events (main -> renderer) GITHUB_INVESTIGATION_PROGRESS: 'github:investigationProgress', diff --git a/auto-claude-ui/src/shared/types/ipc.ts b/auto-claude-ui/src/shared/types/ipc.ts index 072a8fca..b86f3bd4 100644 --- a/auto-claude-ui/src/shared/types/ipc.ts +++ b/auto-claude-ui/src/shared/types/ipc.ts @@ -322,6 +322,8 @@ export interface ElectronAPI { getGitHubToken: () => Promise>; getGitHubUser: () => Promise>; listGitHubUserRepos: () => Promise }>>; + detectGitHubRepo: (projectPath: string) => Promise>; + getGitHubBranches: (repo: string, token: string) => Promise>; // GitHub event listeners onGitHubInvestigationProgress: (