feat(cli): add top-level /reload command to refresh all systems

- Consolidates existing reload subcommands (/agents, /commands, /extensions, /mcp, /memory, /skills) into a single top-level /reload (alias /refresh) command.
- Adds support for refreshing settings.json from disk via LoadedSettings.reload().
- Updates all relevant tests for LoadedSettings constructor changes.
This commit is contained in:
Taylor Mullen
2026-04-01 15:09:24 -07:00
parent 0d7e778e08
commit 75d9bcb791
9 changed files with 182 additions and 4 deletions
+3
View File
@@ -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
[],
'',
);
});
+32 -4
View File
@@ -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<Settings> | 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.
@@ -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 () => {
@@ -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),
+1
View File
@@ -63,6 +63,7 @@ export const createMockSettings = (
(workspace as any) || { path: '', settings: {}, originalSettings: {} },
isTrusted ?? true,
errors || [],
'',
);
if (mergedOverride) {
@@ -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<void | SlashCommandActionReturn> {
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,
};
@@ -71,6 +71,7 @@ const createMockSettings = (
},
true,
[],
'',
);
// Mock setValue
@@ -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({
@@ -213,6 +213,7 @@ Another paragraph.
{ path: '', settings: {}, originalSettings: {} },
true,
[],
'',
);
const { lastFrame, unmount } = await renderWithProviders(