From d743c6fae6bf83542adef2374b0f1971e1dfb173 Mon Sep 17 00:00:00 2001 From: Coco Sheng Date: Thu, 30 Apr 2026 10:11:06 -0400 Subject: [PATCH] fix: suppress duplicate extension warnings during startup (#26208) --- packages/cli/src/config/config.ts | 40 ++++++++------ .../cli/src/config/skipExtensions.test.ts | 54 +++++++++++++++++++ packages/cli/src/gemini.tsx | 1 + 3 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 packages/cli/src/config/skipExtensions.test.ts diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 7e9ba97bf5..97689b5fe5 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -37,6 +37,7 @@ import { getAdminErrorMessage, isHeadlessMode, Config, + SimpleExtensionLoader, resolveToRealPath, applyAdminAllowlist, applyRequiredServers, @@ -558,6 +559,7 @@ export interface LoadCliConfigOptions { disabled?: string[]; }; worktreeSettings?: WorktreeSettings; + skipExtensions?: boolean; } export async function loadCliConfig( @@ -566,7 +568,7 @@ export async function loadCliConfig( argv: CliArgs, options: LoadCliConfigOptions = {}, ): Promise { - const { cwd = process.cwd(), projectHooks } = options; + const { cwd = process.cwd(), projectHooks, skipExtensions = false } = options; const debugMode = isDebugMode(argv); const worktreeSettings = @@ -641,21 +643,24 @@ export async function loadCliConfig( includeDirectories.push(...ideFolders); } - const extensionManager = new ExtensionManager({ - settings, - requestConsent: requestConsentNonInteractive, - requestSetting: promptForSetting, - workspaceDir: cwd, - enabledExtensionOverrides: argv.extensions, - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - eventEmitter: coreEvents as EventEmitter, - clientVersion: await getVersion(), - }); - await extensionManager.loadExtensions(); + let extensionManager: ExtensionManager | undefined; + if (!skipExtensions) { + extensionManager = new ExtensionManager({ + settings, + requestConsent: requestConsentNonInteractive, + requestSetting: promptForSetting, + workspaceDir: cwd, + enabledExtensionOverrides: argv.extensions, + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + eventEmitter: coreEvents as EventEmitter, + clientVersion: await getVersion(), + }); + await extensionManager.loadExtensions(); + } const extensionPlanSettings = extensionManager - .getExtensions() - .find((ext) => ext.isActive && ext.plan?.directory)?.plan; + ?.getExtensions() + ?.find((ext) => ext.isActive && ext.plan?.directory)?.plan; const experimentalJitContext = settings.experimental.jitContext ?? true; @@ -673,6 +678,9 @@ export async function loadCliConfig( let fileCount = 0; let filePaths: string[] = []; + const finalExtensionLoader = + extensionManager ?? new SimpleExtensionLoader([]); + if (!experimentalJitContext) { // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const result = await loadServerHierarchicalMemory( @@ -681,7 +689,7 @@ export async function loadCliConfig( ? includeDirectories : [], fileService, - extensionManager, + finalExtensionLoader, trustedFolder, memoryImportFormat, memoryFileFiltering, @@ -1037,7 +1045,7 @@ export async function loadCliConfig( listSessions: argv.listSessions || false, deleteSession: argv.deleteSession, enabledExtensions: argv.extensions, - extensionLoader: extensionManager, + extensionLoader: finalExtensionLoader, extensionRegistryURI, enableExtensionReloading: settings.experimental?.extensionReloading, enableAgents: settings.experimental?.enableAgents, diff --git a/packages/cli/src/config/skipExtensions.test.ts b/packages/cli/src/config/skipExtensions.test.ts new file mode 100644 index 0000000000..415c3a9529 --- /dev/null +++ b/packages/cli/src/config/skipExtensions.test.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { loadCliConfig, type CliArgs } from './config.js'; +import { ExtensionManager } from './extension-manager.js'; +import { createTestMergedSettings } from './settings.js'; + +vi.mock('./extension-manager.js', () => ({ + ExtensionManager: vi.fn().mockImplementation(() => ({ + loadExtensions: vi.fn().mockResolvedValue([]), + getExtensions: vi.fn().mockReturnValue([]), + })), +})); + +describe('loadCliConfig skipExtensions', () => { + const settings = createTestMergedSettings(); + const argv = { + query: undefined, + model: undefined, + sandbox: undefined, + debug: undefined, + prompt: undefined, + promptInteractive: undefined, + yolo: undefined, + approvalMode: undefined, + policy: undefined, + adminPolicy: undefined, + allowedMcpServerNames: undefined, + allowedTools: undefined, + extensions: undefined, + listExtensions: undefined, + resume: undefined, + sessionId: undefined, + listSessions: undefined, + } as unknown as CliArgs; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should load extensions by default', async () => { + await loadCliConfig(settings, 'session-id', argv); + expect(ExtensionManager).toHaveBeenCalled(); + }); + + it('should skip extensions when skipExtensions is true', async () => { + await loadCliConfig(settings, 'session-id', argv, { skipExtensions: true }); + expect(ExtensionManager).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 846be5890b..2e44b8865f 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -409,6 +409,7 @@ export async function main() { const partialConfig = await loadCliConfig(settings.merged, sessionId, argv, { projectHooks: settings.workspace.settings.hooks, + skipExtensions: true, }); adminControlsListner.setConfig(partialConfig);