Match model and thinking effort selects in agent setup

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-02 22:21:52 -05:00
1 parent 970bd7e8dc
commit cd1e43ab0e
4 files changed
+78 -14

No files matched your search

+46 -1
View File
@@ -44,7 +44,8 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { selectTriggerClassName } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { NativeSelect, selectTriggerClassName } from "@/components/ui/select";
import { AdapterMark } from "./AdapterMark";
import { FolderOpen, Heart, ChevronDown, X, Copy, Check, ExternalLink, Loader2, TriangleAlert, Bug } from "lucide-react";
import { asBoolean, asFiniteNumber, asObject, cn } from "../lib/utils";
@@ -3755,6 +3756,7 @@ export function ModelDropdown({
detectModelLabel,
emptyDetectHint,
defaultLabel,
presentation = "searchable",
}: {
models: AdapterModel[];
value: string;
@@ -3776,8 +3778,10 @@ export function ModelDropdown({
detectModelLabel?: string;
emptyDetectHint?: string;
defaultLabel?: string;
presentation?: "searchable" | "native";
}) {
const [modelSearch, setModelSearch] = useState("");
const [enteringCustomModel, setEnteringCustomModel] = useState(false);
const [detectingModel, setDetectingModel] = useState(false);
const selected = models.find((m) => m.id === value);
const manualModel = modelSearch.trim();
@@ -3846,6 +3850,47 @@ export function ModelDropdown({
}
}
if (presentation === "native") {
const customOption = "__paperclip_custom_model__";
const extraModels = [...new Set([value, ...promotedModelIds])].filter(id => id && (!models.some(model => model.id === id) || promotedModelIds.has(id)));
return (
<Field label="Model" hint={help.model}>
<NativeSelect
aria-label="Model"
aria-busy={loadingModels || refreshingModels}
value={enteringCustomModel ? customOption : value}
required={required && !enteringCustomModel}
onChange={event => {
const next = event.target.value;
setEnteringCustomModel(next === customOption);
if (next !== customOption) onChange(next);
}}
>
<option value="" disabled={!allowDefault}>
{allowDefault ? (defaultLabel ?? "Default") : loadingModels ? "Loading models…" : required ? "Select model (required)" : "Select model"}
</option>
{extraModels.map(id => <option key={id} value={id}>{models.find(model => model.id === id)?.label ?? id}</option>)}
{groupedModels.map(({ provider, entries }) => groupByProvider ? (
<optgroup key={provider} label={provider}>
{entries.map(model => <option key={model.id} value={model.id}>{model.label}</option>)}
</optgroup>
) : entries.map(model => <option key={model.id} value={model.id}>{model.label}</option>))}
{creatable && <option value={customOption}>Enter custom model…</option>}
</NativeSelect>
{enteringCustomModel && (
<label className="mt-3 block space-y-1 text-xs text-muted-foreground">
Model ID
<Input aria-label="Model ID" value={value} onChange={event => onChange(event.target.value)}
placeholder="Enter model ID or alias" autoFocus required={required} />
</label>
)}
{loadingModels && <p role="status" className="mt-2 text-xs text-muted-foreground">Loading models…</p>}
{onRefreshModels && <Button type="button" variant="ghost" size="sm" disabled={refreshingModels}
onClick={() => void onRefreshModels()}>{refreshingModels ? "Refreshing…" : "Refresh models"}</Button>}
</Field>
);
}
return (
<Field label="Model" hint={help.model}>
<Popover
@@ -54,6 +54,7 @@ import { Field } from "../agent-config-primitives";
import { SecretPicker } from "../environment-variables-editor/SecretPicker";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { NativeSelect, nativeSelectClassName } from "../ui/select";
import {
OnboardingCard,
OnboardingHeading,
@@ -67,8 +68,7 @@ import {
} from "./AgentProviderConnection";
import { adapterCuratesModelOrder } from "../../lib/model-utils";
const controlClass =
"w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-sm leading-5 outline-none focus-visible:ring-2 focus-visible:ring-ring";
const controlClass = nativeSelectClassName;
const blocking = (result: AdapterEnvironmentTestResult) =>
result.status === "fail" ||
result.checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE);
@@ -844,6 +844,7 @@ function Setup({
<div className="grid items-start gap-5 sm:grid-cols-2">
{showModel && !usingKimiApi && (
<ModelDropdown
presentation="native"
models={connectionModels?.models ?? models.data ?? []}
loadingModels={connectionModels?.isLoading ?? models.isLoading}
onRefreshModels={connectionModels?.refreshModels}
@@ -881,9 +882,8 @@ function Setup({
)}
{efforts.length > 0 && (
<Field label="Thinking effort">
<select
<NativeSelect
aria-label="Thinking effort"
className={controlClass}
value={effort}
onChange={(event) => {
setEffort(event.target.value);
@@ -896,7 +896,7 @@ function Setup({
{value}
</option>
))}
</select>
</NativeSelect>
</Field>
)}
</div>
+6
View File
@@ -4,6 +4,12 @@ import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
export const nativeSelectClassName = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-sm leading-5 outline-none focus-visible:ring-2 focus-visible:ring-ring"
export function NativeSelect({ className, ...props }: React.ComponentProps<"select">) {
return <select {...props} className={cn(nativeSelectClassName, className)} />
}
// Shared with searchable picker triggers so their size and states match Select.
export const selectTriggerClassName = "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-base md:text-sm whitespace-nowrap shadow-xs transition-(--tp-color-box-shadow) outline-none focus-visible:ring-(length:--rad-3) disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
@@ -7,6 +7,8 @@ import { AiConnectionField } from "@/components/ai-connections/AiConnectionField
import { AgentProviderConnection, type ProviderConnection } from "@/components/new-agent/AgentProviderConnection";
import { useConnectionModels } from "@/components/ai-connections/useConnectionModels";
import { ModelDropdown } from "@/components/AgentConfigForm";
import { Field } from "@/components/agent-config-primitives";
import { NativeSelect } from "@/components/ui/select";
import { queryKeys } from "@/lib/queryKeys";
import type {
AiConnectionBinding,
@@ -107,13 +109,19 @@ function Connection() {
function OpenRouterModelPicker() {
const [open, setOpen] = useState(false);
const [value, setValue] = useState("");
const [effort, setEffort] = useState("");
const connection = useConnectionModels(companyId, {
provider: "openrouter", method: "api_key", mode: "shared",
connectionId: accounts[1].id, grantId: accounts[1].grantId,
}, "opencode_local");
return <ModelDropdown models={connection?.models ?? []} value={value} onChange={setValue}
return <div className="grid gap-5 sm:grid-cols-2">
<ModelDropdown presentation="native" models={connection?.models ?? []} value={value} onChange={setValue}
open={open} onOpenChange={setOpen} allowDefault={false} required groupByProvider={false}
preserveOrder creatable loadingModels={connection?.isLoading} />;
preserveOrder creatable loadingModels={connection?.isLoading} />
<Field label="Thinking effort"><NativeSelect aria-label="Thinking effort" value={effort} onChange={event => setEffort(event.target.value)}>
<option value="">Auto</option><option value="high">high</option>
</NativeSelect></Field>
</div>;
}
function OpenRouterModels() {
const [client] = useState(() => {
@@ -192,13 +200,18 @@ export const OpenRouterPopularModels: Story = {
render: () => <OpenRouterModels />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: "Select model (required)" }));
const body = within(canvasElement.ownerDocument.body);
const labels = Array.from(canvasElement.ownerDocument.querySelectorAll('span[title^="openrouter/"]')).map(element => element.textContent);
const model = canvas.getByRole("combobox", { name: "Model" });
const labels = within(model).getAllByRole("option").filter(option => (option as HTMLOptionElement).value.startsWith("openrouter/")).map(option => option.textContent);
await expect(labels).toEqual(["Z.AI GLM 5", "Claude Sonnet 4.5", "OpenAI GPT-5.4"]);
await userEvent.type(body.getByPlaceholderText("Search models... (type to create)"), "claude");
await userEvent.click(body.getByRole("button", { name: "Claude Sonnet 4.5" }));
await expect(canvas.getByRole("button", { name: "Claude Sonnet 4.5" })).toBeVisible();
await userEvent.selectOptions(model, "openrouter/anthropic/claude-sonnet-4.5");
await expect(model).toHaveValue("openrouter/anthropic/claude-sonnet-4.5");
await userEvent.selectOptions(canvas.getByRole("combobox", { name: "Thinking effort" }), "high");
await userEvent.selectOptions(model, within(model).getByRole("option", { name: "Enter custom model…" }));
await userEvent.clear(canvas.getByRole("textbox", { name: "Model ID" }));
await userEvent.type(canvas.getByRole("textbox", { name: "Model ID" }), "my-gateway/model-alias");
await expect(canvas.getByRole("textbox", { name: "Model ID" })).toHaveValue("my-gateway/model-alias");
await userEvent.selectOptions(model, "openrouter/anthropic/claude-sonnet-4.5");
await expect(canvas.queryByRole("textbox", { name: "Model ID" })).not.toBeInTheDocument();
},
};
export const ChooseProvider: Story = {