diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index a58b9889a2..98769e717d 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -2857,6 +2857,7 @@ describe('Settings Loading and Merging', () => { { ...emptySettingsFile, path: MOCK_WORKSPACE_SETTINGS_PATH }, true, // isTrusted [], + MOCK_WORKSPACE_DIR, ); }); @@ -3181,6 +3182,8 @@ describe('LoadedSettings Isolation and Serializability', () => { { ...emptyScope }, // user emptyScope, // workspace true, // isTrusted + [], + '', ); }); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 7eec1c61b8..e14e9a81fb 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -312,6 +312,7 @@ export class LoadedSettings { workspace: SettingsFile, isTrusted: boolean, errors: SettingsError[] = [], + workspaceDir: string, ) { this.system = system; this.systemDefaults = systemDefaults; @@ -322,21 +323,23 @@ export class LoadedSettings { ? workspace : this.createEmptyWorkspace(workspace); this.errors = errors; + this._workspaceDir = workspaceDir; this._merged = this.computeMergedSettings(); this._snapshot = this.computeSnapshot(); } - readonly system: SettingsFile; - readonly systemDefaults: SettingsFile; - readonly user: SettingsFile; + system: SettingsFile; + systemDefaults: SettingsFile; + user: SettingsFile; workspace: SettingsFile; isTrusted: boolean; - readonly errors: SettingsError[]; + errors: SettingsError[]; private _workspaceFile: SettingsFile; private _merged: MergedSettings; private _snapshot: LoadedSettingsSnapshot; private _remoteAdminSettings: Partial | undefined; + private _workspaceDir: string; get merged(): MergedSettings { return this._merged; @@ -492,6 +495,30 @@ export class LoadedSettings { this._remoteAdminSettings = { admin }; this._merged = this.computeMergedSettings(); } + + /** + * Updates this instance with data from another instance. + * This preserves the object identity of this instance while refreshing its content. + */ + updateFrom(other: LoadedSettings): void { + this.system = other.system; + this.systemDefaults = other.systemDefaults; + this.user = other.user; + this._workspaceFile = other._workspaceFile; + this.isTrusted = other.isTrusted; + this.workspace = other.workspace; + this.errors = [...other.errors]; + this._merged = this.computeMergedSettings(); + this._snapshot = this.computeSnapshot(); + coreEvents.emitSettingsChanged(); + } + + /** + * Reloads settings from disk for the current workspace. + */ + reload(): void { + this.updateFrom(loadSettings(this._workspaceDir)); + } } function findEnvFile(startDir: string): string | null { @@ -816,6 +843,7 @@ function _doLoadSettings(workspaceDir: string): LoadedSettings { }, isTrusted, settingsErrors, + workspaceDir, ); // Automatically migrate deprecated settings when loading. diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index f166c161cd..7ed27be91c 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -122,6 +122,9 @@ vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} })); vi.mock('../ui/commands/skillsCommand.js', () => ({ skillsCommand: { name: 'skills' }, })); +vi.mock('../ui/commands/reloadCommand.js', () => ({ + reloadCommand: { name: 'reload' }, +})); vi.mock('../ui/commands/planCommand.js', async () => { const { CommandKind } = await import('../ui/commands/types.js'); return { @@ -247,6 +250,9 @@ describe('BuiltinCommandLoader', () => { const mcpCmd = commands.find((c) => c.name === 'mcp'); expect(mcpCmd).toBeDefined(); + + const reloadCmd = commands.find((c) => c.name === 'reload'); + expect(reloadCmd).toBeDefined(); }); it('should include permissions command when folder trust is enabled', async () => { diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index c1cbd5621e..a9a99b17df 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -55,6 +55,7 @@ import { statsCommand } from '../ui/commands/statsCommand.js'; import { themeCommand } from '../ui/commands/themeCommand.js'; import { toolsCommand } from '../ui/commands/toolsCommand.js'; import { skillsCommand } from '../ui/commands/skillsCommand.js'; +import { reloadCommand } from '../ui/commands/reloadCommand.js'; import { settingsCommand } from '../ui/commands/settingsCommand.js'; import { tasksCommand } from '../ui/commands/tasksCommand.js'; import { vimCommand } from '../ui/commands/vimCommand.js'; @@ -187,6 +188,7 @@ export class BuiltinCommandLoader implements ICommandLoader { ...(this.config?.isPlanEnabled() ? [planCommand] : []), policiesCommand, privacyCommand, + reloadCommand, ...(isDevelopment ? [profileCommand] : []), quitCommand, restoreCommand(this.config), diff --git a/packages/cli/src/test-utils/settings.ts b/packages/cli/src/test-utils/settings.ts index 20d0613f83..1a2b2b1b3d 100644 --- a/packages/cli/src/test-utils/settings.ts +++ b/packages/cli/src/test-utils/settings.ts @@ -63,6 +63,7 @@ export const createMockSettings = ( (workspace as any) || { path: '', settings: {}, originalSettings: {} }, isTrusted ?? true, errors || [], + '', ); if (mergedOverride) { diff --git a/packages/cli/src/ui/commands/reloadCommand.ts b/packages/cli/src/ui/commands/reloadCommand.ts new file mode 100644 index 0000000000..4e55bbe71b --- /dev/null +++ b/packages/cli/src/ui/commands/reloadCommand.ts @@ -0,0 +1,133 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { refreshMemory } from '@google/gemini-cli-core'; +import { MessageType } from '../types.js'; +import { + CommandKind, + type CommandContext, + type SlashCommand, + type SlashCommandActionReturn, +} from './types.js'; + +/** + * Action for the top-level `/reload` command. + * Orchestrates re-syncing the agent by reloading skills, agents, MCP servers, + * memory, and then refreshing the slash commands. + */ +async function reloadAllAction( + context: CommandContext, +): Promise { + const agentContext = context.services.agentContext; + const config = agentContext?.config; + + if (!config) { + return { + type: 'message', + messageType: 'error', + content: 'Could not retrieve configuration for reload.', + }; + } + + context.ui.addItem({ + type: MessageType.INFO, + text: 'Reloading all agent systems...', + }); + + const errors: string[] = []; + + // 0. Reload settings.json + try { + context.services.settings.reload(); + } catch (error) { + errors.push( + `Settings: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 1. Reload Skills & Extensions + try { + await config.reloadSkills(); + } catch (error) { + errors.push( + `Skills: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 2. Reload Agent Registry + const agentRegistry = config.getAgentRegistry(); + if (agentRegistry) { + try { + await agentRegistry.reload(); + } catch (error) { + errors.push( + `Agents: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + // 3. Reload MCP Servers + const mcpClientManager = config.getMcpClientManager(); + if (mcpClientManager) { + try { + await mcpClientManager.restart(); + // Update the client with the new tools + if (agentContext.geminiClient?.isInitialized()) { + await agentContext.geminiClient.setTools(); + } + } catch (error) { + errors.push( + `MCP: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + // 4. Reload Memory + try { + const memoryResult = await refreshMemory(config); + context.ui.addItem({ + type: MessageType.INFO, + text: memoryResult.content, + }); + } catch (error) { + errors.push( + `Memory: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 5. Finally, reload slash commands to reflect all changes + try { + context.ui.reloadCommands(); + } catch (error) { + errors.push( + `Commands: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + if (errors.length > 0) { + return { + type: 'message', + messageType: 'error', + content: `Reload completed with errors:\n- ${errors.join('\n- ')}`, + }; + } + + return { + type: 'message', + messageType: 'info', + content: 'All systems reloaded successfully.', + }; +} + +export const reloadCommand: SlashCommand = { + name: 'reload', + altNames: ['refresh'], + description: + 'Reload all agent systems (skills, agents, MCP, memory, and commands)', + kind: CommandKind.BUILT_IN, + autoExecute: true, + action: reloadAllAction, +}; diff --git a/packages/cli/src/ui/components/AgentConfigDialog.test.tsx b/packages/cli/src/ui/components/AgentConfigDialog.test.tsx index 2c6ea454db..6048eebbec 100644 --- a/packages/cli/src/ui/components/AgentConfigDialog.test.tsx +++ b/packages/cli/src/ui/components/AgentConfigDialog.test.tsx @@ -71,6 +71,7 @@ const createMockSettings = ( }, true, [], + '', ); // Mock setValue diff --git a/packages/cli/src/ui/utils/CodeColorizer.test.tsx b/packages/cli/src/ui/utils/CodeColorizer.test.tsx index 0979e3e123..b68ac592ad 100644 --- a/packages/cli/src/ui/utils/CodeColorizer.test.tsx +++ b/packages/cli/src/ui/utils/CodeColorizer.test.tsx @@ -25,6 +25,7 @@ describe('colorizeCode', () => { { path: '', settings: {}, originalSettings: {} }, true, [], + '', ); const result = colorizeCode({ @@ -63,6 +64,7 @@ describe('colorizeCode', () => { { path: '', settings: {}, originalSettings: {} }, true, [], + '', ); const result = colorizeCode({ @@ -89,6 +91,7 @@ describe('colorizeCode', () => { { path: '', settings: {}, originalSettings: {} }, true, [], + '', ); const result = colorizeCode({ diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx index ed68adb9c5..05794e0747 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx @@ -213,6 +213,7 @@ Another paragraph. { path: '', settings: {}, originalSettings: {} }, true, [], + '', ); const { lastFrame, unmount } = await renderWithProviders(