diff --git a/frontend/src/pages/settings/settings-provider.test.tsx b/frontend/src/pages/settings/settings-provider.test.tsx
index 3af4c6a3..ce5f3313 100644
--- a/frontend/src/pages/settings/settings-provider.test.tsx
+++ b/frontend/src/pages/settings/settings-provider.test.tsx
@@ -26,8 +26,6 @@ const enabled = {
qwen: true,
};
-const emptyProvider = { agents: {} };
-
const userDefined = [
{
agents: {},
@@ -47,13 +45,23 @@ const userDefined = [
},
];
+// The create form seeds agents from the type's defaults, and bails when the type has no models —
+// so a fixture with an empty catalogue would make the seeding tests vacuously green.
+const agentDefaults = (model: string, temperature: number) => ({
+ simple: { maxTokens: 1000, model, temperature },
+});
+
const settingsProviders = {
default: {
- anthropic: emptyProvider,
- openai: emptyProvider,
+ anthropic: { agents: agentDefaults('claude-e2e', 0.2) },
+ openai: { agents: agentDefaults('gpt-e2e', 0.7) },
},
enabled,
- models: { anthropic: [], minimax: [], openai: [] },
+ models: {
+ anthropic: [{ name: 'claude-e2e' }],
+ minimax: [],
+ openai: [{ name: 'gpt-e2e' }],
+ },
userDefined,
};
@@ -159,6 +167,47 @@ describe('SettingsProvider create-form type guards', () => {
expect(navigate).not.toHaveBeenCalled();
});
+ const expandAgent = () => {
+ if (!screen.queryByLabelText('Temperature')) {
+ fireEvent.click(screen.getByRole('button', { name: /Simple/ }));
+ }
+ };
+
+ const temperatureInput = () => screen.getByLabelText('Temperature') as HTMLInputElement;
+
+ it('preserves an in-flight agent edit on the create form across a background refetch', () => {
+ setSearch('type=openai');
+ const { rerender } = render();
+
+ expandAgent();
+ expect(temperatureInput().value).toBe('0.7');
+
+ fireEvent.change(temperatureInput(), { target: { value: '1.5' } });
+
+ // A refetch that carries no change leaves `data` referentially equal and is inert, so the
+ // payload has to actually differ for this to exercise the seeding effect.
+ queryResult.data = {
+ settingsProviders: { ...settingsProviders, userDefined: [...userDefined] },
+ };
+ rerender();
+
+ expect(temperatureInput().value).toBe('1.5');
+ });
+
+ it('still re-seeds the agents when the type changes mid-edit', () => {
+ setSearch('type=openai');
+ const { rerender } = render();
+
+ expandAgent();
+ fireEvent.change(temperatureInput(), { target: { value: '1.5' } });
+
+ setSearch('type=anthropic');
+ rerender();
+ expandAgent();
+
+ expect(temperatureInput().value).toBe('0.2');
+ });
+
it('preserves an in-flight edit across a background refetch', () => {
state.providerId = 'edit-1';
const { rerender } = render();
diff --git a/frontend/src/pages/settings/settings-provider.tsx b/frontend/src/pages/settings/settings-provider.tsx
index 85f04b48..856949ef 100644
--- a/frontend/src/pages/settings/settings-provider.tsx
+++ b/frontend/src/pages/settings/settings-provider.tsx
@@ -12,7 +12,7 @@ import {
Trash2,
XCircle,
} from 'lucide-react';
-import { type ComponentProps, useEffect, useMemo, useState } from 'react';
+import { type ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
import {
type Control,
type FieldPath,
@@ -1213,6 +1213,7 @@ function SettingsProvider() {
const { control, formState, handleSubmit: handleFormSubmit, reset, setValue, trigger, watch } = form;
const { isDirty } = useFormState({ control });
+ const seededTypeRef = useRef(null);
useEffect(() => {
if (submitError) {
@@ -1283,6 +1284,17 @@ function SettingsProvider() {
return;
}
+ // setValue is outside the form's keepDirtyValues, so a background refetch re-runs this effect
+ // with a fresh `data` identity and overwrites agent edits the user has not saved. Re-seed only
+ // when the type actually changed — that is the case where the previous type's agents are wrong.
+ const isSameType = seededTypeRef.current === selectedType;
+
+ seededTypeRef.current = selectedType;
+
+ if (isSameType && form.getFieldState('agents').isDirty) {
+ return;
+ }
+
const defaultProvider =
data.settingsProviders.default[selectedType as keyof typeof data.settingsProviders.default];