/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import type { LoadedSettings } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { useRuntimeApi } from '../contexts/RuntimeContext.js'; import type { AuthStatus } from '@vybestack/llxprt-code-providers/auth.js'; import { firstNonEmptyString } from '../../utils/coalesce.js'; import { getBorderStyle } from '../contexts/UnicodeRenderingContext.js'; interface AuthDialogProps { onSelect: (authMethod: string | undefined, scope: SettingScope) => void; settings: LoadedSettings; initialErrorMessage?: string | null; } interface AuthDialogState { enabledProviders: Set; setEnabledProviders: React.Dispatch>>; authStatuses: Map; reloadAuthStatuses: () => Promise; } function getEnabledProviders( oauthProviders: Record | undefined, ): Set { const enabled = new Set(); const providers = oauthProviders ?? {}; for (const [provider, isEnabled] of Object.entries(providers)) { if (isEnabled) { enabled.add(`oauth_${provider}`); } } return enabled; } const AuthDialogHeader: React.FC = () => ( <> OAuth Authentication Select an OAuth provider to toggle enable/disable: Note: You can also use API keys via /key, /keyfile, --key, --keyfile, --key-name, or environment variables ); const AuthDialogFooter: React.FC = () => ( <> (Use Enter to select, ESC to close) Terms of Services and Privacy Notice for Gemini CLI { 'https://github.com/acoliver/llxprt-code/blob/main/docs/tos-privacy.md' } ); function getStatusLabel( provider: string, authStatuses: ReadonlyMap, ): string { const status = authStatuses.get(provider); if (status === undefined) return ''; return status.authenticated ? ' (Authenticated)' : ' (Not authenticated)'; } function buildAuthItems( enabledProviders: Set, authStatuses: ReadonlyMap, ): Array<{ key: string; label: string; value: string; }> { return [ { key: 'oauth_claudecode', label: `Claude Code (Claude.ai OAuth) ${enabledProviders.has('oauth_claudecode') ? '[ON]' : '[OFF]'}${getStatusLabel('claudecode', authStatuses)}`, value: 'oauth_claudecode', }, { key: 'oauth_codex', label: `Codex (ChatGPT OAuth) ${enabledProviders.has('oauth_codex') ? '[ON]' : '[OFF]'}${getStatusLabel('codex', authStatuses)}`, value: 'oauth_codex', }, { key: 'close', label: 'Close', value: 'close', }, ]; } interface AuthDialogContentProps { items: Array<{ key: string; label: string; value: string }>; errorMessage: string | null; handleAuthSelect: (authMethod: string) => void; } const AuthDialogContent: React.FC = ({ items, errorMessage, handleAuthSelect, }) => ( <> {errorMessage && ( {errorMessage} )} ); function useMountedRef(): React.MutableRefObject { const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); return mountedRef; } function useAuthDialogState( settings: LoadedSettings, runtime: ReturnType, mountedRef: React.MutableRefObject, ): AuthDialogState { const [enabledProviders, setEnabledProviders] = useState>(() => getEnabledProviders(settings.merged.oauthEnabledProviders), ); const [authStatuses, setAuthStatuses] = useState>( () => new Map(), ); const reloadAuthStatuses = useCallback(async (): Promise => { try { const statuses = await runtime.getCliOAuthManager().getAuthStatus(); if (!mountedRef.current) return; setAuthStatuses( new Map(statuses.map((status) => [status.provider, status])), ); syncEnabledProvidersFromAuthStatus(setEnabledProviders, statuses); } catch { if (mountedRef.current) setAuthStatuses(new Map()); } }, [mountedRef, runtime, setEnabledProviders]); useEffect(() => { setEnabledProviders( getEnabledProviders(settings.merged.oauthEnabledProviders), ); }, [settings.merged.oauthEnabledProviders]); useEffect(() => { void reloadAuthStatuses(); }, [reloadAuthStatuses]); return { enabledProviders, setEnabledProviders, authStatuses, reloadAuthStatuses, }; } function syncEnabledProvidersFromAuthStatus( setEnabledProviders: React.Dispatch>>, statuses: AuthStatus[], ): void { setEnabledProviders((prev) => { const next = new Set(prev); for (const status of statuses) { const key = `oauth_${status.provider}`; if (status.oauthEnabled === true) { next.add(key); } else if (status.oauthEnabled === false) { next.delete(key); } } return next; }); } function useCloseAuthDialogOnEscape( onSelect: AuthDialogProps['onSelect'], ): void { useKeypress( (key) => { if (key.name === 'escape') { onSelect(undefined, SettingScope.User); } }, { isActive: true }, ); } export function AuthDialog({ onSelect, settings, initialErrorMessage, }: AuthDialogProps): React.JSX.Element { const runtime = useRuntimeApi(); const mountedRef = useMountedRef(); const [errorMessage, setErrorMessage] = useState( firstNonEmptyString(initialErrorMessage) ?? null, ); const { enabledProviders, setEnabledProviders, authStatuses, reloadAuthStatuses, } = useAuthDialogState(settings, runtime, mountedRef); const items = useMemo( () => buildAuthItems(enabledProviders, authStatuses), [enabledProviders, authStatuses], ); const handleAuthSelect = useCallback( (authMethod: string) => { setErrorMessage(null); if (authMethod === 'close') { onSelect(undefined, SettingScope.User); return; } const providerName = authMethod.replace('oauth_', ''); void (async () => { try { const enabled = await runtime .getCliOAuthManager() .toggleOAuthEnabled(providerName); if (!mountedRef.current) return; setEnabledProviders((prev) => { const next = new Set(prev); if (enabled) { next.add(authMethod); } else { next.delete(authMethod); } return next; }); await reloadAuthStatuses(); } catch (error) { if (mountedRef.current) { const errorMessage = error instanceof Error ? error.message : String(error); setErrorMessage( `Failed to toggle OAuth for ${providerName}: ${errorMessage}`, ); } } })(); }, [mountedRef, onSelect, reloadAuthStatuses, runtime, setEnabledProviders], ); useCloseAuthDialogOnEscape(onSelect); return ( ); }