mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user