/** * 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 { mergeCodexProviderStatusWithSnapshot, useCodexAccountSnapshot, } from '@features/codex-account/renderer'; import { useAppTranslation } from '@features/localization/renderer'; import { api, isElectronMode } from '@renderer/api'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; 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 { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice'; import { formatCliExtensionCapabilityStatus, getVisibleMultimodelProviders, isMultimodelRuntimeStatus, } from '@renderer/utils/multimodelProviderVisibility'; import { resolveProjectPathById } from '@renderer/utils/projectLookup'; import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus'; import { getRuntimeDisplayName } from '@renderer/utils/runtimeDisplayName'; import { getExtensionActionDisableReason } from '@shared/utils/extensionNormalizers'; import { getCliProviderExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities'; import { AlertTriangle, BookOpen, Info, Key, Loader2, 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'; import type { CliProviderStatus } from '@shared/types'; const ProviderCapabilityCardSkeleton = ({ providerId, displayName, }: { providerId: 'anthropic' | 'codex' | 'gemini' | 'opencode'; displayName: string; }): React.JSX.Element => { const { t } = useAppTranslation('extensions'); return (

{displayName}

{t('store.provider.checkingStatus')}
{t('store.provider.loading')}
{Array.from({ length: 3 }, (_, index) => ( ))}
); }; function isProviderCapabilityCardLoading( provider: CliProviderStatus, providerLoading: boolean ): boolean { return ( providerLoading || (!provider.authenticated && provider.statusMessage === 'Checking...' && provider.models.length === 0 && provider.backend == null) ); } function isCodexSnapshotPending( provider: CliProviderStatus, codexSnapshotPending: boolean ): boolean { return provider.providerId === 'codex' && codexSnapshotPending; } export const ExtensionStoreView = (): React.JSX.Element => { const { t } = useAppTranslation('extensions'); const isElectron = useMemo(() => isElectronMode(), []); const tabId = useTabIdOptional(); const { fetchPluginCatalog, bootstrapCliStatus, fetchCliStatus, fetchApiKeys, fetchSkillsCatalog, mcpBrowse, mcpFetchInstalled, apiKeysLoading, pluginCatalogLoading, mcpBrowseLoading, skillsLoading, cliStatus, cliStatusLoading, cliProviderStatusLoading, appConfig, openDashboard, sessions, projects, repositoryGroups, } = useStore( useShallow((s) => ({ fetchPluginCatalog: s.fetchPluginCatalog, bootstrapCliStatus: s.bootstrapCliStatus, 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, cliProviderStatusLoading: s.cliProviderStatusLoading, appConfig: s.appConfig, openDashboard: s.openDashboard, sessions: s.sessions, projects: s.projects, repositoryGroups: s.repositoryGroups, })) ); const multimodelEnabled = appConfig?.general?.multimodelEnabled ?? true; const loadingCliStatus = useMemo( () => !cliStatus && cliStatusLoading && multimodelEnabled ? createLoadingMultimodelCliStatus() : cliStatus, [cliStatus, cliStatusLoading, multimodelEnabled] ); const codexAccount = useCodexAccountSnapshot({ enabled: isElectron && multimodelEnabled && loadingCliStatus?.flavor === 'agent_teams_orchestrator' && Boolean( loadingCliStatus?.providers.some( (provider: CliProviderStatus) => provider.providerId === 'codex' ) ), includeRateLimits: true, }); const codexSnapshotPending = codexAccount.loading && Boolean( loadingCliStatus?.providers.some( (provider: CliProviderStatus) => provider.providerId === 'codex' ) ) && !codexAccount.snapshot; const effectiveCliStatus = useMemo( () => loadingCliStatus ? { ...loadingCliStatus, providers: loadingCliStatus.providers.map((provider: CliProviderStatus) => provider.providerId === 'codex' ? mergeCodexProviderStatusWithSnapshot(provider, codexAccount.snapshot) : provider ), } : loadingCliStatus, [loadingCliStatus, codexAccount.snapshot] ); const effectiveCliStatusLoading = cliStatusLoading && effectiveCliStatus === null; const runtimeDisplayName = getRuntimeDisplayName(effectiveCliStatus, multimodelEnabled); const cliInstalled = effectiveCliStatus?.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: t('store.tabs.plugins.label'), icon: Puzzle, description: t('store.tabs.plugins.description'), }, { value: 'mcp-servers' as const, label: t('store.tabs.mcpServers.label'), icon: Server, description: t('store.tabs.mcpServers.description'), }, { value: 'skills' as const, label: t('store.tabs.skills.label'), icon: BookOpen, description: t('store.tabs.skills.description'), }, { value: 'api-keys' as const, label: t('store.tabs.apiKeys.label'), icon: Key, description: t('store.tabs.apiKeys.description'), }, ], [t] ); // Fetch plugin catalog on mount useEffect(() => { void fetchPluginCatalog(projectPath ?? undefined); }, [fetchPluginCatalog, projectPath]); useEffect(() => { const cliStatusMatchesCurrentMode = cliStatus && (multimodelEnabled ? cliStatus.flavor === 'agent_teams_orchestrator' : cliStatus.flavor !== 'agent_teams_orchestrator'); if (cliStatusLoading || cliStatusMatchesCurrentMode) { return; } void refreshCliStatusForCurrentMode({ multimodelEnabled, providerStatusMode: 'defer', bootstrapCliStatus, fetchCliStatus, }); }, [bootstrapCliStatus, cliStatus, cliStatusLoading, fetchCliStatus, multimodelEnabled]); // Fetch MCP installed state on mount useEffect(() => { void mcpFetchInstalled(projectPath ?? undefined); }, [mcpFetchInstalled, projectPath]); // 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 refreshCliStatusForCurrentMode({ multimodelEnabled, bootstrapCliStatus, fetchCliStatus, }); if (tabState.activeSubTab === 'api-keys') { void fetchApiKeys(); } void fetchPluginCatalog(projectPath ?? undefined, true); void mcpBrowse(); // re-fetch first page void mcpFetchInstalled(projectPath ?? undefined); void fetchSkillsCatalog(projectPath ?? undefined); }, [ bootstrapCliStatus, fetchApiKeys, fetchCliStatus, fetchPluginCatalog, fetchSkillsCatalog, multimodelEnabled, mcpBrowse, mcpFetchInstalled, projectPath, tabState.activeSubTab, ]); const isRefreshing = effectiveCliStatusLoading || apiKeysLoading || pluginCatalogLoading || mcpBrowseLoading || skillsLoading; const mcpMutationDisableReason = useMemo( () => getExtensionActionDisableReason({ isInstalled: false, cliStatus: effectiveCliStatus, cliStatusLoading: effectiveCliStatusLoading, section: 'mcp', }), [effectiveCliStatus, effectiveCliStatusLoading] ); const cliStatusBanner = useMemo(() => { const providers = effectiveCliStatus?.providers ?? []; const visibleProviders = getVisibleMultimodelProviders(providers); const isMultimodel = isMultimodelRuntimeStatus(effectiveCliStatus); const shouldShowMultimodelProviderCards = isMultimodel && visibleProviders.length > 0 && effectiveCliStatus !== null; if ( (effectiveCliStatusLoading || effectiveCliStatus === null) && !shouldShowMultimodelProviderCards ) { return (

{t('store.runtime.checkingAvailabilityTitle')}

{t('store.runtime.checkingAvailabilityDescription')}

); } if (!effectiveCliStatus.installed) { const cliLaunchIssue = Boolean( effectiveCliStatus.binaryPath && effectiveCliStatus.launchError ); return (

{cliLaunchIssue ? t('store.runtime.failedToStartTitle') : t('store.runtime.notAvailableTitle')}

{cliLaunchIssue ? t('store.runtime.failedToStartDescription') : t('store.runtime.notAvailableDescription')}

{cliLaunchIssue && effectiveCliStatus.launchError && (

{effectiveCliStatus.launchError}

)}
); } if (!isMultimodel && !effectiveCliStatus.authLoggedIn) { return (

{t('store.runtime.needsSignInTitle', { runtime: runtimeDisplayName })}

{t('store.runtime.needsSignInDescription', { runtime: runtimeDisplayName, version: effectiveCliStatus.installedVersion ? ` (${effectiveCliStatus.installedVersion})` : '', })}

); } if (isMultimodel) { return (

{t('store.runtime.multimodelCapabilitiesTitle')}

{t('store.runtime.multimodelCapabilitiesDescription')}

{visibleProviders.length > 0 && (
{visibleProviders.map((provider) => { const providerLoading = cliProviderStatusLoading[provider.providerId] === true; if ( isProviderCapabilityCardLoading(provider, providerLoading) || isCodexSnapshotPending(provider, codexSnapshotPending) ) { return ( ); } const statusTone = provider.authenticated ? 'border-emerald-500/30 bg-emerald-500/5 text-emerald-300' : provider.supported ? 'border-amber-500/30 bg-amber-500/5 text-amber-300' : 'border-border bg-surface-raised text-text-muted'; const statusLabel = provider.authenticated ? 'Connected' : provider.supported ? t('store.provider.needsSetup') : t('store.provider.unsupported'); const finalStatusLabel = provider.authenticated ? t('store.provider.connected') : statusLabel; const extensionCapabilities = getCliProviderExtensionCapabilities(provider); const pluginStatus = extensionCapabilities.plugins.status; return (

{provider.displayName}

{provider.statusMessage ?? provider.backend?.label ?? t('store.provider.readyToConfigure')}

{finalStatusLabel}
{t('store.capabilities.plugins', { status: formatCliExtensionCapabilityStatus(pluginStatus), })} {t('store.capabilities.mcp', { status: formatCliExtensionCapabilityStatus( extensionCapabilities.mcp.status ), })} {t('store.capabilities.skills', { status: extensionCapabilities.skills.ownership, })}
); })}
)}
); } return (

{t('store.runtime.readyTitle', { runtime: runtimeDisplayName })}

{t('store.runtime.readyDescription', { runtime: runtimeDisplayName, versionSuffix: effectiveCliStatus.installedVersion ? ` using ${runtimeDisplayName} ${effectiveCliStatus.installedVersion}` : '', })}

); }, [ cliProviderStatusLoading, codexSnapshotPending, effectiveCliStatus, effectiveCliStatusLoading, openDashboard, runtimeDisplayName, t, ]); // Browser mode guard if (!api.plugins && !api.mcpRegistry && !api.skills) { return (

{t('store.title')}

{t('store.desktopOnly')}

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

{t('store.title')}

{t('store.actions.refreshCatalog')}
{/* Sub-tabs */}
{/* CLI not installed warning */} {!cliInstalled && (
{t('store.runtime.requiredForMutations')}
)} {/* Active sessions warning */} {hasOngoingSessions && (
{t('store.sessionsRestartWarning')}
)} tabState.setActiveSubTab(v as 'plugins' | 'mcp-servers' | 'skills' | 'api-keys') } >
{subTabs.map((subTab) => ( ))} {tabState.activeSubTab === 'mcp-servers' && ( {mcpMutationDisableReason && ( {mcpMutationDisableReason} )} )}
{/* Custom MCP server dialog (lifted to store view level) */} setCustomMcpDialogOpen(false)} projectPath={projectPath} cliStatus={effectiveCliStatus} cliStatusLoading={effectiveCliStatusLoading} />
); };