summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBilly Biggs <[email protected]>2025-06-13 09:19:08 -0700
committerGitHub <[email protected]>2025-06-13 09:19:08 -0700
commit2a1ad1f5d961b9f9593a6016eea7dd398bdeed0b (patch)
tree9e83f9421ef9b01f30454c5af3db90540381d1e3
parent34e0d9c0b65b91b12df4f205d9835e05913992b9 (diff)
Update contextFileName to support an optional list of strings (#1001)
-rw-r--r--docs/cli/configuration.md4
-rw-r--r--packages/cli/src/config/extension.ts17
-rw-r--r--packages/cli/src/config/settings.ts2
-rw-r--r--packages/cli/src/ui/App.test.tsx23
-rw-r--r--packages/cli/src/ui/App.tsx15
-rw-r--r--packages/cli/src/ui/components/ContextSummaryDisplay.tsx8
-rw-r--r--packages/core/src/config/config.ts2
-rw-r--r--packages/core/src/tools/memoryTool.test.ts8
-rw-r--r--packages/core/src/tools/memoryTool.ts20
-rw-r--r--packages/core/src/utils/memoryDiscovery.ts190
10 files changed, 174 insertions, 115 deletions
diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md
index 4eacc6fd..1b880e0f 100644
--- a/docs/cli/configuration.md
+++ b/docs/cli/configuration.md
@@ -38,9 +38,9 @@ When you create a `.gemini/settings.json` file for project-specific settings, or
### Available Settings in `settings.json`:
-- **`contextFileName`** (string, optional):
+- **`contextFileName`** (string or array of strings, optional):
- - **Description:** Specifies the filename for context files (e.g., `GEMINI.md`, `AGENTS.md`).
+ - **Description:** Specifies the filename for context files (e.g., `GEMINI.md`, `AGENTS.md`). May be a single filename or a list of accepted filenames.
- **Default:** `GEMINI.md`
- **Example:** `"contextFileName": "AGENTS.md"`
diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts
index 9dd33e1b..685c0b74 100644
--- a/packages/cli/src/config/extension.ts
+++ b/packages/cli/src/config/extension.ts
@@ -16,7 +16,7 @@ export interface ExtensionConfig {
name: string;
version: string;
mcpServers?: Record<string, MCPServerConfig>;
- contextFileName?: string;
+ contextFileName?: string | string[];
}
export function loadExtensions(workspaceDir: string): ExtensionConfig[] {
@@ -76,12 +76,15 @@ function loadExtensionsFromDir(dir: string): ExtensionConfig[] {
}
if (extensionConfig.contextFileName) {
- const contextFilePath = path.join(
- extensionDir,
- extensionConfig.contextFileName,
- );
- if (fs.existsSync(contextFilePath)) {
- extensionConfig.contextFileName = contextFilePath;
+ const contextFileNames = Array.isArray(extensionConfig.contextFileName)
+ ? extensionConfig.contextFileName
+ : [extensionConfig.contextFileName];
+ const resolvedPaths = contextFileNames
+ .map((fileName) => path.join(extensionDir, fileName))
+ .filter((filePath) => fs.existsSync(filePath));
+ if (resolvedPaths.length > 0) {
+ extensionConfig.contextFileName =
+ resolvedPaths.length === 1 ? resolvedPaths[0] : resolvedPaths;
}
} else {
const contextFilePath = path.join(extensionDir, 'gemini.md');
diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts
index e00ebb79..25f9d79d 100644
--- a/packages/cli/src/config/settings.ts
+++ b/packages/cli/src/config/settings.ts
@@ -35,7 +35,7 @@ export interface Settings {
mcpServerCommand?: string;
mcpServers?: Record<string, MCPServerConfig>;
showMemoryUsage?: boolean;
- contextFileName?: string;
+ contextFileName?: string | string[];
accessibility?: AccessibilitySettings;
telemetry?: boolean;
preferredEditor?: string;
diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx
index b8959bfb..201d0698 100644
--- a/packages/cli/src/ui/App.test.tsx
+++ b/packages/cli/src/ui/App.test.tsx
@@ -64,6 +64,7 @@ interface MockServerConfig {
getShowMemoryUsage: Mock<() => boolean>;
getAccessibility: Mock<() => AccessibilitySettings>;
getProjectRoot: Mock<() => string | undefined>;
+ getAllGeminiMdFilenames: Mock<() => string[]>;
}
// Mock @gemini-cli/core and its Config class
@@ -124,12 +125,14 @@ vi.mock('@gemini-cli/core', async (importOriginal) => {
getProjectRoot: vi.fn(() => opts.projectRoot),
getGeminiClient: vi.fn(() => ({})),
getCheckpointEnabled: vi.fn(() => opts.checkpoint ?? true),
+ getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
};
});
return {
...actualCore,
Config: ConfigClassMock,
MCPServerConfig: actualCore.MCPServerConfig,
+ getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
};
});
@@ -269,6 +272,26 @@ describe('App UI', () => {
expect(lastFrame()).toContain('Using 1 AGENTS.MD file');
});
+ it('should display the first custom contextFileName when an array is provided', async () => {
+ mockSettings = createMockSettings({
+ contextFileName: ['AGENTS.MD', 'CONTEXT.MD'],
+ theme: 'Default',
+ });
+ mockConfig.getGeminiMdFileCount.mockReturnValue(2);
+ mockConfig.getDebugMode.mockReturnValue(false);
+ mockConfig.getShowMemoryUsage.mockReturnValue(false);
+
+ const { lastFrame, unmount } = render(
+ <App
+ config={mockConfig as unknown as ServerConfig}
+ settings={mockSettings}
+ />,
+ );
+ currentUnmount = unmount;
+ await Promise.resolve();
+ expect(lastFrame()).toContain('Using 2 AGENTS.MD files');
+ });
+
it('should display custom contextFileName with plural when set and count is > 1', async () => {
mockSettings = createMockSettings({
contextFileName: 'MY_NOTES.TXT',
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 2d37c42a..c7ed9a81 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -45,7 +45,7 @@ import process from 'node:process';
import {
getErrorMessage,
type Config,
- getCurrentGeminiMdFilename,
+ getAllGeminiMdFilenames,
ApprovalMode,
isEditorAvailable,
EditorType,
@@ -373,6 +373,14 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
const branchName = useGitBranchName(config.getTargetDir());
+ const contextFileNames = useMemo(() => {
+ const fromSettings = settings.merged.contextFileName;
+ if (fromSettings) {
+ return Array.isArray(fromSettings) ? fromSettings : [fromSettings];
+ }
+ return getAllGeminiMdFilenames();
+ }, [settings.merged.contextFileName]);
+
if (quittingMessages) {
return (
<Box flexDirection="column" marginBottom={1}>
@@ -509,10 +517,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
) : (
<ContextSummaryDisplay
geminiMdFileCount={geminiMdFileCount}
- contextFileName={
- settings.merged.contextFileName ||
- getCurrentGeminiMdFilename()
- }
+ contextFileNames={contextFileNames}
mcpServers={config.getMcpServers()}
showToolDescriptions={showToolDescriptions}
/>
diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx
index c4527066..904bf81f 100644
--- a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx
+++ b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx
@@ -11,14 +11,14 @@ import { type MCPServerConfig } from '@gemini-cli/core';
interface ContextSummaryDisplayProps {
geminiMdFileCount: number;
- contextFileName: string;
+ contextFileNames: string[];
mcpServers?: Record<string, MCPServerConfig>;
showToolDescriptions?: boolean;
}
export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
geminiMdFileCount,
- contextFileName,
+ contextFileNames,
mcpServers,
showToolDescriptions,
}) => {
@@ -30,7 +30,9 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
const geminiMdText =
geminiMdFileCount > 0
- ? `${geminiMdFileCount} ${contextFileName} file${geminiMdFileCount > 1 ? 's' : ''}`
+ ? `${geminiMdFileCount} ${contextFileNames[0]} file${
+ geminiMdFileCount > 1 ? 's' : ''
+ }`
: '';
const mcpText =
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 2576080b..4962d2a7 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -74,7 +74,7 @@ export interface ConfigParameters {
geminiMdFileCount?: number;
approvalMode?: ApprovalMode;
showMemoryUsage?: boolean;
- contextFileName?: string;
+ contextFileName?: string | string[];
geminiIgnorePatterns?: string[];
accessibility?: AccessibilitySettings;
telemetry?: boolean;
diff --git a/packages/core/src/tools/memoryTool.test.ts b/packages/core/src/tools/memoryTool.test.ts
index 612a08dc..aff0cc2e 100644
--- a/packages/core/src/tools/memoryTool.test.ts
+++ b/packages/core/src/tools/memoryTool.test.ts
@@ -9,6 +9,7 @@ import {
MemoryTool,
setGeminiMdFilename,
getCurrentGeminiMdFilename,
+ getAllGeminiMdFilenames,
DEFAULT_CONTEXT_FILENAME,
} from './memoryTool.js';
import * as fs from 'fs/promises';
@@ -74,6 +75,13 @@ describe('MemoryTool', () => {
setGeminiMdFilename('');
expect(getCurrentGeminiMdFilename()).toBe(initialName);
});
+
+ it('should handle an array of filenames', () => {
+ const newNames = ['CUSTOM_CONTEXT.md', 'ANOTHER_CONTEXT.md'];
+ setGeminiMdFilename(newNames);
+ expect(getCurrentGeminiMdFilename()).toBe('CUSTOM_CONTEXT.md');
+ expect(getAllGeminiMdFilenames()).toEqual(newNames);
+ });
});
describe('performAddMemoryEntry (static method)', () => {
diff --git a/packages/core/src/tools/memoryTool.ts b/packages/core/src/tools/memoryTool.ts
index a0c62eae..2c6f41c8 100644
--- a/packages/core/src/tools/memoryTool.ts
+++ b/packages/core/src/tools/memoryTool.ts
@@ -51,18 +51,32 @@ export const MEMORY_SECTION_HEADER = '## Gemini Added Memories';
// This variable will hold the currently configured filename for GEMINI.md context files.
// It defaults to DEFAULT_CONTEXT_FILENAME but can be overridden by setGeminiMdFilename.
-let currentGeminiMdFilename = DEFAULT_CONTEXT_FILENAME;
+let currentGeminiMdFilename: string | string[] = DEFAULT_CONTEXT_FILENAME;
-export function setGeminiMdFilename(newFilename: string): void {
- if (newFilename && newFilename.trim() !== '') {
+export function setGeminiMdFilename(newFilename: string | string[]): void {
+ if (Array.isArray(newFilename)) {
+ if (newFilename.length > 0) {
+ currentGeminiMdFilename = newFilename.map((name) => name.trim());
+ }
+ } else if (newFilename && newFilename.trim() !== '') {
currentGeminiMdFilename = newFilename.trim();
}
}
export function getCurrentGeminiMdFilename(): string {
+ if (Array.isArray(currentGeminiMdFilename)) {
+ return currentGeminiMdFilename[0];
+ }
return currentGeminiMdFilename;
}
+export function getAllGeminiMdFilenames(): string[] {
+ if (Array.isArray(currentGeminiMdFilename)) {
+ return currentGeminiMdFilename;
+ }
+ return [currentGeminiMdFilename];
+}
+
interface SaveMemoryParams {
fact: string;
}
diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts
index 07649415..2180b7a3 100644
--- a/packages/core/src/utils/memoryDiscovery.ts
+++ b/packages/core/src/utils/memoryDiscovery.ts
@@ -11,7 +11,7 @@ import { homedir } from 'os';
import { bfsFileSearch } from './bfsFileSearch.js';
import {
GEMINI_CONFIG_DIR,
- getCurrentGeminiMdFilename,
+ getAllGeminiMdFilenames,
} from '../tools/memoryTool.js';
// Simple console logger, similar to the one previously in CLI's config.ts
@@ -83,131 +83,135 @@ async function getGeminiMdFilePathsInternal(
debugMode: boolean,
extensionContextFilePaths: string[] = [],
): Promise<string[]> {
- const resolvedCwd = path.resolve(currentWorkingDirectory);
- const resolvedHome = path.resolve(userHomePath);
- const globalMemoryPath = path.join(
- resolvedHome,
- GEMINI_CONFIG_DIR,
- getCurrentGeminiMdFilename(),
- );
- const paths: string[] = [];
+ const allPaths = new Set<string>();
+ const geminiMdFilenames = getAllGeminiMdFilenames();
- if (debugMode)
- logger.debug(
- `Searching for ${getCurrentGeminiMdFilename()} starting from CWD: ${resolvedCwd}`,
+ for (const geminiMdFilename of geminiMdFilenames) {
+ const resolvedCwd = path.resolve(currentWorkingDirectory);
+ const resolvedHome = path.resolve(userHomePath);
+ const globalMemoryPath = path.join(
+ resolvedHome,
+ GEMINI_CONFIG_DIR,
+ geminiMdFilename,
);
- if (debugMode) logger.debug(`User home directory: ${resolvedHome}`);
- try {
- await fs.access(globalMemoryPath, fsSync.constants.R_OK);
- paths.push(globalMemoryPath);
if (debugMode)
logger.debug(
- `Found readable global ${getCurrentGeminiMdFilename()}: ${globalMemoryPath}`,
+ `Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`,
);
- } catch {
- if (debugMode)
- logger.debug(
- `Global ${getCurrentGeminiMdFilename()} not found or not readable: ${globalMemoryPath}`,
- );
- }
+ if (debugMode) logger.debug(`User home directory: ${resolvedHome}`);
- const projectRoot = await findProjectRoot(resolvedCwd);
- if (debugMode)
- logger.debug(`Determined project root: ${projectRoot ?? 'None'}`);
+ try {
+ await fs.access(globalMemoryPath, fsSync.constants.R_OK);
+ allPaths.add(globalMemoryPath);
+ if (debugMode)
+ logger.debug(
+ `Found readable global ${geminiMdFilename}: ${globalMemoryPath}`,
+ );
+ } catch {
+ if (debugMode)
+ logger.debug(
+ `Global ${geminiMdFilename} not found or not readable: ${globalMemoryPath}`,
+ );
+ }
- const upwardPaths: string[] = [];
- let currentDir = resolvedCwd;
- // Determine the directory that signifies the top of the project or user-specific space.
- const ultimateStopDir = projectRoot
- ? path.dirname(projectRoot)
- : path.dirname(resolvedHome);
+ const projectRoot = await findProjectRoot(resolvedCwd);
+ if (debugMode)
+ logger.debug(`Determined project root: ${projectRoot ?? 'None'}`);
- while (currentDir && currentDir !== path.dirname(currentDir)) {
- // Loop until filesystem root or currentDir is empty
- if (debugMode) {
- logger.debug(
- `Checking for ${getCurrentGeminiMdFilename()} in (upward scan): ${currentDir}`,
- );
- }
+ const upwardPaths: string[] = [];
+ let currentDir = resolvedCwd;
+ // Determine the directory that signifies the top of the project or user-specific space.
+ const ultimateStopDir = projectRoot
+ ? path.dirname(projectRoot)
+ : path.dirname(resolvedHome);
- // Skip the global .gemini directory itself during upward scan from CWD,
- // as global is handled separately and explicitly first.
- if (currentDir === path.join(resolvedHome, GEMINI_CONFIG_DIR)) {
+ while (currentDir && currentDir !== path.dirname(currentDir)) {
+ // Loop until filesystem root or currentDir is empty
if (debugMode) {
logger.debug(
- `Upward scan reached global config dir path, stopping upward search here: ${currentDir}`,
+ `Checking for ${geminiMdFilename} in (upward scan): ${currentDir}`,
);
}
- break;
- }
- const potentialPath = path.join(currentDir, getCurrentGeminiMdFilename());
- try {
- await fs.access(potentialPath, fsSync.constants.R_OK);
- // Add to upwardPaths only if it's not the already added globalMemoryPath
- if (potentialPath !== globalMemoryPath) {
- upwardPaths.unshift(potentialPath);
+ // Skip the global .gemini directory itself during upward scan from CWD,
+ // as global is handled separately and explicitly first.
+ if (currentDir === path.join(resolvedHome, GEMINI_CONFIG_DIR)) {
if (debugMode) {
logger.debug(
- `Found readable upward ${getCurrentGeminiMdFilename()}: ${potentialPath}`,
+ `Upward scan reached global config dir path, stopping upward search here: ${currentDir}`,
);
}
+ break;
}
- } catch {
- if (debugMode) {
- logger.debug(
- `Upward ${getCurrentGeminiMdFilename()} not found or not readable in: ${currentDir}`,
- );
+
+ const potentialPath = path.join(currentDir, geminiMdFilename);
+ try {
+ await fs.access(potentialPath, fsSync.constants.R_OK);
+ // Add to upwardPaths only if it's not the already added globalMemoryPath
+ if (potentialPath !== globalMemoryPath) {
+ upwardPaths.unshift(potentialPath);
+ if (debugMode) {
+ logger.debug(
+ `Found readable upward ${geminiMdFilename}: ${potentialPath}`,
+ );
+ }
+ }
+ } catch {
+ if (debugMode) {
+ logger.debug(
+ `Upward ${geminiMdFilename} not found or not readable in: ${currentDir}`,
+ );
+ }
}
- }
- // Stop condition: if currentDir is the ultimateStopDir, break after this iteration.
- if (currentDir === ultimateStopDir) {
- if (debugMode)
- logger.debug(
- `Reached ultimate stop directory for upward scan: ${currentDir}`,
- );
- break;
- }
+ // Stop condition: if currentDir is the ultimateStopDir, break after this iteration.
+ if (currentDir === ultimateStopDir) {
+ if (debugMode)
+ logger.debug(
+ `Reached ultimate stop directory for upward scan: ${currentDir}`,
+ );
+ break;
+ }
- currentDir = path.dirname(currentDir);
- }
- paths.push(...upwardPaths);
+ currentDir = path.dirname(currentDir);
+ }
+ upwardPaths.forEach((p) => allPaths.add(p));
- const downwardPaths = await bfsFileSearch(resolvedCwd, {
- fileName: getCurrentGeminiMdFilename(),
- maxDirs: MAX_DIRECTORIES_TO_SCAN_FOR_MEMORY,
- debug: debugMode,
- respectGitIgnore: true,
- projectRoot: projectRoot || resolvedCwd,
- });
- downwardPaths.sort(); // Sort for consistent ordering, though hierarchy might be more complex
- if (debugMode && downwardPaths.length > 0)
- logger.debug(
- `Found downward ${getCurrentGeminiMdFilename()} files (sorted): ${JSON.stringify(
- downwardPaths,
- )}`,
- );
- // Add downward paths only if they haven't been included already (e.g. from upward scan)
- for (const dPath of downwardPaths) {
- if (!paths.includes(dPath)) {
- paths.push(dPath);
+ const downwardPaths = await bfsFileSearch(resolvedCwd, {
+ fileName: geminiMdFilename,
+ maxDirs: MAX_DIRECTORIES_TO_SCAN_FOR_MEMORY,
+ debug: debugMode,
+ respectGitIgnore: true,
+ projectRoot: projectRoot || resolvedCwd,
+ });
+ downwardPaths.sort(); // Sort for consistent ordering, though hierarchy might be more complex
+ if (debugMode && downwardPaths.length > 0)
+ logger.debug(
+ `Found downward ${geminiMdFilename} files (sorted): ${JSON.stringify(
+ downwardPaths,
+ )}`,
+ );
+ // Add downward paths only if they haven't been included already (e.g. from upward scan)
+ for (const dPath of downwardPaths) {
+ allPaths.add(dPath);
}
}
// Add extension context file paths
for (const extensionPath of extensionContextFilePaths) {
- if (!paths.includes(extensionPath)) {
- paths.push(extensionPath);
- }
+ allPaths.add(extensionPath);
}
+ const finalPaths = Array.from(allPaths);
+
if (debugMode)
logger.debug(
- `Final ordered ${getCurrentGeminiMdFilename()} paths to read: ${JSON.stringify(paths)}`,
+ `Final ordered ${getAllGeminiMdFilenames()} paths to read: ${JSON.stringify(
+ finalPaths,
+ )}`,
);
- return paths;
+ return finalPaths;
}
async function readGeminiMdFiles(
@@ -228,7 +232,7 @@ async function readGeminiMdFiles(
if (!isTestEnv) {
const message = error instanceof Error ? error.message : String(error);
logger.warn(
- `Warning: Could not read ${getCurrentGeminiMdFilename()} file at ${filePath}. Error: ${message}`,
+ `Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`,
);
}
results.push({ filePath, content: null }); // Still include it with null content