feat(desktop): Keys tab groups by backend provider identity

buildProviderKeyGroups now groups provider env vars by the backend-supplied
provider/provider_label (from the unified catalog — the same identity hermes
model uses), falling back to the desktop PROVIDER_GROUPS prefix match only when
the backend gives no hint. A provider the backend tags now always renders its
own Keys card, even with no hand-maintained PROVIDER_GROUPS prefix row —
PROVIDER_GROUPS is demoted to a presentation overlay (priority/blurb/docs).

Adds provider/provider_label to EnvVarInfo. New vitest asserts a backend-tagged
provider with no prefix row still renders a card.
This commit is contained in:
Austin Pickett 2026-06-18 23:29:31 -04:00 committed by Teknium
parent 60dfa0f31b
commit 6cb04be779
3 changed files with 50 additions and 3 deletions

View file

@ -97,4 +97,31 @@ describe('ProvidersSettings', () => {
expect(screen.queryByRole('button', { name: 'Remove Qwen Code' })).toBeNull()
expect(screen.getByText(/managed by its own CLI/)).toBeTruthy()
})
it('renders a Keys card for a backend-tagged provider with no PROVIDER_GROUPS prefix', async () => {
// A provider the backend catalog tags (provider/provider_label) but that has
// no desktop PROVIDER_GROUPS prefix row must still render its own card —
// this is the GUI/CLI drift fix: membership comes from the backend, not
// from the hand-maintained prefix list.
getEnvVars.mockResolvedValue({
WIDGETAI_API_KEY: {
advanced: false,
category: 'provider',
description: 'WidgetAI direct API',
is_password: true,
is_set: false,
provider: 'widgetai',
provider_label: 'WidgetAI',
redacted_value: null,
tools: [],
url: 'https://widgetai.example/keys'
}
})
listOAuthProviders.mockResolvedValue({ providers: [] })
const { ProvidersSettings } = await import('./providers-settings')
render(<ProvidersSettings onClose={vi.fn()} onViewChange={vi.fn()} view="keys" />)
expect(await screen.findByText('WidgetAI')).toBeTruthy()
})
})

View file

@ -45,8 +45,17 @@ export const PROVIDER_VIEWS = ['accounts', 'keys'] as const
export type ProviderView = (typeof PROVIDER_VIEWS)[number]
// Group the env catalog by provider — one ListRow per vendor plus optional
// advanced overrides (base URL, region, etc.). Groups without a key field and
// the "Other" bucket are skipped.
// advanced overrides (base URL, region, etc.). Groups without a key field are
// skipped.
//
// Grouping key precedence:
// 1. Backend `provider_label` / `provider` (from the unified provider catalog
// in hermes_cli/provider_catalog.py) — the SAME provider identity
// `hermes model` uses. This is authoritative: a provider tagged by the
// backend always renders a card, even with no PROVIDER_GROUPS row.
// 2. Desktop prefix match (`providerGroup`) — legacy fallback for provider
// env vars that predate the backend tagging.
// Only entries that resolve to neither (the "Other" bucket) are skipped.
function buildProviderKeyGroups(vars: Record<string, EnvVarInfo>): ProviderKeyGroup[] {
const buckets = new Map<string, [string, EnvVarInfo][]>()
@ -55,7 +64,9 @@ function buildProviderKeyGroups(vars: Record<string, EnvVarInfo>): ProviderKeyGr
continue
}
const name = providerGroup(key)
// Prefer the backend-supplied provider label/id so the Keys tab groups by
// the same identity the CLI picker uses; fall back to the prefix guess.
const name = info.provider_label?.trim() || info.provider?.trim() || providerGroup(key)
if (name === 'Other') {
continue
@ -73,6 +84,9 @@ function buildProviderKeyGroups(vars: Record<string, EnvVarInfo>): ProviderKeyGr
continue
}
// Presentation overlay (priority, blurb, docs) is keyed by the prefix-based
// group name; when the backend introduced this provider it may have no
// overlay entry, so fall back to the backend/env metadata for display.
const meta = providerMeta(name)
groups.push({

View file

@ -108,6 +108,12 @@ export interface EnvVarInfo {
description: string
is_password: boolean
is_set: boolean
// Backend-derived provider grouping hints (from the unified provider catalog
// in hermes_cli/provider_catalog.py). When present, the Keys tab groups by
// this provider identity — the SAME one `hermes model` uses — instead of
// desktop-only env-var prefix guesses. Empty for non-provider env vars.
provider?: string
provider_label?: string
redacted_value: null | string
tools: string[]
url: null | string