/** * ExtensionStoreView — top-level component for the Extensions tab. * Uses per-tab UI state via useExtensionsTabState() hook. * Global catalog data comes from Zustand store. */ import { useCallback, useEffect, useMemo, useState } from 'react'; import { api } from '@renderer/api'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Tabs, TabsContent, TabsList } from '@renderer/components/ui/tabs'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@renderer/components/ui/tooltip'; import { useTabIdOptional } from '@renderer/contexts/useTabUIContext'; import { useExtensionsTabState } from '@renderer/hooks/useExtensionsTabState'; import { useStore } from '@renderer/store'; import { resolveProjectPathById } from '@renderer/utils/projectLookup'; import { AlertTriangle, BookOpen, Info, Key, Plus, Puzzle, RefreshCw, Server } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { ApiKeysPanel } from './apikeys/ApiKeysPanel'; import { CustomMcpServerDialog } from './mcp/CustomMcpServerDialog'; import { McpServersPanel } from './mcp/McpServersPanel'; import { PluginsPanel } from './plugins/PluginsPanel'; import { SkillsPanel } from './skills/SkillsPanel'; import { ExtensionsSubTabTrigger } from './ExtensionsSubTabTrigger'; export const ExtensionStoreView = (): React.JSX.Element => { const tabId = useTabIdOptional(); const { fetchPluginCatalog, fetchCliStatus, fetchApiKeys, fetchSkillsCatalog, mcpBrowse, mcpFetchInstalled, apiKeysLoading, pluginCatalogLoading, mcpBrowseLoading, skillsLoading, cliStatus, cliStatusLoading, openDashboard, sessions, projects, repositoryGroups, } = useStore( useShallow((s) => ({ fetchPluginCatalog: s.fetchPluginCatalog, fetchCliStatus: s.fetchCliStatus, fetchApiKeys: s.fetchApiKeys, fetchSkillsCatalog: s.fetchSkillsCatalog, mcpBrowse: s.mcpBrowse, mcpFetchInstalled: s.mcpFetchInstalled, apiKeysLoading: s.apiKeysLoading, pluginCatalogLoading: s.pluginCatalogLoading, mcpBrowseLoading: s.mcpBrowseLoading, skillsLoading: s.skillsLoading, cliStatus: s.cliStatus, cliStatusLoading: s.cliStatusLoading, openDashboard: s.openDashboard, sessions: s.sessions, projects: s.projects, repositoryGroups: s.repositoryGroups, })) ); const cliInstalled = cliStatus?.installed ?? true; const hasOngoingSessions = sessions.some((sess) => sess.isOngoing); const extensionsTabProjectId = useStore((s) => tabId ? (s.paneLayout.panes.flatMap((pane) => pane.tabs).find((tab) => tab.id === tabId) ?.projectId ?? null) : null ); const tabState = useExtensionsTabState(); const [customMcpDialogOpen, setCustomMcpDialogOpen] = useState(false); const resolvedProject = useMemo( () => resolveProjectPathById(extensionsTabProjectId, projects, repositoryGroups), [extensionsTabProjectId, projects, repositoryGroups] ); const projectPath = resolvedProject?.path ?? null; const projectLabel = resolvedProject?.name ?? null; const subTabs = useMemo( () => [ { value: 'plugins' as const, label: 'Plugins', icon: Puzzle, description: 'Small add-ons for Claude. They give the app extra features and integrations you can install when you need them.', }, { value: 'mcp-servers' as const, label: 'MCP Servers', icon: Server, description: 'Connections to outside tools and apps. They let Claude read data or do actions beyond this app.', }, { value: 'skills' as const, label: 'Skills', icon: BookOpen, description: 'Ready-made instructions for common jobs. They help Claude do specific tasks better and more consistently.', }, { value: 'api-keys' as const, label: 'API Keys', icon: Key, description: 'Secret keys for online services. Add them here so plugins, servers, and integrations can connect and work.', }, ], [] ); // Fetch plugin catalog on mount useEffect(() => { void fetchPluginCatalog(projectPath ?? undefined); }, [fetchPluginCatalog, projectPath]); useEffect(() => { void fetchCliStatus(); }, [fetchCliStatus]); // Fetch MCP installed state on mount useEffect(() => { void mcpFetchInstalled(projectPath ?? undefined); }, [mcpFetchInstalled, projectPath]); // Fetch API keys on mount useEffect(() => { void fetchApiKeys(); }, [fetchApiKeys]); // Fetch Skills catalog on mount / project change useEffect(() => { void fetchSkillsCatalog(projectPath ?? undefined); }, [fetchSkillsCatalog, projectPath]); // Refresh all data (plugins + MCP browse + installed + skills) const handleRefresh = useCallback(() => { void fetchCliStatus(); void fetchApiKeys(); void fetchPluginCatalog(projectPath ?? undefined, true); void mcpBrowse(); // re-fetch first page void mcpFetchInstalled(projectPath ?? undefined); void fetchSkillsCatalog(projectPath ?? undefined); }, [ fetchApiKeys, fetchCliStatus, fetchPluginCatalog, fetchSkillsCatalog, mcpBrowse, mcpFetchInstalled, projectPath, ]); const isRefreshing = cliStatusLoading || apiKeysLoading || pluginCatalogLoading || mcpBrowseLoading || skillsLoading; const cliStatusBanner = useMemo(() => { const providers = cliStatus?.providers ?? []; const isMultimodel = cliStatus?.flavor === 'agent_teams_orchestrator' && providers.length > 0; if (cliStatusLoading || cliStatus === null) { return (
Checking extensions runtime availability
Extensions need the configured runtime to manage plugins, MCP servers, skills, and provider connections.
{cliLaunchIssue ? 'The configured runtime was found but failed to start' : 'The configured runtime is not available'}
{cliLaunchIssue ? 'Extensions are disabled until the runtime passes its startup health check. Open the Dashboard to repair or reinstall it.' : 'Extensions are disabled until the runtime is installed. Open the Dashboard to install it and retry.'}
{cliLaunchIssue && cliStatus.launchError && ({cliStatus.launchError}
)}Claude CLI needs sign-in
Claude CLI was found {cliStatus.installedVersion ? ` (${cliStatus.installedVersion})` : ''}, but plugin installs are disabled until you sign in from the Dashboard.
Multimodel runtime capabilities
Provider support can differ by section. Plugins are shown only where the runtime explicitly declares support.
{provider.displayName}
{provider.statusMessage ?? provider.backend?.label ?? 'Ready to configure'}
Claude CLI is ready
Plugins can be installed from this page {cliStatus.installedVersion ? ` using Claude CLI ${cliStatus.installedVersion}` : ''}.
Available in the desktop app only.