Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ export async function loadCliConfig(
enabledExtensions: argv.extensions,
extensionLoader: extensionManager,
enableExtensionReloading: settings.experimental?.extensionReloading,
dynamicExtensionLoading: settings.experimental?.dynamicExtensionLoading,
enableAgents: settings.experimental?.enableAgents,
skillsSupport: settings.experimental?.skills,
disabledSkills: settings.skills?.disabled,
Expand Down
39 changes: 27 additions & 12 deletions packages/cli/src/config/extension-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
type HookEventName,
type ResolvedExtensionSetting,
coreEvents,
ExtensionScope,
} from '@google/gemini-cli-core';
import { maybeRequestConsentOrFail } from './extensions/consent.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
Expand Down Expand Up @@ -565,17 +566,22 @@ Would you like to attempt to install via "git clone" instead?`,

const extension: GeminiCLIExtension = {
name: config.name,
description: config.description,
version: config.version,
path: effectiveExtensionPath,
contextFiles,
installMetadata,
mcpServers: config.mcpServers,
excludeTools: config.excludeTools,
hooks,
isActive: this.extensionEnablementManager.isEnabled(
config.name,
this.workspaceDir,
),
isActive:
this.settings.experimental?.extensionReloading &&
this.settings.experimental?.dynamicExtensionLoading
? false
: this.extensionEnablementManager.isEnabled(
config.name,
this.workspaceDir,
),
id: getExtensionId(config, installMetadata),
settings: config.settings,
resolvedSettings,
Expand Down Expand Up @@ -632,6 +638,7 @@ Would you like to attempt to install via "git clone" instead?`,
) as unknown as ExtensionConfig;

validateName(config.name);

