diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index 1ab4ddf65..f7cc56a69 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -96,6 +96,8 @@ with a TypeScript lookup table or an id comparison in a component. editor persists its visible fallback on the next save. Its dependent provider/model defaults are also ignored for new implicit fallback agents, without changing the persisted configuration used by existing agents. + Create-mode dialogs use the implicit masked config; existing definition and + instance edit dialogs use the raw persisted config. `resolvePersonaRuntime` is the shared visibility boundary for every runtime-less deployment or provisioning path. Definition-to-instance starts use the stricter `resolveStartRuntimeForDefinition` wrapper; call one of diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx index 888ea9fc1..4557de3d7 100644 --- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx @@ -68,7 +68,7 @@ import { usePersonaModelDiscovery, } from "./usePersonaModelDiscovery"; import { useBakedBuildEnvKeysQuery, useRuntimeFileConfigQuery } from "../hooks"; -import { useAgentDialogDefaults } from "./useAgentDialogDefaults"; +import { useDefinitionAgentDialogDefaults } from "./useAgentDialogDefaults"; import { AgentDefaultsDialog } from "./AgentDefaultsDialog"; import { AgentHarnessField } from "./AgentHarnessField"; import { @@ -164,7 +164,7 @@ export function AgentDefinitionDialog({ model: inheritedModelDefault, }, inheritedEnvVars: inheritedEnvVarsForAdvanced, - } = useAgentDialogDefaults({ open }); + } = useDefinitionAgentDialogDefaults(initialValues, open); const selectableRuntimes = useSelectableAcpRuntimes(runtimes); const defaultRuntime = getDefaultPersonaRuntime( selectableRuntimes, diff --git a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx index 52edfcf18..1f8e140f4 100644 --- a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx +++ b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx @@ -367,7 +367,11 @@ export function AgentInstanceEditDialog({ model: inheritedModelDefault, }, inheritedEnvVars: inheritedEnvVarsForAdvanced, - } = useAgentDialogDefaults({ inheritedEnvVars, open }); + } = useAgentDialogDefaults({ + configScope: "existing", + inheritedEnvVars, + open, + }); // Runtime/provider-required credential state, derived from the PROSPECTIVE // post-submit runtime — see the hook for the inherit-transition rationale. diff --git a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs index ffa680006..ed71f6477 100644 --- a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs +++ b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs @@ -207,6 +207,20 @@ test("reconcilePreferredRuntimeFallback updates a hidden saved default", () => { assert.equal(reconcilePreferredRuntimeFallback(config, "goose"), config); }); +test("reconcilePreferredRuntimeFallback persists an unsaved displayed fallback", () => { + const config = { + env_vars: { SHARED: "kept" }, + provider: "relay-mesh", + model: "auto", + preferred_runtime: null, + }; + + assert.deepEqual(reconcilePreferredRuntimeFallback(config, "buzz-agent"), { + ...config, + preferred_runtime: "buzz-agent", + }); +}); + // ── getPersonaModelOptions — codex/claude do not use global provider ────────── // // The discovery call in AgentDefinitionDialog passes diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx index 120098c16..2eecb3671 100644 --- a/desktop/src/features/agents/ui/agentConfigOptions.tsx +++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx @@ -199,21 +199,25 @@ export function resetConfigForHarnessChange( } /** - * Align a stale or hidden saved preference with the fallback shown by a - * runtime selector, clearing values that are not portable across harnesses. + * Align a missing, stale, or hidden saved preference with the shown fallback. + * A missing preference adopts the current context without clearing its draft; + * switching away from a saved harness clears values that are not portable. */ export function reconcilePreferredRuntimeFallback( config: GlobalAgentConfig, fallbackRuntimeId: string | null, ): GlobalAgentConfig { - if ( - !config.preferred_runtime || - !fallbackRuntimeId || - config.preferred_runtime === fallbackRuntimeId - ) { + if (!fallbackRuntimeId || config.preferred_runtime === fallbackRuntimeId) { return config; } + if (!config.preferred_runtime) { + return { + ...config, + preferred_runtime: fallbackRuntimeId, + }; + } + return resetConfigForHarnessChange(config, fallbackRuntimeId); } diff --git a/desktop/src/features/agents/ui/useAgentDialogDefaults.test.mjs b/desktop/src/features/agents/ui/useAgentDialogDefaults.test.mjs new file mode 100644 index 000000000..12e0a02b2 --- /dev/null +++ b/desktop/src/features/agents/ui/useAgentDialogDefaults.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + agentDefinitionConfigScope, + resolveAgentDialogGlobalConfig, +} from "./useAgentDialogDefaults.ts"; + +const persistedConfig = { + env_vars: { SHARED: "kept" }, + provider: "relay-mesh", + model: "auto", + preferred_runtime: "buzz-agent", +}; + +test("create-mode defaults mask values owned by a hidden harness", () => { + assert.deepEqual( + resolveAgentDialogGlobalConfig(persistedConfig, "implicit", ["buzz-agent"]), + { + env_vars: { SHARED: "kept" }, + provider: null, + model: null, + preferred_runtime: null, + }, + ); +}); + +test("existing edit defaults preserve values owned by a hidden harness", () => { + assert.equal( + resolveAgentDialogGlobalConfig(persistedConfig, "existing", ["buzz-agent"]), + persistedConfig, + ); +}); + +test("definition dialogs select config scope from create versus edit values", () => { + assert.equal(agentDefinitionConfigScope(null), "implicit"); + assert.equal(agentDefinitionConfigScope({ displayName: "New" }), "implicit"); + assert.equal( + agentDefinitionConfigScope({ id: "existing", displayName: "Existing" }), + "existing", + ); +}); diff --git a/desktop/src/features/agents/ui/useAgentDialogDefaults.ts b/desktop/src/features/agents/ui/useAgentDialogDefaults.ts index 5ede9558f..f9fbbd31c 100644 --- a/desktop/src/features/agents/ui/useAgentDialogDefaults.ts +++ b/desktop/src/features/agents/ui/useAgentDialogDefaults.ts @@ -1,18 +1,57 @@ import * as React from "react"; +import type { + CreatePersonaInput, + GlobalAgentConfig, + UpdatePersonaInput, +} from "@/shared/api/types"; import { useBakedBuildEnvQuery } from "../hooks"; +import { + maskDisabledAcpRuntimePreference, + useDisabledAcpRuntimeIds, +} from "../lib/runtimeVisibilityPreference"; import { useGlobalAgentConfig } from "../useGlobalAgentConfig"; import { BUZZ_AGENT_THINKING_EFFORT } from "./buzzAgentConfig"; import { getInheritedAgentDefaults } from "./bakedEnvHelpers"; +export type AgentDialogConfigScope = "existing" | "implicit"; + +export function agentDefinitionConfigScope( + initialValues: CreatePersonaInput | UpdatePersonaInput | null, +): AgentDialogConfigScope { + return initialValues && "id" in initialValues ? "existing" : "implicit"; +} + +export function resolveAgentDialogGlobalConfig( + persistedConfig: GlobalAgentConfig, + configScope: AgentDialogConfigScope, + disabledRuntimeIds: readonly string[], +): GlobalAgentConfig { + return configScope === "implicit" + ? maskDisabledAcpRuntimePreference(persistedConfig, disabledRuntimeIds) + : persistedConfig; +} + export function useAgentDialogDefaults({ + configScope, inheritedEnvVars = {}, open, }: { + configScope: AgentDialogConfigScope; inheritedEnvVars?: Record; open: boolean; }) { - const { globalConfig } = useGlobalAgentConfig(); + const { globalConfig: persistedConfig } = useGlobalAgentConfig(); + const disabledRuntimeIds = useDisabledAcpRuntimeIds(); + const globalConfig = React.useMemo( + () => + resolveAgentDialogGlobalConfig( + persistedConfig, + configScope, + disabledRuntimeIds, + ), + [configScope, disabledRuntimeIds, persistedConfig], + ); const { data: bakedEnv } = useBakedBuildEnvQuery({ enabled: open }); const inheritedDefaults = getInheritedAgentDefaults(globalConfig, bakedEnv); const effectiveInheritedEnvVars = React.useMemo( @@ -31,3 +70,13 @@ export function useAgentDialogDefaults({ inheritedEnvVars: effectiveInheritedEnvVars, }; } + +export function useDefinitionAgentDialogDefaults( + initialValues: CreatePersonaInput | UpdatePersonaInput | null, + open: boolean, +) { + return useAgentDialogDefaults({ + configScope: agentDefinitionConfigScope(initialValues), + open, + }); +}