fix(desktop): submit only edited fields from the full-config modal

Unstored fields render their schema default, and 'Save all' persisted every
one of them — pinning values that runtime defaults still own. Concretely:
an existing Honcho user without an explicit observationMode runs 'unified'
via the client's migration guard, but one untouched save-all flipped them to
the schema's 'directional'. Diff against the seeded snapshot and submit only
what the user changed.
This commit is contained in:
Erosika 2026-07-02 18:42:31 -04:00
parent a9cce6d844
commit 4d4db4281e
2 changed files with 15 additions and 13 deletions

View file

@ -80,20 +80,15 @@ describe('ProviderConfigModal', () => {
expect(screen.getByDisplayValue('{"t":"eri"}')).toBeTruthy()
})
it('saves all fields, serializing the toggled bool to "false"', async () => {
it('saves only edited fields, serializing the toggled bool to "false"', async () => {
const { onSaved, onOpenChange } = await renderModal()
fireEvent.click(await screen.findByRole('switch'))
fireEvent.click(screen.getByRole('button', { name: 'Save all' }))
fireEvent.click(screen.getByRole('button', { name: 'Save changes' }))
await waitFor(() =>
expect(saveMemoryProviderConfig).toHaveBeenCalledWith('honcho', {
workspace: 'myws',
saveMessages: 'false',
dialecticMaxChars: '1200',
userPeerAliases: '{"t":"eri"}'
})
)
// Untouched fields stay unsubmitted so a save never ratifies rendered
// defaults the backend does not actually store.
await waitFor(() => expect(saveMemoryProviderConfig).toHaveBeenCalledWith('honcho', { saveMessages: 'false' }))
await waitFor(() => expect(onSaved).toHaveBeenCalled())
expect(onOpenChange).toHaveBeenCalledWith(false)
})

View file

@ -53,20 +53,27 @@ export function ProviderConfigModal({
onSaved: () => Promise<void> | void
}) {
const [values, setValues] = useState<Record<string, string>>({})
const [seeded, setSeeded] = useState<Record<string, string>>({})
const [saving, setSaving] = useState(false)
// Reseed from the latest config each time the dialog opens so edits never
// start from a stale snapshot left over from a prior session.
useEffect(() => {
if (open) {
setValues(seedAll(config))
const seed = seedAll(config)
setSeeded(seed)
setValues(seed)
}
}, [open, config])
const save = async () => {
// Unstored fields render their schema default; persisting untouched keys
// would pin values that runtime defaults (e.g. migration guards) still own.
const edited = Object.fromEntries(Object.entries(values).filter(([key, value]) => value !== seeded[key]))
setSaving(true)
try {
await saveMemoryProviderConfig(provider, values)
await saveMemoryProviderConfig(provider, edited)
notify({ kind: 'success', title: `${config.label} saved`, message: 'Memory provider configuration updated.' })
await onSaved()
onOpenChange(false)
@ -138,7 +145,7 @@ export function ProviderConfigModal({
</DialogClose>
<Button disabled={saving} onClick={() => void save()} size="sm">
{saving ? <Loader2 className="size-3.5 animate-spin" /> : <Save />}
Save all
Save changes
</Button>
</DialogFooter>
</DialogContent>