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:
@@ -0,0 +1,78 @@
|
|||||||
|
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||||
|
import type { AgentProfile } from "@getpaseo/protocol/messages";
|
||||||
|
import { AgentProfileEditModal } from "./settings/agent-profile-edit-modal";
|
||||||
|
import { generateAgentProfileId } from "./internal/profile-id";
|
||||||
|
import type { AgentProfileSeed, AgentProfileValue } from "./internal/profile-form-model";
|
||||||
|
import { useAgentProfiles } from "./internal/use-agent-profiles";
|
||||||
|
|
||||||
|
export interface AgentProfileEditorControls {
|
||||||
|
/** Open the create form pre-filled with provider, model, and suggested name. */
|
||||||
|
openCreateFromModel: (seed: AgentProfileSeed) => void;
|
||||||
|
/** Open the edit form for one profile. */
|
||||||
|
openEdit: (profileId: string) => void;
|
||||||
|
/** Render once where a sheet can portal; it self-hides until opened. */
|
||||||
|
element: ReactElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditorRequest =
|
||||||
|
| { mode: "create"; seed?: AgentProfileSeed }
|
||||||
|
| { mode: "edit"; profile: AgentProfile };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model chooser's create/edit surface. Owns the write path so call sites
|
||||||
|
* never see `saveProfiles` or construct profile records themselves.
|
||||||
|
*/
|
||||||
|
export function useAgentProfileEditor(serverId: string | null): AgentProfileEditorControls {
|
||||||
|
const { profiles, saveProfiles } = useAgentProfiles(serverId);
|
||||||
|
const [request, setRequest] = useState<EditorRequest | null>(null);
|
||||||
|
|
||||||
|
const openCreateFromModel = useCallback((seed: AgentProfileSeed) => {
|
||||||
|
setRequest({ mode: "create", seed });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openEdit = useCallback(
|
||||||
|
(profileId: string) => {
|
||||||
|
const profile = profiles?.find((entry) => entry.id === profileId);
|
||||||
|
if (!profile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRequest({ mode: "edit", profile });
|
||||||
|
},
|
||||||
|
[profiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const close = useCallback(() => setRequest(null), []);
|
||||||
|
|
||||||
|
const handleSave = useCallback(
|
||||||
|
async (value: AgentProfileValue) => {
|
||||||
|
const current = profiles ?? [];
|
||||||
|
const editing = request?.mode === "edit" ? request.profile : undefined;
|
||||||
|
const next: AgentProfile[] = editing
|
||||||
|
? current.map((entry) => (entry.id === editing.id ? { id: entry.id, ...value } : entry))
|
||||||
|
: [...current, { id: generateAgentProfileId(), ...value }];
|
||||||
|
await saveProfiles(next);
|
||||||
|
setRequest(null);
|
||||||
|
},
|
||||||
|
[profiles, request, saveProfiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const element = useMemo(
|
||||||
|
() => (
|
||||||
|
<AgentProfileEditModal
|
||||||
|
serverId={serverId ?? ""}
|
||||||
|
visible={request !== null}
|
||||||
|
mode={request?.mode ?? "create"}
|
||||||
|
{...(request?.mode === "edit" && request.profile ? { profile: request.profile } : {})}
|
||||||
|
{...(request?.mode === "create" && request.seed ? { seed: request.seed } : {})}
|
||||||
|
onClose={close}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[close, handleSave, request, serverId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({ openCreateFromModel, openEdit, element }),
|
||||||
|
[openCreateFromModel, openEdit, element],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
export type { AgentProfile } from "@getpaseo/protocol/messages";
|
export type { AgentProfile } from "@getpaseo/protocol/messages";
|
||||||
export type { MaterializedAgentProfile } from "./internal/materialize-profile";
|
export type { MaterializedAgentProfile } from "./internal/materialize-profile";
|
||||||
|
export type { AgentProfileSeed } from "./internal/profile-form-model";
|
||||||
|
export { useAgentProfileEditor, type AgentProfileEditorControls } from "./agent-profile-editor";
|
||||||
export { useAgentProfiles } from "./internal/use-agent-profiles";
|
export { useAgentProfiles } from "./internal/use-agent-profiles";
|
||||||
export {
|
export {
|
||||||
useAgentProfilePicker,
|
useAgentProfilePicker,
|
||||||
|
|||||||
@@ -82,6 +82,53 @@ describe("openAgentProfileForm", () => {
|
|||||||
expect(state.disclosure.showModelField).toBe(false);
|
expect(state.disclosure.showModelField).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("seeds create mode from a model seed before the catalog lands", () => {
|
||||||
|
const model = openAgentProfileForm({
|
||||||
|
mode: "create",
|
||||||
|
seed: { provider: "claude", modelId: "claude-opus-5", name: "Opus 5" },
|
||||||
|
});
|
||||||
|
const state = model.getState();
|
||||||
|
|
||||||
|
expect(state.name).toBe("Opus 5");
|
||||||
|
expect(state.provider).toBe("claude");
|
||||||
|
expect(state.modelId).toBe("claude-opus-5");
|
||||||
|
expect(state.providerDisplay).toEqual({ label: "claude" });
|
||||||
|
expect(state.modelDisplay).toEqual({ label: "claude-opus-5" });
|
||||||
|
expect(state.catalogResolution).toBe("idle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("upgrades a seeded model to catalog labels without touching the selection", () => {
|
||||||
|
const model = openAgentProfileForm({
|
||||||
|
mode: "create",
|
||||||
|
seed: { provider: "claude", modelId: "claude-opus-5", name: "Opus 5" },
|
||||||
|
});
|
||||||
|
model.applyProviderCatalog(ENTRIES);
|
||||||
|
const state = model.getState();
|
||||||
|
|
||||||
|
expect(state.providerDisplay).toEqual({ label: "Claude Code" });
|
||||||
|
expect(state.modelDisplay).toEqual({ label: "Opus 5" });
|
||||||
|
expect(state.modelId).toBe("claude-opus-5");
|
||||||
|
expect(state.canSubmit).toBe(true);
|
||||||
|
expect(state.submitValue).toMatchObject({
|
||||||
|
name: "Opus 5",
|
||||||
|
provider: "claude",
|
||||||
|
model: "claude-opus-5",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores the seed in edit mode", () => {
|
||||||
|
const model = openAgentProfileForm({
|
||||||
|
mode: "edit",
|
||||||
|
profile: { id: "p1", name: "UI work", provider: "claude", model: "claude-haiku-4-5" },
|
||||||
|
seed: { provider: "codex", modelId: "gpt-5.2-codex", name: "Nope" },
|
||||||
|
});
|
||||||
|
const state = model.getState();
|
||||||
|
|
||||||
|
expect(state.name).toBe("UI work");
|
||||||
|
expect(state.provider).toBe("claude");
|
||||||
|
expect(state.modelId).toBe("claude-haiku-4-5");
|
||||||
|
});
|
||||||
|
|
||||||
it("lists only enabled providers", () => {
|
it("lists only enabled providers", () => {
|
||||||
const model = openWithCatalog({ mode: "create" });
|
const model = openWithCatalog({ mode: "create" });
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,20 @@ export interface AgentProfileFormOption {
|
|||||||
testID: string;
|
testID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-fill values for create mode. Only used when `mode === "create"`; edit
|
||||||
|
* mode always seeds from the stored `profile` and ignores this.
|
||||||
|
*/
|
||||||
|
export interface AgentProfileSeed {
|
||||||
|
provider: string;
|
||||||
|
modelId?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentProfileFormSnapshot {
|
export interface AgentProfileFormSnapshot {
|
||||||
mode: "create" | "edit";
|
mode: "create" | "edit";
|
||||||
profile?: AgentProfile;
|
profile?: AgentProfile;
|
||||||
|
seed?: AgentProfileSeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,14 +378,18 @@ function seedDisplay(value: string | undefined): AgentProfileFormDisplay | null
|
|||||||
*/
|
*/
|
||||||
function buildInitialState(snapshot: AgentProfileFormSnapshot): AgentProfileFormState {
|
function buildInitialState(snapshot: AgentProfileFormSnapshot): AgentProfileFormState {
|
||||||
const profile = snapshot.profile ?? BLANK_PROFILE;
|
const profile = snapshot.profile ?? BLANK_PROFILE;
|
||||||
|
const seed = snapshot.mode === "create" ? snapshot.seed : undefined;
|
||||||
|
const name = seed?.name ?? profile.name;
|
||||||
|
const provider = seed?.provider ?? profile.provider;
|
||||||
|
const modelId = seed?.modelId ?? profile.model ?? "";
|
||||||
return {
|
return {
|
||||||
mode: snapshot.mode,
|
mode: snapshot.mode,
|
||||||
name: profile.name,
|
name,
|
||||||
icon: profile.icon ?? "",
|
icon: profile.icon ?? "",
|
||||||
color: profile.color ?? "",
|
color: profile.color ?? "",
|
||||||
notes: profile.notes ?? "",
|
notes: profile.notes ?? "",
|
||||||
provider: profile.provider,
|
provider,
|
||||||
modelId: profile.model ?? "",
|
modelId,
|
||||||
modeId: profile.modeId ?? "",
|
modeId: profile.modeId ?? "",
|
||||||
thinkingOptionId: profile.thinkingOptionId ?? "",
|
thinkingOptionId: profile.thinkingOptionId ?? "",
|
||||||
featureValues: { ...profile.featureValues },
|
featureValues: { ...profile.featureValues },
|
||||||
@@ -383,8 +398,8 @@ function buildInitialState(snapshot: AgentProfileFormSnapshot): AgentProfileForm
|
|||||||
modeOptions: [],
|
modeOptions: [],
|
||||||
thinkingOptions: [],
|
thinkingOptions: [],
|
||||||
features: [],
|
features: [],
|
||||||
providerDisplay: seedDisplay(profile.provider),
|
providerDisplay: seedDisplay(provider),
|
||||||
modelDisplay: seedDisplay(profile.model),
|
modelDisplay: seedDisplay(modelId),
|
||||||
modeDisplay: seedDisplay(profile.modeId),
|
modeDisplay: seedDisplay(profile.modeId),
|
||||||
thinkingDisplay: seedDisplay(profile.thinkingOptionId),
|
thinkingDisplay: seedDisplay(profile.thinkingOptionId),
|
||||||
catalogResolution: "idle",
|
catalogResolution: "idle",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function generateAgentProfileId(): string {
|
||||||
|
return `agent_profile_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ export type AgentProfileApplyTarget =
|
|||||||
export interface AgentProfilePickerRow {
|
export interface AgentProfilePickerRow {
|
||||||
id: string;
|
id: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
|
/** Empty when the profile names no model. */
|
||||||
|
modelId: string;
|
||||||
/** Icon registry key and identity colour; either may be empty for the default glyph. */
|
/** Icon registry key and identity colour; either may be empty for the default glyph. */
|
||||||
icon: string;
|
icon: string;
|
||||||
color: string;
|
color: string;
|
||||||
@@ -95,6 +97,7 @@ export function useAgentProfilePicker(
|
|||||||
applicableProfiles.map((profile) => ({
|
applicableProfiles.map((profile) => ({
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
provider: profile.provider,
|
provider: profile.provider,
|
||||||
|
modelId: profile.model?.trim() ?? "",
|
||||||
icon: profile.icon ?? "",
|
icon: profile.icon ?? "",
|
||||||
color: profile.color ?? "",
|
color: profile.color ?? "",
|
||||||
name: profile.name,
|
name: profile.name,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { AgentProfileAppearanceField } from "./agent-profile-appearance-field";
|
|||||||
import type {
|
import type {
|
||||||
AgentProfileFormModel,
|
AgentProfileFormModel,
|
||||||
AgentProfileFormOption,
|
AgentProfileFormOption,
|
||||||
|
AgentProfileSeed,
|
||||||
AgentProfileValue,
|
AgentProfileValue,
|
||||||
} from "../internal/profile-form-model";
|
} from "../internal/profile-form-model";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +37,7 @@ export interface AgentProfileEditModalProps {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
mode: "create" | "edit";
|
mode: "create" | "edit";
|
||||||
profile?: AgentProfile;
|
profile?: AgentProfile;
|
||||||
|
seed?: AgentProfileSeed;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (value: AgentProfileValue) => Promise<void>;
|
onSave: (value: AgentProfileValue) => Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -47,7 +49,10 @@ export interface AgentProfileEditModalProps {
|
|||||||
const UNSET_VALUE = "";
|
const UNSET_VALUE = "";
|
||||||
|
|
||||||
function openKey(props: AgentProfileEditModalProps): string {
|
function openKey(props: AgentProfileEditModalProps): string {
|
||||||
return props.mode === "edit" ? `edit:${props.profile?.id ?? ""}` : "create";
|
if (props.mode === "edit") {
|
||||||
|
return `edit:${props.profile?.id ?? ""}`;
|
||||||
|
}
|
||||||
|
return `create:${props.seed?.provider ?? ""}:${props.seed?.modelId ?? ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,13 +127,17 @@ function OpenAgentProfileEditModal({
|
|||||||
visible,
|
visible,
|
||||||
mode,
|
mode,
|
||||||
profile,
|
profile,
|
||||||
|
seed,
|
||||||
onClose,
|
onClose,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
onSave,
|
onSave,
|
||||||
}: AgentProfileEditModalProps & { onDismiss: () => void }): ReactElement {
|
}: AgentProfileEditModalProps & { onDismiss: () => void }): ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const controlSize: FieldControlSize = useIsCompactFormFactor() ? "md" : "sm";
|
const controlSize: FieldControlSize = useIsCompactFormFactor() ? "md" : "sm";
|
||||||
const snapshot = useMemo(() => ({ mode, ...(profile ? { profile } : {}) }), [mode, profile]);
|
const snapshot = useMemo(
|
||||||
|
() => ({ mode, ...(profile ? { profile } : {}), ...(seed ? { seed } : {}) }),
|
||||||
|
[mode, profile, seed],
|
||||||
|
);
|
||||||
const model = useAgentProfileFormModel(snapshot);
|
const model = useAgentProfileFormModel(snapshot);
|
||||||
const state = useAgentProfileFormState(model);
|
const state = useAgentProfileFormState(model);
|
||||||
useAgentProfileFormCatalog({ serverId, model });
|
useAgentProfileFormCatalog({ serverId, model });
|
||||||
@@ -234,7 +243,7 @@ function OpenAgentProfileEditModal({
|
|||||||
testID="agent-profile-name-field"
|
testID="agent-profile-name-field"
|
||||||
>
|
>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
initialValue={profile?.name ?? ""}
|
initialValue={seed?.name ?? profile?.name ?? ""}
|
||||||
onChangeText={model.setName}
|
onChangeText={model.setName}
|
||||||
placeholder={t("settings.host.agentProfiles.namePlaceholder")}
|
placeholder={t("settings.host.agentProfiles.namePlaceholder")}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { settingsStyles } from "@/styles/settings";
|
|||||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||||
import { useAgentProfiles } from "../internal/use-agent-profiles";
|
import { useAgentProfiles } from "../internal/use-agent-profiles";
|
||||||
|
import { generateAgentProfileId } from "../internal/profile-id";
|
||||||
import type { AgentProfileValue } from "../internal/profile-form-model";
|
import type { AgentProfileValue } from "../internal/profile-form-model";
|
||||||
import { AgentProfileEditModal } from "./agent-profile-edit-modal";
|
import { AgentProfileEditModal } from "./agent-profile-edit-modal";
|
||||||
import { AgentProfileRow } from "./agent-profile-row";
|
import { AgentProfileRow } from "./agent-profile-row";
|
||||||
@@ -20,10 +21,6 @@ const ThemedPlus = withUnistyles(Plus);
|
|||||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
const addIcon = <ThemedPlus size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
const addIcon = <ThemedPlus size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
||||||
|
|
||||||
function generateAgentProfileId(): string {
|
|
||||||
return `agent_profile_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EditTarget {
|
interface EditTarget {
|
||||||
mode: "create" | "edit";
|
mode: "create" | "edit";
|
||||||
profile?: AgentProfile;
|
profile?: AgentProfile;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Pressable, Text, View, type PressableStateCallbackType } from "react-na
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
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 { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
||||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { Combobox, type ComboboxOption, type ComboboxProps } from "@/components/ui/combobox";
|
import { Combobox, type ComboboxOption, type ComboboxProps } from "@/components/ui/combobox";
|
||||||
@@ -30,6 +30,8 @@ interface CombinedModelSelectorProps {
|
|||||||
profiles?: AgentProfilePicker | null;
|
profiles?: AgentProfilePicker | null;
|
||||||
onApplyProfile?: (profileId: string) => void;
|
onApplyProfile?: (profileId: string) => void;
|
||||||
onEditProfiles?: () => void;
|
onEditProfiles?: () => void;
|
||||||
|
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditProfile?: (profileId: string) => void;
|
||||||
renderTrigger?: (input: {
|
renderTrigger?: (input: {
|
||||||
selectedModelLabel: string;
|
selectedModelLabel: string;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
@@ -70,6 +72,8 @@ export function CombinedModelSelector({
|
|||||||
profiles = null,
|
profiles = null,
|
||||||
onApplyProfile,
|
onApplyProfile,
|
||||||
onEditProfiles,
|
onEditProfiles,
|
||||||
|
onCreateProfile,
|
||||||
|
onEditProfile,
|
||||||
renderTrigger,
|
renderTrigger,
|
||||||
onOpen,
|
onOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -168,12 +172,30 @@ export function CombinedModelSelector({
|
|||||||
onEditProfiles?.();
|
onEditProfiles?.();
|
||||||
}, [handleOpenChange, 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 ? (
|
const selectorBody = isContentReady ? (
|
||||||
<ModelBrowser
|
<ModelBrowser
|
||||||
state={browser}
|
state={browser}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
onApplyProfile={handleApplyProfile}
|
onApplyProfile={handleApplyProfile}
|
||||||
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
|
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
|
||||||
|
onCreateProfile={onCreateProfile ? handleCreateProfile : undefined}
|
||||||
|
onEditProfile={onEditProfile ? handleEditProfile : undefined}
|
||||||
onRetryProvider={onRetryProvider}
|
onRetryProvider={onRetryProvider}
|
||||||
isRetryingProvider={isRetryingProvider}
|
isRetryingProvider={isRetryingProvider}
|
||||||
scrolling={isWeb ? "independent" : "sheet"}
|
scrolling={isWeb ? "independent" : "sheet"}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import type {
|
|||||||
ProviderSelectionModelRow,
|
ProviderSelectionModelRow,
|
||||||
ProviderSelectorProvider,
|
ProviderSelectorProvider,
|
||||||
} from "@/provider-selection/provider-selection";
|
} from "@/provider-selection/provider-selection";
|
||||||
import { resolveInitialModelBrowserView, resolveModelBrowserAllView } from "./model-browser-view";
|
import {
|
||||||
|
resolveInitialModelBrowserView,
|
||||||
|
resolveModelBrowserAllView,
|
||||||
|
groupProfilesByProviderModel,
|
||||||
|
} from "./model-browser-view";
|
||||||
|
|
||||||
function provider(
|
function provider(
|
||||||
id: string,
|
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", () => {
|
describe("model browser all view", () => {
|
||||||
const claude = provider("claude", "Claude Code", [
|
const claude = provider("claude", "Claude Code", [
|
||||||
modelRow("claude", "Claude Code", "opus-5", "Opus 5"),
|
modelRow("claude", "Claude Code", "opus-5", "Opus 5"),
|
||||||
|
|||||||
@@ -9,6 +9,36 @@ export type ModelBrowserView =
|
|||||||
| { kind: "all" }
|
| { kind: "all" }
|
||||||
| { kind: "provider"; providerId: string; providerLabel: string };
|
| { 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. */
|
/** What the root view shows: the provider drill-down, or ranked cross-provider results. */
|
||||||
export type ModelBrowserAllView =
|
export type ModelBrowserAllView =
|
||||||
| { kind: "browse" }
|
| { kind: "browse" }
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
AgentProfileGlyph,
|
AgentProfileGlyph,
|
||||||
type AgentProfilePicker,
|
type AgentProfilePicker,
|
||||||
type AgentProfilePickerRow as AgentProfilePickerRowModel,
|
type AgentProfilePickerRow as AgentProfilePickerRowModel,
|
||||||
|
type AgentProfileSeed,
|
||||||
} from "@/agent-profiles";
|
} from "@/agent-profiles";
|
||||||
import type { SheetHeader } from "@/components/adaptive-modal-sheet";
|
import type { SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -52,6 +53,7 @@ import { useProviderSettingsStore } from "@/stores/provider-settings-store";
|
|||||||
import { useCurrentOverlayLayer } from "@/lib/overlay-root";
|
import { useCurrentOverlayLayer } from "@/lib/overlay-root";
|
||||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||||
import {
|
import {
|
||||||
|
groupProfilesByProviderModel,
|
||||||
resolveInitialModelBrowserView,
|
resolveInitialModelBrowserView,
|
||||||
resolveModelBrowserAllView,
|
resolveModelBrowserAllView,
|
||||||
type ModelBrowserView,
|
type ModelBrowserView,
|
||||||
@@ -176,6 +178,8 @@ interface ModelBrowserProps {
|
|||||||
/** Applying a profile resolves the pick and dismisses, exactly like a model row. */
|
/** Applying a profile resolves the pick and dismisses, exactly like a model row. */
|
||||||
onApplyProfile?: (profileId: string) => void;
|
onApplyProfile?: (profileId: string) => void;
|
||||||
onEditProfiles?: () => void;
|
onEditProfiles?: () => void;
|
||||||
|
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditProfile?: (profileId: string) => void;
|
||||||
onRetryProvider?: (provider: AgentProvider) => void;
|
onRetryProvider?: (provider: AgentProvider) => void;
|
||||||
isRetryingProvider?: boolean;
|
isRetryingProvider?: boolean;
|
||||||
scrolling?: "sheet" | "independent";
|
scrolling?: "sheet" | "independent";
|
||||||
@@ -183,6 +187,8 @@ interface ModelBrowserProps {
|
|||||||
searchAllOnFocus?: boolean;
|
searchAllOnFocus?: boolean;
|
||||||
/** Replaces provider rows only while the all-provider search is empty. */
|
/** Replaces provider rows only while the all-provider search is empty. */
|
||||||
rootBrowseContent?: React.ReactNode;
|
rootBrowseContent?: React.ReactNode;
|
||||||
|
/** Hide the pinned Profiles section while still using rows for model matching. */
|
||||||
|
showProfilesSection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelBrowserContentProps extends Omit<ModelBrowserProps, "state" | "scrolling"> {
|
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({
|
function ModelRow({
|
||||||
row,
|
row,
|
||||||
isSelected,
|
isSelected,
|
||||||
showProviderLabel = false,
|
showProviderLabel = false,
|
||||||
onPress,
|
onPress,
|
||||||
|
profiledRows,
|
||||||
|
onCreateProfile,
|
||||||
|
onEditProfile,
|
||||||
|
onEditProfiles,
|
||||||
}: {
|
}: {
|
||||||
row: ProviderSelectionModelRow;
|
row: ProviderSelectionModelRow;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
showProviderLabel?: boolean;
|
showProviderLabel?: boolean;
|
||||||
onPress: () => void;
|
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(
|
const leadingSlot = useMemo(
|
||||||
() => <ModelProviderGlyph provider={row.provider} size={ICON_SIZE.sm} />,
|
() => <ModelProviderGlyph provider={row.provider} size={ICON_SIZE.sm} />,
|
||||||
[row.provider],
|
[row.provider],
|
||||||
);
|
);
|
||||||
|
|
||||||
const description = showProviderLabel ? buildProviderQualifiedDescription(row) : row.description;
|
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 (
|
return (
|
||||||
<ModelBrowserRow
|
<View
|
||||||
label={row.modelLabel}
|
style={styles.modelRowHoverBoundary}
|
||||||
description={description}
|
onPointerEnter={handlePointerEnter}
|
||||||
selected={isSelected}
|
onPointerLeave={handlePointerLeave}
|
||||||
selectionIndicator
|
>
|
||||||
onPress={onPress}
|
<ModelBrowserPressable
|
||||||
leadingSlot={leadingSlot}
|
onPress={onPress}
|
||||||
testID={`model-row-${row.provider}-${row.modelId}`}
|
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,
|
isSelected,
|
||||||
showProviderLabel,
|
showProviderLabel,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
profiledRows,
|
||||||
|
onCreateProfile,
|
||||||
|
onEditProfile,
|
||||||
|
onEditProfiles,
|
||||||
}: {
|
}: {
|
||||||
row: ProviderSelectionModelRow;
|
row: ProviderSelectionModelRow;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
showProviderLabel?: boolean;
|
showProviderLabel?: boolean;
|
||||||
onSelect: (provider: string, modelId: string) => void;
|
onSelect: (provider: string, modelId: string) => void;
|
||||||
|
profiledRows: AgentProfilePickerRowModel[];
|
||||||
|
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditProfile?: (profileId: string) => void;
|
||||||
|
onEditProfiles?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
onSelect(row.provider, row.modelId);
|
onSelect(row.provider, row.modelId);
|
||||||
@@ -642,6 +836,10 @@ function SelectableModelRow({
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
showProviderLabel={showProviderLabel}
|
showProviderLabel={showProviderLabel}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
|
profiledRows={profiledRows}
|
||||||
|
onCreateProfile={onCreateProfile}
|
||||||
|
onEditProfile={onEditProfile}
|
||||||
|
onEditProfiles={onEditProfiles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -919,6 +1117,10 @@ function ModelRowList({
|
|||||||
showProviderLabel = false,
|
showProviderLabel = false,
|
||||||
header,
|
header,
|
||||||
scrolling,
|
scrolling,
|
||||||
|
profiledLookup,
|
||||||
|
onCreateProfile,
|
||||||
|
onEditProfile,
|
||||||
|
onEditProfiles,
|
||||||
}: {
|
}: {
|
||||||
rows: ProviderSelectionModelRow[];
|
rows: ProviderSelectionModelRow[];
|
||||||
selectedProvider: string;
|
selectedProvider: string;
|
||||||
@@ -927,6 +1129,10 @@ function ModelRowList({
|
|||||||
showProviderLabel?: boolean;
|
showProviderLabel?: boolean;
|
||||||
header?: React.ReactElement;
|
header?: React.ReactElement;
|
||||||
scrolling: "sheet" | "independent";
|
scrolling: "sheet" | "independent";
|
||||||
|
profiledLookup: Map<string, AgentProfilePickerRowModel[]>;
|
||||||
|
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditProfile?: (profileId: string) => void;
|
||||||
|
onEditProfiles?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const isCompact = useIsCompactFormFactor();
|
const isCompact = useIsCompactFormFactor();
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
@@ -936,9 +1142,22 @@ function ModelRowList({
|
|||||||
isSelected={item.provider === selectedProvider && item.modelId === selectedModel}
|
isSelected={item.provider === selectedProvider && item.modelId === selectedModel}
|
||||||
showProviderLabel={showProviderLabel}
|
showProviderLabel={showProviderLabel}
|
||||||
onSelect={onSelect}
|
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, []);
|
const keyExtractor = useCallback((row: ProviderSelectionModelRow) => row.favoriteKey, []);
|
||||||
|
|
||||||
@@ -1019,6 +1238,10 @@ function ProviderModelBrowserContent({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onApplyProfile,
|
onApplyProfile,
|
||||||
onEditProfiles,
|
onEditProfiles,
|
||||||
|
onCreateProfile,
|
||||||
|
onEditProfile,
|
||||||
|
profiledLookup,
|
||||||
|
showProfilesSection = true,
|
||||||
onRetryProvider,
|
onRetryProvider,
|
||||||
isRetryingProvider,
|
isRetryingProvider,
|
||||||
scrolling,
|
scrolling,
|
||||||
@@ -1032,6 +1255,10 @@ function ProviderModelBrowserContent({
|
|||||||
onSelect: (provider: string, modelId: string) => void;
|
onSelect: (provider: string, modelId: string) => void;
|
||||||
onApplyProfile?: (profileId: string) => void;
|
onApplyProfile?: (profileId: string) => void;
|
||||||
onEditProfiles?: () => void;
|
onEditProfiles?: () => void;
|
||||||
|
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditProfile?: (profileId: string) => void;
|
||||||
|
profiledLookup: Map<string, AgentProfilePickerRowModel[]>;
|
||||||
|
showProfilesSection?: boolean;
|
||||||
onRetryProvider?: (provider: AgentProvider) => void;
|
onRetryProvider?: (provider: AgentProvider) => void;
|
||||||
isRetryingProvider: boolean;
|
isRetryingProvider: boolean;
|
||||||
scrolling: "sheet" | "independent";
|
scrolling: "sheet" | "independent";
|
||||||
@@ -1047,14 +1274,21 @@ function ProviderModelBrowserContent({
|
|||||||
);
|
);
|
||||||
const profileHeader = useMemo(
|
const profileHeader = useMemo(
|
||||||
() =>
|
() =>
|
||||||
normalizedQuery.length === 0 && profiles ? (
|
normalizedQuery.length === 0 && showProfilesSection && profiles ? (
|
||||||
<AgentProfilesPickerContent
|
<AgentProfilesPickerContent
|
||||||
rows={providerProfileRows}
|
rows={providerProfileRows}
|
||||||
onApplyProfile={onApplyProfile}
|
onApplyProfile={onApplyProfile}
|
||||||
onEditProfiles={onEditProfiles}
|
onEditProfiles={onEditProfiles}
|
||||||
/>
|
/>
|
||||||
) : undefined,
|
) : undefined,
|
||||||
[normalizedQuery, onApplyProfile, onEditProfiles, profiles, providerProfileRows],
|
[
|
||||||
|
normalizedQuery,
|
||||||
|
onApplyProfile,
|
||||||
|
onEditProfiles,
|
||||||
|
profiles,
|
||||||
|
providerProfileRows,
|
||||||
|
showProfilesSection,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!provider) return <ModelSearchEmptyState />;
|
if (!provider) return <ModelSearchEmptyState />;
|
||||||
@@ -1090,6 +1324,10 @@ function ProviderModelBrowserContent({
|
|||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
header={profileHeader}
|
header={profileHeader}
|
||||||
scrolling={scrolling}
|
scrolling={scrolling}
|
||||||
|
profiledLookup={profiledLookup}
|
||||||
|
onCreateProfile={onCreateProfile}
|
||||||
|
onEditProfile={onEditProfile}
|
||||||
|
onEditProfiles={onEditProfiles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1105,15 +1343,22 @@ function ModelBrowserContent({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onApplyProfile,
|
onApplyProfile,
|
||||||
onEditProfiles,
|
onEditProfiles,
|
||||||
|
onCreateProfile,
|
||||||
|
onEditProfile,
|
||||||
onDrillDown,
|
onDrillDown,
|
||||||
onRetryProvider,
|
onRetryProvider,
|
||||||
isRetryingProvider = false,
|
isRetryingProvider = false,
|
||||||
scrolling,
|
scrolling,
|
||||||
searchAllOnFocus,
|
searchAllOnFocus,
|
||||||
rootBrowseContent,
|
rootBrowseContent,
|
||||||
|
showProfilesSection = true,
|
||||||
}: ModelBrowserContentProps) {
|
}: ModelBrowserContentProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]);
|
const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]);
|
||||||
|
const profiledLookup = useMemo(
|
||||||
|
() => groupProfilesByProviderModel(profiles?.rows ?? []),
|
||||||
|
[profiles],
|
||||||
|
);
|
||||||
const selectedViewProvider = useMemo(
|
const selectedViewProvider = useMemo(
|
||||||
() =>
|
() =>
|
||||||
view.kind === "provider"
|
view.kind === "provider"
|
||||||
@@ -1144,6 +1389,10 @@ function ModelBrowserContent({
|
|||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onApplyProfile={onApplyProfile}
|
onApplyProfile={onApplyProfile}
|
||||||
onEditProfiles={onEditProfiles}
|
onEditProfiles={onEditProfiles}
|
||||||
|
onCreateProfile={onCreateProfile}
|
||||||
|
onEditProfile={onEditProfile}
|
||||||
|
profiledLookup={profiledLookup}
|
||||||
|
showProfilesSection={showProfilesSection}
|
||||||
onRetryProvider={onRetryProvider}
|
onRetryProvider={onRetryProvider}
|
||||||
isRetryingProvider={isRetryingProvider}
|
isRetryingProvider={isRetryingProvider}
|
||||||
scrolling={scrolling}
|
scrolling={scrolling}
|
||||||
@@ -1171,13 +1420,17 @@ function ModelBrowserContent({
|
|||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
showProviderLabel
|
showProviderLabel
|
||||||
scrolling={scrolling}
|
scrolling={scrolling}
|
||||||
|
profiledLookup={profiledLookup}
|
||||||
|
onCreateProfile={onCreateProfile}
|
||||||
|
onEditProfile={onEditProfile}
|
||||||
|
onEditProfiles={onEditProfiles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allProvidersContent = (
|
const allProvidersContent = (
|
||||||
<View>
|
<View>
|
||||||
{profiles ? (
|
{showProfilesSection && profiles ? (
|
||||||
<AgentProfilesPickerContent
|
<AgentProfilesPickerContent
|
||||||
rows={profiles.rows}
|
rows={profiles.rows}
|
||||||
onApplyProfile={onApplyProfile}
|
onApplyProfile={onApplyProfile}
|
||||||
@@ -1187,7 +1440,7 @@ function ModelBrowserContent({
|
|||||||
{rootBrowseContent ??
|
{rootBrowseContent ??
|
||||||
(providers.length > 0 ? (
|
(providers.length > 0 ? (
|
||||||
<View>
|
<View>
|
||||||
{profiles ? (
|
{showProfilesSection && profiles ? (
|
||||||
<View style={styles.sectionHeading}>
|
<View style={styles.sectionHeading}>
|
||||||
<Text style={styles.sectionHeadingText}>{t("modelSelector.providers")}</Text>
|
<Text style={styles.sectionHeadingText}>{t("modelSelector.providers")}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -1211,11 +1464,14 @@ export function ModelBrowser({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onApplyProfile,
|
onApplyProfile,
|
||||||
onEditProfiles,
|
onEditProfiles,
|
||||||
|
onCreateProfile,
|
||||||
|
onEditProfile,
|
||||||
onRetryProvider,
|
onRetryProvider,
|
||||||
isRetryingProvider = false,
|
isRetryingProvider = false,
|
||||||
scrolling = "sheet",
|
scrolling = "sheet",
|
||||||
searchAllOnFocus = false,
|
searchAllOnFocus = false,
|
||||||
rootBrowseContent,
|
rootBrowseContent,
|
||||||
|
showProfilesSection,
|
||||||
}: ModelBrowserProps) {
|
}: ModelBrowserProps) {
|
||||||
return (
|
return (
|
||||||
<ModelBrowserContent
|
<ModelBrowserContent
|
||||||
@@ -1229,12 +1485,15 @@ export function ModelBrowser({
|
|||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onApplyProfile={onApplyProfile}
|
onApplyProfile={onApplyProfile}
|
||||||
onEditProfiles={onEditProfiles}
|
onEditProfiles={onEditProfiles}
|
||||||
|
onCreateProfile={onCreateProfile}
|
||||||
|
onEditProfile={onEditProfile}
|
||||||
onDrillDown={state.drillDown}
|
onDrillDown={state.drillDown}
|
||||||
onRetryProvider={onRetryProvider}
|
onRetryProvider={onRetryProvider}
|
||||||
isRetryingProvider={isRetryingProvider}
|
isRetryingProvider={isRetryingProvider}
|
||||||
scrolling={scrolling}
|
scrolling={scrolling}
|
||||||
searchAllOnFocus={searchAllOnFocus}
|
searchAllOnFocus={searchAllOnFocus}
|
||||||
rootBrowseContent={rootBrowseContent}
|
rootBrowseContent={rootBrowseContent}
|
||||||
|
showProfilesSection={showProfilesSection}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1279,6 +1538,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingVertical: theme.spacing[2],
|
paddingVertical: theme.spacing[2],
|
||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
},
|
},
|
||||||
|
modelRowHoverBoundary: {
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
browserModelRow: isWeb ? {} : { marginBottom: theme.spacing[1] },
|
browserModelRow: isWeb ? {} : { marginBottom: theme.spacing[1] },
|
||||||
browserRowHovered: {
|
browserRowHovered: {
|
||||||
backgroundColor: theme.colors.surface1,
|
backgroundColor: theme.colors.surface1,
|
||||||
@@ -1371,6 +1633,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
rowIconButtonPressed: {
|
rowIconButtonPressed: {
|
||||||
backgroundColor: theme.colors.surface1,
|
backgroundColor: theme.colors.surface1,
|
||||||
},
|
},
|
||||||
|
profileActionHidden: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
paddingVertical: theme.spacing[4],
|
paddingVertical: theme.spacing[4],
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -82,9 +82,12 @@ import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph";
|
|||||||
import { AgentControlTrigger } from "@/composer/agent-controls/control";
|
import { AgentControlTrigger } from "@/composer/agent-controls/control";
|
||||||
import { CompactModelSheet } from "@/composer/agent-controls/model-sheet";
|
import { CompactModelSheet } from "@/composer/agent-controls/model-sheet";
|
||||||
import {
|
import {
|
||||||
|
useAgentProfileEditor,
|
||||||
useAgentProfilePicker,
|
useAgentProfilePicker,
|
||||||
type AgentProfileApplyTarget,
|
type AgentProfileApplyTarget,
|
||||||
|
type AgentProfileEditorControls,
|
||||||
type AgentProfilePicker,
|
type AgentProfilePicker,
|
||||||
|
type AgentProfileSeed,
|
||||||
type DraftAgentProfileControls,
|
type DraftAgentProfileControls,
|
||||||
} from "@/agent-profiles";
|
} from "@/agent-profiles";
|
||||||
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
|
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
|
||||||
@@ -116,6 +119,8 @@ interface ControlledAgentControlsProps {
|
|||||||
agentProfiles?: AgentProfilePicker | null;
|
agentProfiles?: AgentProfilePicker | null;
|
||||||
onApplyAgentProfile?: (profileId: string) => void;
|
onApplyAgentProfile?: (profileId: string) => void;
|
||||||
onEditAgentProfiles?: () => void;
|
onEditAgentProfiles?: () => void;
|
||||||
|
onCreateAgentProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditAgentProfile?: (profileId: string) => void;
|
||||||
features?: AgentFeature[];
|
features?: AgentFeature[];
|
||||||
onSetFeature?: (featureId: string, value: unknown) => void;
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
||||||
onDropdownClose?: () => void;
|
onDropdownClose?: () => void;
|
||||||
@@ -281,6 +286,22 @@ function useEditAgentProfilesNavigation(
|
|||||||
return serverId && isSupported ? handleEdit : undefined;
|
return serverId && isSupported ? handleEdit : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAgentProfileEditorActions(
|
||||||
|
isSupported: boolean,
|
||||||
|
editor: AgentProfileEditorControls,
|
||||||
|
): {
|
||||||
|
create?: (seed: AgentProfileSeed) => void;
|
||||||
|
edit?: (profileId: string) => void;
|
||||||
|
} {
|
||||||
|
if (!isSupported) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
create: editor.openCreateFromModel,
|
||||||
|
edit: editor.openEdit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildFallbackModelSelectorProviders(
|
function buildFallbackModelSelectorProviders(
|
||||||
provider: string,
|
provider: string,
|
||||||
modelOptions: AgentControlOption[] | undefined,
|
modelOptions: AgentControlOption[] | undefined,
|
||||||
@@ -468,6 +489,8 @@ function ControlledAgentControls({
|
|||||||
agentProfiles = null,
|
agentProfiles = null,
|
||||||
onApplyAgentProfile,
|
onApplyAgentProfile,
|
||||||
onEditAgentProfiles,
|
onEditAgentProfiles,
|
||||||
|
onCreateAgentProfile,
|
||||||
|
onEditAgentProfile,
|
||||||
features,
|
features,
|
||||||
onSetFeature,
|
onSetFeature,
|
||||||
onDropdownClose,
|
onDropdownClose,
|
||||||
@@ -722,6 +745,8 @@ function ControlledAgentControls({
|
|||||||
onSetFeature={onSetFeature}
|
onSetFeature={onSetFeature}
|
||||||
onApplyAgentProfile={onApplyAgentProfile}
|
onApplyAgentProfile={onApplyAgentProfile}
|
||||||
onEditAgentProfiles={onEditAgentProfiles}
|
onEditAgentProfiles={onEditAgentProfiles}
|
||||||
|
onCreateAgentProfile={onCreateAgentProfile}
|
||||||
|
onEditAgentProfile={onEditAgentProfile}
|
||||||
onDropdownClose={onDropdownClose}
|
onDropdownClose={onDropdownClose}
|
||||||
onModelSelectorOpen={onModelSelectorOpen}
|
onModelSelectorOpen={onModelSelectorOpen}
|
||||||
onRetryModelProvider={onRetryModelProvider}
|
onRetryModelProvider={onRetryModelProvider}
|
||||||
@@ -769,6 +794,8 @@ function ControlledAgentControls({
|
|||||||
onSetFeature={onSetFeature}
|
onSetFeature={onSetFeature}
|
||||||
onApplyAgentProfile={onApplyAgentProfile}
|
onApplyAgentProfile={onApplyAgentProfile}
|
||||||
onEditAgentProfiles={onEditAgentProfiles}
|
onEditAgentProfiles={onEditAgentProfiles}
|
||||||
|
onCreateAgentProfile={onCreateAgentProfile}
|
||||||
|
onEditAgentProfile={onEditAgentProfile}
|
||||||
onDropdownClose={onDropdownClose}
|
onDropdownClose={onDropdownClose}
|
||||||
onModelSelectorOpen={onModelSelectorOpen}
|
onModelSelectorOpen={onModelSelectorOpen}
|
||||||
onRetryModelProvider={onRetryModelProvider}
|
onRetryModelProvider={onRetryModelProvider}
|
||||||
@@ -813,6 +840,8 @@ interface DesktopAgentControlsContentProps {
|
|||||||
onSetFeature?: (featureId: string, value: unknown) => void;
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
||||||
onApplyAgentProfile?: (profileId: string) => void;
|
onApplyAgentProfile?: (profileId: string) => void;
|
||||||
onEditAgentProfiles?: () => void;
|
onEditAgentProfiles?: () => void;
|
||||||
|
onCreateAgentProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditAgentProfile?: (profileId: string) => void;
|
||||||
onDropdownClose?: () => void;
|
onDropdownClose?: () => void;
|
||||||
onModelSelectorOpen?: () => void;
|
onModelSelectorOpen?: () => void;
|
||||||
onRetryModelProvider?: (provider: AgentProvider) => void;
|
onRetryModelProvider?: (provider: AgentProvider) => void;
|
||||||
@@ -873,6 +902,8 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
|
|||||||
onSetFeature,
|
onSetFeature,
|
||||||
onApplyAgentProfile,
|
onApplyAgentProfile,
|
||||||
onEditAgentProfiles,
|
onEditAgentProfiles,
|
||||||
|
onCreateAgentProfile,
|
||||||
|
onEditAgentProfile,
|
||||||
onDropdownClose,
|
onDropdownClose,
|
||||||
onModelSelectorOpen,
|
onModelSelectorOpen,
|
||||||
onRetryModelProvider,
|
onRetryModelProvider,
|
||||||
@@ -961,6 +992,8 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
|
|||||||
profiles={agentProfiles}
|
profiles={agentProfiles}
|
||||||
onApplyProfile={onApplyAgentProfile}
|
onApplyProfile={onApplyAgentProfile}
|
||||||
onEditProfiles={onEditAgentProfiles}
|
onEditProfiles={onEditAgentProfiles}
|
||||||
|
onCreateProfile={onCreateAgentProfile}
|
||||||
|
onEditProfile={onEditAgentProfile}
|
||||||
isLoading={isModelLoading}
|
isLoading={isModelLoading}
|
||||||
disabled={modelDisabled}
|
disabled={modelDisabled}
|
||||||
onOpen={onModelSelectorOpen}
|
onOpen={onModelSelectorOpen}
|
||||||
@@ -1079,6 +1112,8 @@ interface SheetAgentControlsContentProps {
|
|||||||
onSetFeature?: (featureId: string, value: unknown) => void;
|
onSetFeature?: (featureId: string, value: unknown) => void;
|
||||||
onApplyAgentProfile?: (profileId: string) => void;
|
onApplyAgentProfile?: (profileId: string) => void;
|
||||||
onEditAgentProfiles?: () => void;
|
onEditAgentProfiles?: () => void;
|
||||||
|
onCreateAgentProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditAgentProfile?: (profileId: string) => void;
|
||||||
onDropdownClose?: () => void;
|
onDropdownClose?: () => void;
|
||||||
onModelSelectorOpen?: () => void;
|
onModelSelectorOpen?: () => void;
|
||||||
onRetryModelProvider?: (provider: AgentProvider) => void;
|
onRetryModelProvider?: (provider: AgentProvider) => void;
|
||||||
@@ -1121,6 +1156,8 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
|
|||||||
onSetFeature,
|
onSetFeature,
|
||||||
onApplyAgentProfile,
|
onApplyAgentProfile,
|
||||||
onEditAgentProfiles,
|
onEditAgentProfiles,
|
||||||
|
onCreateAgentProfile,
|
||||||
|
onEditAgentProfile,
|
||||||
onDropdownClose,
|
onDropdownClose,
|
||||||
onModelSelectorOpen,
|
onModelSelectorOpen,
|
||||||
onRetryModelProvider,
|
onRetryModelProvider,
|
||||||
@@ -1222,6 +1259,8 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
|
|||||||
profiles={agentProfiles}
|
profiles={agentProfiles}
|
||||||
onApplyProfile={onApplyAgentProfile}
|
onApplyProfile={onApplyAgentProfile}
|
||||||
onEditProfiles={onEditAgentProfiles}
|
onEditProfiles={onEditAgentProfiles}
|
||||||
|
onCreateProfile={onCreateAgentProfile}
|
||||||
|
onEditProfile={onEditAgentProfile}
|
||||||
isLoading={isModelLoading}
|
isLoading={isModelLoading}
|
||||||
disabled={modelDisabled}
|
disabled={modelDisabled}
|
||||||
onOpen={onModelSelectorOpen}
|
onOpen={onModelSelectorOpen}
|
||||||
@@ -1610,6 +1649,8 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
target: profileTarget,
|
target: profileTarget,
|
||||||
});
|
});
|
||||||
const handleEditAgentProfiles = useEditAgentProfilesNavigation(serverId, agentProfiles !== null);
|
const handleEditAgentProfiles = useEditAgentProfilesNavigation(serverId, agentProfiles !== null);
|
||||||
|
const profileEditor = useAgentProfileEditor(serverId);
|
||||||
|
const profileActions = resolveAgentProfileEditorActions(agentProfiles !== null, profileEditor);
|
||||||
|
|
||||||
const handleSelectThinkingOption = useCallback(
|
const handleSelectThinkingOption = useCallback(
|
||||||
(thinkingOptionId: string) => {
|
(thinkingOptionId: string) => {
|
||||||
@@ -1735,6 +1776,7 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{commandCenterRegistration}
|
{commandCenterRegistration}
|
||||||
|
{profileEditor.element}
|
||||||
<ControlledAgentControls
|
<ControlledAgentControls
|
||||||
provider={agent.provider}
|
provider={agent.provider}
|
||||||
modelSelectorProviders={agentModelSelectorProviders}
|
modelSelectorProviders={agentModelSelectorProviders}
|
||||||
@@ -1744,6 +1786,8 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
agentProfiles={agentProfiles}
|
agentProfiles={agentProfiles}
|
||||||
onApplyAgentProfile={agentProfiles?.applyProfile}
|
onApplyAgentProfile={agentProfiles?.applyProfile}
|
||||||
onEditAgentProfiles={handleEditAgentProfiles}
|
onEditAgentProfiles={handleEditAgentProfiles}
|
||||||
|
onCreateAgentProfile={profileActions.create}
|
||||||
|
onEditAgentProfile={profileActions.edit}
|
||||||
thinkingOptions={thinkingOptions.length > 1 ? thinkingOptions : undefined}
|
thinkingOptions={thinkingOptions.length > 1 ? thinkingOptions : undefined}
|
||||||
selectedThinkingOptionId={modelSelection.selectedThinkingId ?? undefined}
|
selectedThinkingOptionId={modelSelection.selectedThinkingId ?? undefined}
|
||||||
onSelectThinkingOption={handleSelectThinkingOption}
|
onSelectThinkingOption={handleSelectThinkingOption}
|
||||||
@@ -1830,6 +1874,8 @@ export function DraftAgentControls({
|
|||||||
modelSelectorServerId,
|
modelSelectorServerId,
|
||||||
agentProfiles !== null,
|
agentProfiles !== null,
|
||||||
);
|
);
|
||||||
|
const profileEditor = useAgentProfileEditor(modelSelectorServerId);
|
||||||
|
const profileActions = resolveAgentProfileEditorActions(agentProfiles !== null, profileEditor);
|
||||||
|
|
||||||
const modeControl = useMemo<AgentModeControlValue | null>(
|
const modeControl = useMemo<AgentModeControlValue | null>(
|
||||||
() =>
|
() =>
|
||||||
@@ -1847,31 +1893,36 @@ export function DraftAgentControls({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlledAgentControls
|
<>
|
||||||
provider={selectedProvider ?? ""}
|
{profileEditor.element}
|
||||||
modelSelectorProviders={modelSelectorProviders}
|
<ControlledAgentControls
|
||||||
modelOptions={modelOptions}
|
provider={selectedProvider ?? ""}
|
||||||
selectedModelId={selectedModel}
|
modelSelectorProviders={modelSelectorProviders}
|
||||||
onSelectModel={onSelectModel}
|
modelOptions={modelOptions}
|
||||||
onSelectProviderAndModel={onSelectProviderAndModel}
|
selectedModelId={selectedModel}
|
||||||
isModelLoading={isAllModelsLoading}
|
onSelectModel={onSelectModel}
|
||||||
agentProfiles={agentProfiles}
|
onSelectProviderAndModel={onSelectProviderAndModel}
|
||||||
onApplyAgentProfile={agentProfiles?.applyProfile}
|
isModelLoading={isAllModelsLoading}
|
||||||
onEditAgentProfiles={handleEditAgentProfiles}
|
agentProfiles={agentProfiles}
|
||||||
thinkingOptions={mappedThinkingOptions.length > 0 ? mappedThinkingOptions : undefined}
|
onApplyAgentProfile={agentProfiles?.applyProfile}
|
||||||
selectedThinkingOptionId={effectiveSelectedThinkingOption}
|
onEditAgentProfiles={handleEditAgentProfiles}
|
||||||
onSelectThinkingOption={onSelectThinkingOption}
|
onCreateAgentProfile={profileActions.create}
|
||||||
features={features}
|
onEditAgentProfile={profileActions.edit}
|
||||||
onSetFeature={onSetFeature}
|
thinkingOptions={mappedThinkingOptions.length > 0 ? mappedThinkingOptions : undefined}
|
||||||
onDropdownClose={onDropdownClose}
|
selectedThinkingOptionId={effectiveSelectedThinkingOption}
|
||||||
onModelSelectorOpen={onModelSelectorOpen}
|
onSelectThinkingOption={onSelectThinkingOption}
|
||||||
onRetryModelProvider={onRetryModelProvider}
|
features={features}
|
||||||
isRetryingModelProvider={isRetryingModelProvider}
|
onSetFeature={onSetFeature}
|
||||||
disabled={disabled}
|
onDropdownClose={onDropdownClose}
|
||||||
modeControl={modeControl}
|
onModelSelectorOpen={onModelSelectorOpen}
|
||||||
modelSelectorServerId={modelSelectorServerId}
|
onRetryModelProvider={onRetryModelProvider}
|
||||||
isCompactLayout={isCompactLayout}
|
isRetryingModelProvider={isRetryingModelProvider}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
modeControl={modeControl}
|
||||||
|
modelSelectorServerId={modelSelectorServerId}
|
||||||
|
isCompactLayout={isCompactLayout}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Keyboard, ScrollView, Text, View, type PressableStateCallbackType } fro
|
|||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { Bot } from "lucide-react-native";
|
import { Bot } from "lucide-react-native";
|
||||||
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
||||||
import type { AgentProfilePicker } from "@/agent-profiles";
|
import type { AgentProfilePicker, AgentProfileSeed } from "@/agent-profiles";
|
||||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||||
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
@@ -36,6 +36,8 @@ interface CompactModelSheetProps {
|
|||||||
profiles?: AgentProfilePicker | null;
|
profiles?: AgentProfilePicker | null;
|
||||||
onApplyProfile?: (profileId: string) => void;
|
onApplyProfile?: (profileId: string) => void;
|
||||||
onEditProfiles?: () => void;
|
onEditProfiles?: () => void;
|
||||||
|
onCreateProfile?: (seed: AgentProfileSeed) => void;
|
||||||
|
onEditProfile?: (profileId: string) => void;
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onRetryProvider?: (provider: AgentProvider) => void;
|
onRetryProvider?: (provider: AgentProvider) => void;
|
||||||
@@ -52,6 +54,30 @@ function shortModelLabel(label: string): string {
|
|||||||
return separatorIndex === -1 ? label : label.slice(separatorIndex + 1);
|
return separatorIndex === -1 ? label : label.slice(separatorIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveModelSheetProfileActions(
|
||||||
|
onCreateProfile: ((seed: AgentProfileSeed) => void) | undefined,
|
||||||
|
onEditProfile: ((profileId: string) => void) | undefined,
|
||||||
|
close: () => void,
|
||||||
|
): {
|
||||||
|
create?: (seed: AgentProfileSeed) => void;
|
||||||
|
edit?: (profileId: string) => void;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
create: onCreateProfile
|
||||||
|
? (seed: AgentProfileSeed) => {
|
||||||
|
close();
|
||||||
|
onCreateProfile(seed);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
edit: onEditProfile
|
||||||
|
? (profileId: string) => {
|
||||||
|
close();
|
||||||
|
onEditProfile(profileId);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function CompactModelSheet({
|
export function CompactModelSheet({
|
||||||
providers,
|
providers,
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
@@ -62,6 +88,8 @@ export function CompactModelSheet({
|
|||||||
profiles = null,
|
profiles = null,
|
||||||
onApplyProfile,
|
onApplyProfile,
|
||||||
onEditProfiles,
|
onEditProfiles,
|
||||||
|
onCreateProfile,
|
||||||
|
onEditProfile,
|
||||||
onOpen,
|
onOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onRetryProvider,
|
onRetryProvider,
|
||||||
@@ -98,6 +126,7 @@ export function CompactModelSheet({
|
|||||||
selectedModel,
|
selectedModel,
|
||||||
isLoading,
|
isLoading,
|
||||||
autoFocusSearch: isWeb && !usesBottomSheet,
|
autoFocusSearch: isWeb && !usesBottomSheet,
|
||||||
|
profiles,
|
||||||
serverId,
|
serverId,
|
||||||
});
|
});
|
||||||
const ProviderIcon =
|
const ProviderIcon =
|
||||||
@@ -191,6 +220,8 @@ export function CompactModelSheet({
|
|||||||
onEditProfiles?.();
|
onEditProfiles?.();
|
||||||
}, [close, onEditProfiles]);
|
}, [close, onEditProfiles]);
|
||||||
|
|
||||||
|
const profileActions = resolveModelSheetProfileActions(onCreateProfile, onEditProfile, close);
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
close();
|
close();
|
||||||
@@ -284,6 +315,8 @@ export function CompactModelSheet({
|
|||||||
onSelect={usesBottomSheet ? handleSearchSelect : handleDesktopSelect}
|
onSelect={usesBottomSheet ? handleSearchSelect : handleDesktopSelect}
|
||||||
onApplyProfile={handleApplyProfile}
|
onApplyProfile={handleApplyProfile}
|
||||||
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
|
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
|
||||||
|
onCreateProfile={profileActions.create}
|
||||||
|
onEditProfile={profileActions.edit}
|
||||||
onRetryProvider={onRetryProvider}
|
onRetryProvider={onRetryProvider}
|
||||||
isRetryingProvider={isRetryingProvider}
|
isRetryingProvider={isRetryingProvider}
|
||||||
scrolling={modelBrowserScrolling}
|
scrolling={modelBrowserScrolling}
|
||||||
@@ -326,10 +359,14 @@ export function CompactModelSheet({
|
|||||||
<ModelBrowser
|
<ModelBrowser
|
||||||
state={modelBrowser}
|
state={modelBrowser}
|
||||||
onSelect={handleBrowserSelect}
|
onSelect={handleBrowserSelect}
|
||||||
|
onEditProfiles={onEditProfiles ? handleEditProfiles : undefined}
|
||||||
|
onCreateProfile={profileActions.create}
|
||||||
|
onEditProfile={profileActions.edit}
|
||||||
onRetryProvider={onRetryProvider}
|
onRetryProvider={onRetryProvider}
|
||||||
isRetryingProvider={isRetryingProvider}
|
isRetryingProvider={isRetryingProvider}
|
||||||
scrolling={modelBrowserScrolling}
|
scrolling={modelBrowserScrolling}
|
||||||
searchAllOnFocus
|
searchAllOnFocus
|
||||||
|
showProfilesSection={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</AdaptiveModalSheet>
|
</AdaptiveModalSheet>
|
||||||
|
|||||||
@@ -1463,6 +1463,9 @@ export const ar: TranslationResources = {
|
|||||||
editProfiles: "تحرير",
|
editProfiles: "تحرير",
|
||||||
editProfilesLabel: "تحرير ملفات الوكيل",
|
editProfilesLabel: "تحرير ملفات الوكيل",
|
||||||
createProfile: "إنشاء ملف شخصي",
|
createProfile: "إنشاء ملف شخصي",
|
||||||
|
createProfileFromModel: "إنشاء ملف شخصي من هذا النموذج",
|
||||||
|
editProfileLabel: "تحرير {{name}}",
|
||||||
|
editProfilesCount: "تحرير الملفات الشخصية ({{count}})",
|
||||||
modelCount: "{{count}} نموذج",
|
modelCount: "{{count}} نموذج",
|
||||||
modelCountPlural: "{{count}} نماذج",
|
modelCountPlural: "{{count}} نماذج",
|
||||||
retry: "أعد المحاولة",
|
retry: "أعد المحاولة",
|
||||||
|
|||||||
@@ -1473,6 +1473,9 @@ export const en = {
|
|||||||
editProfiles: "Edit",
|
editProfiles: "Edit",
|
||||||
editProfilesLabel: "Edit agent profiles",
|
editProfilesLabel: "Edit agent profiles",
|
||||||
createProfile: "Create profile",
|
createProfile: "Create profile",
|
||||||
|
createProfileFromModel: "Create profile from this model",
|
||||||
|
editProfileLabel: "Edit {{name}}",
|
||||||
|
editProfilesCount: "Edit profiles ({{count}})",
|
||||||
modelCount: "{{count}} model",
|
modelCount: "{{count}} model",
|
||||||
modelCountPlural: "{{count}} models",
|
modelCountPlural: "{{count}} models",
|
||||||
retry: "Retry",
|
retry: "Retry",
|
||||||
|
|||||||
@@ -1507,6 +1507,9 @@ export const es: TranslationResources = {
|
|||||||
editProfiles: "Editar",
|
editProfiles: "Editar",
|
||||||
editProfilesLabel: "Editar perfiles de agente",
|
editProfilesLabel: "Editar perfiles de agente",
|
||||||
createProfile: "Crear perfil",
|
createProfile: "Crear perfil",
|
||||||
|
createProfileFromModel: "Crear perfil desde este modelo",
|
||||||
|
editProfileLabel: "Editar {{name}}",
|
||||||
|
editProfilesCount: "Editar perfiles ({{count}})",
|
||||||
modelCount: "{{count}} modelo",
|
modelCount: "{{count}} modelo",
|
||||||
modelCountPlural: "{{count}} modelos",
|
modelCountPlural: "{{count}} modelos",
|
||||||
retry: "Rever",
|
retry: "Rever",
|
||||||
|
|||||||
@@ -1511,6 +1511,9 @@ export const fr: TranslationResources = {
|
|||||||
editProfiles: "Modifier",
|
editProfiles: "Modifier",
|
||||||
editProfilesLabel: "Modifier les profils d'agent",
|
editProfilesLabel: "Modifier les profils d'agent",
|
||||||
createProfile: "Créer un profil",
|
createProfile: "Créer un profil",
|
||||||
|
createProfileFromModel: "Créer un profil à partir de ce modèle",
|
||||||
|
editProfileLabel: "Modifier {{name}}",
|
||||||
|
editProfilesCount: "Modifier les profils ({{count}})",
|
||||||
modelCount: "{{count}} modèle",
|
modelCount: "{{count}} modèle",
|
||||||
modelCountPlural: "{{count}} modèles",
|
modelCountPlural: "{{count}} modèles",
|
||||||
retry: "Réessayer",
|
retry: "Réessayer",
|
||||||
|
|||||||
@@ -1478,6 +1478,9 @@ export const ja: TranslationResources = {
|
|||||||
editProfiles: "編集",
|
editProfiles: "編集",
|
||||||
editProfilesLabel: "エージェントプロファイルを編集",
|
editProfilesLabel: "エージェントプロファイルを編集",
|
||||||
createProfile: "プロファイルを作成",
|
createProfile: "プロファイルを作成",
|
||||||
|
createProfileFromModel: "このモデルからプロファイルを作成",
|
||||||
|
editProfileLabel: "{{name}}を編集",
|
||||||
|
editProfilesCount: "プロファイルを編集({{count}})",
|
||||||
modelCount: "{{count}}つのモデル",
|
modelCount: "{{count}}つのモデル",
|
||||||
modelCountPlural: "{{count}}つのモデル",
|
modelCountPlural: "{{count}}つのモデル",
|
||||||
retry: "再試行",
|
retry: "再試行",
|
||||||
|
|||||||
@@ -1473,6 +1473,9 @@ export const ko: TranslationResources = {
|
|||||||
editProfiles: "편집",
|
editProfiles: "편집",
|
||||||
editProfilesLabel: "에이전트 프로필 편집",
|
editProfilesLabel: "에이전트 프로필 편집",
|
||||||
createProfile: "프로필 만들기",
|
createProfile: "프로필 만들기",
|
||||||
|
createProfileFromModel: "이 모델에서 프로필 만들기",
|
||||||
|
editProfileLabel: "{{name}} 편집",
|
||||||
|
editProfilesCount: "프로필 편집 ({{count}})",
|
||||||
modelCount: "모델 {{count}}개",
|
modelCount: "모델 {{count}}개",
|
||||||
modelCountPlural: "모델 {{count}}개",
|
modelCountPlural: "모델 {{count}}개",
|
||||||
retry: "다시 시도",
|
retry: "다시 시도",
|
||||||
|
|||||||
@@ -1493,6 +1493,9 @@ export const ptBR: TranslationResources = {
|
|||||||
editProfiles: "Editar",
|
editProfiles: "Editar",
|
||||||
editProfilesLabel: "Editar perfis de agente",
|
editProfilesLabel: "Editar perfis de agente",
|
||||||
createProfile: "Criar perfil",
|
createProfile: "Criar perfil",
|
||||||
|
createProfileFromModel: "Criar perfil a partir deste modelo",
|
||||||
|
editProfileLabel: "Editar {{name}}",
|
||||||
|
editProfilesCount: "Editar perfis ({{count}})",
|
||||||
modelCount: "{{count}} modelo",
|
modelCount: "{{count}} modelo",
|
||||||
modelCountPlural: "{{count}} modelos",
|
modelCountPlural: "{{count}} modelos",
|
||||||
retry: "Tentar novamente",
|
retry: "Tentar novamente",
|
||||||
|
|||||||
@@ -1496,6 +1496,9 @@ export const ru: TranslationResources = {
|
|||||||
editProfiles: "Изменить",
|
editProfiles: "Изменить",
|
||||||
editProfilesLabel: "Изменить профили агентов",
|
editProfilesLabel: "Изменить профили агентов",
|
||||||
createProfile: "Создать профиль",
|
createProfile: "Создать профиль",
|
||||||
|
createProfileFromModel: "Создать профиль из этой модели",
|
||||||
|
editProfileLabel: "Изменить {{name}}",
|
||||||
|
editProfilesCount: "Изменить профили ({{count}})",
|
||||||
modelCount: "{{count}} модель",
|
modelCount: "{{count}} модель",
|
||||||
modelCountPlural: "{{count}} моделей",
|
modelCountPlural: "{{count}} моделей",
|
||||||
retry: "Повторить попытку",
|
retry: "Повторить попытку",
|
||||||
|
|||||||
@@ -1446,6 +1446,9 @@ export const zhCN: TranslationResources = {
|
|||||||
editProfiles: "编辑",
|
editProfiles: "编辑",
|
||||||
editProfilesLabel: "编辑智能体配置文件",
|
editProfilesLabel: "编辑智能体配置文件",
|
||||||
createProfile: "创建配置文件",
|
createProfile: "创建配置文件",
|
||||||
|
createProfileFromModel: "从此模型创建配置文件",
|
||||||
|
editProfileLabel: "编辑 {{name}}",
|
||||||
|
editProfilesCount: "编辑配置文件 ({{count}})",
|
||||||
modelCount: "{{count}} 个模型",
|
modelCount: "{{count}} 个模型",
|
||||||
modelCountPlural: "{{count}} 个模型",
|
modelCountPlural: "{{count}} 个模型",
|
||||||
retry: "重试",
|
retry: "重试",
|
||||||
|
|||||||
Reference in New Issue
Block a user