/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useMemo, useCallback } from 'react'; import { Box, Text } from 'ink'; import type { RegistryExtension } from '../../../config/extensionRegistryClient.js'; import { SearchableList, type GenericListItem, } from '../shared/SearchableList.js'; import { theme } from '../../semantic-colors.js'; import { useExtensionRegistry } from '../../hooks/useExtensionRegistry.js'; import { ExtensionUpdateState } from '../../state/extensions.js'; import { useExtensionUpdates } from '../../hooks/useExtensionUpdates.js'; import { useConfig } from '../../contexts/ConfigContext.js'; import type { ExtensionManager } from '../../../config/extension-manager.js'; import { useRegistrySearch } from '../../hooks/useRegistrySearch.js'; interface ExtensionRegistryViewProps { onSelect?: (extension: RegistryExtension) => void; onClose?: () => void; extensionManager: ExtensionManager; } interface ExtensionItem extends GenericListItem { extension: RegistryExtension; } export function ExtensionRegistryView({ onSelect, onClose, extensionManager, }: ExtensionRegistryViewProps): React.JSX.Element { const { extensions, loading, error, search } = useExtensionRegistry(); const config = useConfig(); const { extensionsUpdateState } = useExtensionUpdates( extensionManager, () => 0, config.getEnableExtensionReloading(), ); const installedExtensions = extensionManager.getExtensions(); const items: ExtensionItem[] = useMemo( () => extensions.map((ext) => ({ key: ext.id, label: ext.extensionName, description: ext.extensionDescription || ext.repoDescription, extension: ext, })), [extensions], ); const handleSelect = useCallback( (item: ExtensionItem) => { onSelect?.(item.extension); }, [onSelect], ); const renderItem = useCallback( (item: ExtensionItem, isActive: boolean, _labelWidth: number) => { const isInstalled = installedExtensions.some( (e) => e.name === item.extension.extensionName, ); const updateState = extensionsUpdateState.get( item.extension.extensionName, ); const hasUpdate = updateState === ExtensionUpdateState.UPDATE_AVAILABLE; return ( {isActive ? '> ' : ' '} {item.label} | {isInstalled && ( [Installed] )} {hasUpdate && ( [Update available] )} {item.description} {' '} {item.extension.stars || 0} ); }, [installedExtensions, extensionsUpdateState], ); const header = useMemo( () => ( Browse and search extensions from the registry. {installedExtensions.length && `${installedExtensions.length} installed`} ), [installedExtensions.length], ); const footer = useCallback( ({ startIndex, endIndex, totalVisible, }: { startIndex: number; endIndex: number; totalVisible: number; }) => ( ({startIndex + 1}-{endIndex}) / {totalVisible} ), [], ); if (loading) { return ( Loading extensions... ); } if (error) { return ( Error loading extensions: {error} ); } return ( title="Extensions" items={items} onSelect={handleSelect} onClose={onClose || (() => {})} searchPlaceholder="Search extension gallery" renderItem={renderItem} header={header} footer={footer} maxItemsToShow={8} useSearch={useRegistrySearch} onSearch={search} resetSelectionOnItemsChange={true} /> ); }