fix(desktop): onboarding defaults — API key field and model search (#2081)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-07-18 12:44:01 -06:00
committed by GitHub
parent 463db6de3e
commit 741610f138
3 changed files with 90 additions and 33 deletions

View File

@@ -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,
@@ -76,7 +75,6 @@ export type GlobalAgentConfigFieldsProps = {
disableModelSelectDuringDiscovery?: boolean;
effortPlaceholderLabel?: string;
effortLabel?: string;
hideUnconfiguredCredentialProviders?: boolean;
keepSelectedModelValueLabel?: boolean;
modelPlaceholderLabel?: string;
placeholderClassName?: string;
@@ -112,7 +110,6 @@ export function GlobalAgentConfigFields({
disableModelSelectDuringDiscovery = true,
effortPlaceholderLabel,
effortLabel = "Thinking/effort",
hideUnconfiguredCredentialProviders = false,
keepSelectedModelValueLabel = false,
modelPlaceholderLabel = "Select model",
placeholderClassName,
@@ -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 &&
@@ -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,
@@ -510,7 +483,7 @@ export function GlobalAgentConfigFields({
</div>
) : null}
{showAdvancedFields && providerFieldVisible && apiKeyEnvVar ? (
{providerFieldVisible && apiKeyEnvVar ? (
<div className={blockClassName}>
<PersonaProviderApiKeyField
disabled={false}

View File

@@ -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";
@@ -47,6 +47,7 @@ export function AgentDropdownSelect({
placeholder = "Select",
placeholderClassName,
placeholderValue,
searchable = false,
selectedLabel,
testId,
value,
@@ -60,11 +61,14 @@ 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 &&
@@ -72,8 +76,24 @@ export function AgentDropdownSelect({
(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`}
@@ -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
@@ -373,6 +413,7 @@ export function AgentModelField({
options={modelOptions}
placeholder={placeholder}
placeholderClassName={placeholderClassName}
searchable
selectedLabel={stableSelectedModelLabel}
testId={testId ?? id}
value={modelSelectValue}

View File

@@ -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,
}) => {
@@ -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);
});