import React, { useEffect, useMemo } from 'react'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { Label } from '@renderer/components/ui/label'; import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@renderer/components/ui/tooltip'; import { cn } from '@renderer/lib/utils'; import { useStore } from '@renderer/store'; import { GEMINI_UI_DISABLED_BADGE_LABEL, GEMINI_UI_DISABLED_REASON, isGeminiUiFrozen, } from '@renderer/utils/geminiUiFreeze'; import { stripTrailingOneMillionSuffixes } from '@renderer/utils/teamModelContext'; import { doesTeamModelCarryProviderBrand, getProviderScopedTeamModelLabel, getTeamModelLabel as getCatalogTeamModelLabel, getTeamModelUiDisabledReason, getTeamProviderLabel as getCatalogTeamProviderLabel, getTeamProviderModelOptions, normalizeTeamModelForUi, TEAM_MODEL_UI_DISABLED_BADGE_LABEL, } from '@renderer/utils/teamModelCatalog'; import { Info } from 'lucide-react'; export { getProviderScopedTeamModelLabel } from '@renderer/utils/teamModelCatalog'; // --- Provider definitions --- interface ProviderDef { id: 'anthropic' | 'codex' | 'gemini' | 'opencode'; label: string; comingSoon: boolean; } const PROVIDERS: ProviderDef[] = [ { id: 'anthropic', label: 'Anthropic', comingSoon: false }, { id: 'codex', label: 'Codex', comingSoon: false }, { id: 'gemini', label: 'Gemini', comingSoon: false }, { id: 'opencode', label: 'OpenCode', comingSoon: false }, ]; const OPENCODE_UI_DISABLED_REASON = 'OpenCode in development'; export function getTeamModelLabel(model: string): string { return getCatalogTeamModelLabel(model) ?? model; } export function getTeamProviderLabel(providerId: 'anthropic' | 'codex' | 'gemini'): string { return getCatalogTeamProviderLabel(providerId) ?? '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 rawModelLabel = model.trim() ? getTeamModelLabel(model.trim()) : 'Default'; const modelLabel = model.trim() ? getProviderScopedTeamModelLabel(providerId, model.trim()) : 'Default'; const effortLabel = effort?.trim() ? getTeamEffortLabel(effort) : ''; const modelAlreadyCarriesProviderBrand = doesTeamModelCarryProviderBrand(providerId, rawModelLabel) || (providerId === 'codex' && model.trim().toLowerCase().startsWith('gpt-')); const providerActsAsBackendOnly = providerId !== 'anthropic' && modelLabel !== 'Default' && !modelAlreadyCarriesProviderBrand; const parts = modelAlreadyCarriesProviderBrand ? [modelLabel, effortLabel] : providerActsAsBackendOnly ? [modelLabel, `via ${providerLabel}`, 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 = stripTrailingOneMillionSuffixes(selectedModel); 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; disableGeminiOption?: boolean; } export const TeamModelSelector: React.FC = ({ providerId, onProviderChange, value, onValueChange, id, disableGeminiOption = false, }) => { const cliStatus = useStore((s) => s.cliStatus); const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true); const multimodelAvailable = multimodelEnabled || cliStatus?.flavor === 'agent_teams_orchestrator'; const effectiveProviderId = disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId; const defaultModelTooltip = useMemo(() => { if (effectiveProviderId === '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.'; }, [effectiveProviderId]); const getProviderDisabledReason = (candidateProviderId: string): string | null => { if (candidateProviderId === 'opencode') { return OPENCODE_UI_DISABLED_REASON; } if (disableGeminiOption && isGeminiUiFrozen() && candidateProviderId === 'gemini') { return GEMINI_UI_DISABLED_REASON; } return null; }; const isProviderTemporarilyDisabled = (candidateProviderId: string): boolean => getProviderDisabledReason(candidateProviderId) !== null; const isProviderSelectable = (candidateProviderId: string): boolean => !isProviderTemporarilyDisabled(candidateProviderId) && (multimodelAvailable || candidateProviderId === 'anthropic'); const activeProviderSelectable = isProviderSelectable(effectiveProviderId); const getProviderStatusBadge = (candidateProviderId: string): string | null => { if (candidateProviderId === 'opencode') { return 'In development'; } const providerDisabledReason = getProviderDisabledReason(candidateProviderId); if (providerDisabledReason) { return GEMINI_UI_DISABLED_BADGE_LABEL; } if (!isProviderSelectable(candidateProviderId)) { return 'Multimodel off'; } return null; }; const getProviderStatusBadgeLabel = (statusBadge: string | null): string | null => { if (statusBadge === 'In development') { return 'Dev'; } if (statusBadge === 'Multimodel off') { return 'Off'; } return statusBadge; }; const runtimeModels = useMemo( () => cliStatus?.providers.find((provider) => provider.providerId === effectiveProviderId) ?.models ?? [], [cliStatus?.providers, effectiveProviderId] ); const normalizedValue = normalizeTeamModelForUi(effectiveProviderId, value); useEffect(() => { if (normalizedValue !== value) { onValueChange(normalizedValue); } }, [normalizedValue, onValueChange, value]); const modelOptions = useMemo(() => { const fallback = getTeamProviderModelOptions(effectiveProviderId); if (effectiveProviderId === 'anthropic' || runtimeModels.length === 0) { return fallback.map((option) => ({ ...option, label: option.value === '' ? option.label : getProviderScopedTeamModelLabel(effectiveProviderId, option.value), })); } const dynamicOptions = runtimeModels.map((model) => ({ value: model, label: getProviderScopedTeamModelLabel(effectiveProviderId, model), })); return [{ value: '', label: 'Default' }, ...dynamicOptions]; }, [effectiveProviderId, runtimeModels]); return (
{ if ( (nextValue === 'anthropic' || nextValue === 'codex' || nextValue === 'gemini') && isProviderSelectable(nextValue) ) { onProviderChange(nextValue); } }} >
{PROVIDERS.map((provider) => { const providerDisabledReason = getProviderDisabledReason(provider.id); const providerSelectable = isProviderSelectable(provider.id); const statusBadge = getProviderStatusBadge(provider.id); const statusBadgeLabel = getProviderStatusBadgeLabel(statusBadge); return ( {provider.label} {statusBadgeLabel ? ( {statusBadgeLabel} ) : null} ); })}
{!multimodelAvailable ? (

Codex and Gemini require Multimodel mode.

) : null}
{modelOptions.map((opt) => (() => { const modelDisabledReason = getTeamModelUiDisabledReason( effectiveProviderId, opt.value ); const modelSelectable = activeProviderSelectable && !modelDisabledReason; return ( ); })() )}
); };