import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Terminal, Cloud, Cpu, Brain, Github, Loader2, KeyRound, ShieldCheck } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Input } from '@/components/ui/input'; import type { OpencodeModelId, OpencodeProvider, OpencodeModelConfig, ModelDefinition, } from '@automaker/types'; import { OPENCODE_MODELS, OPENCODE_MODEL_CONFIG_MAP } from '@automaker/types'; import type { OpenCodeProviderInfo } from '../cli-status/opencode-cli-status'; import { OpenCodeIcon, DeepSeekIcon, QwenIcon, NovaIcon, AnthropicIcon, OpenRouterIcon, MistralIcon, MetaIcon, GeminiIcon, OpenAIIcon, GrokIcon, getProviderIconForModel, } from '@/components/ui/provider-icon'; import { useEffect, useMemo, useRef, useState, type ComponentType } from 'react'; interface OpencodeModelConfigurationProps { enabledOpencodeModels: OpencodeModelId[]; opencodeDefaultModel: OpencodeModelId; isSaving: boolean; onDefaultModelChange: (model: OpencodeModelId) => void; onModelToggle: (model: OpencodeModelId, enabled: boolean) => void; providers?: OpenCodeProviderInfo[]; // Dynamic models dynamicModels: ModelDefinition[]; enabledDynamicModelIds: string[]; onDynamicModelToggle: (modelId: string, enabled: boolean) => void; isLoadingDynamicModels?: boolean; } /** * Returns the appropriate icon component for a given OpenCode model ID */ function getModelIcon(modelId: OpencodeModelId): ComponentType<{ className?: string }> { return getProviderIconForModel(modelId); } /** * Returns a formatted provider label for display */ function getProviderLabel(provider: OpencodeProvider): string { switch (provider) { case 'opencode': return 'OpenCode (Free)'; default: return provider; } } /** * Configuration for dynamic provider display */ const DYNAMIC_PROVIDER_CONFIG: Record< string, { label: string; icon: ComponentType<{ className?: string }> } > = { 'github-copilot': { label: 'GitHub Copilot', icon: Github }, google: { label: 'Google AI', icon: GeminiIcon }, openai: { label: 'OpenAI', icon: OpenAIIcon }, openrouter: { label: 'OpenRouter', icon: OpenRouterIcon }, anthropic: { label: 'Anthropic', icon: AnthropicIcon }, opencode: { label: 'OpenCode (Free)', icon: Terminal }, ollama: { label: 'Ollama (Local)', icon: Cpu }, lmstudio: { label: 'LM Studio (Local)', icon: Cpu }, azure: { label: 'Azure OpenAI', icon: Cloud }, 'amazon-bedrock': { label: 'AWS Bedrock', icon: Cloud }, xai: { label: 'xAI', icon: GrokIcon }, deepseek: { label: 'DeepSeek', icon: Brain }, }; function getDynamicProviderConfig(providerId: string) { return ( DYNAMIC_PROVIDER_CONFIG[providerId] || { label: providerId.charAt(0).toUpperCase() + providerId.slice(1).replace(/-/g, ' '), icon: Cloud, } ); } const OPENCODE_AUTH_METHOD_LABELS: Record = { oauth: 'OAuth', api_key: 'Key', api: 'Key', key: 'Key', }; const OPENCODE_AUTH_METHOD_ICONS: Record> = { oauth: ShieldCheck, api_key: KeyRound, api: KeyRound, key: KeyRound, }; const OPENCODE_PROVIDER_FILTER_CLEAR_LABEL = 'Clear'; const OPENCODE_PROVIDER_FILTER_SEARCH_PLACEHOLDER = 'Search models...'; const OPENCODE_PROVIDER_FILTER_EMPTY_LABEL = 'No models match your filters.'; const OPENCODE_PROVIDER_FILTER_EMPTY_HINT = 'Try a different search or provider.'; const OPENCODE_PROVIDER_MODELS_EMPTY_LABEL = 'No models available yet.'; const OPENCODE_PROVIDER_MODELS_EMPTY_HINT = 'Connect or refresh OpenCode CLI to load models.'; const OPENCODE_DYNAMIC_MODELS_SECTION_LABEL = 'Dynamic Models (from OpenCode providers)'; const OPENCODE_SELECT_DYNAMIC_LABEL = 'Select all'; const OPENCODE_SELECT_STATIC_LABEL = 'Select all'; const OPENCODE_SELECT_ALL_CONTAINER_CLASS = 'flex items-center gap-2 rounded-full border border-border/60 bg-card/60 px-2.5 py-1 text-xs text-muted-foreground'; function formatProviderAuthLabel(provider?: OpenCodeProviderInfo): string | null { if (!provider?.authMethod) return null; return OPENCODE_AUTH_METHOD_LABELS[provider.authMethod] || provider.authMethod; } function getProviderAuthIcon( provider?: OpenCodeProviderInfo ): ComponentType<{ className?: string }> | null { if (!provider?.authMethod) return null; return OPENCODE_AUTH_METHOD_ICONS[provider.authMethod] || null; } function getDynamicProviderBaseLabel( providerId: string, providerInfo: OpenCodeProviderInfo | undefined ): string { const providerConfig = getDynamicProviderConfig(providerId); return providerInfo?.name || providerConfig.label; } function getDynamicProviderLabel( providerId: string, providerInfo: OpenCodeProviderInfo | undefined ): string { const providerConfig = getDynamicProviderConfig(providerId); const baseLabel = providerInfo?.name || providerConfig.label; const authLabel = formatProviderAuthLabel(providerInfo); return authLabel ? `${baseLabel} (${authLabel})` : baseLabel; } function getSelectionState( candidateIds: string[], selectedIds: string[] ): boolean | 'indeterminate' { if (candidateIds.length === 0) return false; const allSelected = candidateIds.every((modelId) => selectedIds.includes(modelId)); if (allSelected) return true; const anySelected = candidateIds.some((modelId) => selectedIds.includes(modelId)); return anySelected ? 'indeterminate' : false; } /** * Group dynamic models by their provider */ function groupDynamicModelsByProvider( models: ModelDefinition[] ): Record { return models.reduce( (acc, model) => { const provider = model.provider || 'unknown'; if (!acc[provider]) { acc[provider] = []; } acc[provider].push(model); return acc; }, {} as Record ); } function matchesDynamicModelQuery(model: ModelDefinition, query: string): boolean { if (!query) return true; const haystack = `${model.name} ${model.description} ${model.id}`.toLowerCase(); return haystack.includes(query); } export function OpencodeModelConfiguration({ enabledOpencodeModels, opencodeDefaultModel, isSaving, onDefaultModelChange, onModelToggle, providers, dynamicModels, enabledDynamicModelIds, onDynamicModelToggle, isLoadingDynamicModels = false, }: OpencodeModelConfigurationProps) { // Group static models by provider for organized display const modelsByProvider = OPENCODE_MODELS.reduce( (acc, model) => { if (!acc[model.provider]) { acc[model.provider] = []; } acc[model.provider].push(model); return acc; }, {} as Record ); // Group dynamic models by provider const dynamicModelsByProvider = groupDynamicModelsByProvider(dynamicModels); const authenticatedProviders = (providers || []).filter((provider) => provider.authenticated); const [dynamicProviderFilter, setDynamicProviderFilter] = useState(null); const hasInitializedDynamicProviderFilter = useRef(false); const [dynamicProviderSearch, setDynamicProviderSearch] = useState(''); const normalizedDynamicSearch = dynamicProviderSearch.trim().toLowerCase(); const hasDynamicSearch = normalizedDynamicSearch.length > 0; const allStaticModelIds = OPENCODE_MODELS.map((model) => model.id); const selectableStaticModelIds = allStaticModelIds.filter( (modelId) => modelId !== opencodeDefaultModel ); const allDynamicModelIds = dynamicModels.map((model) => model.id); const hasDynamicModels = allDynamicModelIds.length > 0; const staticSelectState = getSelectionState(selectableStaticModelIds, enabledOpencodeModels); // Order: Free tier first, then Claude, then others const providerOrder: OpencodeProvider[] = ['opencode']; // Dynamic provider order (prioritize commonly used ones) const dynamicProviderOrder = [ 'github-copilot', 'google', 'openai', 'openrouter', 'anthropic', 'xai', 'deepseek', 'ollama', 'lmstudio', 'azure', 'amazon-bedrock', 'opencode', // Skip opencode in dynamic since it's in static ]; const sortedDynamicProviders = useMemo(() => { const providerIndex = (providerId: string) => dynamicProviderOrder.indexOf(providerId); const providerIds = new Set([ ...Object.keys(dynamicModelsByProvider), ...(providers || []).map((provider) => provider.id), ]); providerIds.delete('opencode'); // Don't show opencode twice return Array.from(providerIds).sort((a, b) => { const aIndex = providerIndex(a); const bIndex = providerIndex(b); if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; if (aIndex !== -1) return -1; if (bIndex !== -1) return 1; return a.localeCompare(b); }); }, [dynamicModelsByProvider, providers]); useEffect(() => { if ( dynamicProviderFilter && sortedDynamicProviders.length > 0 && !sortedDynamicProviders.includes(dynamicProviderFilter) ) { setDynamicProviderFilter(sortedDynamicProviders[0]); return; } if ( !hasInitializedDynamicProviderFilter.current && !dynamicProviderFilter && sortedDynamicProviders.length > 0 ) { hasInitializedDynamicProviderFilter.current = true; setDynamicProviderFilter(sortedDynamicProviders[0]); } }, [dynamicProviderFilter, sortedDynamicProviders]); const filteredDynamicProviders = useMemo(() => { const baseProviders = dynamicProviderFilter ? [dynamicProviderFilter] : sortedDynamicProviders; if (!hasDynamicSearch) { return baseProviders; } return baseProviders.filter((providerId) => { const models = dynamicModelsByProvider[providerId] || []; return models.some((model) => matchesDynamicModelQuery(model, normalizedDynamicSearch)); }); }, [ dynamicModelsByProvider, dynamicProviderFilter, hasDynamicSearch, normalizedDynamicSearch, sortedDynamicProviders, ]); const hasDynamicProviders = sortedDynamicProviders.length > 0; const showDynamicProviderFilters = sortedDynamicProviders.length > 1; const hasFilteredDynamicProviders = filteredDynamicProviders.length > 0; const toggleDynamicProviderFilter = (providerId: string) => { setDynamicProviderFilter((current) => (current === providerId ? current : providerId)); }; const toggleAllStaticModels = (checked: boolean) => { if (checked) { selectableStaticModelIds.forEach((modelId) => { if (!enabledOpencodeModels.includes(modelId)) { onModelToggle(modelId, true); } }); return; } selectableStaticModelIds.forEach((modelId) => { if (enabledOpencodeModels.includes(modelId)) { onModelToggle(modelId, false); } }); }; const toggleProviderDynamicModels = (modelIds: string[], checked: boolean) => { if (checked) { modelIds.forEach((modelId) => { if (!enabledDynamicModelIds.includes(modelId)) { onDynamicModelToggle(modelId, true); } }); return; } modelIds.forEach((modelId) => { if (enabledDynamicModelIds.includes(modelId)) { onDynamicModelToggle(modelId, false); } }); }; return (

Model Configuration

Configure which OpenCode models are available in the feature modal

{/* Default Model Selection */}
{/* Available Models grouped by provider */}
{selectableStaticModelIds.length > 0 && (
{OPENCODE_SELECT_STATIC_LABEL}
)}
{/* Static models grouped by provider (Built-in) */} {providerOrder.map((provider) => { const models = modelsByProvider[provider]; if (!models || models.length === 0) return null; // Use the first model's icon as the provider icon const ProviderIconComponent = models.length > 0 ? getModelIcon(models[0].id) : OpenCodeIcon; return (
{getProviderLabel(provider)} {provider === 'opencode' && ( Free )}
{models.map((model) => { const isEnabled = enabledOpencodeModels.includes(model.id); const isDefault = model.id === opencodeDefaultModel; return (
onModelToggle(model.id, !!checked)} disabled={isSaving || isDefault} />
{model.label} {model.supportsVision && ( Vision )} {model.tier === 'free' && ( Free )} {isDefault && ( Default )}

{model.description}

); })}
); })} {/* Dynamic models from OpenCode providers */} {(hasDynamicProviders || isLoadingDynamicModels) && ( <> {/* Separator between static and dynamic models */}

{OPENCODE_DYNAMIC_MODELS_SECTION_LABEL}

{isLoadingDynamicModels && (
Discovering...
)}
{showDynamicProviderFilters && (
{sortedDynamicProviders.map((providerId) => { const providerInfo = authenticatedProviders.find( (provider) => provider.id === providerId ); const providerLabel = getDynamicProviderBaseLabel(providerId, providerInfo); const providerConfig = getDynamicProviderConfig(providerId); const ProviderIcon = providerConfig.icon; const AuthIcon = getProviderAuthIcon(providerInfo); const authLabel = formatProviderAuthLabel(providerInfo); const isActive = dynamicProviderFilter === providerId; const authBadgeClass = cn( 'inline-flex h-5 w-5 items-center justify-center rounded-full border border-transparent bg-transparent text-muted-foreground/80 transition-colors', isActive && 'text-accent-foreground' ); return ( ); })}
)} {hasDynamicProviders && (
setDynamicProviderSearch(event.target.value)} placeholder={OPENCODE_PROVIDER_FILTER_SEARCH_PLACEHOLDER} className="h-8 text-xs" /> {dynamicProviderSearch && ( )}
)} {hasDynamicSearch && !hasFilteredDynamicProviders && (

{OPENCODE_PROVIDER_FILTER_EMPTY_LABEL}

{OPENCODE_PROVIDER_FILTER_EMPTY_HINT}

)} {filteredDynamicProviders.map((providerId) => { const models = dynamicModelsByProvider[providerId] || []; const providerConfig = getDynamicProviderConfig(providerId); const providerInfo = authenticatedProviders.find( (provider) => provider.id === providerId ); const providerLabel = getDynamicProviderLabel(providerId, providerInfo); const DynamicProviderIcon = providerConfig.icon; const filteredModels = hasDynamicSearch ? models.filter((model) => matchesDynamicModelQuery(model, normalizedDynamicSearch) ) : models; if (hasDynamicSearch && filteredModels.length === 0) { return null; } return (
{providerLabel} Dynamic
{models.length > 0 && (
model.id), enabledDynamicModelIds )} onCheckedChange={(checked) => toggleProviderDynamicModels( models.map((model) => model.id), checked ) } disabled={isSaving} /> {OPENCODE_SELECT_DYNAMIC_LABEL}
)}
{filteredModels.length === 0 ? (

{OPENCODE_PROVIDER_MODELS_EMPTY_LABEL}

{OPENCODE_PROVIDER_MODELS_EMPTY_HINT}

) : ( filteredModels.map((model) => { const isEnabled = enabledDynamicModelIds.includes(model.id); return (
onDynamicModelToggle(model.id, !!checked) } disabled={isSaving} />
{model.name} {model.supportsVision && ( Vision )}

{model.description}

); }) )}
); })} )}
); }