import type { Dispatch, SetStateAction } from "react"; import type { AuthProvider, ManualOAuthCodeInfo, OAuthDeviceCodeInfo } from "../../../api"; import type { ToastType } from "../../../hooks/useToast"; import { useTranslation } from "react-i18next"; import { ClaudeCliProviderCard } from "../../ClaudeCliProviderCard"; import { CursorCliProviderCard } from "../../CursorCliProviderCard"; import { LlamaCppProviderCard } from "../../LlamaCppProviderCard"; import { ProviderIcon } from "../../ProviderIcon"; import { PluginSlot } from "../../PluginSlot"; import { LoginInstructions } from "../../LoginInstructions"; import { LoadingSpinner } from "../../LoadingSpinner"; import { OAuthManualCodeForm } from "../../OAuthManualCodeForm"; import { CustomProvidersSection } from "../../CustomProvidersSection"; import { copyTextToClipboard } from "../../../utils/copyToClipboard"; import { appendTokenQuery } from "../../../auth"; export interface AuthenticationSectionData { projectId?: string; addToast: (message: string, type?: ToastType) => void; authProviders: AuthProvider[]; authLoading: boolean; authActionInProgress: string | null; apiKeyInputs: Record; setApiKeyInputs: Dispatch>>; apiKeyErrors: Record; opencodeApiKeyRefreshStatus: Record; deviceCodes: Record; loginInstructions: Record; manualCodeConfigs: Record; manualCodeInputs: Record; setManualCodeInputs: Dispatch>>; manualCodeSubmitInProgress: string | null; loadAuthStatus: () => void | Promise; handleLogin: (providerId: string) => void; handleLogout: (providerId: string) => void; handleCancelLogin: (providerId: string) => void; handleSaveApiKey: (providerId: string) => void; handleClearApiKey: (providerId: string) => void; handleSubmitManualCode: (providerId: string) => void | Promise; onReopenOnboarding?: () => void; } export interface AuthenticationSectionProps { auth: AuthenticationSectionData; } export function AuthenticationSection({ auth }: AuthenticationSectionProps) { const { t } = useTranslation("app"); const { projectId, addToast, authProviders, authLoading, authActionInProgress, apiKeyInputs, setApiKeyInputs, apiKeyErrors, opencodeApiKeyRefreshStatus, deviceCodes, loginInstructions, manualCodeConfigs, manualCodeInputs, setManualCodeInputs, manualCodeSubmitInProgress, loadAuthStatus, handleLogin, handleLogout, handleCancelLogin, handleSaveApiKey, handleClearApiKey, handleSubmitManualCode, onReopenOnboarding, } = auth; // CLI-backed providers render their own compact card; filter them out of the // standard OAuth/API-key sort and render alongside. const cliAuthProviders = authProviders.filter((p) => p.type === "cli"); const nonCliProviders = authProviders.filter((p) => p.type !== "cli"); const sortedProviders = [...nonCliProviders].sort((a, b) => { if (a.authenticated !== b.authenticated) { return a.authenticated ? -1 : 1; } return a.name.localeCompare(b.name); }); const authenticatedProviders = sortedProviders.filter((p) => p.authenticated); const unauthenticatedProviders = sortedProviders.filter((p) => !p.authenticated); const claudeCliProvider = cliAuthProviders.find((p) => p.id === "claude-cli"); const cursorCliProvider = cliAuthProviders.find((p) => p.id === "cursor-cli"); const llamaCppProvider = cliAuthProviders.find((p) => p.id === "llama-cpp"); const claudeCliCard = claudeCliProvider ? ( { void loadAuthStatus(); }}/>) : null; const cursorCliCard = cursorCliProvider ? ( { void loadAuthStatus(); }}/>) : null; const llamaCppCard = llamaCppProvider ? ( { void loadAuthStatus(); }}/>) : null; const showAuthenticatedGroup = authenticatedProviders.length > 0 || (claudeCliProvider?.authenticated ?? false) || (cursorCliProvider?.authenticated ?? false) || (llamaCppProvider?.authenticated ?? false); const showAvailableGroup = unauthenticatedProviders.length > 0 || (claudeCliProvider && !claudeCliProvider.authenticated) || (cursorCliProvider && !cursorCliProvider.authenticated) || (llamaCppProvider && !llamaCppProvider.authenticated); return (<>

{t("settings.auth.title", "Authentication")}

{authLoading ? (
) : authProviders.length === 0 ? (
{t("settings.auth.noProviders", "No providers available")}
) : (
{ void loadAuthStatus(); } }}/> { void loadAuthStatus(); } }}/> {!showAuthenticatedGroup && (
{t("settings.auth.signInHint", "Sign in to at least one provider to get started with AI models.")}
)} {showAuthenticatedGroup && (
{t("settings.auth.groupAuthenticated", "Authenticated")}
{claudeCliProvider?.authenticated && claudeCliCard} {cursorCliProvider?.authenticated && cursorCliCard} {llamaCppProvider?.authenticated && llamaCppCard} {authenticatedProviders.map((provider) => (
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon- */} {provider.name} {t("settings.auth.statusActive", "✓ Active")} {provider.authenticated && provider.keyHint && ({t("settings.authentication.key", "Key: ")}{provider.keyHint})}
{provider.type === "api_key" ? (
setApiKeyInputs((prev) => ({ ...prev, [provider.id]: e.target.value }))} disabled={authActionInProgress === provider.id}/> {provider.authenticated && !apiKeyInputs[provider.id] ? () : ()}
{authActionInProgress === provider.id && ({t("settings.auth.savingKey", "Saving…")})} {apiKeyErrors[provider.id] && ({apiKeyErrors[provider.id]})} {(provider.id === "opencode" || provider.id === "opencode-go") && opencodeApiKeyRefreshStatus[provider.id] && ( {opencodeApiKeyRefreshStatus[provider.id].message} )}
) : (
{authActionInProgress === provider.id ? () : provider.loginInProgress ? (
) : ()}
)}
))}
)} {showAvailableGroup && (
{t("settings.auth.groupAvailable", "Available")}
{claudeCliProvider && !claudeCliProvider.authenticated && claudeCliCard} {cursorCliProvider && !cursorCliProvider.authenticated && cursorCliCard} {llamaCppProvider && !llamaCppProvider.authenticated && llamaCppCard} {unauthenticatedProviders.map((provider) => (
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon- */} {provider.name} {t("settings.auth.statusNotConnected", "✗ Not connected")}
{provider.type === "api_key" ? (
setApiKeyInputs((prev) => ({ ...prev, [provider.id]: e.target.value }))} disabled={authActionInProgress === provider.id}/>
{authActionInProgress === provider.id && ({t("settings.auth.savingKey", "Saving…")})} {apiKeyErrors[provider.id] && ({apiKeyErrors[provider.id]})} {(provider.id === "opencode" || provider.id === "opencode-go") && opencodeApiKeyRefreshStatus[provider.id] && ( {opencodeApiKeyRefreshStatus[provider.id].message} )}
) : (
{authActionInProgress === provider.id ? () : provider.loginInProgress ? (
) : ()} {provider.id === "github-copilot" && deviceCodes[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && (
{t("settings.auth.enterCodeOnGitHub", "Enter this code on GitHub")}
{deviceCodes[provider.id].userCode}
)} {loginInstructions[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && ()} {manualCodeConfigs[provider.id] && (provider.loginInProgress || authActionInProgress === provider.id) && ( setManualCodeInputs((prev) => ({ ...prev, [provider.id]: value }))} onSubmit={() => void handleSubmitManualCode(provider.id)} prompt={manualCodeConfigs[provider.id].prompt} placeholder={manualCodeConfigs[provider.id].placeholder} helpText={manualCodeConfigs[provider.id].helpText} disabled={manualCodeSubmitInProgress === provider.id} submitLabel={manualCodeSubmitInProgress === provider.id ? "Submitting…" : "Submit code"} data-testid={`auth-manual-code-${provider.id}`}/>)}
)}
))}
)}
)} {t("settings.auth.hint", "Authentication changes take effect immediately — no need to save.")} {onReopenOnboarding && (
{t("settings.auth.reopenOnboardingHint", "Re-run the setup wizard to review or update your AI provider and model configuration.")}
)} ); } export default AuthenticationSection;