From 29c9dd99a4a7d210c97af475a18fc030d40f58d5 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 2 Jul 2026 20:20:27 -0500 Subject: [PATCH] fix(desktop): autosave Mixture-of-Agents preset edits MoA was internally inconsistent: preset-level ops (set default / add / delete) persisted on click, but reference-model and aggregator slot edits sat behind a manual Save button. Debounce-persist slot/aggregator edits like the rest of settings and drop the redundant button, so MoA is uniformly autosave. --- .../src/app/settings/model-settings.tsx | 70 ++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/apps/desktop/src/app/settings/model-settings.tsx b/apps/desktop/src/app/settings/model-settings.tsx index 040781942e29..fa7d15a5d1ed 100644 --- a/apps/desktop/src/app/settings/model-settings.tsx +++ b/apps/desktop/src/app/settings/model-settings.tsx @@ -298,23 +298,62 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) { return moa.presets[selectedMoaPreset] || moa.presets[moa.default_preset] || Object.values(moa.presets)[0] || null }, [moa, selectedMoaPreset]) + // Mirror of `moa` so inline edits compute the next state purely (outside the + // setState updater) and hand it straight to the debounced autosave. + const moaRef = useRef(null) + + useEffect(() => { + moaRef.current = moa + }, [moa]) + + const moaSaveTimer = useRef(null) + + useEffect( + () => () => { + if (moaSaveTimer.current) { + window.clearTimeout(moaSaveTimer.current) + } + }, + [] + ) + + // Quiet debounced persist for inline MoA edits — mirrors the config page's + // autosave so slot/aggregator tweaks save themselves, matching the + // preset-level ops (set default / add / delete) that already persist on + // click. No `applying` spinner, so selecting stays responsive. + const scheduleMoaSave = useCallback((next: MoaConfigResponse) => { + if (moaSaveTimer.current) { + window.clearTimeout(moaSaveTimer.current) + } + + moaSaveTimer.current = window.setTimeout(() => { + void saveMoaModels(next) + .then(setMoa) + .catch(err => setError(err instanceof Error ? err.message : String(err))) + }, 600) + }, []) + const updateMoaPreset = useCallback( (updater: (preset: NonNullable) => NonNullable) => { - setMoa(prev => { - if (!prev || !selectedMoaPreset || !prev.presets[selectedMoaPreset]) { - return prev - } + const prev = moaRef.current - return { - ...prev, - presets: { - ...prev.presets, - [selectedMoaPreset]: updater(prev.presets[selectedMoaPreset]) - } + if (!prev || !selectedMoaPreset || !prev.presets[selectedMoaPreset]) { + return + } + + const next: MoaConfigResponse = { + ...prev, + presets: { + ...prev.presets, + [selectedMoaPreset]: updater(prev.presets[selectedMoaPreset]) } - }) + } + + moaRef.current = next + setMoa(next) + scheduleMoaSave(next) }, - [selectedMoaPreset] + [scheduleMoaSave, selectedMoaPreset] ) const updateMoaSlot = useCallback((slot: MoaModelSlot, patch: Partial): MoaModelSlot => { @@ -841,12 +880,7 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) { {moa && currentMoaPreset && (
-
- - -
+

Configure named presets that appear as models under the Mixture of Agents provider. The aggregator is the acting model.