Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 2 additions & 29 deletions desktop/src/features/agents/ui/GlobalAgentConfigFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
AUTO_PROVIDER_DROPDOWN_VALUE,
BLOCK_BUILD_HIDDEN_PROVIDER_IDS,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
PERSONA_LLM_PROVIDER_OPTIONS,
getPersonaProviderOptions,
getProviderApiKeyEnvVar,
requiredCredentialEnvKeys,
Expand Down Expand Up @@ -76,7 +75,6 @@ export type GlobalAgentConfigFieldsProps = {
disableModelSelectDuringDiscovery?: boolean;
effortPlaceholderLabel?: string;
effortLabel?: string;
hideUnconfiguredCredentialProviders?: boolean;
keepSelectedModelValueLabel?: boolean;
modelPlaceholderLabel?: string;
placeholderClassName?: string;
Expand Down Expand Up @@ -112,7 +110,6 @@ export function GlobalAgentConfigFields({
disableModelSelectDuringDiscovery = true,
effortPlaceholderLabel,
effortLabel = "Thinking/effort",
hideUnconfiguredCredentialProviders = false,
keepSelectedModelValueLabel = false,
modelPlaceholderLabel = "Select model",
placeholderClassName,
Expand Down Expand Up @@ -191,13 +188,6 @@ export function GlobalAgentConfigFields({
() => bakedEnv.map((entry) => entry.key),
[bakedEnv],
);
const configuredCredentialKeys = React.useMemo(() => {
const keys = new Set(bakedEnvKeys);
for (const [key, value] of Object.entries(config.env_vars)) {
if (value.trim().length > 0) keys.add(key);
}
return keys;
}, [bakedEnvKeys, config.env_vars]);
const apiKeyInherited =
apiKeyEnvVar !== null &&
apiKeyValue.length === 0 &&
Expand Down Expand Up @@ -355,28 +345,11 @@ export function GlobalAgentConfigFields({
hidden.add(providerId);
}
}
if (hideUnconfiguredCredentialProviders) {
for (const option of PERSONA_LLM_PROVIDER_OPTIONS) {
const requiredKeys = requiredCredentialEnvKeys(
credentialRuntimeId,
option.id,
);
if (requiredKeys.some((key) => !configuredCredentialKeys.has(key))) {
hidden.add(option.id);
}
}
}
if (selectedRuntimeId !== "buzz-agent") {
hidden.add("relay-mesh");
}
return hidden;
}, [
bakedEnvKeys,
configuredCredentialKeys,
credentialRuntimeId,
hideUnconfiguredCredentialProviders,
selectedRuntimeId,
]);
}, [bakedEnvKeys, selectedRuntimeId]);
const providerOptions = getPersonaProviderOptions(
providerValue,
credentialRuntimeId,
Expand Down Expand Up @@ -510,7 +483,7 @@ export function GlobalAgentConfigFields({
</div>
) : null}

{showAdvancedFields && providerFieldVisible && apiKeyEnvVar ? (
{providerFieldVisible && apiKeyEnvVar ? (
<div className={blockClassName}>
<PersonaProviderApiKeyField
disabled={false}
Expand Down
47 changes: 44 additions & 3 deletions desktop/src/features/agents/ui/personaProviderModelFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* instead of duplicating the picker logic.
*/
import * as React from "react";
import { Check, ChevronDown } from "lucide-react";
import { Check, ChevronDown, Search } from "lucide-react";

import type { AcpRuntimeCatalogEntry } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
Expand Down Expand Up @@ -47,6 +47,7 @@ export function AgentDropdownSelect({
placeholder = "Select",
placeholderClassName,
placeholderValue,
searchable = false,
selectedLabel,
testId,
value,
Expand All @@ -60,20 +61,39 @@ export function AgentDropdownSelect({
placeholder?: string;
placeholderClassName?: string;
placeholderValue?: string;
/** Show a filter input above the options when the list is long. */
searchable?: boolean;
selectedLabel?: React.ReactNode;
testId?: string;
value: string;
}) {
const [open, setOpen] = React.useState(false);
const [query, setQuery] = React.useState("");
const selectedOption = options.find((option) => option.value === value);
const isPlaceholderSelection =
selectedLabel === undefined &&
(selectedOption === undefined ||
(placeholderValue !== undefined &&
selectedOption.value === placeholderValue));

const showSearch = searchable && options.length > 1;
const filteredOptions = React.useMemo(() => {
const trimmed = query.trim().toLowerCase();
if (!showSearch || trimmed === "") return options;
return options.filter((option) =>
(typeof option.label === "string" ? option.label : option.value)
.toLowerCase()
.includes(trimmed),
);
}, [options, query, showSearch]);

function handleOpenChange(next: boolean) {
setOpen(next);
if (!next) setQuery("");
}

return (
<Popover open={open} onOpenChange={setOpen}>
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<button
aria-controls={`${id}-listbox`}
Expand Down Expand Up @@ -125,7 +145,27 @@ export function AgentDropdownSelect({
id={`${id}-listbox`}
role="listbox"
>
{options.map((option) => {
{showSearch ? (
<div className="relative">
<Search
aria-hidden="true"
className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-foreground/45"
/>
<Input
aria-label="Search models"
autoFocus
className="h-9 rounded-xl border-foreground/10 bg-white pl-9 text-sm"
data-testid={testId ? `${testId}-search` : undefined}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search models…"
value={query}
/>
</div>
) : null}
{showSearch && filteredOptions.length === 0 ? (
<p className="px-3 py-2 text-sm text-foreground/55">No matches</p>
) : null}
{filteredOptions.map((option) => {
const selected = option.value === value;
return (
<button
Expand Down Expand Up @@ -373,6 +413,7 @@ export function AgentModelField({
options={modelOptions}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
searchable
selectedLabel={stableSelectedModelLabel}
testId={testId ?? id}
value={modelSelectValue}
Expand Down
45 changes: 44 additions & 1 deletion desktop/tests/e2e/onboarding-agent-defaults.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,49 @@ test("config page gates stale saved model and effort until provider selection",
await expect(effortSelect).toHaveText("Select effort level");
});

test("config page model dropdown filters options via search", async ({
page,
}) => {
await installMockBridge(
page,
{
globalAgentConfig: {
env_vars: { OPENAI_COMPAT_API_KEY: "sk-test" },
provider: "openai",
model: null,
},
},
{
skipCommunitySeed: true,
skipOnboardingSeed: true,
},
);
await page.goto("/");

await navigateToConfigPage(page);

const modelSelect = page.getByTestId("global-agent-model");
await expect(modelSelect).toBeEnabled();
await modelSelect.click();
const search = page.getByTestId("global-agent-model-search");
await expect(search).toBeVisible();
await expect(
page.getByTestId("global-agent-model-option-gpt-5.5"),
).toBeVisible();
await search.fill("mini");
await expect(
page.getByTestId("global-agent-model-option-gpt-5.4-mini"),
).toBeVisible();
await expect(
page.getByTestId("global-agent-model-option-gpt-5.5"),
).toHaveCount(0);
await search.fill("zzz-no-such-model");
await expect(page.getByText("No matches")).toBeVisible();
await search.fill("");
await page.getByTestId("global-agent-model-option-gpt-5.4-nano").click();
await expect(modelSelect).toHaveAttribute("data-value", "gpt-5.4-nano");
});

test("config page defaults model after provider selection", async ({
page,
}) => {
Expand Down Expand Up @@ -1077,7 +1120,7 @@ test("compact default config still persists rapid provider edits", async ({
await chooseConfigDropdownOption(page, "global-agent-provider", "anthropic");
await expect(providerSelect).toHaveAttribute("data-value", "anthropic");

await expect(page.getByLabel("Anthropic API Key")).toHaveCount(0);
await expect(page.getByLabel("Anthropic API Key")).toBeVisible();
await expect(page.getByLabel("OpenAI API Key")).toHaveCount(0);
await expect(page.getByLabel("Value for DATABRICKS_HOST")).toHaveCount(0);
});
Expand Down
Loading