Let users choose the metadata generation model (#3215)

Expose Automatic and Manual metadata model selection in Host Settings, preserve existing configured fallbacks, and keep structured generation resilient through discovered candidates. Includes Playwright coverage, translations, public documentation, and settings-row alignment polish.
This commit is contained in:
Mohamed Boudra
2026-08-11 14:01:00 +02:00
committed by GitHub
parent b599d38a77
commit 36a13601dd
19 changed files with 466 additions and 66 deletions
@@ -36,6 +36,7 @@ import {
SquareTerminal,
Code2,
Smartphone,
Sparkles,
} from "lucide-react-native";
import { DropdownTrigger } from "@/components/ui/dropdown-trigger";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
@@ -107,6 +108,7 @@ import {
HostWorkspacesPage,
HostTerminalsPage,
} from "@/screens/settings/host-page";
import { MetadataGenerationPage } from "@/screens/settings/metadata-generation-page";
import ProjectsScreen from "@/screens/projects-screen";
import ProjectSettingsScreen from "@/screens/project-settings-screen";
import { SETTINGS_DESKTOP_SIDEBAR_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
@@ -184,6 +186,7 @@ const HOST_SECTION_ITEMS: HostSectionItem[] = [
{ id: "connections", labelKey: "settings.hostSections.connections", icon: Network },
{ id: "pair-device", labelKey: "openProject.tiles.pairDevice.title", icon: Smartphone },
{ id: "agents", labelKey: "settings.hostSections.agents", icon: Bot },
{ id: "metadata", labelKey: "settings.hostSections.metadata", icon: Sparkles },
{ id: "workspaces", labelKey: "settings.hostSections.workspaces", icon: FolderGit2 },
{ id: "providers", labelKey: "settings.hostSections.providers", icon: Boxes },
{ id: "usage", labelKey: "settings.hostSections.usage", icon: Gauge },
@@ -203,6 +206,8 @@ function renderHostSettingsContent(
return <HostPairDevicePage serverId={view.serverId} />;
case "agents":
return <HostAgentsPage serverId={view.serverId} />;
case "metadata":
return <MetadataGenerationPage serverId={view.serverId} />;
case "workspaces":
return <HostWorkspacesPage serverId={view.serverId} />;
case "providers":
@@ -0,0 +1,181 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Alert, Text, View } from "react-native";
import { useTranslation } from "react-i18next";
import { StyleSheet } from "react-native-unistyles";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import { ExternalLink } from "@/components/ui/external-link";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { useDaemonConfig } from "@/hooks/use-daemon-config";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { buildSelectableProviderSelectorProviders } from "@/provider-selection/provider-selection";
import { SettingsSection } from "@/screens/settings/settings-section";
import { settingsStyles } from "@/styles/settings";
const METADATA_GENERATION_DOCS_URL = "https://paseo.sh/docs/metadata-generation";
type SelectionMode = "automatic" | "preferred";
export function MetadataGenerationPage({ serverId }: { serverId: string }) {
const { t } = useTranslation();
const { config, isLoading: isConfigLoading, patchConfig } = useDaemonConfig(serverId);
const snapshot = useProvidersSnapshot(serverId);
const providers = useMemo(
() => buildSelectableProviderSelectorProviders(snapshot.entries),
[snapshot.entries],
);
const configuredProviders = config?.metadataGeneration.providers;
const configuredProvider = configuredProviders?.[0] ?? null;
const savedMode: SelectionMode = configuredProvider ? "preferred" : "automatic";
const [draftMode, setDraftMode] = useState<SelectionMode | null>(null);
const [isSaving, setIsSaving] = useState(false);
const mode = draftMode ?? savedMode;
useEffect(() => {
setDraftMode(null);
}, [configuredProvider?.model, configuredProvider?.provider]);
const modeOptions = useMemo(
() => [
{ value: "automatic" as const, label: t("settings.metadataGeneration.automatic") },
{ value: "preferred" as const, label: t("settings.metadataGeneration.preferred") },
],
[t],
);
const saveProviders = useCallback(
async (providersPatch: { provider: string; model?: string }[]) => {
setIsSaving(true);
try {
await patchConfig({ metadataGeneration: { providers: providersPatch } });
} catch (error) {
setDraftMode(null);
Alert.alert(
t("settings.metadataGeneration.saveError"),
error instanceof Error ? error.message : String(error),
);
} finally {
setIsSaving(false);
}
},
[patchConfig, t],
);
const handleModeChange = useCallback(
(next: SelectionMode) => {
setDraftMode(next);
if (next === "automatic") {
void saveProviders([]);
}
},
[saveProviders],
);
const handleModelSelect = useCallback(
(provider: AgentProvider, model: string) => {
setDraftMode("preferred");
void saveProviders([
{ provider, ...(model ? { model } : {}) },
...(configuredProviders?.slice(1) ?? []),
]);
},
[configuredProviders, saveProviders],
);
const handleSelectorOpen = useCallback(() => {
snapshot.refetchIfStale(configuredProvider?.provider);
}, [configuredProvider?.provider, snapshot]);
const handleRetryProvider = useCallback(
(provider: AgentProvider) => snapshot.refresh([provider]),
[snapshot],
);
const docsLink = useMemo(
() => (
<ExternalLink
href={METADATA_GENERATION_DOCS_URL}
label={t("settings.metadataGeneration.docs")}
/>
),
[t],
);
if (isConfigLoading || !config) {
return (
<View style={styles.loading}>
<LoadingSpinner size="large" color={styles.spinnerColor.color} />
</View>
);
}
return (
<SettingsSection
title={t("settings.metadataGeneration.title")}
trailing={docsLink}
testID="metadata-generation-settings"
>
<Text style={styles.description}>{t("settings.metadataGeneration.description")}</Text>
<View style={settingsStyles.card}>
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>
{t("settings.metadataGeneration.selection")}
</Text>
<Text style={settingsStyles.rowHint}>
{mode === "automatic"
? t("settings.metadataGeneration.automaticHint")
: t("settings.metadataGeneration.preferredHint")}
</Text>
</View>
<SegmentedControl
options={modeOptions}
value={mode}
onValueChange={handleModeChange}
size="sm"
testID="metadata-generation-mode"
/>
</View>
{mode === "preferred" ? (
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{t("settings.metadataGeneration.model")}</Text>
<Text style={settingsStyles.rowHint}>
{t("settings.metadataGeneration.fallbackHint")}
</Text>
</View>
<CombinedModelSelector
providers={providers}
selectedProvider={configuredProvider?.provider ?? ""}
selectedModel={configuredProvider?.model ?? ""}
onSelect={handleModelSelect}
isLoading={snapshot.isLoading || snapshot.isFetching}
onOpen={handleSelectorOpen}
onRetryProvider={handleRetryProvider}
isRetryingProvider={snapshot.isRefreshing}
disabled={isSaving}
serverId={serverId}
desktopPlacement="bottom-start"
desktopMinWidth={360}
/>
</View>
) : null}
</View>
</SettingsSection>
);
}
const styles = StyleSheet.create((theme) => ({
description: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
lineHeight: theme.fontSize.xs * 1.45,
marginHorizontal: theme.spacing[1],
},
loading: {
alignItems: "center",
justifyContent: "center",
minHeight: 180,
},
spinnerColor: {
color: theme.colors.foregroundMuted,
},
}));