import React, { useEffect, useMemo, useState } from 'react'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { Checkbox } from '@renderer/components/ui/checkbox'; 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 { useEffectiveCliProviderStatus } from '@renderer/hooks/useEffectiveCliProviderStatus'; 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 { compareOpenCodeTeamModelRecommendations, getOpenCodeTeamModelRecommendation, isOpenCodeTeamModelRecommended, } from '@renderer/utils/openCodeModelRecommendations'; import { getAvailableTeamProviderModelOptions, getTeamModelUiDisabledReason, isTeamProviderModelVerificationPending, normalizeTeamModelForUi, TEAM_MODEL_UI_DISABLED_BADGE_LABEL, } from '@renderer/utils/teamModelAvailability'; import { doesTeamModelCarryProviderBrand, getProviderScopedTeamModelLabel, getRuntimeAwareProviderScopedTeamModelLabel, getTeamModelLabel as getCatalogTeamModelLabel, getTeamModelSourceBadgeLabel, getTeamProviderLabel as getCatalogTeamProviderLabel, isAnthropicHaikuTeamModel, } from '@renderer/utils/teamModelCatalog'; import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext'; import { resolveAnthropicLaunchModel } from '@shared/utils/anthropicLaunchModel'; import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults'; import { isTeamProviderId } from '@shared/utils/teamProvider'; import { AlertTriangle, Info, Star } from 'lucide-react'; import type { CliProviderStatus, TeamProviderId } from '@shared/types'; export { getProviderScopedTeamModelLabel } from '@renderer/utils/teamModelCatalog'; // --- Provider definitions --- interface ProviderDef { id: TeamProviderId; 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 team launch is not ready.'; export const OPENCODE_TEAM_LEAD_DISABLED_REASON = 'OpenCode is teammate-only in this phase. Use Anthropic, Codex, or Gemini as the team lead, then add OpenCode as a teammate.'; export const OPENCODE_TEAM_LEAD_DISABLED_BADGE_LABEL = 'side lane'; export function getTeamModelLabel(model: string): string { return getCatalogTeamModelLabel(model) ?? model; } export function getTeamProviderLabel(providerId: TeamProviderId): string { return getCatalogTeamProviderLabel(providerId) ?? 'Anthropic'; } export function getTeamEffortLabel(effort: string): string { const trimmed = effort.trim(); if (!trimmed) return 'Default'; if (trimmed === 'xhigh') return 'XHigh'; return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); } export function formatTeamModelSummary( providerId: TeamProviderId, model: string, effort?: string ): string { const providerLabel = getTeamProviderLabel(providerId); const routeLabel = providerId === 'opencode' ? (getTeamModelSourceBadgeLabel(providerId, model.trim()) ?? providerLabel) : providerLabel; 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 ${routeLabel}`, 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: TeamProviderId = 'anthropic', providerStatus?: Pick | null ): string | undefined { if (providerId !== 'anthropic') { return selectedModel.trim() || undefined; } const catalog = providerStatus?.providerId === 'anthropic' ? (providerStatus.modelCatalog ?? null) : null; return ( resolveAnthropicLaunchModel({ selectedModel, limitContext, availableLaunchModels: catalog?.models.map((model) => model.launchModel), defaultLaunchModel: catalog?.defaultLaunchModel ?? null, }) ?? getAnthropicDefaultTeamModel(limitContext) ); } export interface TeamModelSelectorProps { providerId: TeamProviderId; onProviderChange: (providerId: TeamProviderId) => void; value: string; onValueChange: (value: string) => void; id?: string; disableGeminiOption?: boolean; providerDisabledReasonById?: Partial>; providerDisabledBadgeLabelById?: Partial>; modelIssueReasonByValue?: Partial>; } export const TeamModelSelector: React.FC = ({ providerId, onProviderChange, value, onValueChange, id, disableGeminiOption = false, providerDisabledReasonById, providerDisabledBadgeLabelById, modelIssueReasonByValue, }) => { const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true); const [recommendedOnly, setRecommendedOnly] = useState(false); const effectiveProviderId = disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId; const { cliStatus: effectiveCliStatus, providerStatus: runtimeProviderStatus, loading: effectiveCliStatusLoading, } = useEffectiveCliProviderStatus(effectiveProviderId); const multimodelAvailable = multimodelEnabled || effectiveCliStatus?.flavor === 'agent_teams_orchestrator'; const runtimeProviderStatusById = useMemo( () => new Map( (effectiveCliStatus?.providers ?? []).map((provider) => [provider.providerId, provider]) ), [effectiveCliStatus?.providers] ); const defaultModelTooltip = useMemo(() => { if (effectiveProviderId === 'anthropic') { const defaultLongContextModel = getRuntimeAwareProviderScopedTeamModelLabel( 'anthropic', getAnthropicDefaultTeamModel(false), runtimeProviderStatus ) ?? 'Opus 4.7 (1M)'; const defaultLimitedContextModel = getRuntimeAwareProviderScopedTeamModelLabel( 'anthropic', getAnthropicDefaultTeamModel(true), runtimeProviderStatus ) ?? 'Opus 4.7'; return `Uses the Claude team default model.\nResolves to ${defaultLongContextModel} with 1M context, or ${defaultLimitedContextModel} with 200K context when Limit context is enabled.`; } return 'Uses the runtime default for the selected provider.'; }, [effectiveProviderId, runtimeProviderStatus]); const getProviderDisabledReason = (candidateProviderId: string): string | null => { if (isTeamProviderId(candidateProviderId)) { const overrideReason = providerDisabledReasonById?.[candidateProviderId]?.trim(); if (overrideReason) { return overrideReason; } } if (candidateProviderId === 'opencode') { const providerStatus = runtimeProviderStatusById.get('opencode') ?? null; if (!providerStatus) { return 'OpenCode runtime status is still loading.'; } if (!providerStatus.supported) { return ( providerStatus.detailMessage ?? providerStatus.statusMessage ?? 'OpenCode CLI is not installed.' ); } if (!providerStatus.authenticated) { return ( providerStatus.detailMessage ?? providerStatus.statusMessage ?? 'OpenCode has no connected provider.' ); } if (!providerStatus.capabilities.teamLaunch) { return ( providerStatus.detailMessage ?? providerStatus.statusMessage ?? OPENCODE_UI_DISABLED_REASON ); } return null; } 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 (isTeamProviderId(candidateProviderId)) { const overrideReason = providerDisabledReasonById?.[candidateProviderId]?.trim(); const overrideBadge = providerDisabledBadgeLabelById?.[candidateProviderId]?.trim(); if (overrideReason && overrideBadge) { return overrideBadge; } } if (candidateProviderId === 'opencode') { return getProviderDisabledReason(candidateProviderId) ? 'Gated' : null; } 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 === 'Gated') { return 'Gate'; } if (statusBadge === 'Multimodel off') { return 'Off'; } return statusBadge; }; const shouldAwaitRuntimeModelList = effectiveProviderId !== 'anthropic' && (runtimeProviderStatus == null || isTeamProviderModelVerificationPending(effectiveProviderId, runtimeProviderStatus)); 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]); const hasRecommendedOpenCodeModels = useMemo( () => effectiveProviderId === 'opencode' && modelOptions.some((option) => isOpenCodeTeamModelRecommended(option.value)), [effectiveProviderId, modelOptions] ); useEffect(() => { if (effectiveProviderId !== 'opencode' || !hasRecommendedOpenCodeModels) { setRecommendedOnly(false); } }, [effectiveProviderId, hasRecommendedOpenCodeModels]); const visibleModelOptions = useMemo(() => { if (effectiveProviderId !== 'opencode') { return modelOptions; } const concreteOptions = modelOptions .filter((option) => option.value.trim().length > 0) .map((option, index) => ({ option, index })) .filter(({ option }) => !recommendedOnly || isOpenCodeTeamModelRecommended(option.value)) .sort((left, right) => { const recommendationOrder = compareOpenCodeTeamModelRecommendations( left.option.value, right.option.value ); return recommendationOrder || left.index - right.index; }) .map(({ option }) => option); if (recommendedOnly) { return concreteOptions; } return [ ...modelOptions.filter((option) => option.value.trim().length === 0), ...concreteOptions, ]; }, [effectiveProviderId, modelOptions, recommendedOnly]); const shouldConstrainModelListHeight = visibleModelOptions.length > 8; return (
{ if (isTeamProviderId(nextValue) && 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} {hasRecommendedOpenCodeModels ? (
setRecommendedOnly(checked === true)} className="size-3.5" />
) : null}
{visibleModelOptions.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; const sourceBadgeLabel = effectiveProviderId === 'opencode' && opt.value !== '' ? opt.badgeLabel?.trim() || null : null; const modelRecommendation = effectiveProviderId === 'opencode' ? getOpenCodeTeamModelRecommendation(opt.value) : null; return ( ); })() )}
{effectiveProviderId === 'opencode' && visibleModelOptions.length === 0 ? (
No recommended OpenCode models are available in the current runtime list.
) : null}
); };