import { useState, useEffect, useCallback, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion'; import { useSetupStore } from '@/store/setup-store'; import { useAppStore } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { ArrowRight, ArrowLeft, CheckCircle2, Key, ExternalLink, Copy, RefreshCw, Download, XCircle, Trash2, AlertTriangle, Terminal, AlertCircle, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon, GeminiIcon, } from '@/components/ui/provider-icon'; import { TerminalOutput } from '../components'; import { useCliInstallation, useTokenSave } from '../hooks'; interface ProvidersSetupStepProps { onNext: () => void; onBack: () => void; } type ProviderTab = 'claude' | 'cursor' | 'codex' | 'opencode' | 'gemini'; // ============================================================================ // Claude Content // ============================================================================ function ClaudeContent() { const { claudeCliStatus, claudeAuthStatus, setClaudeCliStatus, setClaudeAuthStatus, setClaudeInstallProgress, setClaudeIsVerifying, } = useSetupStore(); const { setApiKeys, apiKeys } = useAppStore(); const [apiKey, setApiKey] = useState(''); const [isChecking, setIsChecking] = useState(false); const [isVerifying, setIsVerifying] = useState(false); const [verificationError, setVerificationError] = useState(null); const [isDeletingApiKey, setIsDeletingApiKey] = useState(false); const hasVerifiedRef = useRef(false); const installApi = useCallback( () => getElectronAPI().setup?.installClaude() || Promise.reject(), [] ); const getStoreState = useCallback(() => useSetupStore.getState().claudeCliStatus, []); // Auto-verify CLI authentication const verifyAuth = useCallback(async () => { // Guard against duplicate verification if (hasVerifiedRef.current) { return; } setIsVerifying(true); setClaudeIsVerifying(true); // Update store for parent to see setVerificationError(null); try { const api = getElectronAPI(); if (!api.setup?.verifyClaudeAuth) { return; } const result = await api.setup.verifyClaudeAuth('cli'); const hasLimitReachedError = result.error?.toLowerCase().includes('limit reached') || result.error?.toLowerCase().includes('rate limit'); if (result.authenticated && !hasLimitReachedError) { hasVerifiedRef.current = true; // Use getState() to avoid dependency on claudeAuthStatus const currentAuthStatus = useSetupStore.getState().claudeAuthStatus; setClaudeAuthStatus({ authenticated: true, method: 'cli_authenticated', hasCredentialsFile: currentAuthStatus?.hasCredentialsFile || false, }); toast.success('Claude CLI authenticated!'); } else if (hasLimitReachedError) { setVerificationError('Rate limit reached. Please try again later.'); } else if (result.error) { setVerificationError(result.error); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Verification failed'; setVerificationError(errorMessage); } finally { setIsVerifying(false); setClaudeIsVerifying(false); // Update store when done } }, [setClaudeAuthStatus, setClaudeIsVerifying]); // Check status and auto-verify const checkStatus = useCallback(async () => { setIsChecking(true); setVerificationError(null); // Reset verification guard to allow fresh verification (for manual refresh) hasVerifiedRef.current = false; try { const api = getElectronAPI(); if (!api.setup?.getClaudeStatus) return; const result = await api.setup.getClaudeStatus(); if (result.success) { setClaudeCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, method: 'none', }); if (result.installed) { toast.success('Claude CLI installed!'); // Auto-verify if CLI is installed setIsChecking(false); await verifyAuth(); return; } } } catch { // Ignore errors } finally { setIsChecking(false); } }, [setClaudeCliStatus, verifyAuth]); const onInstallSuccess = useCallback(() => { hasVerifiedRef.current = false; checkStatus(); }, [checkStatus]); const { isInstalling, installProgress, install } = useCliInstallation({ cliType: 'claude', installApi, onProgressEvent: getElectronAPI().setup?.onInstallProgress, onSuccess: onInstallSuccess, getStoreState, }); const { isSaving: isSavingApiKey, saveToken: saveApiKeyToken } = useTokenSave({ provider: 'anthropic', onSuccess: () => { setClaudeAuthStatus({ authenticated: true, method: 'api_key', hasCredentialsFile: false, apiKeyValid: true, }); setApiKeys({ ...apiKeys, anthropic: apiKey }); toast.success('API key saved successfully!'); }, }); const deleteApiKey = useCallback(async () => { setIsDeletingApiKey(true); try { const api = getElectronAPI(); if (!api.setup?.deleteApiKey) { toast.error('Delete API not available'); return; } const result = await api.setup.deleteApiKey('anthropic'); if (result.success) { setApiKey(''); setApiKeys({ ...apiKeys, anthropic: '' }); // Use getState() to avoid dependency on claudeAuthStatus const currentAuthStatus = useSetupStore.getState().claudeAuthStatus; setClaudeAuthStatus({ authenticated: false, method: 'none', hasCredentialsFile: currentAuthStatus?.hasCredentialsFile || false, }); // Reset verification guard so next check can verify again hasVerifiedRef.current = false; toast.success('API key deleted successfully'); } } catch { toast.error('Failed to delete API key'); } finally { setIsDeletingApiKey(false); } }, [apiKeys, setApiKeys, setClaudeAuthStatus]); useEffect(() => { setClaudeInstallProgress({ isInstalling, output: installProgress.output }); }, [isInstalling, installProgress, setClaudeInstallProgress]); useEffect(() => { checkStatus(); }, [checkStatus]); const copyCommand = (command: string) => { navigator.clipboard.writeText(command); toast.success('Command copied to clipboard'); }; const hasApiKey = !!apiKeys.anthropic || claudeAuthStatus?.method === 'api_key' || claudeAuthStatus?.method === 'api_key_env'; const isCliAuthenticated = claudeAuthStatus?.method === 'cli_authenticated'; const isApiKeyAuthenticated = claudeAuthStatus?.method === 'api_key' || claudeAuthStatus?.method === 'api_key_env'; const isReady = claudeCliStatus?.installed && claudeAuthStatus?.authenticated; return (
Claude CLI Status
{claudeCliStatus?.installed ? claudeAuthStatus?.authenticated ? `Authenticated${claudeCliStatus.version ? ` (v${claudeCliStatus.version})` : ''}` : isVerifying ? 'Verifying authentication...' : 'Installed but not authenticated' : 'Not installed on your system'}
{/* Success State - CLI Ready */} {isReady && (

CLI Installed

{claudeCliStatus?.version && `Version: ${claudeCliStatus.version}`}

{isCliAuthenticated ? 'CLI Authenticated' : 'API Key Configured'}

)} {/* Checking/Verifying State */} {(isChecking || isVerifying) && (

{isChecking ? 'Checking Claude CLI status...' : 'Verifying authentication...'}

)} {/* Not Installed */} {!claudeCliStatus?.installed && !isChecking && !isVerifying && (

Claude CLI not found

Install Claude CLI to use Claude Code subscription.

Install Claude CLI:

curl -fsSL https://claude.ai/install.sh | bash
{isInstalling && }
)} {/* Installed but not authenticated */} {claudeCliStatus?.installed && !claudeAuthStatus?.authenticated && !isChecking && !isVerifying && (
{/* Show CLI installed toast */}

CLI Installed

{claudeCliStatus?.version && `Version: ${claudeCliStatus.version}`}

{/* Error state */} {verificationError && (

Authentication failed

{verificationError}

)} {/* Not authenticated warning */}

Claude CLI not authenticated

Run claude login in your terminal or provide an API key below.

{/* API Key alternative */}
Use Anthropic API Key instead
setApiKey(e.target.value)} className="bg-input border-border text-foreground" />

Don't have an API key?{' '} Get one from Anthropic Console

{hasApiKey && ( )}
)}
); } // ============================================================================ // Cursor Content // ============================================================================ function CursorContent() { const { cursorCliStatus, setCursorCliStatus } = useSetupStore(); const [isChecking, setIsChecking] = useState(false); const [isLoggingIn, setIsLoggingIn] = useState(false); const pollIntervalRef = useRef(null); const checkStatus = useCallback(async () => { setIsChecking(true); try { const api = getElectronAPI(); if (!api.setup?.getCursorStatus) return; const result = await api.setup.getCursorStatus(); if (result.success) { setCursorCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, auth: result.auth, installCommand: result.installCommand, loginCommand: result.loginCommand, }); if (result.auth?.authenticated) { toast.success('Cursor CLI is ready!'); } } } catch { // Ignore errors } finally { setIsChecking(false); } }, [setCursorCliStatus]); useEffect(() => { checkStatus(); return () => { if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }; }, [checkStatus]); const copyCommand = (command: string) => { navigator.clipboard.writeText(command); toast.success('Command copied to clipboard'); }; const handleLogin = async () => { setIsLoggingIn(true); try { const loginCommand = cursorCliStatus?.loginCommand || 'cursor-agent login'; await navigator.clipboard.writeText(loginCommand); toast.info('Login command copied! Paste in terminal to authenticate.'); let attempts = 0; pollIntervalRef.current = setInterval(async () => { attempts++; try { const api = getElectronAPI(); if (!api.setup?.getCursorStatus) return; const result = await api.setup.getCursorStatus(); if (result.auth?.authenticated) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setCursorCliStatus({ ...cursorCliStatus, installed: result.installed ?? true, version: result.version, path: result.path, auth: result.auth, }); setIsLoggingIn(false); toast.success('Successfully logged in to Cursor!'); } } catch { // Ignore } if (attempts >= 60) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setIsLoggingIn(false); toast.error('Login timed out. Please try again.'); } }, 2000); } catch { toast.error('Failed to start login process'); setIsLoggingIn(false); } }; const isReady = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated; return (
Cursor CLI Status
{cursorCliStatus?.installed ? cursorCliStatus.auth?.authenticated ? `Authenticated${cursorCliStatus.version ? ` (v${cursorCliStatus.version})` : ''}` : 'Installed but not authenticated' : 'Not installed on your system'}
{isReady && (

CLI Installed

{cursorCliStatus?.version && `Version: ${cursorCliStatus.version}`}

Authenticated

)} {!cursorCliStatus?.installed && !isChecking && (

Cursor CLI not found

Install Cursor IDE to use Cursor AI agent.

Install Cursor:

{cursorCliStatus?.installCommand || 'npm install -g @anthropic/cursor-agent'}
)} {cursorCliStatus?.installed && !cursorCliStatus?.auth?.authenticated && !isChecking && (
{/* Show CLI installed toast */}

CLI Installed

{cursorCliStatus?.version && `Version: ${cursorCliStatus.version}`}

Cursor CLI not authenticated

Run the login command to authenticate.

{cursorCliStatus?.loginCommand || 'cursor-agent login'}
)} {isChecking && (

Checking Cursor CLI status...

)}
); } // ============================================================================ // Codex Content // ============================================================================ function CodexContent() { const { codexCliStatus, codexAuthStatus, setCodexCliStatus, setCodexAuthStatus } = useSetupStore(); const { setApiKeys, apiKeys } = useAppStore(); const [isChecking, setIsChecking] = useState(false); const [apiKey, setApiKey] = useState(''); const [isSaving, setIsSaving] = useState(false); const [isLoggingIn, setIsLoggingIn] = useState(false); const pollIntervalRef = useRef(null); const checkStatus = useCallback(async () => { setIsChecking(true); try { const api = getElectronAPI(); if (!api.setup?.getCodexStatus) return; const result = await api.setup.getCodexStatus(); if (result.success) { setCodexCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, method: 'none', }); if (result.auth?.authenticated) { setCodexAuthStatus({ authenticated: true, method: result.auth.method || 'cli_authenticated', }); toast.success('Codex CLI is ready!'); } } } catch { // Ignore } finally { setIsChecking(false); } }, [setCodexCliStatus, setCodexAuthStatus]); useEffect(() => { checkStatus(); return () => { if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }; }, [checkStatus]); const copyCommand = (command: string) => { navigator.clipboard.writeText(command); toast.success('Command copied to clipboard'); }; const handleSaveApiKey = async () => { if (!apiKey.trim()) return; setIsSaving(true); try { const api = getElectronAPI(); if (!api.setup?.saveApiKey) { toast.error('Save API not available'); return; } const result = await api.setup.saveApiKey('openai', apiKey); if (result.success) { setApiKeys({ ...apiKeys, openai: apiKey }); setCodexAuthStatus({ authenticated: true, method: 'api_key' }); toast.success('API key saved successfully!'); } } catch { toast.error('Failed to save API key'); } finally { setIsSaving(false); } }; const handleLogin = async () => { setIsLoggingIn(true); try { await navigator.clipboard.writeText('codex login'); toast.info('Login command copied! Paste in terminal to authenticate.'); let attempts = 0; pollIntervalRef.current = setInterval(async () => { attempts++; try { const api = getElectronAPI(); if (!api.setup?.getCodexStatus) return; const result = await api.setup.getCodexStatus(); if (result.auth?.authenticated) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setCodexAuthStatus({ authenticated: true, method: 'cli_authenticated' }); setIsLoggingIn(false); toast.success('Successfully logged in to Codex!'); } } catch { // Ignore } if (attempts >= 60) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setIsLoggingIn(false); toast.error('Login timed out. Please try again.'); } }, 2000); } catch { toast.error('Failed to start login process'); setIsLoggingIn(false); } }; const isReady = codexCliStatus?.installed && codexAuthStatus?.authenticated; const hasApiKey = !!apiKeys.openai || codexAuthStatus?.method === 'api_key'; return (
Codex CLI Status
{codexCliStatus?.installed ? codexAuthStatus?.authenticated ? `Authenticated${codexCliStatus.version ? ` (v${codexCliStatus.version})` : ''}` : 'Installed but not authenticated' : 'Not installed on your system'}
{isReady && (

CLI Installed

{codexCliStatus?.version && `Version: ${codexCliStatus.version}`}

{codexAuthStatus?.method === 'api_key' ? 'API Key Configured' : 'Authenticated'}

)} {!codexCliStatus?.installed && !isChecking && (

Codex CLI not found

Install the Codex CLI to use OpenAI models.

Install Codex CLI:

npm install -g @openai/codex
)} {codexCliStatus?.installed && !codexAuthStatus?.authenticated && !isChecking && (
{/* Show CLI installed toast */}

CLI Installed

{codexCliStatus?.version && `Version: ${codexCliStatus.version}`}

Codex CLI not authenticated

Run the login command or provide an API key below.

Codex CLI Login
codex login
OpenAI API Key
setApiKey(e.target.value)} className="bg-input border-border text-foreground" />

Get an API key from OpenAI

)} {isChecking && (

Checking Codex CLI status...

)}
); } // ============================================================================ // OpenCode Content // ============================================================================ function OpencodeContent() { const { opencodeCliStatus, setOpencodeCliStatus } = useSetupStore(); const [isChecking, setIsChecking] = useState(false); const [isLoggingIn, setIsLoggingIn] = useState(false); const pollIntervalRef = useRef(null); const checkStatus = useCallback(async () => { setIsChecking(true); try { const api = getElectronAPI(); if (!api.setup?.getOpencodeStatus) return; const result = await api.setup.getOpencodeStatus(); if (result.success) { setOpencodeCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, auth: result.auth, installCommand: result.installCommand, loginCommand: result.loginCommand, }); if (result.auth?.authenticated) { toast.success('OpenCode CLI is ready!'); } } } catch { // Ignore } finally { setIsChecking(false); } }, [setOpencodeCliStatus]); useEffect(() => { checkStatus(); return () => { if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }; }, [checkStatus]); const copyCommand = (command: string) => { navigator.clipboard.writeText(command); toast.success('Command copied to clipboard'); }; const handleLogin = async () => { setIsLoggingIn(true); try { const loginCommand = opencodeCliStatus?.loginCommand || 'opencode auth login'; await navigator.clipboard.writeText(loginCommand); toast.info('Login command copied! Paste in terminal to authenticate.'); let attempts = 0; pollIntervalRef.current = setInterval(async () => { attempts++; try { const api = getElectronAPI(); if (!api.setup?.getOpencodeStatus) return; const result = await api.setup.getOpencodeStatus(); if (result.auth?.authenticated) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setOpencodeCliStatus({ ...opencodeCliStatus, installed: result.installed ?? true, version: result.version, path: result.path, auth: result.auth, }); setIsLoggingIn(false); toast.success('Successfully logged in to OpenCode!'); } } catch { // Ignore } if (attempts >= 60) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setIsLoggingIn(false); toast.error('Login timed out. Please try again.'); } }, 2000); } catch { toast.error('Failed to start login process'); setIsLoggingIn(false); } }; const isReady = opencodeCliStatus?.installed && opencodeCliStatus?.auth?.authenticated; return (
OpenCode CLI Status
{opencodeCliStatus?.installed ? opencodeCliStatus.auth?.authenticated ? `Authenticated${opencodeCliStatus.version ? ` (v${opencodeCliStatus.version})` : ''}` : 'Installed but not authenticated' : 'Not installed on your system'}
{isReady && (

CLI Installed

{opencodeCliStatus?.version && `Version: ${opencodeCliStatus.version}`}

Authenticated

)} {!opencodeCliStatus?.installed && !isChecking && (

OpenCode CLI not found

Install the OpenCode CLI for free tier models and connected providers.

Install OpenCode CLI:

{opencodeCliStatus?.installCommand || 'curl -fsSL https://opencode.ai/install | bash'}
)} {opencodeCliStatus?.installed && !opencodeCliStatus?.auth?.authenticated && !isChecking && (
{/* Show CLI installed toast */}

CLI Installed

{opencodeCliStatus?.version && `Version: ${opencodeCliStatus.version}`}

OpenCode CLI not authenticated

Run the login command to authenticate.

{opencodeCliStatus?.loginCommand || 'opencode auth login'}
)} {isChecking && (

Checking OpenCode CLI status...

)}
); } // ============================================================================ // Gemini Content // ============================================================================ function GeminiContent() { const { geminiCliStatus, setGeminiCliStatus } = useSetupStore(); const { setApiKeys, apiKeys } = useAppStore(); const [isChecking, setIsChecking] = useState(false); const [apiKey, setApiKey] = useState(''); const [isSaving, setIsSaving] = useState(false); const [isLoggingIn, setIsLoggingIn] = useState(false); const pollIntervalRef = useRef(null); const checkStatus = useCallback(async () => { setIsChecking(true); try { const api = getElectronAPI(); if (!api.setup?.getGeminiStatus) return; const result = await api.setup.getGeminiStatus(); if (result.success) { setGeminiCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, auth: result.auth, installCommand: result.installCommand, loginCommand: result.loginCommand, }); if (result.auth?.authenticated) { toast.success('Gemini CLI is ready!'); } } } catch { // Ignore } finally { setIsChecking(false); } }, [setGeminiCliStatus]); useEffect(() => { checkStatus(); return () => { if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }; }, [checkStatus]); const copyCommand = (command: string) => { navigator.clipboard.writeText(command); toast.success('Command copied to clipboard'); }; const handleSaveApiKey = async () => { if (!apiKey.trim()) return; setIsSaving(true); try { const api = getElectronAPI(); if (!api.setup?.saveApiKey) { toast.error('Save API not available'); return; } const result = await api.setup.saveApiKey('google', apiKey); if (result.success) { setApiKeys({ ...apiKeys, google: apiKey }); setGeminiCliStatus({ ...geminiCliStatus, installed: geminiCliStatus?.installed ?? false, auth: { authenticated: true, method: 'api_key' }, }); toast.success('API key saved successfully!'); } } catch { toast.error('Failed to save API key'); } finally { setIsSaving(false); } }; const handleLogin = async () => { setIsLoggingIn(true); try { const loginCommand = geminiCliStatus?.loginCommand || 'gemini auth login'; await navigator.clipboard.writeText(loginCommand); toast.info('Login command copied! Paste in terminal to authenticate.'); let attempts = 0; pollIntervalRef.current = setInterval(async () => { attempts++; try { const api = getElectronAPI(); if (!api.setup?.getGeminiStatus) return; const result = await api.setup.getGeminiStatus(); if (result.auth?.authenticated) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setGeminiCliStatus({ ...geminiCliStatus, installed: result.installed ?? true, version: result.version, path: result.path, auth: result.auth, }); setIsLoggingIn(false); toast.success('Successfully logged in to Gemini!'); } } catch { // Ignore } if (attempts >= 60) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setIsLoggingIn(false); toast.error('Login timed out. Please try again.'); } }, 2000); } catch { toast.error('Failed to start login process'); setIsLoggingIn(false); } }; const isReady = geminiCliStatus?.installed && geminiCliStatus?.auth?.authenticated; return (
Gemini CLI Status
{geminiCliStatus?.installed ? geminiCliStatus.auth?.authenticated ? `Authenticated${geminiCliStatus.version ? ` (v${geminiCliStatus.version})` : ''}` : 'Installed but not authenticated' : 'Not installed on your system'}
{isReady && (

CLI Installed

{geminiCliStatus?.version && `Version: ${geminiCliStatus.version}`}

Authenticated

)} {!geminiCliStatus?.installed && !isChecking && (

Gemini CLI not found

Install the Gemini CLI to use Google Gemini models.

Install Gemini CLI:

{geminiCliStatus?.installCommand || 'npm install -g @google/gemini-cli'}
)} {geminiCliStatus?.installed && !geminiCliStatus?.auth?.authenticated && !isChecking && (
{/* Show CLI installed toast */}

CLI Installed

{geminiCliStatus?.version && `Version: ${geminiCliStatus.version}`}

Gemini CLI not authenticated

Run the login command or provide a Google API key below.

Google OAuth Login
{geminiCliStatus?.loginCommand || 'gemini auth login'}
Google API Key
setApiKey(e.target.value)} className="bg-input border-border text-foreground" />

Get an API key from Google AI Studio

)} {isChecking && (

Checking Gemini CLI status...

)}
); } // ============================================================================ // Main Component // ============================================================================ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps) { const [activeTab, setActiveTab] = useState('claude'); const [isInitialChecking, setIsInitialChecking] = useState(true); const hasCheckedRef = useRef(false); const { claudeCliStatus, claudeAuthStatus, claudeIsVerifying, cursorCliStatus, codexCliStatus, codexAuthStatus, opencodeCliStatus, geminiCliStatus, setClaudeCliStatus, setCursorCliStatus, setCodexCliStatus, setCodexAuthStatus, setOpencodeCliStatus, setGeminiCliStatus, } = useSetupStore(); // Check all providers on mount const checkAllProviders = useCallback(async () => { const api = getElectronAPI(); // Check Claude - only check CLI status, let ClaudeContent handle auth verification const checkClaude = async () => { try { if (!api.setup?.getClaudeStatus) return; const result = await api.setup.getClaudeStatus(); if (result.success) { setClaudeCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, method: 'none', }); // Note: Auth verification is handled by ClaudeContent component to avoid duplicate calls } } catch { // Ignore errors } }; // Check Cursor const checkCursor = async () => { try { if (!api.setup?.getCursorStatus) return; const result = await api.setup.getCursorStatus(); if (result.success) { setCursorCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, auth: result.auth, installCommand: result.installCommand, loginCommand: result.loginCommand, }); } } catch { // Ignore errors } }; // Check Codex const checkCodex = async () => { try { if (!api.setup?.getCodexStatus) return; const result = await api.setup.getCodexStatus(); if (result.success) { setCodexCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, method: 'none', }); if (result.auth?.authenticated) { setCodexAuthStatus({ authenticated: true, method: result.auth.method || 'cli_authenticated', }); } } } catch { // Ignore errors } }; // Check OpenCode const checkOpencode = async () => { try { if (!api.setup?.getOpencodeStatus) return; const result = await api.setup.getOpencodeStatus(); if (result.success) { setOpencodeCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, auth: result.auth, installCommand: result.installCommand, loginCommand: result.loginCommand, }); } } catch { // Ignore errors } }; // Check Gemini const checkGemini = async () => { try { if (!api.setup?.getGeminiStatus) return; const result = await api.setup.getGeminiStatus(); if (result.success) { setGeminiCliStatus({ installed: result.installed ?? false, version: result.version, path: result.path, auth: result.auth, installCommand: result.installCommand, loginCommand: result.loginCommand, }); } } catch { // Ignore errors } }; // Run all checks in parallel await Promise.all([checkClaude(), checkCursor(), checkCodex(), checkOpencode(), checkGemini()]); setIsInitialChecking(false); }, [ setClaudeCliStatus, setCursorCliStatus, setCodexCliStatus, setCodexAuthStatus, setOpencodeCliStatus, setGeminiCliStatus, ]); useEffect(() => { if (!hasCheckedRef.current) { hasCheckedRef.current = true; checkAllProviders(); } }, [checkAllProviders]); // Determine status for each provider const isClaudeInstalled = claudeCliStatus?.installed === true; const isClaudeAuthenticated = claudeAuthStatus?.authenticated === true && (claudeAuthStatus?.method === 'cli_authenticated' || claudeAuthStatus?.method === 'api_key' || claudeAuthStatus?.method === 'api_key_env'); const isCursorInstalled = cursorCliStatus?.installed === true; const isCursorAuthenticated = cursorCliStatus?.auth?.authenticated === true; const isCodexInstalled = codexCliStatus?.installed === true; const isCodexAuthenticated = codexAuthStatus?.authenticated === true; const isOpencodeInstalled = opencodeCliStatus?.installed === true; const isOpencodeAuthenticated = opencodeCliStatus?.auth?.authenticated === true; const isGeminiInstalled = geminiCliStatus?.installed === true; const isGeminiAuthenticated = geminiCliStatus?.auth?.authenticated === true; const hasAtLeastOneProvider = isClaudeAuthenticated || isCursorAuthenticated || isCodexAuthenticated || isOpencodeAuthenticated || isGeminiAuthenticated; type ProviderStatus = 'not_installed' | 'installed_not_auth' | 'authenticated' | 'verifying'; const getProviderStatus = ( installed: boolean, authenticated: boolean, isVerifying?: boolean ): ProviderStatus => { if (!installed) return 'not_installed'; if (isVerifying) return 'verifying'; if (!authenticated) return 'installed_not_auth'; return 'authenticated'; }; const providers = [ { id: 'claude' as const, label: 'Claude', icon: AnthropicIcon, status: getProviderStatus(isClaudeInstalled, isClaudeAuthenticated, claudeIsVerifying), color: 'text-brand-500', }, { id: 'cursor' as const, label: 'Cursor', icon: CursorIcon, status: getProviderStatus(isCursorInstalled, isCursorAuthenticated), color: 'text-blue-500', }, { id: 'codex' as const, label: 'Codex', icon: OpenAIIcon, status: getProviderStatus(isCodexInstalled, isCodexAuthenticated), color: 'text-emerald-500', }, { id: 'opencode' as const, label: 'OpenCode', icon: OpenCodeIcon, status: getProviderStatus(isOpencodeInstalled, isOpencodeAuthenticated), color: 'text-green-500', }, { id: 'gemini' as const, label: 'Gemini', icon: GeminiIcon, status: getProviderStatus(isGeminiInstalled, isGeminiAuthenticated), color: 'text-blue-500', }, ]; const renderStatusIcon = (status: ProviderStatus) => { switch (status) { case 'authenticated': return ( ); case 'verifying': return ( ); case 'installed_not_auth': return ( ); default: return null; } }; return (

AI Provider Setup

Configure at least one AI provider to continue

{isInitialChecking && (

Checking provider status...

)} setActiveTab(v as ProviderTab)}> {providers.map((provider) => { const Icon = provider.icon; return (
{!isInitialChecking && renderStatusIcon(provider.status)}
{provider.label}
); })}
{!hasAtLeastOneProvider && (

You can configure providers later in Settings

)}
); }