web(coder): provider settings UI — Settings → Providers tab, picker filter, ACP catalog
v2.3 Phase 5. Provider management lives in Settings → Providers: lists every registered provider with a status badge, enable/disable toggle (sends the full override so a custom ACP entry's command survives the wholesale-replace PATCH), per-provider refresh, and a plaintext diagnostic. The composer provider picker now filters to enabled && (status==='ready' || 'loading') — disabled/unavailable providers leave the picker and are managed only in settings; native boocode always shows. Adds a curated ACP catalog + AddProviderModal (PATCH config then subset refresh; the modal caps to the viewport with a single overscroll-contain scroll region). Loading state uses a capped client poll (no WS frame). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Check, ChevronDown, RefreshCw, Shield, Brain, Bird, Bot, Dog, Terminal as TermIcon } from 'lucide-react';
|
||||
import { Check, ChevronDown, RefreshCw, Loader2, Shield, Brain, Bird, Bot, Dog, Terminal as TermIcon } from 'lucide-react';
|
||||
import { ClaudeIcon, OpenCodeIcon } from '@/components/icons/ProviderIcons';
|
||||
import { api } from '@/api/client';
|
||||
import type { AgentSessionConfig, ProviderSnapshotEntry, AgentCommand } from '@/api/types';
|
||||
@@ -176,8 +176,12 @@ interface Props {
|
||||
|
||||
export function AgentComposerBar({ projectPath, value, onChange, onProviderCommandsChange, connected }: Props) {
|
||||
const allEntries = useProviderSnapshot(projectPath);
|
||||
// 5.5 — the composer picker only offers ENABLED providers that are ready (or
|
||||
// still loading). Disabled (enabled:false) and unavailable/error providers are
|
||||
// hidden here and managed in Settings → Providers. Native boocode is always
|
||||
// enabled+ready, so it always appears.
|
||||
const entries = useMemo(
|
||||
() => allEntries?.filter((e) => e.installed && e.status !== 'error') ?? null,
|
||||
() => allEntries?.filter((e) => e.enabled && (e.status === 'ready' || e.status === 'loading')) ?? null,
|
||||
[allEntries],
|
||||
);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -200,6 +204,35 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
|
||||
onChange(resolveConfig(entry, prefs));
|
||||
}, [entries, onChange, value.provider]);
|
||||
|
||||
// If the active provider is disabled in the settings drawer it drops out of
|
||||
// `entries` (the 5.5 filter) — fall back to boocode so the composer never
|
||||
// strands on an unselectable provider with empty model/mode pickers.
|
||||
useEffect(() => {
|
||||
if (!entries?.length) return;
|
||||
if (entries.some((e) => e.name === value.provider)) return;
|
||||
const fallback = entries.find((e) => e.name === 'boocode') ?? entries[0];
|
||||
if (!fallback) return;
|
||||
onChange(resolveConfig(fallback, loadPrefs()));
|
||||
}, [entries, value.provider, onChange]);
|
||||
|
||||
// 5.6 — loading poll: while any entry is loading (Phase 2's sync cache-miss
|
||||
// return), refetch until terminal. Capped; no provider_snapshot_updated WS
|
||||
// frame (deferred Tier-2). Dormant today since the snapshot awaits the build.
|
||||
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(projectPath);
|
||||
}, 2000);
|
||||
return () => clearTimeout(t);
|
||||
}, [allEntries, projectPath]);
|
||||
|
||||
const currentEntry = useMemo(
|
||||
() => entries?.find((e) => e.name === value.provider),
|
||||
[entries, value.provider],
|
||||
@@ -283,7 +316,11 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
|
||||
value={value.provider}
|
||||
options={providerOptions}
|
||||
onPick={pickProvider}
|
||||
icon={providerIcon(value.provider)}
|
||||
icon={
|
||||
currentEntry?.status === 'loading'
|
||||
? <Loader2 size={13} className="shrink-0 animate-spin" />
|
||||
: providerIcon(value.provider)
|
||||
}
|
||||
/>
|
||||
<CompactPicker
|
||||
label="Mode"
|
||||
|
||||
Reference in New Issue
Block a user