import { useCallback, useEffect, useRef, useState } from 'react'; import { authenticatedFetch } from '../../../utils/api'; import type { PendingPermissionRequest, PermissionMode } from '../types/types'; import type { ProjectSession, LLMProvider, Project, ProviderModelsCacheInfo, ProviderModelsDefinition, } from '../../../types/app'; const FALLBACK_DEFAULT_MODEL: Record = { claude: 'opus', cursor: 'gpt-5.3-codex', codex: 'gpt-5.4', gemini: 'gemini-3.1-pro-preview', opencode: 'anthropic/claude-sonnet-4-5', }; const getPermissionModesForProvider = (provider: LLMProvider): PermissionMode[] => { if (provider === 'codex') { return ['default', 'acceptEdits', 'bypassPermissions']; } if (provider === 'claude') { return ['default', 'auto', 'acceptEdits', 'bypassPermissions', 'plan']; } if (provider === 'opencode') { return ['default']; } return ['default', 'acceptEdits', 'bypassPermissions', 'plan']; }; interface UseChatProviderStateArgs { selectedSession: ProjectSession | null; selectedProject: Project | null; } type ProviderModelsApiResponse = { success?: boolean; data?: { models?: ProviderModelsDefinition; cache?: ProviderModelsCacheInfo; }; }; export function useChatProviderState({ selectedSession, selectedProject }: UseChatProviderStateArgs) { const [permissionMode, setPermissionMode] = useState('default'); const [pendingPermissionRequests, setPendingPermissionRequests] = useState([]); const [provider, setProvider] = useState(() => { return (localStorage.getItem('selected-provider') as LLMProvider) || 'claude'; }); const [cursorModel, setCursorModel] = useState(() => { return localStorage.getItem('cursor-model') || FALLBACK_DEFAULT_MODEL.cursor; }); const [claudeModel, setClaudeModel] = useState(() => { return localStorage.getItem('claude-model') || FALLBACK_DEFAULT_MODEL.claude; }); const [codexModel, setCodexModel] = useState(() => { return localStorage.getItem('codex-model') || FALLBACK_DEFAULT_MODEL.codex; }); const [geminiModel, setGeminiModel] = useState(() => { return localStorage.getItem('gemini-model') || FALLBACK_DEFAULT_MODEL.gemini; }); const [opencodeModel, setOpenCodeModel] = useState(() => { return localStorage.getItem('opencode-model') || FALLBACK_DEFAULT_MODEL.opencode; }); const [providerModelCatalog, setProviderModelCatalog] = useState< Partial> >({}); const [providerModelCacheCatalog, setProviderModelCacheCatalog] = useState< Partial> >({}); const [providerModelsLoading, setProviderModelsLoading] = useState(true); const [providerModelsRefreshing, setProviderModelsRefreshing] = useState(false); const lastProviderRef = useRef(provider); const providerModelsRequestIdRef = useRef(0); const loadProviderModels = useCallback(async (options: { bypassCache?: boolean } = {}) => { const providers: LLMProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'opencode']; const requestId = providerModelsRequestIdRef.current + 1; providerModelsRequestIdRef.current = requestId; const isHardRefresh = options.bypassCache === true; if (isHardRefresh) { setProviderModelsRefreshing(true); } else { setProviderModelsLoading(true); } try { const results = await Promise.all( providers.map(async (p) => { const params = new URLSearchParams(); if (options.bypassCache) { params.set('bypassCache', 'true'); } const queryString = params.toString(); const response = await authenticatedFetch(`/api/providers/${p}/models${queryString ? `?${queryString}` : ''}`); const body = (await response.json()) as ProviderModelsApiResponse; if (!body.success || !body.data?.models || !body.data?.cache) { return null; } return body.data; }), ); if (providerModelsRequestIdRef.current !== requestId) { return; } const nextCatalog: Partial> = {}; const nextCacheCatalog: Partial> = {}; providers.forEach((p, i) => { const entry = results[i]; if (!entry) { return; } nextCatalog[p] = entry.models; nextCacheCatalog[p] = entry.cache; }); setProviderModelCatalog(nextCatalog); setProviderModelCacheCatalog(nextCacheCatalog); } catch (error) { console.error('Error loading provider models:', error); } finally { if (providerModelsRequestIdRef.current === requestId) { setProviderModelsLoading(false); setProviderModelsRefreshing(false); } } }, []); useEffect(() => { void loadProviderModels(); }, [loadProviderModels]); const pickStoredOrCurrent = ( storageKey: string, current: string, def: ProviderModelsDefinition, ): string => { const stored = localStorage.getItem(storageKey); if (stored && def.OPTIONS.some((o) => o.value === stored)) { return stored; } if (current && def.OPTIONS.some((o) => o.value === current)) { return current; } return def.DEFAULT; }; useEffect(() => { const claude = providerModelCatalog.claude; if (claude) { const next = pickStoredOrCurrent('claude-model', claudeModel, claude); if (next !== claudeModel) { setClaudeModel(next); } if (localStorage.getItem('claude-model') !== next) { localStorage.setItem('claude-model', next); } } }, [providerModelCatalog.claude, claudeModel]); useEffect(() => { const cursor = providerModelCatalog.cursor; if (cursor) { const next = pickStoredOrCurrent('cursor-model', cursorModel, cursor); if (next !== cursorModel) { setCursorModel(next); } if (localStorage.getItem('cursor-model') !== next) { localStorage.setItem('cursor-model', next); } } }, [providerModelCatalog.cursor, cursorModel]); useEffect(() => { const codex = providerModelCatalog.codex; if (codex) { const next = pickStoredOrCurrent('codex-model', codexModel, codex); if (next !== codexModel) { setCodexModel(next); } if (localStorage.getItem('codex-model') !== next) { localStorage.setItem('codex-model', next); } } }, [providerModelCatalog.codex, codexModel]); useEffect(() => { const gemini = providerModelCatalog.gemini; if (gemini) { const next = pickStoredOrCurrent('gemini-model', geminiModel, gemini); if (next !== geminiModel) { setGeminiModel(next); } if (localStorage.getItem('gemini-model') !== next) { localStorage.setItem('gemini-model', next); } } }, [providerModelCatalog.gemini, geminiModel]); useEffect(() => { const opencode = providerModelCatalog.opencode; if (opencode) { const next = pickStoredOrCurrent('opencode-model', opencodeModel, opencode); if (next !== opencodeModel) { setOpenCodeModel(next); } if (localStorage.getItem('opencode-model') !== next) { localStorage.setItem('opencode-model', next); } } }, [providerModelCatalog.opencode, opencodeModel]); useEffect(() => { if (!selectedSession?.id) { return; } const savedMode = localStorage.getItem(`permissionMode-${selectedSession.id}`) as PermissionMode | null; const validModes = getPermissionModesForProvider(provider); setPermissionMode(savedMode && validModes.includes(savedMode) ? savedMode : 'default'); }, [selectedSession?.id, provider]); useEffect(() => { if (!selectedSession?.__provider || selectedSession.__provider === provider) { return; } setProvider(selectedSession.__provider); localStorage.setItem('selected-provider', selectedSession.__provider); }, [provider, selectedSession]); useEffect(() => { if (lastProviderRef.current === provider) { return; } setPendingPermissionRequests([]); lastProviderRef.current = provider; }, [provider]); useEffect(() => { setPendingPermissionRequests((previous) => previous.filter((request) => !request.sessionId || request.sessionId === selectedSession?.id), ); }, [selectedSession?.id]); useEffect(() => { if (provider !== 'cursor') { return; } authenticatedFetch('/api/cursor/config') .then((response) => response.json()) .then((data) => { if (!data.success || !data.config?.model?.modelId) { return; } const modelId = data.config.model.modelId as string; if (!localStorage.getItem('cursor-model')) { setCursorModel(modelId); } }) .catch((error) => { console.error('Error loading Cursor config:', error); }); }, [provider]); const cyclePermissionMode = useCallback(() => { const modes = getPermissionModesForProvider(provider); const currentIndex = modes.indexOf(permissionMode); const nextIndex = (currentIndex + 1) % modes.length; const nextMode = modes[nextIndex]; setPermissionMode(nextMode); if (selectedSession?.id) { localStorage.setItem(`permissionMode-${selectedSession.id}`, nextMode); } }, [permissionMode, provider, selectedSession?.id]); return { provider, setProvider, cursorModel, setCursorModel, claudeModel, setClaudeModel, codexModel, setCodexModel, geminiModel, setGeminiModel, opencodeModel, setOpenCodeModel, permissionMode, setPermissionMode, pendingPermissionRequests, setPendingPermissionRequests, cyclePermissionMode, providerModelCatalog, providerModelCacheCatalog, providerModelsLoading, providerModelsRefreshing, hardRefreshProviderModels: () => loadProviderModels({ bypassCache: true }), }; }