feat: hide official provider sections in ModelSelector when OAuth not logged in

Only show Claude Official and ChatGPT Official model groups in the
runtime-scoped ModelSelector dropdown when the corresponding OAuth
session is active. This prevents users from selecting official models
they cannot actually use.

- Import useHahaOAuthStore and useHahaOpenAIOAuthStore into ModelSelector
- Fetch OAuth status on mount
- Pass login flags to buildProviderChoices to conditionally include sections
- Add test case verifying hidden sections when not logged in

Co-Authored-By: claude-opus-4-6 <noreply@anthropic.com>
This commit is contained in:
派大星 2026-05-24 00:30:15 +08:00
parent 91224cbd20
commit aa1321a02b
2 changed files with 76 additions and 8 deletions

View File

@ -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(<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 = {
@ -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