[feat]: Add /extensions restart command (#12739)

This commit is contained in:
Jacob MacDonald
2025-11-07 15:17:23 -08:00
committed by GitHub
parent fdb6088603
commit bafbcbbe8b
9 changed files with 457 additions and 10 deletions

View File

@@ -7,7 +7,11 @@
import { debugLogger, listExtensions } from '@google/gemini-cli-core';
import type { ExtensionUpdateInfo } from '../../config/extension.js';
import { getErrorMessage } from '../../utils/errors.js';
import { MessageType, type HistoryItemExtensionsList } from '../types.js';
import {
MessageType,
type HistoryItemExtensionsList,
type HistoryItemInfo,
} from '../types.js';
import {
type CommandContext,
type SlashCommand,
@@ -17,6 +21,7 @@ import open from 'open';
import process from 'node:process';
import { ExtensionManager } from '../../config/extension-manager.js';
import { SettingScope } from '../../config/settings.js';
import { theme } from '../semantic-colors.js';
async function listAction(context: CommandContext) {
const historyItem: HistoryItemExtensionsList = {
@@ -116,6 +121,118 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
return updateComplete.then((_) => {});
}
async function restartAction(
context: CommandContext,
args: string,
): Promise<void> {
const extensionLoader = context.services.config?.getExtensionLoader();
if (!extensionLoader) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: "Extensions are not yet loaded, can't restart yet",
},
Date.now(),
);
return;
}
const restartArgs = args.split(' ').filter((value) => value.length > 0);
const all = restartArgs.length === 1 && restartArgs[0] === '--all';
const names = all ? null : restartArgs;
if (!all && names?.length === 0) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Usage: /extensions restart <extension-names>|--all',
},
Date.now(),
);
return Promise.resolve();
}
let extensionsToRestart = extensionLoader
.getExtensions()
.filter((extension) => extension.isActive);
if (names) {
extensionsToRestart = extensionsToRestart.filter((extension) =>
names.includes(extension.name),
);
if (names.length !== extensionsToRestart.length) {
const notFound = names.filter(
(name) =>
!extensionsToRestart.some((extension) => extension.name === name),
);
if (notFound.length > 0) {
context.ui.addItem(
{
type: MessageType.WARNING,
text: `Extension(s) not found or not active: ${notFound.join(
', ',
)}`,
},
Date.now(),
);
}
}
}
if (extensionsToRestart.length === 0) {
// We will have logged a different message above already.
return;
}
const s = extensionsToRestart.length > 1 ? 's' : '';
const restartingMessage = {
type: MessageType.INFO,
text: `Restarting ${extensionsToRestart.length} extension${s}...`,
color: theme.text.primary,
};
context.ui.addItem(restartingMessage, Date.now());
const results = await Promise.allSettled(
extensionsToRestart.map(async (extension) => {
if (extension.isActive) {
await extensionLoader.restartExtension(extension);
context.ui.dispatchExtensionStateUpdate({
type: 'RESTARTED',
payload: {
name: extension.name,
},
});
}
}),
);
const failures = results.filter(
(result): result is PromiseRejectedResult => result.status === 'rejected',
);
if (failures.length > 0) {
const errorMessages = failures
.map((failure, index) => {
const extensionName = extensionsToRestart[index].name;
return `${extensionName}: ${getErrorMessage(failure.reason)}`;
})
.join('\n ');
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Failed to restart some extensions:\n ${errorMessages}`,
},
Date.now(),
);
} else {
const infoItem: HistoryItemInfo = {
type: MessageType.INFO,
text: `${extensionsToRestart.length} extension${s} restarted successfully.`,
icon: ' ',
color: theme.text.primary,
};
context.ui.addItem(infoItem, Date.now());
}
}
async function exploreAction(context: CommandContext) {
const extensionsUrl = 'https://geminicli.com/extensions/';
@@ -284,10 +401,14 @@ export function completeExtensions(
partialArg: string,
) {
let extensions = context.services.config?.getExtensions() ?? [];
if (context.invocation?.name === 'enable') {
extensions = extensions.filter((ext) => !ext.isActive);
}
if (context.invocation?.name === 'disable') {
if (
context.invocation?.name === 'disable' ||
context.invocation?.name === 'restart'
) {
extensions = extensions.filter((ext) => ext.isActive);
}
const extensionNames = extensions.map((ext) => ext.name);
@@ -351,6 +472,14 @@ const exploreExtensionsCommand: SlashCommand = {
action: exploreAction,
};
const restartCommand: SlashCommand = {
name: 'restart',
description: 'Restart all extensions',
kind: CommandKind.BUILT_IN,
action: restartAction,
completion: completeExtensions,
};
export function extensionsCommand(
enableExtensionReloading?: boolean,
): SlashCommand {
@@ -365,6 +494,7 @@ export function extensionsCommand(
listExtensionsCommand,
updateExtensionsCommand,
exploreExtensionsCommand,
restartCommand,
...conditionalCommands,
],
action: (context, args) =>