diff --git a/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx index d412444f..ca2b1759 100644 --- a/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx @@ -23,18 +23,17 @@ import { Copy, RefreshCw, Download, - Info, - ShieldCheck, XCircle, Trash2, AlertTriangle, Terminal, + AlertCircle, } from 'lucide-react'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon'; -import { StatusBadge, TerminalOutput } from '../components'; -import { useCliStatus, useCliInstallation, useTokenSave } from '../hooks'; +import { TerminalOutput } from '../components'; +import { useCliInstallation, useTokenSave } from '../hooks'; interface ProvidersSetupStepProps { onNext: () => void; @@ -42,7 +41,6 @@ interface ProvidersSetupStepProps { } type ProviderTab = 'claude' | 'cursor' | 'codex' | 'opencode'; -type VerificationStatus = 'idle' | 'verifying' | 'verified' | 'error'; // ============================================================================ // Claude Content @@ -54,35 +52,104 @@ function ClaudeContent() { setClaudeCliStatus, setClaudeAuthStatus, setClaudeInstallProgress, + setClaudeIsVerifying, } = useSetupStore(); const { setApiKeys, apiKeys } = useAppStore(); const [apiKey, setApiKey] = useState(''); - const [cliVerificationStatus, setCliVerificationStatus] = useState('idle'); - const [cliVerificationError, setCliVerificationError] = useState(null); - const [apiKeyVerificationStatus, setApiKeyVerificationStatus] = - useState('idle'); - const [apiKeyVerificationError, setApiKeyVerificationError] = useState(null); + 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 statusApi = useCallback( - () => getElectronAPI().setup?.getClaudeStatus() || Promise.reject(), - [] - ); const installApi = useCallback( () => getElectronAPI().setup?.installClaude() || Promise.reject(), [] ); const getStoreState = useCallback(() => useSetupStore.getState().claudeCliStatus, []); - const { isChecking, checkStatus } = useCliStatus({ - cliType: 'claude', - statusApi, - setCliStatus: setClaudeCliStatus, - setAuthStatus: setClaudeAuthStatus, - }); + // Auto-verify CLI authentication + const verifyAuth = useCallback(async () => { + // Guard against duplicate verification + if (hasVerifiedRef.current) { + return; + } - const onInstallSuccess = useCallback(() => checkStatus(), [checkStatus]); + 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', @@ -106,74 +173,6 @@ function ClaudeContent() { }, }); - const verifyCliAuth = useCallback(async () => { - setCliVerificationStatus('verifying'); - setCliVerificationError(null); - try { - const api = getElectronAPI(); - if (!api.setup?.verifyClaudeAuth) { - setCliVerificationStatus('error'); - setCliVerificationError('Verification API not available'); - 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) { - setCliVerificationStatus('verified'); - setClaudeAuthStatus({ - authenticated: true, - method: 'cli_authenticated', - hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false, - }); - toast.success('Claude CLI authentication verified!'); - } else { - setCliVerificationStatus('error'); - setCliVerificationError( - hasLimitReachedError - ? 'Rate limit reached. Please try again later.' - : result.error || 'Authentication failed' - ); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Verification failed'; - setCliVerificationStatus('error'); - setCliVerificationError(errorMessage); - } - }, [claudeAuthStatus, setClaudeAuthStatus]); - - const verifyApiKeyAuth = useCallback(async () => { - setApiKeyVerificationStatus('verifying'); - setApiKeyVerificationError(null); - try { - const api = getElectronAPI(); - if (!api.setup?.verifyClaudeAuth) { - setApiKeyVerificationStatus('error'); - setApiKeyVerificationError('Verification API not available'); - return; - } - const result = await api.setup.verifyClaudeAuth('api_key'); - if (result.authenticated) { - setApiKeyVerificationStatus('verified'); - setClaudeAuthStatus({ - authenticated: true, - method: 'api_key', - hasCredentialsFile: false, - apiKeyValid: true, - }); - toast.success('API key authentication verified!'); - } else { - setApiKeyVerificationStatus('error'); - setApiKeyVerificationError(result.error || 'Authentication failed'); - } - } catch (error) { - setApiKeyVerificationStatus('error'); - setApiKeyVerificationError(error instanceof Error ? error.message : 'Verification failed'); - } - }, [setClaudeAuthStatus]); - const deleteApiKey = useCallback(async () => { setIsDeletingApiKey(true); try { @@ -186,12 +185,15 @@ function ClaudeContent() { if (result.success) { setApiKey(''); setApiKeys({ ...apiKeys, anthropic: '' }); - setApiKeyVerificationStatus('idle'); + // Use getState() to avoid dependency on claudeAuthStatus + const currentAuthStatus = useSetupStore.getState().claudeAuthStatus; setClaudeAuthStatus({ authenticated: false, method: 'none', - hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false, + hasCredentialsFile: currentAuthStatus?.hasCredentialsFile || false, }); + // Reset verification guard so next check can verify again + hasVerifiedRef.current = false; toast.success('API key deleted successfully'); } } catch { @@ -199,7 +201,7 @@ function ClaudeContent() { } finally { setIsDeletingApiKey(false); } - }, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]); + }, [apiKeys, setApiKeys, setClaudeAuthStatus]); useEffect(() => { setClaudeInstallProgress({ isInstalling, output: installProgress.output }); @@ -219,263 +221,228 @@ function ClaudeContent() { 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 (
- - Authentication Methods + + Claude CLI Status -
- Choose one of the following methods to authenticate with Claude: + {claudeCliStatus?.installed + ? claudeAuthStatus?.authenticated + ? `Authenticated${claudeCliStatus.version ? ` (v${claudeCliStatus.version})` : ''}` + : isVerifying + ? 'Verifying authentication...' + : 'Installed but not authenticated' + : 'Not installed on your system'}
- - - {/* CLI Option */} - - -
-
- -
-

Claude CLI

-

Use Claude Code subscription

-
-
- + + {/* Success State - CLI Ready */} + {isReady && ( +
+
+ +
+

CLI Installed

+

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

- - - {!claudeCliStatus?.installed && ( -
-
- -

Install Claude CLI

-
-
- -
- - curl -fsSL https://claude.ai/install.sh | bash - - -
-
- {isInstalling && } +
+
+ +

+ {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 +
- )} - - {cliVerificationStatus === 'verified' && ( -
- -

CLI Authentication verified!

-
- )} - - {cliVerificationStatus === 'error' && cliVerificationError && ( -
- -
-

Verification failed

-

{cliVerificationError}

-
-
- )} - - {cliVerificationStatus !== 'verified' && ( - - )} - - - - {/* API Key Option */} - - -
-
- -
-

Anthropic API Key

-

- Pay-per-use with your own API key -

-
-
-
-
- -
-
- - setApiKey(e.target.value)} - className="bg-input border-border text-foreground" - /> -

- Don't have an API key?{' '} - - Get one from Anthropic Console - - + {isInstalling && } + +

+
+ )} + + {/* Installed but not authenticated */} + {claudeCliStatus?.installed && + !claudeAuthStatus?.authenticated && + !isChecking && + !isVerifying && ( +
+ {/* Show CLI installed toast */} +
+ +
+

CLI Installed

+

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

-
- - {hasApiKey && ( - - )} -
- {apiKeyVerificationStatus === 'verified' && ( -
- -

API Key verified!

-
- )} - - {apiKeyVerificationStatus === 'error' && apiKeyVerificationError && ( + {/* Error state */} + {verificationError && (
-

Verification failed

-

{apiKeyVerificationError}

+

Authentication failed

+

{verificationError}

)} - {apiKeyVerificationStatus !== 'verified' && ( - - )} - - - + {/* 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 && ( + + )} +
+
+
+
+
+ )} ); @@ -599,9 +566,20 @@ function CursorContent() { {isReady && ( -
- -

Cursor CLI is ready!

+
+
+ +
+

CLI Installed

+

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

+
+
+
+ +

Authenticated

+
)} @@ -640,6 +618,17 @@ function CursorContent() { {cursorCliStatus?.installed && !cursorCliStatus?.auth?.authenticated && !isChecking && (
+ {/* Show CLI installed toast */} +
+ +
+

CLI Installed

+

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

+
+
+
@@ -715,6 +704,7 @@ function CodexContent() { installed: result.installed ?? false, version: result.version, path: result.path, + method: 'none', }); if (result.auth?.authenticated) { setCodexAuthStatus({ @@ -830,9 +820,22 @@ function CodexContent() { {isReady && ( -
- -

Codex CLI is ready!

+
+
+ +
+

CLI Installed

+

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

+
+
+
+ +

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

+
)} @@ -866,78 +869,101 @@ function CodexContent() { )} {codexCliStatus?.installed && !codexAuthStatus?.authenticated && !isChecking && ( - - - -
- - Codex CLI Login -
-
- -
- - codex login - - -
- -
-
+
+ {/* Show CLI installed toast */} +
+ +
+

CLI Installed

+

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

+
+
- - -
- - OpenAI API Key -
-
- -
- setApiKey(e.target.value)} - className="bg-input border-border text-foreground" - /> -

- - Get an API key from OpenAI - - -

-
- -
-
- +
+ +
+

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 && ( @@ -1069,9 +1095,20 @@ function OpencodeContent() { {isReady && ( -
- -

OpenCode CLI is ready!

+
+
+ +
+

CLI Installed

+

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

+
+
+
+ +

Authenticated

+
)} @@ -1112,6 +1149,17 @@ function OpencodeContent() { {opencodeCliStatus?.installed && !opencodeCliStatus?.auth?.authenticated && !isChecking && (
+ {/* Show CLI installed toast */} +
+ +
+

CLI Installed

+

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

+
+
+
@@ -1170,54 +1218,215 @@ function OpencodeContent() { // ============================================================================ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps) { const [activeTab, setActiveTab] = useState('claude'); + const [isInitialChecking, setIsInitialChecking] = useState(true); + const hasCheckedRef = useRef(false); - const { claudeAuthStatus, cursorCliStatus, codexAuthStatus, opencodeCliStatus } = useSetupStore(); + const { + claudeCliStatus, + claudeAuthStatus, + claudeIsVerifying, + cursorCliStatus, + codexCliStatus, + codexAuthStatus, + opencodeCliStatus, + setClaudeCliStatus, + setCursorCliStatus, + setCodexCliStatus, + setCodexAuthStatus, + setOpencodeCliStatus, + } = useSetupStore(); - const isClaudeConfigured = + // 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 + } + }; + + // Run all checks in parallel + await Promise.all([checkClaude(), checkCursor(), checkCodex(), checkOpencode()]); + setIsInitialChecking(false); + }, [ + setClaudeCliStatus, + setCursorCliStatus, + setCodexCliStatus, + setCodexAuthStatus, + setOpencodeCliStatus, + ]); + + 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 isCursorConfigured = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated; - const isCodexConfigured = codexAuthStatus?.authenticated === true; - const isOpencodeConfigured = - opencodeCliStatus?.installed && opencodeCliStatus?.auth?.authenticated; + 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 hasAtLeastOneProvider = - isClaudeConfigured || isCursorConfigured || isCodexConfigured || isOpencodeConfigured; + isClaudeAuthenticated || + isCursorAuthenticated || + isCodexAuthenticated || + isOpencodeAuthenticated; + + 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, - configured: isClaudeConfigured, + status: getProviderStatus(isClaudeInstalled, isClaudeAuthenticated, claudeIsVerifying), color: 'text-brand-500', }, { id: 'cursor' as const, label: 'Cursor', icon: CursorIcon, - configured: isCursorConfigured, + status: getProviderStatus(isCursorInstalled, isCursorAuthenticated), color: 'text-blue-500', }, { id: 'codex' as const, label: 'Codex', icon: OpenAIIcon, - configured: isCodexConfigured, + status: getProviderStatus(isCodexInstalled, isCodexAuthenticated), color: 'text-emerald-500', }, { id: 'opencode' as const, label: 'OpenCode', icon: OpenCodeIcon, - configured: isOpencodeConfigured, + status: getProviderStatus(isOpencodeInstalled, isOpencodeAuthenticated), color: 'text-green-500', }, ]; + const renderStatusIcon = (status: ProviderStatus) => { + switch (status) { + case 'authenticated': + return ( + + ); + case 'verifying': + return ( + + ); + case 'installed_not_auth': + return ( + + ); + default: + return null; + } + }; + return (
@@ -1225,6 +1434,13 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps)

Configure at least one AI provider to continue

+ {isInitialChecking && ( +
+ +

Checking provider status...

+
+ )} + setActiveTab(v as ProviderTab)}> {providers.map((provider) => { @@ -1242,12 +1458,16 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps) - {provider.configured && ( - - )} + {!isInitialChecking && renderStatusIcon(provider.status)}
{provider.label} diff --git a/apps/ui/src/store/setup-store.ts b/apps/ui/src/store/setup-store.ts index 386896ee..6b872819 100644 --- a/apps/ui/src/store/setup-store.ts +++ b/apps/ui/src/store/setup-store.ts @@ -132,6 +132,7 @@ export interface SetupState { claudeCliStatus: CliStatus | null; claudeAuthStatus: ClaudeAuthStatus | null; claudeInstallProgress: InstallProgress; + claudeIsVerifying: boolean; // GitHub CLI state ghCliStatus: GhCliStatus | null; @@ -164,6 +165,7 @@ export interface SetupActions { setClaudeAuthStatus: (status: ClaudeAuthStatus | null) => void; setClaudeInstallProgress: (progress: Partial) => void; resetClaudeInstallProgress: () => void; + setClaudeIsVerifying: (isVerifying: boolean) => void; // GitHub CLI setGhCliStatus: (status: GhCliStatus | null) => void; @@ -202,6 +204,7 @@ const initialState: SetupState = { claudeCliStatus: null, claudeAuthStatus: null, claudeInstallProgress: { ...initialInstallProgress }, + claudeIsVerifying: false, ghCliStatus: null, cursorCliStatus: null, @@ -255,6 +258,8 @@ export const useSetupStore = create()((set, get) => ( claudeInstallProgress: { ...initialInstallProgress }, }), + setClaudeIsVerifying: (isVerifying) => set({ claudeIsVerifying: isVerifying }), + // GitHub CLI setGhCliStatus: (status) => set({ ghCliStatus: status }),