import { useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import {
Activity,
BarChart3,
BookOpen,
Clock,
Download,
ExternalLink,
GitBranch,
Loader2,
RefreshCw,
ServerCrash,
ShieldAlert,
Terminal,
Trash2,
type LucideIcon,
} from 'lucide-react';
import { usePlugins } from '../../../contexts/PluginsContext';
import type { Plugin } from '../../../contexts/PluginsContext';
import PluginIcon from './PluginIcon';
const STARTER_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-starter';
const TERMINAL_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-terminal';
const SCHEDULED_PROMPT_PLUGIN_URL = 'https://github.com/grostim/cloudcli-cron';
const CLAUDE_WATCH_PLUGIN_URL = 'https://github.com/satsuki19980613/cloudcli-claude-watch';
type PluginRecommendation = {
id: string;
translationKey: string;
repoUrl: string;
installedNames: string[];
icon: LucideIcon;
source: 'official' | 'unofficial';
};
const OFFICIAL_PLUGIN_RECOMMENDATIONS: PluginRecommendation[] = [
{
id: 'project-stats',
translationKey: 'starterPlugin',
repoUrl: STARTER_PLUGIN_URL,
installedNames: ['project-stats'],
icon: BarChart3,
source: 'official',
},
{
id: 'web-terminal',
translationKey: 'terminalPlugin',
repoUrl: TERMINAL_PLUGIN_URL,
installedNames: ['web-terminal'],
icon: Terminal,
source: 'official',
},
];
const UNOFFICIAL_PLUGIN_RECOMMENDATIONS: PluginRecommendation[] = [
{
id: 'cloudcli-claude-watch',
translationKey: 'claudeWatchPlugin',
repoUrl: CLAUDE_WATCH_PLUGIN_URL,
installedNames: ['cloudcli-claude-watch'],
icon: Activity,
source: 'unofficial',
},
{
id: 'workspace-scheduled-prompts',
translationKey: 'scheduledPromptPlugin',
repoUrl: SCHEDULED_PROMPT_PLUGIN_URL,
installedNames: ['workspace-scheduled-prompts'],
icon: Clock,
source: 'unofficial',
},
];
function repoSlug(repoUrl: string) {
return repoUrl.replace(/^https?:\/\/(www\.)?github\.com\//, '');
}
function normalizeRepoUrl(repoUrl: string | null) {
return repoUrl?.replace(/\.git$/, '').replace(/\/$/, '').toLowerCase() ?? null;
}
function pluginMatchesRecommendation(plugin: Plugin, recommendation: PluginRecommendation) {
return (
recommendation.installedNames.includes(plugin.name)
|| normalizeRepoUrl(plugin.repoUrl) === normalizeRepoUrl(recommendation.repoUrl)
);
}
/* ─── Toggle Switch ─────────────────────────────────────────────────────── */
function ToggleSwitch({ checked, onChange, ariaLabel }: { checked: boolean; onChange: (v: boolean) => void; ariaLabel: string }) {
return (
);
}
/* ─── Server Dot ────────────────────────────────────────────────────────── */
function ServerDot({ running, t }: { running: boolean; t: any }) {
if (!running) return null;
return (
{t('pluginSettings.runningStatus')}
);
}
/* ─── Plugin Card ───────────────────────────────────────────────────────── */
type PluginCardProps = {
plugin: Plugin;
index: number;
onToggle: (enabled: boolean) => void;
onUpdate: () => void;
onUninstall: () => void;
updating: boolean;
confirmingUninstall: boolean;
onCancelUninstall: () => void;
updateError: string | null;
};
function PluginCard({
plugin,
index,
onToggle,
onUpdate,
onUninstall,
updating,
confirmingUninstall,
onCancelUninstall,
updateError,
}: PluginCardProps) {
const { t } = useTranslation('settings');
const accentColor = plugin.enabled
? 'bg-emerald-500'
: 'bg-muted-foreground/20';
return (
{/* Left accent bar */}
{/* Header row */}
{plugin.displayName}
v{plugin.version}
{plugin.slot}
{plugin.description && (
{plugin.description}
)}
{/* Controls */}
{/* Confirm uninstall banner */}
{confirmingUninstall && (
{t('pluginSettings.confirmUninstallMessage', { name: plugin.displayName })}
)}
{/* Update error */}
{updateError && (
{updateError}
)}
);
}
/* ─── Recommendation Section ────────────────────────────────────────────── */
function RecommendationSection({
title,
description,
children,
}: {
title: string;
description: string;
children: ReactNode;
}) {
return (
);
}
/* ─── Plugin Recommendation Card ────────────────────────────────────────── */
function PluginRecommendationCard({
recommendation,
onInstall,
disabled,
installing,
}: {
recommendation: PluginRecommendation;
onInstall: () => void;
disabled: boolean;
installing: boolean;
}) {
const { t } = useTranslation('settings');
const Icon = recommendation.icon;
const isOfficial = recommendation.source === 'official';
const accentClass = isOfficial ? 'bg-blue-500/30' : 'bg-amber-500/40';
const hoverClass = isOfficial ? 'hover:border-blue-400 dark:hover:border-blue-500' : 'hover:border-amber-400 dark:hover:border-amber-500';
const iconClass = isOfficial ? 'text-blue-500' : 'text-amber-500';
return (
{t(`pluginSettings.${recommendation.translationKey}.name`)}
{t('pluginSettings.tab')}
{t(`pluginSettings.${recommendation.translationKey}.description`)}
{repoSlug(recommendation.repoUrl)}
);
}
/* ─── Main Component ────────────────────────────────────────────────────── */
export default function PluginSettingsTab() {
const { t } = useTranslation('settings');
const { plugins, loading, installPlugin, uninstallPlugin, updatePlugin, togglePlugin } =
usePlugins();
const [gitUrl, setGitUrl] = useState('');
const [installing, setInstalling] = useState(false);
const [installingRecommendation, setInstallingRecommendation] = useState(null);
const [installError, setInstallError] = useState(null);
const [confirmUninstall, setConfirmUninstall] = useState(null);
const [updatingPlugins, setUpdatingPlugins] = useState>(new Set());
const [updateErrors, setUpdateErrors] = useState>({});
const handleUpdate = async (name: string) => {
setUpdatingPlugins((prev) => new Set(prev).add(name));
setUpdateErrors((prev) => { const next = { ...prev }; delete next[name]; return next; });
const result = await updatePlugin(name);
if (!result.success) {
setUpdateErrors((prev) => ({ ...prev, [name]: result.error || t('pluginSettings.updateFailed') }));
}
setUpdatingPlugins((prev) => { const next = new Set(prev); next.delete(name); return next; });
};
const handleInstall = async () => {
if (!gitUrl.trim()) return;
setInstalling(true);
setInstallError(null);
const result = await installPlugin(gitUrl.trim());
if (result.success) {
setGitUrl('');
} else {
setInstallError(result.error || t('pluginSettings.installFailed'));
}
setInstalling(false);
};
const handleInstallRecommendation = async (recommendation: PluginRecommendation) => {
if (installingRecommendation) return;
setInstallingRecommendation(recommendation.id);
setInstallError(null);
try {
const result = await installPlugin(recommendation.repoUrl);
if (!result.success) {
setInstallError(result.error || t('pluginSettings.installFailed'));
}
} finally {
setInstallingRecommendation(null);
}
};
const handleUninstall = async (name: string) => {
if (confirmUninstall !== name) {
setConfirmUninstall(name);
return;
}
const result = await uninstallPlugin(name);
if (result.success) {
setConfirmUninstall(null);
} else {
setInstallError(result.error || t('pluginSettings.uninstallFailed'));
setConfirmUninstall(null);
}
};
const isRecommendationInstalled = (recommendation: PluginRecommendation) => {
return plugins.some((plugin) => pluginMatchesRecommendation(plugin, recommendation));
};
const isOfficialPlugin = (plugin: Plugin) => {
return OFFICIAL_PLUGIN_RECOMMENDATIONS.some((recommendation) => (
pluginMatchesRecommendation(plugin, recommendation)
));
};
const officialPlugins = plugins.filter(isOfficialPlugin);
const otherPlugins = plugins.filter((plugin) => !isOfficialPlugin(plugin));
const officialRecommendations = OFFICIAL_PLUGIN_RECOMMENDATIONS.filter(
(recommendation) => !isRecommendationInstalled(recommendation),
);
const unofficialRecommendations = UNOFFICIAL_PLUGIN_RECOMMENDATIONS.filter(
(recommendation) => !isRecommendationInstalled(recommendation),
);
const hasOfficialSection = officialPlugins.length > 0 || officialRecommendations.length > 0;
const hasOtherSection = otherPlugins.length > 0 || unofficialRecommendations.length > 0;
const renderPluginCard = (plugin: Plugin, index: number) => {
const handleToggle = async (enabled: boolean) => {
const r = await togglePlugin(plugin.name, enabled);
if (!r.success) {
setInstallError(r.error || t('pluginSettings.toggleFailed'));
}
};
return (
void handleToggle(enabled)}
onUpdate={() => void handleUpdate(plugin.name)}
onUninstall={() => void handleUninstall(plugin.name)}
updating={updatingPlugins.has(plugin.name)}
confirmingUninstall={confirmUninstall === plugin.name}
onCancelUninstall={() => setConfirmUninstall(null)}
updateError={updateErrors[plugin.name] ?? null}
/>
);
};
return (
{/* Header */}
{t('pluginSettings.title')}
{t('pluginSettings.description')}
{/* Install from Git — compact */}
{
setGitUrl(e.target.value);
setInstallError(null);
}}
placeholder={t('pluginSettings.installPlaceholder')}
aria-label={t('pluginSettings.installAriaLabel')}
className="flex-1 bg-transparent px-2 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/40 focus:outline-none"
onKeyDown={(e) => {
if (e.key === 'Enter') void handleInstall();
}}
/>
{installError && (
{installError}
)}
{t('pluginSettings.securityWarning')}
{/* Plugin sections */}
{loading ? (
{t('pluginSettings.scanningPlugins')}
) : (
{hasOfficialSection && (
{officialPlugins.map((plugin, index) => renderPluginCard(plugin, index))}
{officialRecommendations.map((recommendation) => (
void handleInstallRecommendation(recommendation)}
disabled={!!installingRecommendation}
installing={installingRecommendation === recommendation.id}
/>
))}
)}
{hasOtherSection && (
{otherPlugins.map((plugin, index) => renderPluginCard(plugin, officialPlugins.length + index))}
{unofficialRecommendations.map((recommendation) => (
void handleInstallRecommendation(recommendation)}
disabled={!!installingRecommendation}
installing={installingRecommendation === recommendation.id}
/>
))}
)}
)}
{/* Starter plugin */}
);
}