feat(app): create agent profiles from the model chooser (#3533)
* feat(app): create agent profiles from the model chooser Restore quick model pinning by creating an agent profile directly from a model row. A trailing plus on unprofiled model rows opens the pre-filled profile form; rows that already name the provider+model show the profile glyph and open it for edit. - seed the profile form model with provider, model, and suggested name - expose useAgentProfileEditor from @/agent-profiles for the write path - hover-refactor model rows per docs/hover.md and add the trailing action - wire the composer desktop selector and compact model sheet (close-then-open) Closes #3529 * fix(app): match favourites star position for model row profile action The star sat after the selection checkmark at the far-right edge. Put the profile create/edit action in the same slot instead of before the checkmark. * fix(app): disambiguate per-model create profile action The per-model + buttons shared the accessible name "Create profile" with the empty-state row, which broke the empty-host picker test (strict mode matched five buttons). Name the action "Create profile from <model>" so it is specific and screen readers get the model context. * fix(app): keep per-model create label out of model row names Naming the action "Create profile from <model>" put the model label into a second button on the row, so regex selectors targeting a model row by name matched both. Use a generic "Create profile from this model" label that neither collides with the empty-state "Create profile" row nor with model row names.
This commit is contained in:
@@ -3,7 +3,7 @@ import { Pressable, Text, View, type PressableStateCallbackType } from "react-na
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||
import type { AgentProfilePicker } from "@/agent-profiles";
|
||||
import type { AgentProfilePicker, AgentProfileSeed } from "@/agent-profiles";
|
||||
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { Combobox, type ComboboxOption, type ComboboxProps } from "@/components/ui/combobox";
|
||||
@@ -30,6 +30,8 @@ interface CombinedModelSelectorProps {
|
||||
profiles?: AgentProfilePicker | null;
|
||||
onApplyProfile?: (profileId: string) => void;
|
||||
onEditProfiles?: () => void;
|
||||
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||
onEditProfile?: (profileId: string) => void;
|
||||
renderTrigger?: (input: {
|
||||
selectedModelLabel: string;
|
||||
onPress: () => void;
|
||||
@@ -70,6 +72,8 @@ export function CombinedModelSelector({
|
||||
profiles = null,
|
||||
onApplyProfile,
|
||||
onEditProfiles,
|
||||
onCreateProfile,
|
||||
onEditProfile,
|
||||
renderTrigger,
|
||||
onOpen,
|
||||
onClose,
|
||||
@@ -168,12 +172,30 @@ export function CombinedModelSelector({
|
||||
onEditProfiles?.();
|
||||
}, [handleOpenChange, onEditProfiles]);
|
||||
|
||||
const handleCreateProfile = useCallback(
|
||||
(seed: AgentProfileSeed) => {
|
||||
handleOpenChange(false);
|
||||
onCreateProfile?.(seed);
|
||||
},
|
||||
[handleOpenChange, onCreateProfile],
|
||||
);
|
||||
|
||||
const handleEditProfile = useCallback(
|
||||
(profileId: string) => {
|
||||
handleOpenChange(false);
|
||||
onEditProfile?.(profileId);
|
||||
},
|
||||
[handleOpenChange, onEditProfile],
|
||||
);
|
||||
|
||||
const selectorBody = isContentReady ? (
|
||||
<ModelBrowser
|
||||
state={browser}
|
||||
onSelect={handleSelect}
|
||||
onApplyProfile={handleApplyProfile}
|
||||
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
|
||||
onCreateProfile={onCreateProfile ? handleCreateProfile : undefined}
|
||||
onEditProfile={onEditProfile ? handleEditProfile : undefined}
|
||||
onRetryProvider={onRetryProvider}
|
||||
isRetryingProvider={isRetryingProvider}
|
||||
scrolling={isWeb ? "independent" : "sheet"}
|
||||
|
||||
@@ -3,7 +3,11 @@ import type {
|
||||
ProviderSelectionModelRow,
|
||||
ProviderSelectorProvider,
|
||||
} from "@/provider-selection/provider-selection";
|
||||
import { resolveInitialModelBrowserView, resolveModelBrowserAllView } from "./model-browser-view";
|
||||
import {
|
||||
resolveInitialModelBrowserView,
|
||||
resolveModelBrowserAllView,
|
||||
groupProfilesByProviderModel,
|
||||
} from "./model-browser-view";
|
||||
|
||||
function provider(
|
||||
id: string,
|
||||
@@ -93,6 +97,36 @@ describe("model browser initial view", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("groupProfilesByProviderModel", () => {
|
||||
it("groups profiles by provider and model, skipping profiles without a model", () => {
|
||||
const lookup = groupProfilesByProviderModel([
|
||||
{ provider: "claude", modelId: "opus-5" },
|
||||
{ provider: "claude", modelId: "opus-5" },
|
||||
{ provider: "claude", modelId: "sonnet-4.6" },
|
||||
{ provider: "claude", modelId: "" },
|
||||
{ provider: "codex", modelId: "gpt-5.4" },
|
||||
]);
|
||||
|
||||
expect(lookup.get("claude:opus-5")).toHaveLength(2);
|
||||
expect(lookup.get("claude:sonnet-4.6")).toHaveLength(1);
|
||||
expect(lookup.get("codex:gpt-5.4")).toHaveLength(1);
|
||||
expect(lookup.has("claude:")).toBe(false);
|
||||
});
|
||||
|
||||
it("trims model ids so whitespace cannot create a separate key", () => {
|
||||
const lookup = groupProfilesByProviderModel([
|
||||
{ provider: "claude", modelId: "opus-5" },
|
||||
{ provider: "claude", modelId: " opus-5 " },
|
||||
]);
|
||||
|
||||
expect(lookup.get("claude:opus-5")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("returns an empty map for no refs", () => {
|
||||
expect(groupProfilesByProviderModel([]).size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("model browser all view", () => {
|
||||
const claude = provider("claude", "Claude Code", [
|
||||
modelRow("claude", "Claude Code", "opus-5", "Opus 5"),
|
||||
|
||||
@@ -9,6 +9,36 @@ export type ModelBrowserView =
|
||||
| { kind: "all" }
|
||||
| { kind: "provider"; providerId: string; providerLabel: string };
|
||||
|
||||
/** A profile's model reference; used to match profiles back to model rows. */
|
||||
export interface ModelProfileRef {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups profiles by `provider:modelId`, skipping profiles that name no model.
|
||||
* Pure so the model browser can test it apart from the component tree.
|
||||
*/
|
||||
export function groupProfilesByProviderModel<T extends ModelProfileRef>(
|
||||
refs: readonly T[],
|
||||
): Map<string, T[]> {
|
||||
const lookup = new Map<string, T[]>();
|
||||
for (const ref of refs) {
|
||||
const modelId = ref.modelId.trim();
|
||||
if (!modelId) {
|
||||
continue;
|
||||
}
|
||||
const key = `${ref.provider}:${modelId}`;
|
||||
const existing = lookup.get(key);
|
||||
if (existing) {
|
||||
existing.push(ref);
|
||||
} else {
|
||||
lookup.set(key, [ref]);
|
||||
}
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
/** What the root view shows: the provider drill-down, or ranked cross-provider results. */
|
||||
export type ModelBrowserAllView =
|
||||
| { kind: "browse" }
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
AgentProfileGlyph,
|
||||
type AgentProfilePicker,
|
||||
type AgentProfilePickerRow as AgentProfilePickerRowModel,
|
||||
type AgentProfileSeed,
|
||||
} from "@/agent-profiles";
|
||||
import type { SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -52,6 +53,7 @@ import { useProviderSettingsStore } from "@/stores/provider-settings-store";
|
||||
import { useCurrentOverlayLayer } from "@/lib/overlay-root";
|
||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||
import {
|
||||
groupProfilesByProviderModel,
|
||||
resolveInitialModelBrowserView,
|
||||
resolveModelBrowserAllView,
|
||||
type ModelBrowserView,
|
||||
@@ -176,6 +178,8 @@ interface ModelBrowserProps {
|
||||
/** Applying a profile resolves the pick and dismisses, exactly like a model row. */
|
||||
onApplyProfile?: (profileId: string) => void;
|
||||
onEditProfiles?: () => void;
|
||||
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||
onEditProfile?: (profileId: string) => void;
|
||||
onRetryProvider?: (provider: AgentProvider) => void;
|
||||
isRetryingProvider?: boolean;
|
||||
scrolling?: "sheet" | "independent";
|
||||
@@ -183,6 +187,8 @@ interface ModelBrowserProps {
|
||||
searchAllOnFocus?: boolean;
|
||||
/** Replaces provider rows only while the all-provider search is empty. */
|
||||
rootBrowseContent?: React.ReactNode;
|
||||
/** Hide the pinned Profiles section while still using rows for model matching. */
|
||||
showProfilesSection?: boolean;
|
||||
}
|
||||
|
||||
interface ModelBrowserContentProps extends Omit<ModelBrowserProps, "state" | "scrolling"> {
|
||||
@@ -591,34 +597,214 @@ function ModelBrowserRow({
|
||||
);
|
||||
}
|
||||
|
||||
function ModelRowProfileAction({
|
||||
hovered,
|
||||
onPress,
|
||||
label,
|
||||
testID,
|
||||
children,
|
||||
}: {
|
||||
hovered: boolean;
|
||||
onPress: () => void;
|
||||
label: string;
|
||||
testID: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const visible = hovered || isNative || isCompact;
|
||||
const handlePress = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
event.stopPropagation();
|
||||
onPress();
|
||||
},
|
||||
[onPress],
|
||||
);
|
||||
const pressableStyle = useCallback(
|
||||
({ hovered: buttonHovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.rowIconButton,
|
||||
Boolean(buttonHovered) && styles.rowIconButtonHovered,
|
||||
pressed && styles.rowIconButtonPressed,
|
||||
!visible && styles.profileActionHidden,
|
||||
],
|
||||
[visible],
|
||||
);
|
||||
return (
|
||||
<Tooltip delayDuration={250} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger asChild>
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
hitSlop={8}
|
||||
style={pressableStyle}
|
||||
pointerEvents={visible ? "auto" : "none"}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={label}
|
||||
testID={testID}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="center" offset={8}>
|
||||
<Text style={styles.tooltipText}>{label}</Text>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelRow({
|
||||
row,
|
||||
isSelected,
|
||||
showProviderLabel = false,
|
||||
onPress,
|
||||
profiledRows,
|
||||
onCreateProfile,
|
||||
onEditProfile,
|
||||
onEditProfiles,
|
||||
}: {
|
||||
row: ProviderSelectionModelRow;
|
||||
isSelected: boolean;
|
||||
showProviderLabel?: boolean;
|
||||
onPress: () => void;
|
||||
profiledRows: AgentProfilePickerRowModel[];
|
||||
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||
onEditProfile?: (profileId: string) => void;
|
||||
onEditProfiles?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const leadingSlot = useMemo(
|
||||
() => <ModelProviderGlyph provider={row.provider} size={ICON_SIZE.sm} />,
|
||||
[row.provider],
|
||||
);
|
||||
|
||||
const description = showProviderLabel ? buildProviderQualifiedDescription(row) : row.description;
|
||||
const primary = profiledRows[profiledRows.length - 1];
|
||||
|
||||
const handleCreateProfile = useCallback(() => {
|
||||
onCreateProfile?.({
|
||||
provider: row.provider,
|
||||
modelId: row.modelId,
|
||||
name: row.modelLabel,
|
||||
});
|
||||
}, [onCreateProfile, row.modelId, row.modelLabel, row.provider]);
|
||||
|
||||
const handleEditProfile = useCallback(() => {
|
||||
onEditProfile?.(primary.id);
|
||||
}, [onEditProfile, primary]);
|
||||
|
||||
const handleEditProfiles = useCallback(() => {
|
||||
onEditProfiles?.();
|
||||
}, [onEditProfiles]);
|
||||
|
||||
const handlePointerEnter = useCallback(() => setIsHovered(true), []);
|
||||
const handlePointerLeave = useCallback(() => setIsHovered(false), []);
|
||||
|
||||
const profileAction = useMemo(() => {
|
||||
if (row.modelId.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (profiledRows.length === 0) {
|
||||
if (!onCreateProfile) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ModelRowProfileAction
|
||||
hovered={isHovered}
|
||||
onPress={handleCreateProfile}
|
||||
label={t("modelSelector.createProfileFromModel")}
|
||||
testID={`model-create-profile-${row.provider}-${row.modelId}`}
|
||||
>
|
||||
<ThemedPlus size={ICON_SIZE.xs} uniProps={foregroundMutedMapping} />
|
||||
</ModelRowProfileAction>
|
||||
);
|
||||
}
|
||||
if (profiledRows.length === 1) {
|
||||
if (!onEditProfile) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ModelRowProfileAction
|
||||
hovered={isHovered}
|
||||
onPress={handleEditProfile}
|
||||
label={t("modelSelector.editProfileLabel", { name: primary.name })}
|
||||
testID={`model-edit-profile-${row.provider}-${row.modelId}`}
|
||||
>
|
||||
<AgentProfileGlyph icon={primary.icon} color={primary.color} size={ICON_SIZE.xs} />
|
||||
</ModelRowProfileAction>
|
||||
);
|
||||
}
|
||||
if (!onEditProfiles) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ModelRowProfileAction
|
||||
hovered={isHovered}
|
||||
onPress={handleEditProfiles}
|
||||
label={t("modelSelector.editProfilesCount", { count: profiledRows.length })}
|
||||
testID={`model-edit-profiles-${row.provider}-${row.modelId}`}
|
||||
>
|
||||
<AgentProfileGlyph icon={primary.icon} color={primary.color} size={ICON_SIZE.xs} />
|
||||
</ModelRowProfileAction>
|
||||
);
|
||||
}, [
|
||||
handleCreateProfile,
|
||||
handleEditProfile,
|
||||
handleEditProfiles,
|
||||
isHovered,
|
||||
onCreateProfile,
|
||||
onEditProfile,
|
||||
onEditProfiles,
|
||||
primary,
|
||||
profiledRows,
|
||||
row.modelId,
|
||||
row.provider,
|
||||
t,
|
||||
]);
|
||||
|
||||
const pressableStyle = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.browserRow,
|
||||
styles.browserModelRow,
|
||||
Boolean(hovered) && styles.browserRowHovered,
|
||||
pressed && styles.browserRowPressed,
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<ModelBrowserRow
|
||||
label={row.modelLabel}
|
||||
description={description}
|
||||
selected={isSelected}
|
||||
selectionIndicator
|
||||
onPress={onPress}
|
||||
leadingSlot={leadingSlot}
|
||||
testID={`model-row-${row.provider}-${row.modelId}`}
|
||||
/>
|
||||
<View
|
||||
style={styles.modelRowHoverBoundary}
|
||||
onPointerEnter={handlePointerEnter}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
>
|
||||
<ModelBrowserPressable
|
||||
onPress={onPress}
|
||||
style={pressableStyle}
|
||||
accessibilitySelected={isSelected}
|
||||
testID={`model-row-${row.provider}-${row.modelId}`}
|
||||
>
|
||||
<View style={styles.browserRowContent}>
|
||||
<View style={styles.browserRowLeading}>{leadingSlot}</View>
|
||||
<View style={[styles.browserRowText, description && styles.browserRowTextInline]}>
|
||||
<Text numberOfLines={1} style={styles.browserRowLabel}>
|
||||
{row.modelLabel}
|
||||
</Text>
|
||||
{description ? (
|
||||
<Text numberOfLines={1} style={styles.browserRowDescription}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.browserRowTrailing}>
|
||||
<View style={styles.browserRowSelection}>
|
||||
{isSelected ? (
|
||||
<ThemedCheck size={ICON_SIZE.sm} uniProps={foregroundMutedMapping} />
|
||||
) : null}
|
||||
</View>
|
||||
{profileAction}
|
||||
</View>
|
||||
</View>
|
||||
</ModelBrowserPressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -627,11 +813,19 @@ function SelectableModelRow({
|
||||
isSelected,
|
||||
showProviderLabel,
|
||||
onSelect,
|
||||
profiledRows,
|
||||
onCreateProfile,
|
||||
onEditProfile,
|
||||
onEditProfiles,
|
||||
}: {
|
||||
row: ProviderSelectionModelRow;
|
||||
isSelected: boolean;
|
||||
showProviderLabel?: boolean;
|
||||
onSelect: (provider: string, modelId: string) => void;
|
||||
profiledRows: AgentProfilePickerRowModel[];
|
||||
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||
onEditProfile?: (profileId: string) => void;
|
||||
onEditProfiles?: () => void;
|
||||
}) {
|
||||
const handlePress = useCallback(() => {
|
||||
onSelect(row.provider, row.modelId);
|
||||
@@ -642,6 +836,10 @@ function SelectableModelRow({
|
||||
isSelected={isSelected}
|
||||
showProviderLabel={showProviderLabel}
|
||||
onPress={handlePress}
|
||||
profiledRows={profiledRows}
|
||||
onCreateProfile={onCreateProfile}
|
||||
onEditProfile={onEditProfile}
|
||||
onEditProfiles={onEditProfiles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -919,6 +1117,10 @@ function ModelRowList({
|
||||
showProviderLabel = false,
|
||||
header,
|
||||
scrolling,
|
||||
profiledLookup,
|
||||
onCreateProfile,
|
||||
onEditProfile,
|
||||
onEditProfiles,
|
||||
}: {
|
||||
rows: ProviderSelectionModelRow[];
|
||||
selectedProvider: string;
|
||||
@@ -927,6 +1129,10 @@ function ModelRowList({
|
||||
showProviderLabel?: boolean;
|
||||
header?: React.ReactElement;
|
||||
scrolling: "sheet" | "independent";
|
||||
profiledLookup: Map<string, AgentProfilePickerRowModel[]>;
|
||||
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||
onEditProfile?: (profileId: string) => void;
|
||||
onEditProfiles?: () => void;
|
||||
}) {
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const renderItem = useCallback(
|
||||
@@ -936,9 +1142,22 @@ function ModelRowList({
|
||||
isSelected={item.provider === selectedProvider && item.modelId === selectedModel}
|
||||
showProviderLabel={showProviderLabel}
|
||||
onSelect={onSelect}
|
||||
profiledRows={profiledLookup.get(`${item.provider}:${item.modelId}`) ?? []}
|
||||
onCreateProfile={onCreateProfile}
|
||||
onEditProfile={onEditProfile}
|
||||
onEditProfiles={onEditProfiles}
|
||||
/>
|
||||
),
|
||||
[onSelect, selectedModel, selectedProvider, showProviderLabel],
|
||||
[
|
||||
onEditProfile,
|
||||
onEditProfiles,
|
||||
onCreateProfile,
|
||||
onSelect,
|
||||
profiledLookup,
|
||||
selectedModel,
|
||||
selectedProvider,
|
||||
showProviderLabel,
|
||||
],
|
||||
);
|
||||
const keyExtractor = useCallback((row: ProviderSelectionModelRow) => row.favoriteKey, []);
|
||||
|
||||
@@ -1019,6 +1238,10 @@ function ProviderModelBrowserContent({
|
||||
onSelect,
|
||||
onApplyProfile,
|
||||
onEditProfiles,
|
||||
onCreateProfile,
|
||||
onEditProfile,
|
||||
profiledLookup,
|
||||
showProfilesSection = true,
|
||||
onRetryProvider,
|
||||
isRetryingProvider,
|
||||
scrolling,
|
||||
@@ -1032,6 +1255,10 @@ function ProviderModelBrowserContent({
|
||||
onSelect: (provider: string, modelId: string) => void;
|
||||
onApplyProfile?: (profileId: string) => void;
|
||||
onEditProfiles?: () => void;
|
||||
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||
onEditProfile?: (profileId: string) => void;
|
||||
profiledLookup: Map<string, AgentProfilePickerRowModel[]>;
|
||||
showProfilesSection?: boolean;
|
||||
onRetryProvider?: (provider: AgentProvider) => void;
|
||||
isRetryingProvider: boolean;
|
||||
scrolling: "sheet" | "independent";
|
||||
@@ -1047,14 +1274,21 @@ function ProviderModelBrowserContent({
|
||||
);
|
||||
const profileHeader = useMemo(
|
||||
() =>
|
||||
normalizedQuery.length === 0 && profiles ? (
|
||||
normalizedQuery.length === 0 && showProfilesSection && profiles ? (
|
||||
<AgentProfilesPickerContent
|
||||
rows={providerProfileRows}
|
||||
onApplyProfile={onApplyProfile}
|
||||
onEditProfiles={onEditProfiles}
|
||||
/>
|
||||
) : undefined,
|
||||
[normalizedQuery, onApplyProfile, onEditProfiles, profiles, providerProfileRows],
|
||||
[
|
||||
normalizedQuery,
|
||||
onApplyProfile,
|
||||
onEditProfiles,
|
||||
profiles,
|
||||
providerProfileRows,
|
||||
showProfilesSection,
|
||||
],
|
||||
);
|
||||
|
||||
if (!provider) return <ModelSearchEmptyState />;
|
||||
@@ -1090,6 +1324,10 @@ function ProviderModelBrowserContent({
|
||||
onSelect={onSelect}
|
||||
header={profileHeader}
|
||||
scrolling={scrolling}
|
||||
profiledLookup={profiledLookup}
|
||||
onCreateProfile={onCreateProfile}
|
||||
onEditProfile={onEditProfile}
|
||||
onEditProfiles={onEditProfiles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1105,15 +1343,22 @@ function ModelBrowserContent({
|
||||
onSelect,
|
||||
onApplyProfile,
|
||||
onEditProfiles,
|
||||
onCreateProfile,
|
||||
onEditProfile,
|
||||
onDrillDown,
|
||||
onRetryProvider,
|
||||
isRetryingProvider = false,
|
||||
scrolling,
|
||||
searchAllOnFocus,
|
||||
rootBrowseContent,
|
||||
showProfilesSection = true,
|
||||
}: ModelBrowserContentProps) {
|
||||
const { t } = useTranslation();
|
||||
const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]);
|
||||
const profiledLookup = useMemo(
|
||||
() => groupProfilesByProviderModel(profiles?.rows ?? []),
|
||||
[profiles],
|
||||
);
|
||||
const selectedViewProvider = useMemo(
|
||||
() =>
|
||||
view.kind === "provider"
|
||||
@@ -1144,6 +1389,10 @@ function ModelBrowserContent({
|
||||
onSelect={onSelect}
|
||||
onApplyProfile={onApplyProfile}
|
||||
onEditProfiles={onEditProfiles}
|
||||
onCreateProfile={onCreateProfile}
|
||||
onEditProfile={onEditProfile}
|
||||
profiledLookup={profiledLookup}
|
||||
showProfilesSection={showProfilesSection}
|
||||
onRetryProvider={onRetryProvider}
|
||||
isRetryingProvider={isRetryingProvider}
|
||||
scrolling={scrolling}
|
||||
@@ -1171,13 +1420,17 @@ function ModelBrowserContent({
|
||||
onSelect={onSelect}
|
||||
showProviderLabel
|
||||
scrolling={scrolling}
|
||||
profiledLookup={profiledLookup}
|
||||
onCreateProfile={onCreateProfile}
|
||||
onEditProfile={onEditProfile}
|
||||
onEditProfiles={onEditProfiles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const allProvidersContent = (
|
||||
<View>
|
||||
{profiles ? (
|
||||
{showProfilesSection && profiles ? (
|
||||
<AgentProfilesPickerContent
|
||||
rows={profiles.rows}
|
||||
onApplyProfile={onApplyProfile}
|
||||
@@ -1187,7 +1440,7 @@ function ModelBrowserContent({
|
||||
{rootBrowseContent ??
|
||||
(providers.length > 0 ? (
|
||||
<View>
|
||||
{profiles ? (
|
||||
{showProfilesSection && profiles ? (
|
||||
<View style={styles.sectionHeading}>
|
||||
<Text style={styles.sectionHeadingText}>{t("modelSelector.providers")}</Text>
|
||||
</View>
|
||||
@@ -1211,11 +1464,14 @@ export function ModelBrowser({
|
||||
onSelect,
|
||||
onApplyProfile,
|
||||
onEditProfiles,
|
||||
onCreateProfile,
|
||||
onEditProfile,
|
||||
onRetryProvider,
|
||||
isRetryingProvider = false,
|
||||
scrolling = "sheet",
|
||||
searchAllOnFocus = false,
|
||||
rootBrowseContent,
|
||||
showProfilesSection,
|
||||
}: ModelBrowserProps) {
|
||||
return (
|
||||
<ModelBrowserContent
|
||||
@@ -1229,12 +1485,15 @@ export function ModelBrowser({
|
||||
onSelect={onSelect}
|
||||
onApplyProfile={onApplyProfile}
|
||||
onEditProfiles={onEditProfiles}
|
||||
onCreateProfile={onCreateProfile}
|
||||
onEditProfile={onEditProfile}
|
||||
onDrillDown={state.drillDown}
|
||||
onRetryProvider={onRetryProvider}
|
||||
isRetryingProvider={isRetryingProvider}
|
||||
scrolling={scrolling}
|
||||
searchAllOnFocus={searchAllOnFocus}
|
||||
rootBrowseContent={rootBrowseContent}
|
||||
showProfilesSection={showProfilesSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1279,6 +1538,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
paddingVertical: theme.spacing[2],
|
||||
minHeight: 36,
|
||||
},
|
||||
modelRowHoverBoundary: {
|
||||
position: "relative",
|
||||
},
|
||||
browserModelRow: isWeb ? {} : { marginBottom: theme.spacing[1] },
|
||||
browserRowHovered: {
|
||||
backgroundColor: theme.colors.surface1,
|
||||
@@ -1371,6 +1633,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
rowIconButtonPressed: {
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
profileActionHidden: {
|
||||
opacity: 0,
|
||||
},
|
||||
emptyState: {
|
||||
paddingVertical: theme.spacing[4],
|
||||
alignItems: "center",
|
||||
|
||||
Reference in New Issue
Block a user