mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-12 08:51:53 +00:00
fix(desktop): preserve explicit hide-all choice in model visibility dialog (#43496)
When a user toggles off the last visible model for a provider group, the effectiveVisibleKeys() function treated the missing provider prefix as 'never customized' and re-added the default models on the next render, causing all models to snap back to enabled. Fix: store a sentinel key (e.g. 'provider::') when the last model for a provider is toggled off. The sentinel distinguishes 'user hid everything' from 'user never customized', preventing the default-fallback path from re-adding models the user explicitly chose to hide. Fixes #43485
This commit is contained in:
parent
8505e9d669
commit
93a2f680fd
3 changed files with 88 additions and 3 deletions
|
|
@ -14,6 +14,8 @@ import {
|
|||
$visibleModels,
|
||||
collapseModelFamilies,
|
||||
effectiveVisibleKeys,
|
||||
emptyProviderSentinelKey,
|
||||
isProviderSentinel,
|
||||
modelVisibilityKey,
|
||||
setVisibleModels
|
||||
} from '@/store/model-visibility'
|
||||
|
|
@ -61,10 +63,21 @@ export function ModelVisibilityDialog({
|
|||
const toggle = (provider: ModelOptionProvider, model: string) => {
|
||||
const next = new Set(effectiveVisibleKeys($visibleModels.get(), providers))
|
||||
const key = modelVisibilityKey(provider.slug, model)
|
||||
const sentinel = emptyProviderSentinelKey(provider.slug)
|
||||
|
||||
if (next.has(key)) {
|
||||
next.delete(key)
|
||||
|
||||
// Check if this was the last real model for this provider.
|
||||
const remainingForProvider = [...next].some(
|
||||
k => k.startsWith(`${provider.slug}::`) && !isProviderSentinel(k)
|
||||
)
|
||||
|
||||
if (!remainingForProvider) {
|
||||
next.add(sentinel)
|
||||
}
|
||||
} else {
|
||||
next.delete(sentinel)
|
||||
next.add(key)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ import { describe, expect, it } from 'vitest'
|
|||
|
||||
import type { ModelOptionProvider } from '@/types/hermes'
|
||||
|
||||
import { effectiveVisibleKeys, modelVisibilityKey } from './model-visibility'
|
||||
import {
|
||||
effectiveVisibleKeys,
|
||||
emptyProviderSentinelKey,
|
||||
isProviderSentinel,
|
||||
modelVisibilityKey
|
||||
} from './model-visibility'
|
||||
|
||||
const provider = (slug: string, models: string[]): ModelOptionProvider => ({
|
||||
models,
|
||||
|
|
@ -34,4 +39,48 @@ describe('model visibility', () => {
|
|||
expect(visible.has(modelVisibilityKey('local-ollama', 'qwen3:latest'))).toBe(true)
|
||||
expect(visible.has(modelVisibilityKey('local-ollama', 'llama3.2:latest'))).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves hidden-provider sentinel without re-adding defaults', () => {
|
||||
// User explicitly hid all models for "nous" — sentinel marks this choice.
|
||||
const stored = new Set([emptyProviderSentinelKey('nous')])
|
||||
|
||||
const visible = effectiveVisibleKeys(stored, [
|
||||
provider('nous', ['hermes-3-llama-3.1-70b', 'hermes-3-llama-3.1-8b']),
|
||||
provider('ollama', ['qwen3:latest'])
|
||||
])
|
||||
|
||||
expect(visible.has(modelVisibilityKey('nous', 'hermes-3-llama-3.1-70b'))).toBe(false)
|
||||
expect(visible.has(modelVisibilityKey('nous', 'hermes-3-llama-3.1-8b'))).toBe(false)
|
||||
// Sentinel itself is stripped from the result.
|
||||
expect(visible.has(emptyProviderSentinelKey('nous'))).toBe(false)
|
||||
// Other providers still get defaults.
|
||||
expect(visible.has(modelVisibilityKey('ollama', 'qwen3:latest'))).toBe(true)
|
||||
})
|
||||
|
||||
it('restores model when toggling on after hiding all', () => {
|
||||
// Simulates: user hid all "nous" models, then toggles one back on.
|
||||
const stored = new Set([
|
||||
emptyProviderSentinelKey('nous'),
|
||||
modelVisibilityKey('ollama', 'qwen3:latest')
|
||||
])
|
||||
|
||||
// After toggle: sentinel removed, one model added.
|
||||
const afterToggle = new Set(stored)
|
||||
afterToggle.delete(emptyProviderSentinelKey('nous'))
|
||||
afterToggle.add(modelVisibilityKey('nous', 'hermes-3-llama-3.1-70b'))
|
||||
|
||||
const visible = effectiveVisibleKeys(afterToggle, [
|
||||
provider('nous', ['hermes-3-llama-3.1-70b', 'hermes-3-llama-3.1-8b']),
|
||||
provider('ollama', ['qwen3:latest'])
|
||||
])
|
||||
|
||||
expect(visible.has(modelVisibilityKey('nous', 'hermes-3-llama-3.1-70b'))).toBe(true)
|
||||
expect(visible.has(modelVisibilityKey('nous', 'hermes-3-llama-3.1-8b'))).toBe(false)
|
||||
})
|
||||
|
||||
it('sentinel key helper produces correct format', () => {
|
||||
expect(emptyProviderSentinelKey('openai')).toBe('openai::')
|
||||
expect(isProviderSentinel('openai::')).toBe(true)
|
||||
expect(isProviderSentinel('openai::gpt-4o')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,19 @@ export const DEFAULT_VISIBLE_PER_PROVIDER = 50
|
|||
* that contain a single colon, e.g. `model:tag`). */
|
||||
export const modelVisibilityKey = (provider: string, model: string): string => `${provider}::${model}`
|
||||
|
||||
/** Sentinel key suffix stored when the user explicitly hides ALL models for a
|
||||
* provider. Distinguishes "user hid everything" from "never customized" so
|
||||
* `effectiveVisibleKeys` does not re-add defaults for that provider. */
|
||||
export const EMPTY_PROVIDER_SENTINEL = ''
|
||||
|
||||
/** Build the sentinel key for a provider whose last model was toggled off. */
|
||||
export const emptyProviderSentinelKey = (provider: string): string =>
|
||||
modelVisibilityKey(provider, EMPTY_PROVIDER_SENTINEL)
|
||||
|
||||
/** Check whether a stored key is a provider-hidden sentinel. */
|
||||
export const isProviderSentinel = (key: string): boolean =>
|
||||
key.endsWith('::')
|
||||
|
||||
/** A model and its optional `…-fast` sibling, collapsed into one logical row.
|
||||
* `id` is the canonical (base) model; `fastId` is the fast variant if present. */
|
||||
export interface ModelFamily {
|
||||
|
|
@ -116,9 +129,12 @@ export function effectiveVisibleKeys(
|
|||
|
||||
for (const provider of providers) {
|
||||
const providerPrefix = `${provider.slug}::`
|
||||
const hasStoredProvider = [...stored].some(key => key.startsWith(providerPrefix))
|
||||
const hasStoredProvider = [...stored].some(
|
||||
key => key.startsWith(providerPrefix) && !isProviderSentinel(key)
|
||||
)
|
||||
const hasSentinel = stored.has(emptyProviderSentinelKey(provider.slug))
|
||||
|
||||
if (hasStoredProvider) {
|
||||
if (hasStoredProvider || hasSentinel) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -129,5 +145,12 @@ export function effectiveVisibleKeys(
|
|||
}
|
||||
}
|
||||
|
||||
// Strip sentinel keys — they are bookkeeping, not real visibility entries.
|
||||
for (const key of [...next]) {
|
||||
if (isProviderSentinel(key)) {
|
||||
next.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue