Initial support for reloading extensions in the CLI - mcp servers only (#12239)

This commit is contained in:
Jacob MacDonald
2025-10-30 11:05:49 -07:00
committed by GitHub
parent d4cad0cdcc
commit cc081337b7
20 changed files with 437 additions and 107 deletions
@@ -30,11 +30,12 @@ const updateOutput = (info: ExtensionUpdateInfo) =>
export async function handleUpdate(args: UpdateArgs) {
const workspaceDir = process.cwd();
const settings = loadSettings(workspaceDir).merged;
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent: requestConsentNonInteractive,
requestSetting: promptForSetting,
settings: loadSettings(workspaceDir).merged,
settings,
});
const extensions = await extensionManager.loadExtensions();
@@ -67,6 +68,7 @@ export async function handleUpdate(args: UpdateArgs) {
extensionManager,
updateState,
() => {},
settings.experimental?.extensionReloading,
))!;
if (
updatedExtensionInfo.originalVersion !==
+1
View File
@@ -680,6 +680,7 @@ export async function loadCliConfig(
listExtensions: argv.listExtensions || false,
enabledExtensions: argv.extensions,
extensionLoader: extensionManager,
enableExtensionReloading: settings.experimental?.extensionReloading,
blockedMcpServers,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
+31 -23
View File
@@ -28,6 +28,7 @@ export async function updateExtension(
extensionManager: ExtensionManager,
currentState: ExtensionUpdateState,
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void,
enableExtensionReloading?: boolean,
): Promise<ExtensionUpdateInfo | undefined> {
if (currentState === ExtensionUpdateState.UPDATING) {
return undefined;
@@ -81,7 +82,9 @@ export async function updateExtension(
type: 'SET_STATE',
payload: {
name: extension.name,
state: ExtensionUpdateState.UPDATED_NEEDS_RESTART,
state: enableExtensionReloading
? ExtensionUpdateState.UPDATED
: ExtensionUpdateState.UPDATED_NEEDS_RESTART,
},
});
return {
@@ -109,6 +112,7 @@ export async function updateAllUpdatableExtensions(
extensionsState: Map<string, ExtensionUpdateStatus>,
extensionManager: ExtensionManager,
dispatch: (action: ExtensionUpdateAction) => void,
enableExtensionReloading?: boolean,
): Promise<ExtensionUpdateInfo[]> {
return (
await Promise.all(
@@ -124,6 +128,7 @@ export async function updateAllUpdatableExtensions(
extensionManager,
extensionsState.get(extension.name)!.status,
dispatch,
enableExtensionReloading,
),
),
)
@@ -141,34 +146,37 @@ export async function checkForAllExtensionUpdates(
dispatch: (action: ExtensionUpdateAction) => void,
): Promise<void> {
dispatch({ type: 'BATCH_CHECK_START' });
const promises: Array<Promise<void>> = [];
for (const extension of extensions) {
if (!extension.installMetadata) {
try {
const promises: Array<Promise<void>> = [];
for (const extension of extensions) {
if (!extension.installMetadata) {
dispatch({
type: 'SET_STATE',
payload: {
name: extension.name,
state: ExtensionUpdateState.NOT_UPDATABLE,
},
});
continue;
}
dispatch({
type: 'SET_STATE',
payload: {
name: extension.name,
state: ExtensionUpdateState.NOT_UPDATABLE,
state: ExtensionUpdateState.CHECKING_FOR_UPDATES,
},
});
continue;
promises.push(
checkForExtensionUpdate(extension, extensionManager).then((state) =>
dispatch({
type: 'SET_STATE',
payload: { name: extension.name, state },
}),
),
);
}
dispatch({
type: 'SET_STATE',
payload: {
name: extension.name,
state: ExtensionUpdateState.CHECKING_FOR_UPDATES,
},
});
promises.push(
checkForExtensionUpdate(extension, extensionManager).then((state) =>
dispatch({
type: 'SET_STATE',
payload: { name: extension.name, state },
}),
),
);
await Promise.all(promises);
} finally {
dispatch({ type: 'BATCH_CHECK_END' });
}
await Promise.all(promises);
dispatch({ type: 'BATCH_CHECK_END' });
}
+10
View File
@@ -1075,6 +1075,16 @@ const SETTINGS_SCHEMA = {
description: 'Enable extension management features.',
showInDialog: false,
},
extensionReloading: {
type: 'boolean',
label: 'Extension Reloading',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enables extension loading/unloading within the CLI session.',
showInDialog: false,
},
useModelRouter: {
type: 'boolean',
label: 'Use Model Router',
+5 -1
View File
@@ -183,7 +183,11 @@ export const AppContainer = (props: AppContainerProps) => {
extensionsUpdateState,
extensionsUpdateStateInternal,
dispatchExtensionStateUpdate,
} = useExtensionUpdates(extensionManager, historyManager.addItem);
} = useExtensionUpdates(
extensionManager,
historyManager.addItem,
config.getEnableExtensionReloading(),
);
const [isPermissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
const openPermissionsDialog = useCallback(
@@ -97,6 +97,10 @@ describe('<ExtensionsList />', () => {
state: ExtensionUpdateState.UPDATED_NEEDS_RESTART,
expectedText: '(updated, needs restart)',
},
{
state: ExtensionUpdateState.UPDATED,
expectedText: '(updated)',
},
{
state: ExtensionUpdateState.ERROR,
expectedText: '(error)',
@@ -48,6 +48,7 @@ export const ExtensionsList: React.FC<ExtensionsList> = ({ extensions }) => {
break;
case ExtensionUpdateState.UP_TO_DATE:
case ExtensionUpdateState.NOT_UPDATABLE:
case ExtensionUpdateState.UPDATED:
stateColor = 'green';
break;
case undefined:
@@ -84,6 +84,7 @@ describe('handleAtCommand', () => {
getReadManyFilesExcludes: () => [],
}),
getUsageStatisticsEnabled: () => false,
getEnableExtensionReloading: () => false,
} as unknown as Config;
const registry = new ToolRegistry(mockConfig);
@@ -96,7 +96,7 @@ describe('useExtensionUpdates', () => {
);
function TestComponent() {
useExtensionUpdates(extensionManager, addItem);
useExtensionUpdates(extensionManager, addItem, false);
return null;
}
@@ -146,7 +146,7 @@ describe('useExtensionUpdates', () => {
});
function TestComponent() {
useExtensionUpdates(extensionManager, addItem);
useExtensionUpdates(extensionManager, addItem, false);
return null;
}
@@ -224,7 +224,7 @@ describe('useExtensionUpdates', () => {
});
function TestComponent() {
useExtensionUpdates(extensionManager, addItem);
useExtensionUpdates(extensionManager, addItem, false);
return null;
}
@@ -307,7 +307,7 @@ describe('useExtensionUpdates', () => {
);
function TestComponent() {
useExtensionUpdates(extensionManager, addItem);
useExtensionUpdates(extensionManager, addItem, false);
return null;
}
@@ -80,6 +80,7 @@ export const useConfirmUpdateRequests = () => {
export const useExtensionUpdates = (
extensionManager: ExtensionManager,
addItem: UseHistoryManagerReturn['addItem'],
enableExtensionReloading: boolean,
) => {
const [extensionsUpdateState, dispatchExtensionStateUpdate] = useReducer(
extensionUpdatesReducer,
@@ -163,6 +164,7 @@ export const useExtensionUpdates = (
extensionManager,
currentState.status,
dispatchExtensionStateUpdate,
enableExtensionReloading,
);
updatePromises.push(updatePromise);
updatePromise
@@ -209,7 +211,13 @@ export const useExtensionUpdates = (
});
});
}
}, [extensions, extensionManager, extensionsUpdateState, addItem]);
}, [
extensions,
extensionManager,
extensionsUpdateState,
addItem,
enableExtensionReloading,
]);
const extensionsUpdateStateComputed = useMemo(() => {
const result = new Map<string, ExtensionUpdateState>();
+1
View File
@@ -10,6 +10,7 @@ import { checkExhaustive } from '../../utils/checks.js';
export enum ExtensionUpdateState {
CHECKING_FOR_UPDATES = 'checking for updates',
UPDATED_NEEDS_RESTART = 'updated, needs restart',
UPDATED = 'updated',
UPDATING = 'updating',
UPDATE_AVAILABLE = 'update available',
UP_TO_DATE = 'up to date',