Files
buzz/desktop/src/features/agents/ui/AgentConfigFields.tsx
T
985cdcc6ea feat(agents): model-tuning parity in global Agent Defaults editor (#4578)
## Overview

The global Agent Defaults surface (Settings card, defaults modal,
onboarding) exposed structured controls for Effort but left Max Output
Tokens, Context Limit, and Max Rounds as raw env vars. Per-agent dialogs
had structured numeric fields but only for `isBuzzAgentRuntime` —
incorrectly excluding Goose. This PR unifies numeric-tuning capability
across all surfaces, fixes a pre-existing dual-editor defect, and adds
full test coverage.

## What changed

### Phase 1 — Catalog projection

- Add `max_rounds_env_var` to `KnownAcpRuntime` in `runtime_metadata.rs`
(`Some("BUZZ_AGENT_MAX_ROUNDS")` for buzz-agent, `None` elsewhere).
- Project all three numeric env-var fields (`max_tokens_env_var`,
`context_limit_env_var`, `max_rounds_env_var`) end-to-end:
`AcpRuntimeCatalogEntry` Rust struct, TS `types.ts`,
`RawAcpRuntimeCatalogEntry` + `fromRawAcpRuntimeCatalogEntry` in
`tauri.ts`, and the e2e mock bridge (`withMockRuntimeConfigMetadata`).

### Phase 2 — Field model

- `deriveAgentConfigFieldModel` now derives `maxOutputTokens` /
`contextLimit` / `maxRounds` descriptors from catalog-projected fields.
- `structuredEnvKeys(descriptors)` — exported helper that takes the
**rendered** descriptor set (not the whole model). Hidden keys follow
what is actually rendered per surface: global hides effort + all three
numeric keys for buzz-agent / two for Goose; per-agent buzz-agent hides
effort + three numeric keys; per-agent Goose hides only its two numeric
keys. `BUZZ_AGENT_THINKING_EFFORT` stays a visible generic env row
per-agent because no effort control renders there.

### Phase 3 — UI

- Extract `NumericTuningFields` from `buzzAgentModelTuningFields.tsx` as
a shared descriptor-driven component (`descriptors`, `envVars`,
`inheritedEnvVars`, `onEnvVarChange`). Kind-specific minima:
`NUMERIC_KIND_MIN` map (`maxOutputTokens`/`contextLimit`: 1,
`maxRounds`: 0) applied to `<input min>`.
- **Global surface** (`AgentConfigFields.tsx`): deduplicate the
previously duplicated Advanced env-editor block; render
`NumericTuningFields` below the env editor when descriptors exist;
`hiddenKeys` and `bakedGenericRows` exclusions use `structuredEnvKeys`
so structured keys are never double-rendered. Under 1000 lines.
- **Per-agent surfaces** (`EditAgentAdvancedFields`,
`PersonaAdvancedFields`): replace `isBuzzAgentRuntime` as the
numeric-field gate with `deriveNumericDescriptors(selectedRuntime)` from
`agentConfigCore`; hidden keys come from
`structuredEnvKeys(numericDescriptors)` — the same rendered descriptor
set, no local rebuilding (fixes pre-existing dual-editor defect).
Catalog status carried as `RuntimeCatalogStatus` (`loading | ready |
error`); both error and loading withhold structured controls and leave
saved values visible as generic rows, making error distinguishable from
"runtime not capable" (`ready` + no runtime).
- **Dialogs** (`AgentDefinitionDialog`, `AgentInstanceEditDialog`,
callers): `AgentDefinitionDialog` accepts `runtimeCatalogStatus?:
"loading" | "ready" | "error"` (replaces separate
`runtimesLoading`/`runtimesError` booleans); all call sites —
`AgentManagementDialogs`, `AgentsView`, `RequestedAgentCreateDialogs`,
`UserProfilePersonaDialogs` — compute and pass the status.

### Phase 4 — Tests

- `buildRecord` exported from `EnvVarsEditor.tsx` as a pure `(nextRows,
value, requiredKeys, hiddenKeys) => Record<string, string>` helper for
isolation testing.
- **17 new node tests** in `agentConfigCore.test.mjs`:
`deriveNumericDescriptors` (all three fields, partial, undefined
runtime, matches field-model subset); `structuredEnvKeys` per surface
including discriminating Goose per-agent effort-key invariant;
`NUMERIC_KIND_MIN` values.
- **4 new node tests** in `EnvVarsEditor.test.mjs`: hidden tuning key
preserved through generic row edits; runtime-switch then generic edit
(derives both descriptor sets, asserts new-runtime hidden key survives
`buildRecord` via `hiddenKeys` and old-runtime key survives via generic
rows); baked numeric key excluded via `filterBakedGenericRows` with
`numericTuningPlaceholder` assertion; clearing a structured override —
`numericTuningPlaceholder` verifies placeholder text.
- **5 new Playwright tests** in `agent-numeric-tuning.spec.ts` (added to
smoke project `testMatch`): global numeric fields visible for
buzz-agent; global: non-capable runtime hides numeric controls; Goose
per-agent shows `Inherit (16384)` after saving global value through the
UI; delayed catalog: saved values visible as generic rows while loading
then structured controls appear after settle; failed catalog: saved
values remain visible as generic rows (never the "unsupported" empty
state).

## Result

- buzz-agent global defaults: Max output tokens, Context limit, Max
rounds as structured inputs with `Inherit (N)` placeholders from baked
env.
- Goose global defaults: Max output tokens, Context limit as structured
inputs.
- A Goose global value surfaces as `Inherit (<value>)` in the per-agent
Goose edit dialog.
- No structured key is editable in two places on any surface; no
persisted key has zero editors.
- No `runtime.id === "buzz-agent"` comparison decides numeric-field
visibility anywhere — capability flows catalog →
`AcpRuntimeCatalogEntry` → field model → UI.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-03 18:09:24 -04:00

995 lines
34 KiB
TypeScript

/**
* Controlled field group for global agent config (provider, model, effort, env vars).
*
* Used by AgentDefaultsSettingsCard (settings panel) and AgentDefaultsSection
* (onboarding setup step). The parent manages load/save state; this component is
* purely presentational and calls onConfigChange on every user edit.
*/
import * as React from "react";
import { ChevronDown } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type {
BakedEnvEntry,
RuntimeFileConfigSubset,
} from "@/shared/api/tauri";
import type {
AcpRuntimeCatalogEntry,
GlobalAgentConfig,
} from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { EnvVarsEditor } from "@/features/agents/ui/EnvVarsEditor";
import type { InheritedEnvRow } from "@/features/agents/ui/EnvVarsEditor";
import {
deriveAgentConfigFieldModel,
getRenderableEffortField,
hasRenderableAgentConfigField,
structuredEnvKeys,
filterBakedGenericRows,
} from "@/features/agents/lib/agentConfigCore";
import {
getBakedProviderInheritLabel,
getGlobalModelFallback,
} from "@/features/agents/ui/bakedEnvHelpers";
import {
AUTO_PROVIDER_DROPDOWN_VALUE,
BLOCK_BUILD_HIDDEN_PROVIDER_IDS,
CARD_MINT_KEY_ANNOTATIONS,
CUSTOM_PROVIDER_DROPDOWN_VALUE,
getPersonaProviderOptions,
getProviderApiKeyEnvVar,
getProviderApiKeyLabel,
runtimeSupportsLlmProviderSelection,
} from "@/features/agents/ui/agentConfigOptions";
import {
AgentConfigTextInput,
AgentDropdownSelect,
AgentModelField,
} from "@/features/agents/ui/agentConfigControls";
import { PersonaProviderApiKeyField } from "@/features/agents/ui/PersonaProviderApiKeyField";
import { usePersonaModelDiscovery } from "@/features/agents/ui/usePersonaModelDiscovery";
import {
BUZZ_AGENT_THINKING_EFFORT,
getProviderEffortConfig,
} from "@/features/agents/ui/buzzAgentConfig";
import {
EffortSelectField,
NumericTuningFields,
useEffortAutoClear,
type NumericDescriptor,
} from "@/features/agents/ui/buzzAgentModelTuningFields";
import { SettingsOptionGroup } from "@/features/settings/ui/SettingsOptionGroup";
import { AdvancedRequiredBadge } from "./AdvancedRequiredBadge";
import { CardMintKeyCue } from "./CardMintKeyCue";
import { getGlobalAgentCredentialState } from "./globalAgentCredentialState";
export const EMPTY_GLOBAL_CONFIG: GlobalAgentConfig = {
env_vars: {},
provider: null,
model: null,
preferred_runtime: null,
};
const BAKED_STRUCTURED_KEYS = new Set([
"BUZZ_AGENT_PROVIDER",
"BUZZ_AGENT_MODEL",
BUZZ_AGENT_THINKING_EFFORT,
]);
const PROGRESSIVE_FIELDS_TRANSITION = {
duration: 0.22,
ease: [0.23, 1, 0.32, 1],
} as const;
type AgentConfigDisclosure =
| "full"
| "onboarding-essential"
| "progressive-defaults";
// Canonical behaviors (PR 2 flag cleanup). These were per-surface props;
// onboarding's values won every call and are now the only behavior:
// - auto-select a valid model when the provider changes
// - keep the model select usable during discovery
// - preserve credential env vars across provider switches (the abandoned
// provider's key stays in env_vars — visible/deletable under Advanced)
// - require a provider before model/effort are editable (no saveable
// invalid state — design principle #4)
const autoSelectModelOnProviderChange = true;
const disableModelSelectDuringDiscovery = false;
const preserveCredentialEnvVarsOnProviderChange = true;
const requireProviderForModelAndEffort = true;
/** The canonical behavior contract, exported for the contract test. */
export const CANONICAL_CONFIG_BEHAVIORS = {
autoSelectModelOnProviderChange,
disableModelSelectDuringDiscovery,
preserveCredentialEnvVarsOnProviderChange,
requireProviderForModelAndEffort,
} as const;
/** Disclosure preset → the eight visibility decisions it owns. Exported for the contract test. */
export function resolveDisclosure(disclosure: AgentConfigDisclosure) {
const full = disclosure !== "onboarding-essential";
return {
showAdvancedFields: full,
showCustomModelOption: full,
showCustomProviderOption: full,
showDescriptions: full,
showEffortField: true,
showProviderPlaceholderOption: full,
showRequiredIndicators: full,
showUnavailableEffortOptions: full,
} as const;
}
export function shouldRevealDependentConfigFields({
disclosure,
providerFieldVisible,
providerValue,
}: {
disclosure: AgentConfigDisclosure;
providerFieldVisible: boolean;
providerValue: string;
}): boolean {
return (
disclosure !== "progressive-defaults" ||
!providerFieldVisible ||
providerValue.trim().length > 0
);
}
/** Whether the status line under the Model field renders. Discovery warnings bypass onboarding-essential so first-run failures are never invisible. */
export function shouldShowModelStatusMessage(
showDescriptions: boolean,
status: { message: string; tone: string } | null,
): boolean {
return showDescriptions || status !== null;
}
/**
* Renders the Model control given discovery state. Optional-model harnesses omit it while
* discovery is loading or after confirmed successful empty; failures keep it for the #2246 UI.
*/
export function shouldRenderModelControl({
discoveredModelOptions,
modelDiscoveryLoading,
modelDiscoverySuccessfulEmpty,
modelIsOptional,
showCustomModelOption,
}: {
discoveredModelOptions: readonly { id: string }[] | null;
modelDiscoveryLoading: boolean;
/** True only when discovery IPC resolved with a response that yielded no options. */
modelDiscoverySuccessfulEmpty: boolean;
modelIsOptional: boolean;
showCustomModelOption: boolean;
}): boolean {
if (!modelIsOptional) return true;
if (modelDiscoveryLoading) return false;
const hasExplicitModel = (discoveredModelOptions ?? []).some(
(option) => option.id.trim().length > 0,
);
if (hasExplicitModel) return true;
if (showCustomModelOption) return true;
// Omit only on confirmed successful empty — not on failure/unavailable.
return !modelDiscoverySuccessfulEmpty;
}
export type AgentConfigFieldsProps = {
bakedEnv: BakedEnvEntry[];
selectedRuntime: AcpRuntimeCatalogEntry | undefined;
config: GlobalAgentConfig;
isCustomModelEditing: boolean;
isCustomProvider: boolean;
onConfigChange: (next: GlobalAgentConfig) => void;
onCustomModelEditingChange: (value: boolean) => void;
onIsCustomProviderChange: (value: boolean) => void;
onValidityChange?: (valid: boolean) => void;
runtimeFileConfig?: RuntimeFileConfigSubset | null;
placeholderClassName?: string;
selectClassName?: string;
/**
* Which disclosure preset to render (PR 2 flag cleanup — replaces eight
* independent show* booleans):
* - "full" (default): the evergreen stance — every field, escape hatch
* (custom model/provider), description, required indicator, and
* unavailable option is visible. Settings, defaults modal, dialogs.
* - "onboarding-essential": onboarding page 4's first-run stance — only
* valid forward choices. No advanced section, no custom escape hatches,
* no descriptions (the page copy does that job), no un-choosing via
* placeholder options, no greyed-out effort levels.
* - "progressive-defaults": the defaults modal's full controls, revealed in
* order. Provider appears after harness selection; model, effort, and
* Advanced appear after a provider is configured.
* If a second surface wants the trimmed view, rename this value to plain
* "essential" — and have the conversation about whether it should really
* match onboarding.
*/
disclosure?: AgentConfigDisclosure;
unstyled?: boolean;
useCustomSelect?: boolean;
useChevronSelectIcon?: boolean;
};
export function AgentConfigFields({
bakedEnv,
selectedRuntime,
config,
isCustomModelEditing,
isCustomProvider,
onConfigChange,
onCustomModelEditingChange,
onIsCustomProviderChange,
onValidityChange,
runtimeFileConfig,
placeholderClassName,
selectClassName,
disclosure = "full",
unstyled = false,
useCustomSelect = false,
useChevronSelectIcon = false,
}: AgentConfigFieldsProps) {
const shouldReduceMotion = useReducedMotion();
const {
showAdvancedFields,
showCustomModelOption,
showCustomProviderOption,
showDescriptions,
showEffortField,
showProviderPlaceholderOption,
showRequiredIndicators,
showUnavailableEffortOptions,
} = resolveDisclosure(disclosure);
const fieldModel = React.useMemo(
() =>
deriveAgentConfigFieldModel({
config,
runtime: selectedRuntime,
scope: disclosure === "onboarding-essential" ? "onboarding" : "global",
}),
[config, disclosure, selectedRuntime],
);
const effortField = getRenderableEffortField(fieldModel);
const effortPersistenceKey =
effortField?.currentPersistence.kind === "envVar"
? effortField.currentPersistence.key
: null;
const numericDescriptors = fieldModel.fields.filter(
(d): d is NumericDescriptor =>
(d.kind === "maxOutputTokens" ||
d.kind === "contextLimit" ||
d.kind === "maxRounds") &&
d.render === "control",
);
const allStructuredKeys = structuredEnvKeys([
...(effortField ? [effortField] : []),
...numericDescriptors,
]);
const bakedEnvMap = Object.fromEntries(bakedEnv.map((e) => [e.key, e.value]));
const bakedProvider = React.useMemo(
() => bakedEnv.find((e) => e.key === "BUZZ_AGENT_PROVIDER")?.value ?? null,
[bakedEnv],
);
const selectedRuntimeId = selectedRuntime?.id ?? "";
const providerFieldVisible = hasRenderableAgentConfigField(
fieldModel,
"provider",
);
const effectiveProvider = providerFieldVisible
? config.provider?.trim() || bakedProvider || ""
: "";
const fallbackModel = React.useMemo(
() => getGlobalModelFallback(bakedEnv, effectiveProvider, config.env_vars),
[bakedEnv, config.env_vars, effectiveProvider],
);
const modelField = fieldModel.fields.find(
(field) => field.kind === "model" && field.render === "control",
);
// CLI-login harnesses apply this setting through ACP rather than an env var
// and provide their own default when no model override is persisted.
const modelIsOptional = modelField?.targetApplication.kind === "acpNative";
const modelIsValid =
modelIsOptional ||
(config.model?.trim().length ?? 0) > 0 ||
fallbackModel !== null;
const bakedEffort = React.useMemo(
() =>
bakedEnv.find((e) => e.key === BUZZ_AGENT_THINKING_EFFORT)?.value ?? null,
[bakedEnv],
);
const bakedGenericRows = React.useMemo<readonly InheritedEnvRow[]>(
() =>
filterBakedGenericRows(bakedEnv, [
...BAKED_STRUCTURED_KEYS,
...allStructuredKeys,
]),
[bakedEnv, allStructuredKeys],
);
const providerValue = providerFieldVisible ? (config.provider ?? "") : "";
const providerForDiscovery =
providerFieldVisible && !isCustomProvider
? providerValue || bakedProvider || ""
: "";
const configuredProviderValue = isCustomProvider
? providerValue
: providerForDiscovery;
const dependentFieldsDisabled =
providerFieldVisible &&
requireProviderForModelAndEffort &&
configuredProviderValue.trim().length === 0;
const revealDependentFields = shouldRevealDependentConfigFields({
disclosure,
providerFieldVisible,
providerValue: configuredProviderValue,
});
const credentialProvider =
providerFieldVisible && !isCustomProvider ? effectiveProvider : "";
const credentialRuntimeId = runtimeSupportsLlmProviderSelection(
selectedRuntimeId,
)
? selectedRuntimeId
: "buzz-agent";
const bakedEnvKeys = React.useMemo(
() => bakedEnv.map((entry) => entry.key),
[bakedEnv],
);
const {
advancedCredentialMissing,
advancedFileSatisfiedEnvKeys,
advancedRequiredEnvKeys,
apiKeyEnvVar,
apiKeyFileSatisfied,
apiKeyInherited,
apiKeyValue,
credentialsValid,
} = getGlobalAgentCredentialState({
bakedEnvKeys,
envVars: config.env_vars,
provider: credentialProvider,
runtimeFileConfig,
runtimeId: credentialRuntimeId,
});
const configIsValid =
selectedRuntimeId.length > 0 && modelIsValid && credentialsValid;
React.useEffect(() => {
onValidityChange?.(configIsValid);
}, [configIsValid, onValidityChange]);
const {
discoveredModelOptions,
modelDiscoveryLoading,
modelDiscoveryStatus,
modelDiscoverySuccessfulEmpty,
} = usePersonaModelDiscovery({
envVars: config.env_vars,
isCustomProviderEditing: isCustomProvider,
modelFieldVisible: !dependentFieldsDisabled,
open: true,
provider: providerForDiscovery,
selectedRuntime,
});
const modelControlVisible = shouldRenderModelControl({
discoveredModelOptions: dependentFieldsDisabled
? null
: discoveredModelOptions,
modelDiscoveryLoading: dependentFieldsDisabled
? false
: modelDiscoveryLoading,
modelDiscoverySuccessfulEmpty:
!dependentFieldsDisabled && modelDiscoverySuccessfulEmpty,
modelIsOptional,
showCustomModelOption,
});
// Mount-time healing policy: onboarding page 4 edits the root config during
// first-run (no higher layers to inherit from), so acting on open is safe
// and intentional there — it heals stale state and picks a valid model.
// Evergreen surfaces (Settings, dialogs) edit saved data that may pair with
// higher layers (see PR #2148 review thread), so they only act after the
// user explicitly edits the provider in this session.
const healOnMount =
fieldModel.dependentValuePolicy.onCatalogMismatch === "onboardingCleanup";
const userEditedProviderRef = React.useRef(false);
// Advanced visibility is user-controlled. Provider changes can add required
// rows, but must not open this section without an explicit toggle click.
const [advancedOpen, setAdvancedOpen] = React.useState(false);
// Read inside effects via ref so biome's exhaustive-deps stays honest:
// refs are stable, and healOnMount is captured at declaration.
const mayMutateDependentFieldsRef = React.useRef(false);
mayMutateDependentFieldsRef.current =
healOnMount || userEditedProviderRef.current;
const autoSelectedModelScopeRef = React.useRef<string | null>(null);
React.useEffect(() => {
if (!autoSelectModelOnProviderChange) return;
if (!mayMutateDependentFieldsRef.current) return;
const trimmedProvider = providerForDiscovery.trim();
if (trimmedProvider.length === 0 || isCustomProvider) {
autoSelectedModelScopeRef.current = null;
return;
}
if ((config.model ?? "").trim().length > 0) return;
if (modelDiscoveryLoading || discoveredModelOptions === null) return;
const selectionScope = `${selectedRuntimeId}:${trimmedProvider}`;
if (autoSelectedModelScopeRef.current === selectionScope) return;
const firstModel = discoveredModelOptions.find(
(option) => option.id.trim().length > 0,
);
if (!firstModel) return;
autoSelectedModelScopeRef.current = selectionScope;
onCustomModelEditingChange(false);
onConfigChange({ ...config, model: firstModel.id });
}, [
config,
discoveredModelOptions,
isCustomProvider,
modelDiscoveryLoading,
onConfigChange,
onCustomModelEditingChange,
providerForDiscovery,
selectedRuntimeId,
]);
const currentEffortForAutoClear = effortPersistenceKey
? (config.env_vars[effortPersistenceKey] ?? "")
: "";
// When the selected harness changes outside this component (Back → setup
// page → choose a different harness → Next), the saved model can belong to
// the old harness. In onboarding, heal that stale value as soon as the new
// harness catalog proves it is unsupported; otherwise a Codex id like
// `gpt-5.5[low]` appears as a Claude Code custom model.
// Also clear when the Model control is omitted after a confirmed successful
// empty catalog — never while discovery failed/unavailable (transient
// failures must not erase saved model/effort).
React.useEffect(() => {
if (!healOnMount) return;
const currentModel = (config.model ?? "").trim();
if (currentModel.length === 0) return;
if (modelDiscoveryLoading) return;
const catalogMiss =
discoveredModelOptions !== null &&
!discoveredModelOptions.some(
(option) => option.id.trim() === currentModel,
);
const omittedAfterSuccessfulEmpty =
modelIsOptional && !modelControlVisible && modelDiscoverySuccessfulEmpty;
if (!catalogMiss && !omittedAfterSuccessfulEmpty) return;
const nextEnvVars = { ...config.env_vars };
if (effortPersistenceKey) delete nextEnvVars[effortPersistenceKey];
onCustomModelEditingChange(false);
onConfigChange({ ...config, env_vars: nextEnvVars, model: null });
}, [
config,
discoveredModelOptions,
modelControlVisible,
modelDiscoveryLoading,
modelDiscoverySuccessfulEmpty,
modelIsOptional,
onConfigChange,
onCustomModelEditingChange,
healOnMount,
effortPersistenceKey,
]);
// Orphan-model clearing follows the mount-time healing policy above: the
// backend resolves provider and model independently across layers
// (agent → definition → global), so a saved global model WITHOUT a global
// provider can be a deliberate, working pattern (provider supplied by a
// higher layer). Clearing it on page-open in evergreen surfaces silently
// breaks that agent on its next restart — see PR #2148 review thread.
// Onboarding heals on open by design (discriminating spec: "gates stale
// saved model and effort until provider selection").
React.useEffect(() => {
if (!mayMutateDependentFieldsRef.current) return;
if (!dependentFieldsDisabled) return;
if (
(config.model ?? "").trim().length === 0 &&
currentEffortForAutoClear.length === 0
) {
return;
}
const nextEnvVars = { ...config.env_vars };
if (effortPersistenceKey) delete nextEnvVars[effortPersistenceKey];
onCustomModelEditingChange(false);
onConfigChange({ ...config, env_vars: nextEnvVars, model: null });
}, [
config,
currentEffortForAutoClear,
dependentFieldsDisabled,
onConfigChange,
onCustomModelEditingChange,
effortPersistenceKey,
]);
const { validValues: effortValidForAutoClear } = getProviderEffortConfig(
config.provider ?? "",
config.model ?? "",
);
useEffortAutoClear({
currentEffort: currentEffortForAutoClear,
effortValid: effortValidForAutoClear,
onClear: () => {
const nextEnvVars = { ...config.env_vars };
if (effortPersistenceKey) delete nextEnvVars[effortPersistenceKey];
onConfigChange({ ...config, env_vars: nextEnvVars });
},
});
function handleProviderChange(value: string) {
userEditedProviderRef.current = true;
const previousApiKey = getProviderApiKeyEnvVar(effectiveProvider);
if (value === CUSTOM_PROVIDER_DROPDOWN_VALUE) {
const nextEnvVars = { ...config.env_vars };
if (!preserveCredentialEnvVarsOnProviderChange && previousApiKey) {
delete nextEnvVars[previousApiKey];
}
onIsCustomProviderChange(true);
onConfigChange({ ...config, env_vars: nextEnvVars, provider: null });
return;
}
const nextProvider =
value === AUTO_PROVIDER_DROPDOWN_VALUE || value === "" ? null : value;
const nextApiKey = getProviderApiKeyEnvVar(
nextProvider ?? bakedProvider ?? "",
);
const nextEnvVars = { ...config.env_vars };
if (
!preserveCredentialEnvVarsOnProviderChange &&
previousApiKey &&
previousApiKey !== nextApiKey
) {
delete nextEnvVars[previousApiKey];
}
const providerChanged = nextProvider !== (config.provider ?? null);
onIsCustomProviderChange(false);
onConfigChange({
...config,
env_vars: nextEnvVars,
provider: nextProvider,
model:
nextProvider === "relay-mesh"
? config.model || "auto"
: autoSelectModelOnProviderChange && providerChanged
? null
: config.model,
});
}
function handleCustomProviderInput(value: string) {
onConfigChange({ ...config, provider: value || null });
}
function handleModelChange(value: string) {
onConfigChange({
...config,
model: config.provider === "relay-mesh" ? value || "auto" : value || null,
});
}
function handleEnvVarsChange(next: Record<string, string>) {
onConfigChange({ ...config, env_vars: next });
}
const handleNumericEnvVarChange = (key: string, value: string) => {
const next = { ...config.env_vars, [key]: value };
if (value === "") delete next[key];
onConfigChange({ ...config, env_vars: next });
};
// On internal Block builds, BUZZ_AGENT_PROVIDER is baked in and a boot
// migration rewrites v1→v2. Hide the legacy v1 option so it is not offered
// for new selections; OSS builds show it.
const hideProviderIds = React.useMemo(() => {
const hidden = new Set<string>();
if (bakedEnvKeys.includes("BUZZ_AGENT_PROVIDER")) {
for (const providerId of BLOCK_BUILD_HIDDEN_PROVIDER_IDS) {
hidden.add(providerId);
}
}
if (selectedRuntimeId !== "buzz-agent") {
hidden.add("relay-mesh");
}
return hidden;
}, [bakedEnvKeys, selectedRuntimeId]);
const providerOptions = getPersonaProviderOptions(
providerValue,
credentialRuntimeId,
undefined,
hideProviderIds,
);
const providerSelectValue = isCustomProvider
? CUSTOM_PROVIDER_DROPDOWN_VALUE
: providerValue || AUTO_PROVIDER_DROPDOWN_VALUE;
const providerZeroLabel = React.useMemo(() => {
if (!bakedProvider) return null;
return getBakedProviderInheritLabel(bakedProvider, providerOptions);
}, [bakedProvider, providerOptions]);
const compactProviderZeroLabel = React.useMemo(() => {
if (bakedProvider) {
return (
providerOptions.find((option) => option.id === bakedProvider)?.label ??
bakedProvider
);
}
return "Select a provider";
}, [bakedProvider, providerOptions]);
const implicitEffortProvider =
selectedRuntimeId === "claude"
? "anthropic"
: selectedRuntimeId === "codex"
? "openai"
: "";
const effortProvider = providerFieldVisible
? (config.provider ?? "")
: implicitEffortProvider;
const { validValues: effortValid, defaultValue: effortDefault } =
getProviderEffortConfig(effortProvider, config.model ?? "");
const currentEffort = effortPersistenceKey
? (config.env_vars[effortPersistenceKey] ?? "")
: "";
const effortFieldVisible = showEffortField && effortField !== undefined;
const progressiveDefaults = disclosure === "progressive-defaults";
const fieldClassName = unstyled
? progressiveDefaults
? "space-y-1.5"
: "space-y-4"
: "space-y-1.5 p-3";
const blockClassName = unstyled ? "" : "p-3";
const fieldLabelClassName =
unstyled && !progressiveDefaults ? "pl-3" : undefined;
const providerDropdownOptions = [
...providerOptions
.filter(
(opt) =>
showProviderPlaceholderOption ||
opt.id !== "" ||
providerSelectValue === AUTO_PROVIDER_DROPDOWN_VALUE,
)
.map((opt) => ({
label:
opt.id === ""
? showProviderPlaceholderOption
? (providerZeroLabel ?? opt.label)
: compactProviderZeroLabel
: opt.label,
value: opt.id || AUTO_PROVIDER_DROPDOWN_VALUE,
})),
...(showCustomProviderOption
? [{ label: "Custom provider…", value: CUSTOM_PROVIDER_DROPDOWN_VALUE }]
: []),
];
const providerSelect = useCustomSelect ? (
<AgentDropdownSelect
className={selectClassName}
id="global-agent-provider"
onValueChange={handleProviderChange}
options={providerDropdownOptions}
placeholder={
showProviderPlaceholderOption
? "Select provider"
: compactProviderZeroLabel
}
placeholderClassName={placeholderClassName}
placeholderValue={
!showProviderPlaceholderOption && !bakedProvider
? AUTO_PROVIDER_DROPDOWN_VALUE
: undefined
}
testId="global-agent-provider"
value={providerSelectValue}
/>
) : (
<select
className={cn(
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs",
useChevronSelectIcon && "appearance-none pr-10",
selectClassName,
)}
id="global-agent-provider"
onChange={(e) => handleProviderChange(e.target.value)}
value={providerSelectValue}
>
{providerDropdownOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
const providerContent = providerFieldVisible ? (
<div className={fieldClassName}>
<label
className={cn("text-sm font-medium", fieldLabelClassName)}
htmlFor="global-agent-provider"
>
Provider
</label>
{!useCustomSelect && useChevronSelectIcon ? (
<div className="relative">
{providerSelect}
<ChevronDown
aria-hidden="true"
className="pointer-events-none absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-foreground"
/>
</div>
) : (
providerSelect
)}
{isCustomProvider ? (
<AgentConfigTextInput
aria-label="Custom global provider ID"
autoCorrect="off"
onChange={(e) => handleCustomProviderInput(e.target.value)}
placeholder="Custom provider ID"
usePersonaInputStyle={progressiveDefaults}
value={providerValue}
/>
) : null}
</div>
) : null;
const advancedEditorBlock = (
<>
<EnvVarsEditor
fileSatisfiedKeys={advancedFileSatisfiedEnvKeys}
hiddenKeys={[
...(apiKeyEnvVar ? [apiKeyEnvVar] : []),
...allStructuredKeys,
]}
inheritedRows={bakedGenericRows}
inheritedRowsLabel="build"
keyAnnotations={CARD_MINT_KEY_ANNOTATIONS}
label="Environment variables"
onChange={handleEnvVarsChange}
requiredKeys={advancedRequiredEnvKeys}
value={config.env_vars}
/>
{numericDescriptors.length > 0 ? (
<NumericTuningFields
descriptors={numericDescriptors}
envVars={config.env_vars}
inheritedEnvVars={bakedEnvMap}
onEnvVarChange={handleNumericEnvVarChange}
/>
) : null}
</>
);
const dependentContent = (
<>
{providerFieldVisible && apiKeyEnvVar ? (
<div className={blockClassName}>
<PersonaProviderApiKeyField
disabled={false}
envVarName={apiKeyEnvVar}
inheritedLabel={
apiKeyFileSatisfied
? "Set in runtime config"
: "Provided by this build"
}
isInherited={apiKeyInherited}
isRequired={!apiKeyInherited && apiKeyValue.length === 0}
label={getProviderApiKeyLabel(effectiveProvider) ?? "API Key"}
onValueChange={(value) =>
onConfigChange({
...config,
env_vars: { ...config.env_vars, [apiKeyEnvVar]: value },
})
}
value={apiKeyValue}
/>
</div>
) : null}
{/* Model field — omitted only after confirmed successful empty discovery */}
{modelControlVisible ? (
<div className={showDescriptions ? fieldClassName : undefined}>
<AgentModelField
allowDefaultModel={fallbackModel !== null}
defaultModelLabel={
fallbackModel ? `Default model (${fallbackModel})` : undefined
}
disableSelectDuringDiscovery={disableModelSelectDuringDiscovery}
disabled={dependentFieldsDisabled}
discoveredModelOptions={
dependentFieldsDisabled ? null : discoveredModelOptions
}
globalModel={fallbackModel ?? undefined}
id="global-agent-model"
isCustomModelEditing={isCustomModelEditing}
isRequired={
showRequiredIndicators &&
!modelIsOptional &&
fallbackModel === null &&
!dependentFieldsDisabled
}
keepSelectedModelValueLabel
model={dependentFieldsDisabled ? "" : (config.model ?? "")}
modelDiscoveryLoading={
dependentFieldsDisabled ? false : modelDiscoveryLoading
}
modelDiscoveryStatus={
dependentFieldsDisabled ? null : modelDiscoveryStatus
}
onIsCustomModelEditingChange={onCustomModelEditingChange}
onModelChange={handleModelChange}
placeholderClassName={placeholderClassName}
placeholder="Select a model"
provider={providerForDiscovery}
fieldClassName={unstyled ? fieldClassName : undefined}
labelClassName={fieldLabelClassName}
selectClassName={selectClassName}
showCustomModelOption={showCustomModelOption}
showStatusMessage={shouldShowModelStatusMessage(
showDescriptions,
dependentFieldsDisabled ? null : modelDiscoveryStatus,
)}
testId="global-agent-model"
useCustomSelect={useCustomSelect}
useChevronIcon={useChevronSelectIcon}
usePersonaInputStyle={progressiveDefaults}
/>
</div>
) : null}
{/* Thinking / Effort */}
{effortFieldVisible ? (
<div className={blockClassName}>
<EffortSelectField
currentEffort={dependentFieldsDisabled ? "" : currentEffort}
disabled={dependentFieldsDisabled}
emptyOptionLabel={
// Semantic, not copy: onboarding-essential hides inheritance
// concepts (first-run users pick, they don't inherit), so the
// zero option is a plain placeholder. Full disclosure leaves
// this unset so EffortSelectField computes the inherit/default
// label ("Default (medium)", "Inherit (high)", …).
disclosure === "onboarding-essential"
? "Select effort level"
: undefined
}
effortDefault={effortDefault}
effortValid={effortValid}
fieldClassName={unstyled ? fieldClassName : undefined}
htmlFor="global-agent-thinking-effort"
inheritFallbackLabel={
effortDefault !== null ? `Default (${effortDefault})` : undefined
}
inheritedEffort={bakedEffort ?? undefined}
label="Effort"
labelClassName={fieldLabelClassName}
onChange={(value) => {
const nextEnvVars = { ...config.env_vars };
if (value === "") {
if (effortPersistenceKey)
delete nextEnvVars[effortPersistenceKey];
} else {
if (effortPersistenceKey)
nextEnvVars[effortPersistenceKey] = value;
}
onConfigChange({ ...config, env_vars: nextEnvVars });
}}
placeholderClassName={placeholderClassName}
selectClassName={selectClassName}
showUnavailableOptions={showUnavailableEffortOptions}
testId="global-agent-thinking-effort-select"
useCustomSelect={useCustomSelect}
/>
</div>
) : null}
{showAdvancedFields ? (
<div className={cn(blockClassName, "space-y-3")}>
<CardMintKeyCue envVars={config.env_vars} />
<button
aria-expanded={advancedOpen}
className={cn(
"inline-flex h-9 items-center gap-1.5 text-sm font-medium text-foreground transition-colors hover:text-foreground/80 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
unstyled && "ml-3",
)}
data-testid="global-agent-advanced-toggle"
onClick={() => setAdvancedOpen((current) => !current)}
type="button"
>
<span>Advanced</span>
<AdvancedRequiredBadge
show={advancedCredentialMissing}
testId="global-agent-advanced-required-badge"
/>
<ChevronDown
className={cn(
"h-4 w-4 text-muted-foreground transition-transform duration-150 ease-out",
advancedOpen && "rotate-180",
)}
/>
</button>
{disclosure === "progressive-defaults" ? (
<AnimatePresence initial={false}>
{advancedOpen ? (
<motion.div
animate={{ height: "auto", opacity: 1 }}
className="overflow-hidden"
data-testid="global-agent-advanced-fields-motion"
exit={{ height: 0, opacity: 0 }}
initial={{ height: 0, opacity: 0 }}
key="global-agent-advanced-fields"
transition={
shouldReduceMotion
? { duration: 0 }
: PROGRESSIVE_FIELDS_TRANSITION
}
>
{advancedEditorBlock}
</motion.div>
) : null}
</AnimatePresence>
) : advancedOpen ? (
advancedEditorBlock
) : null}
</div>
) : null}
</>
);
const content = (
<>
{providerContent}
{disclosure === "progressive-defaults" ? (
<AnimatePresence initial={false}>
{revealDependentFields ? (
<motion.div
animate={{ height: "auto", opacity: 1 }}
className={cn(
"overflow-hidden",
unstyled && (progressiveDefaults ? "space-y-5" : "space-y-7"),
)}
data-testid="global-agent-dependent-fields-motion"
exit={{ height: 0, opacity: 0 }}
initial={{ height: 0, opacity: 0 }}
key="global-agent-dependent-fields"
transition={
shouldReduceMotion
? { duration: 0 }
: PROGRESSIVE_FIELDS_TRANSITION
}
>
{dependentContent}
</motion.div>
) : null}
</AnimatePresence>
) : (
dependentContent
)}
</>
);
if (unstyled) {
return (
<div
className={progressiveDefaults ? "space-y-5" : "space-y-7"}
data-testid="global-agent-config-fields"
>
{content}
</div>
);
}
return (
<SettingsOptionGroup data-testid="global-agent-config-fields">
{content}
</SettingsOptionGroup>
);
}