Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/app/src/create-agent-preferences/preferences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,39 @@ describe("create agent preferences", () => {
});
});

it("does not erase a saved mode when a later partial update has no mode", () => {
expect(
mergeProviderPreferences({
preferences: {
provider: "codex",
providerPreferences: {
codex: {
model: "gpt-5.5",
mode: "full-access",
thinkingByModel: { "gpt-5.5": "high" },
},
},
},
provider: "codex",
updates: {
model: "gpt-5.6",
mode: undefined,
thinkingByModel: undefined,
featureValues: undefined,
},
}),
).toEqual({
provider: "codex",
providerPreferences: {
codex: {
model: "gpt-5.6",
mode: "full-access",
thinkingByModel: { "gpt-5.5": "high" },
},
},
});
});

it("loads invalid stored preferences as empty preferences", () => {
expect(parseFormPreferences({ providerPreferences: { codex: { mode: 42 } } })).toEqual({});
});
Expand Down
58 changes: 38 additions & 20 deletions packages/app/src/create-agent-preferences/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,43 @@ export function parseFormPreferences(value: unknown): FormPreferences {
return result.success ? result.data : DEFAULT_FORM_PREFERENCES;
}

function mergeDefinedRecord<T>(
existing: Record<string, T> | undefined,
updates: Record<string, T> | undefined,
): Record<string, T> | undefined {
if (updates === undefined) {
return existing;
}
return {
...existing,
...updates,
};
}

function applyProviderPreferenceUpdates(
existing: ProviderPreferences,
updates: Partial<ProviderPreferences>,
): ProviderPreferences {
const next: ProviderPreferences = { ...existing };
const nextThinkingByModel = mergeDefinedRecord(existing.thinkingByModel, updates.thinkingByModel);
const nextFeatureValues = mergeDefinedRecord(existing.featureValues, updates.featureValues);

if (updates.model !== undefined) {
next.model = updates.model;
}
if (updates.mode !== undefined) {
next.mode = updates.mode;
}
if (nextThinkingByModel !== undefined) {
next.thinkingByModel = nextThinkingByModel;
}
if (nextFeatureValues !== undefined) {
next.featureValues = nextFeatureValues;
}

return next;
}

