/** * 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, useState } from 'react'; import { api } from '@renderer/api'; import { Button } from '@renderer/components/ui/button'; import { useExtensionsTabState } from '@renderer/hooks/useExtensionsTabState'; import { useStore } from '@renderer/store'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@renderer/components/ui/tooltip'; import { AlertTriangle, 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'; export const ExtensionStoreView = (): React.JSX.Element => { const fetchPluginCatalog = useStore((s) => s.fetchPluginCatalog); const fetchApiKeys = useStore((s) => s.fetchApiKeys); 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 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 tabState = useExtensionsTabState(); const [customMcpDialogOpen, setCustomMcpDialogOpen] = useState(false); // Fetch plugin catalog on mount useEffect(() => { void fetchPluginCatalog(); }, [fetchPluginCatalog]); // Fetch MCP installed state on mount useEffect(() => { void mcpFetchInstalled(); }, [mcpFetchInstalled]); // Fetch API keys on mount useEffect(() => { void fetchApiKeys(); }, [fetchApiKeys]); // Refresh all data (plugins + MCP browse + installed) const handleRefresh = useCallback(() => { void fetchPluginCatalog(undefined, true); void mcpBrowse(); // re-fetch first page void mcpFetchInstalled(); }, [fetchPluginCatalog, mcpBrowse, mcpFetchInstalled]); const isRefreshing = pluginCatalogLoading || mcpBrowseLoading; // Browser mode guard if (!api.plugins && !api.mcpRegistry) { 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' | 'api-keys') } >
Plugins MCP Servers API Keys {tabState.activeSubTab === 'mcp-servers' && ( )}
{/* Custom MCP server dialog (lifted to store view level) */} setCustomMcpDialogOpen(false)} />
); };