diff --git a/tests/unit/cliproxy/backend-ui-default-ports-sync.test.ts b/tests/unit/cliproxy/backend-ui-default-ports-sync.test.ts
index c8fe0d1d..5670eecf 100644
--- a/tests/unit/cliproxy/backend-ui-default-ports-sync.test.ts
+++ b/tests/unit/cliproxy/backend-ui-default-ports-sync.test.ts
@@ -19,9 +19,12 @@ import {
} from '../../../ui/src/lib/default-ports';
import {
CLIPROXY_PROVIDERS as UI_CLIPROXY_PROVIDERS,
+ CORE_CLIPROXY_PROVIDERS as UI_CORE_CLIPROXY_PROVIDERS,
DEVICE_CODE_PROVIDERS as UI_DEVICE_CODE_PROVIDERS,
+ PLUS_EXTRA_CLIPROXY_PROVIDERS as UI_PLUS_EXTRA_CLIPROXY_PROVIDERS,
PROVIDER_METADATA as UI_PROVIDER_METADATA,
} from '../../../ui/src/lib/provider-config';
+import { PLUS_ONLY_PROVIDERS as BACKEND_PLUS_ONLY_PROVIDERS } from '../../../src/cliproxy/types';
function sorted(values: readonly string[]): string[] {
return [...values].sort((a, b) => a.localeCompare(b));
@@ -44,6 +47,17 @@ describe('Default Port Sync', () => {
expect(sorted(UI_DEVICE_CODE_PROVIDERS)).toEqual(sorted(getProvidersByOAuthFlow('device_code')));
});
+ test('plus-extra providers are synced between backend and UI', () => {
+ expect(sorted(UI_PLUS_EXTRA_CLIPROXY_PROVIDERS)).toEqual(sorted(BACKEND_PLUS_ONLY_PROVIDERS));
+ expect(sorted(UI_CORE_CLIPROXY_PROVIDERS)).toEqual(
+ sorted(
+ BACKEND_CLIPROXY_PROVIDER_IDS.filter(
+ (provider) => !BACKEND_PLUS_ONLY_PROVIDERS.includes(provider)
+ )
+ )
+ );
+ });
+
test('Provider display names are synced between backend and UI', () => {
for (const provider of BACKEND_CLIPROXY_PROVIDER_IDS) {
expect(UI_PROVIDER_METADATA[provider].displayName).toBe(getBackendProviderDisplayName(provider));
diff --git a/ui/src/components/cliproxy/cliproxy-dialog.tsx b/ui/src/components/cliproxy/cliproxy-dialog.tsx
index b221b8a6..3256f236 100644
--- a/ui/src/components/cliproxy/cliproxy-dialog.tsx
+++ b/ui/src/components/cliproxy/cliproxy-dialog.tsx
@@ -19,7 +19,13 @@ import { useTranslation } from 'react-i18next';
import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy';
import { usePrivacy } from '@/contexts/privacy-context';
import { formatAccountDisplayName } from '@/lib/account-identity';
-import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config';
+import {
+ CLIPROXY_PROVIDERS,
+ CLIPROXY_PROVIDER_SECTIONS,
+ getProviderDisplayName,
+ getProviderSection,
+ isPlusExtraProvider,
+} from '@/lib/provider-config';
import { isDeniedAgyModelId } from '@/lib/utils';
const singleProviderSchema = z.object({
@@ -104,6 +110,8 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
});
const selectedProvider = useWatch({ control: singleForm.control, name: 'provider' });
+ const compositeTiers = useWatch({ control: compositeForm.control, name: 'tiers' });
+ const selectedProviderSection = getProviderSection(selectedProvider);
const providerAuth = authData?.authStatus.find((s) => s.provider === selectedProvider);
const providerAccounts = providerAuth?.accounts || [];
@@ -197,10 +205,16 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
- {providerOptions.map((opt) => (
-
+ {CLIPROXY_PROVIDER_SECTIONS.map((section) => (
+
))}
{singleForm.formState.errors.provider && (
@@ -208,6 +222,14 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
{singleForm.formState.errors.provider.message}
)}
+ {selectedProviderSection && (
+
+ {selectedProviderSection.hint}
+ {isPlusExtraProvider(selectedProvider)
+ ? ' Requires the optional Plus backend while that track remains community-maintained.'
+ : ''}
+
+ )}
{selectedProvider && providerAccounts.length > 0 && (
@@ -297,12 +319,26 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
{...compositeForm.register(`tiers.${tier}.provider`)}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
- {providerOptions.map((opt) => (
-
+ {CLIPROXY_PROVIDER_SECTIONS.map((section) => (
+
))}
+ {compositeTiers?.[tier]?.provider && (
+
+ {getProviderSection(compositeTiers[tier].provider)?.hint}
+ {isPlusExtraProvider(compositeTiers[tier].provider)
+ ? ' Requires the optional Plus backend while that track remains community-maintained.'
+ : ''}
+
+ )}
diff --git a/ui/src/components/cliproxy/cliproxy-edit-dialog.tsx b/ui/src/components/cliproxy/cliproxy-edit-dialog.tsx
index 9274e975..16825e7f 100644
--- a/ui/src/components/cliproxy/cliproxy-edit-dialog.tsx
+++ b/ui/src/components/cliproxy/cliproxy-edit-dialog.tsx
@@ -3,7 +3,7 @@
* Phase 05: Dashboard UI full CRUD for composite variants
*/
-import { useForm } from 'react-hook-form';
+import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useEffect } from 'react';
@@ -15,7 +15,13 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { useUpdateVariant } from '@/hooks/use-cliproxy';
-import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config';
+import {
+ CLIPROXY_PROVIDERS,
+ CLIPROXY_PROVIDER_SECTIONS,
+ getProviderDisplayName,
+ getProviderSection,
+ isPlusExtraProvider,
+} from '@/lib/provider-config';
import type { UpdateVariant, Variant } from '@/lib/api-client';
import { isDeniedAgyModelId } from '@/lib/utils';
@@ -137,6 +143,8 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
const compositeForm = useForm
({
resolver: zodResolver(compositeSchema),
});
+ const selectedProvider = useWatch({ control: singleForm.control, name: 'provider' });
+ const compositeTiers = useWatch({ control: compositeForm.control, name: 'tiers' });
// Pre-populate form when variant changes
useEffect(() => {
@@ -318,12 +326,26 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
{...compositeForm.register(`tiers.${tier}.provider`)}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
- {providerOptions.map((opt) => (
-
+ {CLIPROXY_PROVIDER_SECTIONS.map((section) => (
+
))}
+ {compositeTiers?.[tier]?.provider && (
+
+ {getProviderSection(compositeTiers[tier].provider)?.hint}
+ {isPlusExtraProvider(compositeTiers[tier].provider)
+ ? ' Requires the optional Plus backend while that track remains community-maintained.'
+ : ''}
+
+ )}
@@ -399,12 +421,26 @@ export function CliproxyEditDialog({ variant, open, onOpenChange }: CliproxyEdit
{...singleForm.register('provider')}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
- {providerOptions.map((opt) => (
-
+ {CLIPROXY_PROVIDER_SECTIONS.map((section) => (
+
))}
+ {selectedProvider && (
+
+ {getProviderSection(selectedProvider)?.hint}
+ {isPlusExtraProvider(selectedProvider)
+ ? ' Requires the optional Plus backend while that track remains community-maintained.'
+ : ''}
+
+ )}
diff --git a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx
index 2ea26cdf..22edadaa 100644
--- a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx
+++ b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx
@@ -10,6 +10,7 @@ import { Info, Shield } from 'lucide-react';
import { UsageCommand } from './usage-command';
import type { SettingsResponse } from './types';
import type { AuthStatus, CliTarget } from '@/lib/api-client';
+import { getProviderSection, isPlusExtraProvider } from '@/lib/provider-config';
import { useTranslation } from 'react-i18next';
interface ProviderInfoTabProps {
@@ -33,6 +34,7 @@ export function ProviderInfoTab({
const resolvedTarget = defaultTarget || 'claude';
const isDroidTarget = resolvedTarget === 'droid';
const isCodexProvider = provider === 'codex';
+ const providerSection = getProviderSection(provider);
const managementPrefix =
resolvedTarget === 'claude' ? `ccs ${provider}` : `ccs ${provider} --target claude`;
const changeModelCommand = `${managementPrefix} --config`;
@@ -101,6 +103,20 @@ export function ProviderInfoTab({
{resolvedTarget}
+ {providerSection && (
+
+
Track
+
+
{providerSection.label}
+
+ {providerSection.hint}
+ {isPlusExtraProvider(provider)
+ ? ' Requires the optional Plus backend while that track remains community-maintained.'
+ : ''}
+
+
+
+ )}
diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts
index 3500e4a4..7f824ce0 100644
--- a/ui/src/lib/provider-config.ts
+++ b/ui/src/lib/provider-config.ts
@@ -10,6 +10,7 @@ import {
getProvidersByOAuthFlow,
} from '../../../src/cliproxy/provider-capabilities';
import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/cliproxy/ai-providers';
+import { PLUS_ONLY_PROVIDERS } from '../../../src/cliproxy/types';
import i18n from './i18n';
// Monorepo contract: UI consumes provider capability constants directly from backend
@@ -19,6 +20,39 @@ import i18n from './i18n';
export const CLIPROXY_PROVIDERS = CLIPROXY_PROVIDER_IDS;
export type CLIProxyProvider = (typeof CLIPROXY_PROVIDERS)[number];
export type ProviderVisualId = CLIProxyProvider | 'openai' | 'vertex';
+export type CLIProxyProviderSectionId = 'core' | 'plus-extra';
+
+export interface CLIProxyProviderSection {
+ id: CLIProxyProviderSectionId;
+ label: string;
+ hint: string;
+ providers: readonly CLIProxyProvider[];
+}
+
+const PLUS_ONLY_PROVIDER_SET = new Set(PLUS_ONLY_PROVIDERS);
+
+export const CORE_CLIPROXY_PROVIDERS: readonly CLIProxyProvider[] = Object.freeze(
+ CLIPROXY_PROVIDERS.filter((provider) => !PLUS_ONLY_PROVIDER_SET.has(provider))
+);
+
+export const PLUS_EXTRA_CLIPROXY_PROVIDERS: readonly CLIProxyProvider[] = Object.freeze(
+ CLIPROXY_PROVIDERS.filter((provider) => PLUS_ONLY_PROVIDER_SET.has(provider))
+);
+
+export const CLIPROXY_PROVIDER_SECTIONS: readonly CLIProxyProviderSection[] = Object.freeze([
+ {
+ id: 'core',
+ label: 'Core / original backend',
+ hint: 'Default, always-available provider track',
+ providers: CORE_CLIPROXY_PROVIDERS,
+ },
+ {
+ id: 'plus-extra',
+ label: 'Plus extras / community-maintained',
+ hint: 'Still supported, but separated from the default backend for now',
+ providers: PLUS_EXTRA_CLIPROXY_PROVIDERS,
+ },
+]);
/** Check if a string is a backend-supported CLIProxy provider. */
export function isValidProvider(provider: string): provider is CLIProxyProvider {
@@ -259,6 +293,35 @@ export function getProviderDisplayName(provider: unknown): string {
return PROVIDER_NAMES[normalized] || i18n.t('toasts.providerUnknown', { provider: normalized });
}
+export function isPlusExtraProvider(provider: unknown): boolean {
+ const normalized = normalizeProviderInput(provider);
+ return isValidProvider(normalized) && PLUS_ONLY_PROVIDER_SET.has(normalized);
+}
+
+export function getProviderSection(provider: unknown): CLIProxyProviderSection | null {
+ const normalized = normalizeProviderInput(provider);
+ if (!isValidProvider(normalized)) {
+ return null;
+ }
+
+ return (
+ CLIPROXY_PROVIDER_SECTIONS.find((section) => section.providers.includes(normalized)) || null
+ );
+}
+
+export function groupProvidersBySection(
+ items: readonly T[],
+ getProvider: (item: T) => unknown
+): Array {
+ return CLIPROXY_PROVIDER_SECTIONS.map((section) => ({
+ ...section,
+ items: items.filter((item) => {
+ const normalized = normalizeProviderInput(getProvider(item));
+ return isValidProvider(normalized) && section.providers.includes(normalized);
+ }),
+ })).filter((section) => section.items.length > 0);
+}
+
/** Map provider to user-facing short description */
export function getProviderDescription(provider: unknown): string {
const normalized = normalizeProviderInput(provider);
diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx
index 6aba8344..f48374e2 100644
--- a/ui/src/pages/cliproxy.tsx
+++ b/ui/src/pages/cliproxy.tsx
@@ -33,7 +33,11 @@ import {
} from '@/hooks/use-cliproxy';
import type { AuthStatus, Variant } from '@/lib/api-client';
import { buildUiCatalogs } from '@/lib/model-catalogs';
-import { getProviderDisplayName, isValidProvider } from '@/lib/provider-config';
+import {
+ getProviderDisplayName,
+ groupProvidersBySection,
+ isValidProvider,
+} from '@/lib/provider-config';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
@@ -261,6 +265,10 @@ export function CliproxyPage() {
});
const providers = useMemo(() => authData?.authStatus || [], [authData?.authStatus]);
+ const providerSections = useMemo(
+ () => groupProvidersBySection(providers, (status) => status.provider),
+ [providers]
+ );
const isRemoteMode = authData?.source === 'remote';
const variants = useMemo(() => variantsData?.variants || [], [variantsData?.variants]);
const catalogs = useMemo(() => buildUiCatalogs(catalogData?.catalogs), [catalogData?.catalogs]);
@@ -393,14 +401,28 @@ export function CliproxyPage() {
))}
) : (
-
- {providers.map((status) => (
-
handleSelectProvider(status.provider)}
- />
+
+ {providerSections.map((section) => (
+
+
+
+ {section.label}
+
+
+ {section.hint}
+
+
+
+ {section.items.map((status) => (
+
handleSelectProvider(status.provider)}
+ />
+ ))}
+
+
))}
)}
diff --git a/ui/src/pages/settings/sections/proxy/index.tsx b/ui/src/pages/settings/sections/proxy/index.tsx
index 76ec80e1..f5b237ae 100644
--- a/ui/src/pages/settings/sections/proxy/index.tsx
+++ b/ui/src/pages/settings/sections/proxy/index.tsx
@@ -29,15 +29,18 @@ import { ProxyStatusWidget } from '@/components/monitoring/proxy-status-widget';
import { api } from '@/lib/api-client';
import { CLIPROXY_DEFAULT_PORT } from '@/lib/preset-utils';
import { RISK_ACK_PHRASE } from '@/components/account/antigravity-responsibility-constants';
+import {
+ CORE_CLIPROXY_PROVIDERS,
+ PLUS_EXTRA_CLIPROXY_PROVIDERS,
+ getProviderDisplayName,
+ isPlusExtraProvider,
+} from '@/lib/provider-config';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
/** LocalStorage key for debug mode preference */
const DEBUG_MODE_KEY = 'ccs_debug_mode';
-/** Providers only available on CLIProxyAPIPlus */
-const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp', 'cursor', 'gitlab', 'codebuddy', 'kilo'];
-
function normalizeRiskAckPhrase(value: string): string {
return value.trim().replace(/\s+/g, ' ').toUpperCase();
}
@@ -196,6 +199,8 @@ export default function ProxySection() {
const updateBackendMutation = useUpdateBackend();
const { data: proxyStatus } = useProxyStatus();
const isProxyRunning = proxyStatus?.running ?? false;
+ const coreProviderNames = CORE_CLIPROXY_PROVIDERS.map(getProviderDisplayName).join(', ');
+ const plusProviderNames = PLUS_EXTRA_CLIPROXY_PROVIDERS.map(getProviderDisplayName).join(', ');
// Fetch backend setting
const fetchBackend = useCallback(async () => {
@@ -211,7 +216,7 @@ export default function ProxySection() {
const checkPlusOnlyVariants = useCallback(async () => {
try {
const result = await api.cliproxy.list();
- const hasIncompatible = result.variants.some((v) => PLUS_ONLY_PROVIDERS.includes(v.provider));
+ const hasIncompatible = result.variants.some((v) => isPlusExtraProvider(v.provider));
setHasKiroGhcpVariants(hasIncompatible);
} catch (err) {
console.error('[Proxy] Failed to check variants:', err);
@@ -491,7 +496,13 @@ export default function ProxySection() {
{t('settingsProxy.backendPlusApi')}
- {t('settingsProxy.plusDesc')}
+
+ Optional track for extra providers. Still supported, but currently
+ community-maintained instead of upstream-maintained.
+
+
+ {plusProviderNames}
+
{/* Original Backend Card */}
@@ -510,15 +521,20 @@ export default function ProxySection() {
{t('settingsProxy.default')}
- {t('settingsProxy.originalDesc')}
+
+ Default, always-available backend for the core provider track.
+
+
+ {coreProviderNames}
+
{backend === 'plus' && (
- CLIProxyAPIPlus upstream is currently unavailable. Local CLIProxy will use the
- original backend until issue #1062 is resolved.
+ The Plus provider track is not deprecated, but local CLIProxy still falls back to
+ the original backend while the maintained fork path is being brought back.
)}
@@ -526,7 +542,10 @@ export default function ProxySection() {
{backend === 'original' && hasKiroGhcpVariants && (
- {t('settingsProxy.variantsIncompatible')}
+
+ Existing plus-extra variants ({plusProviderNames}) will not run on the original
+ backend. Keep them visible for reference, but switch to Plus before using them.
+
)}
diff --git a/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx b/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx
index 9d71278d..0606a688 100644
--- a/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx
+++ b/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx
@@ -51,4 +51,28 @@ describe('ProviderInfoTab', () => {
expect(screen.queryByText('Change model')).not.toBeInTheDocument();
expect(screen.getByText('ccs custom-provider --auth --add')).toBeInTheDocument();
});
+
+ it('shows the plus-extra track note for community-maintained providers', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Track')).toBeInTheDocument();
+ expect(screen.getByText('Plus extras / community-maintained')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ /Requires the optional Plus backend while that track remains community-maintained\./
+ )
+ ).toBeInTheDocument();
+ });
});
diff --git a/ui/tests/unit/pages/cliproxy-page.test.tsx b/ui/tests/unit/pages/cliproxy-page.test.tsx
index bd651bfb..92b57035 100644
--- a/ui/tests/unit/pages/cliproxy-page.test.tsx
+++ b/ui/tests/unit/pages/cliproxy-page.test.tsx
@@ -2,6 +2,23 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent } from '@tests/setup/test-utils';
const hookState = vi.hoisted(() => ({
+ authData: {
+ authStatus: [
+ {
+ provider: 'gemini',
+ displayName: 'Gemini',
+ authenticated: true,
+ accounts: [{ id: 'acct-1', provider: 'gemini' }],
+ },
+ {
+ provider: 'ghcp',
+ displayName: 'GitHub Copilot (OAuth)',
+ authenticated: false,
+ accounts: [],
+ },
+ ],
+ source: 'local' as const,
+ },
catalogData: undefined as
| {
catalogs: Record<
@@ -23,17 +40,7 @@ vi.mock('@/hooks/use-cliproxy', () => ({
isFetching: false,
}),
useCliproxyAuth: () => ({
- data: {
- authStatus: [
- {
- provider: 'gemini',
- displayName: 'Gemini',
- authenticated: true,
- accounts: [{ id: 'acct-1', provider: 'gemini' }],
- },
- ],
- source: 'local',
- },
+ data: hookState.authData,
isLoading: false,
}),
useCliproxyCatalog: () => ({
@@ -85,9 +92,39 @@ import { CliproxyPage } from '@/pages/cliproxy';
describe('CliproxyPage add-account catalog gating', () => {
beforeEach(() => {
+ hookState.authData = {
+ authStatus: [
+ {
+ provider: 'gemini',
+ displayName: 'Gemini',
+ authenticated: true,
+ accounts: [{ id: 'acct-1', provider: 'gemini' }],
+ },
+ {
+ provider: 'ghcp',
+ displayName: 'GitHub Copilot (OAuth)',
+ authenticated: false,
+ accounts: [],
+ },
+ ],
+ source: 'local',
+ };
hookState.catalogData = undefined;
});
+ it('separates core providers from plus extras in the sidebar', () => {
+ render();
+
+ expect(screen.getByText('Core / original backend')).toBeInTheDocument();
+ expect(screen.getByText('Plus extras / community-maintained')).toBeInTheDocument();
+ expect(screen.getByText('Default, always-available provider track')).toBeInTheDocument();
+ expect(
+ screen.getByText('Still supported, but separated from the default backend for now')
+ ).toBeInTheDocument();
+ expect(screen.getByText('Gemini')).toBeInTheDocument();
+ expect(screen.getByText('GitHub Copilot (OAuth)')).toBeInTheDocument();
+ });
+
it('does not pass a static fallback catalog before the catalog query resolves', async () => {
render();
diff --git a/ui/tests/unit/ui/lib/provider-config.test.ts b/ui/tests/unit/ui/lib/provider-config.test.ts
index b33f24b1..89905299 100644
--- a/ui/tests/unit/ui/lib/provider-config.test.ts
+++ b/ui/tests/unit/ui/lib/provider-config.test.ts
@@ -1,14 +1,20 @@
import { describe, expect, it } from 'vitest';
import {
+ CLIPROXY_PROVIDER_SECTIONS,
+ CORE_CLIPROXY_PROVIDERS,
formatRequestedUpstreamModelRules,
getProviderDescription,
getProviderDisplayName,
getProviderFallbackVisual,
getProviderLogoAsset,
+ getProviderSection,
getRequestedUpstreamModelRuleErrors,
getRequestedModelId,
+ groupProvidersBySection,
+ isPlusExtraProvider,
parseRequestedUpstreamModelRules,
+ PLUS_EXTRA_CLIPROXY_PROVIDERS,
PROVIDER_COLORS,
} from '@/lib/provider-config';
@@ -49,6 +55,40 @@ describe('provider model mapping helpers', () => {
});
describe('provider presentation metadata', () => {
+ it('splits providers into core and plus-extra sections', () => {
+ expect(CLIPROXY_PROVIDER_SECTIONS.map((section) => section.id)).toEqual(['core', 'plus-extra']);
+ expect(CORE_CLIPROXY_PROVIDERS).toContain('gemini');
+ expect(CORE_CLIPROXY_PROVIDERS).toContain('kimi');
+ expect(PLUS_EXTRA_CLIPROXY_PROVIDERS).toEqual([
+ 'kiro',
+ 'ghcp',
+ 'cursor',
+ 'gitlab',
+ 'codebuddy',
+ 'kilo',
+ ]);
+ expect(getProviderSection('gitlab')?.id).toBe('plus-extra');
+ expect(getProviderSection('gemini')?.id).toBe('core');
+ expect(isPlusExtraProvider('cursor')).toBe(true);
+ expect(isPlusExtraProvider('gemini')).toBe(false);
+ });
+
+ it('groups provider-backed data by shared section metadata', () => {
+ const grouped = groupProvidersBySection(
+ [
+ { provider: 'cursor', value: 'plus' },
+ { provider: 'gemini', value: 'core' },
+ ],
+ (entry) => entry.provider
+ );
+
+ expect(grouped).toHaveLength(2);
+ expect(grouped[0]?.id).toBe('core');
+ expect(grouped[0]?.items.map((entry) => entry.provider)).toEqual(['gemini']);
+ expect(grouped[1]?.id).toBe('plus-extra');
+ expect(grouped[1]?.items.map((entry) => entry.provider)).toEqual(['cursor']);
+ });
+
it.each([
['cursor', 'Cursor', 'Cursor browser-authenticated provider', '/assets/sidebar/cursor.svg'],
['gitlab', 'GitLab Duo', 'GitLab Duo with OAuth or PAT auth', '/assets/providers/gitlab.svg'],