mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 02:24:09 -07:00
Add initial implementation of /extensions explore command (#19029)
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @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 (
|
||||
<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>
|
||||
{isInstalled && (
|
||||
<Box marginRight={1} flexShrink={0}>
|
||||
<Text color={theme.status.success}>[Installed]</Text>
|
||||
</Box>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
<Box marginRight={1} flexShrink={0}>
|
||||
<Text color={theme.status.warning}>[Update available]</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>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
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 (
|
||||
<SearchableList<ExtensionItem>
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user