* fix(app): preserve composer mode preferences Treat undefined provider preference updates as no-ops, and keep saved mode selections instead of silently defaulting them when provider metadata is incomplete or does not list the saved mode. * fix(app): document mode preference preservation
167 lines
4.8 KiB
TypeScript
167 lines
4.8 KiB
TypeScript
import { z } from "zod";
|
|
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
|
|
|
export interface FavoriteModelPreference {
|
|
provider: string;
|
|
modelId: string;
|
|
}
|
|
|
|
export interface FavoriteModelRow {
|
|
favoriteKey: string;
|
|
provider: string;
|
|
providerLabel: string;
|
|
modelId: string;
|
|
modelLabel: string;
|
|
description?: string;
|
|
}
|
|
|
|
const providerPreferencesSchema = z.object({
|
|
model: z.string().optional(),
|
|
mode: z.string().optional(),
|
|
thinkingByModel: z.record(z.string(), z.string()).optional(),
|
|
featureValues: z.record(z.string(), z.unknown()).optional(),
|
|
});
|
|
|
|
const formPreferencesSchema = z.object({
|
|
provider: z.string().optional(),
|
|
providerPreferences: z.record(z.string(), providerPreferencesSchema).optional(),
|
|
favoriteModels: z
|
|
.array(
|
|
z.object({
|
|
provider: z.string(),
|
|
modelId: z.string(),
|
|
}),
|
|
)
|
|
.optional(),
|
|
isolation: z.enum(["local", "worktree"]).optional(),
|
|
});
|
|
|
|
export type ProviderPreferences = z.infer<typeof providerPreferencesSchema>;
|
|
export type FormPreferences = z.infer<typeof formPreferencesSchema>;
|
|
|
|
export const DEFAULT_FORM_PREFERENCES: FormPreferences = {};
|
|
|
|
export function parseFormPreferences(value: unknown): FormPreferences {
|
|
const result = formPreferencesSchema.safeParse(value);
|
|
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;
|
|
updates: Partial<ProviderPreferences>;
|
|
}): FormPreferences {
|
|
const { preferences, provider, updates } = args;
|
|
const existingProviderPreferences = preferences.providerPreferences ?? {};
|
|
const existing = existingProviderPreferences[provider] ?? {};
|
|
|
|
return {
|
|
...preferences,
|
|
provider,
|
|
providerPreferences: {
|
|
...existingProviderPreferences,
|
|
[provider]: applyProviderPreferenceUpdates(existing, updates),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function mergeCreateAgentSelectionPreferences(args: {
|
|
preferences: FormPreferences;
|
|
provider: AgentProvider | null;
|
|
modelId?: string | null;
|
|
modeId?: string | null;
|
|
thinkingOptionId?: string | null;
|
|
featureValues?: Record<string, unknown>;
|
|
}): FormPreferences {
|
|
if (!args.provider) {
|
|
return args.preferences;
|
|
}
|
|
|
|
const modelId = args.modelId?.trim() ?? "";
|
|
const modeId = args.modeId?.trim() ?? "";
|
|
const thinkingOptionId = args.thinkingOptionId?.trim() ?? "";
|
|
|
|
return mergeProviderPreferences({
|
|
preferences: args.preferences,
|
|
provider: args.provider,
|
|
updates: {
|
|
model: modelId || undefined,
|
|
mode: modeId || undefined,
|
|
...(modelId && thinkingOptionId ? { thinkingByModel: { [modelId]: thinkingOptionId } } : {}),
|
|
...(args.featureValues ? { featureValues: args.featureValues } : {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
export function buildFavoriteModelKey(input: FavoriteModelPreference): string {
|
|
return `${input.provider}:${input.modelId}`;
|
|
}
|
|
|
|
export function isFavoriteModel(args: {
|
|
preferences: FormPreferences;
|
|
provider: string;
|
|
modelId: string;
|
|
}): boolean {
|
|
const favoriteKey = buildFavoriteModelKey({ provider: args.provider, modelId: args.modelId });
|
|
return (args.preferences.favoriteModels ?? []).some(
|
|
(favorite) => buildFavoriteModelKey(favorite) === favoriteKey,
|
|
);
|
|
}
|
|
|
|
export function toggleFavoriteModel(args: {
|
|
preferences: FormPreferences;
|
|
provider: string;
|
|
modelId: string;
|
|
}): FormPreferences {
|
|
const favorite = { provider: args.provider, modelId: args.modelId };
|
|
const favoriteKey = buildFavoriteModelKey(favorite);
|
|
const existingFavorites = args.preferences.favoriteModels ?? [];
|
|
const hasFavorite = existingFavorites.some(
|
|
(entry) => buildFavoriteModelKey(entry) === favoriteKey,
|
|
);
|
|
|
|
return {
|
|
...args.preferences,
|
|
favoriteModels: hasFavorite
|
|
? existingFavorites.filter((entry) => buildFavoriteModelKey(entry) !== favoriteKey)
|
|
: [...existingFavorites, favorite],
|
|
};
|
|
}
|