return config;
} catch (e) {
throw new Error(
Expand Down Expand Up @@ -750,10 +757,12 @@ Would you like to attempt to install via "git clone" instead?`,
return output;
}

async disableExtension(name: string, scope: SettingScope) {
async disableExtension(name: string, scope: SettingScope | ExtensionScope) {
if (
scope === SettingScope.System ||
scope === SettingScope.SystemDefaults
scope === SettingScope.SystemDefaults ||
scope === ExtensionScope.System ||
scope === ExtensionScope.SystemDefaults
) {
throw new Error('System and SystemDefaults scopes are not supported.');
}
Expand All @@ -764,9 +773,11 @@ Would you like to attempt to install via "git clone" instead?`,
throw new Error(`Extension with name ${name} does not exist.`);
}

if (scope !== SettingScope.Session) {
if (scope !== SettingScope.Session && scope !== ExtensionScope.Session) {
const scopePath =
scope === SettingScope.Workspace ? this.workspaceDir : homedir();
scope === SettingScope.Workspace || scope === ExtensionScope.Workspace
? this.workspaceDir
: homedir();
this.extensionEnablementManager.disable(name, true, scopePath);
}
await logExtensionDisable(
Expand All @@ -785,10 +796,12 @@ Would you like to attempt to install via "git clone" instead?`,
* Enables an existing extension for a given scope, and starts it if
* appropriate.
*/
async enableExtension(name: string, scope: SettingScope) {
async enableExtension(name: string, scope: SettingScope | ExtensionScope) {
if (
scope === SettingScope.System ||
scope === SettingScope.SystemDefaults
scope === SettingScope.SystemDefaults ||
scope === ExtensionScope.System ||
scope === ExtensionScope.SystemDefaults
) {
throw new Error('System and SystemDefaults scopes are not supported.');
}
Expand All @@ -799,9 +812,11 @@ Would you like to attempt to install via "git clone" instead?`,
throw new Error(`Extension with name ${name} does not exist.`);
}

if (scope !== SettingScope.Session) {
if (scope !== SettingScope.Session && scope !== ExtensionScope.Session) {
const scopePath =
scope === SettingScope.Workspace ? this.workspaceDir : homedir();
scope === SettingScope.Workspace || scope === ExtensionScope.Workspace
? this.workspaceDir
: homedir();
this.extensionEnablementManager.enable(name, true, scopePath);
}
await logExtensionEnable(
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/config/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { ExtensionSetting } from './extensions/extensionSettings.js';
*/
export interface ExtensionConfig {
name: string;
description?: string;
version: string;
mcpServers?: Record<string, MCPServerConfig>;
contextFileName?: string | string[];
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,16 @@ const SETTINGS_SCHEMA = {
'Enables extension loading/unloading within the CLI session.',
showInDialog: false,
},
dynamicExtensionLoading: {
type: 'boolean',
label: 'Dynamic Extension Loading',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enables the model to dynamically activate extensions (requires extensionReloading).',
showInDialog: false,
},
jitContext: {
type: 'boolean',
label: 'JIT Context Loading',
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { GrepTool } from '../tools/grep.js';
import { canUseRipgrep, RipGrepTool } from '../tools/ripGrep.js';
import { GlobTool } from '../tools/glob.js';
import { ActivateSkillTool } from '../tools/activate-skill.js';
import { ActivateExtensionTool } from '../tools/activate-extension.js';
import { EditTool } from '../tools/edit.js';
import { ShellTool } from '../tools/shell.js';
import { WriteFileTool } from '../tools/write-file.js';
Expand Down Expand Up @@ -163,6 +164,7 @@ export interface IntrospectionAgentSettings {
*/
export interface GeminiCLIExtension {
name: string;
description?: string;
version: string;
isActive: boolean;
path: string;
Expand Down Expand Up @@ -303,6 +305,7 @@ export interface ConfigParameters {
extensionLoader?: ExtensionLoader;
enabledExtensions?: string[];
enableExtensionReloading?: boolean;
dynamicExtensionLoading?: boolean;
allowedMcpServers?: string[];
blockedMcpServers?: string[];
allowedEnvironmentVariables?: string[];
Expand Down Expand Up @@ -430,6 +433,7 @@ export class Config {
private readonly _extensionLoader: ExtensionLoader;
private readonly _enabledExtensions: string[];
private readonly enableExtensionReloading: boolean;
private readonly dynamicExtensionLoading: boolean;
fallbackModelHandler?: FallbackModelHandler;
private quotaErrorOccurred: boolean = false;
private readonly summarizeToolOutput:
Expand Down Expand Up @@ -601,6 +605,8 @@ export class Config {
this.truncateToolOutputLines =
params.truncateToolOutputLines ?? DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES;
this.enableToolOutputTruncation = params.enableToolOutputTruncation ?? true;
this.enableExtensionReloading = params.enableExtensionReloading ?? false;
this.dynamicExtensionLoading = params.dynamicExtensionLoading ?? false;
// // TODO(joshualitt): Re-evaluate the todo tool for 3 family.
this.useWriteTodos = isPreviewModel(this.model)
? false
Expand Down Expand Up @@ -631,6 +637,7 @@ export class Config {
(params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes
this.extensionManagement = params.extensionManagement ?? true;
this.enableExtensionReloading = params.enableExtensionReloading ?? false;
this.dynamicExtensionLoading = params.dynamicExtensionLoading ?? false;
this.storage = new Storage(this.targetDir);
this.fakeResponses = params.fakeResponses;
this.recordResponses = params.recordResponses;
Expand Down Expand Up @@ -742,6 +749,13 @@ export class Config {
]);
initMcpHandle?.end();

// Register ActivateExtensionTool
if (this.enableExtensionReloading && this.dynamicExtensionLoading) {
this.getToolRegistry().registerTool(
new ActivateExtensionTool(this, this.messageBus),
);
}

// Discover skills if enabled
if (this.skillsSupport) {
await this.getSkillManager().discoverSkills(
Expand Down Expand Up @@ -1412,6 +1426,10 @@ export class Config {
return this.enableExtensionReloading;
}

getDynamicExtensionLoading(): boolean {
return this.dynamicExtensionLoading;
}

isAgentsEnabled(): boolean {
return this.enableAgents;
}
Expand Down
Loading
Loading