From c5e180a44271cdad3c62190163f2295c6f31dd26 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Mon, 6 Jul 2026 11:08:55 -0400 Subject: [PATCH 1/2] feat(agents): add tier-1 buzz-agent model-tuning fields to Create/Edit dialogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four dedicated, editable fields to the managed-agent Create and Edit dialogs so users can configure buzz-agent model-tuning knobs without hand-typing raw env var keys: - Thinking / Effort (select: none|minimal|low|medium|high|xhigh|max) - Max rounds (numeric) - Max output tokens (numeric) - Context limit (numeric) Each field writes its value as a BUZZ_AGENT_* key into the existing envVars map — no new persistence path or record-schema change. Empty field = inherit: the key is absent from envVars and the placeholder shows the global/persona baseline from inheritedEnvVars. Non-empty = explicit override. Fields are gated on isBuzzAgentRuntime(selectedRuntimeId) and rendered via a private BuzzAgentModelTuningFields component — non-buzz-agent runtimes see nothing. A source-of-truth constant BUZZ_AGENT_THINKING_EFFORT_VALUES mirrors parse_thinking_effort in crates/buzz-agent/src/config.rs. 14 behavior-focused unit tests cover: empty→inherit, set→writes key, clear→deletes key, correct key names, bounded select values, multi- field isolation. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../features/agents/ui/CreateAgentDialog.tsx | 5 +- .../agents/ui/CreateAgentDialogSections.tsx | 199 ++++++++++++++++++ .../features/agents/ui/EditAgentDialog.tsx | 3 + .../agents/ui/buzzAgentConfig.test.mjs | 174 +++++++++++++++ .../src/features/agents/ui/buzzAgentConfig.ts | 43 ++++ 5 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 desktop/src/features/agents/ui/buzzAgentConfig.test.mjs create mode 100644 desktop/src/features/agents/ui/buzzAgentConfig.ts diff --git a/desktop/src/features/agents/ui/CreateAgentDialog.tsx b/desktop/src/features/agents/ui/CreateAgentDialog.tsx index 50d2922813..ae866ff345 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialog.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialog.tsx @@ -820,14 +820,17 @@ export function CreateAgentDialog({ acpCommand={acpCommand} agentArgs={agentArgs} agentCommand={agentCommand} + envVars={envVars} + inheritedEnvVars={globalConfig.env_vars} mcpCommand={mcpCommand} mcpToolsets={mcpToolsets} - onParallelismChange={setParallelism} onAcpCommandChange={setAcpCommand} onAgentArgsChange={setAgentArgs} onAgentCommandChange={setAgentCommand} + onEnvVarsChange={setEnvVars} onMcpCommandChange={setMcpCommand} onMcpToolsetsChange={setMcpToolsets} + onParallelismChange={setParallelism} onRelayUrlChange={setRelayUrl} onSystemPromptChange={setSystemPrompt} onTurnTimeoutChange={setTurnTimeoutSeconds} diff --git a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx index f8be21c2ef..480c35cfd6 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx @@ -3,6 +3,15 @@ import { cn } from "@/shared/lib/cn"; import { Input } from "@/shared/ui/input"; import { Textarea } from "@/shared/ui/textarea"; import { describeResolvedCommand } from "./agentUi"; +import { + BUZZ_AGENT_MAX_CONTEXT_TOKENS, + BUZZ_AGENT_MAX_OUTPUT_TOKENS, + BUZZ_AGENT_MAX_ROUNDS, + BUZZ_AGENT_THINKING_EFFORT, + BUZZ_AGENT_THINKING_EFFORT_VALUES, + isBuzzAgentRuntime, +} from "./buzzAgentConfig"; +import type { EnvVarsValue } from "./EnvVarsEditor"; export function CreateAgentBasicsFields({ name, @@ -105,6 +114,8 @@ export function CreateAgentRuntimeFields({ acpCommand, agentArgs, agentCommand, + envVars, + inheritedEnvVars, mcpCommand, mcpToolsets, parallelism, @@ -115,6 +126,7 @@ export function CreateAgentRuntimeFields({ onAcpCommandChange, onAgentArgsChange, onAgentCommandChange, + onEnvVarsChange, onMcpCommandChange, onMcpToolsetsChange, onParallelismChange, @@ -125,6 +137,10 @@ export function CreateAgentRuntimeFields({ acpCommand: string; agentArgs: string; agentCommand: string; + /** Current agent env vars map — used to read/write tier-1 knob overrides. */ + envVars: EnvVarsValue; + /** Inherited baseline for tier-1 knobs (global → persona chain). Empty = no baseline. */ + inheritedEnvVars: EnvVarsValue; mcpCommand: string; mcpToolsets: string; parallelism: string; @@ -135,6 +151,8 @@ export function CreateAgentRuntimeFields({ onAcpCommandChange: (value: string) => void; onAgentArgsChange: (value: string) => void; onAgentCommandChange: (value: string) => void; + /** Called when a tier-1 field change writes/deletes a key in envVars. */ + onEnvVarsChange: (updated: EnvVarsValue) => void; onMcpCommandChange: (value: string) => void; onMcpToolsetsChange: (value: string) => void; onParallelismChange: (value: string) => void; @@ -142,6 +160,19 @@ export function CreateAgentRuntimeFields({ onSystemPromptChange: (value: string) => void; onTurnTimeoutChange: (value: string) => void; }) { + const isBuzzAgent = isBuzzAgentRuntime(selectedRuntimeId); + + /** Write or delete a single env var key. Empty string = inherit (delete). */ + function handleEnvVarChange(key: string, value: string) { + const next = { ...envVars }; + if (value === "") { + delete next[key]; + } else { + next[key] = value; + } + onEnvVarsChange(next); + } + return ( <>
@@ -337,10 +368,178 @@ export function CreateAgentRuntimeFields({ Blank means no override. buzz-acp will not add a [System] prompt.

+ + {/* Tier-1 buzz-agent model-tuning knobs — only shown for buzz-agent. */} + {isBuzzAgent ? ( + + ) : null} ); } +/** + * Dedicated editable fields for the four buzz-agent model-tuning knobs. + * Renders only when the selected runtime is buzz-agent. + * + * Empty field = inherit (key absent from envVars; placeholder shows baseline). + * Non-empty = explicit override written into envVars. + */ +function BuzzAgentModelTuningFields({ + envVars, + inheritedEnvVars, + onEnvVarChange, +}: { + envVars: EnvVarsValue; + inheritedEnvVars: EnvVarsValue; + onEnvVarChange: (key: string, value: string) => void; +}) { + return ( +
+

+ buzz-agent model tuning +

+ +
+ {/* Thinking / Effort */} +
+ + +

+ Controls how much reasoning effort the LLM applies per turn. Leave + blank to inherit from the global or persona default. +

+
+ + {/* Max Rounds */} +
+ + + onEnvVarChange(BUZZ_AGENT_MAX_ROUNDS, event.target.value) + } + placeholder={ + inheritedEnvVars[BUZZ_AGENT_MAX_ROUNDS] + ? `Inherit (${inheritedEnvVars[BUZZ_AGENT_MAX_ROUNDS]})` + : "Inherit (agent default)" + } + step="1" + type="number" + value={envVars[BUZZ_AGENT_MAX_ROUNDS] ?? ""} + /> +

+ Maximum LLM + tool-call rounds per turn. Leave blank to inherit. +

+
+ + {/* Max Output Tokens */} +
+ + + onEnvVarChange(BUZZ_AGENT_MAX_OUTPUT_TOKENS, event.target.value) + } + placeholder={ + inheritedEnvVars[BUZZ_AGENT_MAX_OUTPUT_TOKENS] + ? `Inherit (${inheritedEnvVars[BUZZ_AGENT_MAX_OUTPUT_TOKENS]})` + : "Inherit (agent default)" + } + step="1" + type="number" + value={envVars[BUZZ_AGENT_MAX_OUTPUT_TOKENS] ?? ""} + /> +

+ Maximum tokens the LLM may generate per response. Leave blank to + inherit. +

+
+ + {/* Context Limit */} +
+ + + onEnvVarChange(BUZZ_AGENT_MAX_CONTEXT_TOKENS, event.target.value) + } + placeholder={ + inheritedEnvVars[BUZZ_AGENT_MAX_CONTEXT_TOKENS] + ? `Inherit (${inheritedEnvVars[BUZZ_AGENT_MAX_CONTEXT_TOKENS]})` + : "Inherit (agent default)" + } + step="1" + type="number" + value={envVars[BUZZ_AGENT_MAX_CONTEXT_TOKENS] ?? ""} + /> +

+ Maximum context window tokens buzz-agent tracks before a handoff. + Leave blank to inherit. +

+
+
+
+ ); +} + export function CreateAgentOptionToggles({ isSpawnSupported, prereqs, diff --git a/desktop/src/features/agents/ui/EditAgentDialog.tsx b/desktop/src/features/agents/ui/EditAgentDialog.tsx index d5273b82fa..655bf5d5f9 100644 --- a/desktop/src/features/agents/ui/EditAgentDialog.tsx +++ b/desktop/src/features/agents/ui/EditAgentDialog.tsx @@ -765,11 +765,14 @@ export function EditAgentDialog({ acpCommand={acpCommand} agentArgs={agentArgs} agentCommand={agentCommand} + envVars={envVars} + inheritedEnvVars={inheritedWithGlobal} mcpCommand={mcpCommand} mcpToolsets={mcpToolsets} onAcpCommandChange={setAcpCommand} onAgentArgsChange={setAgentArgs} onAgentCommandChange={setAgentCommand} + onEnvVarsChange={setEnvVars} onMcpCommandChange={setMcpCommand} onMcpToolsetsChange={setMcpToolsets} onParallelismChange={setParallelism} diff --git a/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs b/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs new file mode 100644 index 0000000000..ad53174b49 --- /dev/null +++ b/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs @@ -0,0 +1,174 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + BUZZ_AGENT_MAX_CONTEXT_TOKENS, + BUZZ_AGENT_MAX_OUTPUT_TOKENS, + BUZZ_AGENT_MAX_ROUNDS, + BUZZ_AGENT_THINKING_EFFORT, + BUZZ_AGENT_THINKING_EFFORT_VALUES, + isBuzzAgentRuntime, +} from "./buzzAgentConfig.ts"; + +// --------------------------------------------------------------------------- +// Thinking effort values +// --------------------------------------------------------------------------- + +test("BUZZ_AGENT_THINKING_EFFORT_VALUES contains exactly the 7 accepted values", () => { + assert.deepEqual( + [...BUZZ_AGENT_THINKING_EFFORT_VALUES], + ["none", "minimal", "low", "medium", "high", "xhigh", "max"], + ); +}); + +test("BUZZ_AGENT_THINKING_EFFORT_VALUES has no duplicates", () => { + const set = new Set(BUZZ_AGENT_THINKING_EFFORT_VALUES); + assert.equal(set.size, BUZZ_AGENT_THINKING_EFFORT_VALUES.length); +}); + +// --------------------------------------------------------------------------- +// Env var key constants +// --------------------------------------------------------------------------- + +test("env var key constants match expected BUZZ_AGENT_* names", () => { + assert.equal(BUZZ_AGENT_THINKING_EFFORT, "BUZZ_AGENT_THINKING_EFFORT"); + assert.equal(BUZZ_AGENT_MAX_OUTPUT_TOKENS, "BUZZ_AGENT_MAX_OUTPUT_TOKENS"); + assert.equal(BUZZ_AGENT_MAX_CONTEXT_TOKENS, "BUZZ_AGENT_MAX_CONTEXT_TOKENS"); + assert.equal(BUZZ_AGENT_MAX_ROUNDS, "BUZZ_AGENT_MAX_ROUNDS"); +}); + +// --------------------------------------------------------------------------- +// isBuzzAgentRuntime +// --------------------------------------------------------------------------- + +test("isBuzzAgentRuntime returns true only for buzz-agent id", () => { + assert.equal(isBuzzAgentRuntime("buzz-agent"), true); +}); + +test("isBuzzAgentRuntime returns false for other runtimes", () => { + assert.equal(isBuzzAgentRuntime("goose"), false); + assert.equal(isBuzzAgentRuntime("custom"), false); + assert.equal(isBuzzAgentRuntime(""), false); + assert.equal(isBuzzAgentRuntime("buzz-agent-v2"), false); +}); + +// --------------------------------------------------------------------------- +// handleEnvVarChange logic (the field→envVars mapping) +// --------------------------------------------------------------------------- + +/** + * Mirrors the handleEnvVarChange helper in CreateAgentRuntimeFields. + * Tests this directly without rendering React. + */ +function applyEnvVarChange(envVars, key, value) { + const next = { ...envVars }; + if (value === "") { + delete next[key]; + } else { + next[key] = value; + } + return next; +} + +test("setting a thinking effort value writes the key into envVars", () => { + const initial = {}; + const result = applyEnvVarChange(initial, BUZZ_AGENT_THINKING_EFFORT, "high"); + assert.equal(result[BUZZ_AGENT_THINKING_EFFORT], "high"); +}); + +test("clearing thinking effort removes the key so the agent inherits", () => { + const initial = { [BUZZ_AGENT_THINKING_EFFORT]: "high" }; + const result = applyEnvVarChange(initial, BUZZ_AGENT_THINKING_EFFORT, ""); + assert.equal(Object.hasOwn(result, BUZZ_AGENT_THINKING_EFFORT), false); +}); + +test("setting max output tokens writes the exact BUZZ_AGENT_MAX_OUTPUT_TOKENS key", () => { + const initial = {}; + const result = applyEnvVarChange( + initial, + BUZZ_AGENT_MAX_OUTPUT_TOKENS, + "4096", + ); + assert.equal(result[BUZZ_AGENT_MAX_OUTPUT_TOKENS], "4096"); + // Must not affect other keys + assert.equal(Object.keys(result).length, 1); +}); + +test("clearing max output tokens removes the key", () => { + const initial = { [BUZZ_AGENT_MAX_OUTPUT_TOKENS]: "4096" }; + const result = applyEnvVarChange(initial, BUZZ_AGENT_MAX_OUTPUT_TOKENS, ""); + assert.equal(Object.hasOwn(result, BUZZ_AGENT_MAX_OUTPUT_TOKENS), false); +}); + +test("setting context limit writes the exact BUZZ_AGENT_MAX_CONTEXT_TOKENS key", () => { + const initial = {}; + const result = applyEnvVarChange( + initial, + BUZZ_AGENT_MAX_CONTEXT_TOKENS, + "100000", + ); + assert.equal(result[BUZZ_AGENT_MAX_CONTEXT_TOKENS], "100000"); +}); + +test("clearing context limit removes the key", () => { + const initial = { [BUZZ_AGENT_MAX_CONTEXT_TOKENS]: "100000" }; + const result = applyEnvVarChange(initial, BUZZ_AGENT_MAX_CONTEXT_TOKENS, ""); + assert.equal(Object.hasOwn(result, BUZZ_AGENT_MAX_CONTEXT_TOKENS), false); +}); + +test("setting max rounds writes the exact BUZZ_AGENT_MAX_ROUNDS key", () => { + const initial = {}; + const result = applyEnvVarChange(initial, BUZZ_AGENT_MAX_ROUNDS, "50"); + assert.equal(result[BUZZ_AGENT_MAX_ROUNDS], "50"); +}); + +test("clearing max rounds removes the key", () => { + const initial = { [BUZZ_AGENT_MAX_ROUNDS]: "50" }; + const result = applyEnvVarChange(initial, BUZZ_AGENT_MAX_ROUNDS, ""); + assert.equal(Object.hasOwn(result, BUZZ_AGENT_MAX_ROUNDS), false); +}); + +test("changing one field does not disturb other env vars", () => { + const initial = { + SOME_OTHER_KEY: "value", + [BUZZ_AGENT_MAX_OUTPUT_TOKENS]: "2048", + }; + const result = applyEnvVarChange(initial, BUZZ_AGENT_MAX_ROUNDS, "20"); + assert.equal(result.SOME_OTHER_KEY, "value"); + assert.equal(result[BUZZ_AGENT_MAX_OUTPUT_TOKENS], "2048"); + assert.equal(result[BUZZ_AGENT_MAX_ROUNDS], "20"); +}); + +test("clearing one field does not disturb other env vars", () => { + const initial = { + SOME_OTHER_KEY: "value", + [BUZZ_AGENT_MAX_OUTPUT_TOKENS]: "2048", + [BUZZ_AGENT_MAX_ROUNDS]: "20", + }; + const result = applyEnvVarChange(initial, BUZZ_AGENT_MAX_ROUNDS, ""); + assert.equal(Object.hasOwn(result, BUZZ_AGENT_MAX_ROUNDS), false); + assert.equal(result.SOME_OTHER_KEY, "value"); + assert.equal(result[BUZZ_AGENT_MAX_OUTPUT_TOKENS], "2048"); +}); + +test("thinking effort select is bounded: all 7 accepted values are present in the constant", () => { + const expected = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]; + for (const v of expected) { + assert.ok( + BUZZ_AGENT_THINKING_EFFORT_VALUES.includes(v), + `missing value: ${v}`, + ); + } + assert.equal(BUZZ_AGENT_THINKING_EFFORT_VALUES.length, expected.length); +}); + +test("non-numeric string is stored as-is (validation is at the backend)", () => { + // HTML type=number inputs enforce numeric in the browser; we verify the + // mapping function itself is not a validator — that's intentional. + const result = applyEnvVarChange( + {}, + BUZZ_AGENT_MAX_OUTPUT_TOKENS, + "not-a-number", + ); + assert.equal(result[BUZZ_AGENT_MAX_OUTPUT_TOKENS], "not-a-number"); +}); diff --git a/desktop/src/features/agents/ui/buzzAgentConfig.ts b/desktop/src/features/agents/ui/buzzAgentConfig.ts new file mode 100644 index 0000000000..9e8ecf8cdb --- /dev/null +++ b/desktop/src/features/agents/ui/buzzAgentConfig.ts @@ -0,0 +1,43 @@ +/** + * Source-of-truth constants for buzz-agent model-tuning configuration knobs. + * + * Values must stay in sync with `crates/buzz-agent/src/config.rs` + * `parse_thinking_effort` — that function is the authoritative list. + */ + +/** Env var key for the thinking/effort level sent to the LLM. */ +export const BUZZ_AGENT_THINKING_EFFORT = "BUZZ_AGENT_THINKING_EFFORT"; + +/** Env var key for the maximum output token count per turn. */ +export const BUZZ_AGENT_MAX_OUTPUT_TOKENS = "BUZZ_AGENT_MAX_OUTPUT_TOKENS"; + +/** Env var key for the context window token limit. */ +export const BUZZ_AGENT_MAX_CONTEXT_TOKENS = "BUZZ_AGENT_MAX_CONTEXT_TOKENS"; + +/** Env var key for the maximum number of LLM/tool rounds per turn. */ +export const BUZZ_AGENT_MAX_ROUNDS = "BUZZ_AGENT_MAX_ROUNDS"; + +/** + * Ordered set of valid thinking-effort values accepted by buzz-agent. + * Mirrors `parse_thinking_effort` in `crates/buzz-agent/src/config.rs`. + */ +export const BUZZ_AGENT_THINKING_EFFORT_VALUES = [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +] as const; + +export type ThinkingEffortValue = + (typeof BUZZ_AGENT_THINKING_EFFORT_VALUES)[number]; + +/** + * Returns true when the given runtime id is buzz-agent, which is the only + * runtime that supports the tier-1 model-tuning knobs above. + */ +export function isBuzzAgentRuntime(runtimeId: string): boolean { + return runtimeId === "buzz-agent"; +} From 02dda4419651132fb265aba4d10372c8ec470179 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Mon, 6 Jul 2026 11:24:33 -0400 Subject: [PATCH 2/2] fix(agents): split modelTuningRuntimeId from selectedRuntimeId sentinel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Edit dialog collapses selectedRuntimeId to an 'inherit'/'custom' sentinel that drives the custom-command input visibility. The tier-1 model-tuning gate was using this same prop, so isBuzzAgentRuntime always saw 'inherit' in Edit and the fields were never rendered. Fixes: - Add modelTuningRuntimeId prop to CreateAgentRuntimeFields, separate from the existing selectedRuntimeId sentinel. Gate the tier-1 fields on isBuzzAgentRuntime(modelTuningRuntimeId). - Edit passes prospectiveRuntimeId (already resolves inherit→persona command, covers both inherited and pinned buzz-agent personas). - Create passes selectedRuntimeId (no sentinel needed; the real id is always available there). - Fix min="0" on Max Rounds (backend default is 0 = unlimited; explicit 0 is a valid override to lift a finite cap set via persona/global). Add '0 = unlimited' to the help text. - Add 4 regression tests covering the Edit runtime-id mapping: sentinel 'inherit' must not trigger fields; prospectiveRuntimeId 'buzz-agent' must; goose/empty must not. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../features/agents/ui/CreateAgentDialog.tsx | 1 + .../agents/ui/CreateAgentDialogSections.tsx | 20 ++++++-- .../features/agents/ui/EditAgentDialog.tsx | 1 + .../agents/ui/buzzAgentConfig.test.mjs | 50 +++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/desktop/src/features/agents/ui/CreateAgentDialog.tsx b/desktop/src/features/agents/ui/CreateAgentDialog.tsx index ae866ff345..45e6ae3046 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialog.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialog.tsx @@ -824,6 +824,7 @@ export function CreateAgentDialog({ inheritedEnvVars={globalConfig.env_vars} mcpCommand={mcpCommand} mcpToolsets={mcpToolsets} + modelTuningRuntimeId={selectedRuntimeId} onAcpCommandChange={setAcpCommand} onAgentArgsChange={setAgentArgs} onAgentCommandChange={setAgentCommand} diff --git a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx index 480c35cfd6..3bbf538dd3 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx @@ -118,6 +118,7 @@ export function CreateAgentRuntimeFields({ inheritedEnvVars, mcpCommand, mcpToolsets, + modelTuningRuntimeId, parallelism, relayUrl, selectedRuntimeId, @@ -143,8 +144,20 @@ export function CreateAgentRuntimeFields({ inheritedEnvVars: EnvVarsValue; mcpCommand: string; mcpToolsets: string; + /** + * The actual/prospective runtime id used to decide whether to show the + * buzz-agent model-tuning fields. Kept separate from `selectedRuntimeId` + * because Edit collapses `selectedRuntimeId` to the "inherit"/"custom" + * sentinel that drives the custom-command input — that sentinel must NOT + * be used to gate the model-tuning section. + */ + modelTuningRuntimeId: string; parallelism: string; relayUrl: string; + /** + * Drives the custom-command input only: "custom" shows it, anything else + * hides it. In Edit this carries the "inherit"/"custom" sentinel. + */ selectedRuntimeId: string; systemPrompt: string; turnTimeoutSeconds: string; @@ -160,7 +173,7 @@ export function CreateAgentRuntimeFields({ onSystemPromptChange: (value: string) => void; onTurnTimeoutChange: (value: string) => void; }) { - const isBuzzAgent = isBuzzAgentRuntime(selectedRuntimeId); + const isBuzzAgent = isBuzzAgentRuntime(modelTuningRuntimeId); /** Write or delete a single env var key. Empty string = inherit (delete). */ function handleEnvVarChange(key: string, value: string) { @@ -449,7 +462,7 @@ function BuzzAgentModelTuningFields({ data-testid="ba-max-rounds-input" id="ba-max-rounds" inputMode="numeric" - min="1" + min="0" onChange={(event) => onEnvVarChange(BUZZ_AGENT_MAX_ROUNDS, event.target.value) } @@ -463,7 +476,8 @@ function BuzzAgentModelTuningFields({ value={envVars[BUZZ_AGENT_MAX_ROUNDS] ?? ""} />

- Maximum LLM + tool-call rounds per turn. Leave blank to inherit. + Maximum LLM + tool-call rounds per turn. 0 = unlimited. Leave blank + to inherit.

diff --git a/desktop/src/features/agents/ui/EditAgentDialog.tsx b/desktop/src/features/agents/ui/EditAgentDialog.tsx index 655bf5d5f9..ce0879e99e 100644 --- a/desktop/src/features/agents/ui/EditAgentDialog.tsx +++ b/desktop/src/features/agents/ui/EditAgentDialog.tsx @@ -769,6 +769,7 @@ export function EditAgentDialog({ inheritedEnvVars={inheritedWithGlobal} mcpCommand={mcpCommand} mcpToolsets={mcpToolsets} + modelTuningRuntimeId={prospectiveRuntimeId} onAcpCommandChange={setAcpCommand} onAgentArgsChange={setAgentArgs} onAgentCommandChange={setAgentCommand} diff --git a/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs b/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs index ad53174b49..4b86353b98 100644 --- a/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs +++ b/desktop/src/features/agents/ui/buzzAgentConfig.test.mjs @@ -172,3 +172,53 @@ test("non-numeric string is stored as-is (validation is at the backend)", () => ); assert.equal(result[BUZZ_AGENT_MAX_OUTPUT_TOKENS], "not-a-number"); }); + +// --------------------------------------------------------------------------- +// modelTuningRuntimeId → visibility mapping (regression for Edit dialog path) +// --------------------------------------------------------------------------- + +// Mirrors the `isBuzzAgent` derivation in CreateAgentRuntimeFields. +// The point of modelTuningRuntimeId is that the Edit dialog can pass +// prospectiveRuntimeId (the real resolved runtime) while selectedRuntimeId +// carries the "inherit"/"custom" sentinel — the two must not be conflated. + +test("isBuzzAgentRuntime(prospectiveRuntimeId) shows fields when Edit resolves buzz-agent even though selectedRuntimeId sentinel is 'inherit'", () => { + // Simulates Edit dialog state: inheritHarness=true, persona is buzz-agent. + // selectedRuntimeId would be "inherit" (sentinel for custom-command hiding), + // but prospectiveRuntimeId correctly resolves to "buzz-agent". + const selectedRuntimeIdSentinel = "inherit"; // what Edit passes to selectedRuntimeId + const prospectiveRuntimeId = "buzz-agent"; // what Edit passes to modelTuningRuntimeId + + assert.equal( + isBuzzAgentRuntime(selectedRuntimeIdSentinel), + false, + "sentinel 'inherit' must NOT trigger model-tuning fields", + ); + assert.equal( + isBuzzAgentRuntime(prospectiveRuntimeId), + true, + "prospectiveRuntimeId 'buzz-agent' MUST trigger model-tuning fields", + ); +}); + +test("isBuzzAgentRuntime(prospectiveRuntimeId) shows fields when Edit has a pinned buzz-agent (selectedRuntimeId sentinel is also 'inherit')", () => { + // Simulates Edit dialog with a pinned non-custom runtime: + // selectedRuntimeId sentinel = "inherit" (non-custom known runtime), + // prospectiveRuntimeId = "buzz-agent" (selectedRuntime?.id). + const selectedRuntimeIdSentinel = "inherit"; + const prospectiveRuntimeId = "buzz-agent"; + + assert.equal(isBuzzAgentRuntime(prospectiveRuntimeId), true); + assert.equal(isBuzzAgentRuntime(selectedRuntimeIdSentinel), false); +}); + +test("isBuzzAgentRuntime(prospectiveRuntimeId) hides fields when Edit resolves to non-buzz-agent", () => { + // E.g. user switches from buzz-agent to goose in Edit — prospectiveRuntimeId = "goose" + const prospectiveRuntimeId = "goose"; + assert.equal(isBuzzAgentRuntime(prospectiveRuntimeId), false); +}); + +test("isBuzzAgentRuntime(prospectiveRuntimeId) hides fields when Edit has no resolved runtime (empty string)", () => { + // prospectiveRuntimeId falls back to "" when catalog hasn't loaded yet + assert.equal(isBuzzAgentRuntime(""), false); +});