import { useMemo, useState, type ReactNode } from 'react' import { usePluginStore } from '../../stores/pluginStore' import { useSessionStore } from '../../stores/sessionStore' import { useTranslation } from '../../i18n' import { useUIStore } from '../../stores/uiStore' import { Button } from '../shared/Button' import { ConfirmDialog } from '../shared/ConfirmDialog' import type { PluginCapabilityKey } from '../../types/plugin' import { SETTINGS_TAB_ID, useTabStore } from '../../stores/tabStore' import { useSkillStore } from '../../stores/skillStore' import { useAgentStore } from '../../stores/agentStore' import { useMcpStore } from '../../stores/mcpStore' const CAPABILITY_ORDER: PluginCapabilityKey[] = [ 'lspServers', ] export function PluginDetail() { const { selectedPlugin, isDetailLoading, isApplying, clearSelection, enablePlugin, disablePlugin, updatePlugin, uninstallPlugin, reloadPlugins, } = usePluginStore() const sessions = useSessionStore((s) => s.sessions) const activeSessionId = useSessionStore((s) => s.activeSessionId) const addToast = useUIStore((s) => s.addToast) const fetchSkillDetail = useSkillStore((s) => s.fetchSkillDetail) const fetchAgents = useAgentStore((s) => s.fetchAgents) const selectAgent = useAgentStore((s) => s.selectAgent) const fetchServers = useMcpStore((s) => s.fetchServers) const selectServer = useMcpStore((s) => s.selectServer) const t = useTranslation() const [actionKey, setActionKey] = useState(null) const [showUninstallDialog, setShowUninstallDialog] = useState(false) const activeSession = sessions.find((session) => session.id === activeSessionId) const currentWorkDir = activeSession?.workDir || undefined const otherCapabilityItems = useMemo( () => CAPABILITY_ORDER.map((key) => ({ key, items: selectedPlugin?.capabilities[key] ?? [], })), [selectedPlugin], ) if (isDetailLoading) { return (
) } if (!selectedPlugin) return null const canMutate = selectedPlugin.scope !== 'managed' && selectedPlugin.scope !== 'builtin' const canNavigateSharedCapabilities = selectedPlugin.enabled const runAction = async (key: string, fn: () => Promise) => { setActionKey(key) try { const message = await fn() addToast({ type: 'success', message }) } catch (err) { addToast({ type: 'error', message: err instanceof Error ? err.message : String(err), }) } finally { setActionKey(null) } } const handleReload = async () => { setActionKey('reload') try { const summary = await reloadPlugins(currentWorkDir, activeSessionId || undefined) addToast({ type: summary.errors > 0 ? 'warning' : 'success', message: t('settings.plugins.reloadToast', { enabled: String(summary.enabled), skills: String(summary.skills), errors: String(summary.errors), }), }) } catch (err) { addToast({ type: 'error', message: err instanceof Error ? err.message : String(err), }) } finally { setActionKey(null) } } const openSettingsTab = (tab: 'skills' | 'agents' | 'mcp') => { useUIStore.getState().setPendingSettingsTab(tab) useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings') } const handleOpenSkill = async (skillName: string) => { if (!canNavigateSharedCapabilities) { addToast({ type: 'warning', message: t('settings.plugins.sharedNavigationDisabled'), }) return } openSettingsTab('skills') await fetchSkillDetail('plugin', skillName, currentWorkDir, 'plugins') const { selectedSkill, error } = useSkillStore.getState() if (!selectedSkill && error) { addToast({ type: 'error', message: error }) } } const handleOpenAgent = async (agentType: string) => { if (!canNavigateSharedCapabilities) { addToast({ type: 'warning', message: t('settings.plugins.sharedNavigationDisabled'), }) return } openSettingsTab('agents') await fetchAgents(currentWorkDir) const state = useAgentStore.getState() const agent = state.allAgents.find((entry) => entry.agentType === agentType) if (!agent) { addToast({ type: 'error', message: `Unable to locate agent: ${agentType}`, }) return } selectAgent(agent, 'plugins') } const handleOpenMcpServer = async (serverName: string) => { if (!canNavigateSharedCapabilities) { addToast({ type: 'warning', message: t('settings.plugins.sharedNavigationDisabled'), }) return } openSettingsTab('mcp') await fetchServers(undefined, currentWorkDir) const state = useMcpStore.getState() const server = state.servers.find((entry) => entry.name === serverName) if (!server) { addToast({ type: 'error', message: `Unable to locate MCP server: ${serverName}`, }) return } selectServer(server) } return (
{t('settings.plugins.entryEyebrow')}

{selectedPlugin.name}

{t(`settings.plugins.scope.${selectedPlugin.scope}`)} {selectedPlugin.marketplace} {selectedPlugin.version && v{selectedPlugin.version}}

{selectedPlugin.description || t('settings.plugins.noDescription')}

{selectedPlugin.authorName && ( {t('settings.plugins.author', { value: selectedPlugin.authorName })} )} {selectedPlugin.projectPath && ( {t('settings.plugins.projectPath', { value: selectedPlugin.projectPath })} )} {selectedPlugin.installPath && ( {t('settings.plugins.installPath', { value: selectedPlugin.installPath })} )}
{canMutate && ( selectedPlugin.enabled ? ( ) : ( ) )} {canMutate && ( )} {canMutate && ( )}
{!canMutate && (

{selectedPlugin.scope === 'managed' ? t('settings.plugins.managedHint') : t('settings.plugins.builtinHint')}

)}

{t('settings.plugins.applyHint')}

{selectedPlugin.errors.length > 0 && (
error

{t('settings.plugins.errorsTitle')}

{selectedPlugin.errors.map((error) => (
{error}
))}
)}

{t('settings.plugins.capabilitiesTitle')}

{t('settings.plugins.capabilitiesHint')}

{selectedPlugin.skillEntries.length > 0 ? (
{selectedPlugin.skillEntries.map((skill) => ( void handleOpenSkill(skill.name)} disabled={!canNavigateSharedCapabilities} /> ))}
) : null}
{selectedPlugin.mcpServerEntries.length > 0 ? (
{selectedPlugin.mcpServerEntries.map((server) => ( void handleOpenMcpServer(server.name)} disabled={!canNavigateSharedCapabilities} /> ))}
) : null}
{selectedPlugin.commandEntries.length > 0 ? (
{selectedPlugin.commandEntries.map((command) => ( ))}
) : null}
{selectedPlugin.agentEntries.length > 0 ? (
{selectedPlugin.agentEntries.map((agent) => ( void handleOpenAgent(agent.name)} disabled={!canNavigateSharedCapabilities} /> ))}
) : null}
{selectedPlugin.hookEntries.length > 0 ? (
{selectedPlugin.hookEntries.map((hook, index) => ( ))}
) : null}
{otherCapabilityItems.map(({ key, items }) => (
{t(`settings.plugins.capabilityLabel.${key}`)}
{items.length}
{items.length > 0 ? (
{items.map((item) => ( {item} ))}
) : (
{t('settings.plugins.capabilityEmpty')}
)}
))}
{ if (isApplying && actionKey === 'uninstall') return setShowUninstallDialog(false) }} onConfirm={async () => { setShowUninstallDialog(false) await runAction('uninstall', () => uninstallPlugin(selectedPlugin.id, selectedPlugin.scope, false, currentWorkDir, activeSessionId || undefined)) }} title={t('settings.plugins.uninstall')} body={t('settings.plugins.confirmUninstall', { name: selectedPlugin.name })} confirmLabel={t('settings.plugins.uninstall')} cancelLabel={t('common.cancel')} confirmVariant="danger" loading={isApplying && actionKey === 'uninstall'} />
) } function CapabilityPreviewSection({ title, count, children, emptyLabel, hint, }: { title: string count: number children: ReactNode emptyLabel: string hint?: string }) { return (
{title}
{count}
{hint && count > 0 && (
{hint}
)} {count > 0 ? children : (
{emptyLabel}
)}
) } function SkillPreviewCard({ name, rawName, description, version, onClick, disabled, }: { name: string rawName?: string description: string version?: string onClick: () => void disabled?: boolean }) { const t = useTranslation() const slashName = rawName || name return ( ) } function CommandPreviewCard({ name, description, }: { name: string description: string }) { return (
/{name}
{description}
) } function AgentPreviewCard({ name, description, onClick, disabled, }: { name: string description: string onClick: () => void disabled?: boolean }) { return ( ) } function McpPreviewCard({ name, transport, summary, onClick, disabled, }: { name: string transport: string summary: string onClick: () => void disabled?: boolean }) { return ( ) } function HookPreviewCard({ event, matcher, actions, }: { event: string matcher?: string actions: string[] }) { return (
{event} {matcher && ( {matcher} )}
{actions.map((action) => ( {action} ))}
) } function MetaPill({ children }: { children: ReactNode }) { return ( {children} ) } function DetailStat({ label, value, icon, }: { label: string value: string icon: string }) { return (
{icon} {label}
{value}
) } function StatusPill({ enabled, hasErrors, }: { enabled: boolean hasErrors: boolean }) { const t = useTranslation() const classes = hasErrors ? 'bg-[var(--color-error)]/12 text-[var(--color-error)]' : enabled ? 'bg-[var(--color-success-container)] text-[var(--color-success)]' : 'bg-[var(--color-surface-container-high)] text-[var(--color-text-tertiary)]' const label = hasErrors ? t('settings.plugins.status.attention') : enabled ? t('settings.plugins.status.enabled') : t('settings.plugins.status.disabled') return ( {label} ) }