Compare commits

...

1 Commits

Author SHA1 Message Date
Brooklyn Nicholson
61176c861e 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.
2026-07-02 20:20:27 -05:00

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -231,23 +231,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<MoaConfigResponse | null>(null)
useEffect(() => {
moaRef.current = moa
}, [moa])
const moaSaveTimer = useRef<number | null>(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<typeof currentMoaPreset>) => NonNullable<typeof currentMoaPreset>) => {
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>): MoaModelSlot => {
@@ -755,12 +794,7 @@ export function ModelSettings({ onMainModelChanged }: ModelSettingsProps) {
</section>
{moa && currentMoaPreset && (
<section>
<div className="mb-2.5 flex items-center justify-between">
<SectionHeading icon={Cpu} title="Mixture of Agents" />
<Button disabled={applying} onClick={() => void saveMoa(moa)} size="sm" variant="textStrong">
{applying ? m.applying : t.common.save}
</Button>
</div>
<SectionHeading icon={Cpu} title="Mixture of Agents" />
<p className="mb-2 text-xs text-muted-foreground">
Configure named presets that appear as models under the Mixture of Agents provider. The aggregator is the
acting model.