diff --git a/desktop/src/components/controls/ModelSelector.test.tsx b/desktop/src/components/controls/ModelSelector.test.tsx index 59f9e0ea..62f65dab 100644 --- a/desktop/src/components/controls/ModelSelector.test.tsx +++ b/desktop/src/components/controls/ModelSelector.test.tsx @@ -4,6 +4,8 @@ import '@testing-library/jest-dom' import { ModelSelector } from './ModelSelector' import { useChatStore } from '../../stores/chatStore' +import { useHahaOAuthStore } from '../../stores/hahaOAuthStore' +import { useHahaOpenAIOAuthStore } from '../../stores/hahaOpenAIOAuthStore' import { useProviderStore } from '../../stores/providerStore' import { useSessionRuntimeStore } from '../../stores/sessionRuntimeStore' import { useSettingsStore } from '../../stores/settingsStore' @@ -28,6 +30,8 @@ afterEach(() => { useProviderStore.setState(useProviderStore.getInitialState(), true) useSessionRuntimeStore.setState(useSessionRuntimeStore.getInitialState(), true) useChatStore.setState(useChatStore.getInitialState(), true) + useHahaOAuthStore.setState(useHahaOAuthStore.getInitialState(), true) + useHahaOpenAIOAuthStore.setState(useHahaOpenAIOAuthStore.getInitialState(), true) }) describe('ModelSelector', () => { @@ -135,6 +139,7 @@ describe('ModelSelector', () => { }, ] const setSessionRuntime = vi.fn() + useHahaOpenAIOAuthStore.setState({ status: { loggedIn: true, expiresAt: null, email: null, accountId: null } }) useSettingsStore.setState({ locale: 'en', availableModels: openAIModels, @@ -169,6 +174,45 @@ describe('ModelSelector', () => { }) }) + it('hides official provider sections when OAuth is not logged in', async () => { + useHahaOAuthStore.setState({ status: { loggedIn: false } }) + useHahaOpenAIOAuthStore.setState({ status: { loggedIn: false } }) + useSettingsStore.setState({ + locale: 'en', + availableModels: MODELS, + currentModel: MODELS[0], + activeProviderName: 'Provider A', + }) + useProviderStore.setState({ + providers: [{ + id: 'provider-a', + presetId: 'custom', + name: 'Provider A', + apiKey: '***', + baseUrl: 'https://api.example.com', + apiFormat: 'anthropic', + models: { + main: 'provider-main', + haiku: '', + sonnet: '', + opus: '', + }, + }], + activeId: 'provider-a', + hasLoadedProviders: true, + isLoading: true, + }) + + render() + + await clickByRole(/alpha/i) + + const dropdown = screen.getByTestId('model-selector-dropdown') + expect(dropdown.textContent).not.toContain('Claude Official') + expect(dropdown.textContent).not.toContain('ChatGPT Official') + expect(dropdown.textContent).toContain('Provider A') + }) + it('portals the dropdown outside clipping containers and positions it below the trigger', async () => { useSettingsStore.setState({ locale: 'en', diff --git a/desktop/src/components/controls/ModelSelector.tsx b/desktop/src/components/controls/ModelSelector.tsx index b39ed9a7..0b38c88a 100644 --- a/desktop/src/components/controls/ModelSelector.tsx +++ b/desktop/src/components/controls/ModelSelector.tsx @@ -16,6 +16,8 @@ import type { RuntimeSelection } from '../../types/runtime' import type { EffortLevel, ModelInfo } from '../../types/settings' import { useMobileViewport } from '../../hooks/useMobileViewport' import { isTauriRuntime } from '../../lib/desktopRuntime' +import { useHahaOAuthStore } from '../../stores/hahaOAuthStore' +import { useHahaOpenAIOAuthStore } from '../../stores/hahaOpenAIOAuthStore' import { MobileBottomSheet } from '../shared/MobileBottomSheet' type ProviderChoice = { @@ -101,6 +103,8 @@ function buildProviderChoices( officialName: string, openAIOfficialName: string, labels: Record<'main' | 'haiku' | 'sonnet' | 'opus', string>, + claudeOfficialLoggedIn: boolean, + openAIOfficialLoggedIn: boolean, ): ProviderChoice[] { const claudeOfficialModels = activeId === null && availableModels.length > 0 ? availableModels @@ -109,21 +113,30 @@ function buildProviderChoices( ? availableModels : OPENAI_OFFICIAL_MODELS - return [ - officialChoices(null, claudeOfficialModels, activeId === null, officialName), - officialChoices( + const choices: ProviderChoice[] = [] + + if (claudeOfficialLoggedIn) { + choices.push(officialChoices(null, claudeOfficialModels, activeId === null, officialName)) + } + if (openAIOfficialLoggedIn) { + choices.push(officialChoices( OPENAI_OFFICIAL_PROVIDER_ID, openAIOfficialModels, activeId === OPENAI_OFFICIAL_PROVIDER_ID, openAIOfficialName, - ), - ...providers.map((provider) => ({ + )) + } + + for (const provider of providers) { + choices.push({ providerId: provider.id, providerName: provider.name, isDefault: activeId === provider.id, models: buildProviderModels(provider, labels), - })), - ] + }) + } + + return choices } function resolveDefaultRuntimeSelection( @@ -173,6 +186,10 @@ export function ModelSelector({ isLoading: providersLoading, fetchProviders, } = useProviderStore() + const claudeOAuthStatus = useHahaOAuthStore((s) => s.status) + const fetchClaudeOAuthStatus = useHahaOAuthStore((s) => s.fetchStatus) + const openAIOAuthStatus = useHahaOpenAIOAuthStore((s) => s.status) + const fetchOpenAIOAuthStatus = useHahaOpenAIOAuthStore((s) => s.fetchStatus) const runtimeSelection = useSessionRuntimeStore((state) => runtimeKey ? state.selections[runtimeKey] : undefined, ) @@ -200,6 +217,11 @@ export function ModelSelector({ void fetchProviders() }, [fetchProviders, isRuntimeScoped, providersLoading]) + useEffect(() => { + void fetchClaudeOAuthStatus() + void fetchOpenAIOAuthStatus() + }, [fetchClaudeOAuthStatus, fetchOpenAIOAuthStatus]) + useEffect(() => { if (!open) return const handleClick = (e: MouseEvent) => { @@ -290,8 +312,10 @@ export function ModelSelector({ t('settings.providers.officialName'), t('settings.providers.openaiOfficialName'), roleLabels, + claudeOAuthStatus?.loggedIn === true, + openAIOAuthStatus?.loggedIn === true, ), - [activeId, availableModels, providers, roleLabels, t], + [activeId, availableModels, providers, roleLabels, t, claudeOAuthStatus, openAIOAuthStatus], ) const selectedModel = isControlled