import React from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Progress } from '@/components/ui/progress'; import { Button } from '@/components/ui/button'; import { Download, Package, Server, Sparkles, Rocket, CheckCircle2, XCircle, Loader2, RefreshCcw, ShieldCheck, } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { usePluginInstallTasks, InstallStage, PluginInstallTask, } from './PluginInstallTaskContext'; import { cn } from '@/lib/utils'; type StageConfig = { key: InstallStage; icon: React.ElementType; i18nKey: string; }; function getStages(task: PluginInstallTask): StageConfig[] { return [ ...(task.operation === 'upgrade' ? [ { key: InstallStage.CHECKING, icon: RefreshCcw, i18nKey: 'plugins.installProgress.checkingUpdate', } as StageConfig, ] : []), { key: InstallStage.DOWNLOADING, icon: Download, i18nKey: 'plugins.installProgress.downloading', }, { key: InstallStage.VALIDATING, icon: ShieldCheck, i18nKey: 'plugins.installProgress.validating', }, { key: InstallStage.INSTALLING_DEPS, icon: Package, i18nKey: task.operation === 'upgrade' ? 'plugins.installProgress.applyingUpdate' : 'plugins.installProgress.installingDeps', }, { key: InstallStage.LAUNCHING, icon: Rocket, i18nKey: 'plugins.installProgress.activating', }, ]; } function getStageIndex(stages: StageConfig[], stage: InstallStage): number { const displayStage = stage === InstallStage.INITIALIZING ? InstallStage.LAUNCHING : stage; const idx = stages.findIndex((item) => item.key === displayStage); return idx >= 0 ? idx : -1; } function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; } /** * A single stage row — used in both active (single) and completed (all) views. */ function StageRow({ icon: Icon, label, isActive, isCompleted, isError, detail, }: { icon: React.ElementType; label: string; isActive: boolean; isCompleted: boolean; isError: boolean; detail?: React.ReactNode; }) { return (
{/* Left: status indicator */}
{isCompleted ? ( ) : isError && isActive ? ( ) : isActive ? ( ) : ( )}
{/* Middle: label + detail */}
{label} {/* Small icon after text */}
{detail && (
{detail}
)}
); } function formatSpeed(bytesPerSec: number): string { if (bytesPerSec === 0) return '0 B/s'; const k = 1024; const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']; const i = Math.floor(Math.log(bytesPerSec) / Math.log(k)); return (bytesPerSec / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; } function TaskProgressContent({ task }: { task: PluginInstallTask }) { const { t } = useTranslation(); const stages = getStages(task); const isDone = task.stage === InstallStage.DONE; const isError = task.stage === InstallStage.ERROR; const currentStageIndex = getStageIndex( stages, isError ? (task.failedStage ?? InstallStage.INSTALLING_DEPS) : task.stage, ); // MCP / Skill don't have the plugin's download + dependency-install stages; // show a single "installing → done/failed" row instead of plugin steps. const isPlugin = task.extensionType === 'plugin'; const simpleIcon = task.extensionType === 'mcp' ? Server : Sparkles; const simpleInstallingLabel = task.extensionType === 'mcp' ? t('addExtension.installStage.mcpInstalling') : t('addExtension.installStage.skillInstalling'); /** Build detail node for a stage */ const getStageDetail = ( stageKey: InstallStage, isCompletedView: boolean, ): React.ReactNode | undefined => { if (stageKey === InstallStage.DOWNLOADING) { // Show download progress: current / total + speed const dlTotal = task.downloadTotal || task.fileSize; const dlCurrent = task.downloadCurrent; const dlSpeed = task.downloadSpeed; if (isCompletedView && dlTotal) { // Done view: just show total size return t('plugins.installProgress.downloadSize', { size: formatFileSize(dlTotal), }); } if (dlTotal && dlCurrent != null) { const parts: string[] = []; parts.push(`${formatFileSize(dlCurrent)} / ${formatFileSize(dlTotal)}`); if (dlSpeed && dlSpeed > 0) { parts.push(formatSpeed(dlSpeed)); } return parts.join(' · '); } if (dlTotal) { return t('plugins.installProgress.downloadSize', { size: formatFileSize(dlTotal), }); } return undefined; } if (stageKey === InstallStage.INSTALLING_DEPS) { // The runtime installs dependencies and starts the plugin in one step // and does not report per-dependency progress, so this stage has no // detail to show beyond its label. return undefined; } return undefined; }; return (
{/* Overall progress bar — always blue */}
{isDone ? t('plugins.installProgress.completed') : isError ? t('plugins.installProgress.failed') : t('plugins.installProgress.overallProgress')} {isDone ? '100%' : `${task.overallProgress}%`}
div]:bg-blue-500 dark:[&>div]:bg-blue-400', 'bg-blue-100 dark:bg-blue-900/30', isDone && '[&>div]:bg-green-500 dark:[&>div]:bg-green-400 bg-green-100 dark:bg-green-900/30', isError && '[&>div]:bg-red-500 dark:[&>div]:bg-red-400 bg-red-100 dark:bg-red-900/30', )} />
{/* Stage display */}
{!isPlugin ? ( /* MCP / Skill: single installing → done/failed row */ ) : ( stages.map((stageConfig, index) => ( )) )}
{/* Done banner */} {isDone && (
{task.operation === 'upgrade' ? t('plugins.installProgress.updateComplete') : t('plugins.installProgress.installComplete')}
)} {/* Error detail */} {isError && task.error && (

{task.error}

)}
); } export default function PluginInstallProgressDialog() { const { t } = useTranslation(); const { tasks, selectedTaskId, setSelectedTaskId, removeTask } = usePluginInstallTasks(); const selectedTask = tasks.find((t) => t.id === selectedTaskId) || null; const TitleIcon = selectedTask?.operation === 'upgrade' ? RefreshCcw : Download; const open = !!selectedTask; const handleClose = () => { setSelectedTaskId(null); }; const handleDismiss = () => { if (selectedTask) { if ( selectedTask.stage === InstallStage.DONE || selectedTask.stage === InstallStage.ERROR ) { removeTask(selectedTask.id); } } setSelectedTaskId(null); }; return ( !o && handleClose()}> {selectedTask ? selectedTask.operation === 'upgrade' ? t('plugins.installProgress.updateTitle', { name: selectedTask.pluginName, }) : t('plugins.installProgress.title', { name: selectedTask.pluginName, }) : t('plugins.installProgress.titleGeneric')} {selectedTask && }
); }