import { useCallback, useEffect, useState } from 'react'; import { LuKeyRound, LuX } from 'react-icons/lu'; import { useT } from '../../../../lib/i18n/I18nContext'; import { type ClaudeCodeAuthStatus, openhumanClaudeCodeAuthStatus, openhumanClaudeCodeLoginLaunch, openhumanClaudeCodeSetFullAccess, openhumanClaudeCodeSettings, } from '../../../../utils/tauriCommands/config'; import Button from '../../../ui/Button'; /** * Claude Code CLI connect control — the peer of the Codex connect button. * * Inline: a "Claude Code" button + a one-line status summary. Clicking the * button opens a modal with the actual controls (enable/disable, sign-in / * reconnect, install hint). * * Auth is probed via `claude auth status --json` (cross-platform: covers the * macOS Keychain as well as the Linux/Windows file stores) or * `ANTHROPIC_API_KEY`. We do NOT spawn the slow `claude --version` probe — a * missing/old binary surfaces as `unknown` from the auth probe, rendered as a * compact install hint rather than "signed out". */ export function ClaudeCodeConnect({ connected, busy = false, onConnect, onDisconnect, }: { connected: boolean; busy?: boolean; onConnect: () => void | Promise; onDisconnect: () => void | Promise; }) { const { t } = useT(); const [open, setOpen] = useState(false); const [auth, setAuth] = useState(null); const [authLoading, setAuthLoading] = useState(false); const [acting, setActing] = useState(false); const probeAuth = useCallback(async () => { setAuthLoading(true); try { // Resolves to the BARE AuthStatus (no `{ result }` envelope) — see the // wrapper in tauriCommands/config.ts. const resp = await openhumanClaudeCodeAuthStatus(); setAuth(resp); } catch { setAuth(null); } finally { setAuthLoading(false); } }, []); // Probe once connected so the inline summary + modal reflect sign-in state. // The disconnected render is DERIVED from `connected` (`shownAuth` below) // rather than clearing `auth` from the effect — synchronous setState in an // effect body is disallowed by `react-hooks/set-state-in-effect`. useEffect(() => { if (connected) void probeAuth(); }, [connected, probeAuth]); const shownAuth = connected ? auth : null; const runConnect = async () => { setActing(true); try { await onConnect(); } finally { setActing(false); } }; const runDisconnect = async () => { setActing(true); try { await onDisconnect(); } finally { setActing(false); } }; return (
{open && ( setOpen(false)} onConnect={runConnect} onDisconnect={runDisconnect} onRecheck={probeAuth} /> )}
); } /** Title-case a raw subscription type (`"max"` → `"Max"`) for display. */ function formatSubscriptionType(raw: string): string { const trimmed = raw.trim(); if (!trimmed) return trimmed; return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); } /** Heuristic: does an `unknown` reason indicate the binary is missing? */ function looksNotInstalled(reason: string | null): boolean { if (!reason) return false; const r = reason.toLowerCase(); return r.includes('not found') || r.includes('not installed') || r.includes('path'); } /** One-line status shown next to the inline "Claude Code" button. */ function InlineSummary({ connected, auth, loading, }: { connected: boolean; auth: ClaudeCodeAuthStatus | null; loading: boolean; }) { const { t } = useT(); if (!connected) { return <>{t('settings.ai.claudeCode.inlineNotConnected')}; } if (!auth) { return ( <> {loading ? t('settings.ai.claudeCode.checkingSignIn') : t('settings.ai.claudeCode.inlineConnected')} ); } if (auth.source === 'subscription') { const who = auth.account_email ?? t('settings.ai.claudeCode.subscriptionFallback'); const plan = auth.subscription_type ? ` (${formatSubscriptionType(auth.subscription_type)})` : ''; return ( {t('settings.ai.claudeCode.signedInAs')} {who} {plan} ); } if (auth.source === 'api_key_env') { return ( {t('settings.ai.claudeCode.usingApiKeyEnv')} ); } if (auth.source === 'unknown') { return ( {looksNotInstalled(auth.reason) ? t('settings.ai.claudeCode.cliNotInstalled') : t('settings.ai.claudeCode.signInUnknown')} ); } return ( {t('settings.ai.claudeCode.connectedNotSignedIn')} ); } /** * Modal with the actual Claude Code controls: enable/disable the provider, * sign in / reconnect via the CLI, and install guidance. */ function ClaudeCodeModal({ connected, busy, auth, authLoading, onClose, onConnect, onDisconnect, onRecheck, }: { connected: boolean; busy: boolean; auth: ClaudeCodeAuthStatus | null; authLoading: boolean; onClose: () => void; onConnect: () => void | Promise; onDisconnect: () => void | Promise; onRecheck: () => void | Promise; }) { const { t } = useT(); const [launching, setLaunching] = useState(false); const [launchError, setLaunchError] = useState(null); // Persisted full-access toggle (bypassPermissions vs the default acceptEdits). // `null` until loaded so the switch can render a disabled placeholder. const [fullAccess, setFullAccess] = useState(null); const [savingAccess, setSavingAccess] = useState(false); useEffect(() => { let cancelled = false; void (async () => { try { const s = await openhumanClaudeCodeSettings(); if (!cancelled) setFullAccess(s.full_access); } catch { // Fail safe to OFF (acceptEdits) if the read fails. if (!cancelled) setFullAccess(false); } })(); return () => { cancelled = true; }; }, []); const toggleFullAccess = async (next: boolean) => { setSavingAccess(true); setFullAccess(next); // optimistic try { const s = await openhumanClaudeCodeSetFullAccess(next); setFullAccess(s.full_access); } catch { setFullAccess(!next); // revert on failure } finally { setSavingAccess(false); } }; const launchLogin = async () => { setLaunching(true); setLaunchError(null); try { await openhumanClaudeCodeLoginLaunch(); } catch { // Surface the failure inline rather than leaving an unhandled rejection. setLaunchError(t('settings.ai.claudeCode.loginError')); } finally { setLaunching(false); } }; return (
e.stopPropagation()}>

