From a950da2a3ca2d8c669c1dc830b21e806f759f5c0 Mon Sep 17 00:00:00 2001 From: Duncan Date: Wed, 2 Sep 2026 18:33:31 -0400 Subject: [PATCH] fix(agents): wire controlled effort state through AgentEditMergedDialog EffortPickerField gained required disabled/value/onChange props in #4625. Wire them through the merged dialog using the same Save-gated pattern as the now-deleted AgentInstanceEditDialog: effortLevel state + effortTouched ref in AgentEditMergedDialog, passed as effortValue/onEffortChange to the instance section, which forwards to EffortPickerField. In useAgentEditMergedSubmit, add effortLevel/effortTouched to AgentEditSubmitState and include effortLevel in the locked update_managed_agent call when touched. When no other I-fields changed (agentInput would be null), synthesise a minimal input from inst.pubkey so the effort write is still atomic. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../agents/ui/AgentEditMergedDialog.tsx | 18 +++++++++++++++ .../AgentEditMergedDialogInstanceSection.tsx | 13 ++++++++++- .../ui/useAgentEditMergedSubmit.test.mjs | 2 ++ .../agents/ui/useAgentEditMergedSubmit.ts | 22 ++++++++++++++----- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/desktop/src/features/agents/ui/AgentEditMergedDialog.tsx b/desktop/src/features/agents/ui/AgentEditMergedDialog.tsx index c629a52e5b..47c59df417 100644 --- a/desktop/src/features/agents/ui/AgentEditMergedDialog.tsx +++ b/desktop/src/features/agents/ui/AgentEditMergedDialog.tsx @@ -219,6 +219,10 @@ export function AgentEditMergedDialog({ const [iIsCustomProviderEditing, setIIsCustomProviderEditing] = React.useState(false); const runtimeTouched = React.useRef(false); + // Effort picker is Save-gated: hold pending selection in dialog state. + // `effortTouched` distinguishes "user picked a value" from "seeded display". + const [effortLevel, setEffortLevel] = React.useState(null); + const effortTouched = React.useRef(false); // Convenience: the active model/provider — D-state when a definition is present, // I-state when instance-only (no definition). Used for the D-section and for @@ -315,6 +319,8 @@ export function AgentEditMergedDialog({ setShowAdvancedFields(false); setIsAvatarUploadPending(false); runtimeTouched.current = false; + setEffortLevel(null); + effortTouched.current = false; autoSeededDefinitionRuntimeRef.current = null; // Capture the definition revision the form is baselined against. seededDefinitionUpdatedAtRef.current = def?.updatedAt ?? null; @@ -548,6 +554,8 @@ export function AgentEditMergedDialog({ onValidate, onOpenChange, onUpdated, + effortLevel, + effortTouched, } satisfies AgentEditSubmitState; const { @@ -921,6 +929,16 @@ export function AgentEditMergedDialog({ systemPrompt={systemPrompt} onSystemPromptChange={setSystemPrompt} effortConfig={configSurfaceQuery.data} + effortValue={ + effortTouched.current + ? effortLevel + : (configSurfaceQuery.data?.normalized.thinkingEffort + ?.value ?? null) + } + onEffortChange={(level) => { + effortTouched.current = true; + setEffortLevel(level); + }} /> ) : null} diff --git a/desktop/src/features/agents/ui/AgentEditMergedDialogInstanceSection.tsx b/desktop/src/features/agents/ui/AgentEditMergedDialogInstanceSection.tsx index 1e08acf239..df2facbdfa 100644 --- a/desktop/src/features/agents/ui/AgentEditMergedDialogInstanceSection.tsx +++ b/desktop/src/features/agents/ui/AgentEditMergedDialogInstanceSection.tsx @@ -129,6 +129,9 @@ export type AgentEditMergedISectionProps = { systemPrompt: string; onSystemPromptChange: (value: string) => void; effortConfig: RuntimeConfigSurface | undefined; + /** Controlled pending effort selection (null = adapter default). */ + effortValue: string | null; + onEffortChange: (level: string | null) => void; }; // ── Component ───────────────────────────────────────────────────────────────── @@ -208,6 +211,8 @@ export function AgentEditMergedInstanceSection({ systemPrompt, onSystemPromptChange, effortConfig, + effortValue, + onEffortChange, }: AgentEditMergedISectionProps) { const shouldReduceMotion = useReducedMotion(); const advancedFieldsTransition = shouldReduceMotion @@ -436,7 +441,13 @@ export function AgentEditMergedInstanceSection({ ) : null} - + onAiDefaultsOpenChange(true)} diff --git a/desktop/src/features/agents/ui/useAgentEditMergedSubmit.test.mjs b/desktop/src/features/agents/ui/useAgentEditMergedSubmit.test.mjs index 16467731f8..41648ddbfb 100644 --- a/desktop/src/features/agents/ui/useAgentEditMergedSubmit.test.mjs +++ b/desktop/src/features/agents/ui/useAgentEditMergedSubmit.test.mjs @@ -66,6 +66,8 @@ function makeDefinitionOnlyState(definition, overrides = {}) { defReadOnly: false, inheritedSubmissionProvider: null, runtimes: [], + effortLevel: null, + effortTouched: { current: false }, ...overrides, }; } diff --git a/desktop/src/features/agents/ui/useAgentEditMergedSubmit.ts b/desktop/src/features/agents/ui/useAgentEditMergedSubmit.ts index 463399f5d1..b424e5c255 100644 --- a/desktop/src/features/agents/ui/useAgentEditMergedSubmit.ts +++ b/desktop/src/features/agents/ui/useAgentEditMergedSubmit.ts @@ -93,6 +93,10 @@ export type AgentEditSubmitState = { onValidate?: () => string | null; onOpenChange: (open: boolean) => void; onUpdated?: (agent: ManagedAgent) => void; + /** Controlled pending effort selection from the dialog (null = adapter default). */ + effortLevel: string | null; + /** Ref tracking whether the user has made an explicit effort selection. */ + effortTouched: React.RefObject; }; export type AgentEditSubmitHookReturn = { @@ -271,11 +275,19 @@ export function useAgentEditMergedSubmit( const seed = seedAgentFormModel(s.ctx); const next = buildNextAgentFormModel(seed, s); - const { personaInput, agentInput, policySets } = emitAgentFormDiff( - seed, - next, - s.ctx, - ); + const { + personaInput, + agentInput: rawAgentInput, + policySets, + } = emitAgentFormDiff(seed, next, s.ctx); + + // Effort write: when touched, resolve and include in the locked update + // (atomic with any access-policy change — PR #4625 semantics). + let agentInput = rawAgentInput; + if (s.effortTouched.current && inst) { + agentInput = agentInput ?? { pubkey: inst.pubkey }; + agentInput = { ...agentInput, effortLevel: s.effortLevel }; + } const refetchStores = () => refetchAgentStores(queryClient, def, inst);