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 { getAvailableTeamProviderModelOptions, getTeamModelUiDisabledReason, normalizeTeamModelForUi, TEAM_MODEL_UI_DISABLED_BADGE_LABEL, } from '@renderer/utils/teamModelAvailability'; import { doesTeamModelCarryProviderBrand, getProviderScopedTeamModelLabel, getTeamModelLabel as getCatalogTeamModelLabel, getTeamProviderLabel as getCatalogTeamProviderLabel, isAnthropicHaikuTeamModel, } from '@renderer/utils/teamModelCatalog'; import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext'; import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults'; import { AlertTriangle, 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 { if (providerId !== 'anthropic') { return selectedModel.trim() || undefined; } const base = extractProviderScopedBaseModel(selectedModel, providerId); if (limitContext) return base || getAnthropicDefaultTeamModel(true); if (isAnthropicHaikuTeamModel(base)) return base; return base ? `${base}[1m]` : getAnthropicDefaultTeamModel(limitContext); } export interface TeamModelSelectorProps { providerId: 'anthropic' | 'codex' | 'gemini'; onProviderChange: (providerId: 'anthropic' | 'codex' | 'gemini') => void; value: string; onValueChange: (value: string) => void; id?: string; disableGeminiOption?: boolean; modelIssueReasonByValue?: Partial>; } export const TeamModelSelector: React.FC = ({ providerId, onProviderChange, value, onValueChange, id, disableGeminiOption = false, modelIssueReasonByValue, }) => { const cliStatus = useStore((s) => s.cliStatus); const cliStatusLoading = useStore((s) => s.cliStatusLoading); 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 'Uses the Claude team default model.\nResolves to Opus 4.7 with 1M context, or Opus 4.7 with 200K context when Limit context is enabled.'; } 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 runtimeProviderStatus = useMemo( () => cliStatus?.providers.find((provider) => provider.providerId === effectiveProviderId) ?? null, [cliStatus?.providers, effectiveProviderId] ); const shouldAwaitRuntimeModelList = effectiveProviderId !== 'anthropic' && (cliStatus == null || cliStatusLoading) && runtimeProviderStatus == null; const normalizedValue = normalizeTeamModelForUi( effectiveProviderId, value, runtimeProviderStatus ); useEffect(() => { if (normalizedValue !== value) { onValueChange(normalizedValue); } }, [normalizedValue, onValueChange, value]); const modelOptions = useMemo(() => { if (shouldAwaitRuntimeModelList) { return [{ value: '', label: 'Default', badgeLabel: 'Default' }]; } return getAvailableTeamProviderModelOptions(effectiveProviderId, runtimeProviderStatus); }, [effectiveProviderId, runtimeProviderStatus, shouldAwaitRuntimeModelList]); 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}
{shouldAwaitRuntimeModelList ? (

Explicit models load from the current runtime. Default remains available while the list is syncing.

) : null}
{modelOptions.map((opt) => (() => { const modelDisabledReason = getTeamModelUiDisabledReason( effectiveProviderId, opt.value, runtimeProviderStatus ); const availabilityStatus = opt.value === '' ? 'available' : (opt.availabilityStatus ?? 'available'); const availabilityReason = opt.value === '' ? null : (opt.availabilityReason ?? null); const modelIssueReason = opt.value === '' ? null : (modelIssueReasonByValue?.[opt.value] ?? null); const hasModelIssue = Boolean(modelIssueReason); const modelSelectable = activeProviderSelectable && !modelDisabledReason && (opt.value === '' || availabilityStatus == null || availabilityStatus === 'available'); const modelStatusMessage = modelIssueReason ?? modelDisabledReason ?? availabilityReason ?? null; return ( ); })() )}
); };