feat: enhance provider setup and authentication flow

- Refactored ProvidersSetupStep component to improve the UI and streamline provider status checks for Claude, Cursor, Codex, and OpenCode.
- Introduced auto-verification for CLI authentication and improved error handling for authentication states.
- Added loading indicators for provider status checks and enhanced user feedback for installation and authentication processes.
- Updated setup store to manage verification states and ensure accurate representation of provider statuses.

These changes enhance the user experience by providing clearer feedback and a more efficient setup process for AI providers.
This commit is contained in:
webdevcody
2026-01-09 11:03:01 -05:00
parent a695d0db7b
commit 4f0f56a7ba
2 changed files with 641 additions and 416 deletions

View File

@@ -23,18 +23,17 @@ import {
Copy, Copy,
RefreshCw, RefreshCw,
Download, Download,
Info,
ShieldCheck,
XCircle, XCircle,
Trash2, Trash2,
AlertTriangle, AlertTriangle,
Terminal, Terminal,
AlertCircle,
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon'; import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon';
import { StatusBadge, TerminalOutput } from '../components'; import { TerminalOutput } from '../components';
import { useCliStatus, useCliInstallation, useTokenSave } from '../hooks'; import { useCliInstallation, useTokenSave } from '../hooks';
interface ProvidersSetupStepProps { interface ProvidersSetupStepProps {
onNext: () => void; onNext: () => void;
@@ -42,7 +41,6 @@ interface ProvidersSetupStepProps {
} }
type ProviderTab = 'claude' | 'cursor' | 'codex' | 'opencode'; type ProviderTab = 'claude' | 'cursor' | 'codex' | 'opencode';
type VerificationStatus = 'idle' | 'verifying' | 'verified' | 'error';
// ============================================================================ // ============================================================================
// Claude Content // Claude Content
@@ -54,35 +52,104 @@ function ClaudeContent() {
setClaudeCliStatus, setClaudeCliStatus,
setClaudeAuthStatus, setClaudeAuthStatus,
setClaudeInstallProgress, setClaudeInstallProgress,
setClaudeIsVerifying,
} = useSetupStore(); } = useSetupStore();
const { setApiKeys, apiKeys } = useAppStore(); const { setApiKeys, apiKeys } = useAppStore();
const [apiKey, setApiKey] = useState(''); const [apiKey, setApiKey] = useState('');
const [cliVerificationStatus, setCliVerificationStatus] = useState<VerificationStatus>('idle'); const [isChecking, setIsChecking] = useState(false);
const [cliVerificationError, setCliVerificationError] = useState<string | null>(null); const [isVerifying, setIsVerifying] = useState(false);
const [apiKeyVerificationStatus, setApiKeyVerificationStatus] = const [verificationError, setVerificationError] = useState<string | null>(null);
useState<VerificationStatus>('idle');
const [apiKeyVerificationError, setApiKeyVerificationError] = useState<string | null>(null);
const [isDeletingApiKey, setIsDeletingApiKey] = useState(false); const [isDeletingApiKey, setIsDeletingApiKey] = useState(false);
const hasVerifiedRef = useRef(false);
const statusApi = useCallback(
() => getElectronAPI().setup?.getClaudeStatus() || Promise.reject(),
[]
);
const installApi = useCallback( const installApi = useCallback(
() => getElectronAPI().setup?.installClaude() || Promise.reject(), () => getElectronAPI().setup?.installClaude() || Promise.reject(),
[] []
); );
const getStoreState = useCallback(() => useSetupStore.getState().claudeCliStatus, []); const getStoreState = useCallback(() => useSetupStore.getState().claudeCliStatus, []);
const { isChecking, checkStatus } = useCliStatus({ // Auto-verify CLI authentication
cliType: 'claude', const verifyAuth = useCallback(async () => {
statusApi, // Guard against duplicate verification
setCliStatus: setClaudeCliStatus, if (hasVerifiedRef.current) {
setAuthStatus: setClaudeAuthStatus, 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',
}); });
const onInstallSuccess = useCallback(() => checkStatus(), [checkStatus]); 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({ const { isInstalling, installProgress, install } = useCliInstallation({
cliType: 'claude', 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 () => { const deleteApiKey = useCallback(async () => {
setIsDeletingApiKey(true); setIsDeletingApiKey(true);
try { try {
@@ -186,12 +185,15 @@ function ClaudeContent() {
if (result.success) { if (result.success) {
setApiKey(''); setApiKey('');
setApiKeys({ ...apiKeys, anthropic: '' }); setApiKeys({ ...apiKeys, anthropic: '' });
setApiKeyVerificationStatus('idle'); // Use getState() to avoid dependency on claudeAuthStatus
const currentAuthStatus = useSetupStore.getState().claudeAuthStatus;
setClaudeAuthStatus({ setClaudeAuthStatus({
authenticated: false, authenticated: false,
method: 'none', 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'); toast.success('API key deleted successfully');
} }
} catch { } catch {
@@ -199,7 +201,7 @@ function ClaudeContent() {
} finally { } finally {
setIsDeletingApiKey(false); setIsDeletingApiKey(false);
} }
}, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]); }, [apiKeys, setApiKeys, setClaudeAuthStatus]);
useEffect(() => { useEffect(() => {
setClaudeInstallProgress({ isInstalling, output: installProgress.output }); setClaudeInstallProgress({ isInstalling, output: installProgress.output });
@@ -219,62 +221,84 @@ function ClaudeContent() {
claudeAuthStatus?.method === 'api_key' || claudeAuthStatus?.method === 'api_key' ||
claudeAuthStatus?.method === 'api_key_env'; 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 ( return (
<Card className="bg-card border-border"> <Card className="bg-card border-border">
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<Info className="w-5 h-5" /> <AnthropicIcon className="w-5 h-5" />
Authentication Methods Claude CLI Status
</CardTitle> </CardTitle>
<Button variant="ghost" size="sm" onClick={checkStatus} disabled={isChecking}> <Button
<RefreshCw className={`w-4 h-4 ${isChecking ? 'animate-spin' : ''}`} /> variant="ghost"
size="sm"
onClick={checkStatus}
disabled={isChecking || isVerifying}
>
<RefreshCw className={`w-4 h-4 ${isChecking || isVerifying ? 'animate-spin' : ''}`} />
</Button> </Button>
</div> </div>
<CardDescription> <CardDescription>
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'}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-4">
<Accordion type="single" collapsible className="w-full"> {/* Success State - CLI Ready */}
{/* CLI Option */} {isReady && (
<AccordionItem value="cli" className="border-border"> <div className="space-y-3">
<AccordionTrigger className="hover:no-underline"> <div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<div className="flex items-center justify-between w-full pr-4"> <CheckCircle2 className="w-5 h-5 text-green-500" />
<div className="flex items-center gap-3"> <div>
<AnthropicIcon <p className="font-medium text-foreground">CLI Installed</p>
className={`w-5 h-5 ${cliVerificationStatus === 'verified' ? 'text-green-500' : 'text-muted-foreground'}`} <p className="text-sm text-muted-foreground">
/> {claudeCliStatus?.version && `Version: ${claudeCliStatus.version}`}
<div className="text-left"> </p>
<p className="font-medium text-foreground">Claude CLI</p>
<p className="text-sm text-muted-foreground">Use Claude Code subscription</p>
</div> </div>
</div> </div>
<StatusBadge <div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
status={ <CheckCircle2 className="w-5 h-5 text-green-500" />
cliVerificationStatus === 'verified' <p className="font-medium text-foreground">
? 'authenticated' {isCliAuthenticated ? 'CLI Authenticated' : 'API Key Configured'}
: claudeCliStatus?.installed </p>
? 'unverified'
: 'not_installed'
}
label={
cliVerificationStatus === 'verified'
? 'Verified'
: claudeCliStatus?.installed
? 'Unverified'
: 'Not Installed'
}
/>
</div> </div>
</AccordionTrigger>
<AccordionContent className="pt-4 space-y-4">
{!claudeCliStatus?.installed && (
<div className="space-y-4 p-4 rounded-lg bg-muted/30 border border-border">
<div className="flex items-center gap-2">
<Download className="w-4 h-4 text-muted-foreground" />
<p className="font-medium text-foreground">Install Claude CLI</p>
</div> </div>
)}
{/* Checking/Verifying State */}
{(isChecking || isVerifying) && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
<Loader2 className="w-5 h-5 text-blue-500 animate-spin" />
<p className="font-medium text-foreground">
{isChecking ? 'Checking Claude CLI status...' : 'Verifying authentication...'}
</p>
</div>
)}
{/* Not Installed */}
{!claudeCliStatus?.installed && !isChecking && !isVerifying && (
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 rounded-lg bg-muted/30 border border-border">
<XCircle className="w-5 h-5 text-muted-foreground shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-foreground">Claude CLI not found</p>
<p className="text-sm text-muted-foreground mt-1">
Install Claude CLI to use Claude Code subscription.
</p>
</div>
</div>
<div className="space-y-3 p-4 rounded-lg bg-muted/30 border border-border">
<p className="font-medium text-foreground text-sm">Install Claude CLI:</p>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm text-muted-foreground">macOS / Linux</Label> <Label className="text-sm text-muted-foreground">macOS / Linux</Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -284,9 +308,7 @@ function ClaudeContent() {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => onClick={() => copyCommand('curl -fsSL https://claude.ai/install.sh | bash')}
copyCommand('curl -fsSL https://claude.ai/install.sh | bash')
}
> >
<Copy className="w-4 h-4" /> <Copy className="w-4 h-4" />
</Button> </Button>
@@ -311,82 +333,59 @@ function ClaudeContent() {
)} )}
</Button> </Button>
</div> </div>
</div>
)} )}
{cliVerificationStatus === 'verified' && ( {/* Installed but not authenticated */}
{claudeCliStatus?.installed &&
!claudeAuthStatus?.authenticated &&
!isChecking &&
!isVerifying && (
<div className="space-y-4">
{/* Show CLI installed toast */}
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20"> <div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" /> <CheckCircle2 className="w-5 h-5 text-green-500" />
<p className="font-medium text-foreground">CLI Authentication verified!</p>
</div>
)}
{cliVerificationStatus === 'error' && cliVerificationError && (
<div className="flex items-start gap-3 p-4 rounded-lg bg-red-500/10 border border-red-500/20">
<XCircle className="w-5 h-5 text-red-500 shrink-0" />
<div> <div>
<p className="font-medium text-foreground">Verification failed</p> <p className="font-medium text-foreground">CLI Installed</p>
<p className="text-sm text-red-400 mt-1">{cliVerificationError}</p>
</div>
</div>
)}
{cliVerificationStatus !== 'verified' && (
<Button
onClick={verifyCliAuth}
disabled={cliVerificationStatus === 'verifying' || !claudeCliStatus?.installed}
className="w-full bg-brand-500 hover:bg-brand-600 text-white"
>
{cliVerificationStatus === 'verifying' ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verifying...
</>
) : (
<>
<ShieldCheck className="w-4 h-4 mr-2" />
Verify CLI Authentication
</>
)}
</Button>
)}
</AccordionContent>
</AccordionItem>
{/* API Key Option */}
<AccordionItem value="api-key" className="border-border">
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center justify-between w-full pr-4">
<div className="flex items-center gap-3">
<Key
className={`w-5 h-5 ${apiKeyVerificationStatus === 'verified' ? 'text-green-500' : 'text-muted-foreground'}`}
/>
<div className="text-left">
<p className="font-medium text-foreground">Anthropic API Key</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Pay-per-use with your own API key {claudeCliStatus?.version && `Version: ${claudeCliStatus.version}`}
</p> </p>
</div> </div>
</div> </div>
<StatusBadge
status={ {/* Error state */}
apiKeyVerificationStatus === 'verified' {verificationError && (
? 'authenticated' <div className="flex items-start gap-3 p-4 rounded-lg bg-red-500/10 border border-red-500/20">
: hasApiKey <XCircle className="w-5 h-5 text-red-500 shrink-0" />
? 'unverified' <div>
: 'not_authenticated' <p className="font-medium text-foreground">Authentication failed</p>
} <p className="text-sm text-red-400 mt-1">{verificationError}</p>
label={ </div>
apiKeyVerificationStatus === 'verified' </div>
? 'Verified' )}
: hasApiKey
? 'Unverified' {/* Not authenticated warning */}
: 'Not Set' <div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20">
} <AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
/> <div className="flex-1">
<p className="font-medium text-foreground">Claude CLI not authenticated</p>
<p className="text-sm text-muted-foreground mt-1">
Run <code className="bg-muted px-1 rounded">claude login</code> in your terminal
or provide an API key below.
</p>
</div>
</div>
{/* API Key alternative */}
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="api-key" className="border-border">
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center gap-3">
<Key className="w-5 h-5 text-muted-foreground" />
<span className="font-medium">Use Anthropic API Key instead</span>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="pt-4 space-y-4"> <AccordionContent className="pt-4 space-y-4">
<div className="space-y-4 p-4 rounded-lg bg-muted/30 border border-border">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="anthropic-key" className="text-foreground"> <Label htmlFor="anthropic-key" className="text-foreground">
Anthropic API Key Anthropic API Key
@@ -418,7 +417,11 @@ function ClaudeContent() {
disabled={isSavingApiKey || !apiKey.trim()} disabled={isSavingApiKey || !apiKey.trim()}
className="flex-1 bg-brand-500 hover:bg-brand-600 text-white" className="flex-1 bg-brand-500 hover:bg-brand-600 text-white"
> >
{isSavingApiKey ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Save API Key'} {isSavingApiKey ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
'Save API Key'
)}
</Button> </Button>
{hasApiKey && ( {hasApiKey && (
<Button <Button
@@ -435,47 +438,11 @@ function ClaudeContent() {
</Button> </Button>
)} )}
</div> </div>
</div>
{apiKeyVerificationStatus === 'verified' && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<p className="font-medium text-foreground">API Key verified!</p>
</div>
)}
{apiKeyVerificationStatus === 'error' && apiKeyVerificationError && (
<div className="flex items-start gap-3 p-4 rounded-lg bg-red-500/10 border border-red-500/20">
<XCircle className="w-5 h-5 text-red-500 shrink-0" />
<div>
<p className="font-medium text-foreground">Verification failed</p>
<p className="text-sm text-red-400 mt-1">{apiKeyVerificationError}</p>
</div>
</div>
)}
{apiKeyVerificationStatus !== 'verified' && (
<Button
onClick={verifyApiKeyAuth}
disabled={apiKeyVerificationStatus === 'verifying' || !hasApiKey}
className="w-full bg-brand-500 hover:bg-brand-600 text-white"
>
{apiKeyVerificationStatus === 'verifying' ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verifying...
</>
) : (
<>
<ShieldCheck className="w-4 h-4 mr-2" />
Verify API Key
</>
)}
</Button>
)}
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -599,9 +566,20 @@ function CursorContent() {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{isReady && ( {isReady && (
<div className="space-y-3">
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20"> <div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" /> <CheckCircle2 className="w-5 h-5 text-green-500" />
<p className="font-medium text-foreground">Cursor CLI is ready!</p> <div>
<p className="font-medium text-foreground">CLI Installed</p>
<p className="text-sm text-muted-foreground">
{cursorCliStatus?.version && `Version: ${cursorCliStatus.version}`}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<p className="font-medium text-foreground">Authenticated</p>
</div>
</div> </div>
)} )}
@@ -640,6 +618,17 @@ function CursorContent() {
{cursorCliStatus?.installed && !cursorCliStatus?.auth?.authenticated && !isChecking && ( {cursorCliStatus?.installed && !cursorCliStatus?.auth?.authenticated && !isChecking && (
<div className="space-y-4"> <div className="space-y-4">
{/* Show CLI installed toast */}
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<div>
<p className="font-medium text-foreground">CLI Installed</p>
<p className="text-sm text-muted-foreground">
{cursorCliStatus?.version && `Version: ${cursorCliStatus.version}`}
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20"> <div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" /> <AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
<div className="flex-1"> <div className="flex-1">
@@ -715,6 +704,7 @@ function CodexContent() {
installed: result.installed ?? false, installed: result.installed ?? false,
version: result.version, version: result.version,
path: result.path, path: result.path,
method: 'none',
}); });
if (result.auth?.authenticated) { if (result.auth?.authenticated) {
setCodexAuthStatus({ setCodexAuthStatus({
@@ -830,9 +820,22 @@ function CodexContent() {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{isReady && ( {isReady && (
<div className="space-y-3">
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20"> <div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" /> <CheckCircle2 className="w-5 h-5 text-green-500" />
<p className="font-medium text-foreground">Codex CLI is ready!</p> <div>
<p className="font-medium text-foreground">CLI Installed</p>
<p className="text-sm text-muted-foreground">
{codexCliStatus?.version && `Version: ${codexCliStatus.version}`}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<p className="font-medium text-foreground">
{codexAuthStatus?.method === 'api_key' ? 'API Key Configured' : 'Authenticated'}
</p>
</div>
</div> </div>
)} )}
@@ -866,6 +869,28 @@ function CodexContent() {
)} )}
{codexCliStatus?.installed && !codexAuthStatus?.authenticated && !isChecking && ( {codexCliStatus?.installed && !codexAuthStatus?.authenticated && !isChecking && (
<div className="space-y-4">
{/* Show CLI installed toast */}
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<div>
<p className="font-medium text-foreground">CLI Installed</p>
<p className="text-sm text-muted-foreground">
{codexCliStatus?.version && `Version: ${codexCliStatus.version}`}
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-foreground">Codex CLI not authenticated</p>
<p className="text-sm text-muted-foreground mt-1">
Run the login command or provide an API key below.
</p>
</div>
</div>
<Accordion type="single" collapsible className="w-full"> <Accordion type="single" collapsible className="w-full">
<AccordionItem value="cli" className="border-border"> <AccordionItem value="cli" className="border-border">
<AccordionTrigger className="hover:no-underline"> <AccordionTrigger className="hover:no-underline">
@@ -938,6 +963,7 @@ function CodexContent() {
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</div>
)} )}
{isChecking && ( {isChecking && (
@@ -1069,9 +1095,20 @@ function OpencodeContent() {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{isReady && ( {isReady && (
<div className="space-y-3">
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20"> <div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" /> <CheckCircle2 className="w-5 h-5 text-green-500" />
<p className="font-medium text-foreground">OpenCode CLI is ready!</p> <div>
<p className="font-medium text-foreground">CLI Installed</p>
<p className="text-sm text-muted-foreground">
{opencodeCliStatus?.version && `Version: ${opencodeCliStatus.version}`}
</p>
</div>
</div>
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<p className="font-medium text-foreground">Authenticated</p>
</div>
</div> </div>
)} )}
@@ -1112,6 +1149,17 @@ function OpencodeContent() {
{opencodeCliStatus?.installed && !opencodeCliStatus?.auth?.authenticated && !isChecking && ( {opencodeCliStatus?.installed && !opencodeCliStatus?.auth?.authenticated && !isChecking && (
<div className="space-y-4"> <div className="space-y-4">
{/* Show CLI installed toast */}
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<div>
<p className="font-medium text-foreground">CLI Installed</p>
<p className="text-sm text-muted-foreground">
{opencodeCliStatus?.version && `Version: ${opencodeCliStatus.version}`}
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20"> <div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20">
<AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" /> <AlertTriangle className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
<div className="flex-1"> <div className="flex-1">
@@ -1170,54 +1218,215 @@ function OpencodeContent() {
// ============================================================================ // ============================================================================
export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps) { export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps) {
const [activeTab, setActiveTab] = useState<ProviderTab>('claude'); const [activeTab, setActiveTab] = useState<ProviderTab>('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?.authenticated === true &&
(claudeAuthStatus?.method === 'cli_authenticated' || (claudeAuthStatus?.method === 'cli_authenticated' ||
claudeAuthStatus?.method === 'api_key' || claudeAuthStatus?.method === 'api_key' ||
claudeAuthStatus?.method === 'api_key_env'); claudeAuthStatus?.method === 'api_key_env');
const isCursorConfigured = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated; const isCursorInstalled = cursorCliStatus?.installed === true;
const isCodexConfigured = codexAuthStatus?.authenticated === true; const isCursorAuthenticated = cursorCliStatus?.auth?.authenticated === true;
const isOpencodeConfigured =
opencodeCliStatus?.installed && opencodeCliStatus?.auth?.authenticated; const isCodexInstalled = codexCliStatus?.installed === true;
const isCodexAuthenticated = codexAuthStatus?.authenticated === true;
const isOpencodeInstalled = opencodeCliStatus?.installed === true;
const isOpencodeAuthenticated = opencodeCliStatus?.auth?.authenticated === true;
const hasAtLeastOneProvider = 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 = [ const providers = [
{ {
id: 'claude' as const, id: 'claude' as const,
label: 'Claude', label: 'Claude',
icon: AnthropicIcon, icon: AnthropicIcon,
configured: isClaudeConfigured, status: getProviderStatus(isClaudeInstalled, isClaudeAuthenticated, claudeIsVerifying),
color: 'text-brand-500', color: 'text-brand-500',
}, },
{ {
id: 'cursor' as const, id: 'cursor' as const,
label: 'Cursor', label: 'Cursor',
icon: CursorIcon, icon: CursorIcon,
configured: isCursorConfigured, status: getProviderStatus(isCursorInstalled, isCursorAuthenticated),
color: 'text-blue-500', color: 'text-blue-500',
}, },
{ {
id: 'codex' as const, id: 'codex' as const,
label: 'Codex', label: 'Codex',
icon: OpenAIIcon, icon: OpenAIIcon,
configured: isCodexConfigured, status: getProviderStatus(isCodexInstalled, isCodexAuthenticated),
color: 'text-emerald-500', color: 'text-emerald-500',
}, },
{ {
id: 'opencode' as const, id: 'opencode' as const,
label: 'OpenCode', label: 'OpenCode',
icon: OpenCodeIcon, icon: OpenCodeIcon,
configured: isOpencodeConfigured, status: getProviderStatus(isOpencodeInstalled, isOpencodeAuthenticated),
color: 'text-green-500', color: 'text-green-500',
}, },
]; ];
const renderStatusIcon = (status: ProviderStatus) => {
switch (status) {
case 'authenticated':
return (
<CheckCircle2 className="w-3 h-3 text-green-500 absolute -top-1 -right-1.5 bg-background rounded-full" />
);
case 'verifying':
return (
<Loader2 className="w-3 h-3 text-blue-500 absolute -top-1 -right-1.5 bg-background rounded-full animate-spin" />
);
case 'installed_not_auth':
return (
<AlertCircle className="w-3 h-3 text-red-500 absolute -top-1 -right-1.5 bg-background rounded-full" />
);
default:
return null;
}
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center mb-6"> <div className="text-center mb-6">
@@ -1225,6 +1434,13 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps)
<p className="text-muted-foreground">Configure at least one AI provider to continue</p> <p className="text-muted-foreground">Configure at least one AI provider to continue</p>
</div> </div>
{isInitialChecking && (
<div className="flex items-center justify-center gap-2 p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
<Loader2 className="w-5 h-5 text-blue-500 animate-spin" />
<p className="font-medium text-foreground">Checking provider status...</p>
</div>
)}
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as ProviderTab)}> <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as ProviderTab)}>
<TabsList className="grid w-full grid-cols-4 h-auto p-1"> <TabsList className="grid w-full grid-cols-4 h-auto p-1">
{providers.map((provider) => { {providers.map((provider) => {
@@ -1242,12 +1458,16 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps)
<Icon <Icon
className={cn( className={cn(
'w-5 h-5', 'w-5 h-5',
provider.configured ? provider.color : 'text-muted-foreground' provider.status === 'authenticated'
? provider.color
: provider.status === 'verifying'
? 'text-blue-500'
: provider.status === 'installed_not_auth'
? 'text-amber-500'
: 'text-muted-foreground'
)} )}
/> />
{provider.configured && ( {!isInitialChecking && renderStatusIcon(provider.status)}
<CheckCircle2 className="w-3 h-3 text-green-500 absolute -top-1 -right-1.5 bg-background rounded-full" />
)}
</div> </div>
<span className="text-xs font-medium">{provider.label}</span> <span className="text-xs font-medium">{provider.label}</span>
</TabsTrigger> </TabsTrigger>

View File

@@ -132,6 +132,7 @@ export interface SetupState {
claudeCliStatus: CliStatus | null; claudeCliStatus: CliStatus | null;
claudeAuthStatus: ClaudeAuthStatus | null; claudeAuthStatus: ClaudeAuthStatus | null;
claudeInstallProgress: InstallProgress; claudeInstallProgress: InstallProgress;
claudeIsVerifying: boolean;
// GitHub CLI state // GitHub CLI state
ghCliStatus: GhCliStatus | null; ghCliStatus: GhCliStatus | null;
@@ -164,6 +165,7 @@ export interface SetupActions {
setClaudeAuthStatus: (status: ClaudeAuthStatus | null) => void; setClaudeAuthStatus: (status: ClaudeAuthStatus | null) => void;
setClaudeInstallProgress: (progress: Partial<InstallProgress>) => void; setClaudeInstallProgress: (progress: Partial<InstallProgress>) => void;
resetClaudeInstallProgress: () => void; resetClaudeInstallProgress: () => void;
setClaudeIsVerifying: (isVerifying: boolean) => void;
// GitHub CLI // GitHub CLI
setGhCliStatus: (status: GhCliStatus | null) => void; setGhCliStatus: (status: GhCliStatus | null) => void;
@@ -202,6 +204,7 @@ const initialState: SetupState = {
claudeCliStatus: null, claudeCliStatus: null,
claudeAuthStatus: null, claudeAuthStatus: null,
claudeInstallProgress: { ...initialInstallProgress }, claudeInstallProgress: { ...initialInstallProgress },
claudeIsVerifying: false,
ghCliStatus: null, ghCliStatus: null,
cursorCliStatus: null, cursorCliStatus: null,
@@ -255,6 +258,8 @@ export const useSetupStore = create<SetupState & SetupActions>()((set, get) => (
claudeInstallProgress: { ...initialInstallProgress }, claudeInstallProgress: { ...initialInstallProgress },
}), }),
setClaudeIsVerifying: (isVerifying) => set({ claudeIsVerifying: isVerifying }),
// GitHub CLI // GitHub CLI
setGhCliStatus: (status) => set({ ghCliStatus: status }), setGhCliStatus: (status) => set({ ghCliStatus: status }),