feat(memory): make directory search limit on memory discovery configurable with settings.json (#4460)

This commit is contained in:
Brandon Keiji
2025-07-23 14:48:35 -07:00
committed by GitHub
parent 9d3164621a
commit d7a304bcff
11 changed files with 104 additions and 83 deletions

View File

@@ -37,7 +37,7 @@ vi.mock('@google/gemini-cli-core', async () => {
...actualServer,
loadEnvironment: vi.fn(),
loadServerHierarchicalMemory: vi.fn(
(cwd, debug, fileService, extensionPaths) =>
(cwd, debug, fileService, extensionPaths, _maxDirs) =>
Promise.resolve({
memoryContent: extensionPaths?.join(',') || '',
fileCount: extensionPaths?.length || 0,
@@ -491,6 +491,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
respectGitIgnore: false,
respectGeminiIgnore: true,
},
undefined, // maxDirs
);
});

View File

@@ -225,6 +225,7 @@ export async function loadHierarchicalGeminiMemory(
currentWorkingDirectory: string,
debugMode: boolean,
fileService: FileDiscoveryService,
settings: Settings,
extensionContextFilePaths: string[] = [],
fileFilteringOptions?: FileFilteringOptions,
): Promise<{ memoryContent: string; fileCount: number }> {
@@ -242,6 +243,7 @@ export async function loadHierarchicalGeminiMemory(
fileService,
extensionContextFilePaths,
fileFilteringOptions,
settings.memoryDiscoveryMaxDirs,
);
}
@@ -298,6 +300,7 @@ export async function loadCliConfig(
process.cwd(),
debugMode,
fileService,
settings,
extensionContextFilePaths,
fileFiltering,
);

View File

@@ -100,6 +100,7 @@ export interface Settings {
// Add other settings here.
ideMode?: boolean;
memoryDiscoveryMaxDirs?: number;
}
export interface SettingsError {

View File

@@ -236,6 +236,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
process.cwd(),
config.getDebugMode(),
config.getFileService(),
settings.merged,
config.getExtensionContextFilePaths(),
config.getFileFilteringOptions(),
);
@@ -267,7 +268,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
);
console.error('Error refreshing memory:', error);
}
}, [config, addItem]);
}, [config, addItem, settings.merged]);
// Watch for model changes (e.g., from Flash fallback)
useEffect(() => {

View File

@@ -9,7 +9,12 @@ import { memoryCommand } from './memoryCommand.js';
import { type CommandContext, SlashCommand } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';
import { getErrorMessage } from '@google/gemini-cli-core';
import { LoadedSettings } from '../../config/settings.js';
import {
getErrorMessage,
loadServerHierarchicalMemory,
type FileDiscoveryService,
} from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
@@ -20,9 +25,12 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
if (error instanceof Error) return error.message;
return String(error);
}),
loadServerHierarchicalMemory: vi.fn(),
};
});
const mockLoadServerHierarchicalMemory = loadServerHierarchicalMemory as Mock;
describe('memoryCommand', () => {
let mockContext: CommandContext;
@@ -139,19 +147,37 @@ describe('memoryCommand', () => {
describe('/memory refresh', () => {
let refreshCommand: SlashCommand;
let mockRefreshMemory: Mock;
let mockSetUserMemory: Mock;
let mockSetGeminiMdFileCount: Mock;
beforeEach(() => {
refreshCommand = getSubCommand('refresh');
mockRefreshMemory = vi.fn();
mockSetUserMemory = vi.fn();
mockSetGeminiMdFileCount = vi.fn();
const mockConfig = {
setUserMemory: mockSetUserMemory,
setGeminiMdFileCount: mockSetGeminiMdFileCount,
getWorkingDir: () => '/test/dir',
getDebugMode: () => false,
getFileService: () => ({}) as FileDiscoveryService,
getExtensionContextFilePaths: () => [],
getFileFilteringOptions: () => ({
ignore: [],
include: [],
}),
};
mockContext = createMockCommandContext({
services: {
config: {
refreshMemory: mockRefreshMemory,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
config: Promise.resolve(mockConfig),
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
},
} as LoadedSettings,
},
});
mockLoadServerHierarchicalMemory.mockClear();
});
it('should display success message when memory is refreshed with content', async () => {
@@ -161,7 +187,7 @@ describe('memoryCommand', () => {
memoryContent: 'new memory content',
fileCount: 2,
};
mockRefreshMemory.mockResolvedValue(refreshResult);
mockLoadServerHierarchicalMemory.mockResolvedValue(refreshResult);
await refreshCommand.action(mockContext, '');
@@ -173,7 +199,13 @@ describe('memoryCommand', () => {
expect.any(Number),
);
expect(mockRefreshMemory).toHaveBeenCalledOnce();
expect(loadServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockSetUserMemory).toHaveBeenCalledWith(
refreshResult.memoryContent,
);
expect(mockSetGeminiMdFileCount).toHaveBeenCalledWith(
refreshResult.fileCount,
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
@@ -188,11 +220,13 @@ describe('memoryCommand', () => {
if (!refreshCommand.action) throw new Error('Command has no action');
const refreshResult = { memoryContent: '', fileCount: 0 };
mockRefreshMemory.mockResolvedValue(refreshResult);
mockLoadServerHierarchicalMemory.mockResolvedValue(refreshResult);
await refreshCommand.action(mockContext, '');
expect(mockRefreshMemory).toHaveBeenCalledOnce();
expect(loadServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockSetUserMemory).toHaveBeenCalledWith('');
expect(mockSetGeminiMdFileCount).toHaveBeenCalledWith(0);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
@@ -207,11 +241,13 @@ describe('memoryCommand', () => {
if (!refreshCommand.action) throw new Error('Command has no action');
const error = new Error('Failed to read memory files.');
mockRefreshMemory.mockRejectedValue(error);
mockLoadServerHierarchicalMemory.mockRejectedValue(error);
await refreshCommand.action(mockContext, '');
expect(mockRefreshMemory).toHaveBeenCalledOnce();
expect(loadServerHierarchicalMemory).toHaveBeenCalledOnce();
expect(mockSetUserMemory).not.toHaveBeenCalled();
expect(mockSetGeminiMdFileCount).not.toHaveBeenCalled();
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
@@ -243,7 +279,7 @@ describe('memoryCommand', () => {
expect.any(Number),
);
expect(mockRefreshMemory).not.toHaveBeenCalled();
expect(loadServerHierarchicalMemory).not.toHaveBeenCalled();
});
});
});

View File

@@ -4,7 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { getErrorMessage } from '@google/gemini-cli-core';
import {
getErrorMessage,
loadServerHierarchicalMemory,
} from '@google/gemini-cli-core';
import { MessageType } from '../types.js';
import {
CommandKind,
@@ -81,10 +84,20 @@ export const memoryCommand: SlashCommand = {
);
try {
const result = await context.services.config?.refreshMemory();
const config = await context.services.config;
if (config) {
const { memoryContent, fileCount } =
await loadServerHierarchicalMemory(
config.getWorkingDir(),
config.getDebugMode(),
config.getFileService(),
config.getExtensionContextFilePaths(),
config.getFileFilteringOptions(),
context.services.settings.merged.memoryDiscoveryMaxDirs,
);
config.setUserMemory(memoryContent);
config.setGeminiMdFileCount(fileCount);
if (result) {
const { memoryContent, fileCount } = result;
const successMessage =
memoryContent.length > 0
? `Memory refreshed successfully. Loaded ${memoryContent.length} characters from ${fileCount} file(s).`