diff --git a/src/components/onboarding/view/Onboarding.tsx b/src/components/onboarding/view/Onboarding.tsx index c97cc9b7..de5cbc42 100644 --- a/src/components/onboarding/view/Onboarding.tsx +++ b/src/components/onboarding/view/Onboarding.tsx @@ -1,14 +1,13 @@ import { Check, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { authenticatedFetch } from '../../../utils/api'; +import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus'; +import type { CliProvider } from '../../provider-auth/types'; import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal'; import AgentConnectionsStep from './subcomponents/AgentConnectionsStep'; import GitConfigurationStep from './subcomponents/GitConfigurationStep'; import OnboardingStepProgress from './subcomponents/OnboardingStepProgress'; -import type { CliProvider, ProviderStatusMap } from './types'; import { - cliProviders, - createInitialProviderStatuses, gitEmailPattern, readErrorMessageFromResponse, } from './utils'; @@ -24,59 +23,14 @@ export default function Onboarding({ onComplete }: OnboardingProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [activeLoginProvider, setActiveLoginProvider] = useState(null); - const [providerStatuses, setProviderStatuses] = useState(createInitialProviderStatuses); + const { + providerAuthStatus, + checkProviderAuthStatus, + refreshProviderAuthStatuses, + } = useProviderAuthStatus(); const previousActiveLoginProviderRef = useRef(undefined); - const checkProviderAuthStatus = useCallback(async (provider: CliProvider) => { - try { - const response = await authenticatedFetch(`/api/cli/${provider}/status`); - if (!response.ok) { - setProviderStatuses((previous) => ({ - ...previous, - [provider]: { - authenticated: false, - email: null, - loading: false, - error: 'Failed to check authentication status', - }, - })); - return; - } - - const payload = (await response.json()) as { - authenticated?: boolean; - email?: string | null; - error?: string | null; - }; - - setProviderStatuses((previous) => ({ - ...previous, - [provider]: { - authenticated: Boolean(payload.authenticated), - email: payload.email ?? null, - loading: false, - error: payload.error ?? null, - }, - })); - } catch (caughtError) { - console.error(`Error checking ${provider} auth status:`, caughtError); - setProviderStatuses((previous) => ({ - ...previous, - [provider]: { - authenticated: false, - email: null, - loading: false, - error: caughtError instanceof Error ? caughtError.message : 'Unknown error', - }, - })); - } - }, []); - - const refreshAllProviderStatuses = useCallback(async () => { - await Promise.all(cliProviders.map((provider) => checkProviderAuthStatus(provider))); - }, [checkProviderAuthStatus]); - const loadGitConfig = useCallback(async () => { try { const response = await authenticatedFetch('/api/user/git-config'); @@ -98,21 +52,22 @@ export default function Onboarding({ onComplete }: OnboardingProps) { useEffect(() => { void loadGitConfig(); - void refreshAllProviderStatuses(); - }, [loadGitConfig, refreshAllProviderStatuses]); + void refreshProviderAuthStatuses(); + }, [loadGitConfig, refreshProviderAuthStatuses]); useEffect(() => { const previousProvider = previousActiveLoginProviderRef.current; previousActiveLoginProviderRef.current = activeLoginProvider; - const isInitialMount = previousProvider === undefined; - const didCloseModal = previousProvider !== null && activeLoginProvider === null; + const didCloseModal = previousProvider !== undefined + && previousProvider !== null + && activeLoginProvider === null; - // Refresh statuses once on mount and again after the login modal is closed. - if (isInitialMount || didCloseModal) { - void refreshAllProviderStatuses(); + // Refresh statuses after the login modal is closed. + if (didCloseModal) { + void refreshProviderAuthStatuses(); } - }, [activeLoginProvider, refreshAllProviderStatuses]); + }, [activeLoginProvider, refreshProviderAuthStatuses]); const handleProviderLoginOpen = (provider: CliProvider) => { setActiveLoginProvider(provider); @@ -208,7 +163,7 @@ export default function Onboarding({ onComplete }: OnboardingProps) { /> ) : ( )} diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx index edf2ef0e..55c40702 100644 --- a/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx +++ b/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx @@ -1,6 +1,6 @@ import { Check } from 'lucide-react'; import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo'; -import type { CliProvider, ProviderAuthStatus } from '../types'; +import type { CliProvider, ProviderAuthStatus } from '../../../provider-auth/types'; type AgentConnectionCardProps = { provider: CliProvider; diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx index 5bca5d33..0443facc 100644 --- a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx +++ b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx @@ -1,8 +1,8 @@ -import type { CliProvider, ProviderStatusMap } from '../types'; +import type { CliProvider, ProviderAuthStatusMap } from '../../../provider-auth/types'; import AgentConnectionCard from './AgentConnectionCard'; type AgentConnectionsStepProps = { - providerStatuses: ProviderStatusMap; + providerStatuses: ProviderAuthStatusMap; onOpenProviderLogin: (provider: CliProvider) => void; }; diff --git a/src/components/onboarding/view/types.ts b/src/components/onboarding/view/types.ts deleted file mode 100644 index 46800813..00000000 --- a/src/components/onboarding/view/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { CliProvider } from '../../provider-auth/types'; - -export type { CliProvider }; - -export type ProviderAuthStatus = { - authenticated: boolean; - email: string | null; - loading: boolean; - error: string | null; -}; - -export type ProviderStatusMap = Record; diff --git a/src/components/onboarding/view/utils.ts b/src/components/onboarding/view/utils.ts index 054b9a9d..0b40ab02 100644 --- a/src/components/onboarding/view/utils.ts +++ b/src/components/onboarding/view/utils.ts @@ -1,16 +1,5 @@ -import type { CliProvider, ProviderStatusMap } from './types'; - -export const cliProviders: CliProvider[] = ['claude', 'cursor', 'codex', 'gemini']; - export const gitEmailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -export const createInitialProviderStatuses = (): ProviderStatusMap => ({ - claude: { authenticated: false, email: null, loading: true, error: null }, - cursor: { authenticated: false, email: null, loading: true, error: null }, - codex: { authenticated: false, email: null, loading: true, error: null }, - gemini: { authenticated: false, email: null, loading: true, error: null }, -}); - export const readErrorMessageFromResponse = async (response: Response, fallback: string) => { try { const payload = (await response.json()) as { error?: string }; diff --git a/src/components/provider-auth/hooks/useProviderAuthStatus.ts b/src/components/provider-auth/hooks/useProviderAuthStatus.ts new file mode 100644 index 00000000..a162b75e --- /dev/null +++ b/src/components/provider-auth/hooks/useProviderAuthStatus.ts @@ -0,0 +1,109 @@ +import { useCallback, useState } from 'react'; +import { authenticatedFetch } from '../../../utils/api'; +import { + CLI_AUTH_STATUS_ENDPOINTS, + CLI_PROVIDERS, + createInitialProviderAuthStatusMap, +} from '../types'; +import type { + CliProvider, + ProviderAuthStatus, + ProviderAuthStatusMap, +} from '../types'; + +type ProviderAuthStatusPayload = { + authenticated?: boolean; + email?: string | null; + method?: string | null; + error?: string | null; +}; + +const FALLBACK_STATUS_ERROR = 'Failed to check authentication status'; +const FALLBACK_UNKNOWN_ERROR = 'Unknown error'; + +const toErrorMessage = (error: unknown): string => ( + error instanceof Error ? error.message : FALLBACK_UNKNOWN_ERROR +); + +const toProviderAuthStatus = ( + payload: ProviderAuthStatusPayload, + fallbackError: string | null = null, +): ProviderAuthStatus => ({ + authenticated: Boolean(payload.authenticated), + email: payload.email ?? null, + method: payload.method ?? null, + error: payload.error ?? fallbackError, + loading: false, +}); + +type UseProviderAuthStatusOptions = { + initialLoading?: boolean; +}; + +export function useProviderAuthStatus( + { initialLoading = true }: UseProviderAuthStatusOptions = {}, +) { + const [providerAuthStatus, setProviderAuthStatus] = useState(() => ( + createInitialProviderAuthStatusMap(initialLoading) + )); + + const setProviderLoading = useCallback((provider: CliProvider) => { + setProviderAuthStatus((previous) => ({ + ...previous, + [provider]: { + ...previous[provider], + loading: true, + error: null, + }, + })); + }, []); + + const setProviderStatus = useCallback((provider: CliProvider, status: ProviderAuthStatus) => { + setProviderAuthStatus((previous) => ({ + ...previous, + [provider]: status, + })); + }, []); + + const checkProviderAuthStatus = useCallback(async (provider: CliProvider) => { + setProviderLoading(provider); + + try { + const response = await authenticatedFetch(CLI_AUTH_STATUS_ENDPOINTS[provider]); + + if (!response.ok) { + setProviderStatus(provider, { + authenticated: false, + email: null, + method: null, + loading: false, + error: FALLBACK_STATUS_ERROR, + }); + return; + } + + const payload = (await response.json()) as ProviderAuthStatusPayload; + setProviderStatus(provider, toProviderAuthStatus(payload)); + } catch (caughtError) { + console.error(`Error checking ${provider} auth status:`, caughtError); + setProviderStatus(provider, { + authenticated: false, + email: null, + method: null, + loading: false, + error: toErrorMessage(caughtError), + }); + } + }, [setProviderLoading, setProviderStatus]); + + const refreshProviderAuthStatuses = useCallback(async (providers: CliProvider[] = CLI_PROVIDERS) => { + await Promise.all(providers.map((provider) => checkProviderAuthStatus(provider))); + }, [checkProviderAuthStatus]); + + return { + providerAuthStatus, + setProviderAuthStatus, + checkProviderAuthStatus, + refreshProviderAuthStatuses, + }; +} diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts index e39a9796..e7ee49f2 100644 --- a/src/components/provider-auth/types.ts +++ b/src/components/provider-auth/types.ts @@ -1 +1,27 @@ export type CliProvider = 'claude' | 'cursor' | 'codex' | 'gemini'; + +export type ProviderAuthStatus = { + authenticated: boolean; + email: string | null; + method: string | null; + error: string | null; + loading: boolean; +}; + +export type ProviderAuthStatusMap = Record; + +export const CLI_PROVIDERS: CliProvider[] = ['claude', 'cursor', 'codex', 'gemini']; + +export const CLI_AUTH_STATUS_ENDPOINTS: Record = { + claude: '/api/cli/claude/status', + cursor: '/api/cli/cursor/status', + codex: '/api/cli/codex/status', + gemini: '/api/cli/gemini/status', +}; + +export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuthStatusMap => ({ + claude: { authenticated: false, email: null, method: null, error: null, loading }, + cursor: { authenticated: false, email: null, method: null, error: null, loading }, + codex: { authenticated: false, email: null, method: null, error: null, loading }, + gemini: { authenticated: false, email: null, method: null, error: null, loading }, +}); diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts index 36f45392..52f16e10 100644 --- a/src/components/settings/constants/constants.ts +++ b/src/components/settings/constants/constants.ts @@ -1,7 +1,6 @@ import type { AgentCategory, AgentProvider, - AuthStatus, ClaudeMcpFormState, CodexMcpFormState, CodeEditorSettingsState, @@ -34,13 +33,6 @@ export const DEFAULT_CODE_EDITOR_SETTINGS: CodeEditorSettingsState = { fontSize: '14', }; -export const DEFAULT_AUTH_STATUS: AuthStatus = { - authenticated: false, - email: null, - loading: true, - error: null, -}; - export const DEFAULT_MCP_TEST_RESULT: McpTestResult = { success: false, message: '', @@ -88,9 +80,3 @@ export const DEFAULT_CURSOR_PERMISSIONS: CursorPermissionsState = { skipPermissions: false, }; -export const AUTH_STATUS_ENDPOINTS: Record = { - claude: '/api/cli/claude/status', - cursor: '/api/cli/cursor/status', - codex: '/api/cli/codex/status', - gemini: '/api/cli/gemini/status', -}; diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index 26f1fe1a..09550265 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -1,15 +1,13 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useTheme } from '../../../contexts/ThemeContext'; import { authenticatedFetch } from '../../../utils/api'; +import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus'; import { - AUTH_STATUS_ENDPOINTS, - DEFAULT_AUTH_STATUS, DEFAULT_CODE_EDITOR_SETTINGS, DEFAULT_CURSOR_PERMISSIONS, } from '../constants/constants'; import type { AgentProvider, - AuthStatus, ClaudeMcpFormState, ClaudePermissionsState, CodeEditorSettingsState, @@ -35,13 +33,6 @@ type UseSettingsControllerArgs = { initialTab: string; }; -type StatusApiResponse = { - authenticated?: boolean; - email?: string | null; - error?: string | null; - method?: string; -}; - type JsonResult = { success?: boolean; error?: string; @@ -225,63 +216,11 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl const [showLoginModal, setShowLoginModal] = useState(false); const [loginProvider, setLoginProvider] = useState(''); - - const [claudeAuthStatus, setClaudeAuthStatus] = useState(DEFAULT_AUTH_STATUS); - const [cursorAuthStatus, setCursorAuthStatus] = useState(DEFAULT_AUTH_STATUS); - const [codexAuthStatus, setCodexAuthStatus] = useState(DEFAULT_AUTH_STATUS); - const [geminiAuthStatus, setGeminiAuthStatus] = useState(DEFAULT_AUTH_STATUS); - - const setAuthStatusByProvider = useCallback((provider: AgentProvider, status: AuthStatus) => { - if (provider === 'claude') { - setClaudeAuthStatus(status); - return; - } - - if (provider === 'cursor') { - setCursorAuthStatus(status); - return; - } - - if (provider === 'gemini') { - setGeminiAuthStatus(status); - return; - } - - setCodexAuthStatus(status); - }, []); - - const checkAuthStatus = useCallback(async (provider: AgentProvider) => { - try { - const response = await authenticatedFetch(AUTH_STATUS_ENDPOINTS[provider]); - - if (!response.ok) { - setAuthStatusByProvider(provider, { - authenticated: false, - email: null, - loading: false, - error: 'Failed to check authentication status', - }); - return; - } - - const data = await toResponseJson(response); - setAuthStatusByProvider(provider, { - authenticated: Boolean(data.authenticated), - email: data.email || null, - loading: false, - error: data.error || null, - method: data.method, - }); - } catch (error) { - console.error(`Error checking ${provider} auth status:`, error); - setAuthStatusByProvider(provider, { - authenticated: false, - email: null, - loading: false, - error: getErrorMessage(error), - }); - } - }, [setAuthStatusByProvider]); + const { + providerAuthStatus, + checkProviderAuthStatus, + refreshProviderAuthStatuses, + } = useProviderAuthStatus(); const fetchCursorMcpServers = useCallback(async () => { try { @@ -715,8 +654,8 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl } setSaveStatus('success'); - void checkAuthStatus(loginProvider); - }, [checkAuthStatus, loginProvider]); + void checkProviderAuthStatus(loginProvider); + }, [checkProviderAuthStatus, loginProvider]); const saveSettings = useCallback(async () => { setSaveStatus(null); @@ -808,11 +747,8 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl setActiveTab(normalizeMainTab(initialTab)); void loadSettings(); - void checkAuthStatus('claude'); - void checkAuthStatus('cursor'); - void checkAuthStatus('codex'); - void checkAuthStatus('gemini'); - }, [checkAuthStatus, initialTab, isOpen, loadSettings]); + void refreshProviderAuthStatuses(); + }, [initialTab, isOpen, loadSettings, refreshProviderAuthStatuses]); useEffect(() => { localStorage.setItem('codeEditorTheme', codeEditorSettings.theme); @@ -916,10 +852,7 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl closeCodexMcpForm, submitCodexMcpForm, handleCodexMcpDelete, - claudeAuthStatus, - cursorAuthStatus, - codexAuthStatus, - geminiAuthStatus, + providerAuthStatus, geminiPermissionMode, setGeminiPermissionMode, openLoginForProvider, diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts index e3af730b..d6328644 100644 --- a/src/components/settings/types/types.ts +++ b/src/components/settings/types/types.ts @@ -1,7 +1,8 @@ import type { Dispatch, SetStateAction } from 'react'; +import type { CliProvider, ProviderAuthStatus } from '../../provider-auth/types'; export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins' | 'about'; -export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini'; +export type AgentProvider = CliProvider; export type AgentCategory = 'account' | 'permissions' | 'mcp'; export type ProjectSortOrder = 'name' | 'date'; export type SaveStatus = 'success' | 'error' | null; @@ -18,13 +19,7 @@ export type SettingsProject = { path?: string; }; -export type AuthStatus = { - authenticated: boolean; - email: string | null; - loading: boolean; - error: string | null; - method?: string; -}; +export type AuthStatus = ProviderAuthStatus; export type KeyValueMap = Record; diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx index 1febfa6a..c1977cf2 100644 --- a/src/components/settings/view/Settings.tsx +++ b/src/components/settings/view/Settings.tsx @@ -56,10 +56,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set closeCodexMcpForm, submitCodexMcpForm, handleCodexMcpDelete, - claudeAuthStatus, - cursorAuthStatus, - codexAuthStatus, - geminiAuthStatus, + providerAuthStatus, geminiPermissionMode, setGeminiPermissionMode, openLoginForProvider, @@ -102,13 +99,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set return null; } - const isAuthenticated = loginProvider === 'claude' - ? claudeAuthStatus.authenticated - : loginProvider === 'cursor' - ? cursorAuthStatus.authenticated - : loginProvider === 'codex' - ? codexAuthStatus.authenticated - : false; + const isAuthenticated = Boolean(loginProvider && providerAuthStatus[loginProvider].authenticated); return (
@@ -155,14 +146,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set {activeTab === 'agents' && ( openLoginForProvider('claude')} - onCursorLogin={() => openLoginForProvider('cursor')} - onCodexLogin={() => openLoginForProvider('codex')} - onGeminiLogin={() => openLoginForProvider('gemini')} + providerAuthStatus={providerAuthStatus} + onProviderLogin={openLoginForProvider} claudePermissions={claudePermissions} onClaudePermissionsChange={setClaudePermissions} cursorPermissions={cursorPermissions} diff --git a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx index dbf098cb..c1ad2e35 100644 --- a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx +++ b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx @@ -6,14 +6,8 @@ import AgentSelectorSection from './sections/AgentSelectorSection'; import type { AgentContext, AgentsSettingsTabProps } from './types'; export default function AgentsSettingsTab({ - claudeAuthStatus, - cursorAuthStatus, - codexAuthStatus, - geminiAuthStatus, - onClaudeLogin, - onCursorLogin, - onCodexLogin, - onGeminiLogin, + providerAuthStatus, + onProviderLogin, claudePermissions, onClaudePermissionsChange, cursorPermissions, @@ -41,30 +35,27 @@ export default function AgentsSettingsTab({ const agentContextById = useMemo>(() => ({ claude: { - authStatus: claudeAuthStatus, - onLogin: onClaudeLogin, + authStatus: providerAuthStatus.claude, + onLogin: () => onProviderLogin('claude'), }, cursor: { - authStatus: cursorAuthStatus, - onLogin: onCursorLogin, + authStatus: providerAuthStatus.cursor, + onLogin: () => onProviderLogin('cursor'), }, codex: { - authStatus: codexAuthStatus, - onLogin: onCodexLogin, + authStatus: providerAuthStatus.codex, + onLogin: () => onProviderLogin('codex'), }, gemini: { - authStatus: geminiAuthStatus, - onLogin: onGeminiLogin, + authStatus: providerAuthStatus.gemini, + onLogin: () => onProviderLogin('gemini'), }, }), [ - claudeAuthStatus, - codexAuthStatus, - cursorAuthStatus, - geminiAuthStatus, - onClaudeLogin, - onCodexLogin, - onCursorLogin, - onGeminiLogin, + onProviderLogin, + providerAuthStatus.claude, + providerAuthStatus.codex, + providerAuthStatus.cursor, + providerAuthStatus.gemini, ]); return ( diff --git a/src/components/settings/view/tabs/agents-settings/types.ts b/src/components/settings/view/tabs/agents-settings/types.ts index 4bf0c5c9..a2cecdc8 100644 --- a/src/components/settings/view/tabs/agents-settings/types.ts +++ b/src/components/settings/view/tabs/agents-settings/types.ts @@ -17,16 +17,11 @@ export type AgentContext = { }; export type AgentContextByProvider = Record; +export type ProviderAuthStatusByProvider = Record; export type AgentsSettingsTabProps = { - claudeAuthStatus: AuthStatus; - cursorAuthStatus: AuthStatus; - codexAuthStatus: AuthStatus; - geminiAuthStatus: AuthStatus; - onClaudeLogin: () => void; - onCursorLogin: () => void; - onCodexLogin: () => void; - onGeminiLogin: () => void; + providerAuthStatus: ProviderAuthStatusByProvider; + onProviderLogin: (provider: AgentProvider) => void; claudePermissions: ClaudePermissionsState; onClaudePermissionsChange: (value: ClaudePermissionsState) => void; cursorPermissions: CursorPermissionsState;