import React, { useEffect, useMemo, useState } from 'react'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { Input } from '@renderer/components/ui/input'; 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 { getAvailableTeamProviderModelOptions, getOpenCodeOpenAiRouteAuthUnavailableReason, 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 { compareTeamModelRecommendations, getTeamModelRecommendation, isTeamModelRecommended, } from '@renderer/utils/teamModelRecommendations'; import { resolveAnthropicLaunchModel } from '@shared/utils/anthropicLaunchModel'; import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults'; import { isTeamProviderId } from '@shared/utils/teamProvider'; import { AlertTriangle, CheckCircle2, Info, Search, 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_ONE_SHOT_DISABLED_REASON = 'OpenCode team launch is available for normal teams, but scheduled one-shot prompts still run through claude -p. Choose Anthropic, Codex, or Gemini for one-shot schedules.'; export const OPENCODE_ONE_SHOT_DISABLED_BADGE_LABEL = 'team only'; 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 Opus 1M context. * When limitContext=true, returns base model without [1m] (200K context). * Standard Sonnet and Haiku selections stay standard context. Explicit Sonnet 1M selections keep * their [1m] suffix unless the 200K limit is enabled. */ 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>; modelUnavailableReasonByValue?: Partial>; } export const TeamModelSelector: React.FC = ({ providerId, onProviderChange, value, onValueChange, id, disableGeminiOption = false, providerDisabledReasonById, providerDisabledBadgeLabelById, modelIssueReasonByValue, modelUnavailableReasonByValue, }) => { const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true); const [recommendedOnly, setRecommendedOnly] = useState(false); const [modelQuery, setModelQuery] = useState(''); 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) => isTeamModelRecommended(effectiveProviderId, option.value)), [effectiveProviderId, modelOptions] ); useEffect(() => { if (effectiveProviderId !== 'opencode' || !hasRecommendedOpenCodeModels) { setRecommendedOnly(false); } }, [effectiveProviderId, hasRecommendedOpenCodeModels]); useEffect(() => { setModelQuery(''); }, [effectiveProviderId]); const visibleModelOptions = useMemo(() => { const normalizedModelQuery = modelQuery.trim().toLowerCase(); const matchesModelQuery = (option: (typeof modelOptions)[number]): boolean => { if (!normalizedModelQuery) { return true; } const modelRecommendation = getTeamModelRecommendation(effectiveProviderId, option.value); return [ option.value, option.label, option.badgeLabel ?? '', modelRecommendation?.label ?? '', modelRecommendation?.reason ?? '', ] .join(' ') .toLowerCase() .includes(normalizedModelQuery); }; if (effectiveProviderId !== 'opencode') { return modelOptions.filter(matchesModelQuery); } const concreteOptions = modelOptions .filter((option) => option.value.trim().length > 0) .map((option, index) => ({ option, index })) .filter( ({ option }) => !recommendedOnly || isTeamModelRecommended(effectiveProviderId, option.value) ) .filter(({ option }) => matchesModelQuery(option)) .sort((left, right) => { const recommendationOrder = compareTeamModelRecommendations( effectiveProviderId, 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, ].filter(matchesModelQuery); }, [effectiveProviderId, modelOptions, modelQuery, recommendedOnly]); const concreteModelOptionCount = modelOptions.filter((option) => option.value.trim()).length; const shouldShowModelSearch = concreteModelOptionCount > 8; const trimmedModelQuery = modelQuery.trim(); 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} {shouldShowModelSearch ? (
setModelQuery(event.target.value)} placeholder="Search models" aria-label="Search models" className="h-9 pr-3 text-sm" style={{ paddingLeft: 40 }} />
) : 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 runtimeUnavailableReason = opt.value !== '' && availabilityStatus === 'unavailable' ? (availabilityReason ?? 'Unavailable in current runtime') : null; const modelIssueReason = opt.value === '' ? null : (modelIssueReasonByValue?.[opt.value] ?? null); const modelUnavailableReason = opt.value === '' ? null : (modelUnavailableReasonByValue?.[opt.value] ?? getOpenCodeOpenAiRouteAuthUnavailableReason( effectiveProviderId, opt.value, runtimeProviderStatus ) ?? runtimeUnavailableReason); const hasModelIssue = Boolean(modelIssueReason || modelUnavailableReason); const modelSelectable = activeProviderSelectable && !modelUnavailableReason && !modelDisabledReason && (opt.value === '' || availabilityStatus == null || availabilityStatus === 'available'); const modelStatusMessage = modelUnavailableReason ?? modelIssueReason ?? modelDisabledReason ?? availabilityReason ?? null; const sourceBadgeLabel = effectiveProviderId === 'opencode' && opt.value !== '' ? opt.badgeLabel?.trim() || null : null; const modelRecommendation = getTeamModelRecommendation( effectiveProviderId, opt.value ); return ( ); })() )}
{visibleModelOptions.length === 0 ? (
{trimmedModelQuery ? 'No models match this search.' : effectiveProviderId === 'opencode' && recommendedOnly ? 'No recommended OpenCode models are available in the current runtime list.' : 'No models are available in the current runtime list.'}
) : null}
); };