import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Label } from '@renderer/components/ui/label'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@renderer/components/ui/tooltip'; import { cn } from '@renderer/lib/utils'; import { useStore } from '@renderer/store'; import { Check, ChevronDown, Info } from 'lucide-react'; // --- Provider SVG Icons (real brand logos from Simple Icons, monochrome currentColor) --- /** Anthropic โ€” official "A" lettermark (Simple Icons) */ const AnthropicIcon: React.FC<{ className?: string }> = ({ className }) => ( ); /** OpenAI โ€” official hexagonal knot logo (Simple Icons) */ const OpenAIIcon: React.FC<{ className?: string }> = ({ className }) => ( ); const GoogleGeminiIcon: React.FC<{ className?: string }> = ({ className }) => ( ); // --- Provider definitions --- interface ProviderDef { id: string; label: string; icon: React.FC<{ className?: string }>; comingSoon: boolean; } const PROVIDERS: ProviderDef[] = [ { id: 'anthropic', label: 'Anthropic', icon: AnthropicIcon, comingSoon: false }, { id: 'codex', label: 'Codex', icon: OpenAIIcon, comingSoon: false }, { id: 'gemini', label: 'Gemini', icon: GoogleGeminiIcon, comingSoon: false }, ]; const ANTHROPIC_MODEL_OPTIONS = [ { value: '', label: 'Default' }, { value: 'opus', label: 'Opus 4.6' }, { value: 'sonnet', label: 'Sonnet 4.6' }, { value: 'haiku', label: 'Haiku 4.5' }, ] as const; const CODEX_MODEL_OPTIONS = [ { value: '', label: 'Default' }, { value: 'gpt-5.4', label: 'GPT-5.4' }, { value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini' }, { value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' }, { value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' }, { value: 'gpt-5.2', label: 'GPT-5.2' }, { value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' }, { value: 'gpt-5.1-codex-mini', label: 'GPT-5.1 Codex Mini' }, { value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' }, ] as const; const GEMINI_MODEL_OPTIONS = [ { value: '', label: 'Default' }, { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }, { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' }, ] as const; const MODEL_LABEL_OVERRIDES: Record = { 'claude-sonnet-4-6': 'Sonnet 4.6', 'claude-sonnet-4-6[1m]': 'Sonnet 4.6 (1M)', 'claude-opus-4-6': 'Opus 4.6', 'claude-opus-4-6[1m]': 'Opus 4.6 (1M)', 'claude-haiku-4-5-20251001': 'Haiku 4.5', 'gpt-5.4': 'GPT-5.4', 'gpt-5.4-mini': 'GPT-5.4 Mini', 'gpt-5.3-codex': 'GPT-5.3 Codex', 'gpt-5.3-codex-spark': 'GPT-5.3 Spark', 'gpt-5.2-codex': 'GPT-5.2 Codex', 'gpt-5.2': 'GPT-5.2', 'gpt-5.1-codex-mini': 'GPT-5.1 Mini', 'gpt-5.1-codex-max': 'GPT-5.1 Max', 'gemini-2.5-pro': 'Gemini 2.5 Pro', 'gemini-2.5-flash': 'Gemini 2.5 Flash', 'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite', }; export function getTeamModelLabel(model: string): string { return MODEL_LABEL_OVERRIDES[model] ?? model; } export function getTeamProviderLabel(providerId: 'anthropic' | 'codex' | 'gemini'): string { switch (providerId) { case 'codex': return 'Codex'; case 'gemini': return 'Gemini'; case 'anthropic': default: return 'Anthropic'; } } export function getTeamEffortLabel(effort: string): string { const trimmed = effort.trim(); if (!trimmed) return 'Default'; return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); } export function formatTeamModelSummary( providerId: 'anthropic' | 'codex' | 'gemini', model: string, effort?: string ): string { const providerLabel = getTeamProviderLabel(providerId); const modelLabel = model.trim() ? getTeamModelLabel(model.trim()) : 'Default'; const effortLabel = effort?.trim() ? getTeamEffortLabel(effort) : ''; const normalizedProvider = providerLabel.trim().toLowerCase(); const normalizedModel = modelLabel.trim().toLowerCase(); const modelAlreadyCarriesProviderBrand = modelLabel !== 'Default' && (normalizedModel.startsWith(normalizedProvider) || (providerId === 'anthropic' && normalizedModel.startsWith('claude')) || (providerId === 'codex' && normalizedModel.startsWith('codex')) || (providerId === 'codex' && normalizedModel.startsWith('gpt')) || (providerId === 'gemini' && normalizedModel.startsWith('gemini'))); const parts = modelAlreadyCarriesProviderBrand ? [modelLabel, effortLabel] : [providerLabel, modelLabel, effortLabel]; return parts.filter(Boolean).join(' ยท '); } /** * Computes the effective model string for team provisioning. * By default adds [1m] suffix for 1M context (Opus/Sonnet). * When limitContext=true, returns base model without [1m] (200K context). * Haiku does not support 1M โ€” always returned as-is. */ export function computeEffectiveTeamModel( selectedModel: string, limitContext: boolean, providerId: 'anthropic' | 'codex' | 'gemini' = 'anthropic' ): string | undefined { const base = selectedModel || undefined; if (providerId !== 'anthropic') return base; if (limitContext) return base; if (base === 'haiku') return base; return base ? `${base}[1m]` : 'opus[1m]'; } export interface TeamModelSelectorProps { providerId: 'anthropic' | 'codex' | 'gemini'; onProviderChange: (providerId: 'anthropic' | 'codex' | 'gemini') => void; value: string; onValueChange: (value: string) => void; id?: string; } export const TeamModelSelector: React.FC = ({ providerId, onProviderChange, value, onValueChange, id, }) => { const cliStatus = useStore((s) => s.cliStatus); const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true); const multimodelAvailable = multimodelEnabled || cliStatus?.flavor === 'free-code'; const [dropdownOpen, setDropdownOpen] = useState(false); const containerRef = useRef(null); // Close dropdown on click outside useEffect(() => { if (!dropdownOpen) return; const handleClickOutside = (event: MouseEvent): void => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setDropdownOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [dropdownOpen]); const activeProvider = PROVIDERS.find((provider) => provider.id === providerId) ?? PROVIDERS[0]; const ProviderIcon = activeProvider.icon; const defaultModelTooltip = useMemo(() => { if (providerId === 'anthropic') { return 'Default model from Claude CLI (/model).\nUses the runtime default for the selected provider.'; } return 'Uses the runtime default for the selected provider.'; }, [providerId]); const isProviderSelectable = (candidateProviderId: string): boolean => multimodelAvailable || candidateProviderId === 'anthropic'; const activeProviderSelectable = isProviderSelectable(providerId); const runtimeModels = cliStatus?.providers.find((provider) => provider.providerId === providerId)?.models ?? []; const modelOptions = useMemo(() => { const fallback = providerId === 'codex' ? CODEX_MODEL_OPTIONS : providerId === 'gemini' ? GEMINI_MODEL_OPTIONS : ANTHROPIC_MODEL_OPTIONS; if (providerId === 'anthropic' || runtimeModels.length === 0) { return [...fallback]; } const dynamicOptions = runtimeModels.map((model) => ({ value: model, label: getTeamModelLabel(model), })); return [{ value: '', label: 'Default' }, ...dynamicOptions]; }, [providerId, runtimeModels]); return (
{/* Provider dropdown */} {dropdownOpen && (
{PROVIDERS.map((provider, index) => { const Icon = provider.icon; const isActive = provider.id === activeProvider.id; const isFirst = index === 0; const prevWasActive = index > 0 && !PROVIDERS[index - 1].comingSoon; return ( {prevWasActive && !isFirst && (
)} ); })}
)}
{!multimodelAvailable && (

Codex and Gemini require Multimodel mode.

)}
{modelOptions.map((opt) => ( ))}
); };