diff --git a/desktop/src/features/agents/ui/AgentAiDefaults.tsx b/desktop/src/features/agents/ui/AgentAiDefaults.tsx index a577113a0a1..39b8d4af1d9 100644 --- a/desktop/src/features/agents/ui/AgentAiDefaults.tsx +++ b/desktop/src/features/agents/ui/AgentAiDefaults.tsx @@ -28,6 +28,7 @@ export function formatAiDefaultsSummary({ export function AgentAiDefaultsNotice({ isConfigured = true, onEditDefaults, + onEditDefinition, triggerRef, explicitModel, explicitProvider, @@ -37,6 +38,12 @@ export function AgentAiDefaultsNotice({ }: { isConfigured?: boolean; onEditDefaults: () => void; + /** + * Set only when a linked definition owns these values, which is also when + * the surface's own model/provider controls are read-only. Without it the + * summary would name the values and offer no way to reach them. + */ + onEditDefinition?: () => void; triggerRef?: React.Ref; explicitModel: string; explicitProvider: string; @@ -91,17 +98,36 @@ export function AgentAiDefaultsNotice({ {model || "Not configured"} - + {onEditDefinition ? ( +

+ Set on the agent definition and shared by every agent created from it. +

+ ) : null} +
+ + {onEditDefinition ? ( + + ) : null} +
); } diff --git a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx index 79d1e9a7907..dd775c1c9e1 100644 --- a/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx +++ b/desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx @@ -25,6 +25,7 @@ import { Dialog } from "@/shared/ui/dialog"; import { Input } from "@/shared/ui/input"; import { setManagedAgentAutoRestart } from "@/shared/api/tauriManagedAgents"; import { EditAgentAdvancedFields } from "./EditAgentAdvancedFields"; +import { EditAgentModelField } from "./EditAgentModelField"; import { AUTO_PROVIDER_DROPDOWN_VALUE, BLOCK_BUILD_HIDDEN_PROVIDER_IDS, @@ -49,6 +50,7 @@ import { } from "./relayMeshModelPicker"; import { computeEditAgentFormValidity, + definitionOwnsAiConfig, envVarsEqual, isEditAgentProviderSaveValid, resolveAgentCommandUpdate, @@ -152,6 +154,12 @@ export function AgentInstanceEditDialog({ : null, [agent.personaId, personasQuery.data], ); + const aiConfigIsDefinitionOwned = definitionOwnsAiConfig(linkedPersona); + // Model and LLM provider stay visible when the definition owns them — the + // value is the answer to "what does this agent run on" — but read-only, and + // the summary below routes the edit to the definition dialog. + const aiFieldsDisabled = + updateMutation.isPending || aiConfigIsDefinitionOwned; const inheritedEnvVars = linkedPersona?.envVars ?? {}; const [respondTo, setRespondTo] = React.useState( agent.respondTo, @@ -589,13 +597,27 @@ export function AgentInstanceEditDialog({ onOpenChange(next); } - const providerValid = isEditAgentProviderSaveValid({ - llmProviderFieldVisible, - currentProvider: provider, - originalProvider: agent.provider, - globalProvider: inheritedProviderDefault.value, - originalRuntimeSupportsProvider, - }); + // Definition-owned fields are edited in the definition dialog, which the + // caller opens; this form closes first so the two are never both mounted. + const handleEditLinkedPersona = onEditLinkedPersona + ? () => { + handleOpenChange(false); + onEditLinkedPersona(); + } + : undefined; + + // A field this form never sends cannot make it invalid. Switching harness + // can blank the provider draft, which would otherwise block Save on a + // read-only field the user has no way to repair. + const providerValid = + aiConfigIsDefinitionOwned || + isEditAgentProviderSaveValid({ + llmProviderFieldVisible, + currentProvider: provider, + originalProvider: agent.provider, + globalProvider: inheritedProviderDefault.value, + originalRuntimeSupportsProvider, + }); const canSubmit = computeEditAgentFormValidity({ @@ -679,35 +701,34 @@ export function AgentInstanceEditDialog({ ? parsedParallelism : undefined, // Linked instances defer model/provider/systemPrompt to the definition. - systemPrompt: - linkedPersona != null - ? undefined - : (systemPrompt.trim() || null) !== agent.systemPrompt - ? systemPrompt.trim() || null - : undefined, - model: - linkedPersona != null - ? undefined - : normalizedModel !== (agent.model ?? null) - ? normalizedModel - : undefined, + // Same predicate the controls above read, so a field can never be + // editable here and dropped on the wire. + systemPrompt: aiConfigIsDefinitionOwned + ? undefined + : (systemPrompt.trim() || null) !== agent.systemPrompt + ? systemPrompt.trim() || null + : undefined, + model: aiConfigIsDefinitionOwned + ? undefined + : normalizedModel !== (agent.model ?? null) + ? normalizedModel + : undefined, // Tri-state provider persistence keyed on providerRuntimeCapability: // "capable" → persist: value if changed, omit if unchanged. // "locked" → clear: send null if provider was set, else omit. // "unknown" → omit always (never send null for a transient state). // llmProviderFieldVisible is for UX visibility only; not used here. - provider: - linkedPersona != null - ? undefined - : providerRuntimeCapability === "capable" - ? normalizedSubmitProvider !== (agent.provider ?? null) - ? normalizedSubmitProvider + provider: aiConfigIsDefinitionOwned + ? undefined + : providerRuntimeCapability === "capable" + ? normalizedSubmitProvider !== (agent.provider ?? null) + ? normalizedSubmitProvider + : undefined + : providerRuntimeCapability === "locked" + ? (agent.provider ?? null) !== null + ? null : undefined - : providerRuntimeCapability === "locked" - ? (agent.provider ?? null) !== null - ? null - : undefined - : undefined, // "unknown" → omit always + : undefined, // "unknown" → omit always envVars: envVarsEqual(submitEnvVars, agent.envVars) ? undefined : submitEnvVars, @@ -887,13 +908,10 @@ export function AgentInstanceEditDialog({ onUploadPendingChange={setIsAvatarUploadPending} onSelectAvatar={setAvatarUrl} /> - {onEditLinkedPersona ? ( + {handleEditLinkedPersona ? (