import { useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { Pencil, Copy, Activity, Loader2, Trash2, BarChart3 } from "lucide-react"; import { useTranslation } from "../i18n"; import { ProviderIcon } from "./ProviderIcon"; import { UsageFooter } from "./UsageFooter"; import { useUsageQuery } from "../hooks/useUsageQuery"; import type { Agent, Model, Provider } from "../types"; interface ConnectivityResult { ok: boolean; latencyMs: number; statusCode?: number | null; error?: string | null; } interface TestState { status: "testing" | "ok" | "fail"; latency?: number; error?: string; } interface ProviderListProps { providers: Provider[]; defaultModel: string | null; models: Record; onEdit: (name: string) => void; onDelete: (name: string) => void; onDuplicate: (name: string) => void; onAdd: () => void; onSwitchProvider: (name: string) => void; onConfigureUsage: (name: string) => void; agent: Agent; } interface ProviderCardProps { provider: Provider; agent: Agent; defaultModel: string | null; models: Record; /** Inline connectivity-test state for this provider, or undefined when idle. */ ts: TestState | undefined; onEdit: (name: string) => void; onDelete: (name: string) => void; onDuplicate: (name: string) => void; onSwitchProvider: (name: string) => void; onConfigureUsage: (name: string) => void; onTest: (provider: Provider) => void; } function ProviderCard({ provider, agent, defaultModel, models, ts, onEdit, onDelete, onDuplicate, onSwitchProvider, onConfigureUsage, onTest, }: ProviderCardProps) { const { t } = useTranslation(); // Usage query state lives once per card; both the compact (card header) and // detail (footer) UsageFooter variants read this same state, so a refresh // from either side updates both. const usage = useUsageQuery( agent, provider.name, provider.usageKinds, provider.usageConfig?.autoQueryIntervalMinutes ); const providerModels = Object.values(models).filter( (m) => m.provider === provider.name ); const defaultModelName = defaultModel ? models[defaultModel]?.model || models[defaultModel]?.display_name || defaultModel : null; const isActive = provider.active === true; return (
onEdit(provider.name)} >

{provider.name}

{isActive && ( {t("inUse")} )} {provider.note && ( {provider.note} )}
{provider.official_url || provider.base_url || t("noUrl")} {provider.provider_type} {t("modelCount", { count: providerModels.length })}
{defaultModel && models[defaultModel]?.provider === provider.name && ( {t("defaultModel", { name: defaultModelName ?? "" })} )} {/* inline connectivity test result */} {ts && ts.status !== "testing" && ( 6000 ? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-500/30" : "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border-green-300 dark:border-green-500/30" : "bg-red-100 dark:bg-red-900/30 text-red-500 dark:text-red-400 border-red-300 dark:border-red-500/30" }`} title={ ts.status === "fail" ? ts.error || t("connectivityFail", { name: provider.name, error: "" }) : ts.latency && ts.latency > 6000 ? t("connectivitySlow", { name: provider.name, latency: ts.latency ?? 0 }) : t("connectivityOk", { name: provider.name, latency: ts.latency ?? 0 }) } > {ts.status === "ok" ? `${ts.latency}ms` : "✕"} )} {/* compact usage summary — sits left of the switch button, mirroring cc-switch's card layout (usage → action buttons) */} {(provider.usageKinds?.length ?? 0) > 0 && ( )} {agent === "kimi_code" && ( ⓘ )} {/* icon button group */}
); } const iconBtn = "w-8 h-8 flex items-center justify-center rounded border border-border text-content-muted hover:text-content-primary hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none transition-colors"; export function ProviderList({ providers, defaultModel, models, onEdit, onDelete, onDuplicate, onAdd, onSwitchProvider, onConfigureUsage, agent, }: ProviderListProps) { const { t } = useTranslation(); const [testState, setTestState] = useState>({}); // managed(OAuth 登录托管)供应商排在最前;其余保持原有顺序(稳定排序)。 const sortedProviders = [...providers].sort( (a, b) => Number(b.managed === true) - Number(a.managed === true) ); const handleTest = async (provider: Provider) => { setTestState((s) => ({ ...s, [provider.name]: { status: "testing" } })); try { const result = await invoke("test_connectivity", { provider }); setTestState((s) => ({ ...s, [provider.name]: { status: result.ok ? "ok" : "fail", latency: result.latencyMs, error: result.error ?? undefined, }, })); } catch (e) { setTestState((s) => ({ ...s, [provider.name]: { status: "fail", error: e instanceof Error ? e.message : String(e), }, })); } // auto-clear the inline result after a few seconds setTimeout(() => { setTestState((s) => { const next = { ...s }; delete next[provider.name]; return next; }); }, 6000); }; return (

{t("providers")}

{providers.length === 0 ? (
⊘
{t("noProviders")}
) : (
{sortedProviders.map((provider) => ( ))}
)}
); }