import { useCallback, useEffect, useMemo, useRef, useState, type Ref, } from 'react'; import { AlertCircleIcon, ArrowLeftIcon, BotIcon, BoxIcon, CommandIcon, EllipsisVerticalIcon, FileTextIcon, PackageIcon, PlusIcon, RefreshCwIcon, SearchIcon, ServerIcon, SparklesIcon, } from 'lucide-react'; import { DaemonHttpError, type DaemonExtensionEntry, type DaemonExtensionUpdateState, type ExtensionActivationState, type ExtensionInteractionResponse, type ExtensionPendingInteraction, } from '@qwen-code/sdk/daemon'; import { useConnection, useWorkspace, useWorkspaceActions, useWorkspaceEventSignals, } from '@qwen-code/webui/daemon-react-sdk'; import { useI18n } from '../../i18n'; import { trimDialogLabel } from '../../utils/dialogLabels'; import styles from './ExtensionsManagerPage.module.css'; import { filterExtensions, preserveSelectedExtensionName, } from './extensions-manager-logic'; import { Alert, AlertDescription } from '../ui/alert'; import { ManagementNotice, type ManagementNoticeTone, } from '../ui/management-notice'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '../ui/alert-dialog'; import { Badge } from '../ui/badge'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '../ui/breadcrumb'; import { Button } from '../ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '../ui/card'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../ui/dropdown-menu'; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '../ui/empty'; import { Input } from '../ui/input'; import { RadioGroup, RadioGroupItem } from '../ui/radio-group'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../ui/select'; import { Separator } from '../ui/separator'; import { Spinner } from '../ui/spinner'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '../ui/tooltip'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import type { EmbeddedManagerPage } from '../plugins/manager-page'; type Scope = 'user' | 'workspace'; type ManagedExtensionEntry = DaemonExtensionEntry & { defaultActivation?: ExtensionActivationState; workspaceActivation?: 'inherit' | ExtensionActivationState; }; type T = ReturnType['t']; type PendingInteractionState = { operationId: string; interaction: ExtensionPendingInteraction; owner: 'install' | 'mutation'; }; const UPDATE_AVAILABLE: DaemonExtensionUpdateState = 'update available'; interface ExtensionsManagerPageProps { onClose: () => void; initialFocusRef?: Ref; embedded?: EmbeddedManagerPage; } function extensionTitle(extension: DaemonExtensionEntry): string { return extension.displayName || extension.name; } function extensionIsActive(extension: ManagedExtensionEntry): boolean { if ( extension.workspaceActivation && extension.workspaceActivation !== 'inherit' ) { return extension.workspaceActivation === 'enabled'; } return extension.defaultActivation ? extension.defaultActivation === 'enabled' : extension.isActive; } function statusLabel(extension: ManagedExtensionEntry, t: T): string { return extensionIsActive(extension) ? t('extensions.manage.status.enabled') : t('extensions.manage.status.disabled'); } function updateLabel( state: DaemonExtensionUpdateState | undefined, t: T, ): string { switch (state) { case 'update available': return t('extensions.manage.updateAvailable'); case 'up to date': return t('extensions.manage.upToDate'); case 'not updatable': return t('extensions.manage.notUpdatable'); case 'checking for updates': return t('extensions.manage.checkingUpdates'); case 'updating': return t('extensions.manage.updating'); case 'updated': return t('extensions.manage.updateComplete'); case 'updated with warnings': return t('extensions.manage.updatedWithWarnings'); case 'updated, needs restart': return t('extensions.manage.restartRequired'); case 'error': return t('extensions.manage.updateError'); case 'unknown': case undefined: return t('extensions.manage.unknownUpdate'); } } function mutationMessage(operation: string, name: string, t: T): string { switch (operation) { case 'enable': return t('extensions.manage.enabling', { name }); case 'disable': return t('extensions.manage.disabling', { name }); case 'uninstall': return t('extensions.manage.uninstalling', { name }); case 'update': return t('extensions.manage.updatingExtension', { name }); default: return t('extensions.manage.queued', { name }); } } function mutationSuccessMessage(operation: string, name: string, t: T): string { switch (operation) { case 'enable': return t('extensions.manage.enabled', { name }); case 'disable': return t('extensions.manage.disabled', { name }); case 'uninstall': return t('extensions.manage.uninstalled', { name }); case 'update': return t('extensions.manage.updated', { name }); default: return t('extensions.manage.queued', { name }); } } function DetailField({ label, value }: { label: string; value: string }) { return (
{trimDialogLabel(label)}
{value}
); } function CapabilityList({ items, empty, icon: Icon, }: { items: string[]; empty: string; icon: typeof CommandIcon; }) { if (!items.length) { return ( {empty} ); } return ( {items.map((item, index) => (
{index > 0 ? : null}
{item}
))}
); } function ExtensionInteractionDialog({ pendingInteraction, submitting, selectedPlugin, interactionValue, setSelectedPlugin, setInteractionValue, submit, t, }: { pendingInteraction: PendingInteractionState | null; submitting: boolean; selectedPlugin: string; interactionValue: string; setSelectedPlugin: (value: string) => void; setInteractionValue: (value: string) => void; submit: (response: ExtensionInteractionResponse) => void; t: T; }) { return ( { if (!open && pendingInteraction && !submitting) { submit({ cancelled: true }); } }} > {pendingInteraction?.interaction.kind === 'setting' ? pendingInteraction.interaction.setting.name : t('extensions.manage.selectExtension')} {pendingInteraction?.interaction.kind === 'marketplace_plugin' ? t('extensions.manage.installSelectPluginDescription', { marketplace: pendingInteraction.interaction.marketplace.name, }) : pendingInteraction?.interaction.setting.description} {pendingInteraction?.interaction.kind === 'marketplace_plugin' ? ( {pendingInteraction.interaction.plugins.map((plugin) => { const id = `marketplace-plugin-${plugin.name}`; return (
); })}
) : null} {pendingInteraction?.interaction.kind === 'marketplace_plugin' ? ( ) : pendingInteraction?.interaction.kind === 'setting' ? ( <> setInteractionValue(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && !submitting) { event.preventDefault(); submit({ value: interactionValue }); } }} /> ) : null}
); } export function ExtensionsManagerPage({ onClose, initialFocusRef, embedded, }: ExtensionsManagerPageProps) { const { t } = useI18n(); const connection = useConnection(); const workspace = useWorkspace(); const actions = useWorkspaceActions(); const signals = useWorkspaceEventSignals(); const [extensions, setExtensions] = useState([]); const [selectedName, setSelectedName] = useState(null); const [query, setQuery] = useState(''); const [updateStates, setUpdateStates] = useState< Record >({}); const [loading, setLoading] = useState(false); const [checkingName, setCheckingName] = useState(null); const [busyName, setBusyName] = useState(null); const [message, setMessage] = useState(null); const [messageTone, setMessageTone] = useState('info'); const [messageOwner, setMessageOwner] = useState(null); const [recoveryError, setRecoveryError] = useState(null); const [actionsOpen, setActionsOpen] = useState(false); const [uninstallName, setUninstallName] = useState(null); const [installOpen, setInstallOpen] = useState(false); const [installSource, setInstallSource] = useState(''); const [installing, setInstalling] = useState(false); const [pendingInstall, setPendingInstall] = useState<{ operationId: string; source: string; } | null>(null); const [pendingInteraction, setPendingInteraction] = useState(null); const interactionIdRef = useRef(null); const [interactionValue, setInteractionValue] = useState(''); const [selectedPlugin, setSelectedPlugin] = useState(''); const [submittingInteraction, setSubmittingInteraction] = useState(false); const [operationsRecovered, setOperationsRecovered] = useState(false); const mutationInFlightRef = useRef(false); const uninstallInFlightNameRef = useRef(null); const interactionOperationIdRef = useRef(null); const cardRefs = useRef(new Map()); const returnFocusNameRef = useRef(null); const [pendingMutation, setPendingMutation] = useState<{ operationId: string; name: string; operation?: string; } | null>(null); const clearInteraction = useCallback((operationId?: string) => { if (operationId && interactionOperationIdRef.current !== operationId) { return; } interactionOperationIdRef.current = null; interactionIdRef.current = null; setPendingInteraction(null); setInteractionValue(''); setSelectedPlugin(''); }, []); const showInteraction = useCallback( ( operationId: string, interaction: ExtensionPendingInteraction, owner: 'install' | 'mutation', ) => { if (interactionIdRef.current !== interaction.id) { setInteractionValue(''); setSelectedPlugin(''); interactionIdRef.current = interaction.id; } interactionOperationIdRef.current = operationId; setPendingInteraction({ operationId, interaction, owner }); }, [], ); const load = useCallback( (preserveMessage = false) => { setLoading(true); const projection = workspace.workspaceCwd ? workspace.client .workspaceByCwd(workspace.workspaceCwd) .workspaceExtensions() .catch(() => null) : Promise.resolve(null); return Promise.all([actions.loadExtensionsStatus(), projection]) .then(([status, activation]) => { const activations = new Map( (activation?.extensions ?? []).map((entry) => [ entry.extensionId, entry, ]), ); const nextExtensions = (status.extensions ?? []).map((extension) => { const entry = activations.get(extension.id); return entry ? { ...extension, defaultActivation: entry.defaultActivation, workspaceActivation: entry.workspaceActivation ?? 'inherit', } : extension; }); setExtensions((current) => { const uninstallName = uninstallInFlightNameRef.current; if ( !uninstallName || nextExtensions.some( (extension) => extension.name === uninstallName, ) ) { return nextExtensions; } const uninstallingExtension = current.find( (extension) => extension.name === uninstallName, ); return uninstallingExtension ? [...nextExtensions, uninstallingExtension] : nextExtensions; }); if (!preserveMessage) { setMessageOwner(null); setMessageTone(status.errors?.[0] ? 'error' : 'info'); setMessage(status.errors?.[0]?.error ?? null); } setSelectedName((name) => name && uninstallInFlightNameRef.current === name ? name : preserveSelectedExtensionName(name, nextExtensions), ); }) .catch((error: unknown) => { if (!preserveMessage) { setMessageOwner(null); setMessageTone('error'); setMessage(error instanceof Error ? error.message : String(error)); } }) .finally(() => setLoading(false)); }, [actions, workspace.client, workspace.workspaceCwd], ); useEffect(() => { void load(); }, [load]); useEffect(() => { let cancelled = false; let timer: ReturnType | undefined; let retryDelay = 2000; const recover = async () => { try { const { operations } = await actions.activeExtensionOperations(); if (cancelled) return; const activeInstall = operations .filter((operation) => operation.operation === 'install') .sort((left, right) => right.createdAt - left.createdAt)[0]; if (activeInstall) { setPendingInstall( (current) => current ?? { operationId: activeInstall.operationId, source: activeInstall.source ?? activeInstall.name ?? 'extension', }, ); } const activeMutation = operations.find( (operation) => operation.operation !== 'install', ); if (activeMutation) { mutationInFlightRef.current = true; if (activeMutation.operation === 'uninstall') { uninstallInFlightNameRef.current = activeMutation.name ?? 'extension'; } setPendingMutation( (current) => current ?? { operationId: activeMutation.operationId, name: activeMutation.name ?? 'extension', operation: activeMutation.operation, }, ); setBusyName((current) => current ?? activeMutation.name ?? null); } setRecoveryError(null); setOperationsRecovered(true); } catch (error) { if (!cancelled) { setRecoveryError( error instanceof Error ? error.message : String(error), ); timer = setTimeout(() => void recover(), retryDelay); retryDelay = Math.min(retryDelay * 2, 30_000); } } }; void recover(); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [actions]); const extensionsVersionRef = useRef(signals?.extensionsVersion ?? 0); useEffect(() => { const version = signals?.extensionsVersion ?? 0; if (version !== extensionsVersionRef.current) { extensionsVersionRef.current = version; setUpdateStates({}); void load(true); } }, [load, signals?.extensionsVersion]); useEffect(() => { if (!pendingInstall) return; let cancelled = false; let timer: ReturnType | undefined; let retryDelay = 1000; const poll = async () => { try { const operation = await actions.extensionOperationStatus( pendingInstall.operationId, ); if (cancelled) return; retryDelay = 1000; if (operation.status === 'waiting_for_input') { if (operation.interaction) { showInteraction( operation.operationId, operation.interaction, 'install', ); timer = setTimeout(() => void poll(), 5000); } else { setMessageTone('error'); setMessage(t('extensions.manage.operationFailed')); clearInteraction(pendingInstall.operationId); setPendingInstall(null); } return; } if (operation.status === 'failed') { setMessageTone('error'); setMessage( t('extensions.install.failed', { source: pendingInstall.source, error: operation.error ?? '', }), ); clearInteraction(pendingInstall.operationId); setPendingInstall(null); return; } if ( operation.status === 'succeeded' || operation.status === 'succeeded_with_refresh_error' ) { setMessageTone( operation.status === 'succeeded_with_refresh_error' ? 'error' : 'success', ); setMessage( operation.status === 'succeeded_with_refresh_error' ? t('extensions.manage.refreshFailed', { error: operation.result?.error ?? '', }) : t('extensions.install.installed', { name: operation.result?.name ?? pendingInstall.source, }), ); clearInteraction(pendingInstall.operationId); setPendingInstall(null); void load(true); return; } setMessageTone('progress'); setMessage( t('extensions.install.started', { source: pendingInstall.source, }), ); timer = setTimeout(() => void poll(), 1000); } catch (error) { if (cancelled) return; setMessageTone('error'); setMessage(error instanceof Error ? error.message : String(error)); if (error instanceof DaemonHttpError && error.status === 404) { clearInteraction(pendingInstall.operationId); setPendingInstall(null); return; } timer = setTimeout(() => void poll(), retryDelay); retryDelay = Math.min(retryDelay * 2, 30_000); } }; void poll(); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [actions, clearInteraction, load, pendingInstall, showInteraction, t]); const submitInteraction = useCallback( (response: ExtensionInteractionResponse) => { if (!pendingInteraction) return; const owner = pendingInteraction.owner; const restartPolling = () => { if (owner === 'install') { setPendingInstall((current) => (current ? { ...current } : current)); } else { setPendingMutation((current) => (current ? { ...current } : current)); } }; setSubmittingInteraction(true); actions .respondToExtensionInteraction( pendingInteraction.operationId, pendingInteraction.interaction.id, response, connection.clientId, ) .then(() => { clearInteraction(pendingInteraction.operationId); restartPolling(); }) .catch((error: unknown) => { setMessageTone('error'); setMessage(error instanceof Error ? error.message : String(error)); clearInteraction(pendingInteraction.operationId); restartPolling(); }) .finally(() => setSubmittingInteraction(false)); }, [actions, clearInteraction, connection.clientId, pendingInteraction], ); useEffect(() => { if (!pendingMutation) return; let cancelled = false; let timer: ReturnType | undefined; let retryDelay = 1000; const poll = async () => { try { const operation = await actions.extensionOperationStatus( pendingMutation.operationId, ); if (cancelled) return; retryDelay = 1000; if (operation.status === 'waiting_for_input') { if (operation.interaction) { showInteraction( operation.operationId, operation.interaction, 'mutation', ); timer = setTimeout(() => void poll(), 5000); } else { setMessageTone('error'); setMessage(t('extensions.manage.operationFailed')); clearInteraction(pendingMutation.operationId); setPendingMutation(null); setBusyName(null); mutationInFlightRef.current = false; if (pendingMutation.operation === 'uninstall') { uninstallInFlightNameRef.current = null; void load(true); } } return; } if (operation.status === 'failed') { setMessageTone('error'); setMessage(operation.error ?? t('extensions.manage.operationFailed')); clearInteraction(pendingMutation.operationId); setPendingMutation(null); setBusyName(null); mutationInFlightRef.current = false; if (operation.operation === 'uninstall') { uninstallInFlightNameRef.current = null; void load(true); } return; } if ( operation.status === 'succeeded' || operation.status === 'succeeded_with_refresh_error' ) { if (operation.status === 'succeeded_with_refresh_error') { setMessageTone('error'); setMessage( t('extensions.manage.refreshFailed', { error: operation.result?.error ?? '', }), ); } else if (operation.operation === 'uninstall') { setMessage(null); } else { setMessageTone('success'); setMessage( mutationSuccessMessage( operation.operation, pendingMutation.name, t, ), ); } clearInteraction(pendingMutation.operationId); setPendingMutation(null); setBusyName(null); mutationInFlightRef.current = false; if (operation.operation === 'uninstall') { uninstallInFlightNameRef.current = null; setMessageOwner(null); setSelectedName(null); } if (operation.operation === 'update') { setUpdateStates((current) => { const next = { ...current }; delete next[pendingMutation.name]; return next; }); } void load(true); return; } setMessageTone('progress'); setMessage( mutationMessage(operation.operation, pendingMutation.name, t), ); timer = setTimeout(() => void poll(), 1000); } catch (error) { if (cancelled) return; setMessageTone('error'); setMessage(error instanceof Error ? error.message : String(error)); if (error instanceof DaemonHttpError && error.status === 404) { clearInteraction(pendingMutation.operationId); setPendingMutation(null); setBusyName(null); mutationInFlightRef.current = false; if (pendingMutation.operation === 'uninstall') { uninstallInFlightNameRef.current = null; void load(true); } return; } timer = setTimeout(() => void poll(), retryDelay); retryDelay = Math.min(retryDelay * 2, 30_000); } }; void poll(); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [actions, clearInteraction, load, pendingMutation, showInteraction, t]); const refreshList = useCallback(() => { setMessageOwner(null); setMessageTone('info'); setMessage(null); void load(); }, [load]); const checkUpdates = useCallback( (name: string) => { setCheckingName(name); setMessageOwner(selectedName === name ? name : null); setMessageTone('info'); setMessage(null); setUpdateStates((current) => ({ ...current, [name]: 'checking for updates', })); actions .checkExtensionUpdates(connection.clientId) .then((result) => { setUpdateStates(result.states); setMessage(updateLabel(result.states[name], t)); }) .catch((error: unknown) => { setUpdateStates((current) => ({ ...current, [name]: 'error' })); setMessageTone('error'); setMessage(error instanceof Error ? error.message : String(error)); }) .finally(() => setCheckingName(null)); }, [actions, connection.clientId, selectedName, t], ); const installExtension = useCallback(() => { const source = installSource.trim(); const clientId = connection.clientId; if ( !source || !operationsRecovered || pendingInstall || pendingMutation || mutationInFlightRef.current ) return; setInstalling(true); setMessageOwner(null); setMessageTone('progress'); setMessage(null); actions .installExtension({ source, consent: true }, clientId) .then((result) => { setPendingInstall({ operationId: result.operationId, source }); setInstallSource(''); setInstallOpen(false); }) .catch((error: unknown) => { setMessageTone('error'); setMessage(error instanceof Error ? error.message : String(error)); }) .finally(() => setInstalling(false)); }, [ actions, connection.clientId, installSource, operationsRecovered, pendingInstall, pendingMutation, ]); const runMutation = useCallback( ( name: string, run: (clientId?: string) => Promise, options: { operation?: string; startMessage?: string } = {}, ): boolean => { const clientId = connection.clientId; if ( !operationsRecovered || pendingInstall || pendingMutation || checkingName || mutationInFlightRef.current ) { return false; } mutationInFlightRef.current = true; if (options.operation === 'uninstall') { uninstallInFlightNameRef.current = name; } setBusyName(name); setMessageOwner(selectedName === name ? name : null); setMessageTone('progress'); setMessage(options.startMessage ?? null); let startedPolling = false; run(clientId) .then((result) => { const operationId = result && typeof result === 'object' && 'operationId' in result && typeof result.operationId === 'string' ? result.operationId : undefined; if (operationId) { startedPolling = true; setPendingMutation({ operationId, name, operation: options.operation, }); return; } setMessage(t('extensions.manage.queued', { name })); }) .catch((error: unknown) => { setMessageTone('error'); setMessage(error instanceof Error ? error.message : String(error)); }) .finally(() => { if (!startedPolling) { mutationInFlightRef.current = false; setBusyName(null); if (options.operation === 'uninstall') { uninstallInFlightNameRef.current = null; } void load(true); } }); return true; }, [ connection.clientId, checkingName, load, operationsRecovered, pendingInstall, pendingMutation, selectedName, t, ], ); const setScopeActivation = useCallback( async ( extension: ManagedExtensionEntry, scope: Scope, activation: 'inherit' | ExtensionActivationState, ) => { if ( busyName || pendingInstall || pendingMutation || checkingName || !workspace.workspaceCwd ) { return; } const operation = activation === 'enabled' ? 'enable' : activation === 'disabled' ? 'disable' : 'inherit'; setBusyName(extension.name); setMessageOwner(extension.name); setMessageTone('progress'); setMessage( operation === 'inherit' ? t('extensions.manage.inheriting', { name: extension.name }) : mutationMessage(operation, extension.name, t), ); try { const result = scope === 'user' ? await workspace.client.setExtensionDefaultActivation( extension.id, activation as ExtensionActivationState, ) : activation === 'inherit' ? await workspace.client .workspaceByCwd(workspace.workspaceCwd) .clearExtensionActivation(extension.id) : await workspace.client .workspaceByCwd(workspace.workspaceCwd) .setExtensionActivation(extension.id, activation); const completed = await workspace.client.waitForExtensionOperation(result); if (completed.status === 'failed') { throw new Error( completed.error ?? t('extensions.manage.operationFailed'), ); } await load(true); setMessageTone('success'); setMessage( operation === 'inherit' ? t('extensions.manage.inherited', { name: extension.name }) : mutationSuccessMessage(operation, extension.name, t), ); } catch (error) { setMessageTone('error'); setMessage(error instanceof Error ? error.message : String(error)); } finally { setBusyName(null); } }, [ busyName, checkingName, load, pendingInstall, pendingMutation, t, workspace.client, workspace.workspaceCwd, ], ); const selectedExtension = useMemo( () => extensions.find((extension) => extension.name === selectedName), [extensions, selectedName], ); useEffect(() => { embedded?.onDetailChange(Boolean(selectedExtension)); }, [embedded, selectedExtension]); const filteredExtensions = useMemo( () => filterExtensions(extensions, query), [extensions, query], ); const returnToList = useCallback(() => { returnFocusNameRef.current = selectedName; setSelectedName(null); }, [selectedName]); useEffect(() => { if (selectedName || !returnFocusNameRef.current) return; cardRefs.current.get(returnFocusNameRef.current)?.focus(); returnFocusNameRef.current = null; }, [selectedName]); const standaloneNavigation = ( {selectedExtension ? ( ) : ( {t('extensions.manage.title')} )} {selectedExtension ? : null} {selectedExtension ? ( {extensionTitle(selectedExtension)} ) : null} ); const navigation = embedded ? ( selectedExtension ? ( {extensionTitle(selectedExtension)} ) : null ) : ( standaloneNavigation ); if (selectedExtension) { const details = selectedExtension.details; const updateState = updateStates[selectedExtension.name] ?? selectedExtension.updateState; const busy = !operationsRecovered || pendingInstall !== null || busyName !== null || pendingMutation !== null; const checking = checkingName === selectedExtension.name; const userActivation = selectedExtension.defaultActivation; const workspaceActivation = selectedExtension.workspaceActivation; const activationUnavailable = userActivation === undefined || workspaceActivation === undefined; const commands = details?.commands ?? []; const skills = details?.skills ?? []; const agents = details?.agents ?? []; const mcpServers = details?.mcpServers ?? []; const contextFiles = details?.contextFiles ?? []; return (
{navigation}

{extensionTitle(selectedExtension)}

v{selectedExtension.version} {statusLabel(selectedExtension, t)}
checkUpdates(selectedExtension.name)} > {t('extensions.manage.checkUpdates')} runMutation( selectedExtension.name, (clientId) => actions.updateExtension( selectedExtension.name, clientId, ), { operation: 'update', startMessage: mutationMessage( 'update', selectedExtension.name, t, ), }, ) } > {t('extensions.manage.update')} { event.preventDefault(); setActionsOpen(false); setUninstallName(selectedExtension.name); }} > {t('extensions.manage.uninstallAction')}
{messageOwner === selectedExtension.name && message ? ( setMessage(null)} className="break-words" > {message} ) : null} {activationUnavailable ? ( {t('extensions.manage.setting.unavailableDescription')} ) : null}

{t('extensions.manage.userSetting')}

{t('extensions.manage.userSettingDescription')}

{t('extensions.manage.workspaceSetting')}

{t('extensions.manage.workspaceSettingDescription')}

{t('extensions.manage.overview')} {trimDialogLabel(t('extensions.manage.commands'))}{' '} {commands.length} {trimDialogLabel(t('extensions.manage.skills'))} {skills.length} {trimDialogLabel(t('extensions.manage.agents'))} {agents.length} {trimDialogLabel(t('extensions.manage.mcpServers'))}{' '} {mcpServers.length} {trimDialogLabel(t('extensions.manage.contextFiles'))}{' '} {contextFiles.length} {t('extensions.manage.overview')} {selectedExtension.description ? ( {selectedExtension.description} ) : null}
{ if (!open) setUninstallName(null); }} > {t('extensions.manage.uninstallAction')} {t('extensions.manage.uninstallConfirm', { name: selectedExtension.name, })} {t('common.cancel')} { if (!uninstallName) return; if ( runMutation( uninstallName, (clientId) => actions.uninstallExtension(uninstallName, clientId), { operation: 'uninstall', startMessage: mutationMessage( 'uninstall', uninstallName, t, ), }, ) ) { setUninstallName(null); } }} > {t('extensions.manage.uninstallAction')}
); } return (
{navigation}

{t('extensions.manage.title')}

{t('extensions.manage.count', { count: extensions.length })}

{(messageOwner === null && message) || recoveryError ? ( { setMessage(null); setRecoveryError(null); }} className="break-words" > {(messageOwner === null ? message : null) ?? recoveryError} ) : null}
setQuery(event.target.value)} placeholder={t('extensions.manage.search')} className="pl-9" />
{filteredExtensions.length ? (
{filteredExtensions.map((extension) => { const state = updateStates[extension.name] ?? extension.updateState; return ( { if (node) cardRefs.current.set(extension.name, node); else cardRefs.current.delete(extension.name); }} size="sm" role="button" tabIndex={0} aria-label={extensionTitle(extension)} className="cursor-pointer transition-colors hover:bg-accent/30 focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none" onClick={() => setSelectedName(extension.name)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setSelectedName(extension.name); } }} >
{extensionTitle(extension)}
{statusLabel(extension, t)}
{state === UPDATE_AVAILABLE ? (
{updateLabel(state, t)}
) : null} {extension.description || t('extensions.manage.noDescription')} {extension.description || t('extensions.manage.noDescription')}
); })}
) : ( {query ? : } {query ? t('extensions.manage.noMatches') : t('extensions.manage.empty')} {!query ? ( {t('extensions.manage.emptyDescription')} ) : null} )}
{ if (open || !installing) setInstallOpen(open); }} > {t('extensions.manage.add')} {t('extensions.manage.installDescription')} setInstallSource(event.target.value)} placeholder={t('extensions.manage.sourcePlaceholder')} /> {t('common.cancel')}
); }