From 0170791800183b81e2afc98f8fb2368219bfb3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Medrano=20Llamas?= <45878745+rmedranollamas@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:46:43 +0200 Subject: [PATCH] feat: Add /config refresh command (#4993) Co-authored-by: Bryan Morgan --- .../cli/src/services/BuiltinCommandLoader.ts | 2 + packages/cli/src/ui/App.tsx | 39 +++++- packages/cli/src/ui/commands/configCommand.ts | 33 +++++ packages/cli/src/ui/commands/types.ts | 1 + .../cli/src/ui/hooks/slashCommandProcessor.ts | 3 + packages/core/src/config/config.ts | 130 +++++++++++++----- 6 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 packages/cli/src/ui/commands/configCommand.ts diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 7ba0d6bb..b0c85d2a 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -29,6 +29,7 @@ import { statsCommand } from '../ui/commands/statsCommand.js'; import { themeCommand } from '../ui/commands/themeCommand.js'; import { toolsCommand } from '../ui/commands/toolsCommand.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 @@ -54,6 +55,7 @@ export class BuiltinCommandLoader implements ICommandLoader { compressCommand, copyCommand, corgiCommand, + configCommand, docsCommand, editorCommand, extensionsCommand, diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index aacf45d7..43060fdb 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -39,8 +39,12 @@ import { EditorSettingsDialog } from './components/EditorSettingsDialog.js'; import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js'; import { Colors } from './colors.js'; import { Help } from './components/Help.js'; -import { loadHierarchicalGeminiMemory } from '../config/config.js'; -import { LoadedSettings } from '../config/settings.js'; +import { + loadHierarchicalGeminiMemory, + loadCliConfig, + parseArguments, +} from '../config/config.js'; +import { LoadedSettings, loadSettings } from '../config/settings.js'; import { Tips } from './components/Tips.js'; import { ConsolePatcher } from './utils/ConsolePatcher.js'; import { registerCleanup } from '../utils/cleanup.js'; @@ -62,6 +66,7 @@ import { AuthType, type IdeContext, ideContext, + sessionId, } from '@google/gemini-cli-core'; import { validateAuthMethod } from '../config/auth.js'; import { useLogger } from './hooks/useLogger.js'; @@ -89,6 +94,7 @@ import { OverflowProvider } from './contexts/OverflowContext.js'; import { ShowMoreLines } from './components/ShowMoreLines.js'; import { PrivacyNotice } from './privacy/PrivacyNotice.js'; import { appEvents, AppEvent } from '../utils/events.js'; +import { loadExtensions } from '../config/extension.js'; const CTRL_EXIT_PROMPT_DURATION_MS = 1000; @@ -107,12 +113,14 @@ export const AppWrapper = (props: AppProps) => ( ); -const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { +const App = (props: AppProps) => { + const [config, setConfig] = useState(props.config); + const [settings, setSettings] = useState(props.settings); const isFocused = useFocus(); useBracketedPaste(); const [updateMessage, setUpdateMessage] = useState(null); const { stdout } = useStdout(); - const nightly = version.includes('nightly'); + const nightly = props.version.includes('nightly'); useEffect(() => { checkForUpdates().then(setUpdateMessage); @@ -307,6 +315,22 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { } }, [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) useEffect(() => { const checkModelChange = () => { @@ -474,6 +498,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { openPrivacyNotice, toggleVimEnabled, setIsProcessing, + refreshConfig, ); const { @@ -777,7 +802,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { {!settings.merged.hideBanner && (
)} @@ -821,7 +846,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { {showHelp && } - {startupWarnings.length > 0 && ( + {props.startupWarnings && props.startupWarnings.length > 0 && ( { marginY={1} flexDirection="column" > - {startupWarnings.map((warning, index) => ( + {props.startupWarnings.map((warning, index) => ( {warning} diff --git a/packages/cli/src/ui/commands/configCommand.ts b/packages/cli/src/ui/commands/configCommand.ts new file mode 100644 index 00000000..3651b221 --- /dev/null +++ b/packages/cli/src/ui/commands/configCommand.ts @@ -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 => { + await context.ui.refreshConfig(); + return { + type: 'message', + messageType: 'info', + content: + 'Configuration, extensions, memory, and tools have been refreshed.', + }; + }, + }, + ], +}; diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts index 2844177f..6665da4b 100644 --- a/packages/cli/src/ui/commands/types.ts +++ b/packages/cli/src/ui/commands/types.ts @@ -59,6 +59,7 @@ export interface CommandContext { /** Toggles a special display mode. */ toggleCorgiMode: () => void; toggleVimEnabled: () => Promise; + refreshConfig: () => Promise; }; // Session-specific data session: { diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index be32de11..67e49c21 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -50,6 +50,7 @@ export const useSlashCommandProcessor = ( openPrivacyNotice: () => void, toggleVimEnabled: () => Promise, setIsProcessing: (isProcessing: boolean) => void, + refreshConfig: () => Promise, ) => { const session = useSessionStats(); const [commands, setCommands] = useState([]); @@ -158,6 +159,7 @@ export const useSlashCommandProcessor = ( setPendingItem: setPendingCompressionItem, toggleCorgiMode, toggleVimEnabled, + refreshConfig, }, session: { stats: session.stats, @@ -180,6 +182,7 @@ export const useSlashCommandProcessor = ( toggleCorgiMode, toggleVimEnabled, sessionShellAllowlist, + refreshConfig, ], ); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 7ccfdbc8..e03abe8a 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -188,60 +188,62 @@ export interface ConfigParameters { export class Config { private toolRegistry!: ToolRegistry; private promptRegistry!: PromptRegistry; - private readonly sessionId: string; + private sessionId: string; private contentGeneratorConfig!: ContentGeneratorConfig; - private readonly embeddingModel: string; - private readonly sandbox: SandboxConfig | undefined; - private readonly targetDir: string; - private readonly debugMode: boolean; - private readonly question: string | undefined; - private readonly fullContext: boolean; - private readonly coreTools: string[] | undefined; - private readonly excludeTools: string[] | undefined; - private readonly toolDiscoveryCommand: string | undefined; - private readonly toolCallCommand: string | undefined; - private readonly mcpServerCommand: string | undefined; - private readonly mcpServers: Record | undefined; + private embeddingModel: string; + private sandbox: SandboxConfig | undefined; + private targetDir: string; + private debugMode: boolean; + private question: string | undefined; + private fullContext: boolean; + private coreTools: string[] | undefined; + private excludeTools: string[] | undefined; + private toolDiscoveryCommand: string | undefined; + private toolCallCommand: string | undefined; + private mcpServerCommand: string | undefined; + private mcpServers: Record | undefined; private userMemory: string; private geminiMdFileCount: number; private approvalMode: ApprovalMode; - private readonly showMemoryUsage: boolean; - private readonly accessibility: AccessibilitySettings; - private readonly telemetrySettings: TelemetrySettings; - private readonly usageStatisticsEnabled: boolean; + private showMemoryUsage: boolean; + private accessibility: AccessibilitySettings; + private telemetrySettings: TelemetrySettings; + private usageStatisticsEnabled: boolean; private geminiClient!: GeminiClient; - private readonly fileFiltering: { + private fileFiltering: { respectGitIgnore: boolean; respectGeminiIgnore: boolean; enableRecursiveFileSearch: boolean; }; private fileDiscoveryService: FileDiscoveryService | null = null; private gitService: GitService | undefined = undefined; - private readonly checkpointing: boolean; - private readonly proxy: string | undefined; - private readonly cwd: string; - private readonly bugCommand: BugCommandSettings | undefined; - private readonly model: string; - private readonly extensionContextFilePaths: string[]; - private readonly noBrowser: boolean; - private readonly ideMode: boolean; - private readonly ideClient: IdeClient | undefined; + private checkpointing: boolean; + private proxy: string | undefined; + private cwd: string; + private bugCommand: BugCommandSettings | undefined; + private model: string; + private extensionContextFilePaths: string[]; + private noBrowser: boolean; + private ideMode: boolean; + private ideClient: IdeClient | undefined; private modelSwitchedDuringSession: boolean = false; - private readonly maxSessionTurns: number; - private readonly listExtensions: boolean; - private readonly _extensions: GeminiCLIExtension[]; - private readonly _blockedMcpServers: Array<{ + private maxSessionTurns: number; + private listExtensions: boolean; + private _extensions: GeminiCLIExtension[]; + private _blockedMcpServers: Array<{ name: string; extensionName: string; }>; flashFallbackHandler?: FlashFallbackHandler; private quotaErrorOccurred: boolean = false; - private readonly summarizeToolOutput: + private summarizeToolOutput: | Record | undefined; - private readonly experimentalAcp: boolean = false; + private experimentalAcp: boolean = false; + private _params: ConfigParameters; constructor(params: ConfigParameters) { + this._params = params; this.sessionId = params.sessionId; this.embeddingModel = 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 { // Initialize centralized FileDiscoveryService this.getFileService();