Simplify mobile agent configuration (#3424)

This commit is contained in:
Mohamed Boudra
2026-08-16 10:10:35 +02:00
committed by GitHub
parent 7f8a485a77
commit 38f176595e
25 changed files with 511 additions and 134 deletions
@@ -2,15 +2,19 @@ import { useCallback, useMemo, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Keyboard, ScrollView, Text, View, type PressableStateCallbackType } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { Bot } from "lucide-react-native";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import type { AgentProfilePicker } from "@/agent-profiles";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { getProviderIcon } from "@/components/provider-icons";
import { ModelBrowser, useModelBrowser } from "@/components/model-browser";
import { AgentControlTrigger } from "@/composer/agent-controls/control";
import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph";
import { resolveModelSheetOpening } from "@/composer/agent-controls/model-sheet-flow";
import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isWeb } from "@/constants/platform";
const SNAP_POINTS = ["80%", "90%"];
const MODEL_LIST_TOP_INSET = 4;
@@ -23,6 +27,7 @@ interface CompactModelSheetProps {
providers: ProviderSelectorProvider[];
selectedProvider: string;
selectedModel: string;
thinkingLabel: string | null;
onSelect: (provider: string, modelId: string) => void;
isLoading: boolean;
profiles?: AgentProfilePicker | null;
@@ -35,6 +40,7 @@ interface CompactModelSheetProps {
disabled?: boolean;
serverId?: string | null;
glyphSize: number;
canSwitchProvider: boolean;
children: ReactNode;
}
@@ -47,6 +53,7 @@ export function CompactModelSheet({
providers,
selectedProvider,
selectedModel,
thinkingLabel,
onSelect,
isLoading,
profiles = null,
@@ -59,47 +66,107 @@ export function CompactModelSheet({
disabled = false,
serverId = null,
glyphSize,
canSwitchProvider,
children,
}: CompactModelSheetProps) {
const { t } = useTranslation();
const usesBottomSheet = useIsCompactFormFactor();
const [isOpen, setIsOpen] = useState(false);
const browser = useModelBrowser({
providers,
const [isModelBrowserOpen, setIsModelBrowserOpen] = useState(false);
const availableProviders = useMemo(() => {
if (canSwitchProvider) return providers;
const fixedProvider =
providers.find((entry) => entry.id === selectedProvider) ?? providers[0] ?? null;
return fixedProvider ? [fixedProvider] : [];
}, [canSwitchProvider, providers, selectedProvider]);
const rootBrowser = useModelBrowser({
providers: availableProviders,
selectedProvider,
selectedModel,
isLoading,
autoFocusSearch: isWeb && !usesBottomSheet,
profiles,
serverId,
});
const { prepareToOpen, reset } = browser;
const modelBrowser = useModelBrowser({
providers: availableProviders,
selectedProvider,
selectedModel,
isLoading,
autoFocusSearch: isWeb && !usesBottomSheet,
serverId,
});
const ProviderIcon =
selectedProvider.trim().length > 0 ? getProviderIcon(selectedProvider) : null;
const compactFooter = useMemo(
() =>
usesBottomSheet ? (
<View style={styles.compactFooter} testID="agent-controls-settings-list">
<View style={styles.modelViewportDivider} />
<View style={[styles.controlsContent, styles.compactControlsContent]}>{children}</View>
</View>
) : undefined,
[children, usesBottomSheet],
const ModelIcon = ProviderIcon ?? Bot;
const rootHeader = useMemo(
() => ({
...rootBrowser.header,
title: t("modelSelector.selectModel"),
search:
rootBrowser.header.search && !canSwitchProvider
? {
...rootBrowser.header.search,
placeholder: t("modelSelector.searchPlaceholder"),
}
: rootBrowser.header.search,
}),
[canSwitchProvider, rootBrowser.header, t],
);
const open = useCallback(() => {
Keyboard.dismiss();
prepareToOpen();
rootBrowser.showAll();
setIsOpen(true);
onOpen?.();
}, [onOpen, prepareToOpen]);
}, [onOpen, rootBrowser]);
const close = useCallback(() => {
setIsModelBrowserOpen(false);
setIsOpen(false);
reset();
rootBrowser.reset();
modelBrowser.reset();
onClose?.();
}, [onClose, reset]);
}, [modelBrowser, onClose, rootBrowser]);
const handleSelect = useCallback(
const handleSearchSelect = useCallback(
(provider: string, modelId: string) => {
onSelect(provider, modelId);
Keyboard.dismiss();
rootBrowser.showAll();
},
[onSelect, rootBrowser],
);
const openModelBrowser = useCallback(() => {
Keyboard.dismiss();
const destination = resolveModelSheetOpening({
canSwitchProvider,
providers: availableProviders,
selectedProvider,
});
if (destination.kind === "all") {
modelBrowser.showAll();
} else {
modelBrowser.drillDown(destination.providerId, destination.providerLabel);
}
setIsModelBrowserOpen(true);
}, [availableProviders, canSwitchProvider, modelBrowser, selectedProvider]);
const closeModelBrowser = useCallback(() => {
setIsModelBrowserOpen(false);
modelBrowser.reset();
}, [modelBrowser]);
const handleBrowserSelect = useCallback(
(provider: string, modelId: string) => {
onSelect(provider, modelId);
closeModelBrowser();
},
[closeModelBrowser, onSelect],
);
const handleDesktopSelect = useCallback(
(provider: string, modelId: string) => {
onSelect(provider, modelId);
close();
@@ -137,6 +204,28 @@ export function CompactModelSheet({
],
[disabled, isOpen],
);
const mobileRootContent = useMemo(
() => (
<View style={styles.mobileRootContent} testID="agent-controls-settings-list">
<View style={styles.controlsContent}>
<AgentControlTrigger
icon={ModelIcon}
surface="sheet"
label={t("modelSelector.model")}
value={rootBrowser.selectedModelLabel}
disabled={disabled}
onPress={openModelBrowser}
accessibilityLabel={t("modelSelector.selectedModel", {
model: rootBrowser.selectedModelLabel,
})}
testID="agent-controls-model"
/>
{children}
</View>
</View>
),
[ModelIcon, children, disabled, openModelBrowser, rootBrowser.selectedModelLabel, t],
);
return (
<>
@@ -147,7 +236,7 @@ export function CompactModelSheet({
style={triggerStyle}
accessibilityRole="button"
accessibilityLabel={t("modelSelector.selectedModel", {
model: browser.selectedModelLabel,
model: rootBrowser.selectedModelLabel,
})}
testID="combined-model-selector"
chevron={null}
@@ -157,20 +246,25 @@ export function CompactModelSheet({
<ProviderIcon size={glyphSize} color={styles.providerIcon.color} />
</ComposerToolbarGlyph>
) : null}
<Text style={styles.triggerText} numberOfLines={1}>
{shortModelLabel(browser.triggerLabel)}
</Text>
<View style={styles.triggerLabels}>
<Text style={styles.triggerText} numberOfLines={1}>
{shortModelLabel(rootBrowser.triggerLabel)}
</Text>
{thinkingLabel ? (
<Text style={styles.triggerThinking} numberOfLines={1}>
{thinkingLabel}
</Text>
) : null}
</View>
</ComboboxTrigger>
<AdaptiveModalSheet
header={browser.header}
header={rootHeader}
visible={isOpen}
onClose={close}
snapPoints={SNAP_POINTS}
scrollable={false}
sizeContentToCurrentSnapPoint={usesBottomSheet}
footer={compactFooter}
footerContainerStyle={usesBottomSheet ? styles.compactFooterContainer : undefined}
contentStyle={styles.sheetBody}
testID="agent-controls-model-sheet"
>
@@ -182,13 +276,15 @@ export function CompactModelSheet({
testID="agent-controls-model-viewport"
>
<ModelBrowser
state={browser}
onSelect={handleSelect}
state={rootBrowser}
onSelect={usesBottomSheet ? handleSearchSelect : handleDesktopSelect}
onApplyProfile={handleApplyProfile}
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
onRetryProvider={onRetryProvider}
isRetryingProvider={isRetryingProvider}
scrolling="independent"
searchAllOnFocus={usesBottomSheet}
rootBrowseContent={usesBottomSheet ? mobileRootContent : undefined}
/>
</View>
{!usesBottomSheet ? (
@@ -206,6 +302,34 @@ export function CompactModelSheet({
</>
) : null}
</AdaptiveModalSheet>
{usesBottomSheet ? (
<AdaptiveModalSheet
header={modelBrowser.header}
visible={isModelBrowserOpen}
onClose={closeModelBrowser}
snapPoints={SNAP_POINTS}
scrollable={false}
sizeContentToCurrentSnapPoint
presentation="push"
contentStyle={styles.sheetBody}
testID="agent-controls-model-browser-sheet"
>
<View
style={[styles.modelViewport, styles.flexibleModelViewport]}
testID="agent-controls-model-browser-viewport"
>
<ModelBrowser
state={modelBrowser}
onSelect={handleBrowserSelect}
onRetryProvider={onRetryProvider}
isRetryingProvider={isRetryingProvider}
scrolling="independent"
searchAllOnFocus
/>
</View>
</AdaptiveModalSheet>
) : null}
</>
);
}
@@ -238,6 +362,19 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
},
triggerLabels: {
minWidth: 0,
flexShrink: 1,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
triggerThinking: {
flexShrink: 0,
color: theme.colors.foregroundExtraMuted,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal,
},
providerIcon: {
color: theme.colors.foregroundMuted,
},
@@ -268,20 +405,10 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
minHeight: 0,
},
compactFooterContainer: {
flexDirection: "column",
alignItems: "stretch",
justifyContent: "flex-start",
gap: 0,
paddingHorizontal: 0,
paddingTop: 0,
borderTopWidth: 0,
},
compactFooter: {
minWidth: 0,
},
compactControlsContent: {
paddingBottom: 0,
mobileRootContent: {
backgroundColor: theme.colors.surfaceSidebar,
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
controlsContent: {
paddingHorizontal: theme.spacing[2],