Download packages/cli/src/ui/components/views/ExtensionRegistryView.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx
-
curl -L -o ExtensionRegistryView.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/views/ExtensionRegistryView.tsx
10.1 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import type React from 'react'; | |
| import { useMemo, useCallback, useState } 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'; | |
| import { useUIState } from '../../contexts/UIStateContext.js'; | |
| import { ExtensionDetails } from './ExtensionDetails.js'; | |
| export interface ExtensionRegistryViewProps { | |
| onSelect?: ( | |
| extension: RegistryExtension, | |
| requestConsentOverride?: (consent: string) => Promise<boolean>, | |
| ) => void | Promise<void>; | |
| onLink?: ( | |
| extension: RegistryExtension, | |
| requestConsentOverride?: (consent: string) => Promise<boolean>, | |
| ) => void | Promise<void>; | |
| onClose?: () => void; | |
| extensionManager: ExtensionManager; | |
| } | |
| interface ExtensionItem extends GenericListItem { | |
| extension: RegistryExtension; | |
| } | |
| export function ExtensionRegistryView({ | |
| onSelect, | |
| onLink, | |
| onClose, | |
| extensionManager, | |
| }: ExtensionRegistryViewProps): React.JSX.Element { | |
| const config = useConfig(); | |
| const { extensions, loading, error, search } = useExtensionRegistry( | |
| '', | |
| config.getExtensionRegistryURI(), | |
| ); | |
| const { terminalHeight, staticExtraHeight, historyManager } = useUIState(); | |
| const [selectedExtension, setSelectedExtension] = | |
| useState<RegistryExtension | null>(null); | |
| const { extensionsUpdateState, dispatchExtensionStateUpdate } = | |
| useExtensionUpdates( | |
| extensionManager, | |
| historyManager.addItem, | |
| config.getEnableExtensionReloading(), | |
| ); | |
| const [installedExtensions, setInstalledExtensions] = useState(() => | |
| 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) => { | |
| setSelectedExtension(item.extension); | |
| }, []); | |
| const handleBack = useCallback(() => { | |
| setSelectedExtension(null); | |
| }, []); | |
| const handleInstall = useCallback( | |
| async ( | |
| extension: RegistryExtension, | |
| requestConsentOverride?: (consent: string) => Promise<boolean>, | |
| ) => { | |
| await onSelect?.(extension, requestConsentOverride); | |
| // Refresh installed extensions list | |
| setInstalledExtensions(extensionManager.getExtensions()); | |
| // Go back to the search page (list view) | |
| setSelectedExtension(null); | |
| }, | |
| [onSelect, extensionManager], | |
| ); | |
| const handleLink = useCallback( | |
| async ( | |
| extension: RegistryExtension, | |
| requestConsentOverride?: (consent: string) => Promise<boolean>, | |
| ) => { | |
| await onLink?.(extension, requestConsentOverride); | |
| // Refresh installed extensions list | |
| setInstalledExtensions(extensionManager.getExtensions()); | |
| // Go back to the search page (list view) | |
| setSelectedExtension(null); | |
| }, | |
| [onLink, extensionManager], | |
| ); | |
| const handleUpdate = useCallback( | |
| async (extension: RegistryExtension) => { | |
| dispatchExtensionStateUpdate({ | |
| type: 'SCHEDULE_UPDATE', | |
| payload: { | |
| all: false, | |
| names: [extension.extensionName], | |
| onComplete: () => { | |
| // Refresh installed extensions list if needed | |
| setInstalledExtensions(extensionManager.getExtensions()); | |
| }, | |
| }, | |
| }); | |
| }, | |
| [dispatchExtensionStateUpdate, extensionManager], | |
| ); | |
| 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, | |
| ); | |
| return ( | |
| <Box flexDirection="row" width="100%" justifyContent="space-between"> | |
| <Box flexDirection="row" flexShrink={1} minWidth={0}> | |
| <Box width={2} flexShrink={0}> | |
| <Text | |
| color={isActive ? theme.status.success : theme.text.secondary} | |
| > | |
| {isActive ? '● ' : ' '} | |
| </Text> | |
| </Box> | |
| <Box flexShrink={0}> | |
| <Text | |
| bold={isActive} | |
| color={isActive ? theme.status.success : theme.text.primary} | |
| > | |
| {item.label} | |
| </Text> | |
| </Box> | |
| <Box flexShrink={0} marginX={1}> | |
| <Text color={theme.text.secondary}>|</Text> | |
| </Box> | |
| {updateState === ExtensionUpdateState.UPDATE_AVAILABLE ? ( | |
| <Box marginRight={1} flexShrink={0}> | |
| <Text color={theme.status.warning}>[Update available]</Text> | |
| </Box> | |
| ) : updateState === ExtensionUpdateState.UPDATING ? ( | |
| <Box marginRight={1} flexShrink={0}> | |
| <Text color={theme.text.secondary}>[Updating...]</Text> | |
| </Box> | |
| ) : ( | |
| isInstalled && ( | |
| <Box marginRight={1} flexShrink={0}> | |
| <Text color={theme.status.success}>[Installed]</Text> | |
| </Box> | |
| ) | |
| )} | |
| <Box flexShrink={1} minWidth={0}> | |
| <Text color={theme.text.secondary} wrap="truncate-end"> | |
| {item.description} | |
| </Text> | |
| </Box> | |
| </Box> | |
| <Box flexShrink={0} marginLeft={2} width={8} flexDirection="row"> | |
| <Text color={theme.status.warning}>⭐</Text> | |
| <Text | |
| color={isActive ? theme.status.success : theme.text.secondary} | |
| > | |
| {' '} | |
| {item.extension.stars || 0} | |
| </Text> | |
| </Box> | |
| </Box> | |
| ); | |
| }, | |
| [installedExtensions, extensionsUpdateState], | |
| ); | |
| const header = useMemo( | |
| () => ( | |
| <Box flexDirection="row" justifyContent="space-between" width="100%"> | |
| <Box flexShrink={1}> | |
| <Text color={theme.text.secondary} wrap="truncate"> | |
| Browse and search extensions from the registry. | |
| </Text> | |
| </Box> | |
| <Box flexShrink={0} marginLeft={2}> | |
| <Text color={theme.text.secondary}> | |
| {installedExtensions.length && | |
| `${installedExtensions.length} installed`} | |
| </Text> | |
| </Box> | |
| </Box> | |
| ), | |
| [installedExtensions.length], | |
| ); | |
| const footer = useCallback( | |
| ({ | |
| startIndex, | |
| endIndex, | |
| totalVisible, | |
| }: { | |
| startIndex: number; | |
| endIndex: number; | |
| totalVisible: number; | |
| }) => ( | |
| <Text color={theme.text.secondary}> | |
| ({startIndex + 1}-{endIndex}) / {totalVisible} | |
| </Text> | |
| ), | |
| [], | |
| ); | |
| const maxItemsToShow = useMemo(() => { | |
| // SearchableList layout overhead: | |
| // Container paddingY: 0 | |
| // Title (marginBottom 1): 2 | |
| // Search buffer (border 2, marginBottom 1): 4 | |
| // Header (marginBottom 1): 2 | |
| // Footer (marginTop 1): 2 | |
| // List item (marginBottom 1): 2 per item | |
| // Total static height = 2 + 4 + 2 + 2 = 10 | |
| const staticHeight = 10; | |
| const availableTerminalHeight = terminalHeight - staticExtraHeight; | |
| const remainingHeight = Math.max(0, availableTerminalHeight - staticHeight); | |
| const itemHeight = 2; // Each item takes 2 lines (content + marginBottom 1) | |
| // Ensure we show at least a few items and not more than we have | |
| return Math.max(4, Math.floor(remainingHeight / itemHeight)); | |
| }, [terminalHeight, staticExtraHeight]); | |
| if (loading) { | |
| return ( | |
| <Box padding={1}> | |
| <Text color={theme.text.secondary}>Loading extensions...</Text> | |
| </Box> | |
| ); | |
| } | |
| if (error) { | |
| return ( | |
| <Box padding={1} flexDirection="column"> | |
| <Text color={theme.status.error}>Error loading extensions:</Text> | |
| <Text color={theme.text.secondary}>{error}</Text> | |
| </Box> | |
| ); | |
| } | |
| return ( | |
| <> | |
| <Box | |
| display={selectedExtension ? 'none' : 'flex'} | |
| flexDirection="column" | |
| width="100%" | |
| height="100%" | |
| > | |
| <SearchableList<ExtensionItem> | |
| title="Extensions" | |
| items={items} | |
| onSelect={handleSelect} | |
| onClose={onClose || (() => {})} | |
| searchPlaceholder="Search extension gallery" | |
| renderItem={renderItem} | |
| header={header} | |
| footer={footer} | |
| maxItemsToShow={maxItemsToShow} | |
| useSearch={useRegistrySearch} | |
| onSearch={search} | |
| resetSelectionOnItemsChange={true} | |
| isFocused={!selectedExtension} | |
| /> | |
| </Box> | |
| {selectedExtension && ( | |
| <ExtensionDetails | |
| extension={selectedExtension} | |
| onBack={handleBack} | |
| onInstall={async (requestConsentOverride) => { | |
| await handleInstall(selectedExtension, requestConsentOverride); | |
| }} | |
| onLink={async (requestConsentOverride) => { | |
| await handleLink(selectedExtension, requestConsentOverride); | |
| }} | |
| isInstalled={installedExtensions.some( | |
| (e) => e.name === selectedExtension.extensionName, | |
| )} | |
| updateState={extensionsUpdateState.get( | |
| selectedExtension.extensionName, | |
| )} | |
| onUpdate={async () => { | |
| await handleUpdate(selectedExtension); | |
| }} | |
| /> | |
| )} | |
| </> | |
| ); | |
| } | |