/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useState } from 'react'; import { Box, Text } from 'ink'; import type { RegistryExtension } from '../../../config/extensionRegistryClient.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { Command } from '../../key/keyMatchers.js'; import { useKeyMatchers } from '../../hooks/useKeyMatchers.js'; import { theme } from '../../semantic-colors.js'; import { ExtensionUpdateState } from '../../state/extensions.js'; export interface ExtensionDetailsProps { extension: RegistryExtension; onBack: () => void; onInstall: ( requestConsentOverride: (consent: string) => Promise, ) => void | Promise; onLink: ( requestConsentOverride: (consent: string) => Promise, ) => void | Promise; isInstalled: boolean; updateState?: ExtensionUpdateState; onUpdate?: () => void | Promise; } export function ExtensionDetails({ extension, onBack, onInstall, onLink, isInstalled, updateState, onUpdate, }: ExtensionDetailsProps): React.JSX.Element { const keyMatchers = useKeyMatchers(); const [consentRequest, setConsentRequest] = useState<{ prompt: string; resolve: (value: boolean) => void; } | null>(null); const [isInstalling, setIsInstalling] = useState(false); const isLinkable = !extension.url.startsWith('http') && !extension.url.startsWith('git@') && !extension.url.startsWith('sso://'); useKeypress( (key) => { if (consentRequest) { if (keyMatchers[Command.ESCAPE](key)) { consentRequest.resolve(false); setConsentRequest(null); setIsInstalling(false); return true; } if (keyMatchers[Command.RETURN](key)) { consentRequest.resolve(true); setConsentRequest(null); return true; } return false; } if (keyMatchers[Command.ESCAPE](key)) { onBack(); return true; } if (keyMatchers[Command.RETURN](key) && !isInstalled && !isInstalling) { setIsInstalling(true); void onInstall( (prompt: string) => new Promise((resolve) => { setConsentRequest({ prompt, resolve }); }), ); return true; } if ( keyMatchers[Command.LINK_EXTENSION](key) && isLinkable && !isInstalled && !isInstalling ) { setIsInstalling(true); void onLink( (prompt: string) => new Promise((resolve) => { setConsentRequest({ prompt, resolve }); }), ); return true; } if ( keyMatchers[Command.UPDATE_EXTENSION](key) && updateState === ExtensionUpdateState.UPDATE_AVAILABLE && !isInstalling ) { void onUpdate?.(); return true; } return false; }, { isActive: true, priority: true }, ); if (consentRequest) { return ( {consentRequest.prompt} [Esc] Cancel [Enter] Accept ); } if (isInstalling) { return ( Installing {extension.extensionName}... ); } return ( {/* Header Row */} {'>'} Extensions {'>'}{' '} {extension.extensionName} {updateState === ExtensionUpdateState.UPDATE_AVAILABLE && ( [I] Update )} {updateState === ExtensionUpdateState.UPDATING && ( [Updating...] )} {extension.extensionVersion ? `v${extension.extensionVersion}` : ''}{' '} |{' '} ⭐ {String(extension.stars || 0)} |{' '} {extension.isGoogleOwned && ( [G] )} {extension.fullName} {/* Description */} {extension.extensionDescription || extension.repoDescription} {/* Features List */} {[ extension.hasMCP && { label: 'MCP', color: theme.text.primary }, extension.hasContext && { label: 'Context file', color: theme.status.error, }, extension.hasHooks && { label: 'Hooks', color: theme.status.warning }, extension.hasSkills && { label: 'Skills', color: theme.status.success, }, extension.hasCustomCommands && { label: 'Commands', color: theme.text.primary, }, ] .filter((f): f is { label: string; color: string } => !!f) .map((feature, index, array) => ( {feature.label} {index < array.length - 1 && ( | )} ))} {/* Details about MCP / Context */} {extension.hasMCP && ( This extension will run the following MCP servers: * {extension.extensionName} (local) )} {extension.hasContext && ( This extension will append info to your gemini.md context using gemini.md )} {/* Spacer to push warning to bottom */} {/* Warning Box */} {!isInstalled && ( The extension you are about to install may have been created by a third-party developer and sourced{'\n'} from a public repository. Google does not vet, endorse, or guarantee the functionality or security{'\n'} of extensions. Please carefully inspect any extension and its source code before installing to{'\n'} understand the permissions it requires and the actions it may perform. [{'Enter'}] Install {isLinkable && [L] Link} )} {isInstalled && updateState !== ExtensionUpdateState.UPDATING && ( Already Installed )} ); }