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