/** * @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 = new Set(); private readonly loadedFileIdentities: Set = 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 { 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, ) { 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 { 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 { return this.loadedPaths; } }