/** * 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 { AlertTriangle, BookOpen, Info, Key, Plus, Puzzle, RefreshCw, Server } from 'lucide-react'; 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 = useStore((s) => s.fetchPluginCatalog); const fetchApiKeys = useStore((s) => s.fetchApiKeys); const fetchSkillsCatalog = useStore((s) => s.fetchSkillsCatalog); const mcpBrowse = useStore((s) => s.mcpBrowse); const mcpFetchInstalled = useStore((s) => s.mcpFetchInstalled); const pluginCatalogLoading = useStore((s) => s.pluginCatalogLoading); const mcpBrowseLoading = useStore((s) => s.mcpBrowseLoading); const skillsLoading = useStore((s) => s.skillsLoading); const cliStatus = useStore((s) => s.cliStatus); const cliInstalled = cliStatus?.installed ?? true; // assume installed until checked const hasOngoingSessions = useStore((s) => s.sessions.some((sess) => sess.isOngoing)); const projects = useStore((s) => s.projects); 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]); // 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; // Browser mode guard if (!api.plugins && !api.mcpRegistry && !api.skills) { return (

Extensions

Available in the desktop app only.

); } return (
{/* Header */}

Extensions

Refresh catalog
{/* Sub-tabs */}
{/* CLI not installed warning */} {!cliInstalled && (
Claude CLI is required to install or uninstall extensions. Install it from Settings.
)} {/* Active sessions warning */} {hasOngoingSessions && (
Running sessions won't pick up extension changes until restarted.
)} tabState.setActiveSubTab(v as 'plugins' | 'mcp-servers' | 'skills' | 'api-keys') } >
{subTabs.map((subTab) => ( ))} {tabState.activeSubTab === 'mcp-servers' && ( )}
{/* Custom MCP server dialog (lifted to store view level) */} setCustomMcpDialogOpen(false)} />
); };