Files
gemini-cli/packages/core/src/services/contextManager.ts

188 lines
5.0 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
loadJitSubdirectoryMemory,
concatenateInstructions,
getGlobalMemoryPaths,
getExtensionMemoryPaths,
getEnvironmentMemoryPaths,
readGeminiMdFiles,
categorizeAndConcatenate,
type GeminiFileContent,
deduplicatePathsByFileIdentity,
} from '../utils/memoryDiscovery.js';
import type { Config } from '../config/config.js';
import { coreEvents, CoreEvent } from '../utils/events.js';
export class ContextManager {
private readonly loadedPaths: Set<string> = new Set();
private readonly loadedFileIdentities: Set<string> = new Set();
private readonly config: Config;
private globalMemory: string = '';
private extensionMemory: string = '';
private projectMemory: string = '';
constructor(config: Config) {
this.config = config;
}
/**
* Refreshes the memory by reloading global, extension, and project memory.
*/
async refresh(): Promise<void> {
this.loadedPaths.clear();
this.loadedFileIdentities.clear();
const paths = await this.discoverMemoryPaths();
const contentsMap = await this.loadMemoryContents(paths);
this.categorizeMemoryContents(paths, contentsMap);
this.emitMemoryChanged();
}
private async discoverMemoryPaths() {
const [global, extension, project] = await Promise.all([
getGlobalMemoryPaths(),
Promise.resolve(
getExtensionMemoryPaths(this.config.getExtensionLoader()),
),
this.config.isTrustedFolder()
? getEnvironmentMemoryPaths([
...this.config.getWorkspaceContext().getDirectories(),
])
: Promise.resolve([]),
]);
return { global, extension, project };
}
private async loadMemoryContents(paths: {
global: string[];
extension: string[];
project: string[];
}) {
const allPathsStringDeduped = Array.from(
new Set([...paths.global, ...paths.extension, ...paths.project]),
);
// deduplicate by file identity to handle case-insensitive filesystems
const { paths: allPaths, identityMap: pathIdentityMap } =
await deduplicatePathsByFileIdentity(allPathsStringDeduped);
const allContents = await readGeminiMdFiles(
allPaths,
this.config.getImportFormat(),
);
const loadedFilePaths = allContents
.filter((c) => c.content !== null)
.map((c) => c.filePath);
this.markAsLoaded(loadedFilePaths);
// Cache file identities for performance optimization
for (const filePath of loadedFilePaths) {
const identity = pathIdentityMap.get(filePath);
if (identity) {
this.loadedFileIdentities.add(identity);
}
}
return new Map(allContents.map((c) => [c.filePath, c]));
}
private categorizeMemoryContents(
paths: { global: string[]; extension: string[]; project: string[] },
contentsMap: Map<string, GeminiFileContent>,
) {
const workingDir = this.config.getWorkingDir();
const hierarchicalMemory = categorizeAndConcatenate(
paths,
contentsMap,
workingDir,
);
this.globalMemory = hierarchicalMemory.global || '';
this.extensionMemory = hierarchicalMemory.extension || '';
const mcpInstructions =
this.config.getMcpClientManager()?.getMcpInstructions() || '';
const projectMemoryWithMcp = [
hierarchicalMemory.project,
mcpInstructions.trimStart(),
]
.filter(Boolean)
.join('\n\n');
this.projectMemory = this.config.isTrustedFolder()
? projectMemoryWithMcp
: '';
}
/**
* Discovers and loads context for a specific accessed path (Tier 3 - JIT).
* Traverses upwards from the accessed path to the project root.
*/
async discoverContext(
accessedPath: string,
trustedRoots: string[],
): Promise<string> {
if (!this.config.isTrustedFolder()) {
return '';
}
const result = await loadJitSubdirectoryMemory(
accessedPath,
trustedRoots,
this.loadedPaths,
this.loadedFileIdentities,
);
if (result.files.length === 0) {
return '';
}
const newFilePaths = result.files.map((f) => f.path);
this.markAsLoaded(newFilePaths);
// Cache identities for newly loaded files
if (result.fileIdentities) {
for (const identity of result.fileIdentities) {
this.loadedFileIdentities.add(identity);
}
}
return concatenateInstructions(
result.files.map((f) => ({ filePath: f.path, content: f.content })),
this.config.getWorkingDir(),
);
}
private emitMemoryChanged(): void {
coreEvents.emit(CoreEvent.MemoryChanged, {
fileCount: this.loadedPaths.size,
});
}
getGlobalMemory(): string {
return this.globalMemory;
}
getExtensionMemory(): string {
return this.extensionMemory;
}
getEnvironmentMemory(): string {
return this.projectMemory;
}
private markAsLoaded(paths: string[]): void {
paths.forEach((p) => this.loadedPaths.add(p));
}
getLoadedPaths(): ReadonlySet<string> {
return this.loadedPaths;
}
}