{t('settings.ai.claudeCode.modalTitle')}

{t('settings.ai.claudeCode.modalDescription')}

{/* Connection */}
{t('settings.ai.claudeCode.connection')}
{connected ? t('settings.ai.claudeCode.enabled') : t('settings.ai.claudeCode.notEnabled')}
{connected ? ( ) : ( )}
{/* Authentication */}
{t('settings.ai.claudeCode.authentication')}

{t('settings.ai.claudeCode.loginHint')}

{launchError && (

{launchError}

)}
{/* Permissions — full access vs. the default acceptEdits posture. */}
{t('settings.ai.claudeCode.fullAccess')}

{fullAccess ? t('settings.ai.claudeCode.fullAccessOn') : t('settings.ai.claudeCode.fullAccessOff')}

{isMac() ? t('settings.ai.claudeCode.sandboxNoteMac') : t('settings.ai.claudeCode.sandboxNoteOther')}

); } /** Best-effort macOS detection for the permissions copy (UA-based). */ function isMac(): boolean { if (typeof navigator === 'undefined') return false; return /Mac|iPhone|iPad/i.test(navigator.platform || navigator.userAgent || ''); } /** Detailed auth line inside the modal. */ function AuthDetail({ auth, loading }: { auth: ClaudeCodeAuthStatus | null; loading: boolean }) { const { t } = useT(); if (!auth) { return (

{loading ? t('settings.ai.claudeCode.checkingSignIn') : t('settings.ai.claudeCode.enableToCheck')}

); } if (auth.source === 'subscription') { const who = auth.account_email ?? t('settings.ai.claudeCode.subscriptionFallback'); const plan = auth.subscription_type ? ` (${formatSubscriptionType(auth.subscription_type)})` : ''; return (

{t('settings.ai.claudeCode.signedInAs')} {who} {plan}

); } if (auth.source === 'api_key_env') { return (

{t('settings.ai.claudeCode.usingApiKeyEnvDetail')}

); } if (auth.source === 'unknown') { if (looksNotInstalled(auth.reason)) { return (

{t('settings.ai.claudeCode.notFoundInstall')}

); } return (

{t('settings.ai.claudeCode.unknownDetail')}

); } return (

{t('settings.ai.claudeCode.notSignedIn')}

); }