mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
feat(core): Implement JIT context memory loading and UI sync (#14469)
This commit is contained in:
@@ -435,9 +435,15 @@ export async function loadCliConfig(
|
|||||||
});
|
});
|
||||||
await extensionManager.loadExtensions();
|
await extensionManager.loadExtensions();
|
||||||
|
|
||||||
|
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||||
|
|
||||||
|
let memoryContent = '';
|
||||||
|
let fileCount = 0;
|
||||||
|
let filePaths: string[] = [];
|
||||||
|
|
||||||
|
if (!experimentalJitContext) {
|
||||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||||
const { memoryContent, fileCount, filePaths } =
|
const result = await loadServerHierarchicalMemory(
|
||||||
await loadServerHierarchicalMemory(
|
|
||||||
cwd,
|
cwd,
|
||||||
[],
|
[],
|
||||||
debugMode,
|
debugMode,
|
||||||
@@ -448,6 +454,10 @@ export async function loadCliConfig(
|
|||||||
memoryFileFiltering,
|
memoryFileFiltering,
|
||||||
settings.context?.discoveryMaxDirs,
|
settings.context?.discoveryMaxDirs,
|
||||||
);
|
);
|
||||||
|
memoryContent = result.memoryContent;
|
||||||
|
fileCount = result.fileCount;
|
||||||
|
filePaths = result.filePaths;
|
||||||
|
}
|
||||||
|
|
||||||
const question = argv.promptInteractive || argv.prompt || '';
|
const question = argv.promptInteractive || argv.prompt || '';
|
||||||
|
|
||||||
|
|||||||
@@ -157,12 +157,14 @@ describe('memoryCommand', () => {
|
|||||||
let mockSetUserMemory: Mock;
|
let mockSetUserMemory: Mock;
|
||||||
let mockSetGeminiMdFileCount: Mock;
|
let mockSetGeminiMdFileCount: Mock;
|
||||||
let mockSetGeminiMdFilePaths: Mock;
|
let mockSetGeminiMdFilePaths: Mock;
|
||||||
|
let mockContextManagerRefresh: Mock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
refreshCommand = getSubCommand('refresh');
|
refreshCommand = getSubCommand('refresh');
|
||||||
mockSetUserMemory = vi.fn();
|
mockSetUserMemory = vi.fn();
|
||||||
mockSetGeminiMdFileCount = vi.fn();
|
mockSetGeminiMdFileCount = vi.fn();
|
||||||
mockSetGeminiMdFilePaths = vi.fn();
|
mockSetGeminiMdFilePaths = vi.fn();
|
||||||
|
mockContextManagerRefresh = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
setUserMemory: mockSetUserMemory,
|
setUserMemory: mockSetUserMemory,
|
||||||
@@ -185,6 +187,12 @@ describe('memoryCommand', () => {
|
|||||||
updateSystemInstructionIfInitialized: vi
|
updateSystemInstructionIfInitialized: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue(undefined),
|
.mockResolvedValue(undefined),
|
||||||
|
isJitContextEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getContextManager: vi.fn().mockReturnValue({
|
||||||
|
refresh: mockContextManagerRefresh,
|
||||||
|
}),
|
||||||
|
getUserMemory: vi.fn().mockReturnValue(''),
|
||||||
|
getGeminiMdFileCount: vi.fn().mockReturnValue(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockContext = createMockCommandContext({
|
mockContext = createMockCommandContext({
|
||||||
@@ -203,7 +211,32 @@ describe('memoryCommand', () => {
|
|||||||
mockRefreshServerHierarchicalMemory.mockClear();
|
mockRefreshServerHierarchicalMemory.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display success message when memory is refreshed with content', async () => {
|
it('should use ContextManager.refresh when JIT is enabled', async () => {
|
||||||
|
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||||
|
|
||||||
|
// Enable JIT in mock config
|
||||||
|
const config = mockContext.services.config;
|
||||||
|
if (!config) throw new Error('Config is undefined');
|
||||||
|
|
||||||
|
vi.mocked(config.isJitContextEnabled).mockReturnValue(true);
|
||||||
|
vi.mocked(config.getUserMemory).mockReturnValue('JIT Memory Content');
|
||||||
|
vi.mocked(config.getGeminiMdFileCount).mockReturnValue(3);
|
||||||
|
|
||||||
|
await refreshCommand.action(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockContextManagerRefresh).toHaveBeenCalledOnce();
|
||||||
|
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: 'Memory refreshed successfully. Loaded 18 characters from 3 file(s).',
|
||||||
|
},
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display success message when memory is refreshed with content (Legacy)', async () => {
|
||||||
if (!refreshCommand.action) throw new Error('Command has no action');
|
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||||
|
|
||||||
const refreshResult: LoadServerHierarchicalMemoryResponse = {
|
const refreshResult: LoadServerHierarchicalMemoryResponse = {
|
||||||
|
|||||||
@@ -87,8 +87,18 @@ export const memoryCommand: SlashCommand = {
|
|||||||
try {
|
try {
|
||||||
const config = context.services.config;
|
const config = context.services.config;
|
||||||
if (config) {
|
if (config) {
|
||||||
const { memoryContent, fileCount } =
|
let memoryContent = '';
|
||||||
await refreshServerHierarchicalMemory(config);
|
let fileCount = 0;
|
||||||
|
|
||||||
|
if (config.isJitContextEnabled()) {
|
||||||
|
await config.getContextManager()?.refresh();
|
||||||
|
memoryContent = config.getUserMemory();
|
||||||
|
fileCount = config.getGeminiMdFileCount();
|
||||||
|
} else {
|
||||||
|
const result = await refreshServerHierarchicalMemory(config);
|
||||||
|
memoryContent = result.memoryContent;
|
||||||
|
fileCount = result.fileCount;
|
||||||
|
}
|
||||||
|
|
||||||
await config.updateSystemInstructionIfInitialized();
|
await config.updateSystemInstructionIfInitialized();
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ vi.mock('../tools/tool-registry', () => {
|
|||||||
return { ToolRegistry: ToolRegistryMock };
|
return { ToolRegistry: ToolRegistryMock };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('../tools/mcp-client-manager.js', () => ({
|
||||||
|
McpClientManager: vi.fn().mockImplementation(() => ({
|
||||||
|
startConfiguredMcpServers: vi.fn(),
|
||||||
|
getMcpInstructions: vi.fn().mockReturnValue('MCP Instructions'),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../utils/memoryDiscovery.js', () => ({
|
vi.mock('../utils/memoryDiscovery.js', () => ({
|
||||||
loadServerHierarchicalMemory: vi.fn(),
|
loadServerHierarchicalMemory: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -168,12 +175,15 @@ vi.mock('../utils/fetch.js', () => ({
|
|||||||
setGlobalProxy: mockSetGlobalProxy,
|
setGlobalProxy: mockSetGlobalProxy,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../services/contextManager.js');
|
||||||
|
|
||||||
import { BaseLlmClient } from '../core/baseLlmClient.js';
|
import { BaseLlmClient } from '../core/baseLlmClient.js';
|
||||||
import { tokenLimit } from '../core/tokenLimits.js';
|
import { tokenLimit } from '../core/tokenLimits.js';
|
||||||
import { uiTelemetryService } from '../telemetry/index.js';
|
import { uiTelemetryService } from '../telemetry/index.js';
|
||||||
import { getCodeAssistServer } from '../code_assist/codeAssist.js';
|
import { getCodeAssistServer } from '../code_assist/codeAssist.js';
|
||||||
import { getExperiments } from '../code_assist/experiments/experiments.js';
|
import { getExperiments } from '../code_assist/experiments/experiments.js';
|
||||||
import type { CodeAssistServer } from '../code_assist/server.js';
|
import type { CodeAssistServer } from '../code_assist/server.js';
|
||||||
|
import { ContextManager } from '../services/contextManager.js';
|
||||||
|
|
||||||
vi.mock('../core/baseLlmClient.js');
|
vi.mock('../core/baseLlmClient.js');
|
||||||
vi.mock('../core/tokenLimits.js', () => ({
|
vi.mock('../core/tokenLimits.js', () => ({
|
||||||
@@ -1777,7 +1787,7 @@ describe('Config Quota & Preview Model Access', () => {
|
|||||||
sessionId: 'test-session',
|
sessionId: 'test-session',
|
||||||
model: 'gemini-pro',
|
model: 'gemini-pro',
|
||||||
usageStatisticsEnabled: false,
|
usageStatisticsEnabled: false,
|
||||||
embeddingModel: 'gemini-embedding', // required in type but not in the original file I copied, adding here
|
embeddingModel: 'gemini-embedding',
|
||||||
sandbox: {
|
sandbox: {
|
||||||
command: 'docker',
|
command: 'docker',
|
||||||
image: 'gemini-cli-sandbox',
|
image: 'gemini-cli-sandbox',
|
||||||
@@ -1877,3 +1887,71 @@ describe('Config Quota & Preview Model Access', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Config JIT Initialization', () => {
|
||||||
|
let config: Config;
|
||||||
|
let mockContextManager: {
|
||||||
|
refresh: Mock;
|
||||||
|
getGlobalMemory: Mock;
|
||||||
|
getEnvironmentMemory: Mock;
|
||||||
|
getLoadedPaths: Mock;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockContextManager = {
|
||||||
|
refresh: vi.fn(),
|
||||||
|
getGlobalMemory: vi.fn().mockReturnValue('Global Memory'),
|
||||||
|
getEnvironmentMemory: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue('Environment Memory\n\nMCP Instructions'),
|
||||||
|
getLoadedPaths: vi.fn().mockReturnValue(new Set(['/path/to/GEMINI.md'])),
|
||||||
|
};
|
||||||
|
(ContextManager as unknown as Mock).mockImplementation(
|
||||||
|
() => mockContextManager,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize ContextManager, load memory, and delegate to it when experimentalJitContext is enabled', async () => {
|
||||||
|
const params: ConfigParameters = {
|
||||||
|
sessionId: 'test-session',
|
||||||
|
targetDir: '/tmp/test',
|
||||||
|
debugMode: false,
|
||||||
|
model: 'test-model',
|
||||||
|
experimentalJitContext: true,
|
||||||
|
userMemory: 'Initial Memory',
|
||||||
|
cwd: '/tmp/test',
|
||||||
|
};
|
||||||
|
|
||||||
|
config = new Config(params);
|
||||||
|
await config.initialize();
|
||||||
|
|
||||||
|
expect(ContextManager).toHaveBeenCalledWith(config);
|
||||||
|
expect(mockContextManager.refresh).toHaveBeenCalled();
|
||||||
|
expect(config.getUserMemory()).toBe(
|
||||||
|
'Global Memory\n\nEnvironment Memory\n\nMCP Instructions',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify state update (delegated to ContextManager)
|
||||||
|
expect(config.getGeminiMdFileCount()).toBe(1);
|
||||||
|
expect(config.getGeminiMdFilePaths()).toEqual(['/path/to/GEMINI.md']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT initialize ContextManager when experimentalJitContext is disabled', async () => {
|
||||||
|
const params: ConfigParameters = {
|
||||||
|
sessionId: 'test-session',
|
||||||
|
targetDir: '/tmp/test',
|
||||||
|
debugMode: false,
|
||||||
|
model: 'test-model',
|
||||||
|
experimentalJitContext: false,
|
||||||
|
userMemory: 'Initial Memory',
|
||||||
|
cwd: '/tmp/test',
|
||||||
|
};
|
||||||
|
|
||||||
|
config = new Config(params);
|
||||||
|
await config.initialize();
|
||||||
|
|
||||||
|
expect(ContextManager).not.toHaveBeenCalled();
|
||||||
|
expect(config.getUserMemory()).toBe('Initial Memory');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -696,6 +696,7 @@ export class Config {
|
|||||||
|
|
||||||
if (this.experimentalJitContext) {
|
if (this.experimentalJitContext) {
|
||||||
this.contextManager = new ContextManager(this);
|
this.contextManager = new ContextManager(this);
|
||||||
|
await this.contextManager.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.geminiClient.initialize();
|
await this.geminiClient.initialize();
|
||||||
@@ -1062,6 +1063,14 @@ export class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUserMemory(): string {
|
getUserMemory(): string {
|
||||||
|
if (this.experimentalJitContext && this.contextManager) {
|
||||||
|
return [
|
||||||
|
this.contextManager.getGlobalMemory(),
|
||||||
|
this.contextManager.getEnvironmentMemory(),
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n');
|
||||||
|
}
|
||||||
return this.userMemory;
|
return this.userMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,6 +1095,9 @@ export class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGeminiMdFileCount(): number {
|
getGeminiMdFileCount(): number {
|
||||||
|
if (this.experimentalJitContext && this.contextManager) {
|
||||||
|
return this.contextManager.getLoadedPaths().size;
|
||||||
|
}
|
||||||
return this.geminiMdFileCount;
|
return this.geminiMdFileCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,6 +1106,9 @@ export class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGeminiMdFilePaths(): string[] {
|
getGeminiMdFilePaths(): string[] {
|
||||||
|
if (this.experimentalJitContext && this.contextManager) {
|
||||||
|
return Array.from(this.contextManager.getLoadedPaths());
|
||||||
|
}
|
||||||
return this.geminiMdFilePaths;
|
return this.geminiMdFilePaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,9 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
getVertexAI: vi.fn().mockReturnValue(false),
|
getVertexAI: vi.fn().mockReturnValue(false),
|
||||||
getUserAgent: vi.fn().mockReturnValue('test-agent'),
|
getUserAgent: vi.fn().mockReturnValue('test-agent'),
|
||||||
getUserMemory: vi.fn().mockReturnValue(''),
|
getUserMemory: vi.fn().mockReturnValue(''),
|
||||||
|
getGlobalMemory: vi.fn().mockReturnValue(''),
|
||||||
|
getEnvironmentMemory: vi.fn().mockReturnValue(''),
|
||||||
|
isJitContextEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
|
||||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
getProxy: vi.fn().mockReturnValue(undefined),
|
getProxy: vi.fn().mockReturnValue(undefined),
|
||||||
@@ -1532,6 +1535,39 @@ ${JSON.stringify(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use getGlobalMemory for system instruction when JIT is enabled', async () => {
|
||||||
|
vi.mocked(mockConfig.isJitContextEnabled).mockReturnValue(true);
|
||||||
|
vi.mocked(mockConfig.getGlobalMemory).mockReturnValue(
|
||||||
|
'Global JIT Memory',
|
||||||
|
);
|
||||||
|
vi.mocked(mockConfig.getUserMemory).mockReturnValue('Full JIT Memory');
|
||||||
|
|
||||||
|
const { getCoreSystemPrompt } = await import('./prompts.js');
|
||||||
|
const mockGetCoreSystemPrompt = vi.mocked(getCoreSystemPrompt);
|
||||||
|
|
||||||
|
await client.updateSystemInstruction();
|
||||||
|
|
||||||
|
expect(mockGetCoreSystemPrompt).toHaveBeenCalledWith(
|
||||||
|
mockConfig,
|
||||||
|
'Global JIT Memory',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use getUserMemory for system instruction when JIT is disabled', async () => {
|
||||||
|
vi.mocked(mockConfig.isJitContextEnabled).mockReturnValue(false);
|
||||||
|
vi.mocked(mockConfig.getUserMemory).mockReturnValue('Legacy Memory');
|
||||||
|
|
||||||
|
const { getCoreSystemPrompt } = await import('./prompts.js');
|
||||||
|
const mockGetCoreSystemPrompt = vi.mocked(getCoreSystemPrompt);
|
||||||
|
|
||||||
|
await client.updateSystemInstruction();
|
||||||
|
|
||||||
|
expect(mockGetCoreSystemPrompt).toHaveBeenCalledWith(
|
||||||
|
mockConfig,
|
||||||
|
'Legacy Memory',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should recursively call sendMessageStream with "Please continue." when InvalidStream event is received', async () => {
|
it('should recursively call sendMessageStream with "Please continue." when InvalidStream event is received', async () => {
|
||||||
vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(
|
vi.spyOn(client['config'], 'getContinueOnFailedApiCall').mockReturnValue(
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -179,8 +179,10 @@ export class GeminiClient {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMemory = this.config.getUserMemory();
|
const systemMemory = this.config.isJitContextEnabled()
|
||||||
const systemInstruction = getCoreSystemPrompt(this.config, userMemory);
|
? this.config.getGlobalMemory()
|
||||||
|
: this.config.getUserMemory();
|
||||||
|
const systemInstruction = getCoreSystemPrompt(this.config, systemMemory);
|
||||||
this.getChat().setSystemInstruction(systemInstruction);
|
this.getChat().setSystemInstruction(systemInstruction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,8 +200,10 @@ export class GeminiClient {
|
|||||||
const history = await getInitialChatHistory(this.config, extraHistory);
|
const history = await getInitialChatHistory(this.config, extraHistory);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userMemory = this.config.getUserMemory();
|
const systemMemory = this.config.isJitContextEnabled()
|
||||||
const systemInstruction = getCoreSystemPrompt(this.config, userMemory);
|
? this.config.getGlobalMemory()
|
||||||
|
: this.config.getUserMemory();
|
||||||
|
const systemInstruction = getCoreSystemPrompt(this.config, systemMemory);
|
||||||
return new GeminiChat(
|
return new GeminiChat(
|
||||||
this.config,
|
this.config,
|
||||||
systemInstruction,
|
systemInstruction,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|||||||
import { ContextManager } from './contextManager.js';
|
import { ContextManager } from './contextManager.js';
|
||||||
import * as memoryDiscovery from '../utils/memoryDiscovery.js';
|
import * as memoryDiscovery from '../utils/memoryDiscovery.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import type { ExtensionLoader } from '../utils/extensionLoader.js';
|
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||||
|
|
||||||
// Mock memoryDiscovery module
|
// Mock memoryDiscovery module
|
||||||
vi.mock('../utils/memoryDiscovery.js', async (importOriginal) => {
|
vi.mock('../utils/memoryDiscovery.js', async (importOriginal) => {
|
||||||
@@ -19,6 +19,9 @@ vi.mock('../utils/memoryDiscovery.js', async (importOriginal) => {
|
|||||||
loadGlobalMemory: vi.fn(),
|
loadGlobalMemory: vi.fn(),
|
||||||
loadEnvironmentMemory: vi.fn(),
|
loadEnvironmentMemory: vi.fn(),
|
||||||
loadJitSubdirectoryMemory: vi.fn(),
|
loadJitSubdirectoryMemory: vi.fn(),
|
||||||
|
concatenateInstructions: vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(actual.concatenateInstructions),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,58 +33,84 @@ describe('ContextManager', () => {
|
|||||||
mockConfig = {
|
mockConfig = {
|
||||||
getDebugMode: vi.fn().mockReturnValue(false),
|
getDebugMode: vi.fn().mockReturnValue(false),
|
||||||
getWorkingDir: vi.fn().mockReturnValue('/app'),
|
getWorkingDir: vi.fn().mockReturnValue('/app'),
|
||||||
|
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||||
|
getDirectories: vi.fn().mockReturnValue(['/app']),
|
||||||
|
}),
|
||||||
|
getExtensionLoader: vi.fn().mockReturnValue({}),
|
||||||
|
getMcpClientManager: vi.fn().mockReturnValue({
|
||||||
|
getMcpInstructions: vi.fn().mockReturnValue('MCP Instructions'),
|
||||||
|
}),
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
contextManager = new ContextManager(mockConfig);
|
contextManager = new ContextManager(mockConfig);
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
vi.spyOn(coreEvents, 'emit');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadGlobalMemory', () => {
|
describe('refresh', () => {
|
||||||
it('should load and format global memory', async () => {
|
it('should load and format global and environment memory', async () => {
|
||||||
const mockResult: memoryDiscovery.MemoryLoadResult = {
|
const mockGlobalResult: memoryDiscovery.MemoryLoadResult = {
|
||||||
files: [
|
files: [
|
||||||
{ path: '/home/user/.gemini/GEMINI.md', content: 'Global Content' },
|
{ path: '/home/user/.gemini/GEMINI.md', content: 'Global Content' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
vi.mocked(memoryDiscovery.loadGlobalMemory).mockResolvedValue(mockResult);
|
vi.mocked(memoryDiscovery.loadGlobalMemory).mockResolvedValue(
|
||||||
|
mockGlobalResult,
|
||||||
const result = await contextManager.loadGlobalMemory();
|
|
||||||
|
|
||||||
expect(memoryDiscovery.loadGlobalMemory).toHaveBeenCalledWith(false);
|
|
||||||
// The path will be relative to CWD (/app), so it might contain ../
|
|
||||||
expect(result).toMatch(/--- Context from: .*GEMINI.md ---/);
|
|
||||||
expect(result).toContain('Global Content');
|
|
||||||
expect(contextManager.getLoadedPaths()).toContain(
|
|
||||||
'/home/user/.gemini/GEMINI.md',
|
|
||||||
);
|
);
|
||||||
expect(contextManager.getGlobalMemory()).toBe(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('loadEnvironmentMemory', () => {
|
const mockEnvResult: memoryDiscovery.MemoryLoadResult = {
|
||||||
it('should load and format environment memory', async () => {
|
|
||||||
const mockResult: memoryDiscovery.MemoryLoadResult = {
|
|
||||||
files: [{ path: '/app/GEMINI.md', content: 'Env Content' }],
|
files: [{ path: '/app/GEMINI.md', content: 'Env Content' }],
|
||||||
};
|
};
|
||||||
vi.mocked(memoryDiscovery.loadEnvironmentMemory).mockResolvedValue(
|
vi.mocked(memoryDiscovery.loadEnvironmentMemory).mockResolvedValue(
|
||||||
mockResult,
|
mockEnvResult,
|
||||||
);
|
);
|
||||||
const mockExtensionLoader = {} as unknown as ExtensionLoader;
|
|
||||||
|
|
||||||
const result = await contextManager.loadEnvironmentMemory(
|
await contextManager.refresh();
|
||||||
['/app'],
|
|
||||||
mockExtensionLoader,
|
expect(memoryDiscovery.loadGlobalMemory).toHaveBeenCalledWith(false);
|
||||||
|
expect(contextManager.getGlobalMemory()).toMatch(
|
||||||
|
/--- Context from: .*GEMINI.md ---/,
|
||||||
);
|
);
|
||||||
|
expect(contextManager.getGlobalMemory()).toContain('Global Content');
|
||||||
|
|
||||||
expect(memoryDiscovery.loadEnvironmentMemory).toHaveBeenCalledWith(
|
expect(memoryDiscovery.loadEnvironmentMemory).toHaveBeenCalledWith(
|
||||||
['/app'],
|
['/app'],
|
||||||
mockExtensionLoader,
|
expect.anything(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
expect(result).toContain('--- Context from: GEMINI.md ---');
|
expect(contextManager.getEnvironmentMemory()).toContain(
|
||||||
expect(result).toContain('Env Content');
|
'--- Context from: GEMINI.md ---',
|
||||||
|
);
|
||||||
|
expect(contextManager.getEnvironmentMemory()).toContain('Env Content');
|
||||||
|
expect(contextManager.getEnvironmentMemory()).toContain(
|
||||||
|
'MCP Instructions',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(contextManager.getLoadedPaths()).toContain(
|
||||||
|
'/home/user/.gemini/GEMINI.md',
|
||||||
|
);
|
||||||
expect(contextManager.getLoadedPaths()).toContain('/app/GEMINI.md');
|
expect(contextManager.getLoadedPaths()).toContain('/app/GEMINI.md');
|
||||||
expect(contextManager.getEnvironmentMemory()).toBe(result);
|
});
|
||||||
|
|
||||||
|
it('should emit MemoryChanged event when memory is refreshed', async () => {
|
||||||
|
const mockGlobalResult = {
|
||||||
|
files: [{ path: '/app/GEMINI.md', content: 'content' }],
|
||||||
|
};
|
||||||
|
const mockEnvResult = {
|
||||||
|
files: [{ path: '/app/src/GEMINI.md', content: 'env content' }],
|
||||||
|
};
|
||||||
|
vi.mocked(memoryDiscovery.loadGlobalMemory).mockResolvedValue(
|
||||||
|
mockGlobalResult,
|
||||||
|
);
|
||||||
|
vi.mocked(memoryDiscovery.loadEnvironmentMemory).mockResolvedValue(
|
||||||
|
mockEnvResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
await contextManager.refresh();
|
||||||
|
|
||||||
|
expect(coreEvents.emit).toHaveBeenCalledWith(CoreEvent.MemoryChanged, {
|
||||||
|
fileCount: 2,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,27 +151,4 @@ describe('ContextManager', () => {
|
|||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('reset', () => {
|
|
||||||
it('should clear loaded paths and memory', async () => {
|
|
||||||
// Setup some state
|
|
||||||
const mockResult: memoryDiscovery.MemoryLoadResult = {
|
|
||||||
files: [
|
|
||||||
{ path: '/home/user/.gemini/GEMINI.md', content: 'Global Content' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
vi.mocked(memoryDiscovery.loadGlobalMemory).mockResolvedValue(mockResult);
|
|
||||||
await contextManager.loadGlobalMemory();
|
|
||||||
|
|
||||||
expect(contextManager.getLoadedPaths().size).toBeGreaterThan(0);
|
|
||||||
expect(contextManager.getGlobalMemory()).toBeTruthy();
|
|
||||||
|
|
||||||
// Reset
|
|
||||||
contextManager.reset();
|
|
||||||
|
|
||||||
expect(contextManager.getLoadedPaths().size).toBe(0);
|
|
||||||
expect(contextManager.getGlobalMemory()).toBe('');
|
|
||||||
expect(contextManager.getEnvironmentMemory()).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
loadJitSubdirectoryMemory,
|
loadJitSubdirectoryMemory,
|
||||||
concatenateInstructions,
|
concatenateInstructions,
|
||||||
} from '../utils/memoryDiscovery.js';
|
} from '../utils/memoryDiscovery.js';
|
||||||
import type { ExtensionLoader } from '../utils/extensionLoader.js';
|
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
|
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||||
|
|
||||||
export class ContextManager {
|
export class ContextManager {
|
||||||
private readonly loadedPaths: Set<string> = new Set();
|
private readonly loadedPaths: Set<string> = new Set();
|
||||||
@@ -24,36 +24,40 @@ export class ContextManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the global memory (Tier 1) and returns the formatted content.
|
* Refreshes the memory by reloading global and environment memory.
|
||||||
*/
|
*/
|
||||||
async loadGlobalMemory(): Promise<string> {
|
async refresh(): Promise<void> {
|
||||||
|
this.loadedPaths.clear();
|
||||||
|
await this.loadGlobalMemory();
|
||||||
|
await this.loadEnvironmentMemory();
|
||||||
|
this.emitMemoryChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadGlobalMemory(): Promise<void> {
|
||||||
const result = await loadGlobalMemory(this.config.getDebugMode());
|
const result = await loadGlobalMemory(this.config.getDebugMode());
|
||||||
this.markAsLoaded(result.files.map((f) => f.path));
|
this.markAsLoaded(result.files.map((f) => f.path));
|
||||||
this.globalMemory = concatenateInstructions(
|
this.globalMemory = concatenateInstructions(
|
||||||
result.files.map((f) => ({ filePath: f.path, content: f.content })),
|
result.files.map((f) => ({ filePath: f.path, content: f.content })),
|
||||||
this.config.getWorkingDir(),
|
this.config.getWorkingDir(),
|
||||||
);
|
);
|
||||||
return this.globalMemory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private async loadEnvironmentMemory(): Promise<void> {
|
||||||
* Loads the environment memory (Tier 2) and returns the formatted content.
|
|
||||||
*/
|
|
||||||
async loadEnvironmentMemory(
|
|
||||||
trustedRoots: string[],
|
|
||||||
extensionLoader: ExtensionLoader,
|
|
||||||
): Promise<string> {
|
|
||||||
const result = await loadEnvironmentMemory(
|
const result = await loadEnvironmentMemory(
|
||||||
trustedRoots,
|
[...this.config.getWorkspaceContext().getDirectories()],
|
||||||
extensionLoader,
|
this.config.getExtensionLoader(),
|
||||||
this.config.getDebugMode(),
|
this.config.getDebugMode(),
|
||||||
);
|
);
|
||||||
this.markAsLoaded(result.files.map((f) => f.path));
|
this.markAsLoaded(result.files.map((f) => f.path));
|
||||||
this.environmentMemory = concatenateInstructions(
|
const envMemory = concatenateInstructions(
|
||||||
result.files.map((f) => ({ filePath: f.path, content: f.content })),
|
result.files.map((f) => ({ filePath: f.path, content: f.content })),
|
||||||
this.config.getWorkingDir(),
|
this.config.getWorkingDir(),
|
||||||
);
|
);
|
||||||
return this.environmentMemory;
|
const mcpInstructions =
|
||||||
|
this.config.getMcpClientManager()?.getMcpInstructions() || '';
|
||||||
|
this.environmentMemory = [envMemory, mcpInstructions.trimStart()]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,6 +86,12 @@ export class ContextManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitMemoryChanged(): void {
|
||||||
|
coreEvents.emit(CoreEvent.MemoryChanged, {
|
||||||
|
fileCount: this.loadedPaths.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getGlobalMemory(): string {
|
getGlobalMemory(): string {
|
||||||
return this.globalMemory;
|
return this.globalMemory;
|
||||||
}
|
}
|
||||||
@@ -96,15 +106,6 @@ export class ContextManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the loaded paths tracking and memory. Useful for testing or full reloads.
|
|
||||||
*/
|
|
||||||
reset(): void {
|
|
||||||
this.loadedPaths.clear();
|
|
||||||
this.globalMemory = '';
|
|
||||||
this.environmentMemory = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
getLoadedPaths(): ReadonlySet<string> {
|
getLoadedPaths(): ReadonlySet<string> {
|
||||||
return this.loadedPaths;
|
return this.loadedPaths;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ describe('getEnvironmentContext', () => {
|
|||||||
getDirectories: vi.fn().mockReturnValue(['/test/dir']),
|
getDirectories: vi.fn().mockReturnValue(['/test/dir']),
|
||||||
}),
|
}),
|
||||||
getFileService: vi.fn(),
|
getFileService: vi.fn(),
|
||||||
|
getEnvironmentMemory: vi.fn().mockReturnValue('Mock Environment Memory'),
|
||||||
|
|
||||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||||
storage: {
|
storage: {
|
||||||
@@ -122,6 +123,7 @@ describe('getEnvironmentContext', () => {
|
|||||||
expect(context).toContain(
|
expect(context).toContain(
|
||||||
'Here is the folder structure of the current working directories:\n\nMock Folder Structure',
|
'Here is the folder structure of the current working directories:\n\nMock Folder Structure',
|
||||||
);
|
);
|
||||||
|
expect(context).toContain('Mock Environment Memory');
|
||||||
expect(getFolderStructure).toHaveBeenCalledWith('/test/dir', {
|
expect(getFolderStructure).toHaveBeenCalledWith('/test/dir', {
|
||||||
fileService: undefined,
|
fileService: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export async function getEnvironmentContext(config: Config): Promise<Part[]> {
|
|||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
const directoryContext = await getDirectoryContextString(config);
|
const directoryContext = await getDirectoryContextString(config);
|
||||||
const tempDir = config.storage.getProjectTempDir();
|
const tempDir = config.storage.getProjectTempDir();
|
||||||
|
const environmentMemory = config.getEnvironmentMemory();
|
||||||
|
|
||||||
const context = `
|
const context = `
|
||||||
This is the Gemini CLI. We are setting up the context for our chat.
|
This is the Gemini CLI. We are setting up the context for our chat.
|
||||||
@@ -69,6 +70,8 @@ Today's date is ${today} (formatted according to the user's locale).
|
|||||||
My operating system is: ${platform}
|
My operating system is: ${platform}
|
||||||
The project's temporary directory is: ${tempDir}
|
The project's temporary directory is: ${tempDir}
|
||||||
${directoryContext}
|
${directoryContext}
|
||||||
|
|
||||||
|
${environmentMemory}
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
const initialParts: Part[] = [{ text: context }];
|
const initialParts: Part[] = [{ text: context }];
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import type { LoadServerHierarchicalMemoryResponse } from './memoryDiscovery.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the severity level for user-facing feedback.
|
* Defines the severity level for user-facing feedback.
|
||||||
@@ -64,7 +63,9 @@ export interface OutputPayload {
|
|||||||
/**
|
/**
|
||||||
* Payload for the 'memory-changed' event.
|
* Payload for the 'memory-changed' event.
|
||||||
*/
|
*/
|
||||||
export type MemoryChangedPayload = LoadServerHierarchicalMemoryResponse;
|
export interface MemoryChangedPayload {
|
||||||
|
fileCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export enum CoreEvent {
|
export enum CoreEvent {
|
||||||
UserFeedback = 'user-feedback',
|
UserFeedback = 'user-feedback',
|
||||||
|
|||||||
@@ -932,7 +932,9 @@ included directory memory
|
|||||||
path.join(extensionPath, 'CustomContext.md'),
|
path.join(extensionPath, 'CustomContext.md'),
|
||||||
);
|
);
|
||||||
expect(config.getGeminiMdFilePaths()).equals(refreshResult.filePaths);
|
expect(config.getGeminiMdFilePaths()).equals(refreshResult.filePaths);
|
||||||
expect(mockEventListener).toHaveBeenCalledExactlyOnceWith(refreshResult);
|
expect(mockEventListener).toHaveBeenCalledExactlyOnceWith({
|
||||||
|
fileCount: refreshResult.fileCount,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include MCP instructions in user memory', async () => {
|
it('should include MCP instructions in user memory', async () => {
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ export async function refreshServerHierarchicalMemory(config: Config) {
|
|||||||
config.setUserMemory(finalMemory);
|
config.setUserMemory(finalMemory);
|
||||||
config.setGeminiMdFileCount(result.fileCount);
|
config.setGeminiMdFileCount(result.fileCount);
|
||||||
config.setGeminiMdFilePaths(result.filePaths);
|
config.setGeminiMdFilePaths(result.filePaths);
|
||||||
coreEvents.emit(CoreEvent.MemoryChanged, result);
|
coreEvents.emit(CoreEvent.MemoryChanged, { fileCount: result.fileCount });
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user