Streamline model selector provider data
This commit is contained in:
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user