Skip to content

feat(desktop): global agent config defaults with readiness/spawn parity#1448

Open
wpfleger96 wants to merge 40 commits into
mainfrom
duncan/global-agent-config
Open

feat(desktop): global agent config defaults with readiness/spawn parity#1448
wpfleger96 wants to merge 40 commits into
mainfrom
duncan/global-agent-config

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Adds a global agent configuration layer that lets users set default provider, model, and environment variables applied to every agent unless overridden per-agent or per-template. Surfaces the baked build environment in the same card. Wires deep-link field focus from ConfigNudgeCard. Adds a save-gate for the instance-edit dialog to guard against completing a runtime switch to a provider-capable runtime without a provider selected.

Global config layer

  • GlobalAgentConfig struct (provider, model, env_vars) persisted to global-agent-config.json.
  • Tauri commands: get_global_agent_config, set_global_agent_config (reserved and derived keys rejected by validate_global_config).
  • Env-var merge at spawn: global < per-agent (last wins); reserved Buzz keys stripped before merge.
  • GlobalAgentConfigSettingsCard rendered in AgentsView; model/provider fields, env-vars editor showing global defaults as inherited rows, and (Block builds only) a read-only "Baked build defaults" section below.
  • useGlobalAgentConfig singleton via TanStack Query (staleTime: Infinity, cache seeded on save).

Readiness / spawn parity (Fix 1)

resolve_effective_model_provider(record, personas, global) in global_config/mod.rs encodes the agent → template → global → None precedence chain and is wired into both resolve_effective_agent_env (readiness evaluation) and spawn_agent_child (runtime). Before this fix, readiness used the full fallback chain but spawn read only record.model/record.provider, so a global-default-only agent reported Ready but spawned without provider/model env.

Override baseline for global-default agents (Fix 2)

In resolve_config_surface, when !had_model && persona_model.is_none() && model_overridden, use (global.model, ConfigOrigin::GlobalDefault) as the baseline so the config surface UI shows the correct secondary row for a global-default-only agent.

Required-row satisfaction (Fix 3)

isRequiredKeyMissing in EnvVarsEditor.tsx now checks inheritedFrom?.[key] in addition to the agent-local value. Before this fix, a globally-satisfied required key still rendered the amber "Required" badge.

Global config wired into dialogs

  • computeLocalModeGate (in PersonaDialog and AgentInstanceEditDialog) receives globalConfig and uses useGlobalAgentConfig to evaluate readiness against the effective merged env — required markers, amber locked-required rows, and the auto-expand Advanced effect all work correctly against global defaults.
  • AgentInstanceEditDialog exposes a providerValid save-gate: blocks saving when the agent has no per-agent or global provider but the selected runtime requires one. The originalRuntimeSupportsProvider flag (snapshotted at dialog open from agent.agentCommand, not the in-dialog selection) ensures the gate only fires on a switch INTO a provider-capable runtime, not for a legacy agent already on one.
  • CreateAgentDialog and EditAgentAdvancedFields surface the global env as inherited rows with label "template / global defaults".

