From 64166f87baff684cb9a71478fab95fd1e6f81d0b Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 28 Jul 2026 03:52:33 -0500 Subject: [PATCH] feat(desktop): default the model picker to the featured shortlist expandProviderDefaults prefers a provider's featured_models when present and falls back to the existing top-N for providers that ship none (single-lab, local, custom). Only aggregators get curated, so exactly the providers with the everything-under-the-sun problem are trimmed; the rest are unchanged. Every non-featured model stays one search or Edit Models toggle away. --- .../src/store/model-visibility.test.ts | 103 ++++++++++++++++++ apps/desktop/src/store/model-visibility.ts | 56 +++++++++- apps/desktop/src/types/hermes.ts | 5 + 3 files changed, 161 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/store/model-visibility.test.ts b/apps/desktop/src/store/model-visibility.test.ts index 042e2d4279c..a63aede8aaf 100644 --- a/apps/desktop/src/store/model-visibility.test.ts +++ b/apps/desktop/src/store/model-visibility.test.ts @@ -10,6 +10,7 @@ import { isProviderSentinel, modelVisibilityKey, resolveVisibleKeys, + setProviderVisibility, toggleModelVisibility } from './model-visibility' @@ -224,3 +225,105 @@ describe('resolveVisibleKeys', () => { expect([...resolveVisibleKeys(new Set(), providers)]).toEqual([]) }) }) + +describe('featured defaults', () => { + const featuredProvider = (slug: string, models: string[], featured_models: string[]): ModelOptionProvider => ({ + featured_models, + models, + name: slug, + slug + }) + + it('defaults to the featured shortlist when a provider publishes one', () => { + const nous = featuredProvider( + 'nous', + ['anthropic/opus', 'anthropic/haiku', 'google/gemini', 'x-ai/grok'], + ['anthropic/opus', 'google/gemini', 'x-ai/grok'] + ) + + const visible = defaultVisibleKeys([nous]) + + // Featured are visible; the non-featured model is hidden by default. + expect(visible.has(modelVisibilityKey('nous', 'anthropic/opus'))).toBe(true) + expect(visible.has(modelVisibilityKey('nous', 'google/gemini'))).toBe(true) + expect(visible.has(modelVisibilityKey('nous', 'x-ai/grok'))).toBe(true) + expect(visible.has(modelVisibilityKey('nous', 'anthropic/haiku'))).toBe(false) + }) + + it('falls back to top-N when a provider ships no featured list', () => { + const plain = provider('ollama', ['qwen3:latest', 'llama3.2:latest']) + + const visible = defaultVisibleKeys([plain]) + + // No featured_models → every model stays a default (top-N, N ≫ 2 here). + expect(visible.has(modelVisibilityKey('ollama', 'qwen3:latest'))).toBe(true) + expect(visible.has(modelVisibilityKey('ollama', 'llama3.2:latest'))).toBe(true) + }) + + it('ignores an empty featured list and falls back to top-N', () => { + const plain = featuredProvider('ollama', ['qwen3:latest', 'llama3.2:latest'], []) + + const visible = defaultVisibleKeys([plain]) + + expect(visible.has(modelVisibilityKey('ollama', 'qwen3:latest'))).toBe(true) + expect(visible.has(modelVisibilityKey('ollama', 'llama3.2:latest'))).toBe(true) + }) +}) + +describe('setProviderVisibility', () => { + const providers = [provider('openai', ['gpt-a', 'gpt-b']), provider('nous', ['hermes-x', 'hermes-y'])] + + it('enabling a provider makes every one of its models visible', () => { + // Start from a hidden-all openai; flip it on. + const stored = new Set([emptyProviderSentinelKey('openai')]) + + const next = setProviderVisibility(stored, providers, 'openai', true) + + const visible = effectiveVisibleKeys(next, providers) + expect(visible.has(modelVisibilityKey('openai', 'gpt-a'))).toBe(true) + expect(visible.has(modelVisibilityKey('openai', 'gpt-b'))).toBe(true) + // Sentinel is cleared. + expect(next.has(emptyProviderSentinelKey('openai'))).toBe(false) + }) + + it('disabling a provider hides all its models and records the sentinel', () => { + const next = setProviderVisibility(null, providers, 'openai', false) + + expect(next.has(emptyProviderSentinelKey('openai'))).toBe(true) + const visible = effectiveVisibleKeys(next, providers) + expect(visible.has(modelVisibilityKey('openai', 'gpt-a'))).toBe(false) + expect(visible.has(modelVisibilityKey('openai', 'gpt-b'))).toBe(false) + }) + + it('leaves other providers untouched (their sentinels survive)', () => { + const stored = new Set([emptyProviderSentinelKey('nous')]) + + // Turn openai fully on; nous must stay hidden. + const next = setProviderVisibility(stored, providers, 'openai', true) + + expect(next.has(emptyProviderSentinelKey('nous'))).toBe(true) + const visible = effectiveVisibleKeys(next, providers) + expect(visible.has(modelVisibilityKey('nous', 'hermes-x'))).toBe(false) + expect(visible.has(modelVisibilityKey('openai', 'gpt-a'))).toBe(true) + }) + + it('round-trips: enable then disable returns to a clean hidden-all', () => { + const enabled = setProviderVisibility(null, providers, 'openai', true) + const disabled = setProviderVisibility(enabled, providers, 'openai', false) + + expect(disabled.has(emptyProviderSentinelKey('openai'))).toBe(true) + // No stray real keys left for the provider. + expect([...disabled].some(k => k.startsWith('openai::') && !isProviderSentinel(k))).toBe(false) + }) + + it('collapses model families to one key per family when enabling', () => { + // A base + its -fast sibling collapse to a single family row/key. + const ps = [provider('nous', ['model', 'model-fast'])] + + const next = setProviderVisibility(null, ps, 'nous', true) + + expect(next.has(modelVisibilityKey('nous', 'model'))).toBe(true) + // The -fast sibling is represented by its base family, not its own key. + expect(next.has(modelVisibilityKey('nous', 'model-fast'))).toBe(false) + }) +}) diff --git a/apps/desktop/src/store/model-visibility.ts b/apps/desktop/src/store/model-visibility.ts index a6fd9a40a18..0fd02dcbac6 100644 --- a/apps/desktop/src/store/model-visibility.ts +++ b/apps/desktop/src/store/model-visibility.ts @@ -111,13 +111,21 @@ export function defaultVisibleKeys(providers: readonly ModelOptionProvider[]): S return keys } -/** Add a provider's curated default model keys (top-N collapsed families) to - * `target`. Shared by `defaultVisibleKeys` and `resolveVisibleKeys` so the +/** Add a provider's curated default model keys to `target`. Prefers the + * backend's `featured_models` shortlist (one flagship per lab) for aggregator + * providers that would otherwise flood the default view with dozens of models; + * falls back to the top-N collapsed families when a provider ships no featured + * list. Shared by `defaultVisibleKeys` and `resolveVisibleKeys` so the * expansion rule lives in exactly one place. */ function expandProviderDefaults(provider: ModelOptionProvider, target: Set): void { const families = collapseModelFamilies(provider.models ?? []) - for (const family of families.slice(0, DEFAULT_VISIBLE_PER_PROVIDER)) { + const featured = provider.featured_models ?? [] + const defaults = featured.length + ? families.filter(family => featured.includes(family.id)) + : families.slice(0, DEFAULT_VISIBLE_PER_PROVIDER) + + for (const family of defaults) { target.add(modelVisibilityKey(provider.slug, family.id)) } } @@ -209,3 +217,45 @@ export function toggleModelVisibility( return next } + +/** Compute the next persisted visibility set when a provider's master switch is + * flipped. `visible=true` enables every one of the provider's collapsed model + * families (and clears its hide-all sentinel); `visible=false` removes them all + * and records the sentinel so the defaults are not silently re-expanded. + * Seeds from `resolveVisibleKeys` so other providers' state (including their + * sentinels) survives the persist, mirroring `toggleModelVisibility`. */ +export function setProviderVisibility( + stored: Set | null, + providers: readonly ModelOptionProvider[], + providerSlug: string, + visible: boolean +): Set { + const next = resolveVisibleKeys(stored, providers) + const sentinel = emptyProviderSentinelKey(providerSlug) + const provider = providers.find(p => p.slug === providerSlug) + const families = collapseModelFamilies(provider?.models ?? []) + + // Drop every existing entry for this provider (real keys + sentinel); we + // rebuild its state from scratch below. + for (const key of [...next]) { + if (key.startsWith(`${providerSlug}::`)) { + next.delete(key) + } + } + + if (visible) { + for (const family of families) { + next.add(modelVisibilityKey(providerSlug, family.id)) + } + + // A provider with zero models can't be "all on" — leave it empty rather + // than stranding a sentinel that reads as an explicit hide-all. + if (families.length === 0) { + next.delete(sentinel) + } + } else { + next.add(sentinel) + } + + return next +} diff --git a/apps/desktop/src/types/hermes.ts b/apps/desktop/src/types/hermes.ts index 8857bca4327..a819a08fed6 100644 --- a/apps/desktop/src/types/hermes.ts +++ b/apps/desktop/src/types/hermes.ts @@ -363,6 +363,11 @@ export interface ModelOptionProvider { slug: string total_models?: number warning?: string + /** Curated shortlist (one flagship per lab) the picker shows by default for + * aggregator providers that serve dozens of models across many labs. Empty + * for providers with no manifest entry — the picker falls back to top-N. + * The rest of `models` stays reachable via search / Edit Models. */ + featured_models?: string[] /** True when the provider has usable credentials. False for canonical * providers surfaced by `include_unconfigured` that the user hasn't set up * yet — render these with a setup affordance instead of hiding them. */