Merge pull request #586 from yjjheizhu/feat/hide-official-providers-when-not-logged-in

feat: hide official provider sections when OAuth not logged in
This commit is contained in:
程序员阿江-Relakkes 2026-05-24 01:53:28 +08:00 committed by GitHub
commit 071ee48e2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 91 additions and 13 deletions

View File

@ -1,9 +1,11 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
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,14 @@ 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)
})
// Prevent real API calls from fetchStatus on mount
beforeEach(() => {
useHahaOAuthStore.setState({ fetchStatus: async () => {} })
useHahaOpenAIOAuthStore.setState({ fetchStatus: async () => {} })
})
describe('ModelSelector', () => {
@ -135,6 +145,10 @@ describe('ModelSelector', () => {
},
]
const setSessionRuntime = vi.fn()
useHahaOpenAIOAuthStore.setState({
status: { loggedIn: true, expiresAt: null, email: null, accountId: null },
fetchStatus: async () => {},
})
useSettingsStore.setState({
locale: 'en',
availableModels: openAIModels,
@ -169,6 +183,45 @@ describe('ModelSelector', () => {
})
})
it('hides official provider sections when OAuth is not logged in', async () => {
useHahaOAuthStore.setState({ status: { loggedIn: false }, fetchStatus: async () => {} })
useHahaOpenAIOAuthStore.setState({ status: { loggedIn: false }, fetchStatus: async () => {} })
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(<ModelSelector runtimeKey="session-hide" />)
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',

View File

@ -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 = {
@ -36,7 +38,8 @@ type Props = {
}
type DropdownPosition = {
top: number
top: number | undefined
bottom: number | undefined
left: number
width: number
maxHeight: number
@ -101,6 +104,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 +114,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 +187,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 +218,11 @@ export function ModelSelector({
void fetchProviders()
}, [fetchProviders, isRuntimeScoped, providersLoading])
useEffect(() => {
void fetchClaudeOAuthStatus()
void fetchOpenAIOAuthStatus()
}, [fetchClaudeOAuthStatus, fetchOpenAIOAuthStatus])
useEffect(() => {
if (!open) return
const handleClick = (e: MouseEvent) => {
@ -245,9 +268,8 @@ export function ModelSelector({
const maxHeight = Math.min(DROPDOWN_MAX_HEIGHT, availableHeight)
setDropdownPosition({
top: placeBelow
? rect.bottom + DROPDOWN_GAP
: Math.max(VIEWPORT_MARGIN, rect.top - DROPDOWN_GAP - maxHeight),
top: placeBelow ? rect.bottom + DROPDOWN_GAP : undefined,
bottom: placeBelow ? undefined : (viewportHeight - rect.top + DROPDOWN_GAP),
left,
width,
maxHeight,
@ -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
@ -511,6 +535,7 @@ export function ModelSelector({
className="fixed z-[80] rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] shadow-[var(--shadow-dropdown)]"
style={{
top: dropdownPosition.top,
bottom: dropdownPosition.bottom,
left: dropdownPosition.left,
width: dropdownPosition.width,
}}