import { useCallback, useEffect, useMemo, useState } from 'react'; import { Bot, ExternalLink, Loader2, Store } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { getCloudServiceClientSync, httpClient } from '@/app/infra/http'; import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic'; import type { PluginV4 } from '@/app/infra/entities/plugin'; import { AgentRunnerMarketplaceError, getErrorMessage, installMarketplaceAgentRunner, loadAgentRunnerCatalog, marketplacePluginId, runnerPluginPrefix, readPendingAgentRunnerInstall, subscribePendingAgentRunnerInstall, type AgentRunnerCatalog, type InstalledAgentRunner, } from '@/app/home/agents/agent-runner-marketplace'; import { InstallStage, usePluginInstallTasks, } from '@/app/home/plugins/components/plugin-install-task'; import MarketplaceInstallButton from '@/app/home/components/MarketplaceInstallButton'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, } from '@/components/ui/select'; function installErrorMessage( error: unknown, t: ReturnType['t'], ) { if (error instanceof AgentRunnerMarketplaceError) { if (error.code === 'version-unavailable') { return t('wizard.aiEngine.versionUnavailable'); } if (error.code === 'install-timeout') { return t('wizard.aiEngine.installTimeout'); } return t('wizard.aiEngine.registrationTimeout'); } return getErrorMessage(error) || t('wizard.aiEngine.installFailed'); } function installedRunnerIconURL(option: IDynamicFormItemOption) { return option.name.startsWith('plugin:') ? (() => { const match = option.name.match(/^plugin:([^/]+)\/([^/]+)(?:\/|$)/); return match ? httpClient.getPluginIconURL(match[1], match[2]) : null; })() : null; } function InstalledRunnerContent({ option, }: { option: IDynamicFormItemOption; }) { const iconURL = installedRunnerIconURL(option); return ( {iconURL ? ( ) : ( )} {extractI18nObject(option.label)} ); } function InstalledRunnerOptionContent({ option, description, }: { option: IDynamicFormItemOption; description: string; }) { const iconURL = installedRunnerIconURL(option); return ( {iconURL ? ( ) : ( )} {extractI18nObject(option.label)} {description} ); } function runnerPluginId(optionName: string) { return optionName.match(/^plugin:([^/]+\/[^/]+)(?:\/|$)/)?.[1] ?? null; } function installedRunnerDescription( option: IDynamicFormItemOption, marketplaceRunners: PluginV4[], installedPluginDescriptions: AgentRunnerCatalog['installedPluginDescriptions'], ) { const pluginId = runnerPluginId(option.name); if (!pluginId) return option.name; const marketplacePlugin = marketplaceRunners.find( (plugin) => marketplacePluginId(plugin) === pluginId, ); return ( (marketplacePlugin?.description ? extractI18nObject(marketplacePlugin.description) : '') || (installedPluginDescriptions[pluginId] ? extractI18nObject(installedPluginDescriptions[pluginId]) : '') || option.name ); } function MarketplaceRunnerContent({ plugin, installing, progress, installDisabled, installLabel, onInstall, }: { plugin: PluginV4; installing: boolean; progress: number; installDisabled: boolean; installLabel: string; onInstall: () => void; }) { const iconURL = getCloudServiceClientSync().resolveMarketplaceIconURL( plugin.type, plugin.author, plugin.name, plugin.icon, ); const description = extractI18nObject(plugin.description) || `${plugin.author}/${plugin.name}`; return (
{extractI18nObject(plugin.label) || plugin.name} {description}
); } export default function AgentRunnerSelect({ options, label, value, onValueChange, installScope, onInstalled, }: { options: IDynamicFormItemOption[]; label: string; value: string; onValueChange: (value: string) => void; installScope: string; onInstalled: (installed: InstalledAgentRunner) => void; }) { const { t } = useTranslation(); const { addTask, tasks } = usePluginInstallTasks(); const [marketplaceRunners, setMarketplaceRunners] = useState([]); const [installedPluginIds, setInstalledPluginIds] = useState([]); const [installedPluginDescriptions, setInstalledPluginDescriptions] = useState({}); const [catalogLoading, setCatalogLoading] = useState(true); const [catalogError, setCatalogError] = useState(false); const [pendingInstall, setPendingInstall] = useState(() => readPendingAgentRunnerInstall(installScope), ); const [installError, setInstallError] = useState(null); const [installingPluginId, setInstallingPluginId] = useState( null, ); const loadCatalog = useCallback(async () => { setCatalogLoading(true); setCatalogError(false); try { const catalog = await loadAgentRunnerCatalog(); setMarketplaceRunners(catalog.marketplaceRunners); setInstalledPluginIds(catalog.installedPluginIds); setInstalledPluginDescriptions(catalog.installedPluginDescriptions); } catch (error) { console.error('Failed to load AgentRunner catalog', error); setCatalogError(true); } finally { setCatalogLoading(false); } }, []); useEffect(() => { void loadCatalog(); }, [loadCatalog]); useEffect(() => { const syncPendingInstall = () => setPendingInstall(readPendingAgentRunnerInstall(installScope)); syncPendingInstall(); return subscribePendingAgentRunnerInstall(installScope, syncPendingInstall); }, [installScope]); const marketplaceOptions = useMemo( () => marketplaceRunners.filter((plugin) => { const pluginId = marketplacePluginId(plugin); if (installedPluginIds.includes(pluginId)) return false; return !options.some((option) => option.name.startsWith(runnerPluginPrefix(plugin)), ); }), [installedPluginIds, marketplaceRunners, options], ); const selectedOption = options.find((option) => option.name === value); const activePluginId = pendingInstall?.pluginId ?? installingPluginId; const activeTask = pendingInstall ? tasks.find((task) => task.taskId === pendingInstall.taskId) : undefined; const installProgress = activePluginId ? activeTask ? activeTask.stage === InstallStage.DONE ? 95 : Math.max(5, activeTask.overallProgress) : 5 : 0; const handleValueChange = useCallback( (nextValue: string) => { setInstallError(null); onValueChange(nextValue); }, [onValueChange], ); const handleInstall = useCallback( async (plugin: PluginV4) => { const pluginId = marketplacePluginId(plugin); if (pendingInstall || installingPluginId) return; setInstallingPluginId(pluginId); setInstallError(null); try { const installed = await installMarketplaceAgentRunner(plugin, { scope: installScope, onTaskCreated: (taskId) => addTask({ taskId, pluginName: marketplacePluginId(plugin), source: 'marketplace', extensionType: 'plugin', }), }); onInstalled(installed); await loadCatalog(); toast.success( t('agents.runnerInstallSuccess', { runner: extractI18nObject(plugin.label) || plugin.name, }), ); } catch (error) { const message = installErrorMessage(error, t); setInstallError(message); toast.error(message); } finally { const current = readPendingAgentRunnerInstall(installScope); setPendingInstall(current); if (!current) setInstallingPluginId(null); } }, [ addTask, installScope, installingPluginId, loadCatalog, onInstalled, pendingInstall, t, ], ); return (
{installError && (

{installError}

)}
); }