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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user