export function mergeProviderPreferences(args: {
preferences: FormPreferences;
provider: AgentProvider;
Expand All @@ -54,32 +91,13 @@ export function mergeProviderPreferences(args: {
const { preferences, provider, updates } = args;
const existingProviderPreferences = preferences.providerPreferences ?? {};
const existing = existingProviderPreferences[provider] ?? {};
const nextThinkingByModel =
updates.thinkingByModel === undefined
? existing.thinkingByModel
: {
...existing.thinkingByModel,
...updates.thinkingByModel,
};
const nextFeatureValues =
updates.featureValues === undefined
? existing.featureValues
: {
...existing.featureValues,
...updates.featureValues,
};

return {
...preferences,
provider,
providerPreferences: {
...existingProviderPreferences,
[provider]: {
...existing,
...updates,
...(nextThinkingByModel ? { thinkingByModel: nextThinkingByModel } : {}),
...(nextFeatureValues ? { featureValues: nextFeatureValues } : {}),
},
[provider]: applyProviderPreferenceUpdates(existing, updates),
},
};
}
Expand Down
53 changes: 53 additions & 0 deletions packages/app/src/provider-selection/resolve-agent-form.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,59 @@ describe("resolveFormState", () => {
expect(resolved.thinkingOptionId).toBe("xhigh");
});

it("preserves the saved mode while provider modes are absent from a loading snapshot", () => {
const loadingEntries: ProviderSnapshotEntry[] = [
{
provider: "codex",
status: "loading",
enabled: true,
label: TEST_CODEX_DEFINITION.label,
description: TEST_CODEX_DEFINITION.description,
defaultModeId: TEST_CODEX_DEFINITION.defaultModeId,
},
];
const providerDefinitions = buildProviderDefinitions(loadingEntries);
const resolvableProviderMap = buildProviderDefinitionMapForStatuses({
snapshotEntries: loadingEntries,
providerDefinitions,
statuses: new Set<ProviderSnapshotEntry["status"]>(["ready", "loading"]),
});

const resolved = resolveFormState(
undefined,
{
provider: "codex",
providerPreferences: { codex: { mode: "full-access", model: "gpt-5.3-codex" } },
},
null,
INITIAL_USER_MODIFIED,
makeState({ provider: "codex", modeId: "full-access", model: "gpt-5.3-codex" }).form,

resolvableProviderMap,
);

expect(resolved.provider).toBe("codex");
expect(resolved.modeId).toBe("full-access");
});

it("preserves a saved mode that is not in the current mode list", () => {
const resolved = resolveFormState(
undefined,
{
provider: "codex",
providerPreferences: { codex: { mode: "workspace-write", model: "gpt-5.3-codex" } },
},
CODEX_MODELS,
INITIAL_USER_MODIFIED,
makeState({ provider: "codex" }).form,

codexProviderMap,
);

expect(resolved.provider).toBe("codex");
expect(resolved.modeId).toBe("workspace-write");
});

it("ignores disabled ready providers when resolving selectable defaults", () => {
const entries: ProviderSnapshotEntry[] = [
{
Expand Down
54 changes: 29 additions & 25 deletions packages/app/src/provider-selection/resolve-agent-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ export function resolveThinkingOptionId(args: {
return effectiveModel?.defaultThinkingOptionId ?? thinkingOptions[0]?.id ?? "";
}

function normalizeSelectedModeId(modeId: string | null | undefined): string {
return typeof modeId === "string" ? modeId.trim() : "";
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

function resolvePreferredModeId(input: {
initialModeId?: string | null;
preferredModeId?: string | null;
providerDef: AgentProviderDefinition | undefined;
}): string {
const initialModeId = normalizeSelectedModeId(input.initialModeId);
if (initialModeId) return initialModeId;

const preferredModeId = normalizeSelectedModeId(input.preferredModeId);
if (preferredModeId) return preferredModeId;

return input.providerDef?.defaultModeId ?? input.providerDef?.modes[0]?.id ?? "";
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

export function mergeSelectedComposerPreferences(args: {
preferences: FormPreferences;
provider: AgentProvider;
Expand Down Expand Up @@ -259,18 +277,11 @@ function resolveModeId(input: {
input;
if (userModified) return currentModeId;
if (!provider) return "";
const validModeIds = providerDef?.modes.map((m) => m.id) ?? [];
if (
typeof initialValues?.modeId === "string" &&
initialValues.modeId.length > 0 &&
validModeIds.includes(initialValues.modeId)
) {
return initialValues.modeId;
}
if (providerPrefs?.mode && validModeIds.includes(providerPrefs.mode)) {
return providerPrefs.mode;
}
return providerDef?.defaultModeId ?? validModeIds[0] ?? "";
return resolvePreferredModeId({
initialModeId: initialValues?.modeId,
preferredModeId: providerPrefs?.mode,
providerDef,
});
}

function resolveModelField(input: {
Expand Down Expand Up @@ -411,11 +422,10 @@ function pickNextModeForProvider(input: {
providerPrefs: ProviderPrefs | undefined;
}): string {
const { providerDef, providerPrefs } = input;
const validModeIds = providerDef?.modes.map((m) => m.id) ?? [];
if (providerPrefs?.mode && validModeIds.includes(providerPrefs.mode)) {
return providerPrefs.mode;
}
return providerDef?.defaultModeId ?? "";
return resolvePreferredModeId({
preferredModeId: providerPrefs?.mode,
providerDef,
});
}

function pickNextModeForProviderAndModel(input: {
Expand All @@ -425,14 +435,8 @@ function pickNextModeForProviderAndModel(input: {
providerDef: AgentProviderDefinition | undefined;
providerPrefs: ProviderPrefs | undefined;
}): string {
const validModeIds = input.providerDef?.modes.map((m) => m.id) ?? [];
if (
input.currentProvider === input.provider &&
input.currentModeId &&
validModeIds.includes(input.currentModeId)
) {
return input.currentModeId;
}
const currentModeId = normalizeSelectedModeId(input.currentModeId);
if (input.currentProvider === input.provider && currentModeId) return currentModeId;
return pickNextModeForProvider({
providerDef: input.providerDef,
providerPrefs: input.providerPrefs,
Expand Down
Loading