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}%`}
{/* 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 && (
)}
);
}
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 (
);
}