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
@@ -81,6 +81,7 @@ import {
useAgentProfilePicker,
type AgentProfileApplyTarget,
type AgentProfilePicker,
type DraftAgentProfileControls,
} from "@/agent-profiles";
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
@@ -125,7 +126,6 @@ interface ControlledAgentControlsProps {
export interface DraftAgentControlsProps {
providerDefinitions: AgentProviderDefinition[];
selectedProvider: AgentProvider | null;
onSelectProvider: (provider: AgentProvider) => void;
modeOptions: AgentMode[];
selectedMode: string;
onSelectMode: (modeId: string) => void;
@@ -139,6 +139,7 @@ export interface DraftAgentControlsProps {
thinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
selectedThinkingOptionId: string;
onSelectThinkingOption: (thinkingOptionId: string) => void;
onApplyAgentProfile: DraftAgentProfileControls["applyProfile"];
features?: AgentFeature[];
onSetFeature?: (featureId: string, value: unknown) => void;
onDropdownClose?: () => void;
@@ -385,6 +386,15 @@ function resolveSnapshotSelectedEntry(
return snapshotEntries.find((e) => e.provider === agentProvider) ?? null;
}
function resolveSnapshotModeIds(
entry: ReturnType<typeof resolveSnapshotSelectedEntry>,
): string[] | null {
if (entry?.status !== "ready" || !entry.modes) {
return null;
}
return entry.modes.map((mode) => mode.id);
}
function buildAgentProviderDefinitions(
agentProvider: string | undefined,
snapshotEntries: ReturnType<typeof useProvidersSnapshot>["entries"],
@@ -1550,9 +1560,13 @@ export const AgentControls = memo(function AgentControls({
// A running agent is one provider's process, so only that provider's profiles
// can apply to it.
const profileProviders = useMemo(() => (agentProvider ? [agentProvider] : []), [agentProvider]);
const profileModeIds = useMemo(
() => resolveSnapshotModeIds(snapshotSelectedEntry),
[snapshotSelectedEntry],
);
const profileTarget = useMemo<AgentProfileApplyTarget>(
() => ({ kind: "agent", agentId }),
[agentId],
() => ({ kind: "agent", agentId, availableModeIds: profileModeIds }),
[agentId, profileModeIds],
);
const agentProfiles = useAgentProfilePicker({
serverId,
@@ -1692,7 +1706,6 @@ export const AgentControls = memo(function AgentControls({
export function DraftAgentControls({
providerDefinitions,
selectedProvider,
onSelectProvider,
modeOptions,
selectedMode,
onSelectMode,
@@ -1706,6 +1719,7 @@ export function DraftAgentControls({
thinkingOptions,
selectedThinkingOptionId,
onSelectThinkingOption,
onApplyAgentProfile,
features,
onSetFeature,
onDropdownClose,
@@ -1742,20 +1756,10 @@ export function DraftAgentControls({
() => ({
kind: "draft",
controls: {
selectProvider: onSelectProvider,
selectProviderAndModel: onSelectProviderAndModel,
selectMode: onSelectMode,
selectThinkingOption: onSelectThinkingOption,
setFeature: onSetFeature,
applyProfile: onApplyAgentProfile,
},
}),
[
onSelectMode,
onSelectProvider,
onSelectProviderAndModel,
onSelectThinkingOption,
onSetFeature,
],
[onApplyAgentProfile],
);
const agentProfiles = useAgentProfilePicker({
serverId: modelSelectorServerId,
@@ -20,7 +20,6 @@ describe("resolveAgentControlsMode", () => {
resolveAgentControlsMode({
providerDefinitions: [],
selectedProvider: "codex",
onSelectProvider: () => undefined,
modeOptions: [],
selectedMode: "",
onSelectMode: () => undefined,
@@ -34,6 +33,7 @@ describe("resolveAgentControlsMode", () => {
thinkingOptions: [],
selectedThinkingOptionId: "",
onSelectThinkingOption: () => undefined,
onApplyAgentProfile: () => undefined,
}),
).toBe("draft");
});
@@ -22,13 +22,13 @@ export function buildDraftAgentControls(input: {
formState: UseAgentFormStateResult;
features?: DraftAgentControlsProps["features"];
onSetFeature?: DraftAgentControlsProps["onSetFeature"];
onApplyAgentProfile: DraftAgentControlsProps["onApplyAgentProfile"];
onDropdownClose?: DraftAgentControlsProps["onDropdownClose"];
}): DraftAgentControlsProps {
const { formState, features, onSetFeature, onDropdownClose } = input;
const { formState, features, onSetFeature, onApplyAgentProfile, onDropdownClose } = input;
return {
providerDefinitions: formState.providerDefinitions,
selectedProvider: formState.selectedProvider,
onSelectProvider: formState.setProviderFromUser,
modeOptions: formState.modeOptions,
selectedMode: formState.selectedMode,
onSelectMode: formState.setModeFromUser,
@@ -42,6 +42,7 @@ export function buildDraftAgentControls(input: {
thinkingOptions: formState.availableThinkingOptions,
selectedThinkingOptionId: formState.selectedThinkingOptionId,
onSelectThinkingOption: formState.setThinkingOptionFromUser,
onApplyAgentProfile,
features,
onSetFeature,
onDropdownClose,
@@ -190,6 +190,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
features: draftFeatures,
featureValues: draftFeatureValues,
setFeatureValue: setDraftFeatureValue,
applyProfileFeatureValues,
} = useDraftAgentFeatures({
serverId: formState.selectedServerId,
provider: formState.selectedProvider,
@@ -200,6 +201,14 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
initialFeatureValues: composerOptions?.initialFeatureValues,
});
const applyDraftAgentProfile = useCallback(
(profile: Parameters<typeof formState.applyProfileFromUser>[0]) => {
formState.applyProfileFromUser(profile);
applyProfileFeatureValues(profile.featureValues);
},
[applyProfileFeatureValues, formState],
);
const commandDraftConfig = useMemo(
() =>
composerOptions
@@ -236,6 +245,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
formState,
features: draftFeatures,
onSetFeature: setDraftFeatureValue,
onApplyAgentProfile: applyDraftAgentProfile,
}),
commandDraftConfig,
};
@@ -246,6 +256,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
effectiveThinkingOptionId,
draftFeatures,
draftFeatureValues,
applyDraftAgentProfile,
formState,
setDraftFeatureValue,
workingDir,