From 6cb04be779de1809c5f6095d9bc9e0b99344e51e Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Thu, 18 Jun 2026 23:29:31 -0400 Subject: [PATCH] feat(desktop): Keys tab groups by backend provider identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../app/settings/providers-settings.test.tsx | 27 +++++++++++++++++++ .../src/app/settings/providers-settings.tsx | 20 +++++++++++--- apps/desktop/src/types/hermes.ts | 6 +++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/settings/providers-settings.test.tsx b/apps/desktop/src/app/settings/providers-settings.test.tsx index 27c029b442c..d20f71b5ab4 100644 --- a/apps/desktop/src/app/settings/providers-settings.test.tsx +++ b/apps/desktop/src/app/settings/providers-settings.test.tsx @@ -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() + + expect(await screen.findByText('WidgetAI')).toBeTruthy() + }) }) diff --git a/apps/desktop/src/app/settings/providers-settings.tsx b/apps/desktop/src/app/settings/providers-settings.tsx index 2585e13995d..e0ae46e7da1 100644 --- a/apps/desktop/src/app/settings/providers-settings.tsx +++ b/apps/desktop/src/app/settings/providers-settings.tsx @@ -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): ProviderKeyGroup[] { const buckets = new Map() @@ -55,7 +64,9 @@ function buildProviderKeyGroups(vars: Record): 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): 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({ diff --git a/apps/desktop/src/types/hermes.ts b/apps/desktop/src/types/hermes.ts index a497e3f10a9..b67cc3041a7 100644 --- a/apps/desktop/src/types/hermes.ts +++ b/apps/desktop/src/types/hermes.ts @@ -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