fix(settings): persist per-model visibility and sibling selector state (#1700)
* fix(settings): persist per-model visibility and sibling selector state
The server-side settings sanitizer only allowlisted favoriteModels and
recentModels, so hiddenModels, collapsedModelProviders, recentAgents, and
recentEfforts were stripped on every write to settings.json — per-model
visibility and collapsed-provider state silently reset on every container
redeploy or settings reload.
Add the four missing fields to sanitizeSettingsUpdate:
- hiddenModels: sanitizeModelRefs(..., 1024) — same shape as favoriteModels;
1024 covers dense multi-provider setups while bounding persistence/memory.
- collapsedModelProviders: normalizeStringArray with Array.isArray gate
(matches usageDropdownProviders).
- recentAgents: normalizeStringArray (Array<string> per ui-store).
- recentEfforts: new sanitizeRecentEfforts validating Record<string, string[]>
(shape confirmed in ui-store + addRecentEffort action); trims/dedupes keys
and variants, caps at 128 keys x 5 variants/key (5 matches client slice).
No ui-store version bump or migration: zustand's default merge spreads
persisted state over defaults, so missing fields fall back to [] / {} until
the next toggle. favoriteModels and recentModels are untouched.
Tests: 8 new cases in settings-helpers.test.js using the real
sanitizeModelRefs / normalizeStringArray — round-trips, empty-[] parity with
favoriteModels, garbage rejection, and a full-payload regression test.
* fix: sync model selector settings
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
307808bec2
commit
eae09d4576
@@ -26,6 +26,9 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
const SHORTCUT_OVERRIDE_VALUE_MAX_LENGTH = 128;
|
||||
const PWA_ORIENTATION_VALUES = new Set(['system', 'portrait', 'landscape']);
|
||||
const MOBILE_KEYBOARD_MODE_VALUES = new Set(['native', 'resize-content']);
|
||||
const HIDDEN_MODELS_MAX = 1024;
|
||||
const RECENT_EFFORTS_MAX_KEYS = 128;
|
||||
const RECENT_EFFORTS_MAX_VARIANTS_PER_KEY = 5;
|
||||
|
||||
const sanitizeShortcutOverrides = (value) => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
@@ -41,6 +44,35 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const sanitizeRecentEfforts = (value) => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const result = {};
|
||||
const seenKeys = new Set();
|
||||
let count = 0;
|
||||
for (const [rawKey, rawVariants] of Object.entries(value)) {
|
||||
const key = typeof rawKey === 'string' ? rawKey.trim() : '';
|
||||
if (!key || seenKeys.has(key)) continue;
|
||||
if (!Array.isArray(rawVariants)) continue;
|
||||
const variants = [];
|
||||
const seenVariants = new Set();
|
||||
for (const rawVariant of rawVariants) {
|
||||
const variant = typeof rawVariant === 'string' ? rawVariant.trim() : '';
|
||||
if (!variant || seenVariants.has(variant)) continue;
|
||||
seenVariants.add(variant);
|
||||
variants.push(variant);
|
||||
if (variants.length >= RECENT_EFFORTS_MAX_VARIANTS_PER_KEY) break;
|
||||
}
|
||||
if (variants.length === 0) continue;
|
||||
seenKeys.add(key);
|
||||
result[key] = variants;
|
||||
count += 1;
|
||||
if (count >= RECENT_EFFORTS_MAX_KEYS) break;
|
||||
}
|
||||
return count > 0 ? result : null;
|
||||
};
|
||||
|
||||
const normalizePwaAppName = (value, fallback = '') => {
|
||||
if (typeof value !== 'string') {
|
||||
return fallback;
|
||||
@@ -474,6 +506,28 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
if (recentModels) {
|
||||
result.recentModels = recentModels;
|
||||
}
|
||||
|
||||
// Cap at 1024: users with several providers (anthropic, openai, google,
|
||||
// bedrock, azure, etc.) each exposing dozens-to-hundreds of models can
|
||||
// exceed 256 hidden entries quickly. 1024 covers dense multi-provider
|
||||
// setups while still bounding persistence/memory.
|
||||
const hiddenModels = sanitizeModelRefs(candidate.hiddenModels, HIDDEN_MODELS_MAX);
|
||||
if (hiddenModels) {
|
||||
result.hiddenModels = hiddenModels;
|
||||
}
|
||||
|
||||
if (Array.isArray(candidate.collapsedModelProviders)) {
|
||||
result.collapsedModelProviders = normalizeStringArray(candidate.collapsedModelProviders);
|
||||
}
|
||||
|
||||
if (Array.isArray(candidate.recentAgents)) {
|
||||
result.recentAgents = normalizeStringArray(candidate.recentAgents);
|
||||
}
|
||||
|
||||
const recentEfforts = sanitizeRecentEfforts(candidate.recentEfforts);
|
||||
if (recentEfforts) {
|
||||
result.recentEfforts = recentEfforts;
|
||||
}
|
||||
if (typeof candidate.diffLayoutPreference === 'string') {
|
||||
const mode = candidate.diffLayoutPreference.trim();
|
||||
if (mode === 'dynamic' || mode === 'inline' || mode === 'side-by-side') {
|
||||
|
||||
Reference in New Issue
Block a user