Tier-1 buzz-agent model-tuning fields (#1534)

BuzzAgentModelTuningFields (temperature, top-p, max-tokens) wired into EditAgentAdvancedFields for the buzz-agent runtime.

Deep-link field focus from ConfigNudgeCard

ConfigNudgeCard can fire an openEditAgent event with a focusTarget field name. UserProfilePanel and the Edit Agent dialog read EditAgentFocusTarget to focus the correct field (provider, model, or an env-var key) when the dialog opens.

Auto-respawn and setup-listener

respawn_newly_ready_agents is called after set_global_agent_config, so agents blocked only on a global default restart automatically once the default is saved. The setup-listener wiring routes auto-respawn through the normal spawn path.

Baked build defaults

get_baked_build_env() Tauri command returns the compile-time baked agent env with secret keys masked in Rust (values for keys matching _API_KEY, _TOKEN, _SECRET, _PASSWORD replaced with •••••• before crossing the IPC boundary). GlobalAgentConfigSettingsCard renders a read-only "Baked build defaults" section only when the list is non-empty; OSS builds return [] and the section is hidden.

Related: block/buzz#1633

@wpfleger96 wpfleger96 changed the base branch from main to duncan/agent-readiness July 1, 2026 22:42
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from 34f9571 to 3903f9c Compare July 1, 2026 23:13
@wpfleger96 wpfleger96 changed the title feat(desktop): add global agent configuration defaults layer feat(desktop): global agent config defaults with readiness/spawn parity Jul 1, 2026
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch 2 times, most recently from 828f73d to 05d2467 Compare July 1, 2026 23:37
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from c7b3b9d to bb71500 Compare July 2, 2026 16:20
@wpfleger96 wpfleger96 force-pushed the duncan/agent-readiness branch from de99aa5 to c65a9ee Compare July 2, 2026 16:32
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from bb71500 to 04d8da2 Compare July 2, 2026 16:36
@wpfleger96 wpfleger96 force-pushed the duncan/agent-readiness branch from c65a9ee to 820461d Compare July 2, 2026 17:52
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch 2 times, most recently from e0a631a to 107f561 Compare July 2, 2026 18:44
@wpfleger96 wpfleger96 force-pushed the duncan/agent-readiness branch from 820461d to 691fce3 Compare July 2, 2026 23:06
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from 7ef6834 to 4f2f7b2 Compare July 2, 2026 23:07
@wpfleger96 wpfleger96 force-pushed the duncan/agent-readiness branch from 691fce3 to 6af58ae Compare July 6, 2026 18:51
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch 2 times, most recently from 607c5fd to 7e1a77d Compare July 6, 2026 21:32
@wpfleger96 wpfleger96 force-pushed the duncan/agent-readiness branch from f623802 to dbf60e1 Compare July 6, 2026 22:10
Base automatically changed from duncan/agent-readiness to main July 6, 2026 22:23
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch 3 times, most recently from 2eac66a to 2353a44 Compare July 6, 2026 23:00
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

Agent defaults card — global provider, model, and env var

Global configuration layer (#1448): GlobalAgentConfigSettingsCard in the Agents view, populated with Anthropic provider, claude-opus-4-5 model, and a global ANTHROPIC_API_KEY env var.

01-global-agent-config-card-populated

Create Agent — required key driven by global provider (Test 2.1 + 2.2 fix)

Global provider = Anthropic, no per-agent provider set → ANTHROPIC_API_KEY appears as a required amber row. Advanced section auto-expands when required env keys appear (Test 2.2 fix).

02-create-global-provider-required-key-advanced-open

Create Agent — global env satisfies required key, Advanced collapsed (Test 4 nuance fix)

Global env_vars sets ANTHROPIC_API_KEYcomputeLocalModeGate excludes it from requiredEnvKeys entirely. Advanced stays collapsed; Create agent enabled.

03-global-env-satisfies-required-key

Create gate BLOCKED — no provider, no global defaults

No per-agent provider and no global provider set → effectiveProvider empty → Create agent disabled (provider-default rule, Test 5).

04-create-blocked-no-provider-no-global

Create gate ENABLED — global provider resolves the default

Global provider = Anthropic → effectiveProvider satisfied → Create agent enabled without per-agent provider selection.

05-create-enabled-with-global-provider

wpfleger96 pushed a commit that referenced this pull request Jul 7, 2026
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from 9bae17f to 1d399c7 Compare July 7, 2026 01:32
wpfleger96 added a commit that referenced this pull request Jul 7, 2026
…and on baked-keys settle

Three bugs introduced in the initial baked-env PR:

1. PersonaDialog.tsx used allowlist semantics for requiredEnvKeys
   (include if in missingEnvKeys OR fileSatisfiedEnvKeys), which caused
   file-satisfied keys to render twice in EnvVarsEditor — once as an amber
   locked row and once as an info row — and caused filled required keys to
   drop out of the amber locked row on first character typed (focus loss).
   Fixed to exclusion semantics matching the other consumers: all required
   keys except baked-satisfied and file-satisfied ones.

2. useRequiredCredentialState auto-expanded the Advanced section before the
   baked-keys query settled, causing a first-open flicker: bakedEnvKeys was
   undefined, baked-covered keys transiently appeared missing, Advanced
   expanded, then the requirement disappeared when the query resolved.
   Fixed by gating the auto-expand on isFetched from the query.

3. useBakedBuildEnvKeysQuery lacked retry: false, causing 3 silent retries
   in web/E2E contexts where the Tauri command doesn't exist.

Also extends getBakedSatisfiedEnvKeys JSDoc with the UX asymmetry rationale
and the #1448 precedence insertion point note, and adds the missing
"Queued to split." trailer to the tauri.ts file-size override comment.
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from 1d399c7 to 8b127a7 Compare July 7, 2026 20:34
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 29 commits July 8, 2026 18:20
PersonaDialog has two owners of the provider API key:
- PersonaProviderApiKeyField above Advanced (the working input)
- A locked amber Required row inside EnvVarsEditor via PersonaAdvancedFields

getAdvancedEnvVars strips the managed key from the envVars value passed
to the editor, but requiredEnvKeys still included it. The editor rendered
a locked row whose value was always empty string; every keystroke was
discarded by the handleAdvancedEnvVarsChange guard. The input appeared dead.

Filter the managed key (providerApiKeyConfig?.envVar) out of requiredEnvKeys
at the PersonaAdvancedFields call site. The dedicated field above remains
the single owner of that credential slot.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
When a user selects a runtime provider in CreateAgentDialog that requires
a credential (e.g. Anthropic needing ANTHROPIC_API_KEY), the required
amber rows appear in the env-vars section below Advanced — but if Advanced
is collapsed the user sees no visual signal that action is needed.

Add an effect that opens Advanced the first time requiredEnvKeys becomes
non-empty during a dialog-open cycle. A ref guards against re-opening
after the user manually collapses the section.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Previously GlobalAgentConfigSettingsCard loaded its own useState copy via a
separate getGlobalAgentConfig() call and only called invalidateQueries after
save. invalidateQueries is a no-op when no Create/Edit dialog has ever mounted
(no cached query entry exists yet), so the first dialog open still started
from placeholderData: EMPTY_CONFIG and saw an async fetch race before
requiredEnvKeys could be computed.

Seed the TanStack Query cache via setQueryData in both paths:
- on load success: any dialog that opens after the settings card mounts reads
  the populated value synchronously on first render
- after save: the canonical saved config propagates synchronously to all open
  dialogs and any that open afterward

Drop the invalidateQueries call — setQueryData is both sufficient and more
correct (synchronous, no refetch round-trip needed).

Fixes Test 2.1 (Create Agent missing required row), Test 4 nuance (Edit Agent
showing stale global), and unblocks Test 3.3 deep-link focus (required row now
present on first render so its ref registers before the focus dispatch fires).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Fix 3 auto-opens the Advanced section the moment required env keys appear.
The parallelism smoke test picked provider=anthropic (triggering auto-open),
then clicked the 'Advanced setup' header unconditionally — which toggled the
already-open section closed. The parallelism input inside was then hidden and
the fill timed out.

Guard the click on aria-expanded: only open if currently collapsed. Add a
toBeVisible assertion before the fill so future regressions surface a clear
failure message rather than a fill timeout.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…t dialogs (#1534)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
… label/gate

PersonaDialog was global-blind: computeLocalModeGate received no
globalProvider/globalEnvVars/globalModel, so persona-bound agents never
showed required credential rows when a global provider was set without a
per-agent override (same root as Test 2.1/Test 4 failures in non-persona
paths before the arc fixes).

Gap A: import useGlobalAgentConfig into PersonaDialog, wrap
computeLocalModeGate in useMemo with global args, replace the standalone
requiredCredentialEnvKeys() call with the gate's own requiredEnvKeys (gate
already filters globally- and file-satisfied keys).

Gap B: add hasAutoOpenedAdvancedRef auto-expand effect (mirrors
CreateAgentDialog) — fires once per dialog-open cycle when requiredEnvKeys
is non-empty so the user sees the required credential row without manually
expanding Advanced.

Fix (a): update getDefaultLlmProviderLabel to accept globalProvider and
return 'Inherit (<provider>)' when set or 'Select a provider…' when not.
Thread through getPersonaProviderOptions and AgentProviderField. Add
effective-provider save gate (effectiveProvider = per-agent || global) to
canSubmit in CreateAgentDialog, EditAgentDialog, and PersonaDialog — save
is only blocked when there is genuinely no provider to fall back to.

Bump PersonaDialog file-size override from 1034 to 1077 (+43 lines for
the global wiring and auto-expand effect).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…-default rule

Shot 01 (buzz-agent empty provider) and Shot 08 (goose empty provider)
previously asserted toBeEnabled — the correct behavior pre-provider-default
rule. Fix (a) now blocks save when no effective provider exists (per-agent
OR global), so the assertions flip to toBeDisabled. Updated comments explain
the new invariant.

Add get_global_agent_config mock to e2eBridge.ts (returns configurable
empty config instead of throwing 'Unsupported command'). Wire globalAgentConfig
option through E2eConfig.mock and MockBridgeOptions so future tests can
exercise Inherit-from-global behavior without a real backend.

Add 9 unit tests to createAgentLocalModeGate.test.mjs covering:
- getDefaultLlmProviderLabel: no/empty/set/whitespace global provider
- effective-provider save gate: empty+no-global=blocked, empty+global=allowed,
  explicit=allowed
- computeLocalModeGate global-aware required key: anthropic key appears when
  global provider=anthropic and key missing; absent when satisfied globally

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…del/provider fields

AgentModelField hardcoded id="agent-model" on both its label and select.
GlobalAgentConfigSettingsCard renders AgentModelField alongside CreateAgentDialog
on the same page — once the get_global_agent_config mock was fixed to resolve
instead of throw, the settings card rendered its AgentModelField and the DOM
had two elements with id="agent-model", causing strict-mode violations in any
page-wide locator('#agent-model') call.

Add an optional id prop to AgentModelField (defaulting to "agent-model" so
all existing callers are unaffected), and pass id="global-agent-model" at
the GlobalAgentConfigSettingsCard call site. The settings card's inline
provider field already used id="global-agent-provider" — this brings the
model field into parity and eliminates duplicate DOM ids entirely.

openEditAgentEvent.ts deep-link focus targets reference "agent-model" and
"agent-provider" for the Edit Agent dialog fields specifically — those paths
are unaffected since the dialogs continue to use the default id.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…anges

Two spec updates reflecting intentional new behavior introduced by this branch:

1. Scope env-vars-editor queries to the dialog: GlobalAgentConfigSettingsCard
   (new in this branch) renders its own EnvVarsEditor in the background settings
   pane, so page-wide getByTestId('env-vars-editor') queries now see count >= 1
   even before the dialog's Advanced section is opened. Scoped all env-vars-*
   testid queries in both affected tests to page.getByRole('dialog').

2. Update post-OpenAI env-vars-editor assertion: the branch added auto-expand of
   Advanced when required env keys are present. Selecting OpenAI surfaces
   OPENAI_COMPAT_API_KEY as required and auto-opens Advanced — so the dialog's
   env-vars-editor is now visible (count=1) immediately after switching provider.
   Changed toHaveCount(0) to toHaveCount(1) + toBeVisible() to match.

3. Update 'Default' provider option name: getDefaultLlmProviderLabel was renamed
   from returning 'Default' to 'Select a provider...' (with Unicode ellipsis) when
   no global provider is set. Updated the selectDropdownOption call to match.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…e additions

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…guard

Post-#1540 the provider/model fields in EditAgentDialog render as
PersonaDropdownField (a <button> via DropdownMenuTrigger), not native
<select> elements. The deep-link focus effect had two compounding bugs:

1. Target IDs "agent-provider" / "agent-model" did not match the Edit
   JSX, which renders id="edit-agent-llm-provider" and id="edit-agent-model".
   getElementById returned null, silently skipping focus.

2. Guard "instanceof HTMLSelectElement" always bailed for a <button>, so
   focus never fired even if the IDs had been correct.

Fix: update targetIds to match the rendered Edit-specific IDs; relax
the guard to instanceof HTMLElement (focus + scrollIntoView both exist
on HTMLElement). All other parts of the effect (llmProviderFieldVisible
bail, rAF scroll/focus, one-shot ref) are unchanged.

Add editAgentNormalizedFieldFocus.test.mjs (7 tests) covering provider
and model field focus dispatch, legacy-ID regression guard, lazy
provider-field visibility, one-shot guard, env_key passthrough, and
closed-dialog no-op.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Add a read-only "Baked build defaults" section to GlobalAgentConfigSettingsCard
that shows the compile-time baked agent env (BUZZ_AGENT_PROVIDER, BUZZ_AGENT_MODEL,
and any other keys from baked_build_env()) with secret keys masked.

Backend (agent_config.rs):
- Add BakedEnvEntry struct { key, value, masked } (serde::Serialize)
- Add is_secret_key() heuristic: key contains _API_KEY, _TOKEN, _SECRET, _PASSWORD
  (case-insensitive); non-secret values shown as-is
- Add get_baked_build_env() Tauri command: filters empty values, masks secrets
  (replacement done in Rust so unmasked values never cross the IPC boundary),
  returns Vec<BakedEnvEntry>
- Register command in lib.rs alongside get_baked_build_env_keys
- 8 unit tests: non-secret shown, api_key/token/secret/password masked, empty
  filtered, mixed keys, case-insensitive heuristic

Frontend (tauri.ts):
- Add BakedEnvEntry type and getBakedBuildEnv() async binding
- Bump tauri.ts file-size override (1388 -> 1416)

Frontend (GlobalAgentConfigSettingsCard.tsx):
- Load baked env on mount via fire-and-forget effect (silent catch = OSS no-op)
- Render "Baked build defaults" section below env-vars editor, only when
  bakedEnv.length > 0 (OSS builds return [] => zero visual change)
- Copy: "Set by your build. Override any of these above."
- Masked rows rendered at text-muted-foreground/50 to distinguish from real values

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…r rebase

Semantic integration fixups after rebasing onto origin/main (48bd8ab):

1. spawn_hash.rs: spawn_config_hash now takes &GlobalAgentConfig so the
   restart-required hash covers global env changes (a global default
   change must trip the badge). Both call sites in runtime.rs updated:
   - build_managed_agent_summary: loads global config for the badge check
   - spawn_agent_child: reuses the already-loaded global at line 1525

2. Test struct completions: ManagedAgentRecord in agents_tests.rs and
   global_config/tests.rs was missing the fields added by Phase 1A.1
   (display_name, slug, runtime, name_pool, is_builtin, is_active,
   source_team, source_team_persona_slug). Added with sensible defaults
   matching the existing spawn_hash/tests.rs pattern.

3. spawn_hash/tests.rs: all spawn_config_hash call sites updated to pass
   &Default::default() as the global arg (tests don't need global env).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…-env

Three fixes from Thufir's re-review of the rebased tip:

1. global_agent_config: use record_agent_command instead of effective_agent_command
   - Both call sites in collect_respawn_candidates and restart_setup_listener_agent
     now use record_agent_command(record, &all_personas), which resolves via
     record.runtime (override → record.runtime → persona fallback) rather than
     the persona-first path that ignored ManagedAgentRecord.runtime.
   - A global-config save that triggers a NotReady→Ready transition now evaluates
     readiness against the runtime the agent actually spawns with.
   - Adds regression test: record.runtime="claude" wins over persona.runtime="goose",
     asserting the record-first command is used.

2. agent_config: invert baked-env masking to allowlist (default-deny)
   - Replaces is_secret_key heuristic (contains _API_KEY/_TOKEN etc.) with
     is_safe_to_reveal allowlist: BUZZ_AGENT_PROVIDER, BUZZ_AGENT_MODEL,
     DATABRICKS_HOST, DATABRICKS_MODEL.
   - Everything not in the allowlist is masked; bare names like APIKEY, TOKEN,
     SECRET, PASSWORD, PRIVATE_KEY are now correctly masked where the old
     heuristic would have leaked them.
   - Updates and expands the 8 existing masking tests to cover bare-name cases.

3. GlobalAgentConfigSettingsCard: friendly labels for baked provider/model
   - BUZZ_AGENT_PROVIDER renders as "Baked provider", BUZZ_AGENT_MODEL as
     "Baked model"; all other keys render as raw key names unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…-size override

runtime.rs: wrap the merged_user_env call introduced in the #1639/1640 rebase
conflict resolution — line exceeded rustfmt's limit.

check-file-sizes.mjs: add override for AgentInstanceEditDialog.tsx (1043 lines).
#1639 renamed EditAgentDialog to AgentInstanceEditDialog; #1448 rebase conflict
resolution threaded initialFocus?/EditAgentFocusTarget through AgentDialog.tsx
into AgentInstanceEditDialog.tsx, adding ~7 lines of feature plumbing.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…us prop

#1448 extends AgentDialogInstanceEditProps with initialFocus to support
deep-link focus in instance-edit mode. AgentDialog explicitly forwards
initialFocus={props.initialFocus} to AgentInstanceEditDialog, so the
contract test's deepEqual must include the prop (undefined when not
passed by the caller).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Previously providerValid blocked Save whenever the LLM-provider field
was visible and no effective provider existed (neither per-agent nor
global). This prevented renaming or editing timeout on any legacy goose
agent that never had a provider configured — a regression from main's
behavior where provider visibility was UX-only.

Narrow to Will's (b): an agent that never had a provider stays fully
editable. Only block when the user ACTIVELY clears a provider the agent
originally had AND no global fallback covers it.

Extract isEditAgentProviderSaveValid() as an exported pure helper so
the three-way logic (field hidden / effective resolves / never had one)
is testable in isolation. Add editAgentProviderSaveValid.test.mjs with
8 cases covering the no-provider agent, global-fallback, clear-existing,
and whitespace-trimming paths.

Bump AgentInstanceEditDialog.tsx file-size override 1043 → 1076 to
account for the extracted helper function (feature logic, not debt).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The hadProvider=false exception in isEditAgentProviderSaveValid was too
broad: it could not distinguish a legacy no-provider agent staying on
its provider-capable runtime (safe name/timeout edits) from a user
actively switching a non-provider-runtime agent INTO a provider-capable
runtime without setting a provider — the latter would persist a
provider-broken agent.

Add originalRuntimeSupportsProvider to the helper's contract. Derive it
at the call site from agent.agentCommand matched against the runtime
catalog — a stable snapshot that does not mutate as the user edits the
dropdown. The no-provider exception now only applies when the original
runtime already supported provider selection (legacy state); switching
INTO a provider-capable runtime from a non-provider one requires setting
a provider.

Update editAgentProviderSaveValid.test.mjs: add the new param to all
existing cases, add two new cases for the runtime-switch scenario (no
global → blocked; global covers → allowed).

Bump AgentInstanceEditDialog.tsx file-size override 1076 → 1103.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Keep the provider save-gate's original runtime signal tied to the dialog-opening agent command while still resolving against the runtime catalog as it loads. This prevents prop refreshes or edit-state runtime ids from changing the legacy no-provider exception after the dialog is open.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Defect 1: useRequiredCredentialState used only the per-agent provider
when computing providerForRequiredKeys, with no global fallback. When a
global provider was set but the per-agent provider was empty, the hook
yielded no required keys — so ANTHROPIC_API_KEY never showed as amber in
Create/Edit Agent dialogs. Fix: add globalProvider param (optional,
defaults to ''), use (provider || globalProvider).trim() as the
effective provider. Caller (AgentInstanceEditDialog) already held
globalConfig — wired it in and moved the useGlobalAgentConfig call
above the hook invocation to satisfy forward-reference ordering.

Defect 2: the Advanced section auto-expanded only on requiredEnvKeyMissing
(missing→present transition). A fully-satisfied buzz-agent instance had
no missing key, so Advanced stayed collapsed and the model-tuning knobs
were unreachable without manual expand. Fix: add a parallel once-per-open
auto-expand effect in both CreateAgentDialog and AgentInstanceEditDialog
that fires whenever isBuzzAgentRuntime(prospectiveRuntimeId/selectedRuntimeId)
is true, independent of key-missing state.

Defect 3: provider and model inherit-option copy. getDefaultLlmProviderLabel
returned 'Inherit (<provider>)'; changed to 'Inherit global default (<provider>)'.
Added getDefaultLlmModelLabel with the same pattern ('Inherit global default (<model>)'
when global model is set, 'Default model' otherwise) and threaded it through
AgentModelField (CreateAgentDialog path) and the inline staticModelOptions
in AgentInstanceEditDialog. Updated provider-label tests and added
4 new model-label tests (2218 total, 0 fail).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…Agent

The requiredEnvKeyMissing flag in useRequiredCredentialState was computed
using only per-agent envVars, ignoring globalConfig.env_vars. When a key
was satisfied exclusively at the global layer (e.g. ANTHROPIC_API_KEY set
in global config), the display filter in AgentInstanceEditDialog correctly
hid the amber row, but requiredEnvKeyMissing remained true and silently
disabled Save with no visible reason (the same Test-4 family defect, now
at the gate rather than the display).

Add globalEnvVars param (default {}) to useRequiredCredentialState. Wire
globalEnvVars: globalConfig.env_vars from AgentInstanceEditDialog. Exclude
globally-satisfied keys from requiredEnvKeys inside the hook itself so that
both the amber-row list and the save-gate marker are computed from a single
source of truth — display and gate can no longer drift.

Add editAgentRequiredCredentials.test.mjs with 4 cases covering the core
precedence: global+per-agent key -> not missing; global present, per-agent
empty -> not missing; global absent, per-agent filled -> not missing;
global absent, per-agent empty -> still missing (gate must block).

Also add buzz-agent model-tuning knobs (BuzzAgentModelTuningFields) to
the template dialogs (AgentDefinitionDialog / PersonaAdvancedFields).
Templates previously lacked the thinking/effort/max-rounds/max-output-
tokens controls available in agent instance dialogs. Add modelTuningRuntimeId
and inheritedEnvVars params to PersonaAdvancedFields; pass runtime and
globalConfig.env_vars from AgentDefinitionDialog. Add a once-per-open
auto-expand effect in AgentDefinitionDialog for isBuzzAgentRuntime(runtime)
so the tuning knobs are immediately reachable — mirrors the agent instance
path. The inherited baseline for template tuning placeholders is
globalConfig.env_vars (the layer templates fall through to).

Bump AgentInstanceEditDialog.tsx file-size override 1118 -> 1125.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Three test struct literals missed the new auto_restart_on_config_change
field added by the auto-restart PR (#1649) that landed on main before this
rebase. Also wire State import in agent_settings.rs which the auto-restart
command requires.

This is a purely mechanical rebase-compatibility fix — no product logic
change.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… template dialogs

AgentDefinitionDialog was computing model requiredness and model-option
scoping from the local provider only, ignoring globalConfig.provider and
the file-config fallback — the same split-source-of-truth class as F1 but
on the model axis.

Three concrete failures (Thufir pass-2):
1. local provider=anthropic, global model set, local model blank →
   template marked model required and disabled Save (wrong: gate satisfied
   by global model).
2. local provider blank, global provider=anthropic, global model blank →
   template allowed Save into a NotReady config (wrong).
3. global model set → template dropdown showed 'Default model' instead of
   'Inherit global default (<model>)'.

Fixes:
- Add getDefaultLlmModelLabel to imports; use it for the zero-value model
  dropdown option so the inherit label is visible in template dialogs.
- Remove providerForModelScope (local-only). Derive effectiveProvider =
  trimmedProvider || globalConfig.provider || fileProvider, mirroring the
  chain inside computeLocalModeGate, and use it for model-option scoping
  (usePersonaModelDiscovery, getPersonaModelOptions,
  shouldClearKnownModelForSelectionScope).
- Replace isExplicitModelRequired with the effectiveProvider-based version
  (static asterisk on the model label now reflects the effective chain).
- Replace the canSubmit model/provider guards with
  missingNormalizedFields.length === 0 — single source of truth with the
  readiness gate so display and Save can never drift again.
- getPersonaProviderOptions first arg stays trimmedProvider (the local
  selection, needed to render the correct current-value custom option in
  the provider dropdown).
- 3 new tests in createAgentLocalModeGate.test.mjs pinning the three
  failure cases above.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…r explicit-model providers

For providers that require an explicit model (e.g. anthropic),
getPersonaModelOptions filters out the zero-value option. The previous
inline map in AgentDefinitionDialog only relabelled an existing id=''
option, so for anthropic the inherit-global entry was never rendered.

Extract buildTemplateModelDropdownOptions to personaDialogPickers.tsx.
The helper guard-prepends { id: '', label: getDefaultLlmModelLabel(globalModel) }
when globalModel is non-empty AND no zero-value option already exists in
the discovered/static list. This makes the 'Inherit global default (<model>)'
entry visible and selectable for explicit-model providers when the global
model satisfies the requirement — without seeding an empty inherit option
when no global model is set (case 2 block preserved).

Three F3b acceptance tests added to createAgentLocalModeGate.test.mjs,
exercising the full option-list composition (not just getDefaultLlmModelLabel
in isolation): anthropic+global model set → inherit entry present; anthropic+no
global model → no zero-value entry; blank provider+global model → no double-seed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…#1633

#1633 added AcpAvailabilityStatus to the types import in readiness.rs
alongside our GlobalAgentConfig import, bringing the merged line count
to 1405 (gate) vs the prior 1403 cap. Ratchet to 1408.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…pi-key field

Replace the two divergent provider->env-key mappings
(requiredCredentialEnvKeys + getProviderApiKeyConfig) with a single
PROVIDER_CREDENTIAL_CONFIG table in personaDialogPickers.tsx. Each entry
carries requiredEnvKeys (keys the gate surfaces as amber rows) and an
optional secretEnvVar (the typed secret cleared on provider switch).
Databricks has no secretEnvVar — DATABRICKS_HOST is a URL cleared by
OAuth PKCE, not a secret credential.

Delete PersonaProviderApiKeyField.tsx and its filter at line 976-978 of
AgentDefinitionDialog. The filter was the root cause of T2: it removed
ANTHROPIC_API_KEY from the required-row list when the provider matched
getProviderApiKeyConfig (anthropic/openai), while DATABRICKS_HOST survived
because it had no entry there. With the filter gone, all providers produce
amber required rows from the same gate — consistent with the instance
dialogs.

Remove envVarsMergingAdvancedEdit (only consumer was AgentDefinitionDialog;
the dedicated field it protected is gone). Remove hasAdvancedEnvVars and
getAdvancedEnvVars from personaDialogEnvVars.ts (same). PersonaAdvancedFields
now receives envVars directly and setEnvVars as its onChange callback.

Update persona-env-vars.spec.ts:
- Test at :265: Advanced is now auto-expanded on buzz-agent open (T3 pass
  confirmed this is intended); remove the collapsed-on-open assertion and
  the click-to-expand step.
- Test at :315: remove persona-provider-api-key testid assertions; verify
  OPENAI_COMPAT_API_KEY and ANTHROPIC_API_KEY as amber required rows in
  EnvVarsEditor instead.

Add 3 regression tests in createAgentLocalModeGate.test.mjs covering:
- explicit anthropic → ANTHROPIC_API_KEY in requiredCredentialEnvKeys
- inherit→explicit anthropic switch preserves required row
- databricks → secretEnvVar is null (no provider-switch clear)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…eyConfig fn, envVarsWithProviderApiKey fn

All three were left after the PROVIDER_CREDENTIAL_CONFIG unification in the
previous commit. None had any production callers:

- ProviderApiKeyConfig and getProviderApiKeyConfig were only referenced by
  each other (the type in the function signature).
- envVarsWithProviderApiKey was only referenced by its own test file.

Leaving them was the exact class of drift that caused the T2 defect: two
independent provider→env-key mappings that could silently diverge. Remove
all three to enforce the single source of truth.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Rebased onto origin/main (fc8a174) which introduced last_error_code
on ManagedAgentRecord. Two test-only struct initializers used exhaustive
init syntax and were missing the new field — cargo test failed to
compile. Add last_error_code: None to each.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 force-pushed the duncan/global-agent-config branch from 014032c to aefde06 Compare July 8, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant