diff --git a/apps/desktop/src/app/settings/providers-settings.test.tsx b/apps/desktop/src/app/settings/providers-settings.test.tsx index ac63be962ae2..0a1da7c45449 100644 --- a/apps/desktop/src/app/settings/providers-settings.test.tsx +++ b/apps/desktop/src/app/settings/providers-settings.test.tsx @@ -8,6 +8,7 @@ const listOAuthProviders = vi.fn() const disconnectOAuthProvider = vi.fn() const getEnvVars = vi.fn() const startManualProviderOAuth = vi.fn() +const startManualLocalEndpoint = vi.fn() const onboarding = atom({ manual: false }) vi.mock('@/hermes', () => ({ @@ -18,7 +19,8 @@ vi.mock('@/hermes', () => ({ vi.mock('@/store/onboarding', () => ({ $desktopOnboarding: onboarding, - startManualProviderOAuth: (providerId: string) => startManualProviderOAuth(providerId) + startManualProviderOAuth: (providerId: string) => startManualProviderOAuth(providerId), + startManualLocalEndpoint: (reason: null | string) => startManualLocalEndpoint(reason) })) function provider(id: string, loggedIn: boolean, patch: Partial = {}): OAuthProvider { @@ -182,4 +184,23 @@ describe('ProvidersSettings', () => { }) expect(await screen.findByText('No providers match your search.')).toBeTruthy() }) + + it('offers a Local / custom endpoint entry in the API-keys tab that opens the custom-endpoint flow', async () => { + // Regression: the composer pill and the providers "have an API key" + // affordance both dead-end on the env-var-driven key catalog, which never + // lists a custom endpoint — so without this row there is no reachable + // Desktop GUI path to add one. See issue #62817. + getEnvVars.mockResolvedValue({}) + listOAuthProviders.mockResolvedValue({ providers: [] }) + + const { ProvidersSettings } = await import('./providers-settings') + render() + + const row = await screen.findByText('Local / custom endpoint') + expect(screen.getByText(/OpenAI-compatible endpoint/)).toBeTruthy() + + fireEvent.click(row) + + await waitFor(() => expect(startManualLocalEndpoint).toHaveBeenCalledWith(null)) + }) }) diff --git a/apps/desktop/src/app/settings/providers-settings.tsx b/apps/desktop/src/app/settings/providers-settings.tsx index c316cb0fc1e4..7b72c604fa12 100644 --- a/apps/desktop/src/app/settings/providers-settings.tsx +++ b/apps/desktop/src/app/settings/providers-settings.tsx @@ -21,7 +21,7 @@ import { Check, ChevronDown, ChevronRight, KeyRound, Loader2, Terminal, Trash2 } import { normalize } from '@/lib/text' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' -import { $desktopOnboarding, startManualProviderOAuth } from '@/store/onboarding' +import { $desktopOnboarding, startManualLocalEndpoint, startManualProviderOAuth } from '@/store/onboarding' import type { EnvVarInfo, OAuthProvider } from '@/types/hermes' import { isKeyVar, ProviderKeyRows } from './credential-key-ui' @@ -300,6 +300,36 @@ function NoProviderKeys() { ) } +// Surfaces the "Local / custom endpoint" entry point directly in the API-keys +// tab so users can add any OpenAI-compatible endpoint (Zyphra, vLLM, Ollama…) +// from the GUI. The composer pill and the providers "have an API key" affordance +// both dead-end on the env-var-driven key catalog, which never lists a custom +// endpoint — so without this row there is no reachable Desktop path to it. +// The whole row is the button so the click target and a11y focus match the +// visible area (the chevron + gutter are inside the button, not beside it). +// Pass reason: null — the onboarding overlay renders an unmapped reason string +// verbatim as a banner (see ReasonNotice in onboarding/index.tsx), and we don't +// want a raw identifier like "providers-keys-tab" showing as literal text. +function LocalEndpointRow({ onOpen }: { onOpen: (reason: null | string) => void }) { + const { t } = useI18n() + const copy = t.settings.providers.localEndpoint + + return ( + onOpen(null)} + > +
+ {copy.title} + + {copy.description} + +
+ +
+ ) +} + export function ProvidersSettings({ onClose, onViewChange, view }: ProvidersSettingsProps) { const { t } = useI18n() const { rowProps, vars } = useEnvCredentials() @@ -417,6 +447,7 @@ export function ProvidersSettings({ onClose, onViewChange, view }: ProvidersSett return ( + {keyGroups.length > 0 ? (