/** * 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 { useShallow } from 'zustand/react/shallow'; import { AlertTriangle, BookOpen, Info, Key, Plus, Puzzle, RefreshCw, Server } from 'lucide-react'; import { api } from '@renderer/api'; 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 { 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, pluginCatalogLoading, mcpBrowseLoading, skillsLoading, cliStatus, cliStatusLoading, openDashboard, sessions, projects, } = useStore( useShallow((s) => ({ fetchPluginCatalog: s.fetchPluginCatalog, fetchCliStatus: s.fetchCliStatus, fetchApiKeys: s.fetchApiKeys, fetchSkillsCatalog: s.fetchSkillsCatalog, mcpBrowse: s.mcpBrowse, mcpFetchInstalled: s.mcpFetchInstalled, pluginCatalogLoading: s.pluginCatalogLoading, mcpBrowseLoading: s.mcpBrowseLoading, skillsLoading: s.skillsLoading, cliStatus: s.cliStatus, cliStatusLoading: s.cliStatusLoading, openDashboard: s.openDashboard, sessions: s.sessions, projects: s.projects, })) ); 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 projectPath = useMemo( () => projects.find((project) => project.id === extensionsTabProjectId)?.path ?? null, [extensionsTabProjectId, projects] ); const projectLabel = useMemo( () => projects.find((project) => project.id === extensionsTabProjectId)?.name ?? null, [extensionsTabProjectId, projects] ); 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 fetchPluginCatalog(projectPath ?? undefined, true); void mcpBrowse(); // re-fetch first page void mcpFetchInstalled(projectPath ?? undefined); void fetchSkillsCatalog(projectPath ?? undefined); }, [fetchPluginCatalog, fetchSkillsCatalog, mcpBrowse, mcpFetchInstalled, projectPath]); const isRefreshing = pluginCatalogLoading || mcpBrowseLoading || skillsLoading; const cliStatusBanner = useMemo(() => { if (cliStatusLoading || cliStatus === null) { return (
Checking Claude CLI availability
Extensions need Claude CLI to install plugins, run MCP servers, and validate auth.
Claude CLI is not available
Plugin installs are disabled until Claude CLI is installed. Open the Dashboard to install it and retry.
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.
Claude CLI is ready
Plugins can be installed from this page {cliStatus.installedVersion ? ` using Claude CLI ${cliStatus.installedVersion}` : ''}.
Available in the desktop app only.