Inline provider catalog in settings (#1423)

Replace the modal Add Provider flow with an inline Add provider section
below the configured providers list. Makes new providers discoverable
without opening a modal.

- Remove add-provider-modal component
- Add provider-catalog-list component with search + Add button
- Remove duplicate ACP catalog entries for built-in providers
- Add spacing between the two sections
This commit is contained in:
Mohamed Boudra
2026-06-09 10:44:37 +08:00
committed by GitHub
parent 0cbb8238c2
commit 02ec937399
6 changed files with 89 additions and 199 deletions
@@ -97,7 +97,6 @@ vi.mock("lucide-react-native", () => {
const icon = (name: string) => () => React.createElement("span", { "data-icon": name });
return {
ChevronRight: icon("ChevronRight"),
Plus: icon("Plus"),
};
});
@@ -143,8 +142,8 @@ vi.mock("@/stores/provider-settings-store", () => ({
selector({ open: openProviderSettingsMock }),
}));
vi.mock("@/components/add-provider-modal", () => ({
AddProviderModal: () => null,
vi.mock("@/components/provider-catalog-list", () => ({
ProviderCatalogList: () => null,
}));
vi.mock("@/hooks/use-providers-snapshot", () => ({
@@ -6,13 +6,17 @@ 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 {
buildAcpProviderConfigPatch,
type AcpProviderCatalogItem,
} from "@/hooks/use-acp-provider-catalog";
import { ProviderCatalogList } from "@/components/provider-catalog-list";
import { getProviderIcon } from "@/components/provider-icons";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Switch } from "@/components/ui/switch";
import { SettingsSection } from "@/screens/settings/settings-section";
import { useProviderSettingsStore } from "@/stores/provider-settings-store";
import { ChevronRight, Plus } from "lucide-react-native";
import { ChevronRight } from "lucide-react-native";
type ProviderDefinition = ReturnType<typeof buildProviderDefinitions>[number];
type ProviderEntry = NonNullable<ReturnType<typeof useProvidersSnapshot>["entries"]>[number];
@@ -177,13 +181,12 @@ export interface ProvidersSectionProps {
}
export function ProvidersSection({ serverId }: ProvidersSectionProps) {
const { theme } = useUnistyles();
const isConnected = useHostRuntimeIsConnected(serverId);
const { entries, isLoading } = useProvidersSnapshot(serverId);
const { entries, isLoading, refresh } = useProvidersSnapshot(serverId);
const { patchConfig } = useDaemonConfig(serverId);
const openProviderSettings = useProviderSettingsStore((state) => state.open);
const [isAddProviderOpen, setIsAddProviderOpen] = useState(false);
const [pendingProviderId, setPendingProviderId] = useState<string | null>(null);
const [installingProviderId, setInstallingProviderId] = useState<string | null>(null);
const providerDefinitions = useMemo(() => buildProviderDefinitions(entries), [entries]);
const hasServer = serverId.length > 0;
@@ -194,8 +197,7 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
},
[openProviderSettings, serverId],
);
const handleOpenAddProvider = useCallback(() => setIsAddProviderOpen(true), []);
const handleCloseAddProvider = useCallback(() => setIsAddProviderOpen(false), []);
const handleToggleEnabled = useCallback(
async (providerId: string, enabled: boolean) => {
setPendingProviderId(providerId);
@@ -213,37 +215,29 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
[patchConfig],
);
const headerActions = useMemo(
() =>
hasServer && isConnected ? (
<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>
</View>
) : undefined,
[
hasServer,
isConnected,
handleOpenAddProvider,
theme.iconSize.sm,
theme.colors.foregroundMuted,
],
const handleInstall = useCallback(
async (entry: AcpProviderCatalogItem) => {
if (installingProviderId) return;
setInstallingProviderId(entry.id);
try {
await patchConfig(buildAcpProviderConfigPatch(entry));
await refresh([entry.id]);
} catch (error) {
Alert.alert(
"Unable to add provider",
error instanceof Error ? error.message : String(error),
);
} finally {
setInstallingProviderId((current) => (current === entry.id ? null : current));
}
},
[installingProviderId, patchConfig, refresh],
);
return (
<>
<SettingsSection
title="Providers"
trailing={headerActions}
testID="host-page-providers-card"
style={styles.sectionSpacing}
>
@@ -279,8 +273,18 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
) : null}
</SettingsSection>
{hasServer && isConnected && isAddProviderOpen ? (
<AddProviderModal serverId={serverId} visible onClose={handleCloseAddProvider} />
{hasServer && isConnected ? (
<SettingsSection
title="Add provider"
testID="host-page-add-provider-card"
style={styles.addProviderSection}
>
<ProviderCatalogList
serverId={serverId}
installingProviderId={installingProviderId}
onInstall={handleInstall}
/>
</SettingsSection>
) : null}
</>
);
@@ -290,6 +294,9 @@ const styles = StyleSheet.create((theme) => ({
sectionSpacing: {
marginBottom: theme.spacing[4],
},
addProviderSection: {
marginTop: theme.spacing[4],
},
emptyCard: {
padding: theme.spacing[4],
alignItems: "center",
@@ -298,11 +305,6 @@ 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,