| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780 |
- import { c as _c } from "react/compiler-runtime";
- import figures from 'figures';
- import * as React from 'react';
- import { useCallback, useEffect, useMemo, useState } from 'react';
- import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
- import { Byline } from '../../components/design-system/Byline.js';
- import { SearchBox } from '../../components/SearchBox.js';
- import { useSearchInput } from '../../hooks/useSearchInput.js';
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
- // eslint-disable-next-line custom-rules/prefer-use-keybindings -- useInput needed for raw search mode text input
- import { Box, Text, useInput, useTerminalFocus } from '../../ink.js';
- import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
- import type { LoadedPlugin } from '../../types/plugin.js';
- import { count } from '../../utils/array.js';
- import { openBrowser } from '../../utils/browser.js';
- import { logForDebugging } from '../../utils/debug.js';
- import { errorMessage } from '../../utils/errors.js';
- import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
- import { formatInstallCount, getInstallCounts } from '../../utils/plugins/installCounts.js';
- import { isPluginGloballyInstalled } from '../../utils/plugins/installedPluginsManager.js';
- import { createPluginId, detectEmptyMarketplaceReason, type EmptyMarketplaceReason, formatFailureDetails, formatMarketplaceLoadingErrors, loadMarketplacesWithGracefulDegradation } from '../../utils/plugins/marketplaceHelpers.js';
- import { loadKnownMarketplacesConfig } from '../../utils/plugins/marketplaceManager.js';
- import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js';
- import { installPluginFromMarketplace } from '../../utils/plugins/pluginInstallationHelpers.js';
- import { isPluginBlockedByPolicy } from '../../utils/plugins/pluginPolicy.js';
- import { plural } from '../../utils/stringUtils.js';
- import { truncateToWidth } from '../../utils/truncate.js';
- import { findPluginOptionsTarget, PluginOptionsFlow } from './PluginOptionsFlow.js';
- import { PluginTrustWarning } from './PluginTrustWarning.js';
- import { buildPluginDetailsMenuOptions, extractGitHubRepo, type InstallablePlugin } from './pluginDetailsHelpers.js';
- import type { ViewState as ParentViewState } from './types.js';
- import { usePagination } from './usePagination.js';
- type Props = {
- error: string | null;
- setError: (error: string | null) => void;
- result: string | null;
- setResult: (result: string | null) => void;
- setViewState: (state: ParentViewState) => void;
- onInstallComplete?: () => void | Promise<void>;
- onSearchModeChange?: (isActive: boolean) => void;
- targetPlugin?: string;
- };
- type ViewState = 'plugin-list' | 'plugin-details' | {
- type: 'plugin-options';
- plugin: LoadedPlugin;
- pluginId: string;
- };
- export function DiscoverPlugins({
- error,
- setError,
- result: _result,
- setResult,
- setViewState: setParentViewState,
- onInstallComplete,
- onSearchModeChange,
- targetPlugin
- }: Props): React.ReactNode {
- // View state
- const [viewState, setViewState] = useState<ViewState>('plugin-list');
- const [selectedPlugin, setSelectedPlugin] = useState<InstallablePlugin | null>(null);
- // Data state
- const [availablePlugins, setAvailablePlugins] = useState<InstallablePlugin[]>([]);
- const [loading, setLoading] = useState(true);
- const [installCounts, setInstallCounts] = useState<Map<string, number> | null>(null);
- // Search state
- const [isSearchMode, setIsSearchModeRaw] = useState(false);
- const setIsSearchMode = useCallback((active: boolean) => {
- setIsSearchModeRaw(active);
- onSearchModeChange?.(active);
- }, [onSearchModeChange]);
- const {
- query: searchQuery,
- setQuery: setSearchQuery,
- cursorOffset: searchCursorOffset
- } = useSearchInput({
- isActive: viewState === 'plugin-list' && isSearchMode && !loading,
- onExit: () => {
- setIsSearchMode(false);
- }
- });
- const isTerminalFocused = useTerminalFocus();
- const {
- columns: terminalWidth
- } = useTerminalSize();
- // Filter plugins based on search query
- const filteredPlugins = useMemo(() => {
- if (!searchQuery) return availablePlugins;
- const lowerQuery = searchQuery.toLowerCase();
- return availablePlugins.filter(plugin => plugin.entry.name.toLowerCase().includes(lowerQuery) || plugin.entry.description?.toLowerCase().includes(lowerQuery) || plugin.marketplaceName.toLowerCase().includes(lowerQuery));
- }, [availablePlugins, searchQuery]);
- // Selection state
- const [selectedIndex, setSelectedIndex] = useState(0);
- const [selectedForInstall, setSelectedForInstall] = useState<Set<string>>(new Set());
- const [installingPlugins, setInstallingPlugins] = useState<Set<string>>(new Set());
- // Pagination for plugin list (continuous scrolling)
- const pagination = usePagination<InstallablePlugin>({
- totalItems: filteredPlugins.length,
- selectedIndex
- });
- // Reset selection when search query changes
- useEffect(() => {
- setSelectedIndex(0);
- }, [searchQuery]);
- // Details view state
- const [detailsMenuIndex, setDetailsMenuIndex] = useState(0);
- const [isInstalling, setIsInstalling] = useState(false);
- const [installError, setInstallError] = useState<string | null>(null);
- // Warning state for non-critical errors
- const [warning, setWarning] = useState<string | null>(null);
- // Empty state reason
- const [emptyReason, setEmptyReason] = useState<EmptyMarketplaceReason | null>(null);
- // Load all plugins from all marketplaces
- useEffect(() => {
- async function loadAllPlugins() {
- try {
- const config = await loadKnownMarketplacesConfig();
- // Load marketplaces with graceful degradation
- const {
- marketplaces,
- failures
- } = await loadMarketplacesWithGracefulDegradation(config);
- // Collect all plugins from all marketplaces
- const allPlugins: InstallablePlugin[] = [];
- for (const {
- name,
- data: marketplace
- } of marketplaces) {
- if (marketplace) {
- for (const entry of marketplace.plugins) {
- const pluginId = createPluginId(entry.name, name);
- allPlugins.push({
- entry,
- marketplaceName: name,
- pluginId,
- // Only block when globally installed (user/managed scope).
- // Project/local-scope installs don't block — user may want to
- // promote to user scope so it's available everywhere (gh-29997).
- isInstalled: isPluginGloballyInstalled(pluginId)
- });
- }
- }
- }
- // Filter out installed and policy-blocked plugins
- const uninstalledPlugins = allPlugins.filter(p => !p.isInstalled && !isPluginBlockedByPolicy(p.pluginId));
- // Fetch install counts and sort by popularity
- try {
- const counts = await getInstallCounts();
- setInstallCounts(counts);
- if (counts) {
- // Sort by install count (descending), then alphabetically
- uninstalledPlugins.sort((a_0, b_0) => {
- const countA = counts.get(a_0.pluginId) ?? 0;
- const countB = counts.get(b_0.pluginId) ?? 0;
- if (countA !== countB) return countB - countA;
- return a_0.entry.name.localeCompare(b_0.entry.name);
- });
- } else {
- // No counts available - sort alphabetically
- uninstalledPlugins.sort((a_1, b_1) => a_1.entry.name.localeCompare(b_1.entry.name));
- }
- } catch (error_0) {
- // Log the error, then gracefully degrade to alphabetical sort
- logForDebugging(`Failed to fetch install counts: ${errorMessage(error_0)}`);
- uninstalledPlugins.sort((a, b) => a.entry.name.localeCompare(b.entry.name));
- }
- setAvailablePlugins(uninstalledPlugins);
- // Detect empty reason if no plugins available
- const configuredCount = Object.keys(config).length;
- if (uninstalledPlugins.length === 0) {
- const reason = await detectEmptyMarketplaceReason({
- configuredMarketplaceCount: configuredCount,
- failedMarketplaceCount: failures.length
- });
- setEmptyReason(reason);
- }
- // Handle marketplace loading errors/warnings
- const successCount = count(marketplaces, m => m.data !== null);
- const errorResult = formatMarketplaceLoadingErrors(failures, successCount);
- if (errorResult) {
- if (errorResult.type === 'warning') {
- setWarning(errorResult.message + '. Showing available plugins.');
- } else {
- throw new Error(errorResult.message);
- }
- }
- // Handle targetPlugin - navigate directly to plugin details
- // Search in allPlugins (before filtering) to handle installed plugins gracefully
- if (targetPlugin) {
- const foundPlugin = allPlugins.find(p_0 => p_0.entry.name === targetPlugin);
- if (foundPlugin) {
- if (foundPlugin.isInstalled) {
- setError(`Plugin '${foundPlugin.pluginId}' is already installed. Use '/plugin' to manage existing plugins.`);
- } else {
- setSelectedPlugin(foundPlugin);
- setViewState('plugin-details');
- }
- } else {
- setError(`Plugin "${targetPlugin}" not found in any marketplace`);
- }
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to load plugins');
- } finally {
- setLoading(false);
- }
- }
- void loadAllPlugins();
- }, [setError, targetPlugin]);
- // Install selected plugins
- const installSelectedPlugins = async () => {
- if (selectedForInstall.size === 0) return;
- const pluginsToInstall = availablePlugins.filter(p_1 => selectedForInstall.has(p_1.pluginId));
- setInstallingPlugins(new Set(pluginsToInstall.map(p_2 => p_2.pluginId)));
- let successCount_0 = 0;
- let failureCount = 0;
- const newFailedPlugins: Array<{
- name: string;
- reason: string;
- }> = [];
- for (const plugin_0 of pluginsToInstall) {
- const result = await installPluginFromMarketplace({
- pluginId: plugin_0.pluginId,
- entry: plugin_0.entry,
- marketplaceName: plugin_0.marketplaceName,
- scope: 'user'
- });
- if (result.success) {
- successCount_0++;
- } else {
- failureCount++;
- newFailedPlugins.push({
- name: plugin_0.entry.name,
- reason: (result as { success: false; error: string }).error
- });
- }
- }
- setInstallingPlugins(new Set());
- setSelectedForInstall(new Set());
- clearAllCaches();
- // Handle installation results
- if (failureCount === 0) {
- const message = `✓ Installed ${successCount_0} ${plural(successCount_0, 'plugin')}. ` + `Run /reload-plugins to activate.`;
- setResult(message);
- } else if (successCount_0 === 0) {
- setError(`Failed to install: ${formatFailureDetails(newFailedPlugins, true)}`);
- } else {
- const message_0 = `✓ Installed ${successCount_0} of ${successCount_0 + failureCount} plugins. ` + `Failed: ${formatFailureDetails(newFailedPlugins, false)}. ` + `Run /reload-plugins to activate successfully installed plugins.`;
- setResult(message_0);
- }
- if (successCount_0 > 0) {
- if (onInstallComplete) {
- await onInstallComplete();
- }
- }
- setParentViewState({
- type: 'menu'
- });
- };
- // Install single plugin from details view
- const handleSinglePluginInstall = async (plugin_1: InstallablePlugin, scope: 'user' | 'project' | 'local' = 'user') => {
- setIsInstalling(true);
- setInstallError(null);
- const result_0 = await installPluginFromMarketplace({
- pluginId: plugin_1.pluginId,
- entry: plugin_1.entry,
- marketplaceName: plugin_1.marketplaceName,
- scope
- });
- if (result_0.success) {
- const loaded = await findPluginOptionsTarget(plugin_1.pluginId);
- if (loaded) {
- setIsInstalling(false);
- setViewState({
- type: 'plugin-options',
- plugin: loaded,
- pluginId: plugin_1.pluginId
- });
- return;
- }
- setResult(result_0.message);
- if (onInstallComplete) {
- await onInstallComplete();
- }
- setParentViewState({
- type: 'menu'
- });
- } else {
- setIsInstalling(false);
- setInstallError((result_0 as { success: false; error: string }).error);
- }
- };
- // Handle error state
- useEffect(() => {
- if (error) {
- setResult(error);
- }
- }, [error, setResult]);
- // Escape in plugin-details view - go back to plugin-list
- useKeybinding('confirm:no', () => {
- setViewState('plugin-list');
- setSelectedPlugin(null);
- }, {
- context: 'Confirmation',
- isActive: viewState === 'plugin-details'
- });
- // Escape in plugin-list view (not search mode) - exit to parent menu
- useKeybinding('confirm:no', () => {
- setParentViewState({
- type: 'menu'
- });
- }, {
- context: 'Confirmation',
- isActive: viewState === 'plugin-list' && !isSearchMode
- });
- // Handle entering search mode (non-escape keys)
- useInput((input, _key) => {
- const keyIsNotCtrlOrMeta = !_key.ctrl && !_key.meta;
- if (!isSearchMode) {
- // Enter search mode with '/' or any printable character
- if (input === '/' && keyIsNotCtrlOrMeta) {
- setIsSearchMode(true);
- setSearchQuery('');
- } else if (keyIsNotCtrlOrMeta && input.length > 0 && !/^\s+$/.test(input) &&
- // Don't enter search mode for navigation keys
- input !== 'j' && input !== 'k' && input !== 'i') {
- setIsSearchMode(true);
- setSearchQuery(input);
- }
- }
- }, {
- isActive: viewState === 'plugin-list' && !loading
- });
- // Plugin-list navigation (non-search mode)
- useKeybindings({
- 'select:previous': () => {
- if (selectedIndex === 0) {
- setIsSearchMode(true);
- } else {
- pagination.handleSelectionChange(selectedIndex - 1, setSelectedIndex);
- }
- },
- 'select:next': () => {
- if (selectedIndex < filteredPlugins.length - 1) {
- pagination.handleSelectionChange(selectedIndex + 1, setSelectedIndex);
- }
- },
- 'select:accept': () => {
- if (selectedIndex === filteredPlugins.length && selectedForInstall.size > 0) {
- void installSelectedPlugins();
- } else if (selectedIndex < filteredPlugins.length) {
- const plugin_2 = filteredPlugins[selectedIndex];
- if (plugin_2) {
- if (plugin_2.isInstalled) {
- setParentViewState({
- type: 'manage-plugins',
- targetPlugin: plugin_2.entry.name,
- targetMarketplace: plugin_2.marketplaceName
- });
- } else {
- setSelectedPlugin(plugin_2);
- setViewState('plugin-details');
- setDetailsMenuIndex(0);
- setInstallError(null);
- }
- }
- }
- }
- }, {
- context: 'Select',
- isActive: viewState === 'plugin-list' && !isSearchMode
- });
- useKeybindings({
- 'plugin:toggle': () => {
- if (selectedIndex < filteredPlugins.length) {
- const plugin_3 = filteredPlugins[selectedIndex];
- if (plugin_3 && !plugin_3.isInstalled) {
- const newSelection = new Set(selectedForInstall);
- if (newSelection.has(plugin_3.pluginId)) {
- newSelection.delete(plugin_3.pluginId);
- } else {
- newSelection.add(plugin_3.pluginId);
- }
- setSelectedForInstall(newSelection);
- }
- }
- },
- 'plugin:install': () => {
- if (selectedForInstall.size > 0) {
- void installSelectedPlugins();
- }
- }
- }, {
- context: 'Plugin',
- isActive: viewState === 'plugin-list' && !isSearchMode
- });
- // Plugin-details navigation
- const detailsMenuOptions = React.useMemo(() => {
- if (!selectedPlugin) return [];
- const hasHomepage = selectedPlugin.entry.homepage;
- const githubRepo = extractGitHubRepo(selectedPlugin);
- return buildPluginDetailsMenuOptions(hasHomepage, githubRepo);
- }, [selectedPlugin]);
- useKeybindings({
- 'select:previous': () => {
- if (detailsMenuIndex > 0) {
- setDetailsMenuIndex(detailsMenuIndex - 1);
- }
- },
- 'select:next': () => {
- if (detailsMenuIndex < detailsMenuOptions.length - 1) {
- setDetailsMenuIndex(detailsMenuIndex + 1);
- }
- },
- 'select:accept': () => {
- if (!selectedPlugin) return;
- const action = detailsMenuOptions[detailsMenuIndex]?.action;
- const hasHomepage_0 = selectedPlugin.entry.homepage;
- const githubRepo_0 = extractGitHubRepo(selectedPlugin);
- if (action === 'install-user') {
- void handleSinglePluginInstall(selectedPlugin, 'user');
- } else if (action === 'install-project') {
- void handleSinglePluginInstall(selectedPlugin, 'project');
- } else if (action === 'install-local') {
- void handleSinglePluginInstall(selectedPlugin, 'local');
- } else if (action === 'homepage' && hasHomepage_0) {
- void openBrowser(hasHomepage_0);
- } else if (action === 'github' && githubRepo_0) {
- void openBrowser(`https://github.com/${githubRepo_0}`);
- } else if (action === 'back') {
- setViewState('plugin-list');
- setSelectedPlugin(null);
- }
- }
- }, {
- context: 'Select',
- isActive: viewState === 'plugin-details' && !!selectedPlugin
- });
- if (typeof viewState === 'object' && viewState.type === 'plugin-options') {
- const {
- plugin: plugin_4,
- pluginId: pluginId_0
- } = viewState;
- function finish(msg: string): void {
- setResult(msg);
- if (onInstallComplete) {
- void onInstallComplete();
- }
- setParentViewState({
- type: 'menu'
- });
- }
- return <PluginOptionsFlow plugin={plugin_4} pluginId={pluginId_0} onDone={(outcome, detail) => {
- switch (outcome) {
- case 'configured':
- finish(`✓ Installed and configured ${plugin_4.name}. Run /reload-plugins to apply.`);
- break;
- case 'skipped':
- finish(`✓ Installed ${plugin_4.name}. Run /reload-plugins to apply.`);
- break;
- case 'error':
- finish(`Installed but failed to save config: ${detail}`);
- break;
- }
- }} />;
- }
- // Loading state
- if (loading) {
- return <Text>Loading…</Text>;
- }
- // Error state
- if (error) {
- return <Text color="error">{error}</Text>;
- }
- // Plugin details view
- if (viewState === 'plugin-details' && selectedPlugin) {
- const hasHomepage_1 = selectedPlugin.entry.homepage;
- const githubRepo_1 = extractGitHubRepo(selectedPlugin);
- const menuOptions = buildPluginDetailsMenuOptions(hasHomepage_1, githubRepo_1);
- return <Box flexDirection="column">
- <Box marginBottom={1}>
- <Text bold>Plugin details</Text>
- </Box>
- <Box flexDirection="column" marginBottom={1}>
- <Text bold>{selectedPlugin.entry.name}</Text>
- <Text dimColor>from {selectedPlugin.marketplaceName}</Text>
- {selectedPlugin.entry.version && <Text dimColor>Version: {selectedPlugin.entry.version}</Text>}
- {selectedPlugin.entry.description && <Box marginTop={1}>
- <Text>{selectedPlugin.entry.description}</Text>
- </Box>}
- {selectedPlugin.entry.author && <Box marginTop={1}>
- <Text dimColor>
- By:{' '}
- {typeof selectedPlugin.entry.author === 'string' ? selectedPlugin.entry.author : selectedPlugin.entry.author.name}
- </Text>
- </Box>}
- </Box>
- <PluginTrustWarning />
- {installError && <Box marginBottom={1}>
- <Text color="error">Error: {installError}</Text>
- </Box>}
- <Box flexDirection="column">
- {menuOptions.map((option, index) => <Box key={option.action}>
- {detailsMenuIndex === index && <Text>{'> '}</Text>}
- {detailsMenuIndex !== index && <Text>{' '}</Text>}
- <Text bold={detailsMenuIndex === index}>
- {isInstalling && option.action.startsWith('install-') ? 'Installing…' : option.label}
- </Text>
- </Box>)}
- </Box>
- <Box marginTop={1}>
- <Text dimColor>
- <Byline>
- <ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="select" />
- <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />
- </Byline>
- </Text>
- </Box>
- </Box>;
- }
- // Empty state
- if (availablePlugins.length === 0) {
- return <Box flexDirection="column">
- <Box marginBottom={1}>
- <Text bold>Discover plugins</Text>
- </Box>
- <EmptyStateMessage reason={emptyReason} />
- <Box marginTop={1}>
- <Text dimColor italic>
- Esc to go back
- </Text>
- </Box>
- </Box>;
- }
- // Get visible plugins from pagination
- const visiblePlugins = pagination.getVisibleItems(filteredPlugins);
- return <Box flexDirection="column">
- <Box>
- <Text bold>Discover plugins</Text>
- {pagination.needsPagination && <Text dimColor>
- {' '}
- ({pagination.scrollPosition.current}/
- {pagination.scrollPosition.total})
- </Text>}
- </Box>
- {/* Search box */}
- <Box marginBottom={1}>
- <SearchBox query={searchQuery} isFocused={isSearchMode} isTerminalFocused={isTerminalFocused} width={terminalWidth - 4} cursorOffset={searchCursorOffset} />
- </Box>
- {/* Warning banner */}
- {warning && <Box marginBottom={1}>
- <Text color="warning">
- {figures.warning} {warning}
- </Text>
- </Box>}
- {/* No search results */}
- {filteredPlugins.length === 0 && searchQuery && <Box marginBottom={1}>
- <Text dimColor>No plugins match "{searchQuery}"</Text>
- </Box>}
- {/* Scroll up indicator */}
- {pagination.scrollPosition.canScrollUp && <Box>
- <Text dimColor> {figures.arrowUp} more above</Text>
- </Box>}
- {/* Plugin list - use startIndex in key to force re-render on scroll */}
- {visiblePlugins.map((plugin_5, visibleIndex) => {
- const actualIndex = pagination.toActualIndex(visibleIndex);
- const isSelected = selectedIndex === actualIndex;
- const isSelectedForInstall = selectedForInstall.has(plugin_5.pluginId);
- const isInstallingThis = installingPlugins.has(plugin_5.pluginId);
- const isLast = visibleIndex === visiblePlugins.length - 1;
- return <Box key={`${pagination.startIndex}-${plugin_5.pluginId}`} flexDirection="column" marginBottom={isLast && !error ? 0 : 1}>
- <Box>
- <Text color={isSelected && !isSearchMode ? 'suggestion' : undefined}>
- {isSelected && !isSearchMode ? figures.pointer : ' '}{' '}
- </Text>
- <Text>
- {isInstallingThis ? figures.ellipsis : isSelectedForInstall ? figures.radioOn : figures.radioOff}{' '}
- {plugin_5.entry.name}
- <Text dimColor> · {plugin_5.marketplaceName}</Text>
- {plugin_5.entry.tags?.includes('community-managed') && <Text dimColor> [Community Managed]</Text>}
- {installCounts && plugin_5.marketplaceName === OFFICIAL_MARKETPLACE_NAME && <Text dimColor>
- {' · '}
- {formatInstallCount(installCounts.get(plugin_5.pluginId) ?? 0)}{' '}
- installs
- </Text>}
- </Text>
- </Box>
- {plugin_5.entry.description && <Box marginLeft={4}>
- <Text dimColor>
- {truncateToWidth(plugin_5.entry.description, 60)}
- </Text>
- </Box>}
- </Box>;
- })}
- {/* Scroll down indicator */}
- {pagination.scrollPosition.canScrollDown && <Box>
- <Text dimColor> {figures.arrowDown} more below</Text>
- </Box>}
- {/* Error messages */}
- {error && <Box marginTop={1}>
- <Text color="error">
- {figures.cross} {error}
- </Text>
- </Box>}
- <DiscoverPluginsKeyHint hasSelection={selectedForInstall.size > 0} canToggle={selectedIndex < filteredPlugins.length && !filteredPlugins[selectedIndex]?.isInstalled} />
- </Box>;
- }
- function DiscoverPluginsKeyHint(t0) {
- const $ = _c(10);
- const {
- hasSelection,
- canToggle
- } = t0;
- let t1;
- if ($[0] !== hasSelection) {
- t1 = hasSelection && <ConfigurableShortcutHint action="plugin:install" context="Plugin" fallback="i" description="install" bold={true} />;
- $[0] = hasSelection;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- let t2;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = <Text>type to search</Text>;
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- let t3;
- if ($[3] !== canToggle) {
- t3 = canToggle && <ConfigurableShortcutHint action="plugin:toggle" context="Plugin" fallback="Space" description="toggle" />;
- $[3] = canToggle;
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- let t4;
- let t5;
- if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
- t4 = <ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="details" />;
- t5 = <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />;
- $[5] = t4;
- $[6] = t5;
- } else {
- t4 = $[5];
- t5 = $[6];
- }
- let t6;
- if ($[7] !== t1 || $[8] !== t3) {
- t6 = <Box marginTop={1}><Text dimColor={true} italic={true}><Byline>{t1}{t2}{t3}{t4}{t5}</Byline></Text></Box>;
- $[7] = t1;
- $[8] = t3;
- $[9] = t6;
- } else {
- t6 = $[9];
- }
- return t6;
- }
- /**
- * Context-aware empty state message for the Discover screen
- */
- function EmptyStateMessage(t0) {
- const $ = _c(6);
- const {
- reason
- } = t0;
- switch (reason) {
- case "git-not-installed":
- {
- let t1;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = <><Text dimColor={true}>Git is required to install marketplaces.</Text><Text dimColor={true}>Please install git and restart Claude Code.</Text></>;
- $[0] = t1;
- } else {
- t1 = $[0];
- }
- return t1;
- }
- case "all-blocked-by-policy":
- {
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = <><Text dimColor={true}>Your organization policy does not allow any external marketplaces.</Text><Text dimColor={true}>Contact your administrator.</Text></>;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- return t1;
- }
- case "policy-restricts-sources":
- {
- let t1;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = <><Text dimColor={true}>Your organization restricts which marketplaces can be added.</Text><Text dimColor={true}>Switch to the Marketplaces tab to view allowed sources.</Text></>;
- $[2] = t1;
- } else {
- t1 = $[2];
- }
- return t1;
- }
- case "all-marketplaces-failed":
- {
- let t1;
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = <><Text dimColor={true}>Failed to load marketplace data.</Text><Text dimColor={true}>Check your network connection.</Text></>;
- $[3] = t1;
- } else {
- t1 = $[3];
- }
- return t1;
- }
- case "all-plugins-installed":
- {
- let t1;
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = <><Text dimColor={true}>All available plugins are already installed.</Text><Text dimColor={true}>Check for new plugins later or add more marketplaces.</Text></>;
- $[4] = t1;
- } else {
- t1 = $[4];
- }
- return t1;
- }
- case "no-marketplaces-configured":
- default:
- {
- let t1;
- if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = <><Text dimColor={true}>No plugins available.</Text><Text dimColor={true}>Add a marketplace first using the Marketplaces tab.</Text></>;
- $[5] = t1;
- } else {
- t1 = $[5];
- }
- return t1;
- }
- }
- }
|