mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): thread baked build env into global agent config UI (#1722)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent
febbb06909
commit
2f2ad409a7
@@ -303,7 +303,9 @@ const overrides = new Map([
|
||||
// global-agent-config: get_agent_config_surface / write_agent_config_field /
|
||||
// put_agent_session_config commands + GlobalAgentConfig serde types. New file
|
||||
// in this PR; queued to split with the command module refactor.
|
||||
["src-tauri/src/commands/agent_config.rs", 1002],
|
||||
// +17: baked-env-global-unify: BUZZ_AGENT_THINKING_EFFORT added to
|
||||
// is_safe_to_reveal allowlist + baked_env_thinking_effort_is_unmasked test.
|
||||
["src-tauri/src/commands/agent_config.rs", 1019],
|
||||
// draft-persistence predicate: submit-time `loadDraft` check + inline comment
|
||||
// + deps-array entry in submitMessage closes the never-persisted-boundary
|
||||
// defect (Thufir Pass-3 finding). Load-bearing correctness fix; queued to
|
||||
|
||||
@@ -275,11 +275,13 @@ pub struct BakedEnvEntry {
|
||||
///
|
||||
/// Allowlist (case-insensitive):
|
||||
/// - `BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL` — agent runtime selection
|
||||
/// - `BUZZ_AGENT_THINKING_EFFORT` — non-secret enum (none/minimal/low/medium/high/xhigh/max)
|
||||
/// - `DATABRICKS_HOST`, `DATABRICKS_MODEL` — Block non-secret defaults
|
||||
fn is_safe_to_reveal(key: &str) -> bool {
|
||||
const SAFE_KEYS: &[&str] = &[
|
||||
"BUZZ_AGENT_PROVIDER",
|
||||
"BUZZ_AGENT_MODEL",
|
||||
"BUZZ_AGENT_THINKING_EFFORT",
|
||||
"DATABRICKS_HOST",
|
||||
"DATABRICKS_MODEL",
|
||||
];
|
||||
@@ -973,6 +975,19 @@ mod tests {
|
||||
assert!(token.masked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baked_env_thinking_effort_is_unmasked() {
|
||||
// BUZZ_AGENT_THINKING_EFFORT is a non-secret enum — must not be masked.
|
||||
let entries = baked_env_from_map(&[("BUZZ_AGENT_THINKING_EFFORT", "medium")]);
|
||||
assert_eq!(entries.len(), 1);
|
||||
let effort = entries
|
||||
.iter()
|
||||
.find(|e| e.key == "BUZZ_AGENT_THINKING_EFFORT")
|
||||
.unwrap();
|
||||
assert_eq!(effort.value, "medium");
|
||||
assert!(!effort.masked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baked_env_allowlist_is_case_insensitive() {
|
||||
// Known-safe keys — case-insensitive match must allow them.
|
||||
@@ -980,6 +995,8 @@ mod tests {
|
||||
assert!(super::is_safe_to_reveal("BUZZ_AGENT_PROVIDER"));
|
||||
assert!(super::is_safe_to_reveal("buzz_agent_model"));
|
||||
assert!(super::is_safe_to_reveal("BUZZ_AGENT_MODEL"));
|
||||
assert!(super::is_safe_to_reveal("buzz_agent_thinking_effort"));
|
||||
assert!(super::is_safe_to_reveal("BUZZ_AGENT_THINKING_EFFORT"));
|
||||
assert!(super::is_safe_to_reveal("databricks_host"));
|
||||
assert!(super::is_safe_to_reveal("DATABRICKS_HOST"));
|
||||
assert!(super::is_safe_to_reveal("databricks_model"));
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
* the emitted record (buildRecord merges required keys from value).
|
||||
* 3. Provider/runtime switch (skipKeys change) triggers a row reprojection
|
||||
* — the guard fires when skipKeys changes, even if value is unchanged.
|
||||
* 4. inheritedRows: render/exclusion/override/no-serialize invariants.
|
||||
* 5. getBakedProviderInheritLabel: label helper correctness.
|
||||
*
|
||||
* These are pure-logic tests — no React renderer needed. The transition tests
|
||||
* (Invariant 3) exercise the real exported `skipKeysEqual` guard that controls
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
skipKeysEqual,
|
||||
isRequiredKeyMissing,
|
||||
} from "./EnvVarsEditor.tsx";
|
||||
import { getBakedProviderInheritLabel } from "./bakedEnvHelpers.ts";
|
||||
|
||||
// ── Invariant 1: toRows excludes skip keys ─────────────────────────────────
|
||||
|
||||
@@ -352,3 +355,160 @@ test("isRequiredKeyMissing_keyExplicitlyEmpty_noInherited_missing", () => {
|
||||
"explicit empty local value with no inherited must be missing",
|
||||
);
|
||||
});
|
||||
|
||||
// ── Invariant 4: inheritedRows — display/exclusion/override/no-serialize ───
|
||||
//
|
||||
// These tests exercise the pure-logic invariants that must hold for the
|
||||
// inherited build-defaults feature in EnvVarsEditor:
|
||||
//
|
||||
// (a) An inherited row with no matching local row IS visible (would render).
|
||||
// (b) An inherited row whose key appears in `rows` is HIDDEN (local wins).
|
||||
// (c) Serialization (buildRecord) NEVER includes inherited-only rows.
|
||||
// (d) A local override row for a masked secret shows the masked value.
|
||||
//
|
||||
// Tests (a)–(d) operate on the same helpers used by the component render
|
||||
// (toRows, toRecord, buildRecord) plus a simulated filter that mirrors the
|
||||
// JSX `.filter((irow) => !rows.some((r) => r.key === irow.key))`.
|
||||
|
||||
function simulateInheritedFilter(inheritedRows, rows) {
|
||||
return inheritedRows.filter((irow) => !rows.some((r) => r.key === irow.key));
|
||||
}
|
||||
|
||||
test("inheritedRows_no_local_row_row_is_visible", () => {
|
||||
// DATABRICKS_HOST baked, no local row → inherited row shows.
|
||||
const inherited = [
|
||||
{
|
||||
key: "DATABRICKS_HOST",
|
||||
value: "https://example.databricks.com/",
|
||||
masked: false,
|
||||
},
|
||||
];
|
||||
const rows = toRows({}, new Set()); // no local env vars
|
||||
const visible = simulateInheritedFilter(inherited, rows);
|
||||
assert.equal(
|
||||
visible.length,
|
||||
1,
|
||||
"inherited row must be visible when no local override",
|
||||
);
|
||||
assert.equal(visible[0].key, "DATABRICKS_HOST");
|
||||
});
|
||||
|
||||
test("inheritedRows_local_row_same_key_inherited_hidden", () => {
|
||||
// User adds a local DATABRICKS_HOST row → inherited row must be hidden.
|
||||
const inherited = [
|
||||
{
|
||||
key: "DATABRICKS_HOST",
|
||||
value: "https://baked.databricks.com/",
|
||||
masked: false,
|
||||
},
|
||||
];
|
||||
const value = { DATABRICKS_HOST: "https://user.databricks.com/" };
|
||||
const rows = toRows(value, new Set());
|
||||
const visible = simulateInheritedFilter(inherited, rows);
|
||||
assert.equal(
|
||||
visible.length,
|
||||
0,
|
||||
"inherited row must be hidden when local row has same key",
|
||||
);
|
||||
});
|
||||
|
||||
test("inheritedRows_not_serialized_in_buildRecord", () => {
|
||||
// buildRecord must never include keys that come only from inherited rows.
|
||||
// Simulate: inherited has SECRET_KEY, local value does not.
|
||||
const requiredKeys = [];
|
||||
const value = { MY_VAR: "foo" };
|
||||
const rows = toRows(value, new Set(requiredKeys));
|
||||
// buildRecord reimplemented inline to match EnvVarsEditor's buildRecord:
|
||||
const base = {};
|
||||
for (const key of requiredKeys) {
|
||||
if (key in value) base[key] = value[key];
|
||||
}
|
||||
const record = { ...base, ...toRecord(rows) };
|
||||
assert.equal(
|
||||
"SECRET_KEY" in record,
|
||||
false,
|
||||
"inherited-only key must NOT appear in serialized record",
|
||||
);
|
||||
assert.equal(record.MY_VAR, "foo", "non-inherited key preserved");
|
||||
});
|
||||
|
||||
test("inheritedRows_masked_secret_local_override_shows_masked_build_value", () => {
|
||||
// Edge case: baked key is a masked secret (e.g. API_KEY → "••••••"),
|
||||
// user types a local override → the hint would show the masked "••••••" value.
|
||||
const inherited = [{ key: "API_KEY", value: "••••••", masked: true }];
|
||||
// The component finds override by: inheritedRows.find(irow => irow.key === row.key)
|
||||
const row = { id: "r1", key: "API_KEY", value: "my-real-key" };
|
||||
const override = inherited.find((irow) => irow.key === row.key);
|
||||
assert.ok(override, "override entry must be found for masked key");
|
||||
assert.equal(
|
||||
override.value,
|
||||
"••••••",
|
||||
"masked baked value shown in override hint",
|
||||
);
|
||||
assert.equal(override.masked, true, "masked flag preserved");
|
||||
});
|
||||
|
||||
test("inheritedRows_structured_keys_excluded_from_generic_rows", () => {
|
||||
// BUZZ_AGENT_PROVIDER, BUZZ_AGENT_MODEL, BUZZ_AGENT_THINKING_EFFORT must
|
||||
// be excluded from bakedGenericRows (they go to structured fields instead).
|
||||
// This mirrors the BAKED_STRUCTURED_KEYS filter in GlobalAgentConfigSettingsCard.
|
||||
const STRUCTURED = new Set([
|
||||
"BUZZ_AGENT_PROVIDER",
|
||||
"BUZZ_AGENT_MODEL",
|
||||
"BUZZ_AGENT_THINKING_EFFORT",
|
||||
]);
|
||||
const allBaked = [
|
||||
{ key: "BUZZ_AGENT_PROVIDER", value: "databricks_v2", masked: false },
|
||||
{ key: "BUZZ_AGENT_MODEL", value: "goose-claude-opus-4-8", masked: false },
|
||||
{ key: "BUZZ_AGENT_THINKING_EFFORT", value: "medium", masked: false },
|
||||
{
|
||||
key: "DATABRICKS_HOST",
|
||||
value: "https://example.databricks.com/",
|
||||
masked: false,
|
||||
},
|
||||
{ key: "DATABRICKS_MODEL", value: "goose-claude-opus-4-8", masked: false },
|
||||
];
|
||||
const generic = allBaked.filter((e) => !STRUCTURED.has(e.key));
|
||||
assert.equal(
|
||||
generic.length,
|
||||
2,
|
||||
"only non-structured keys go to generic rows",
|
||||
);
|
||||
const genericKeys = generic.map((e) => e.key).sort();
|
||||
assert.deepEqual(genericKeys, ["DATABRICKS_HOST", "DATABRICKS_MODEL"]);
|
||||
});
|
||||
|
||||
// ── Invariant 5: getBakedProviderInheritLabel — label helper ───────────────
|
||||
|
||||
test("getBakedProviderInheritLabel_known_provider_returns_friendly_name", () => {
|
||||
const options = [
|
||||
{ id: "anthropic", label: "Anthropic" },
|
||||
{ id: "databricks_v2", label: "Databricks v2 (AI Gateway)" },
|
||||
{ id: "openai", label: "OpenAI" },
|
||||
];
|
||||
const label = getBakedProviderInheritLabel("databricks_v2", options);
|
||||
assert.equal(
|
||||
label,
|
||||
"Databricks v2 (AI Gateway) (inherited from build)",
|
||||
"known provider id must resolve to friendly label",
|
||||
);
|
||||
});
|
||||
|
||||
test("getBakedProviderInheritLabel_unknown_provider_falls_back_to_raw_id", () => {
|
||||
const options = [{ id: "anthropic", label: "Anthropic" }];
|
||||
const label = getBakedProviderInheritLabel("my-custom-provider", options);
|
||||
assert.equal(
|
||||
label,
|
||||
"my-custom-provider (inherited from build)",
|
||||
"unknown provider id must fall back to raw id",
|
||||
);
|
||||
});
|
||||
|
||||
test("getBakedProviderInheritLabel_empty_options_falls_back_to_raw_id", () => {
|
||||
const label = getBakedProviderInheritLabel("databricks_v2", []);
|
||||
assert.equal(
|
||||
label,
|
||||
"databricks_v2 (inherited from build)",
|
||||
"empty options table must fall back to raw id",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,18 @@ import {
|
||||
|
||||
export type EnvVarsValue = Record<string, string>;
|
||||
|
||||
/**
|
||||
* A single baked / build-time env row passed as an inherited default.
|
||||
* The value is already masked server-side for secrets (`masked === true`).
|
||||
*/
|
||||
export type InheritedEnvRow = {
|
||||
key: string;
|
||||
/** Display value — real value or `••••••` for masked keys. */
|
||||
value: string;
|
||||
/** True when Rust replaced the real value with the mask placeholder. */
|
||||
masked: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a rows array from a value record, optionally skipping a set of keys.
|
||||
* Exported for unit tests.
|
||||
@@ -133,6 +145,21 @@ type EnvVarsEditorProps = {
|
||||
* it is set. Only acts on keys that appear in `requiredKeys`.
|
||||
*/
|
||||
focusKey?: string;
|
||||
/**
|
||||
* Read-only rows for baked / build-time inherited defaults. Displayed after
|
||||
* required rows and before user-managed rows. A local row whose key matches
|
||||
* an inherited row takes precedence — the inherited row is hidden and an
|
||||
* "Overrides build default" hint is shown beneath the local row instead.
|
||||
*
|
||||
* **Invariant:** these rows are purely display; they never enter `rows` state
|
||||
* and are never included in `buildRecord` / `onChange` output. Nothing baked
|
||||
* is ever written into `global-agent-config.json`.
|
||||
*
|
||||
* Opt-in — agent create/edit dialogs do not pass this prop and are untouched.
|
||||
*/
|
||||
inheritedRows?: readonly InheritedEnvRow[];
|
||||
/** Label for the inherited-row tag (e.g. "build"). Defaults to "build". */
|
||||
inheritedRowsLabel?: string;
|
||||
};
|
||||
|
||||
type Row = { id: string; key: string; value: string };
|
||||
@@ -156,6 +183,8 @@ export function EnvVarsEditor({
|
||||
requiredKeys = [],
|
||||
fileSatisfiedKeys = [],
|
||||
focusKey,
|
||||
inheritedRows = [],
|
||||
inheritedRowsLabel = "build",
|
||||
}: EnvVarsEditorProps) {
|
||||
// Keys that render as their own special rows (required amber rows or
|
||||
// file-satisfied read-only rows). These must NEVER enter `rows` state —
|
||||
@@ -417,10 +446,64 @@ export function EnvVarsEditor({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Inherited baked-build rows — read-only, visible value (pre-masked by Rust).
|
||||
Hidden when a local user row has the same key (local wins). */}
|
||||
{inheritedRows
|
||||
.filter((irow) => !rows.some((r) => r.key === irow.key))
|
||||
.map((irow) => (
|
||||
<div key={irow.key} className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-11 flex-1 items-center gap-1.5 px-3",
|
||||
PERSONA_FIELD_SHELL_CLASS,
|
||||
"border-muted-foreground/20 bg-muted/20",
|
||||
)}
|
||||
>
|
||||
<Lock
|
||||
className="h-3 w-3 shrink-0 text-muted-foreground/40"
|
||||
aria-hidden
|
||||
/>
|
||||
<span
|
||||
className="font-mono text-sm leading-6 text-foreground/60"
|
||||
data-testid="env-vars-inherited-key"
|
||||
>
|
||||
{irow.key}
|
||||
</span>
|
||||
<span className="ml-1 rounded-sm bg-muted px-1 py-0.5 text-2xs font-medium text-muted-foreground">
|
||||
Inherited from {inheritedRowsLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-11 flex-[2] items-center px-3",
|
||||
PERSONA_FIELD_SHELL_CLASS,
|
||||
"opacity-60",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono text-sm",
|
||||
irow.masked
|
||||
? "text-muted-foreground/50"
|
||||
: "text-foreground/70",
|
||||
)}
|
||||
data-testid="env-vars-inherited-value"
|
||||
>
|
||||
{irow.value}
|
||||
</span>
|
||||
</div>
|
||||
{/* Spacer to align with the remove-button column */}
|
||||
<div className="h-9 w-9 shrink-0" aria-hidden />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* User-managed rows */}
|
||||
{rows.length === 0 &&
|
||||
requiredKeys.length === 0 &&
|
||||
fileSatisfiedKeys.length === 0 ? (
|
||||
fileSatisfiedKeys.length === 0 &&
|
||||
inheritedRows.length === 0 ? (
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
No variables set.
|
||||
</p>
|
||||
@@ -494,6 +577,19 @@ export function EnvVarsEditor({
|
||||
</span>
|
||||
</p>
|
||||
) : null}
|
||||
{(() => {
|
||||
if (row.key.length === 0) return null;
|
||||
const override = inheritedRows.find(
|
||||
(irow) => irow.key === row.key,
|
||||
);
|
||||
if (!override) return null;
|
||||
return (
|
||||
<p className="ml-1 text-xs text-muted-foreground">
|
||||
Overrides {inheritedRowsLabel} default{" "}
|
||||
<span className="font-mono">{override.value}</span>
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Pure helper functions for displaying baked build env values in the global
|
||||
* agent config card. Extracted into their own module so unit tests can import
|
||||
* them without pulling in React, Tauri IPC, or TanStack Query.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return the provider option label for the zero-value (inherit) option when a
|
||||
* baked provider is present. Falls back to the raw provider id when the id
|
||||
* doesn't appear in the options table.
|
||||
*
|
||||
* Used in GlobalAgentConfigSettingsCard to relabel the provider dropdown's
|
||||
* empty-selection option when a baked build provider is set.
|
||||
*/
|
||||
export function getBakedProviderInheritLabel(
|
||||
bakedProviderId: string,
|
||||
options: readonly { id: string; label: string }[],
|
||||
): string {
|
||||
const match = options.find((o) => o.id === bakedProviderId);
|
||||
const friendlyName = match ? match.label : bakedProviderId;
|
||||
return `${friendlyName} (inherited from build)`;
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import { getBakedBuildEnv, type BakedEnvEntry } from "@/shared/api/tauri";
|
||||
import { globalAgentConfigQueryKey } from "@/features/agents/useGlobalAgentConfig";
|
||||
import { useAcpRuntimesQuery } from "@/features/agents/hooks";
|
||||
import { EnvVarsEditor } from "@/features/agents/ui/EnvVarsEditor";
|
||||
import type { InheritedEnvRow } from "@/features/agents/ui/EnvVarsEditor";
|
||||
import { getBakedProviderInheritLabel } from "@/features/agents/ui/bakedEnvHelpers";
|
||||
import {
|
||||
AUTO_PROVIDER_DROPDOWN_VALUE,
|
||||
CUSTOM_PROVIDER_DROPDOWN_VALUE,
|
||||
@@ -47,6 +49,17 @@ const EMPTY_CONFIG: GlobalAgentConfig = {
|
||||
model: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Baked env keys that map to structured fields (provider/model/effort dropdowns).
|
||||
* These are routed to their own UI controls and must NOT appear as generic
|
||||
* inherited rows in the env editor.
|
||||
*/
|
||||
const BAKED_STRUCTURED_KEYS = new Set([
|
||||
"BUZZ_AGENT_PROVIDER",
|
||||
"BUZZ_AGENT_MODEL",
|
||||
BUZZ_AGENT_THINKING_EFFORT,
|
||||
]);
|
||||
|
||||
type SaveState = "idle" | "saving" | "saved" | "error";
|
||||
|
||||
export function GlobalAgentConfigSettingsCard() {
|
||||
@@ -100,6 +113,28 @@ export function GlobalAgentConfigSettingsCard() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Derive structured-field values and generic env rows from bakedEnv.
|
||||
// Structured keys (BUZZ_AGENT_PROVIDER / BUZZ_AGENT_MODEL / BUZZ_AGENT_THINKING_EFFORT)
|
||||
// route to their respective dropdown controls and are excluded from the
|
||||
// generic inherited-rows list passed to EnvVarsEditor.
|
||||
const bakedProvider = React.useMemo(
|
||||
() => bakedEnv.find((e) => e.key === "BUZZ_AGENT_PROVIDER")?.value ?? null,
|
||||
[bakedEnv],
|
||||
);
|
||||
const bakedModel = React.useMemo(
|
||||
() => bakedEnv.find((e) => e.key === "BUZZ_AGENT_MODEL")?.value ?? null,
|
||||
[bakedEnv],
|
||||
);
|
||||
const bakedEffort = React.useMemo(
|
||||
() =>
|
||||
bakedEnv.find((e) => e.key === BUZZ_AGENT_THINKING_EFFORT)?.value ?? null,
|
||||
[bakedEnv],
|
||||
);
|
||||
const bakedGenericRows = React.useMemo<readonly InheritedEnvRow[]>(
|
||||
() => bakedEnv.filter((e) => !BAKED_STRUCTURED_KEYS.has(e.key)),
|
||||
[bakedEnv],
|
||||
);
|
||||
|
||||
// Resolve the buzz-agent runtime catalog entry for model discovery.
|
||||
// The card is always visible (open=true), so the query is always enabled.
|
||||
const runtimesQuery = useAcpRuntimesQuery();
|
||||
@@ -213,6 +248,15 @@ export function GlobalAgentConfigSettingsCard() {
|
||||
? CUSTOM_PROVIDER_DROPDOWN_VALUE
|
||||
: providerValue || AUTO_PROVIDER_DROPDOWN_VALUE;
|
||||
|
||||
// When a baked provider is present and no explicit global provider is set,
|
||||
// relabel the zero-value option to surface the inherited-from-build value.
|
||||
// When an explicit provider IS set, the zero-value option is still shown in
|
||||
// the list but never selected — its label doesn't matter.
|
||||
const providerZeroLabel = React.useMemo(() => {
|
||||
if (!bakedProvider) return null;
|
||||
return getBakedProviderInheritLabel(bakedProvider, providerOptions);
|
||||
}, [bakedProvider, providerOptions]);
|
||||
|
||||
return (
|
||||
<section className="min-w-0" data-testid="settings-global-agent-config">
|
||||
<SettingsSectionHeader
|
||||
@@ -251,7 +295,7 @@ export function GlobalAgentConfigSettingsCard() {
|
||||
key={opt.id}
|
||||
value={opt.id || AUTO_PROVIDER_DROPDOWN_VALUE}
|
||||
>
|
||||
{opt.label}
|
||||
{opt.id === "" ? (providerZeroLabel ?? opt.label) : opt.label}
|
||||
</option>
|
||||
))}
|
||||
<option value={CUSTOM_PROVIDER_DROPDOWN_VALUE}>
|
||||
@@ -277,6 +321,7 @@ export function GlobalAgentConfigSettingsCard() {
|
||||
<AgentModelField
|
||||
disabled={false}
|
||||
discoveredModelOptions={discoveredModelOptions}
|
||||
globalModel={bakedModel ?? undefined}
|
||||
id="global-agent-model"
|
||||
isCustomModelEditing={isCustomModelEditing}
|
||||
isRequired={false}
|
||||
@@ -308,6 +353,7 @@ export function GlobalAgentConfigSettingsCard() {
|
||||
effortDefault={effortDefault}
|
||||
effortValid={effortValid}
|
||||
htmlFor="global-agent-thinking-effort"
|
||||
inheritedEffort={bakedEffort ?? undefined}
|
||||
label="Default thinking / effort"
|
||||
onChange={(value) => {
|
||||
setConfig((prev) => {
|
||||
@@ -352,6 +398,8 @@ export function GlobalAgentConfigSettingsCard() {
|
||||
: next;
|
||||
handleEnvVarsChange(merged);
|
||||
}}
|
||||
inheritedRows={bakedGenericRows}
|
||||
inheritedRowsLabel="build"
|
||||
label="Global environment variables"
|
||||
helperText="Injected into all agents as the lowest-priority layer. Per-agent values override these."
|
||||
/>
|
||||
@@ -359,53 +407,6 @@ export function GlobalAgentConfigSettingsCard() {
|
||||
</SettingsOptionGroup>
|
||||
)}
|
||||
|
||||
{/* Baked build defaults — only visible in internal (Block) builds.
|
||||
OSS builds return an empty array, so this section is hidden entirely. */}
|
||||
{bakedEnv.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<SettingsOptionGroup>
|
||||
<div className="p-3">
|
||||
<p className="mb-1.5 text-xs font-medium text-foreground">
|
||||
Baked build defaults
|
||||
</p>
|
||||
<p className="mb-2 text-xs text-muted-foreground">
|
||||
Set by your build. Override any of these above.
|
||||
</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
{bakedEnv.map((entry) => {
|
||||
const friendlyLabel =
|
||||
entry.key === "BUZZ_AGENT_PROVIDER"
|
||||
? "Baked provider"
|
||||
: entry.key === "BUZZ_AGENT_MODEL"
|
||||
? "Baked model"
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
className="flex items-baseline gap-2 font-mono text-xs"
|
||||
key={entry.key}
|
||||
>
|
||||
<code className="shrink-0 text-muted-foreground">
|
||||
{friendlyLabel ?? entry.key}
|
||||
</code>
|
||||
<span className="text-muted-foreground">=</span>
|
||||
<code
|
||||
className={
|
||||
entry.masked
|
||||
? "text-muted-foreground/50"
|
||||
: "text-foreground"
|
||||
}
|
||||
>
|
||||
{entry.value}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsOptionGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save bar */}
|
||||
{!isLoading && !loadError && (
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user