{"repo":"NousResearch/hermes-agent","public_id":"43485","display_label":"Model visibility dialog hide-all preservation bug fixes","summary":"These issues and pull requests concern a bug in the desktop model visibility dialog where toggling off the last model in a provider group incorrectly re-enables all models, along with multiple fix attempts to preserve explicit hide-all states for providers.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":43496,"merged_at":"2026-06-11T18:27:39+00:00"},"resolution":{"state":"resolved","by":"pr-43496"},"freshness":{"state":"verified","last_verified_at":"2026-06-11T18:27:39+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":43485,"pr_number":43496,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-11T18:27:39+00:00"}],"members":[{"kind":"issue","number":43485,"title":"[Bug] Model visibility: toggling off last model in a provider group re-enables all models","url":null,"state":"closed"},{"kind":"pr","number":43496,"title":"fix(desktop): preserve explicit hide-all choice in model visibility dialog","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/src/components/model-visibility-dialog.tsx","apps/desktop/src/store/model-visibility.test.ts","apps/desktop/src/store/model-visibility.ts"],"fix_hunks":[{"file":"apps/desktop/src/components/model-visibility-dialog.tsx","hunks":["@@ -14,6 +14,8 @@ import {\n   $visibleModels,\n   collapseModelFamilies,\n   effectiveVisibleKeys,\n+  emptyProviderSentinelKey,\n+  isProviderSentinel,\n   modelVisibilityKey,\n   setVisibleModels\n } from '@/store/model-visibility'","@@ -61,10 +63,21 @@ export function ModelVisibilityDialog({\n   const toggle = (provider: ModelOptionProvider, model: string) => {\n     const next = new Set(effectiveVisibleKeys($visibleModels.get(), providers))\n     const key = modelVisibilityKey(provider.slug, model)\n+    const sentinel = emptyProviderSentinelKey(provider.slug)\n \n     if (next.has(key)) {\n       next.delete(key)\n+\n+      // Check if this was the last real model for this provider.\n+      const remainingForProvider = [...next].some(\n+        k => k.startsWith(`${provider.slug}::`) && !isProviderSentinel(k)\n+      )\n+\n+      if (!remainingForProvider) {\n+        next.add(sentinel)\n+      }\n     } else {\n+      next.delete(sentinel)\n       next.add(key)\n     }\n "]},{"file":"apps/desktop/src/store/model-visibility.test.ts","hunks":["@@ -2,7 +2,12 @@ import { describe, expect, it } from 'vitest'\n \n import type { ModelOptionProvider } from '@/types/hermes'\n \n-import { effectiveVisibleKeys, modelVisibilityKey } from './model-visibility'\n+import {\n+  effectiveVisibleKeys,\n+  emptyProviderSentinelKey,\n+  isProviderSentinel,\n+  modelVisibilityKey\n+} from './model-visibility'\n \n const provider = (slug: string, models: string[]): ModelOptionProvider => ({\n   models,","@@ -34,4 +39,48 @@ describe('model visibility', () => {\n     expect(visible.has(modelVisibilityKey('local-ollama', 'qwen3:latest'))).toBe(true)\n     expect(visible.has(modelVisibilityKey('local-ollama', 'llama3.2:latest'))).toBe(false)\n   })\n+\n+  it('preserves hidden-provider sentinel without re-adding defaults', () => {\n+    // User explicitly hid all models for \"nous\" — sentinel marks this choice.\n+    const stored = new Set([emptyProviderSentinelKey('nous')])\n+\n+    const visible = effectiveVisibleKeys(stored, [\n+      provider('nous', ['hermes-3-llama-3.1-70b', 'hermes-3-llama-3.1-8b']),\n+      provider('ollama', ['qwen3:latest'])\n+    ])\n+\n+    expect(visible.has(modelVisibilityKey('nous', 'hermes-3-llama-3.1-70b'))).toBe(false)\n+    expect(visible.has(modelVisibilityKey('nous', 'hermes-3-llama-3.1-8b'))).toBe(false)\n+    // Sentinel itself is stripped from the result.\n+    expect(visible.has(emptyProviderSentinelKey('nous'))).toBe(false)\n+    // Other providers still get defaults.\n+    expect(visible.has(modelVisibilityKey('ollama', 'qwen3:latest'))).toBe(true)\n+  })\n+\n+  it('restores model when toggling on after hiding all', () => {\n+    // Simulates: user hid all \"nous\" models, then toggles one back on.\n+    const stored = new Set([\n+      emptyProviderSentinelKey('nous'),\n+      modelVisibilityKey('ollama', 'qwen3:latest')\n+    ])\n+\n+    // After toggle: sentinel removed, one model added.\n+    const afterToggle = new Set(stored)\n+    afterToggle.delete(emptyProviderSentinelKey('nous'))\n+    afterToggle.add(modelVisibilityKey('nous', 'hermes-3-llama-3.1-70b'))\n+\n+    const visible = effectiveVisibleKeys(afterToggle, [\n+      provider('nous', ['hermes-3-llama-3.1-70b', 'hermes-3-llama-3.1-8b']),\n+      provider('ollama', ['qwen3:latest'])\n+    ])\n+\n+    expect(visible.has(modelVisibilityKey('nous', 'hermes-3-llama-3.1-70b'))).toBe(true)\n+    expect(visible.has(modelVisibilityKey('nous', 'hermes-3-llama-3.1-8b'))).toBe(false)\n+  })\n+\n+  it('sentinel key helper produces correct format', () => {\n+    expect(emptyProviderSentinelKey('openai')).toBe('openai::')\n+    expect(isProviderSentinel('openai::')).toBe(true)\n+    expect(isProviderSentinel('openai::gpt-4o')).toBe(false)\n+  })\n })"]},{"file":"apps/desktop/src/store/model-visibility.ts","hunks":["@@ -13,6 +13,19 @@ export const DEFAULT_VISIBLE_PER_PROVIDER = 50\n  *  that contain a single colon, e.g. `model:tag`). */\n export const modelVisibilityKey = (provider: string, model: string): string => `${provider}::${model}`\n \n+/** Sentinel key suffix stored when the user explicitly hides ALL models for a\n+ *  provider.  Distinguishes \"user hid everything\" from \"never customized\" so\n+ *  `effectiveVisibleKeys` does not re-add defaults for that provider. */\n+export const EMPTY_PROVIDER_SENTINEL = ''\n+\n+/** Build the sentinel key for a provider whose last model was toggled off. */\n+export const emptyProviderSentinelKey = (provider: string): string =>\n+  modelVisibilityKey(provider, EMPTY_PROVIDER_SENTINEL)\n+\n+/** Check whether a stored key is a provider-hidden sentinel. */\n+export const isProviderSentinel = (key: string): boolean =>\n+  key.endsWith('::')\n+\n /** A model and its optional `…-fast` sibling, collapsed into one logical row.\n  *  `id` is the canonical (base) model; `fastId` is the fast variant if present. */\n export interface ModelFamily {","@@ -116,9 +129,12 @@ export function effectiveVisibleKeys(\n \n   for (const provider of providers) {\n     const providerPrefix = `${provider.slug}::`\n-    const hasStoredProvider = [...stored].some(key => key.startsWith(providerPrefix))\n+    const hasStoredProvider = [...stored].some(\n+      key => key.startsWith(providerPrefix) && !isProviderSentinel(key)\n+    )\n+    const hasSentinel = stored.has(emptyProviderSentinelKey(provider.slug))\n \n-    if (hasStoredProvider) {\n+    if (hasStoredProvider || hasSentinel) {\n       continue\n     }\n ","@@ -129,5 +145,12 @@ export function effectiveVisibleKeys(\n     }\n   }\n \n+  // Strip sentinel keys — they are bookkeeping, not real visibility entries.\n+  for (const key of [...next]) {\n+    if (isProviderSentinel(key)) {\n+      next.delete(key)\n+    }\n+  }\n+\n   return next\n }\n"]}]},{"kind":"pr","number":43542,"title":"fix(desktop): preserve empty model visibility providers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#43496","number":43496},{"kind":"pr","value":"#43485","number":43485}],"category":"superseded_by_pr","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}