import React from 'react'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { Button } from '@/components/ui/button'; import { selectProvidersForDirectory, useConfigStore } from '@/stores/useConfigStore'; import { useSettingsDirectory } from '@/hooks/useSettingsDirectory'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { cn } from '@/lib/utils'; import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector'; import { Icon } from "@/components/icon/Icon"; import { opencodeClient } from '@/lib/opencode/client'; import { useI18n } from '@/lib/i18n'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { SETTINGS_PANEL_TITLE_CLASS } from '@/components/sections/shared/SettingsSection'; const ADD_PROVIDER_ID = '__add_provider__'; interface ProviderSourceInfo { exists: boolean; path?: string | null; } interface ProviderSources { auth: ProviderSourceInfo; user: ProviderSourceInfo; project: ProviderSourceInfo; custom?: ProviderSourceInfo; } const getCurrentDirectory = (): string | null => { const dir = opencodeClient.getDirectory(); if (typeof dir === 'string' && dir.trim().length > 0) { return dir.trim(); } return null; }; interface ProvidersSidebarProps { onItemSelect?: () => void; } export const ProvidersSidebar: React.FC = ({ onItemSelect }) => { const { t } = useI18n(); // Settings browses whichever project its own selector points at; the app // stays where it is. const settingsDirectory = useSettingsDirectory(); const providers = useConfigStore((state) => selectProvidersForDirectory(state, settingsDirectory)); const selectedProviderId = useConfigStore((state) => state.selectedProviderId); const setSelectedProvider = useConfigStore((state) => state.setSelectedProvider); const activeProjectId = useProjectsStore((s) => s.activeProjectId); const [sourcesByProvider, setSourcesByProvider] = React.useState>({}); const directory = React.useMemo(() => { if (settingsDirectory) return settingsDirectory; // tie refresh to active project changes (directory is stored in the client) void activeProjectId; return getCurrentDirectory(); }, [activeProjectId, settingsDirectory]); // The app only loads providers for the project it is on; Settings has to ask // for the one it is looking at. const loadProviders = useConfigStore((state) => state.loadProviders); React.useEffect(() => { if (!settingsDirectory) return; void loadProviders({ directory: settingsDirectory, source: 'settings:providers' }); }, [loadProviders, settingsDirectory]); React.useEffect(() => { if (providers.length === 0) { setSourcesByProvider({}); return; } let cancelled = false; const loadAllSources = async () => { const tasks = providers.map(async (provider) => { try { const query = directory ? `?directory=${encodeURIComponent(directory)}` : ''; // OpenChamber-only metadata endpoint: the SDK exposes provider data but // not local auth/source-file provenance used by this settings sidebar. const response = await runtimeFetch(`/api/provider/${encodeURIComponent(provider.id)}/source${query}`, { method: 'GET', headers: { Accept: 'application/json' }, }); if (!response.ok) { return; } const payload = await response.json().catch(() => null); const sources = (payload?.sources ?? payload?.data?.sources) as ProviderSources | undefined; if (!sources) { return; } if (cancelled) { return; } setSourcesByProvider((prev) => ({ ...prev, [provider.id]: sources, })); } catch { // ignore } }); await Promise.all(tasks); }; void loadAllSources(); return () => { cancelled = true; }; }, [directory, providers]); const bgClass = 'bg-background'; const projectProviders = React.useMemo(() => { return providers.filter((p) => Boolean(sourcesByProvider[p.id]?.project?.exists)); }, [providers, sourcesByProvider]); const userProviders = React.useMemo(() => { return providers.filter((p) => !sourcesByProvider[p.id]?.project?.exists); }, [providers, sourcesByProvider]); return (

{t('settings.providers.sidebar.title')}

{t('settings.providers.sidebar.total', { count: providers.length })}
{providers.length === 0 ? (

{t('settings.providers.sidebar.empty.title')}

{t('settings.providers.sidebar.empty.description')}

) : ( <> {userProviders.length > 0 && ( <>
{t('settings.providers.sidebar.section.userProviders')}
{userProviders.map((provider) => ( { setSelectedProvider(provider.id); onItemSelect?.(); }} /> ))} )} {projectProviders.length > 0 && ( <>
0 ? 'pt-3' : 'pt-2')}> {t('settings.providers.sidebar.section.projectProviders')}
{projectProviders.map((provider) => ( { setSelectedProvider(provider.id); onItemSelect?.(); }} /> ))} )} )}
); }; const ProviderListItem: React.FC<{ provider: { id: string; name?: string; models?: unknown[] }; selectedProviderId: string; onSelect: () => void; }> = ({ provider, selectedProviderId, onSelect }) => { const modelCount = Array.isArray(provider.models) ? provider.models.length : 0; const isSelected = provider.id === selectedProviderId; return (
); };