feat: fix CLI authentication detection to prevent unnecessary browser prompts

- Fix Claude, Codex, and Cursor auth handlers to check if CLI is already authenticated
- Use same detection logic as each provider's internal checkAuth/codexAuthIndicators()
- For Codex: Check for API keys and auth files before requiring manual login
- For Cursor: Check for env var and credentials files before requiring manual auth
- For Claude: Check for cached auth tokens, settings, and credentials files
- If CLI is already authenticated: Just reconnect by removing disconnected marker
- If CLI needs auth: Tell user to manually run login command
- This prevents timeout errors when login commands can't run in non-interactive mode
This commit is contained in:
DhanushSantosh
2026-01-09 21:34:14 +05:30
parent 33d02d1df8
commit 1452232409
17 changed files with 741 additions and 30 deletions

View File

@@ -1,9 +1,12 @@
import { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CliStatus } from '../shared/types';
import type { ClaudeAuthStatus } from '@/store/setup-store';
import { AnthropicIcon } from '@/components/ui/provider-icon';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
interface CliStatusProps {
status: CliStatus | null;
@@ -81,6 +84,60 @@ function ClaudeCliStatusSkeleton() {
}
export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) {
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [isDeauthenticating, setIsDeauthenticating] = useState(false);
const handleSignIn = useCallback(async () => {
setIsAuthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.authClaude();
if (result.success) {
toast.success('Signed In', {
description: 'Successfully authenticated Claude CLI',
});
onRefresh();
} else if (result.error) {
toast.error('Authentication Failed', {
description: result.error,
});
}
} catch (error) {
toast.error('Authentication Failed', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setIsAuthenticating(false);
}
}, [onRefresh]);
const handleSignOut = useCallback(async () => {
setIsDeauthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.deauthClaude();
if (result.success) {
toast.success('Signed Out', {
description: 'Successfully signed out from Claude CLI',
});
// Refresh status after successful logout
onRefresh();
} else if (result.error) {
toast.error('Sign Out Failed', {
description: result.error,
});
}
} catch (error) {
toast.error('Sign Out Failed', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setIsDeauthenticating(false);
}
}, [onRefresh]);
if (!status) return <ClaudeCliStatusSkeleton />;
return (
@@ -153,7 +210,7 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
</div>
{/* Authentication Status */}
{authStatus?.authenticated ? (
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="flex items-start gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
</div>
@@ -165,6 +222,15 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleSignOut}
disabled={isDeauthenticating}
className="mt-3 h-8 text-xs"
>
{isDeauthenticating ? 'Signing Out...' : 'Sign Out'}
</Button>
</div>
</div>
) : (
@@ -175,9 +241,17 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
<div className="flex-1">
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
<p className="text-xs text-amber-400/70 mt-1">
Run <code className="font-mono bg-amber-500/10 px-1 rounded">claude login</code>{' '}
or set an API key to authenticate.
Click Sign In below to get authentication instructions.
</p>
<Button
variant="outline"
size="sm"
onClick={handleSignIn}
disabled={isAuthenticating}
className="mt-3 h-8 text-xs"
>
{isAuthenticating ? 'Requesting...' : 'Sign In'}
</Button>
</div>
</div>
)}

View File

@@ -1,9 +1,12 @@
import { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CliStatus } from '../shared/types';
import type { CodexAuthStatus } from '@/store/setup-store';
import { OpenAIIcon } from '@/components/ui/provider-icon';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
interface CliStatusProps {
status: CliStatus | null;
@@ -76,6 +79,60 @@ function CodexCliStatusSkeleton() {
}
export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) {
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [isDeauthenticating, setIsDeauthenticating] = useState(false);
const handleSignIn = useCallback(async () => {
setIsAuthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.authCodex();
if (result.success) {
toast.success('Signed In', {
description: 'Successfully authenticated Codex CLI',
});
onRefresh();
} else if (result.error) {
toast.error('Authentication Failed', {
description: result.error,
});
}
} catch (error) {
toast.error('Authentication Failed', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setIsAuthenticating(false);
}
}, [onRefresh]);
const handleSignOut = useCallback(async () => {
setIsDeauthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.deauthCodex();
if (result.success) {
toast.success('Signed Out', {
description: 'Successfully signed out from Codex CLI',
});
// Refresh status after successful logout
onRefresh();
} else if (result.error) {
toast.error('Sign Out Failed', {
description: result.error,
});
}
} catch (error) {
toast.error('Sign Out Failed', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setIsDeauthenticating(false);
}
}, [onRefresh]);
if (!status) return <CodexCliStatusSkeleton />;
return (
@@ -145,7 +202,7 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
</div>
{/* Authentication Status */}
{authStatus?.authenticated ? (
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="flex items-start gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
</div>
@@ -157,6 +214,15 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleSignOut}
disabled={isDeauthenticating}
className="mt-3 h-8 text-xs"
>
{isDeauthenticating ? 'Signing Out...' : 'Sign Out'}
</Button>
</div>
</div>
) : (
@@ -167,9 +233,17 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
<div className="flex-1">
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
<p className="text-xs text-amber-400/70 mt-1">
Run <code className="font-mono bg-amber-500/10 px-1 rounded">codex login</code>{' '}
or set an API key to authenticate.
Click Sign In below to get authentication instructions.
</p>
<Button
variant="outline"
size="sm"
onClick={handleSignIn}
disabled={isAuthenticating}
className="mt-3 h-8 text-xs"
>
{isAuthenticating ? 'Requesting...' : 'Sign In'}
</Button>
</div>
</div>
)}

View File

@@ -1,7 +1,10 @@
import { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { CursorIcon } from '@/components/ui/provider-icon';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
interface CursorStatus {
installed: boolean;
@@ -201,6 +204,60 @@ export function ModelConfigSkeleton() {
}
export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStatusProps) {
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [isDeauthenticating, setIsDeauthenticating] = useState(false);
const handleSignIn = useCallback(async () => {
setIsAuthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.authCursor();
if (result.success) {
toast.success('Signed In', {
description: 'Successfully authenticated Cursor CLI',
});
onRefresh();
} else if (result.error) {
toast.error('Authentication Failed', {
description: result.error,
});
}
} catch (error) {
toast.error('Authentication Failed', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setIsAuthenticating(false);
}
}, [onRefresh]);
const handleSignOut = useCallback(async () => {
setIsDeauthenticating(true);
try {
const api = getElectronAPI();
const result = await api.setup.deauthCursor();
if (result.success) {
toast.success('Signed Out', {
description: 'Successfully signed out from Cursor CLI',
});
// Refresh status after successful logout
onRefresh();
} else if (result.error) {
toast.error('Sign Out Failed', {
description: result.error,
});
}
} catch (error) {
toast.error('Sign Out Failed', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setIsDeauthenticating(false);
}
}, [onRefresh]);
if (!status) return <CursorCliStatusSkeleton />;
return (
@@ -262,7 +319,7 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
{/* Authentication Status */}
{status.authenticated ? (
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="flex items-start gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
</div>
@@ -276,6 +333,15 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
</span>
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleSignOut}
disabled={isDeauthenticating}
className="mt-3 h-8 text-xs"
>
{isDeauthenticating ? 'Signing Out...' : 'Sign Out'}
</Button>
</div>
</div>
) : (
@@ -286,9 +352,17 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
<div className="flex-1">
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
<p className="text-xs text-amber-400/70 mt-1">
Run <code className="font-mono bg-amber-500/10 px-1 rounded">cursor auth</code>{' '}
to authenticate with Cursor.
Click Sign In below to get authentication instructions.
</p>
<Button
variant="outline"
size="sm"
onClick={handleSignIn}
disabled={isAuthenticating}
className="mt-3 h-8 text-xs"
>
{isAuthenticating ? 'Requesting...' : 'Sign In'}
</Button>
</div>
</div>
)}