Streamline model selector provider data

This commit is contained in:
Mohamed Boudra
2026-05-19 20:01:56 +07:00
parent bc7798af28
commit 5dd6b030f2
8 changed files with 298 additions and 364 deletions
@@ -32,6 +32,10 @@ import {
} from "lucide-react-native"; } from "lucide-react-native";
import { getProviderIcon } from "@/components/provider-icons"; import { getProviderIcon } from "@/components/provider-icons";
import { CombinedModelSelector } from "@/components/combined-model-selector"; import { CombinedModelSelector } from "@/components/combined-model-selector";
import {
buildModelSelectorProviders,
type ModelSelectorProvider,
} from "@/components/combined-model-selector.utils";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { resolveProviderDefinition } from "@/utils/provider-definitions"; import { resolveProviderDefinition } from "@/utils/provider-definitions";
@@ -93,8 +97,7 @@ interface ControlledAgentStatusBarProps {
disabled?: boolean; disabled?: boolean;
isModelLoading?: boolean; isModelLoading?: boolean;
providerDefinitions: AgentProviderDefinition[]; providerDefinitions: AgentProviderDefinition[];
allProviderModels?: Map<string, AgentModelDefinition[]>; modelSelectorProviders?: ModelSelectorProvider[];
canSelectModelProvider?: (providerId: string) => boolean;
favoriteKeys?: Set<string>; favoriteKeys?: Set<string>;
onToggleFavoriteModel?: (provider: string, modelId: string) => void; onToggleFavoriteModel?: (provider: string, modelId: string) => void;
features?: AgentFeature[]; features?: AgentFeature[];
@@ -114,7 +117,7 @@ export interface DraftAgentStatusBarProps {
selectedModel: string; selectedModel: string;
onSelectModel: (modelId: string) => void; onSelectModel: (modelId: string) => void;
isModelLoading: boolean; isModelLoading: boolean;
allProviderModels: Map<string, AgentModelDefinition[]>; modelSelectorProviders: ModelSelectorProvider[];
isAllModelsLoading: boolean; isAllModelsLoading: boolean;
onSelectProviderAndModel: (provider: AgentProvider, modelId: string) => void; onSelectProviderAndModel: (provider: AgentProvider, modelId: string) => void;
thinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>; thinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
@@ -184,10 +187,6 @@ const MODE_ICONS = {
ShieldQuestionMark, ShieldQuestionMark,
} as const; } as const;
function alwaysTrue() {
return true;
}
function resolveDisplayModel( function resolveDisplayModel(
isModelLoading: boolean, isModelLoading: boolean,
modelOptions: StatusOption[] | undefined, modelOptions: StatusOption[] | undefined,
@@ -235,23 +234,26 @@ function toComboboxOptions(options: StatusOption[] | undefined): ComboboxOption[
return (options ?? []).map((o) => ({ id: o.id, label: o.label })); return (options ?? []).map((o) => ({ id: o.id, label: o.label }));
} }
function buildFallbackAllProviderModels( function buildFallbackModelSelectorProviders(
provider: string, provider: string,
modelOptions: StatusOption[] | undefined, modelOptions: StatusOption[] | undefined,
): Map<string, AgentModelDefinition[]> { ): ModelSelectorProvider[] {
const map = new Map<string, AgentModelDefinition[]>();
if (!modelOptions || modelOptions.length === 0) { if (!modelOptions || modelOptions.length === 0) {
return map; return [];
} }
map.set( return [
provider, {
modelOptions.map((option) => ({ id: provider,
provider: provider, label: provider,
id: option.id, rows: modelOptions.map((option) => ({
label: option.label, favoriteKey: buildFavoriteModelKey({ provider, modelId: option.id }),
})), provider,
); providerLabel: provider,
return map; modelId: option.id,
modelLabel: option.label,
})),
},
];
} }
function makeBadgePressableStyle( function makeBadgePressableStyle(
@@ -459,8 +461,7 @@ function ControlledStatusBar({
disabled = false, disabled = false,
isModelLoading = false, isModelLoading = false,
providerDefinitions, providerDefinitions,
allProviderModels, modelSelectorProviders,
canSelectModelProvider,
favoriteKeys = new Set<string>(), favoriteKeys = new Set<string>(),
onToggleFavoriteModel, onToggleFavoriteModel,
features, features,
@@ -521,13 +522,11 @@ function ControlledStatusBar({
() => toComboboxOptions(modeOptions), () => toComboboxOptions(modeOptions),
[modeOptions], [modeOptions],
); );
const fallbackAllProviderModels = useMemo( const fallbackModelSelectorProviders = useMemo(
() => buildFallbackAllProviderModels(provider, modelOptions), () => buildFallbackModelSelectorProviders(provider, modelOptions),
[modelOptions, provider], [modelOptions, provider],
); );
const effectiveProviderDefinitions = providerDefinitions; const effectiveModelSelectorProviders = modelSelectorProviders ?? fallbackModelSelectorProviders;
const effectiveAllProviderModels = allProviderModels ?? fallbackAllProviderModels;
const canSelectProviderInModelMenu = canSelectModelProvider ?? alwaysTrue;
const comboboxThinkingOptions = useMemo<ComboboxOption[]>( const comboboxThinkingOptions = useMemo<ComboboxOption[]>(
() => toComboboxOptions(thinkingOptions), () => toComboboxOptions(thinkingOptions),
[thinkingOptions], [thinkingOptions],
@@ -701,10 +700,8 @@ function ControlledStatusBar({
canSelectMode={canSelectMode} canSelectMode={canSelectMode}
canSelectModel={canSelectModel} canSelectModel={canSelectModel}
canSelectThinking={canSelectThinking} canSelectThinking={canSelectThinking}
canSelectProviderInModelMenu={canSelectProviderInModelMenu} modelSelectorProviders={effectiveModelSelectorProviders}
modelDisabled={modelDisabled} modelDisabled={modelDisabled}
effectiveProviderDefinitions={effectiveProviderDefinitions}
effectiveAllProviderModels={effectiveAllProviderModels}
comboboxProviderOptions={comboboxProviderOptions} comboboxProviderOptions={comboboxProviderOptions}
comboboxModeOptions={comboboxModeOptions} comboboxModeOptions={comboboxModeOptions}
comboboxThinkingOptions={comboboxThinkingOptions} comboboxThinkingOptions={comboboxThinkingOptions}
@@ -751,10 +748,8 @@ function ControlledStatusBar({
canSelectMode={canSelectMode} canSelectMode={canSelectMode}
canSelectModel={canSelectModel} canSelectModel={canSelectModel}
canSelectThinking={canSelectThinking} canSelectThinking={canSelectThinking}
canSelectProviderInModelMenu={canSelectProviderInModelMenu} modelSelectorProviders={effectiveModelSelectorProviders}
modelDisabled={modelDisabled} modelDisabled={modelDisabled}
effectiveProviderDefinitions={effectiveProviderDefinitions}
effectiveAllProviderModels={effectiveAllProviderModels}
comboboxModeOptions={comboboxModeOptions} comboboxModeOptions={comboboxModeOptions}
comboboxThinkingOptions={comboboxThinkingOptions} comboboxThinkingOptions={comboboxThinkingOptions}
ModeIconComponent={ModeIconComponent} ModeIconComponent={ModeIconComponent}
@@ -799,10 +794,8 @@ interface DesktopStatusBarContentProps {
canSelectMode: boolean; canSelectMode: boolean;
canSelectModel: boolean; canSelectModel: boolean;
canSelectThinking: boolean; canSelectThinking: boolean;
canSelectProviderInModelMenu: (providerId: string) => boolean; modelSelectorProviders: ModelSelectorProvider[];
modelDisabled: boolean; modelDisabled: boolean;
effectiveProviderDefinitions: AgentProviderDefinition[];
effectiveAllProviderModels: Map<string, AgentModelDefinition[]>;
comboboxProviderOptions: ComboboxOption[]; comboboxProviderOptions: ComboboxOption[];
comboboxModeOptions: ComboboxOption[]; comboboxModeOptions: ComboboxOption[];
comboboxThinkingOptions: ComboboxOption[]; comboboxThinkingOptions: ComboboxOption[];
@@ -868,10 +861,8 @@ function DesktopStatusBarContent(props: DesktopStatusBarContentProps) {
canSelectMode, canSelectMode,
canSelectModel, canSelectModel,
canSelectThinking, canSelectThinking,
canSelectProviderInModelMenu, modelSelectorProviders,
modelDisabled, modelDisabled,
effectiveProviderDefinitions,
effectiveAllProviderModels,
comboboxProviderOptions, comboboxProviderOptions,
comboboxModeOptions, comboboxModeOptions,
comboboxThinkingOptions, comboboxThinkingOptions,
@@ -942,11 +933,9 @@ function DesktopStatusBarContent(props: DesktopStatusBarContentProps) {
<TooltipTrigger asChild triggerRefProp="ref"> <TooltipTrigger asChild triggerRefProp="ref">
<View> <View>
<CombinedModelSelector <CombinedModelSelector
providerDefinitions={effectiveProviderDefinitions} providers={modelSelectorProviders}
allProviderModels={effectiveAllProviderModels}
selectedProvider={provider} selectedProvider={provider}
selectedModel={selectedModelId ?? ""} selectedModel={selectedModelId ?? ""}
canSelectProvider={canSelectProviderInModelMenu}
onSelect={handleDesktopModelSelect} onSelect={handleDesktopModelSelect}
favoriteKeys={favoriteKeys} favoriteKeys={favoriteKeys}
onToggleFavorite={onToggleFavoriteModel} onToggleFavorite={onToggleFavoriteModel}
@@ -1069,10 +1058,8 @@ interface SheetStatusBarContentProps {
canSelectMode: boolean; canSelectMode: boolean;
canSelectModel: boolean; canSelectModel: boolean;
canSelectThinking: boolean; canSelectThinking: boolean;
canSelectProviderInModelMenu: (providerId: string) => boolean; modelSelectorProviders: ModelSelectorProvider[];
modelDisabled: boolean; modelDisabled: boolean;
effectiveProviderDefinitions: AgentProviderDefinition[];
effectiveAllProviderModels: Map<string, AgentModelDefinition[]>;
comboboxModeOptions: ComboboxOption[]; comboboxModeOptions: ComboboxOption[];
comboboxThinkingOptions: ComboboxOption[]; comboboxThinkingOptions: ComboboxOption[];
ModeIconComponent: (typeof MODE_ICONS)[keyof typeof MODE_ICONS] | null; ModeIconComponent: (typeof MODE_ICONS)[keyof typeof MODE_ICONS] | null;
@@ -1118,10 +1105,8 @@ function SheetStatusBarContent(props: SheetStatusBarContentProps) {
canSelectMode, canSelectMode,
canSelectModel, canSelectModel,
canSelectThinking, canSelectThinking,
canSelectProviderInModelMenu, modelSelectorProviders,
modelDisabled, modelDisabled,
effectiveProviderDefinitions,
effectiveAllProviderModels,
comboboxModeOptions, comboboxModeOptions,
comboboxThinkingOptions, comboboxThinkingOptions,
ModeIconComponent, ModeIconComponent,
@@ -1214,11 +1199,9 @@ function SheetStatusBarContent(props: SheetStatusBarContentProps) {
<> <>
{canSelectModel ? ( {canSelectModel ? (
<CombinedModelSelector <CombinedModelSelector
providerDefinitions={effectiveProviderDefinitions} providers={modelSelectorProviders}
allProviderModels={effectiveAllProviderModels}
selectedProvider={provider} selectedProvider={provider}
selectedModel={selectedModelId ?? ""} selectedModel={selectedModelId ?? ""}
canSelectProvider={canSelectProviderInModelMenu}
onSelect={handleSheetModelSelect} onSelect={handleSheetModelSelect}
favoriteKeys={favoriteKeys} favoriteKeys={favoriteKeys}
onToggleFavorite={onToggleFavoriteModel} onToggleFavorite={onToggleFavoriteModel}
@@ -1678,6 +1661,10 @@ export const AgentStatusBar = memo(function AgentStatusBar({
() => buildAgentProviderModels(agent?.provider, models), () => buildAgentProviderModels(agent?.provider, models),
[agent?.provider, models], [agent?.provider, models],
); );
const agentModelSelectorProviders = useMemo(
() => buildModelSelectorProviders(agentProviderDefinitions, agentProviderModels),
[agentProviderDefinitions, agentProviderModels],
);
const displayMode = resolveAgentDisplayMode(availableModes, agent?.currentModeId); const displayMode = resolveAgentDisplayMode(availableModes, agent?.currentModeId);
@@ -1841,7 +1828,7 @@ export const AgentStatusBar = memo(function AgentStatusBar({
modeOptions={fallbackModeOptions} modeOptions={fallbackModeOptions}
selectedModeId={agent.currentModeId ?? undefined} selectedModeId={agent.currentModeId ?? undefined}
providerDefinitions={agentProviderDefinitions} providerDefinitions={agentProviderDefinitions}
allProviderModels={agentProviderModels} modelSelectorProviders={agentModelSelectorProviders}
onSelectMode={handleSelectMode} onSelectMode={handleSelectMode}
modelOptions={modelOptions} modelOptions={modelOptions}
selectedModelId={modelSelection.activeModelId ?? undefined} selectedModelId={modelSelection.activeModelId ?? undefined}
@@ -1872,7 +1859,7 @@ export function DraftAgentStatusBar({
selectedModel, selectedModel,
onSelectModel, onSelectModel,
isModelLoading: _isModelLoading, isModelLoading: _isModelLoading,
allProviderModels, modelSelectorProviders,
isAllModelsLoading, isAllModelsLoading,
onSelectProviderAndModel, onSelectProviderAndModel,
thinkingOptions, thinkingOptions,
@@ -1937,8 +1924,7 @@ export function DraftAgentStatusBar({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<CombinedModelSelector <CombinedModelSelector
providerDefinitions={providerDefinitions} providers={modelSelectorProviders}
allProviderModels={allProviderModels}
selectedProvider={selectedProvider ?? ""} selectedProvider={selectedProvider ?? ""}
selectedModel={selectedModel} selectedModel={selectedModel}
onSelect={onSelectProviderAndModel} onSelect={onSelectProviderAndModel}
@@ -1973,7 +1959,7 @@ export function DraftAgentStatusBar({
<ControlledStatusBar <ControlledStatusBar
provider={selectedProvider ?? ""} provider={selectedProvider ?? ""}
providerDefinitions={providerDefinitions} providerDefinitions={providerDefinitions}
allProviderModels={allProviderModels} modelSelectorProviders={modelSelectorProviders}
modeOptions={hasSelectedProvider ? mappedModeOptions : undefined} modeOptions={hasSelectedProvider ? mappedModeOptions : undefined}
selectedModeId={effectiveSelectedMode} selectedModeId={effectiveSelectedMode}
onSelectMode={onSelectMode} onSelectMode={onSelectMode}
@@ -1,79 +1,136 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { AgentModelDefinition } from "@server/server/agent/agent-sdk-types"; import type {
AgentModelDefinition,
ProviderSnapshotEntry,
} from "@server/server/agent/agent-sdk-types";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
import { import {
buildModelRows, buildModelSelectorProviders,
buildProviderGroups, buildSelectableModelSelectorProviders,
buildSelectedTriggerLabel, buildSelectedTriggerLabel,
filterAndRankModelRows, filterAndRankModelRows,
matchesSearch, matchesSearch,
resolveProviderLabel,
} from "./combined-model-selector.utils"; } from "./combined-model-selector.utils";
describe("combined model selector helpers", () => { describe("combined model selector data", () => {
const providerDefinitions = [ const codexModel: AgentModelDefinition = {
{ provider: "codex",
id: "claude", id: "gpt-5.4",
label: "Claude", label: "GPT-5.4",
description: "Claude provider", };
defaultModeId: "default",
modes: [],
},
{
id: "codex",
label: "Codex",
description: "Codex provider",
defaultModeId: "auto",
modes: [],
},
{
id: "deepseek-tui",
label: "DeepSeek TUI",
description: "DeepSeek TUI provider",
defaultModeId: "default",
modes: [],
},
];
const claudeModels: AgentModelDefinition[] = [ function snapshotEntry(
{ overrides: Partial<ProviderSnapshotEntry> & Pick<ProviderSnapshotEntry, "provider">,
provider: "claude", ): ProviderSnapshotEntry {
id: "sonnet-4.6", return {
label: "Sonnet 4.6", ...overrides,
}, provider: overrides.provider,
]; status: overrides.status ?? "ready",
enabled: overrides.enabled ?? true,
label: overrides.label ?? overrides.provider,
description: overrides.description ?? `${overrides.provider} provider`,
defaultModeId: overrides.defaultModeId ?? "default",
modes: overrides.modes ?? [],
models: overrides.models ?? [codexModel],
};
}
const codexModels: AgentModelDefinition[] = [ it("builds selector providers from ready enabled snapshot entries", () => {
{ expect(
provider: "codex", buildSelectableModelSelectorProviders([
id: "gpt-5.4", snapshotEntry({
label: "GPT-5.4", provider: "codex",
}, label: "Codex",
]; models: [codexModel],
}),
it("keeps enough data to search by model and provider name", async () => {
const rows = buildModelRows(
providerDefinitions,
new Map([
["claude", claudeModels],
["codex", codexModels],
]), ]),
); ).toEqual([
{
expect(rows).toEqual([ id: "codex",
expect.objectContaining({ label: "Codex",
providerLabel: "Claude", rows: [
modelLabel: "Sonnet 4.6", {
modelId: "sonnet-4.6", favoriteKey: "codex:gpt-5.4",
}), provider: "codex",
expect.objectContaining({ providerLabel: "Codex",
providerLabel: "Codex", modelId: "gpt-5.4",
modelLabel: "GPT-5.4", modelLabel: "GPT-5.4",
modelId: "gpt-5.4", description: undefined,
}), isDefault: undefined,
},
],
},
]); ]);
});
expect(matchesSearch(rows[0], "claude")).toBe(true); it("keeps ready enabled providers with no models as model-less providers", () => {
expect(matchesSearch(rows[1], "gpt-5.4")).toBe(true); expect(
buildSelectableModelSelectorProviders([
snapshotEntry({
provider: "deepseek-tui",
label: "DeepSeek TUI",
models: [],
}),
]),
).toEqual([
{
id: "deepseek-tui",
label: "DeepSeek TUI",
rows: [],
},
]);
});
it("excludes disabled providers from selector data", () => {
expect(
buildSelectableModelSelectorProviders([
snapshotEntry({
provider: "deepseek-tui",
label: "DeepSeek TUI",
enabled: false,
models: [],
}),
]),
).toEqual([]);
});
it("excludes providers that are not ready", () => {
expect(
buildSelectableModelSelectorProviders([
snapshotEntry({ provider: "loading-provider", status: "loading", models: [] }),
snapshotEntry({ provider: "error-provider", status: "error", models: [] }),
snapshotEntry({ provider: "unavailable-provider", status: "unavailable", models: [] }),
]),
).toEqual([]);
});
it("builds selector providers from an already-curated provider list", () => {
const providerDefinitions: AgentProviderDefinition[] = [
{
id: "codex",
label: "Codex",
description: "Codex provider",
defaultModeId: "auto",
modes: [],
},
];
expect(
buildModelSelectorProviders(providerDefinitions, new Map([["codex", [codexModel]]])),
).toEqual([
{
id: "codex",
label: "Codex",
rows: [
expect.objectContaining({
provider: "codex",
providerLabel: "Codex",
modelId: "gpt-5.4",
modelLabel: "GPT-5.4",
}),
],
},
]);
}); });
it("matches across label, provider, and description with multi-token fuzzy search", () => { it("matches across label, provider, and description with multi-token fuzzy search", () => {
@@ -120,55 +177,7 @@ describe("combined model selector helpers", () => {
expect(filterAndRankModelRows(rows, "gpt54").map((row) => row.modelId)).toEqual(["gpt-5.4"]); expect(filterAndRankModelRows(rows, "gpt54").map((row) => row.modelId)).toEqual(["gpt-5.4"]);
}); });
it("includes providers that expose no models", () => {
const rows = buildModelRows(
providerDefinitions,
new Map([
["claude", claudeModels],
["deepseek-tui", []],
]),
);
const groups = buildProviderGroups(
providerDefinitions,
new Map([
["claude", claudeModels],
["deepseek-tui", []],
]),
rows,
"",
);
expect(groups).toEqual([
expect.objectContaining({
providerId: "claude",
hasNoModels: false,
}),
expect.objectContaining({
providerId: "deepseek-tui",
providerLabel: "DeepSeek TUI",
rows: [],
hasNoModels: true,
}),
]);
});
it("matches model-less providers by provider name", () => {
const groups = buildProviderGroups(
providerDefinitions,
new Map([
["claude", claudeModels],
["deepseek-tui", []],
]),
[],
"deepseek",
);
expect(groups.map((group) => group.providerId)).toEqual(["deepseek-tui"]);
});
it("keeps the selected trigger label model-only", () => { it("keeps the selected trigger label model-only", () => {
expect(resolveProviderLabel(providerDefinitions, "codex")).toBe("Codex");
expect(buildSelectedTriggerLabel("GPT-5.4")).toBe("GPT-5.4"); expect(buildSelectedTriggerLabel("GPT-5.4")).toBe("GPT-5.4");
}); });
}); });
@@ -12,8 +12,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useIsCompactFormFactor } from "@/constants/layout"; import { useIsCompactFormFactor } from "@/constants/layout";
import { isNative, isWeb as platformIsWeb } from "@/constants/platform"; import { isNative, isWeb as platformIsWeb } from "@/constants/platform";
import { ChevronDown, ChevronRight, Search, Star } from "lucide-react-native"; import { ChevronDown, ChevronRight, Search, Star } from "lucide-react-native";
import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types"; import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
import type { SheetHeader } from "@/components/adaptive-modal-sheet"; import type { SheetHeader } from "@/components/adaptive-modal-sheet";
const IS_WEB = platformIsWeb; const IS_WEB = platformIsWeb;
@@ -46,12 +45,9 @@ function drillDownRowStyle({
} }
import { getProviderIcon } from "@/components/provider-icons"; import { getProviderIcon } from "@/components/provider-icons";
import { import {
buildModelRows,
buildProviderGroups,
buildSelectedTriggerLabel, buildSelectedTriggerLabel,
filterAndRankModelRows, filterAndRankModelRows,
resolveProviderLabel, type ModelSelectorProvider,
type SelectorProviderGroup,
type SelectorModelRow, type SelectorModelRow,
} from "./combined-model-selector.utils"; } from "./combined-model-selector.utils";
@@ -63,13 +59,11 @@ type SelectorView =
| { kind: "provider"; providerId: string; providerLabel: string }; | { kind: "provider"; providerId: string; providerLabel: string };
interface CombinedModelSelectorProps { interface CombinedModelSelectorProps {
providerDefinitions: AgentProviderDefinition[]; providers: ModelSelectorProvider[];
allProviderModels: Map<string, AgentModelDefinition[]>;
selectedProvider: string; selectedProvider: string;
selectedModel: string; selectedModel: string;
onSelect: (provider: AgentProvider, modelId: string) => void; onSelect: (provider: AgentProvider, modelId: string) => void;
isLoading: boolean; isLoading: boolean;
canSelectProvider?: (provider: string) => boolean;
favoriteKeys?: Set<string>; favoriteKeys?: Set<string>;
onToggleFavorite?: (provider: string, modelId: string) => void; onToggleFavorite?: (provider: string, modelId: string) => void;
renderTrigger?: (input: { renderTrigger?: (input: {
@@ -85,23 +79,21 @@ interface CombinedModelSelectorProps {
interface SelectorContentProps { interface SelectorContentProps {
view: SelectorView; view: SelectorView;
providerDefinitions: AgentProviderDefinition[]; providers: ModelSelectorProvider[];
allProviderModels: Map<string, AgentModelDefinition[]>;
selectedProvider: string; selectedProvider: string;
selectedModel: string; selectedModel: string;
searchQuery: string; searchQuery: string;
favoriteKeys: Set<string>; favoriteKeys: Set<string>;
onSelect: (provider: string, modelId: string) => void; onSelect: (provider: string, modelId: string) => void;
canSelectProvider: (provider: string) => boolean;
onToggleFavorite?: (provider: string, modelId: string) => void; onToggleFavorite?: (provider: string, modelId: string) => void;
onDrillDown: (providerId: string, providerLabel: string) => void; onDrillDown: (providerId: string, providerLabel: string) => void;
} }
function resolveDefaultModelLabel(models: AgentModelDefinition[] | undefined): string { function resolveDefaultModelLabel(rows: SelectorModelRow[] | undefined): string {
if (!models || models.length === 0) { if (!rows || rows.length === 0) {
return "Select model"; return "Select model";
} }
return (models.find((model) => model.isDefault) ?? models[0])?.label ?? "Select model"; return (rows.find((row) => row.isDefault) ?? rows[0])?.modelLabel ?? "Select model";
} }
function normalizeSearchQuery(value: string): string { function normalizeSearchQuery(value: string): string {
@@ -128,7 +120,6 @@ function ModelRow({
row, row,
isSelected, isSelected,
isFavorite, isFavorite,
disabled = false,
elevated = false, elevated = false,
onPress, onPress,
onToggleFavorite, onToggleFavorite,
@@ -136,7 +127,6 @@ function ModelRow({
row: SelectorModelRow; row: SelectorModelRow;
isSelected: boolean; isSelected: boolean;
isFavorite: boolean; isFavorite: boolean;
disabled?: boolean;
elevated?: boolean; elevated?: boolean;
onPress: () => void; onPress: () => void;
onToggleFavorite?: (provider: string, modelId: string) => void; onToggleFavorite?: (provider: string, modelId: string) => void;
@@ -158,7 +148,7 @@ function ModelRow({
); );
const trailingSlot = useMemo( const trailingSlot = useMemo(
() => () =>
onToggleFavorite && !disabled ? ( onToggleFavorite ? (
<Pressable <Pressable
onPress={handleToggleFavorite} onPress={handleToggleFavorite}
hitSlop={8} hitSlop={8}
@@ -184,7 +174,6 @@ function ModelRow({
) : null, ) : null,
[ [
onToggleFavorite, onToggleFavorite,
disabled,
handleToggleFavorite, handleToggleFavorite,
isFavorite, isFavorite,
row.provider, row.provider,
@@ -202,7 +191,6 @@ function ModelRow({
label={row.modelLabel} label={row.modelLabel}
description={showDescription ? row.description : undefined} description={showDescription ? row.description : undefined}
selected={isSelected} selected={isSelected}
disabled={disabled}
elevated={elevated} elevated={elevated}
onPress={onPress} onPress={onPress}
leadingSlot={leadingSlot} leadingSlot={leadingSlot}
@@ -215,7 +203,6 @@ interface SelectableModelRowProps {
row: SelectorModelRow; row: SelectorModelRow;
isSelected: boolean; isSelected: boolean;
isFavorite: boolean; isFavorite: boolean;
disabled?: boolean;
elevated?: boolean; elevated?: boolean;
onSelect: (provider: string, modelId: string) => void; onSelect: (provider: string, modelId: string) => void;
onToggleFavorite?: (provider: string, modelId: string) => void; onToggleFavorite?: (provider: string, modelId: string) => void;
@@ -225,7 +212,6 @@ function SelectableModelRow({
row, row,
isSelected, isSelected,
isFavorite, isFavorite,
disabled,
elevated, elevated,
onSelect, onSelect,
onToggleFavorite, onToggleFavorite,
@@ -238,7 +224,6 @@ function SelectableModelRow({
row={row} row={row}
isSelected={isSelected} isSelected={isSelected}
isFavorite={isFavorite} isFavorite={isFavorite}
disabled={disabled}
elevated={elevated} elevated={elevated}
onPress={handlePress} onPress={handlePress}
onToggleFavorite={onToggleFavorite} onToggleFavorite={onToggleFavorite}
@@ -252,7 +237,6 @@ function FavoritesSection({
selectedModel, selectedModel,
favoriteKeys, favoriteKeys,
onSelect, onSelect,
canSelectProvider,
onToggleFavorite, onToggleFavorite,
}: { }: {
favoriteRows: SelectorModelRow[]; favoriteRows: SelectorModelRow[];
@@ -260,11 +244,8 @@ function FavoritesSection({
selectedModel: string; selectedModel: string;
favoriteKeys: Set<string>; favoriteKeys: Set<string>;
onSelect: (provider: string, modelId: string) => void; onSelect: (provider: string, modelId: string) => void;
canSelectProvider: (provider: string) => boolean;
onToggleFavorite?: (provider: string, modelId: string) => void; onToggleFavorite?: (provider: string, modelId: string) => void;
}) { }) {
const { theme: _theme } = useUnistyles();
if (favoriteRows.length === 0) { if (favoriteRows.length === 0) {
return null; return null;
} }
@@ -280,7 +261,6 @@ function FavoritesSection({
row={row} row={row}
isSelected={row.provider === selectedProvider && row.modelId === selectedModel} isSelected={row.provider === selectedProvider && row.modelId === selectedModel}
isFavorite={favoriteKeys.has(row.favoriteKey)} isFavorite={favoriteKeys.has(row.favoriteKey)}
disabled={!canSelectProvider(row.provider)}
elevated elevated
onSelect={onSelect} onSelect={onSelect}
onToggleFavorite={onToggleFavorite} onToggleFavorite={onToggleFavorite}
@@ -295,7 +275,6 @@ interface GroupProviderButtonProps {
providerLabel: string; providerLabel: string;
rowCount: number; rowCount: number;
hasNoModels: boolean; hasNoModels: boolean;
disabled?: boolean;
onDrillDown: (providerId: string, providerLabel: string) => void; onDrillDown: (providerId: string, providerLabel: string) => void;
onSelectDefault: (providerId: string) => void; onSelectDefault: (providerId: string) => void;
} }
@@ -305,7 +284,6 @@ function GroupProviderButton({
providerLabel, providerLabel,
rowCount, rowCount,
hasNoModels, hasNoModels,
disabled,
onDrillDown, onDrillDown,
onSelectDefault, onSelectDefault,
}: GroupProviderButtonProps) { }: GroupProviderButtonProps) {
@@ -319,7 +297,7 @@ function GroupProviderButton({
onDrillDown(providerId, providerLabel); onDrillDown(providerId, providerLabel);
}, [hasNoModels, onDrillDown, onSelectDefault, providerId, providerLabel]); }, [hasNoModels, onDrillDown, onSelectDefault, providerId, providerLabel]);
return ( return (
<Pressable disabled={disabled} onPress={handlePress} style={drillDownRowStyle}> <Pressable onPress={handlePress} style={drillDownRowStyle}>
<ProvIcon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> <ProvIcon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<Text style={styles.drillDownText}>{providerLabel}</Text> <Text style={styles.drillDownText}>{providerLabel}</Text>
<View style={styles.drillDownTrailing}> <View style={styles.drillDownTrailing}>
@@ -335,28 +313,26 @@ function GroupProviderButton({
} }
function GroupedProviderRows({ function GroupedProviderRows({
groupedRows, providers,
onDrillDown, onDrillDown,
onSelectDefault, onSelectDefault,
canSelectProvider,
}: { }: {
groupedRows: SelectorProviderGroup[]; providers: ModelSelectorProvider[];
onDrillDown: (providerId: string, providerLabel: string) => void; onDrillDown: (providerId: string, providerLabel: string) => void;
onSelectDefault: (providerId: string) => void; onSelectDefault: (providerId: string) => void;
canSelectProvider: (provider: string) => boolean;
}) { }) {
return ( return (
<View> <View>
{groupedRows.map((group, index) => { {providers.map((provider, index) => {
const hasNoModels = provider.rows.length === 0;
return ( return (
<View key={group.providerId}> <View key={provider.id}>
{index > 0 ? <View style={styles.separator} /> : null} {index > 0 ? <View style={styles.separator} /> : null}
<GroupProviderButton <GroupProviderButton
providerId={group.providerId} providerId={provider.id}
providerLabel={group.providerLabel} providerLabel={provider.label}
rowCount={group.rows.length} rowCount={provider.rows.length}
hasNoModels={group.hasNoModels} hasNoModels={hasNoModels}
disabled={group.hasNoModels && !canSelectProvider(group.providerId)}
onDrillDown={onDrillDown} onDrillDown={onDrillDown}
onSelectDefault={onSelectDefault} onSelectDefault={onSelectDefault}
/> />
@@ -370,12 +346,10 @@ function GroupedProviderRows({
function DefaultProviderRow({ function DefaultProviderRow({
providerId, providerId,
isSelected, isSelected,
disabled,
onSelect, onSelect,
}: { }: {
providerId: string; providerId: string;
isSelected: boolean; isSelected: boolean;
disabled?: boolean;
onSelect: (provider: string, modelId: string) => void; onSelect: (provider: string, modelId: string) => void;
}) { }) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
@@ -392,7 +366,6 @@ function DefaultProviderRow({
<ComboboxItem <ComboboxItem
label="Default" label="Default"
selected={isSelected} selected={isSelected}
disabled={disabled}
onPress={handlePress} onPress={handlePress}
leadingSlot={leadingSlot} leadingSlot={leadingSlot}
/> />
@@ -405,7 +378,6 @@ function ProviderModelRows({
selectedModel, selectedModel,
favoriteKeys, favoriteKeys,
onSelect, onSelect,
canSelectProvider,
onToggleFavorite, onToggleFavorite,
normalizedQuery, normalizedQuery,
}: { }: {
@@ -414,7 +386,6 @@ function ProviderModelRows({
selectedModel: string; selectedModel: string;
favoriteKeys: Set<string>; favoriteKeys: Set<string>;
onSelect: (provider: string, modelId: string) => void; onSelect: (provider: string, modelId: string) => void;
canSelectProvider: (provider: string) => boolean;
onToggleFavorite?: (provider: string, modelId: string) => void; onToggleFavorite?: (provider: string, modelId: string) => void;
normalizedQuery: string; normalizedQuery: string;
}) { }) {
@@ -430,12 +401,11 @@ function ProviderModelRows({
row={item} row={item}
isSelected={item.provider === selectedProvider && item.modelId === selectedModel} isSelected={item.provider === selectedProvider && item.modelId === selectedModel}
isFavorite={favoriteKeys.has(item.favoriteKey)} isFavorite={favoriteKeys.has(item.favoriteKey)}
disabled={!canSelectProvider(item.provider)}
onSelect={onSelect} onSelect={onSelect}
onToggleFavorite={onToggleFavorite} onToggleFavorite={onToggleFavorite}
/> />
), ),
[canSelectProvider, favoriteKeys, onSelect, onToggleFavorite, selectedModel, selectedProvider], [favoriteKeys, onSelect, onToggleFavorite, selectedModel, selectedProvider],
); );
const keyExtractor = useCallback((row: SelectorModelRow) => row.favoriteKey, []); const keyExtractor = useCallback((row: SelectorModelRow) => row.favoriteKey, []);
@@ -464,45 +434,37 @@ function ProviderModelRows({
function SelectorContent({ function SelectorContent({
view, view,
providerDefinitions, providers,
allProviderModels,
selectedProvider, selectedProvider,
selectedModel, selectedModel,
searchQuery, searchQuery,
favoriteKeys, favoriteKeys,
onSelect, onSelect,
canSelectProvider,
onToggleFavorite, onToggleFavorite,
onDrillDown, onDrillDown,
}: SelectorContentProps) { }: SelectorContentProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const allRows = useMemo(
() => buildModelRows(providerDefinitions, allProviderModels),
[allProviderModels, providerDefinitions],
);
const scopedRows = useMemo(() => {
if (view.kind === "provider") {
return allRows.filter((row) => row.provider === view.providerId);
}
return allRows;
}, [allRows, view]);
const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]); const normalizedQuery = useMemo(() => normalizeSearchQuery(searchQuery), [searchQuery]);
const selectedViewProvider = useMemo(
() =>
view.kind === "provider"
? providers.find((provider) => provider.id === view.providerId)
: null,
[providers, view],
);
const visibleRows = useMemo( const visibleRows = useMemo(
() => filterAndRankModelRows(scopedRows, normalizedQuery), () =>
[normalizedQuery, scopedRows], selectedViewProvider
? filterAndRankModelRows(selectedViewProvider.rows, normalizedQuery)
: [],
[normalizedQuery, selectedViewProvider],
); );
const favoriteRows = useMemo( const favoriteRows = useMemo(
() => visibleRows.filter((row) => favoriteKeys.has(row.favoriteKey)), () =>
[favoriteKeys, visibleRows], providers.flatMap((provider) =>
); provider.rows.filter((row) => favoriteKeys.has(row.favoriteKey)),
),
const allGroupedRows = useMemo( [favoriteKeys, providers],
() => buildProviderGroups(providerDefinitions, allProviderModels, visibleRows, normalizedQuery),
[allProviderModels, normalizedQuery, providerDefinitions, visibleRows],
); );
const handleSelectDefaultProvider = useCallback( const handleSelectDefaultProvider = useCallback(
(providerId: string) => { (providerId: string) => {
@@ -510,7 +472,7 @@ function SelectorContent({
}, },
[onSelect], [onSelect],
); );
const hasResults = favoriteRows.length > 0 || allGroupedRows.length > 0; const hasResults = favoriteRows.length > 0 || providers.length > 0;
const emptyState = ( const emptyState = (
<View style={styles.emptyState}> <View style={styles.emptyState}>
<Search size={theme.iconSize.md} color={theme.colors.foregroundMuted} /> <Search size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
@@ -519,13 +481,15 @@ function SelectorContent({
); );
if (view.kind === "provider") { if (view.kind === "provider") {
const providerModels = allProviderModels.get(view.providerId); if (!selectedViewProvider) {
if (providerModels && providerModels.length === 0 && !normalizedQuery) { return emptyState;
}
if (selectedViewProvider.rows.length === 0 && !normalizedQuery) {
return ( return (
<DefaultProviderRow <DefaultProviderRow
providerId={view.providerId} providerId={view.providerId}
isSelected={view.providerId === selectedProvider && !selectedModel} isSelected={view.providerId === selectedProvider && !selectedModel}
disabled={!canSelectProvider(view.providerId)}
onSelect={onSelect} onSelect={onSelect}
/> />
); );
@@ -542,7 +506,6 @@ function SelectorContent({
selectedModel={selectedModel} selectedModel={selectedModel}
favoriteKeys={favoriteKeys} favoriteKeys={favoriteKeys}
onSelect={onSelect} onSelect={onSelect}
canSelectProvider={canSelectProvider}
onToggleFavorite={onToggleFavorite} onToggleFavorite={onToggleFavorite}
normalizedQuery={normalizedQuery} normalizedQuery={normalizedQuery}
/> />
@@ -557,16 +520,14 @@ function SelectorContent({
selectedModel={selectedModel} selectedModel={selectedModel}
favoriteKeys={favoriteKeys} favoriteKeys={favoriteKeys}
onSelect={onSelect} onSelect={onSelect}
canSelectProvider={canSelectProvider}
onToggleFavorite={onToggleFavorite} onToggleFavorite={onToggleFavorite}
/> />
{allGroupedRows.length > 0 ? ( {providers.length > 0 ? (
<GroupedProviderRows <GroupedProviderRows
groupedRows={allGroupedRows} providers={providers}
onDrillDown={onDrillDown} onDrillDown={onDrillDown}
onSelectDefault={handleSelectDefaultProvider} onSelectDefault={handleSelectDefaultProvider}
canSelectProvider={canSelectProvider}
/> />
) : null} ) : null}
@@ -576,13 +537,11 @@ function SelectorContent({
} }
export function CombinedModelSelector({ export function CombinedModelSelector({
providerDefinitions, providers,
allProviderModels,
selectedProvider, selectedProvider,
selectedModel, selectedModel,
onSelect, onSelect,
isLoading, isLoading,
canSelectProvider = () => true,
favoriteKeys = new Set<string>(), favoriteKeys = new Set<string>(),
onToggleFavorite, onToggleFavorite,
renderTrigger, renderTrigger,
@@ -598,26 +557,26 @@ export function CombinedModelSelector({
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [searchResetKey, bumpSearchResetKey] = useReducer((key: number) => key + 1, 0); const [searchResetKey, bumpSearchResetKey] = useReducer((key: number) => key + 1, 0);
// Single-provider mode: only one provider with models → skip Level 1 entirely // Single-provider mode: only one provider → skip Level 1 entirely
const singleProviderView = useMemo<SelectorView | null>(() => { const singleProviderView = useMemo<SelectorView | null>(() => {
const providers = Array.from(allProviderModels.keys());
if (providers.length !== 1) return null; if (providers.length !== 1) return null;
const providerId = providers[0]; const provider = providers[0];
const label = resolveProviderLabel(providerDefinitions, providerId); if (!provider) return null;
return { kind: "provider", providerId, providerLabel: label }; return { kind: "provider", providerId: provider.id, providerLabel: provider.label };
}, [allProviderModels, providerDefinitions]); }, [providers]);
const computeInitialView = useCallback((): SelectorView => { const computeInitialView = useCallback((): SelectorView => {
if (singleProviderView) return singleProviderView; if (singleProviderView) return singleProviderView;
const selectedFavoriteKey = `${selectedProvider}:${selectedModel}`; const selectedFavoriteKey = `${selectedProvider}:${selectedModel}`;
if (selectedProvider && selectedModel && !favoriteKeys.has(selectedFavoriteKey)) { if (selectedProvider && selectedModel && !favoriteKeys.has(selectedFavoriteKey)) {
const label = resolveProviderLabel(providerDefinitions, selectedProvider); const provider = providers.find((entry) => entry.id === selectedProvider);
return { kind: "provider", providerId: selectedProvider, providerLabel: label }; if (provider)
return { kind: "provider", providerId: provider.id, providerLabel: provider.label };
} }
return { kind: "all" }; return { kind: "all" };
}, [singleProviderView, selectedProvider, selectedModel, favoriteKeys, providerDefinitions]); }, [singleProviderView, selectedProvider, selectedModel, favoriteKeys, providers]);
const handleOpenChange = useCallback( const handleOpenChange = useCallback(
(open: boolean) => { (open: boolean) => {
@@ -652,28 +611,28 @@ export function CombinedModelSelector({
if (!hasSelectedProvider) { if (!hasSelectedProvider) {
return "Select model"; return "Select model";
} }
const models = allProviderModels.get(selectedProvider); const provider = providers.find((entry) => entry.id === selectedProvider);
if (models && models.length === 0) { if (provider?.rows.length === 0) {
return "Default"; return "Default";
} }
return isLoading ? "Loading..." : "Select model"; return isLoading ? "Loading..." : "Select model";
} }
const models = allProviderModels.get(selectedProvider); const provider = providers.find((entry) => entry.id === selectedProvider);
if (!models) { if (!provider) {
return isLoading ? "Loading..." : "Select model"; return isLoading ? "Loading..." : "Select model";
} }
const model = models.find((entry) => entry.id === selectedModel); const model = provider.rows.find((entry) => entry.modelId === selectedModel);
return model?.label ?? resolveDefaultModelLabel(models); return model?.modelLabel ?? resolveDefaultModelLabel(provider.rows);
}, [allProviderModels, hasSelectedProvider, isLoading, selectedModel, selectedProvider]); }, [hasSelectedProvider, isLoading, providers, selectedModel, selectedProvider]);
const desktopFixedHeight = useMemo(() => { const desktopFixedHeight = useMemo(() => {
if (view.kind !== "provider") { if (view.kind !== "provider") {
return undefined; return undefined;
} }
const models = allProviderModels.get(view.providerId); const modelCount =
const modelCount = models?.length ?? 0; providers.find((provider) => provider.id === view.providerId)?.rows.length ?? 0;
return Math.min(80 + modelCount * 40, 400); return Math.min(80 + modelCount * 40, 400);
}, [allProviderModels, view]); }, [providers, view]);
const triggerLabel = useMemo(() => { const triggerLabel = useMemo(() => {
if (selectedModelLabel === "Loading..." || selectedModelLabel === "Select model") { if (selectedModelLabel === "Loading..." || selectedModelLabel === "Select model") {
@@ -808,14 +767,12 @@ export function CombinedModelSelector({
{isContentReady ? ( {isContentReady ? (
<SelectorContent <SelectorContent
view={view} view={view}
providerDefinitions={providerDefinitions} providers={providers}
allProviderModels={allProviderModels}
selectedProvider={selectedProvider} selectedProvider={selectedProvider}
selectedModel={selectedModel} selectedModel={selectedModel}
searchQuery={searchQuery} searchQuery={searchQuery}
favoriteKeys={favoriteKeys} favoriteKeys={favoriteKeys}
onSelect={handleSelect} onSelect={handleSelect}
canSelectProvider={canSelectProvider}
onToggleFavorite={onToggleFavorite} onToggleFavorite={onToggleFavorite}
onDrillDown={handleDrillDown} onDrillDown={handleDrillDown}
/> />
@@ -1,54 +1,60 @@
import type { AgentModelDefinition } from "@server/server/agent/agent-sdk-types"; import type {
AgentModelDefinition,
ProviderSnapshotEntry,
} from "@server/server/agent/agent-sdk-types";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest"; import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
import { buildFavoriteModelKey, type FavoriteModelRow } from "@/hooks/use-form-preferences"; import { buildFavoriteModelKey, type FavoriteModelRow } from "@/hooks/use-form-preferences";
import { compareMatchScores, scoreTextFields } from "@/utils/score-match"; import { compareMatchScores, scoreTextFields } from "@/utils/score-match";
export type SelectorModelRow = FavoriteModelRow; export type SelectorModelRow = FavoriteModelRow & { isDefault?: boolean };
export interface SelectorProviderGroup { export interface ModelSelectorProvider {
providerId: string; id: string;
providerLabel: string; label: string;
rows: SelectorModelRow[]; rows: SelectorModelRow[];
hasNoModels: boolean;
}
export function resolveProviderLabel(
providerDefinitions: AgentProviderDefinition[],
providerId: string,
): string {
return (
providerDefinitions.find((definition) => definition.id === providerId)?.label ?? providerId
);
} }
export function buildSelectedTriggerLabel(modelLabel: string): string { export function buildSelectedTriggerLabel(modelLabel: string): string {
return modelLabel; return modelLabel;
} }
export function buildModelRows( export function buildModelSelectorProviders(
providerDefinitions: AgentProviderDefinition[], providerDefinitions: AgentProviderDefinition[],
allProviderModels: Map<string, AgentModelDefinition[]>, allProviderModels: Map<string, AgentModelDefinition[]>,
): SelectorModelRow[] { ): ModelSelectorProvider[] {
const providerLabelMap = new Map( return providerDefinitions.map((definition) => ({
providerDefinitions.map((definition) => [definition.id, definition.label]), id: definition.id,
); label: definition.label,
const rows: SelectorModelRow[] = []; rows: (allProviderModels.get(definition.id) ?? []).map((model) => ({
favoriteKey: buildFavoriteModelKey({ provider: definition.id, modelId: model.id }),
provider: definition.id,
providerLabel: definition.label,
modelId: model.id,
modelLabel: model.label,
description: model.description,
isDefault: model.isDefault,
})),
}));
}
for (const definition of providerDefinitions) { export function buildSelectableModelSelectorProviders(
const providerLabel = providerLabelMap.get(definition.id) ?? definition.label; entries: ProviderSnapshotEntry[] | undefined,
for (const model of allProviderModels.get(definition.id) ?? []) { ): ModelSelectorProvider[] {
rows.push({ return (entries ?? [])
favoriteKey: buildFavoriteModelKey({ provider: definition.id, modelId: model.id }), .filter((entry) => entry.enabled && entry.status === "ready")
provider: definition.id, .map((entry) => ({
providerLabel, id: entry.provider,
label: entry.label ?? entry.provider,
rows: (entry.models ?? []).map((model) => ({
favoriteKey: buildFavoriteModelKey({ provider: entry.provider, modelId: model.id }),
provider: entry.provider,
providerLabel: entry.label ?? entry.provider,
modelId: model.id, modelId: model.id,
modelLabel: model.label, modelLabel: model.label,
description: model.description, description: model.description,
}); isDefault: model.isDefault,
} })),
} }));
return rows;
} }
export function matchesSearch(row: SelectorModelRow, normalizedQuery: string): boolean { export function matchesSearch(row: SelectorModelRow, normalizedQuery: string): boolean {
@@ -82,55 +88,3 @@ export function filterAndRankModelRows(
return scored.map((entry) => entry.row); return scored.map((entry) => entry.row);
} }
export function buildProviderGroups(
providerDefinitions: AgentProviderDefinition[],
allProviderModels: Map<string, AgentModelDefinition[]>,
rows: SelectorModelRow[],
normalizedQuery: string,
): SelectorProviderGroup[] {
const rowsByProvider = new Map<string, SelectorModelRow[]>();
for (const row of rows) {
const providerRows = rowsByProvider.get(row.provider);
if (providerRows) {
providerRows.push(row);
} else {
rowsByProvider.set(row.provider, [row]);
}
}
const groups: SelectorProviderGroup[] = [];
for (const definition of providerDefinitions) {
const providerRows = rowsByProvider.get(definition.id) ?? [];
if (providerRows.length > 0) {
groups.push({
providerId: definition.id,
providerLabel: definition.label,
rows: providerRows,
hasNoModels: false,
});
continue;
}
const models = allProviderModels.get(definition.id);
if (!models || models.length > 0) {
continue;
}
const providerMatches =
!normalizedQuery ||
scoreTextFields(normalizedQuery, [definition.label, definition.id]) !== null;
if (!providerMatches) {
continue;
}
groups.push({
providerId: definition.id,
providerLabel: definition.label,
rows: [],
hasNoModels: true,
});
}
return groups;
}
@@ -19,7 +19,7 @@ describe("resolveStatusControlMode", () => {
selectedModel: "", selectedModel: "",
onSelectModel: () => undefined, onSelectModel: () => undefined,
isModelLoading: false, isModelLoading: false,
allProviderModels: new Map(), modelSelectorProviders: [],
isAllModelsLoading: false, isAllModelsLoading: false,
onSelectProviderAndModel: () => undefined, onSelectProviderAndModel: () => undefined,
thinkingOptions: [], thinkingOptions: [],
@@ -8,6 +8,10 @@ import type {
} from "@server/server/agent/agent-sdk-types"; } from "@server/server/agent/agent-sdk-types";
import { useHosts } from "@/runtime/host-runtime"; import { useHosts } from "@/runtime/host-runtime";
import { buildProviderDefinitions } from "@/utils/provider-definitions"; import { buildProviderDefinitions } from "@/utils/provider-definitions";
import {
buildSelectableModelSelectorProviders,
type ModelSelectorProvider,
} from "@/components/combined-model-selector.utils";
import { useProvidersSnapshot } from "./use-providers-snapshot"; import { useProvidersSnapshot } from "./use-providers-snapshot";
import { import {
useFormPreferences, useFormPreferences,
@@ -63,6 +67,7 @@ export interface UseAgentFormStateResult {
modeOptions: AgentMode[]; modeOptions: AgentMode[];
availableModels: AgentModelDefinition[]; availableModels: AgentModelDefinition[];
allProviderModels: Map<string, AgentModelDefinition[]>; allProviderModels: Map<string, AgentModelDefinition[]>;
modelSelectorProviders: ModelSelectorProvider[];
isAllModelsLoading: boolean; isAllModelsLoading: boolean;
availableThinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>; availableThinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
isModelLoading: boolean; isModelLoading: boolean;
@@ -237,6 +242,10 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
() => buildAllProviderModels(snapshotEntries), () => buildAllProviderModels(snapshotEntries),
[snapshotEntries], [snapshotEntries],
); );
const snapshotModelSelectorProviders = useMemo(
() => buildSelectableModelSelectorProviders(snapshotEntries),
[snapshotEntries],
);
const snapshotSelectedEntry = useMemo( const snapshotSelectedEntry = useMemo(
() => () =>
formState.provider formState.provider
@@ -255,6 +264,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
const providerDefinitionMap = snapshotProviderDefinitionMap; const providerDefinitionMap = snapshotProviderDefinitionMap;
const selectableProviderDefinitionMap = snapshotSelectableProviderDefinitionMap; const selectableProviderDefinitionMap = snapshotSelectableProviderDefinitionMap;
const allProviderModels = snapshotAllProviderModels; const allProviderModels = snapshotAllProviderModels;
const modelSelectorProviders = snapshotModelSelectorProviders;
const availableModels = snapshotSelectedProviderModels; const availableModels = snapshotSelectedProviderModels;
const modeOptions = snapshotSelectedProviderModes; const modeOptions = snapshotSelectedProviderModes;
const isAllModelsLoading = snapshotIsLoading || selectedProviderIsLoading; const isAllModelsLoading = snapshotIsLoading || selectedProviderIsLoading;
@@ -516,6 +526,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
modeOptions, modeOptions,
availableModels: availableModels ?? [], availableModels: availableModels ?? [],
allProviderModels, allProviderModels,
modelSelectorProviders,
isAllModelsLoading, isAllModelsLoading,
availableThinkingOptions, availableThinkingOptions,
isModelLoading, isModelLoading,
@@ -548,6 +559,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
modeOptions, modeOptions,
availableModels, availableModels,
allProviderModels, allProviderModels,
modelSelectorProviders,
isAllModelsLoading, isAllModelsLoading,
availableThinkingOptions, availableThinkingOptions,
isModelLoading, isModelLoading,
@@ -88,7 +88,7 @@ export function buildDraftStatusControls(input: {
selectedModel: formState.selectedModel, selectedModel: formState.selectedModel,
onSelectModel: formState.setModelFromUser, onSelectModel: formState.setModelFromUser,
isModelLoading: formState.isModelLoading, isModelLoading: formState.isModelLoading,
allProviderModels: formState.allProviderModels, modelSelectorProviders: formState.modelSelectorProviders,
isAllModelsLoading: formState.isAllModelsLoading, isAllModelsLoading: formState.isAllModelsLoading,
onSelectProviderAndModel: formState.setProviderAndModelFromUser, onSelectProviderAndModel: formState.setProviderAndModelFromUser,
thinkingOptions: formState.availableThinkingOptions, thinkingOptions: formState.availableThinkingOptions,
@@ -81,6 +81,22 @@ vi.mock("./use-agent-form-state", () => ({
], ],
], ],
]), ]),
modelSelectorProviders: [
{
id: "codex",
label: "Codex",
rows: [
{
favoriteKey: "codex:gpt-5.4",
provider: "codex",
providerLabel: "Codex",
modelId: "gpt-5.4",
modelLabel: "gpt-5.4",
isDefault: true,
},
],
},
],
isAllModelsLoading: false, isAllModelsLoading: false,
availableThinkingOptions: [ availableThinkingOptions: [
{ id: "medium", label: "Medium" }, { id: "medium", label: "Medium" },