import { useEffect, useRef, useState } from 'react'; import { Loader2, Plus, RefreshCw, Stethoscope } from 'lucide-react'; import { api } from '@/api/client'; import type { CoderProvidersFile, ProviderOverride, ProviderSnapshotEntry } from '@/api/types'; import { useProviderSnapshot, refreshProviderSnapshot } from '@/hooks/useProviderSnapshot'; import { Button } from '@/components/ui/button'; import { AddProviderModal } from './AddProviderModal'; import { cn } from '@/lib/utils'; /** Map a snapshot entry to a status badge (design.md §7.1 labels). */ function statusBadge(e: ProviderSnapshotEntry): { label: string; cls: string } { if (e.status === 'loading') return { label: 'Loading', cls: 'bg-muted text-muted-foreground' }; if (!e.enabled) return { label: 'Disabled', cls: 'bg-muted text-muted-foreground' }; if (e.status === 'ready') return { label: 'Available', cls: 'bg-green-500/15 text-green-600 dark:text-green-400' }; if (e.status === 'error') return { label: 'Error', cls: 'bg-red-500/15 text-red-600 dark:text-red-400' }; if (!e.installed) return { label: 'Not installed', cls: 'bg-amber-500/15 text-amber-600 dark:text-amber-400' }; return { label: 'Unavailable', cls: 'bg-muted text-muted-foreground' }; } /** * v2.3 — provider management as a Settings tab section (design.md §7.1). Lists * ALL registered providers (including the disabled/unavailable ones the composer * picker hides). Per row: label + model count, status badge, per-id refresh, * diagnostic, and an enable/disable toggle. Native boocode is always-on. * * Uses the home-cwd snapshot (no project arg) — provider management is global, * not per-project (design.md §4.5). */ export function ProvidersSettings() { const allEntries = useProviderSnapshot(); const [config, setConfig] = useState(null); const [busyId, setBusyId] = useState(null); const [error, setError] = useState(null); const [addOpen, setAddOpen] = useState(false); const [diagId, setDiagId] = useState(null); const [diagText, setDiagText] = useState(null); // The raw config is needed to preserve a provider's FULL override when // toggling: the PATCH replaces an id's override wholesale, so a bare // { enabled } would wipe a custom ACP provider's command/label. useEffect(() => { api.coder .getProvidersConfig() .then(setConfig) .catch(() => setConfig({ providers: {} })); }, []); // While any entry is loading, refetch until terminal (capped, no WS frame). const pollsRef = useRef(0); useEffect(() => { const anyLoading = allEntries?.some((e) => e.status === 'loading') ?? false; if (!anyLoading) { pollsRef.current = 0; return; } if (pollsRef.current >= 10) return; const t = setTimeout(() => { pollsRef.current += 1; void refreshProviderSnapshot(); }, 2000); return () => clearTimeout(t); }, [allEntries]); async function toggle(e: ProviderSnapshotEntry): Promise { setBusyId(e.name); setError(null); try { const existing: ProviderOverride = config?.providers[e.name] ?? {}; const resp = await api.coder.patchProvidersConfig({ providers: { [e.name]: { ...existing, enabled: !e.enabled } }, }); setConfig({ providers: resp.providers }); await refreshProviderSnapshot(); } catch (err) { setError(err instanceof Error ? err.message : 'failed to update provider'); } finally { setBusyId(null); } } async function refreshOne(id: string): Promise { setBusyId(id); setError(null); try { await api.coder.refreshProviders([id]); await refreshProviderSnapshot(); } catch (err) { setError(err instanceof Error ? err.message : 'failed to refresh'); } finally { setBusyId(null); } } async function openDiagnostic(id: string): Promise { if (diagId === id) { setDiagId(null); setDiagText(null); return; } setDiagId(id); setDiagText('Loading…'); try { const { diagnostic } = await api.coder.getProviderDiagnostic(id); setDiagText(diagnostic); } catch (err) { setDiagText(err instanceof Error ? err.message : 'failed to load diagnostic'); } } const entries = allEntries ?? []; return (

Enable, disable, refresh, or add coding agents. Disabled and unavailable providers are hidden from the composer picker but managed here.

{allEntries === null && (
Loading…
)} {entries.map((e) => { const badge = statusBadge(e); const isNative = e.transport === 'native'; const busy = busyId === e.name; return (
{e.label}
{e.models.length} model{e.models.length === 1 ? '' : 's'}
{e.status === 'loading' && } {badge.label} {isNative ? ( Always on ) : ( )}
{diagId === e.name && (
                  {diagText}
                
)}
); })}
{error &&
{error}
} void refreshProviderSnapshot()} />
); }