diff --git a/src/components/plugins/view/PluginSettingsTab.tsx b/src/components/plugins/view/PluginSettingsTab.tsx
index a411b94c..106a7511 100644
--- a/src/components/plugins/view/PluginSettingsTab.tsx
+++ b/src/components/plugins/view/PluginSettingsTab.tsx
@@ -1,12 +1,93 @@
-import { useState } from 'react';
+import { useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
-import { Trash2, RefreshCw, GitBranch, Loader2, ServerCrash, ShieldAlert, ExternalLink, BookOpen, Download, BarChart3 } from 'lucide-react';
+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/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: 'claude-watch',
+ translationKey: 'claudeWatchPlugin',
+ repoUrl: CLAUDE_WATCH_PLUGIN_URL,
+ installedNames: ['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 }) {
@@ -208,117 +289,93 @@ function PluginCard({
);
}
-/* ─── Starter Plugin Card ───────────────────────────────────────────────── */
-function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; installing: boolean }) {
- const { t } = useTranslation('settings');
-
+/* ─── Recommendation Section ────────────────────────────────────────────── */
+function RecommendationSection({
+ title,
+ description,
+ children,
+}: {
+ title: string;
+ description: string;
+ children: ReactNode;
+}) {
return (
-
-
-
-
-
-
-
-
-
-
-
- {t('pluginSettings.starterPlugin.name')}
-
-
- {t('pluginSettings.starterPlugin.badge')}
-
-
- {t('pluginSettings.tab')}
-
-
-
- {t('pluginSettings.starterPlugin.description')}
-
-
-
- cloudcli-ai/cloudcli-plugin-starter
-
-
-
-
-
+
+
+
+ {title}
+
+
+ {description}
+
-
+
+ {children}
+
+
);
}
-/* ─── Terminal Plugin Card ──────────────────────────────────────────────── */
-function TerminalPluginCard({ onInstall, installing }: { onInstall: () => void; installing: boolean }) {
+/* ─── Plugin Recommendation Card ────────────────────────────────────────── */
+function PluginRecommendationCard({
+ recommendation,
+ onInstall,
+ installing,
+}: {
+ recommendation: PluginRecommendation;
+ onInstall: () => void;
+ 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.terminalPlugin.name')}
-
-
- {t('pluginSettings.terminalPlugin.badge')}
+ {t(`pluginSettings.${recommendation.translationKey}.name`)}
{t('pluginSettings.tab')}
- {t('pluginSettings.terminalPlugin.description')}
+ {t(`pluginSettings.${recommendation.translationKey}.description`)}
- cloudcli-ai/cloudcli-plugin-terminal
+ {repoSlug(recommendation.repoUrl)}
@@ -334,8 +391,7 @@ export default function PluginSettingsTab() {
const [gitUrl, setGitUrl] = useState('');
const [installing, setInstalling] = useState(false);
- const [installingStarter, setInstallingStarter] = useState(false);
- const [installingTerminal, setInstallingTerminal] = useState(false);
+ const [installingRecommendation, setInstallingRecommendation] = useState
(null);
const [installError, setInstallError] = useState(null);
const [confirmUninstall, setConfirmUninstall] = useState(null);
const [updatingPlugins, setUpdatingPlugins] = useState>(new Set());
@@ -364,24 +420,14 @@ export default function PluginSettingsTab() {
setInstalling(false);
};
- const handleInstallStarter = async () => {
- setInstallingStarter(true);
+ const handleInstallRecommendation = async (recommendation: PluginRecommendation) => {
+ setInstallingRecommendation(recommendation.id);
setInstallError(null);
- const result = await installPlugin(STARTER_PLUGIN_URL);
+ const result = await installPlugin(recommendation.repoUrl);
if (!result.success) {
setInstallError(result.error || t('pluginSettings.installFailed'));
}
- setInstallingStarter(false);
- };
-
- const handleInstallTerminal = async () => {
- setInstallingTerminal(true);
- setInstallError(null);
- const result = await installPlugin(TERMINAL_PLUGIN_URL);
- if (!result.success) {
- setInstallError(result.error || t('pluginSettings.installFailed'));
- }
- setInstallingTerminal(false);
+ setInstallingRecommendation(null);
};
const handleUninstall = async (name: string) => {
@@ -398,8 +444,50 @@ export default function PluginSettingsTab() {
}
};
- const hasStarterInstalled = plugins.some((p) => p.name === 'project-stats');
- const hasTerminalInstalled = plugins.some((p) => p.name === 'web-terminal');
+ 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 (
@@ -456,51 +544,47 @@ export default function PluginSettingsTab() {
- {/* Official plugin suggestions — above the list */}
- {!loading && (!hasStarterInstalled || !hasTerminalInstalled) && (
-
- {!hasStarterInstalled && (
-
- )}
- {!hasTerminalInstalled && (
-
- )}
-
- )}
-
- {/* Plugin List */}
+ {/* Plugin sections */}
{loading ? (
{t('pluginSettings.scanningPlugins')}
- ) : plugins.length === 0 ? (
-
{t('pluginSettings.noPluginsInstalled')}
) : (
-
- {plugins.map((plugin, index) => {
- const handleToggle = async (enabled: boolean) => {
- const r = await togglePlugin(plugin.name, enabled);
- if (!r.success) {
- setInstallError(r.error || t('pluginSettings.toggleFailed'));
- }
- };
+
+ {hasOfficialSection && (
+
+ {officialPlugins.map((plugin, index) => renderPluginCard(plugin, index))}
+ {officialRecommendations.map((recommendation) => (
+ void handleInstallRecommendation(recommendation)}
+ installing={installingRecommendation === recommendation.id}
+ />
+ ))}
+
+ )}
- 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}
- />
- );
- })}
+ {hasOtherSection && (
+
+ {otherPlugins.map((plugin, index) => renderPluginCard(plugin, officialPlugins.length + index))}
+ {unofficialRecommendations.map((recommendation) => (
+ void handleInstallRecommendation(recommendation)}
+ installing={installingRecommendation === recommendation.id}
+ />
+ ))}
+
+ )}
)}
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json
index 2e91e027..b80d17d2 100644
--- a/src/i18n/locales/en/settings.json
+++ b/src/i18n/locales/en/settings.json
@@ -472,6 +472,12 @@
"starterPluginLabel": "Starter Plugin",
"starter": "Starter",
"docs": "Docs",
+ "sections": {
+ "officialTitle": "Official Plugins",
+ "officialDescription": "Maintained by the CloudCLI team and ready for direct install.",
+ "unofficialTitle": "Other Plugins",
+ "unofficialDescription": "Unofficial plugins and integrations from other users. Review the source before installing."
+ },
"starterPlugin": {
"name": "Project Stats",
"badge": "starter",
@@ -484,6 +490,18 @@
"description": "Integrated terminal with full shell access directly within the interface.",
"install": "Install"
},
+ "scheduledPromptPlugin": {
+ "name": "Scheduled Prompts",
+ "badge": "unofficial",
+ "description": "Schedule workspace prompts, review run history, and manage recurring local tasks.",
+ "install": "Install"
+ },
+ "claudeWatchPlugin": {
+ "name": "Claude Watch",
+ "badge": "unofficial",
+ "description": "Watch long-running Claude Code sessions for hangs and expose process controls.",
+ "install": "Install"
+ },
"morePlugins": "More",
"enable": "Enable",
"disable": "Disable",