/** * 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 { 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, 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, 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 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.
{cliLaunchIssue ? 'Claude CLI was found but failed to start' : 'Claude CLI is not available'}
{cliLaunchIssue ? 'Plugin installs are disabled until Claude CLI passes its startup health check. Open the Dashboard to repair or reinstall it.' : 'Plugin installs are disabled until Claude CLI 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.
Claude CLI is ready
Plugins can be installed from this page {cliStatus.installedVersion ? ` using Claude CLI ${cliStatus.installedVersion}` : ''}.
Available in the desktop app only.