Keep agent profile settings valid across providers (#3331)

* fix(app): keep profile modes scoped to providers

Applying a profile through sequential field setters allowed a provider change
to race mode persistence, poisoning modeless provider preferences with another
provider's mode. Apply profiles as one form transition and heal invalid
provider-scoped mode preferences when profiles are used.

* test(app): cover profile features across providers
This commit is contained in:
Mohamed Boudra
2026-08-13 18:48:02 +02:00
committed by GitHub
parent 90f3d95415
commit 9c195feb9b
17 changed files with 622 additions and 193 deletions
+67 -37
View File
@@ -14,6 +14,7 @@ import {
} from "@/provider-selection/provider-selection";
import { filterSelectableModels } from "@/provider-selection/model-catalog";
import { OptimisticFormPreferences } from "@/create-agent-preferences/optimistic-preferences";
import { applyAgentProfilePreferences } from "@/create-agent-preferences/preferences";
import { useProvidersSnapshot } from "./use-providers-snapshot";
import {
useFormPreferences,
@@ -37,6 +38,7 @@ import {
type FormState,
type ProviderModelsByProvider,
} from "@/provider-selection/resolve-agent-form";
import type { MaterializedAgentProfile } from "@/agent-profiles";
export type { FormInitialValues } from "@/provider-selection/resolve-agent-form";
@@ -54,7 +56,6 @@ export interface UseAgentFormStateResult {
setSelectedServerId: (value: string | null) => void;
setSelectedServerIdFromUser: (value: string | null) => void;
selectedProvider: AgentProvider | null;
setProviderFromUser: (provider: AgentProvider) => void;
selectedMode: string;
setModeFromUser: (modeId: string) => void;
selectedModel: string;
@@ -80,6 +81,7 @@ export interface UseAgentFormStateResult {
refreshProviderModels: (provider?: AgentProvider) => void;
refetchProviderModelsIfStale: () => void;
setProviderAndModelFromUser: (provider: AgentProvider, modelId: string) => void;
applyProfileFromUser: (profile: MaterializedAgentProfile) => void;
clearProviderSelectionFromUser: () => void;
workingDirIsEmpty: boolean;
persistFormPreferences: () => Promise<void>;
@@ -254,11 +256,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
}),
);
const reducerStateRef = useRef({ form: formState, userModified });
useEffect(() => {
reducerStateRef.current = { form: formState, userModified };
}, [formState, userModified]);
useEffect(() => {
if (!isVisible) {
dispatch({ type: "RESET" });
@@ -423,27 +420,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
dispatch({ type: "SET_SERVER_ID_FROM_USER", value });
}, []);
const setProviderFromUser = useCallback(
(provider: AgentProvider) => {
if (!selectableProviderDefinitionMap.has(provider)) {
return;
}
const providerModels = allProviderModels.get(provider) ?? null;
const providerDef = selectableProviderDefinitionMap.get(provider);
const providerPrefs = preferenceOverlayRef.current.current().providerPreferences?.[provider];
dispatch({
type: "SET_PROVIDER_FROM_USER",
provider,
providerModels,
providerDef,
providerPrefs,
});
void updateCurrentPreferences({ provider });
},
[allProviderModels, selectableProviderDefinitionMap, updateCurrentPreferences],
);
const setProviderAndModelFromUser = useCallback(
(provider: AgentProvider, modelId: string) => {
if (!selectableProviderDefinitionMap.has(provider)) {
@@ -480,10 +456,64 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
dispatch({ type: "CLEAR_PROVIDER_SELECTION_FROM_USER" });
}, []);
const applyProfileFromUser = useCallback(
(profile: MaterializedAgentProfile) => {
const provider = profile.provider as AgentProvider;
if (!selectableProviderDefinitionMap.has(provider)) {
return;
}
const previousProvider = formState.provider;
const providerDef = selectableProviderDefinitionMap.get(provider);
const providerModels = allProviderModels.get(provider) ?? null;
const providerPrefs = preferenceOverlayRef.current.current().providerPreferences?.[provider];
const action = {
type: "APPLY_PROFILE_FROM_USER" as const,
provider,
modelId: profile.modelId,
modeId: profile.modeId,
thinkingOptionId: profile.thinkingOptionId,
providerDef,
providerModels,
providerPrefs,
};
const nextState = resolveAgentForm({ form: formState, userModified, resolution }, action);
const previousProviderModeIds = previousProvider
? (providerDefinitionMap.get(previousProvider)?.modes.map((mode) => mode.id) ?? [])
: [];
dispatch(action);
void updateCurrentPreferences((current) => {
const { model, modeId, thinkingOptionId } = nextState.form;
return applyAgentProfilePreferences({
preferences: current,
previousProvider,
previousProviderModeIds,
provider,
modelId: model,
modeId,
thinkingOptionId,
featureValues: profile.featureValues,
});
}).catch((error) => {
console.warn("[useAgentFormState] persist profile preference failed", error);
});
},
[
allProviderModels,
formState,
providerDefinitionMap,
resolution,
selectableProviderDefinitionMap,
updateCurrentPreferences,
userModified,
],
);
const setModeFromUser = useCallback(
(modeId: string) => {
dispatch({ type: "SET_MODE_FROM_USER", modeId });
const provider = reducerStateRef.current.form.provider;
const provider = formState.provider;
if (provider) {
void updateCurrentPreferences((current) =>
mergeSelectedComposerPreferences({
@@ -496,12 +526,12 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
);
}
},
[updateCurrentPreferences],
[formState.provider, updateCurrentPreferences],
);
const setModelFromUser = useCallback(
(modelId: string) => {
const provider = reducerStateRef.current.form.provider;
const provider = formState.provider;
const providerPrefs = provider
? preferenceOverlayRef.current.current().providerPreferences?.[provider]
: undefined;
@@ -525,13 +555,13 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
);
}
},
[availableModels, updateCurrentPreferences],
[availableModels, formState.provider, updateCurrentPreferences],
);
const setThinkingOptionFromUser = useCallback(
(thinkingOptionId: string) => {
dispatch({ type: "SET_THINKING_OPTION_FROM_USER", thinkingOptionId });
const { provider, model: modelId } = reducerStateRef.current.form;
const { provider, model: modelId } = formState;
if (provider && modelId) {
void updateCurrentPreferences((current) =>
mergeSelectedComposerPreferences({
@@ -546,7 +576,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
);
}
},
[updateCurrentPreferences],
[formState, updateCurrentPreferences],
);
const setWorkingDir = useCallback((value: string) => {
@@ -569,8 +599,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
);
const refetchProviderModelsIfStale = useCallback(() => {
refetchSnapshotIfStale(reducerStateRef.current.form.provider);
}, [refetchSnapshotIfStale]);
refetchSnapshotIfStale(formState.provider);
}, [formState.provider, refetchSnapshotIfStale]);
const persistFormPreferences = useCallback(async () => {
if (!formState.provider) {
@@ -604,7 +634,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
setSelectedServerId,
setSelectedServerIdFromUser,
selectedProvider: formState.provider,
setProviderFromUser,
selectedMode: formState.modeId,
setModeFromUser,
selectedModel: formState.model,
@@ -630,6 +659,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
refreshProviderModels,
refetchProviderModelsIfStale,
setProviderAndModelFromUser,
applyProfileFromUser,
clearProviderSelectionFromUser,
workingDirIsEmpty,
persistFormPreferences,
@@ -643,7 +673,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
formState.workingDir,
setSelectedServerId,
setSelectedServerIdFromUser,
setProviderFromUser,
setModeFromUser,
setModelFromUser,
setThinkingOptionFromUser,
@@ -665,6 +694,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
refreshProviderModels,
refetchProviderModelsIfStale,
setProviderAndModelFromUser,
applyProfileFromUser,
clearProviderSelectionFromUser,
workingDirIsEmpty,
persistFormPreferences,
@@ -145,10 +145,15 @@ export function useDraftAgentFeatures(input: {
[provider, updatePreferences],
);
const applyProfileFeatureValues = useCallback((values: Record<string, unknown>) => {
setLocalFeatureValues(values);
}, []);
return {
features,
featureValues: effectiveFeatureValues,
isLoading: featuresQuery.isLoading,
setFeatureValue,
applyProfileFeatureValues,
};
}