feat: Add /config refresh command (#4993)

Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
This commit is contained in:
Ramón Medrano Llamas 2025-07-28 17:46:43 +02:00 committed by GitHub
parent e275441651
commit 0170791800
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 168 additions and 40 deletions

View File

@ -29,6 +29,7 @@ import { statsCommand } from '../ui/commands/statsCommand.js';
import { themeCommand } from '../ui/commands/themeCommand.js'; import { themeCommand } from '../ui/commands/themeCommand.js';
import { toolsCommand } from '../ui/commands/toolsCommand.js'; import { toolsCommand } from '../ui/commands/toolsCommand.js';
import { vimCommand } from '../ui/commands/vimCommand.js'; import { vimCommand } from '../ui/commands/vimCommand.js';
import { configCommand } from '../ui/commands/configCommand.js';
/** /**
* Loads the core, hard-coded slash commands that are an integral part * Loads the core, hard-coded slash commands that are an integral part
@ -54,6 +55,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
compressCommand, compressCommand,
copyCommand, copyCommand,
corgiCommand, corgiCommand,
configCommand,
docsCommand, docsCommand,
editorCommand, editorCommand,
extensionsCommand, extensionsCommand,

View File

@ -39,8 +39,12 @@ import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js'; import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
import { Colors } from './colors.js'; import { Colors } from './colors.js';
import { Help } from './components/Help.js'; import { Help } from './components/Help.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js'; import {
import { LoadedSettings } from '../config/settings.js'; loadHierarchicalGeminiMemory,
loadCliConfig,
parseArguments,
} from '../config/config.js';
import { LoadedSettings, loadSettings } from '../config/settings.js';
import { Tips } from './components/Tips.js'; import { Tips } from './components/Tips.js';
import { ConsolePatcher } from './utils/ConsolePatcher.js'; import { ConsolePatcher } from './utils/ConsolePatcher.js';
import { registerCleanup } from '../utils/cleanup.js'; import { registerCleanup } from '../utils/cleanup.js';
@ -62,6 +66,7 @@ import {
AuthType, AuthType,
type IdeContext, type IdeContext,
ideContext, ideContext,
sessionId,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js'; import { validateAuthMethod } from '../config/auth.js';
import { useLogger } from './hooks/useLogger.js'; import { useLogger } from './hooks/useLogger.js';
@ -89,6 +94,7 @@ import { OverflowProvider } from './contexts/OverflowContext.js';
import { ShowMoreLines } from './components/ShowMoreLines.js'; import { ShowMoreLines } from './components/ShowMoreLines.js';
import { PrivacyNotice } from './privacy/PrivacyNotice.js'; import { PrivacyNotice } from './privacy/PrivacyNotice.js';
import { appEvents, AppEvent } from '../utils/events.js'; import { appEvents, AppEvent } from '../utils/events.js';
import { loadExtensions } from '../config/extension.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000; const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
@ -107,12 +113,14 @@ export const AppWrapper = (props: AppProps) => (
</SessionStatsProvider> </SessionStatsProvider>
); );
const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const App = (props: AppProps) => {
const [config, setConfig] = useState<Config>(props.config);
const [settings, setSettings] = useState<LoadedSettings>(props.settings);
const isFocused = useFocus(); const isFocused = useFocus();
useBracketedPaste(); useBracketedPaste();
const [updateMessage, setUpdateMessage] = useState<string | null>(null); const [updateMessage, setUpdateMessage] = useState<string | null>(null);
const { stdout } = useStdout(); const { stdout } = useStdout();
const nightly = version.includes('nightly'); const nightly = props.version.includes('nightly');
useEffect(() => { useEffect(() => {
checkForUpdates().then(setUpdateMessage); checkForUpdates().then(setUpdateMessage);
@ -307,6 +315,22 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
} }
}, [config, addItem, settings.merged]); }, [config, addItem, settings.merged]);
const refreshConfig = useCallback(async () => {
const newSettings = loadSettings(process.cwd());
const newExtensions = loadExtensions(process.cwd());
const argv = await parseArguments();
const newConfig = await loadCliConfig(
newSettings.merged,
newExtensions,
sessionId,
argv,
);
await newConfig.initialize();
setConfig(newConfig);
setSettings(newSettings);
setGeminiMdFileCount(newConfig.getGeminiMdFileCount());
}, []);
// Watch for model changes (e.g., from Flash fallback) // Watch for model changes (e.g., from Flash fallback)
useEffect(() => { useEffect(() => {
const checkModelChange = () => { const checkModelChange = () => {
@ -474,6 +498,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
openPrivacyNotice, openPrivacyNotice,
toggleVimEnabled, toggleVimEnabled,
setIsProcessing, setIsProcessing,
refreshConfig,
); );
const { const {
@ -777,7 +802,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
{!settings.merged.hideBanner && ( {!settings.merged.hideBanner && (
<Header <Header
terminalWidth={terminalWidth} terminalWidth={terminalWidth}
version={version} version={props.version}
nightly={nightly} nightly={nightly}
/> />
)} )}
@ -821,7 +846,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
{showHelp && <Help commands={slashCommands} />} {showHelp && <Help commands={slashCommands} />}
<Box flexDirection="column" ref={mainControlsRef}> <Box flexDirection="column" ref={mainControlsRef}>
{startupWarnings.length > 0 && ( {props.startupWarnings && props.startupWarnings.length > 0 && (
<Box <Box
borderStyle="round" borderStyle="round"
borderColor={Colors.AccentYellow} borderColor={Colors.AccentYellow}
@ -829,7 +854,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
marginY={1} marginY={1}
flexDirection="column" flexDirection="column"
> >
{startupWarnings.map((warning, index) => ( {props.startupWarnings.map((warning, index) => (
<Text key={index} color={Colors.AccentYellow}> <Text key={index} color={Colors.AccentYellow}>
{warning} {warning}
</Text> </Text>

View File

@ -0,0 +1,33 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
CommandKind,
SlashCommand,
SlashCommandActionReturn,
} from './types.js';
export const configCommand: SlashCommand = {
name: 'config',
description: 'Commands for interacting with the CLI configuration.',
kind: CommandKind.BUILT_IN,
subCommands: [
{
name: 'refresh',
description: 'Reload settings and extensions from the filesystem.',
kind: CommandKind.BUILT_IN,
action: async (context): Promise<SlashCommandActionReturn> => {
await context.ui.refreshConfig();
return {
type: 'message',
messageType: 'info',
content:
'Configuration, extensions, memory, and tools have been refreshed.',
};
},
},
],
};

View File

@ -59,6 +59,7 @@ export interface CommandContext {
/** Toggles a special display mode. */ /** Toggles a special display mode. */
toggleCorgiMode: () => void; toggleCorgiMode: () => void;
toggleVimEnabled: () => Promise<boolean>; toggleVimEnabled: () => Promise<boolean>;
refreshConfig: () => Promise<void>;
}; };
// Session-specific data // Session-specific data
session: { session: {

View File

@ -50,6 +50,7 @@ export const useSlashCommandProcessor = (
openPrivacyNotice: () => void, openPrivacyNotice: () => void,
toggleVimEnabled: () => Promise<boolean>, toggleVimEnabled: () => Promise<boolean>,
setIsProcessing: (isProcessing: boolean) => void, setIsProcessing: (isProcessing: boolean) => void,
refreshConfig: () => Promise<void>,
) => { ) => {
const session = useSessionStats(); const session = useSessionStats();
const [commands, setCommands] = useState<readonly SlashCommand[]>([]); const [commands, setCommands] = useState<readonly SlashCommand[]>([]);
@ -158,6 +159,7 @@ export const useSlashCommandProcessor = (
setPendingItem: setPendingCompressionItem, setPendingItem: setPendingCompressionItem,
toggleCorgiMode, toggleCorgiMode,
toggleVimEnabled, toggleVimEnabled,
refreshConfig,
}, },
session: { session: {
stats: session.stats, stats: session.stats,
@ -180,6 +182,7 @@ export const useSlashCommandProcessor = (
toggleCorgiMode, toggleCorgiMode,
toggleVimEnabled, toggleVimEnabled,
sessionShellAllowlist, sessionShellAllowlist,
refreshConfig,
], ],
); );

View File

@ -188,60 +188,62 @@ export interface ConfigParameters {
export class Config { export class Config {
private toolRegistry!: ToolRegistry; private toolRegistry!: ToolRegistry;
private promptRegistry!: PromptRegistry; private promptRegistry!: PromptRegistry;
private readonly sessionId: string; private sessionId: string;
private contentGeneratorConfig!: ContentGeneratorConfig; private contentGeneratorConfig!: ContentGeneratorConfig;
private readonly embeddingModel: string; private embeddingModel: string;
private readonly sandbox: SandboxConfig | undefined; private sandbox: SandboxConfig | undefined;
private readonly targetDir: string; private targetDir: string;
private readonly debugMode: boolean; private debugMode: boolean;
private readonly question: string | undefined; private question: string | undefined;
private readonly fullContext: boolean; private fullContext: boolean;
private readonly coreTools: string[] | undefined; private coreTools: string[] | undefined;
private readonly excludeTools: string[] | undefined; private excludeTools: string[] | undefined;
private readonly toolDiscoveryCommand: string | undefined; private toolDiscoveryCommand: string | undefined;
private readonly toolCallCommand: string | undefined; private toolCallCommand: string | undefined;
private readonly mcpServerCommand: string | undefined; private mcpServerCommand: string | undefined;
private readonly mcpServers: Record<string, MCPServerConfig> | undefined; private mcpServers: Record<string, MCPServerConfig> | undefined;
private userMemory: string; private userMemory: string;
private geminiMdFileCount: number; private geminiMdFileCount: number;
private approvalMode: ApprovalMode; private approvalMode: ApprovalMode;
private readonly showMemoryUsage: boolean; private showMemoryUsage: boolean;
private readonly accessibility: AccessibilitySettings; private accessibility: AccessibilitySettings;
private readonly telemetrySettings: TelemetrySettings; private telemetrySettings: TelemetrySettings;
private readonly usageStatisticsEnabled: boolean; private usageStatisticsEnabled: boolean;
private geminiClient!: GeminiClient; private geminiClient!: GeminiClient;
private readonly fileFiltering: { private fileFiltering: {
respectGitIgnore: boolean; respectGitIgnore: boolean;
respectGeminiIgnore: boolean; respectGeminiIgnore: boolean;
enableRecursiveFileSearch: boolean; enableRecursiveFileSearch: boolean;
}; };
private fileDiscoveryService: FileDiscoveryService | null = null; private fileDiscoveryService: FileDiscoveryService | null = null;
private gitService: GitService | undefined = undefined; private gitService: GitService | undefined = undefined;
private readonly checkpointing: boolean; private checkpointing: boolean;
private readonly proxy: string | undefined; private proxy: string | undefined;
private readonly cwd: string; private cwd: string;
private readonly bugCommand: BugCommandSettings | undefined; private bugCommand: BugCommandSettings | undefined;
private readonly model: string; private model: string;
private readonly extensionContextFilePaths: string[]; private extensionContextFilePaths: string[];
private readonly noBrowser: boolean; private noBrowser: boolean;
private readonly ideMode: boolean; private ideMode: boolean;
private readonly ideClient: IdeClient | undefined; private ideClient: IdeClient | undefined;
private modelSwitchedDuringSession: boolean = false; private modelSwitchedDuringSession: boolean = false;
private readonly maxSessionTurns: number; private maxSessionTurns: number;
private readonly listExtensions: boolean; private listExtensions: boolean;
private readonly _extensions: GeminiCLIExtension[]; private _extensions: GeminiCLIExtension[];
private readonly _blockedMcpServers: Array<{ private _blockedMcpServers: Array<{
name: string; name: string;
extensionName: string; extensionName: string;
}>; }>;
flashFallbackHandler?: FlashFallbackHandler; flashFallbackHandler?: FlashFallbackHandler;
private quotaErrorOccurred: boolean = false; private quotaErrorOccurred: boolean = false;
private readonly summarizeToolOutput: private summarizeToolOutput:
| Record<string, SummarizeToolOutputSettings> | Record<string, SummarizeToolOutputSettings>
| undefined; | undefined;
private readonly experimentalAcp: boolean = false; private experimentalAcp: boolean = false;
private _params: ConfigParameters;
constructor(params: ConfigParameters) { constructor(params: ConfigParameters) {
this._params = params;
this.sessionId = params.sessionId; this.sessionId = params.sessionId;
this.embeddingModel = this.embeddingModel =
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL; params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
@ -310,6 +312,68 @@ export class Config {
} }
} }
async refresh() {
// Re-run initialization logic.
await this.initialize();
// After re-initializing, the tool registry will be updated.
// We need to update the gemini client with the new tools.
await this.geminiClient.setTools();
}
update(params: ConfigParameters) {
this._params = params;
// Re-assign all properties from the new params.
this.sessionId = params.sessionId;
this.embeddingModel =
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
this.sandbox = params.sandbox;
this.targetDir = path.resolve(params.targetDir);
this.debugMode = params.debugMode;
this.question = params.question;
this.fullContext = params.fullContext ?? false;
this.coreTools = params.coreTools;
this.excludeTools = params.excludeTools;
this.toolDiscoveryCommand = params.toolDiscoveryCommand;
this.toolCallCommand = params.toolCallCommand;
this.mcpServerCommand = params.mcpServerCommand;
this.mcpServers = params.mcpServers;
this.userMemory = params.userMemory ?? '';
this.geminiMdFileCount = params.geminiMdFileCount ?? 0;
this.approvalMode = params.approvalMode ?? ApprovalMode.DEFAULT;
this.showMemoryUsage = params.showMemoryUsage ?? false;
this.accessibility = params.accessibility ?? {};
this.telemetrySettings = {
enabled: params.telemetry?.enabled ?? false,
target: params.telemetry?.target ?? DEFAULT_TELEMETRY_TARGET,
otlpEndpoint: params.telemetry?.otlpEndpoint ?? DEFAULT_OTLP_ENDPOINT,
logPrompts: params.telemetry?.logPrompts ?? true,
outfile: params.telemetry?.outfile,
};
this.usageStatisticsEnabled = params.usageStatisticsEnabled ?? true;
this.fileFiltering = {
respectGitIgnore: params.fileFiltering?.respectGitIgnore ?? true,
respectGeminiIgnore: params.fileFiltering?.respectGeminiIgnore ?? true,
enableRecursiveFileSearch:
params.fileFiltering?.enableRecursiveFileSearch ?? true,
};
this.checkpointing = params.checkpointing ?? false;
this.proxy = params.proxy;
this.cwd = params.cwd ?? process.cwd();
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
this.bugCommand = params.bugCommand;
this.model = params.model;
this.extensionContextFilePaths = params.extensionContextFilePaths ?? [];
this.maxSessionTurns = params.maxSessionTurns ?? -1;
this.experimentalAcp = params.experimentalAcp ?? false;
this.listExtensions = params.listExtensions ?? false;
this._extensions = params.extensions ?? [];
this._blockedMcpServers = params.blockedMcpServers ?? [];
this.noBrowser = params.noBrowser ?? false;
this.summarizeToolOutput = params.summarizeToolOutput;
this.ideMode = params.ideMode ?? false;
this.ideClient = params.ideClient;
}
async initialize(): Promise<void> { async initialize(): Promise<void> {
// Initialize centralized FileDiscoveryService // Initialize centralized FileDiscoveryService
this.getFileService(); this.getFileService();