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 <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-09-02 18:33:31 -04:00
parent 08d1193271
commit a950da2a3c
4 changed files with 49 additions and 6 deletions

View File

@@ -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<string | null>(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}

View File

@@ -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({
</div>
) : null}
<EffortPickerField agent={inst} config={effortConfig} />
<EffortPickerField
agent={inst}
config={effortConfig}
disabled={isSaving}
value={effortValue}
onChange={onEffortChange}
/>
<AgentAiDefaultsNotice
onEditDefaults={() => onAiDefaultsOpenChange(true)}

View File

@@ -66,6 +66,8 @@ function makeDefinitionOnlyState(definition, overrides = {}) {
defReadOnly: false,
inheritedSubmissionProvider: null,
runtimes: [],
effortLevel: null,
effortTouched: { current: false },
...overrides,
};
}

View File

@@ -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<boolean>;
};
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);