Create ExtensionManager class which manages all high level extension tasks (#11667)

This commit is contained in:
Jacob MacDonald
2025-10-23 11:39:36 -07:00
committed by GitHub
parent 3a501196f0
commit c4c0c0d182
31 changed files with 1450 additions and 1568 deletions

View File

@@ -93,9 +93,13 @@ import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
import { useSessionStats } from './contexts/SessionContext.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
import { useExtensionUpdates } from './hooks/useExtensionUpdates.js';
import {
useConfirmUpdateRequests,
useExtensionUpdates,
} from './hooks/useExtensionUpdates.js';
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js';
import { ExtensionManager } from '../config/extension-manager.js';
import { requestConsentInteractive } from '../config/extensions/consent.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
@@ -165,21 +169,28 @@ export const AppContainer = (props: AppContainerProps) => {
);
const extensions = config.getExtensions();
const [extensionEnablementManager] = useState<ExtensionEnablementManager>(
new ExtensionEnablementManager(config.getEnabledExtensions()),
const [extensionManager] = useState<ExtensionManager>(
new ExtensionManager({
enabledExtensionOverrides: config.getEnabledExtensions(),
workspaceDir: config.getWorkingDir(),
requestConsent: (description) =>
requestConsentInteractive(
description,
addConfirmUpdateExtensionRequest,
),
// TODO: Support requesting settings in the interactive CLI
requestSetting: null,
loadedSettings: settings,
}),
);
const { addConfirmUpdateExtensionRequest, confirmUpdateExtensionRequests } =
useConfirmUpdateRequests();
const {
extensionsUpdateState,
extensionsUpdateStateInternal,
dispatchExtensionStateUpdate,
confirmUpdateExtensionRequests,
addConfirmUpdateExtensionRequest,
} = useExtensionUpdates(
extensions,
extensionEnablementManager,
historyManager.addItem,
config.getWorkingDir(),
);
} = useExtensionUpdates(extensions, extensionManager, historyManager.addItem);
const [isPermissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
const openPermissionsDialog = useCallback(

View File

@@ -8,7 +8,6 @@ import { vi } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { loadExtension } from '../../config/extension.js';
import { createExtension } from '../../test-utils/createExtension.js';
import { useExtensionUpdates } from './useExtensionUpdates.js';
import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core';
@@ -19,7 +18,8 @@ import {
updateExtension,
} from '../../config/extensions/update.js';
import { ExtensionUpdateState } from '../state/extensions.js';
import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings } from '../../config/settings.js';
vi.mock('os', async (importOriginal) => {
const mockedOs = await importOriginal<typeof os>();
@@ -36,17 +36,29 @@ vi.mock('../../config/extensions/update.js', () => ({
describe('useExtensionUpdates', () => {
let tempHomeDir: string;
let tempWorkspaceDir: string;
let userExtensionsDir: string;
let extensionManager: ExtensionManager;
beforeEach(() => {
tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
);
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
tempWorkspaceDir = fs.mkdtempSync(
path.join(tempHomeDir, 'gemini-cli-test-workspace-'),
);
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
userExtensionsDir = path.join(tempHomeDir, GEMINI_DIR, 'extensions');
fs.mkdirSync(userExtensionsDir, { recursive: true });
vi.mocked(checkForAllExtensionUpdates).mockReset();
vi.mocked(updateExtension).mockReset();
extensionManager = new ExtensionManager({
workspaceDir: tempHomeDir,
requestConsent: vi.fn(),
requestSetting: vi.fn(),
loadedSettings: loadSettings(),
});
});
afterEach(() => {
@@ -71,10 +83,9 @@ describe('useExtensionUpdates', () => {
},
];
const addItem = vi.fn();
const cwd = '/test/cwd';
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
async (_extensions, _extensionEnablementManager, dispatch, _cwd) => {
async (_extensions, _extensionManager, dispatch) => {
dispatch({
type: 'SET_STATE',
payload: {
@@ -88,9 +99,8 @@ describe('useExtensionUpdates', () => {
renderHook(() =>
useExtensionUpdates(
extensions as GeminiCLIExtension[],
new ExtensionEnablementManager(),
extensionManager,
addItem,
cwd,
),
);
@@ -116,17 +126,12 @@ describe('useExtensionUpdates', () => {
autoUpdate: true,
},
});
const extensionEnablementManager = new ExtensionEnablementManager();
const extension = loadExtension({
extensionDir,
workspaceDir: tempHomeDir,
extensionEnablementManager,
})!;
const extension = extensionManager.loadExtension(extensionDir)!;
const addItem = vi.fn();
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
async (_extensions, _extensionEnablementManager, dispatch, _cwd) => {
async (_extensions, _extensionManager, dispatch) => {
dispatch({
type: 'SET_STATE',
payload: {
@@ -144,12 +149,7 @@ describe('useExtensionUpdates', () => {
});
renderHook(() =>
useExtensionUpdates(
[extension],
extensionEnablementManager,
addItem,
tempHomeDir,
),
useExtensionUpdates([extension], extensionManager, addItem),
);
await waitFor(
@@ -188,24 +188,15 @@ describe('useExtensionUpdates', () => {
},
});
const extensionEnablementManager = new ExtensionEnablementManager();
const extensions = [
loadExtension({
extensionDir: extensionDir1,
workspaceDir: tempHomeDir,
extensionEnablementManager,
})!,
loadExtension({
extensionDir: extensionDir2,
workspaceDir: tempHomeDir,
extensionEnablementManager,
})!,
extensionManager.loadExtension(extensionDir1)!,
extensionManager.loadExtension(extensionDir2)!,
];
const addItem = vi.fn();
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
async (_extensions, _extensionEnablementManager, dispatch, _cwd) => {
async (_extensions, _extensionManager, dispatch) => {
dispatch({
type: 'SET_STATE',
payload: {
@@ -236,12 +227,7 @@ describe('useExtensionUpdates', () => {
});
renderHook(() =>
useExtensionUpdates(
extensions,
extensionEnablementManager,
addItem,
tempHomeDir,
),
useExtensionUpdates(extensions, extensionManager, addItem),
);
await waitFor(
@@ -299,10 +285,9 @@ describe('useExtensionUpdates', () => {
},
];
const addItem = vi.fn();
const cwd = '/test/cwd';
vi.mocked(checkForAllExtensionUpdates).mockImplementation(
async (_extensions, _extensionEnablementManager, dispatch, _cwd) => {
async (_extensions, _extensionManager, dispatch) => {
dispatch({ type: 'BATCH_CHECK_START' });
dispatch({
type: 'SET_STATE',
@@ -323,13 +308,11 @@ describe('useExtensionUpdates', () => {
},
);
const extensionEnablementManager = new ExtensionEnablementManager();
renderHook(() =>
useExtensionUpdates(
extensions as GeminiCLIExtension[],
extensionEnablementManager,
extensionManager,
addItem,
cwd,
),
);

View File

@@ -18,12 +18,9 @@ import {
checkForAllExtensionUpdates,
updateExtension,
} from '../../config/extensions/update.js';
import {
requestConsentInteractive,
type ExtensionUpdateInfo,
} from '../../config/extension.js';
import { type ExtensionUpdateInfo } from '../../config/extension.js';
import { checkExhaustive } from '../../utils/checks.js';
import type { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js';
import type { ExtensionManager } from '../../config/extension-manager.js';
type ConfirmationRequestWrapper = {
prompt: React.ReactNode;
@@ -48,16 +45,7 @@ function confirmationRequestsReducer(
}
}
export const useExtensionUpdates = (
extensions: GeminiCLIExtension[],
extensionEnablementManager: ExtensionEnablementManager,
addItem: UseHistoryManagerReturn['addItem'],
cwd: string,
) => {
const [extensionsUpdateState, dispatchExtensionStateUpdate] = useReducer(
extensionUpdatesReducer,
initialExtensionUpdatesState,
);
export const useConfirmUpdateRequests = () => {
const [
confirmUpdateExtensionRequests,
dispatchConfirmUpdateExtensionRequests,
@@ -82,6 +70,22 @@ export const useExtensionUpdates = (
},
[dispatchConfirmUpdateExtensionRequests],
);
return {
addConfirmUpdateExtensionRequest,
confirmUpdateExtensionRequests,
dispatchConfirmUpdateExtensionRequests,
};
};
export const useExtensionUpdates = (
extensions: GeminiCLIExtension[],
extensionManager: ExtensionManager,
addItem: UseHistoryManagerReturn['addItem'],
) => {
const [extensionsUpdateState, dispatchExtensionStateUpdate] = useReducer(
extensionUpdatesReducer,
initialExtensionUpdatesState,
);
useEffect(() => {
const extensionsToCheck = extensions.filter((extension) => {
@@ -95,15 +99,13 @@ export const useExtensionUpdates = (
if (extensionsToCheck.length === 0) return;
checkForAllExtensionUpdates(
extensionsToCheck,
extensionEnablementManager,
extensionManager,
dispatchExtensionStateUpdate,
cwd,
);
}, [
extensions,
extensionEnablementManager,
extensionManager,
extensionsUpdateState.extensionStatuses,
cwd,
dispatchExtensionStateUpdate,
]);
@@ -158,13 +160,7 @@ export const useExtensionUpdates = (
} else {
const updatePromise = updateExtension(
extension,
extensionEnablementManager,
cwd,
(description) =>
requestConsentInteractive(
description,
addConfirmUpdateExtensionRequest,
),
extensionManager,
currentState.status,
dispatchExtensionStateUpdate,
);
@@ -213,14 +209,7 @@ export const useExtensionUpdates = (
});
});
}
}, [
extensions,
extensionEnablementManager,
extensionsUpdateState,
addConfirmUpdateExtensionRequest,
addItem,
cwd,
]);
}, [extensions, extensionManager, extensionsUpdateState, addItem]);
const extensionsUpdateStateComputed = useMemo(() => {
const result = new Map<string, ExtensionUpdateState>();
@@ -237,7 +226,5 @@ export const useExtensionUpdates = (
extensionsUpdateState: extensionsUpdateStateComputed,
extensionsUpdateStateInternal: extensionsUpdateState.extensionStatuses,
dispatchExtensionStateUpdate,
confirmUpdateExtensionRequests,
addConfirmUpdateExtensionRequest,
};
};