Reload gemini memory on extension load/unload + memory refresh refactor (#12651)

This commit is contained in:
Jacob MacDonald
2025-11-07 09:07:25 -08:00
committed by GitHub
parent ef4030331a
commit 47603ef8e1
15 changed files with 276 additions and 259 deletions

View File

@@ -4,21 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import process from 'node:process';
import { mcpCommand } from '../commands/mcp.js';
import type {
FileFilteringOptions,
OutputFormat,
} from '@google/gemini-cli-core';
import type { OutputFormat } from '@google/gemini-cli-core';
import { extensionsCommand } from '../commands/extensions.js';
import {
Config,
loadServerHierarchicalMemory,
setGeminiMdFilename as setServerGeminiMdFilename,
getCurrentGeminiMdFilename,
ApprovalMode,
@@ -36,6 +29,7 @@ import {
getPty,
EDIT_TOOL_NAME,
debugLogger,
loadServerHierarchicalMemory,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
@@ -47,10 +41,7 @@ import { appEvents } from '../utils/events.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { createPolicyEngineConfig } from './policy.js';
import { ExtensionManager } from './extension-manager.js';
import type {
ExtensionEvents,
ExtensionLoader,
} from '@google/gemini-cli-core/src/utils/extensionLoader.js';
import type { ExtensionEvents } from '@google/gemini-cli-core/src/utils/extensionLoader.js';
import { requestConsentNonInteractive } from './extensions/consent.js';
import { promptForSetting } from './extensions/extensionSettings.js';
import type { EventEmitter } from 'node:stream';
@@ -297,49 +288,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
return result as unknown as CliArgs;
}
// This function is now a thin wrapper around the server's implementation.
// It's kept in the CLI for now as App.tsx directly calls it for memory refresh.
// TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself.
export async function loadHierarchicalGeminiMemory(
currentWorkingDirectory: string,
includeDirectoriesToReadGemini: readonly string[] = [],
debugMode: boolean,
fileService: FileDiscoveryService,
settings: Settings,
extensionLoader: ExtensionLoader,
folderTrust: boolean,
memoryImportFormat: 'flat' | 'tree' = 'tree',
fileFilteringOptions?: FileFilteringOptions,
): Promise<{ memoryContent: string; fileCount: number; filePaths: string[] }> {
// FIX: Use real, canonical paths for a reliable comparison to handle symlinks.
const realCwd = fs.realpathSync(path.resolve(currentWorkingDirectory));
const realHome = fs.realpathSync(path.resolve(homedir()));
const isHomeDirectory = realCwd === realHome;
// If it is the home directory, pass an empty string to the core memory
// function to signal that it should skip the workspace search.
const effectiveCwd = isHomeDirectory ? '' : currentWorkingDirectory;
if (debugMode) {
debugLogger.debug(
`CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory} (memoryImportFormat: ${memoryImportFormat})`,
);
}
// Directly call the server function with the corrected path.
return loadServerHierarchicalMemory(
effectiveCwd,
includeDirectoriesToReadGemini,
debugMode,
fileService,
extensionLoader,
folderTrust,
memoryImportFormat,
fileFilteringOptions,
settings.context?.discoveryMaxDirs,
);
}
/**
* Creates a filter function to determine if a tool should be excluded.
*
@@ -437,18 +385,18 @@ export async function loadCliConfig(
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
const { memoryContent, fileCount, filePaths } =
await loadHierarchicalGeminiMemory(
await loadServerHierarchicalMemory(
cwd,
settings.context?.loadMemoryFromIncludeDirectories
? includeDirectories
: [],
debugMode,
fileService,
settings,
extensionManager,
trustedFolder,
memoryImportFormat,
memoryFileFiltering,
settings.context?.discoveryMaxDirs,
);
const question = argv.promptInteractive || argv.prompt || '';

View File

@@ -48,10 +48,11 @@ import {
debugLogger,
coreEvents,
CoreEvent,
refreshServerHierarchicalMemory,
type ModelChangedPayload,
type MemoryChangedPayload,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
import process from 'node:process';
import { useHistory } from './hooks/useHistoryManager.js';
import { useMemoryMonitor } from './hooks/useMemoryMonitor.js';
@@ -160,9 +161,6 @@ export const AppContainer = (props: AppContainerProps) => {
const [showDebugProfiler, setShowDebugProfiler] = useState(false);
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(
initializationResult.geminiMdFileCount,
);
const [shellModeActive, setShellModeActive] = useState(false);
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
useState<boolean>(false);
@@ -569,7 +567,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
refreshStatic,
toggleVimEnabled,
setIsProcessing,
setGeminiMdFileCount,
slashCommandActions,
extensionsUpdateStateInternal,
isConfigInitialized,
@@ -584,26 +581,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
Date.now(),
);
try {
const { memoryContent, fileCount, filePaths } =
await loadHierarchicalGeminiMemory(
process.cwd(),
settings.merged.context?.loadMemoryFromIncludeDirectories
? config.getWorkspaceContext().getDirectories()
: [],
config.getDebugMode(),
config.getFileService(),
settings.merged,
config.getExtensionLoader(),
config.isTrustedFolder(),
settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(),
);
config.setUserMemory(memoryContent);
config.setGeminiMdFileCount(fileCount);
config.setGeminiMdFilePaths(filePaths);
setGeminiMdFileCount(fileCount);
const { memoryContent, fileCount } =
await refreshServerHierarchicalMemory(config);
historyManager.addItem(
{
@@ -635,7 +614,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
);
debugLogger.warn('Error refreshing memory:', error);
}
}, [config, historyManager, settings.merged]);
}, [config, historyManager]);
const cancelHandlerRef = useRef<() => void>(() => {});
@@ -1248,6 +1227,19 @@ Logging in with Google... Please restart Gemini CLI to continue.
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
);
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(
config.getGeminiMdFileCount(),
);
useEffect(() => {
const handleMemoryChanged = (result: MemoryChangedPayload) => {
setGeminiMdFileCount(result.fileCount);
};
coreEvents.on(CoreEvent.MemoryChanged, handleMemoryChanged);
return () => {
coreEvents.off(CoreEvent.MemoryChanged, handleMemoryChanged);
};
}, []);
const uiState: UIState = useMemo(
() => ({
history: historyManager.history,

View File

@@ -9,7 +9,7 @@ import { CommandKind } from './types.js';
import { MessageType } from '../types.js';
import * as os from 'node:os';
import * as path from 'node:path';
import { loadServerHierarchicalMemory } from '@google/gemini-cli-core';
import { refreshServerHierarchicalMemory } from '@google/gemini-cli-core';
export function expandHomeDir(p: string): string {
if (!p) {
@@ -94,25 +94,7 @@ export const directoryCommand: SlashCommand = {
try {
if (config.shouldLoadMemoryFromIncludeDirectories()) {
const { memoryContent, fileCount } =
await loadServerHierarchicalMemory(
config.getWorkingDir(),
[
...config.getWorkspaceContext().getDirectories(),
...pathsToAdd,
],
config.getDebugMode(),
config.getFileService(),
config.getExtensionLoader(),
config.getFolderTrust(),
context.services.settings.merged.context?.importFormat ||
'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(),
context.services.settings.merged.context?.discoveryMaxDirs,
);
config.setUserMemory(memoryContent);
config.setGeminiMdFileCount(fileCount);
context.ui.setGeminiMdFileCount(fileCount);
await refreshServerHierarchicalMemory(config);
}
addItem(
{

View File

@@ -13,11 +13,11 @@ import { MessageType } from '../types.js';
import type { LoadedSettings } from '../../config/settings.js';
import {
getErrorMessage,
refreshServerHierarchicalMemory,
SimpleExtensionLoader,
type FileDiscoveryService,
} from '@google/gemini-cli-core';
import type { LoadServerHierarchicalMemoryResponse } from '@google/gemini-cli-core/index.js';
import { loadHierarchicalGeminiMemory } from '../../config/config.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
@@ -28,19 +28,12 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
if (error instanceof Error) return error.message;
return String(error);
}),
refreshServerHierarchicalMemory: vi.fn(),
};
});
vi.mock('../../config/config.js', async (importOriginal) => {
const original =
await importOriginal<typeof import('../../config/config.js')>();
return {
...original,
loadHierarchicalGeminiMemory: vi.fn(),
};
});
const mockLoadHierarchicalGeminiMemory = loadHierarchicalGeminiMemory as Mock;
const mockRefreshServerHierarchicalMemory =
refreshServerHierarchicalMemory as Mock;
describe('memoryCommand', () => {
let mockContext: CommandContext;
@@ -203,11 +196,8 @@ describe('memoryCommand', () => {
},
} as unknown as LoadedSettings,
},
ui: {
setGeminiMdFileCount: vi.fn(),
},
});
mockLoadHierarchicalGeminiMemory.mockClear();
mockRefreshServerHierarchicalMemory.mockClear();
});
it('should display success message when memory is refreshed with content', async () => {
@@ -218,7 +208,7 @@ describe('memoryCommand', () => {
fileCount: 2,
filePaths: ['/path/one/GEMINI.md', '/path/two/GEMINI.md'],
};
mockLoadHierarchicalGeminiMemory.mockResolvedValue(refreshResult);
mockRefreshServerHierarchicalMemory.mockResolvedValue(refreshResult);
await refreshCommand.action(mockContext, '');
@@ -230,19 +220,7 @@ describe('memoryCommand', () => {
expect.any(Number),
);
expect(mockLoadHierarchicalGeminiMemory).toHaveBeenCalledOnce();
expect(mockSetUserMemory).toHaveBeenCalledWith(
refreshResult.memoryContent,
);
expect(mockSetGeminiMdFileCount).toHaveBeenCalledWith(
refreshResult.fileCount,
);
expect(mockSetGeminiMdFilePaths).toHaveBeenCalledWith(
refreshResult.filePaths,
);
expect(mockContext.ui.setGeminiMdFileCount).toHaveBeenCalledWith(
refreshResult.fileCount,
);
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
@@ -257,14 +235,11 @@ describe('memoryCommand', () => {
if (!refreshCommand.action) throw new Error('Command has no action');
const refreshResult = { memoryContent: '', fileCount: 0, filePaths: [] };
mockLoadHierarchicalGeminiMemory.mockResolvedValue(refreshResult);
mockRefreshServerHierarchicalMemory.mockResolvedValue(refreshResult);
await refreshCommand.action(mockContext, '');
expect(mockLoadHierarchicalGeminiMemory).toHaveBeenCalledOnce();
expect(mockSetUserMemory).toHaveBeenCalledWith('');
expect(mockSetGeminiMdFileCount).toHaveBeenCalledWith(0);
expect(mockSetGeminiMdFilePaths).toHaveBeenCalledWith([]);
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
@@ -279,11 +254,11 @@ describe('memoryCommand', () => {
if (!refreshCommand.action) throw new Error('Command has no action');
const error = new Error('Failed to read memory files.');
mockLoadHierarchicalGeminiMemory.mockRejectedValue(error);
mockRefreshServerHierarchicalMemory.mockRejectedValue(error);
await refreshCommand.action(mockContext, '');
expect(mockLoadHierarchicalGeminiMemory).toHaveBeenCalledOnce();
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockSetUserMemory).not.toHaveBeenCalled();
expect(mockSetGeminiMdFileCount).not.toHaveBeenCalled();
expect(mockSetGeminiMdFilePaths).not.toHaveBeenCalled();
@@ -318,7 +293,7 @@ describe('memoryCommand', () => {
expect.any(Number),
);
expect(mockLoadHierarchicalGeminiMemory).not.toHaveBeenCalled();
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
});
});

View File

@@ -4,9 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { getErrorMessage } from '@google/gemini-cli-core';
import {
getErrorMessage,
refreshServerHierarchicalMemory,
} from '@google/gemini-cli-core';
import { MessageType } from '../types.js';
import { loadHierarchicalGeminiMemory } from '../../config/config.js';
import type { SlashCommand, SlashCommandActionReturn } from './types.js';
import { CommandKind } from './types.js';
@@ -80,26 +82,9 @@ export const memoryCommand: SlashCommand = {
try {
const config = await context.services.config;
const settings = context.services.settings;
if (config) {
const { memoryContent, fileCount, filePaths } =
await loadHierarchicalGeminiMemory(
config.getWorkingDir(),
config.shouldLoadMemoryFromIncludeDirectories()
? config.getWorkspaceContext().getDirectories()
: [],
config.getDebugMode(),
config.getFileService(),
settings.merged,
config.getExtensionLoader(),
config.isTrustedFolder(),
settings.merged.context?.importFormat || 'tree',
config.getFileFilteringOptions(),
);
config.setUserMemory(memoryContent);
config.setGeminiMdFileCount(fileCount);
config.setGeminiMdFilePaths(filePaths);
context.ui.setGeminiMdFileCount(fileCount);
const { memoryContent, fileCount } =
await refreshServerHierarchicalMemory(config);
const successMessage =
memoryContent.length > 0

View File

@@ -68,7 +68,6 @@ export interface CommandContext {
toggleCorgiMode: () => void;
toggleDebugProfiler: () => void;
toggleVimEnabled: () => Promise<boolean>;
setGeminiMdFileCount: (count: number) => void;
reloadCommands: () => void;
extensionsUpdateState: Map<string, ExtensionUpdateStatus>;
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;

View File

@@ -164,7 +164,6 @@ describe('useSlashCommandProcessor', () => {
vi.fn(), // refreshStatic
vi.fn(), // toggleVimEnabled
setIsProcessing,
vi.fn(), // setGeminiMdFileCount
{
openAuthDialog: mockOpenAuthDialog,
openThemeDialog: mockOpenThemeDialog,

View File

@@ -73,7 +73,6 @@ export const useSlashCommandProcessor = (
refreshStatic: () => void,
toggleVimEnabled: () => Promise<boolean>,
setIsProcessing: (isProcessing: boolean) => void,
setGeminiMdFileCount: (count: number) => void,
actions: SlashCommandProcessorActions,
extensionsUpdateState: Map<string, ExtensionUpdateStatus>,
isConfigInitialized: boolean,
@@ -207,7 +206,6 @@ export const useSlashCommandProcessor = (
toggleCorgiMode: actions.toggleCorgiMode,
toggleDebugProfiler: actions.toggleDebugProfiler,
toggleVimEnabled,
setGeminiMdFileCount,
reloadCommands,
extensionsUpdateState,
dispatchExtensionStateUpdate: actions.dispatchExtensionStateUpdate,
@@ -234,7 +232,6 @@ export const useSlashCommandProcessor = (
setPendingItem,
toggleVimEnabled,
sessionShellAllowlist,
setGeminiMdFileCount,
reloadCommands,
extensionsUpdateState,
],

View File

@@ -23,7 +23,6 @@ export function createNonInteractiveUI(): CommandContext['ui'] {
toggleCorgiMode: () => {},
toggleDebugProfiler: () => {},
toggleVimEnabled: async () => false,
setGeminiMdFileCount: (_count) => {},
reloadCommands: () => {},
extensionsUpdateState: new Map(),
dispatchExtensionStateUpdate: (_action: ExtensionUpdateAction) => {},