Vendor ACP provider catalog

This commit is contained in:
Mohamed Boudra
2026-05-06 20:00:40 +08:00
committed by GitHub
parent ac375f152d
commit c36f4cfee4
46 changed files with 1605 additions and 28 deletions
@@ -95,6 +95,7 @@ vi.mock("lucide-react-native", () => {
const icon = (name: string) => () => React.createElement("span", { "data-icon": name });
return {
ChevronRight: icon("ChevronRight"),
Plus: icon("Plus"),
RotateCw: icon("RotateCw"),
};
});
@@ -144,6 +145,10 @@ vi.mock("@/components/provider-diagnostic-sheet", () => ({
}),
}));
vi.mock("@/components/add-provider-modal", () => ({
AddProviderModal: () => null,
}));
vi.mock("@/hooks/use-providers-snapshot", () => ({
useProvidersSnapshot: () => ({
entries: snapshotState.entries,
@@ -323,7 +328,7 @@ describe("ProvidersSection", () => {
expect(sheet?.getAttribute("data-provider")).toBe("codex");
});
it("toggles the provider enabled flag through patchConfig when the switch is pressed", () => {
it("toggles the provider enabled flag through patchConfig when the switch is pressed", async () => {
snapshotState.entries = [claudeEntry];
configState.config = makeConfig();
@@ -334,7 +339,7 @@ describe("ProvidersSection", () => {
expect(switchEl).not.toBeNull();
expect(switchEl?.getAttribute("aria-checked")).toBe("true");
act(() => {
await act(async () => {
switchEl?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
});
@@ -6,12 +6,13 @@ import { useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { useDaemonConfig } from "@/hooks/use-daemon-config";
import { buildProviderDefinitions } from "@/utils/provider-definitions";
import { AddProviderModal } from "@/components/add-provider-modal";
import { getProviderIcon } from "@/components/provider-icons";
import { ProviderDiagnosticSheet } from "@/components/provider-diagnostic-sheet";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Switch } from "@/components/ui/switch";
import { SettingsSection } from "@/screens/settings/settings-section";
import { ChevronRight, RotateCw } from "lucide-react-native";
import { ChevronRight, Plus, RotateCw } from "lucide-react-native";
type ProviderDefinition = ReturnType<typeof buildProviderDefinitions>[number];
type ProviderEntry = NonNullable<ReturnType<typeof useProvidersSnapshot>["entries"]>[number];
@@ -181,6 +182,7 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
const { entries, isLoading, isRefreshing, refresh } = useProvidersSnapshot(serverId);
const { patchConfig } = useDaemonConfig(serverId);
const [diagnosticProvider, setDiagnosticProvider] = useState<string | null>(null);
const [isAddProviderOpen, setIsAddProviderOpen] = useState(false);
const [pendingProviderId, setPendingProviderId] = useState<string | null>(null);
const providerDefinitions = useMemo(() => buildProviderDefinitions(entries), [entries]);
@@ -193,6 +195,8 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
}, [refresh]);
const handleCloseDiagnostic = useCallback(() => setDiagnosticProvider(null), []);
const handleOpenAddProvider = useCallback(() => setIsAddProviderOpen(true), []);
const handleCloseAddProvider = useCallback(() => setIsAddProviderOpen(false), []);
const handleToggleEnabled = useCallback(
async (providerId: string, enabled: boolean) => {
setPendingProviderId(providerId);
@@ -210,29 +214,43 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
[patchConfig],
);
const refreshAction = useMemo(
const headerActions = useMemo(
() =>
hasServer && isConnected ? (
<Pressable
onPress={handleRefresh}
disabled={providerRefreshInFlight}
hitSlop={8}
style={settingsStyles.sectionHeaderLink}
accessibilityRole="button"
accessibilityLabel={
providerRefreshInFlight ? "Refreshing providers" : "Refresh providers"
}
>
{providerRefreshInFlight ? (
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : (
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)}
</Pressable>
<View style={styles.headerActions}>
<Pressable
onPress={handleOpenAddProvider}
hitSlop={8}
style={settingsStyles.sectionHeaderLink}
accessibilityRole="button"
accessibilityLabel="Add provider"
testID="add-provider-button"
>
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<Text style={settingsStyles.sectionHeaderLinkText}>Add provider</Text>
</Pressable>
<Pressable
onPress={handleRefresh}
disabled={providerRefreshInFlight}
hitSlop={8}
style={settingsStyles.sectionHeaderLink}
accessibilityRole="button"
accessibilityLabel={
providerRefreshInFlight ? "Refreshing providers" : "Refresh providers"
}
>
{providerRefreshInFlight ? (
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : (
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)}
</Pressable>
</View>
) : undefined,
[
hasServer,
isConnected,
handleOpenAddProvider,
handleRefresh,
providerRefreshInFlight,
theme.iconSize.sm,
@@ -244,7 +262,7 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
<>
<SettingsSection
title="Providers"
trailing={refreshAction}
trailing={headerActions}
testID="host-page-providers-card"
style={styles.sectionSpacing}
>
@@ -288,6 +306,9 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
serverId={serverId}
/>
) : null}
{hasServer && isConnected && isAddProviderOpen ? (
<AddProviderModal serverId={serverId} visible onClose={handleCloseAddProvider} />
) : null}
</>
);
}
@@ -304,6 +325,11 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
headerActions: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[3],
},
row: {
gap: theme.spacing[3],
minHeight: 56,