From ddfbce071bb378b94b829c37ea043c92174e19b0 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Mon, 15 Dec 2025 23:54:10 +0800 Subject: [PATCH] feat(provider): add agent support filter for provider list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add filter dropdown in provider search to filter by agent support - Create AnthropicProviderListPopover component for showing supported providers - Add filter=agent URL parameter support in ProviderList - Update AgentModal and CodeToolsPage to use the new popover component - Add getAnthropicSupportedProviders utility function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../AnthropicProviderListPopover.tsx | 148 ++++++++++++++++++ .../components/Popups/agent/AgentModal.tsx | 12 +- src/renderer/src/i18n/locales/en-us.json | 9 +- src/renderer/src/i18n/locales/zh-cn.json | 9 +- src/renderer/src/i18n/locales/zh-tw.json | 9 +- src/renderer/src/i18n/translate/de-de.json | 9 +- src/renderer/src/i18n/translate/el-gr.json | 9 +- src/renderer/src/i18n/translate/es-es.json | 9 +- src/renderer/src/i18n/translate/fr-fr.json | 9 +- src/renderer/src/i18n/translate/ja-jp.json | 9 +- src/renderer/src/i18n/translate/pt-pt.json | 9 +- src/renderer/src/i18n/translate/ru-ru.json | 9 +- src/renderer/src/pages/code/CodeToolsPage.tsx | 59 +------ .../ProviderSettings/ProviderList.tsx | 68 +++++++- .../src/utils/__tests__/provider.test.ts | 22 +++ src/renderer/src/utils/provider.ts | 8 + 16 files changed, 338 insertions(+), 69 deletions(-) create mode 100644 src/renderer/src/components/AnthropicProviderListPopover.tsx diff --git a/src/renderer/src/components/AnthropicProviderListPopover.tsx b/src/renderer/src/components/AnthropicProviderListPopover.tsx new file mode 100644 index 000000000..9e38f60ab --- /dev/null +++ b/src/renderer/src/components/AnthropicProviderListPopover.tsx @@ -0,0 +1,148 @@ +import { ProviderAvatar } from '@renderer/components/ProviderAvatar' +import { useAllProviders } from '@renderer/hooks/useProvider' +import ImageStorage from '@renderer/services/ImageStorage' +import type { Provider } from '@renderer/types' +import { getFancyProviderName } from '@renderer/utils' +import { getClaudeSupportedProviders } from '@renderer/utils/provider' +import type { PopoverProps } from 'antd' +import { Popover } from 'antd' +import { ArrowUpRight, HelpCircle } from 'lucide-react' +import type { FC, ReactNode } from 'react' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +interface AnthropicProviderListPopoverProps { + /** Callback when provider is clicked */ + onProviderClick?: () => void + /** Use window.navigate instead of Link (for non-router context like TopView) */ + useWindowNavigate?: boolean + /** Custom trigger element, defaults to HelpCircle icon */ + children?: ReactNode + /** Popover placement */ + placement?: PopoverProps['placement'] + /** Custom filter function for providers, defaults to getClaudeSupportedProviders */ + filterProviders?: (providers: Provider[]) => Provider[] +} + +const AnthropicProviderListPopover: FC = ({ + onProviderClick, + useWindowNavigate = false, + children, + placement = 'right', + filterProviders = getClaudeSupportedProviders +}) => { + const { t } = useTranslation() + const allProviders = useAllProviders() + const providers = filterProviders(allProviders) + const [providerLogos, setProviderLogos] = useState>({}) + + useEffect(() => { + const loadAllLogos = async () => { + const logos: Record = {} + for (const provider of providers) { + if (provider.id) { + try { + const logoData = await ImageStorage.get(`provider-${provider.id}`) + if (logoData) { + logos[provider.id] = logoData + } + } catch { + // Ignore errors loading logos + } + } + } + setProviderLogos(logos) + } + + loadAllLogos() + }, [providers]) + + const handleClick = (providerId: string) => { + onProviderClick?.() + if (useWindowNavigate) { + window.navigate(`/settings/provider?id=${providerId}`) + } + } + + const content = ( + + {t('code.supported_providers')} + + {providers.map((provider) => + useWindowNavigate ? ( + handleClick(provider.id)}> + + {getFancyProviderName(provider)} + + + ) : ( + handleClick(provider.id)}> + + {getFancyProviderName(provider)} + + + ) + )} + + + ) + + return ( + + {children || } + + ) +} + +const PopoverContent = styled.div` + width: 200px; +` + +const PopoverTitle = styled.div` + margin-bottom: 8px; + font-weight: 500; +` + +const ProviderListContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +` + +const ProviderItem = styled.div` + color: var(--color-text); + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + &:hover { + color: var(--color-link); + } +` + +const ProviderLink = styled.a` + color: var(--color-text); + display: flex; + align-items: center; + gap: 4px; + text-decoration: none; + &:hover { + color: var(--color-link); + } +` + +export default AnthropicProviderListPopover diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index 8a8b4fe61..d9d9712a7 100644 --- a/src/renderer/src/components/Popups/agent/AgentModal.tsx +++ b/src/renderer/src/components/Popups/agent/AgentModal.tsx @@ -1,6 +1,6 @@ import { loggerService } from '@logger' +import AnthropicProviderListPopover from '@renderer/components/AnthropicProviderListPopover' import { ErrorBoundary } from '@renderer/components/ErrorBoundary' -import { HelpTooltip } from '@renderer/components/TooltipIcons' import { TopView } from '@renderer/components/TopView' import { permissionModeCards } from '@renderer/config/agent' import { useAgents } from '@renderer/hooks/agents/useAgents' @@ -16,6 +16,7 @@ import type { UpdateAgentForm } from '@renderer/types' import { AgentConfigurationSchema, isAgentType } from '@renderer/types' +import { getAnthropicSupportedProviders } from '@renderer/utils/provider' import { Alert, Button, Input, Modal, Select } from 'antd' import { AlertTriangleIcon } from 'lucide-react' import type { ChangeEvent, FormEvent } from 'react' @@ -420,7 +421,14 @@ const PopupContainer: React.FC = ({ agent, afterSubmit, resolve }) => { - + { + setOpen(false) + resolve(undefined) + }} + /> { const { t } = useTranslation() const { providers } = useProviders() - const allProviders = useAllProviders() const dispatch = useAppDispatch() const isBunInstalled = useAppSelector((state) => state.mcp.isBunInstalled) const { @@ -372,48 +368,7 @@ const CodeToolsPage: FC = () => {
{t('code.model')} - {selectedCliTool === 'claude-code' && ( - -
{t('code.supported_providers')}
-
- {getClaudeSupportedProviders(allProviders).map((provider) => { - return ( - - - {getProviderLabel(provider.id)} - - - ) - })} -
-
- } - trigger="hover" - placement="right"> - - - )} + {selectedCliTool === 'claude-code' && } { const { t } = useTranslation() const [searchText, setSearchText] = useState('') const [dragging, setDragging] = useState(false) + const [agentFilterEnabled, setAgentFilterEnabled] = useState(false) const [providerLogos, setProviderLogos] = useState>({}) const listRef = useRef(null) @@ -71,7 +72,16 @@ const ProviderList: FC = () => { }, [providers]) useEffect(() => { - if (searchParams.get('id')) { + let shouldUpdate = false + const hasFilterParam = searchParams.get('filter') === 'agent' + + // Handle filter param first - when filter is enabled, ignore id param + if (hasFilterParam) { + setAgentFilterEnabled(true) + searchParams.delete('filter') + searchParams.delete('id') // Clear id param when filter is enabled + shouldUpdate = true + } else if (searchParams.get('id')) { const providerId = searchParams.get('id') const provider = providers.find((p) => p.id === providerId) if (provider) { @@ -89,6 +99,10 @@ const ProviderList: FC = () => { setSelectedProvider(providers[0]) } searchParams.delete('id') + shouldUpdate = true + } + + if (shouldUpdate) { setSearchParams(searchParams) } }, [providers, searchParams, setSearchParams, setSelectedProvider, setTimeoutTimer]) @@ -282,6 +296,11 @@ const ProviderList: FC = () => { return false } + // Filter by agent support + if (agentFilterEnabled && !provider.anthropicApiHost) { + return false + } + const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean) const isProviderMatch = matchKeywordsInProvider(keywords, provider) const isModelMatch = provider.models.some((model) => matchKeywordsInModel(keywords, model)) @@ -316,7 +335,34 @@ const ProviderList: FC = () => { placeholder={t('settings.provider.search')} value={searchText} style={{ borderRadius: 'var(--list-item-border-radius)', height: 35 }} - suffix={} + prefix={} + suffix={ + : , + onClick: () => setAgentFilterEnabled(false) + }, + { + label: t('settings.provider.filter.agent'), + key: 'agent', + icon: agentFilterEnabled ? : , + onClick: () => setAgentFilterEnabled(true) + } + ] + }} + trigger={['click']}> + + + + + } onChange={(e) => setSearchText(e.target.value)} onKeyDown={(e) => { if (e.key === 'Escape') { @@ -457,4 +503,20 @@ const AddButtonWrapper = styled.div` padding: 10px 8px; ` +const FilterButton = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 4px; + cursor: pointer; +` + +const CheckPlaceholder = styled.span` + display: inline-block; + width: 14px; + height: 14px; +` + export default ProviderList diff --git a/src/renderer/src/utils/__tests__/provider.test.ts b/src/renderer/src/utils/__tests__/provider.test.ts index 269c38490..1fc4fc6f3 100644 --- a/src/renderer/src/utils/__tests__/provider.test.ts +++ b/src/renderer/src/utils/__tests__/provider.test.ts @@ -2,9 +2,11 @@ import { type AzureOpenAIProvider, type Provider, SystemProviderIds } from '@ren import { describe, expect, it, vi } from 'vitest' import { + getAnthropicSupportedProviders, getClaudeSupportedProviders, isAIGatewayProvider, isAnthropicProvider, + isAnthropicSupportedProvider, isAzureOpenAIProvider, isCherryAIProvider, isGeminiProvider, @@ -67,6 +69,26 @@ describe('provider utils', () => { expect(getClaudeSupportedProviders(providers)).toEqual(providers.slice(0, 3)) }) + it('filters Anthropic supported providers', () => { + const providers = [ + createProvider({ id: 'anthropic-official', type: 'anthropic' }), + createProvider({ id: 'custom-host', anthropicApiHost: 'https://anthropic.local' }), + createProvider({ id: 'aihubmix' }), + createProvider({ id: 'other' }) + ] + + expect(getAnthropicSupportedProviders(providers)).toEqual(providers.slice(0, 2)) + }) + + it('checks Anthropic supported provider', () => { + expect(isAnthropicSupportedProvider(createProvider({ id: 'anthropic-official', type: 'anthropic' }))).toBe(true) + expect( + isAnthropicSupportedProvider(createProvider({ id: 'custom-host', anthropicApiHost: 'https://anthropic.local' })) + ).toBe(true) + expect(isAnthropicSupportedProvider(createProvider({ id: 'aihubmix' }))).toBe(false) + expect(isAnthropicSupportedProvider(createProvider({ id: 'other' }))).toBe(false) + }) + it('evaluates message array content support', () => { expect(isSupportArrayContentProvider(createProvider())).toBe(true) diff --git a/src/renderer/src/utils/provider.ts b/src/renderer/src/utils/provider.ts index 86544de99..17f3357b9 100644 --- a/src/renderer/src/utils/provider.ts +++ b/src/renderer/src/utils/provider.ts @@ -12,6 +12,14 @@ export const getClaudeSupportedProviders = (providers: Provider[]) => { ) } +export const getAnthropicSupportedProviders = (providers: Provider[]) => { + return providers.filter(isAnthropicSupportedProvider) +} + +export const isAnthropicSupportedProvider = (provider: Provider) => { + return provider.type === 'anthropic' || !!provider.anthropicApiHost +} + const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [ 'deepseek', 'baichuan',