Download desktop/src/components/plugins/PluginList.tsx from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 15.3 kB
-
https://huggingface.co/chenbhao/codev/resolve/e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/components/plugins/PluginList.tsx
- Command line
-
hf download hf://chenbhao/codev@e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/components/plugins/PluginList.tsx
-
curl -L -o PluginList.tsx https://huggingface.co/chenbhao/codev/resolve/e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/components/plugins/PluginList.tsx
15.3 kB
| import { useEffect, useMemo } 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 type { PluginSummary } from '../../types/plugin' | |
| type PluginBucket = 'attention' | 'enabled' | 'disabled' | |
| export function PluginList() { | |
| const { | |
| plugins, | |
| marketplaces, | |
| summary, | |
| lastReloadSummary, | |
| isLoading, | |
| isApplying, | |
| error, | |
| fetchPlugins, | |
| fetchPluginDetail, | |
| reloadPlugins, | |
| } = usePluginStore() | |
| const sessions = useSessionStore((s) => s.sessions) | |
| const activeSessionId = useSessionStore((s) => s.activeSessionId) | |
| const addToast = useUIStore((s) => s.addToast) | |
| const t = useTranslation() | |
| const activeSession = sessions.find((session) => session.id === activeSessionId) | |
| const currentWorkDir = activeSession?.workDir || undefined | |
| useEffect(() => { | |
| void fetchPlugins(currentWorkDir) | |
| }, [fetchPlugins, currentWorkDir]) | |
| const grouped = useMemo(() => { | |
| const buckets: Record<PluginBucket, PluginSummary[]> = { | |
| attention: [], | |
| enabled: [], | |
| disabled: [], | |
| } | |
| for (const plugin of plugins) { | |
| if (plugin.hasErrors) { | |
| buckets.attention.push(plugin) | |
| } else if (plugin.enabled) { | |
| buckets.enabled.push(plugin) | |
| } else { | |
| buckets.disabled.push(plugin) | |
| } | |
| } | |
| return buckets | |
| }, [plugins]) | |
| const handleReload = async () => { | |
| try { | |
| const reloadSummary = await reloadPlugins(currentWorkDir, activeSessionId || undefined) | |
| addToast({ | |
| type: reloadSummary.errors > 0 ? 'warning' : 'success', | |
| message: t('settings.plugins.reloadToast', { | |
| enabled: String(reloadSummary.enabled), | |
| skills: String(reloadSummary.skills), | |
| errors: String(reloadSummary.errors), | |
| }), | |
| }) | |
| } catch (err) { | |
| addToast({ | |
| type: 'error', | |
| message: err instanceof Error ? err.message : String(err), | |
| }) | |
| } | |
| } | |
| if (isLoading) { | |
| return ( | |
| <div className="flex justify-center py-12"> | |
| <div className="animate-spin w-5 h-5 border-2 border-[var(--color-brand)] border-t-transparent rounded-full" /> | |
| </div> | |
| ) | |
| } | |
| if (error) { | |
| return <div className="text-sm text-[var(--color-error)] py-4">{error}</div> | |
| } | |
| if (plugins.length === 0) { | |
| return ( | |
| <div className="text-center py-12 rounded-2xl border border-dashed border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-6"> | |
| <span className="material-symbols-outlined text-[40px] text-[var(--color-text-tertiary)] mb-2 block"> | |
| extension | |
| </span> | |
| <p className="text-sm text-[var(--color-text-tertiary)]"> | |
| {t('settings.plugins.empty')} | |
| </p> | |
| <p className="text-xs text-[var(--color-text-tertiary)] mt-1"> | |
| {t('settings.plugins.emptyHint')} | |
| </p> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="flex flex-col gap-6 min-w-0"> | |
| <section className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] overflow-hidden"> | |
| <div className="flex flex-col gap-4 px-5 py-5 min-w-0"> | |
| <div className="flex flex-col gap-4 min-w-0 xl:flex-row xl:items-start xl:justify-between"> | |
| <div className="min-w-0 max-w-4xl"> | |
| <div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-[var(--color-text-tertiary)] mb-2"> | |
| {t('settings.plugins.browserEyebrow')} | |
| </div> | |
| <div className="flex items-center gap-3 mb-2"> | |
| <span className="material-symbols-outlined text-[22px] text-[var(--color-brand)]"> | |
| extension | |
| </span> | |
| <h3 className="text-lg font-semibold text-[var(--color-text-primary)]"> | |
| {t('settings.plugins.browserTitle')} | |
| </h3> | |
| </div> | |
| <p className="text-sm leading-6 text-[var(--color-text-secondary)]"> | |
| {t('settings.plugins.browserDescription')} | |
| </p> | |
| </div> | |
| <div className="flex flex-wrap gap-2 xl:justify-end"> | |
| <Button | |
| variant="secondary" | |
| size="sm" | |
| className="min-h-9 flex-1 sm:flex-none" | |
| onClick={() => void fetchPlugins(currentWorkDir)} | |
| > | |
| <span className="material-symbols-outlined text-[16px]">refresh</span> | |
| {t('settings.plugins.refresh')} | |
| </Button> | |
| <Button | |
| size="sm" | |
| className="min-h-9 flex-1 sm:flex-none" | |
| onClick={handleReload} | |
| loading={isApplying} | |
| > | |
| <span className="material-symbols-outlined text-[16px]">sync</span> | |
| {t('settings.plugins.apply')} | |
| </Button> | |
| </div> | |
| </div> | |
| <div className="grid min-w-0 grid-cols-2 gap-2 md:grid-cols-4"> | |
| <SummaryCard | |
| label={t('settings.plugins.summary.total')} | |
| value={String(summary?.total ?? plugins.length)} | |
| icon="extension" | |
| /> | |
| <SummaryCard | |
| label={t('settings.plugins.summary.enabled')} | |
| value={String(summary?.enabled ?? plugins.filter((plugin) => plugin.enabled).length)} | |
| icon="check_circle" | |
| /> | |
| <SummaryCard | |
| label={t('settings.plugins.summary.attention')} | |
| value={String(grouped.attention.length)} | |
| icon="warning" | |
| /> | |
| <SummaryCard | |
| label={t('settings.plugins.summary.marketplaces')} | |
| value={String(summary?.marketplaceCount ?? marketplaces.length)} | |
| icon="storefront" | |
| /> | |
| </div> | |
| {lastReloadSummary && ( | |
| <p className="text-xs text-[var(--color-text-tertiary)]"> | |
| {t('settings.plugins.lastReload', { | |
| enabled: String(lastReloadSummary.enabled), | |
| skills: String(lastReloadSummary.skills), | |
| errors: String(lastReloadSummary.errors), | |
| })} | |
| </p> | |
| )} | |
| </div> | |
| </section> | |
| {marketplaces.length > 0 && ( | |
| <section className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] overflow-hidden"> | |
| <div className="px-5 py-4 border-b border-[var(--color-border)] bg-[var(--color-surface-container-low)]"> | |
| <h4 className="text-sm font-semibold text-[var(--color-text-primary)]"> | |
| {t('settings.plugins.marketplacesTitle')} | |
| </h4> | |
| <p className="text-xs text-[var(--color-text-tertiary)] mt-1"> | |
| {t('settings.plugins.marketplacesHint')} | |
| </p> | |
| </div> | |
| <div className="grid gap-3 p-4 md:grid-cols-2 xl:grid-cols-3"> | |
| {marketplaces.map((marketplace) => ( | |
| <div | |
| key={marketplace.name} | |
| className="rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-4 py-3" | |
| > | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm font-semibold text-[var(--color-text-primary)]"> | |
| {marketplace.name} | |
| </span> | |
| <span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${ | |
| marketplace.autoUpdate | |
| ? 'bg-[var(--color-success-container)] text-[var(--color-success)]' | |
| : 'bg-[var(--color-surface-container-high)] text-[var(--color-text-tertiary)]' | |
| }`}> | |
| {marketplace.autoUpdate | |
| ? t('settings.plugins.marketplaceAutoUpdateOn') | |
| : t('settings.plugins.marketplaceAutoUpdateOff')} | |
| </span> | |
| </div> | |
| <div className="mt-2 text-xs leading-5 text-[var(--color-text-secondary)] break-words"> | |
| {marketplace.source} | |
| </div> | |
| <div className="mt-3 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-[var(--color-text-tertiary)]"> | |
| <span>{t('settings.plugins.marketplaceInstalledCount', { count: String(marketplace.installedCount) })}</span> | |
| {marketplace.lastUpdated && ( | |
| <span>{t('settings.plugins.marketplaceUpdatedAt', { value: new Date(marketplace.lastUpdated).toLocaleString() })}</span> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </section> | |
| )} | |
| {renderGroup('attention', grouped.attention, fetchPluginDetail, currentWorkDir, t)} | |
| {renderGroup('enabled', grouped.enabled, fetchPluginDetail, currentWorkDir, t)} | |
| {renderGroup('disabled', grouped.disabled, fetchPluginDetail, currentWorkDir, t)} | |
| </div> | |
| ) | |
| } | |
| function renderGroup( | |
| bucket: PluginBucket, | |
| items: PluginSummary[], | |
| fetchPluginDetail: (id: string, cwd?: string) => Promise<void>, | |
| cwd: string | undefined, | |
| t: ReturnType<typeof useTranslation>, | |
| ) { | |
| if (items.length === 0) return null | |
| const titleKey = | |
| bucket === 'attention' | |
| ? 'settings.plugins.group.attention' | |
| : bucket === 'enabled' | |
| ? 'settings.plugins.group.enabled' | |
| : 'settings.plugins.group.disabled' | |
| return ( | |
| <section | |
| key={bucket} | |
| className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] overflow-hidden" | |
| > | |
| <div className="flex items-start justify-between gap-3 px-5 py-4 border-b border-[var(--color-border)] bg-[var(--color-surface-container-low)]"> | |
| <div className="min-w-0"> | |
| <h4 className="text-sm font-semibold text-[var(--color-text-primary)]"> | |
| {t(titleKey)} | |
| </h4> | |
| <p className="text-xs leading-5 text-[var(--color-text-tertiary)] mt-1"> | |
| {t('settings.plugins.groupHint', { count: String(items.length) })} | |
| </p> | |
| </div> | |
| <span className="text-xs text-[var(--color-text-tertiary)]">{items.length}</span> | |
| </div> | |
| <div className="flex flex-col p-2"> | |
| {items.map((plugin) => ( | |
| <button | |
| key={plugin.id} | |
| onClick={() => void fetchPluginDetail(plugin.id, cwd)} | |
| className="group rounded-xl border border-transparent px-3 py-3 text-left transition-all hover:border-[var(--color-border-focus)] hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface)]" | |
| > | |
| <div className="flex items-start gap-3"> | |
| <span className="mt-0.5 material-symbols-outlined text-[18px] text-[var(--color-text-tertiary)]"> | |
| {plugin.hasErrors ? 'warning' : plugin.enabled ? 'extension' : 'extension_off'} | |
| </span> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2 flex-wrap"> | |
| <span className="text-sm font-semibold text-[var(--color-text-primary)] break-all"> | |
| {plugin.name} | |
| </span> | |
| <StatusPill plugin={plugin} /> | |
| <ScopePill scope={plugin.scope} /> | |
| {plugin.version && ( | |
| <span className="rounded-full bg-[var(--color-surface-container-high)] px-2 py-0.5 text-[10px] font-medium text-[var(--color-text-tertiary)]"> | |
| v{plugin.version} | |
| </span> | |
| )} | |
| </div> | |
| <p className="mt-1 text-xs leading-5 text-[var(--color-text-secondary)] break-words"> | |
| {plugin.description || t('settings.plugins.noDescription')} | |
| </p> | |
| <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[var(--color-text-tertiary)]"> | |
| <span>{plugin.marketplace}</span> | |
| {plugin.componentCounts.skills > 0 && ( | |
| <span>{t('settings.plugins.capability.skills', { count: String(plugin.componentCounts.skills) })}</span> | |
| )} | |
| {plugin.componentCounts.agents > 0 && ( | |
| <span>{t('settings.plugins.capability.agents', { count: String(plugin.componentCounts.agents) })}</span> | |
| )} | |
| {plugin.componentCounts.mcpServers > 0 && ( | |
| <span>{t('settings.plugins.capability.mcpServers', { count: String(plugin.componentCounts.mcpServers) })}</span> | |
| )} | |
| {plugin.errors.length > 0 && ( | |
| <span className="text-[var(--color-error)]"> | |
| {t('settings.plugins.errorCount', { count: String(plugin.errors.length) })} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| <span className="material-symbols-outlined text-[18px] text-[var(--color-text-tertiary)] opacity-60 transition-transform group-hover:translate-x-0.5 group-hover:opacity-100"> | |
| chevron_right | |
| </span> | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| </section> | |
| ) | |
| } | |
| function SummaryCard({ | |
| label, | |
| value, | |
| icon, | |
| }: { | |
| label: string | |
| value: string | |
| icon: string | |
| }) { | |
| return ( | |
| <div className="min-w-0 rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-3"> | |
| <div className="flex min-w-0 items-center gap-1.5 text-[11px] uppercase tracking-[0.08em] text-[var(--color-text-tertiary)]"> | |
| <span className="material-symbols-outlined text-[14px] flex-shrink-0">{icon}</span> | |
| <span className="min-w-0 truncate text-[10px] leading-4"> | |
| {label} | |
| </span> | |
| </div> | |
| <div className="mt-1.5 truncate text-lg font-semibold text-[var(--color-text-primary)]"> | |
| {value} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| function StatusPill({ plugin }: { plugin: PluginSummary }) { | |
| const t = useTranslation() | |
| if (plugin.hasErrors) { | |
| return ( | |
| <span className="rounded-full bg-[var(--color-error)]/12 px-2 py-0.5 text-[10px] font-medium text-[var(--color-error)]"> | |
| {t('settings.plugins.status.attention')} | |
| </span> | |
| ) | |
| } | |
| return ( | |
| <span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${ | |
| plugin.enabled | |
| ? 'bg-[var(--color-success-container)] text-[var(--color-success)]' | |
| : 'bg-[var(--color-surface-container-high)] text-[var(--color-text-tertiary)]' | |
| }`}> | |
| {plugin.enabled | |
| ? t('settings.plugins.status.enabled') | |
| : t('settings.plugins.status.disabled')} | |
| </span> | |
| ) | |
| } | |
| function ScopePill({ scope }: { scope: PluginSummary['scope'] }) { | |
| const t = useTranslation() | |
| return ( | |
| <span className="rounded-full border border-[var(--color-border)] px-2 py-0.5 text-[10px] font-medium text-[var(--color-text-tertiary)]"> | |
| {t(`settings.plugins.scope.${scope}`)} | |
| </span> | |
| ) | |
| } | |