mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
Reload gemini memory on extension load/unload + memory refresh refactor (#12651)
This commit is contained in:
@@ -4,21 +4,14 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'node:fs';
|
|
||||||
import * as path from 'node:path';
|
|
||||||
import { homedir } from 'node:os';
|
|
||||||
import yargs from 'yargs/yargs';
|
import yargs from 'yargs/yargs';
|
||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from 'yargs/helpers';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { mcpCommand } from '../commands/mcp.js';
|
import { mcpCommand } from '../commands/mcp.js';
|
||||||
import type {
|
import type { OutputFormat } from '@google/gemini-cli-core';
|
||||||
FileFilteringOptions,
|
|
||||||
OutputFormat,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
import { extensionsCommand } from '../commands/extensions.js';
|
import { extensionsCommand } from '../commands/extensions.js';
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
loadServerHierarchicalMemory,
|
|
||||||
setGeminiMdFilename as setServerGeminiMdFilename,
|
setGeminiMdFilename as setServerGeminiMdFilename,
|
||||||
getCurrentGeminiMdFilename,
|
getCurrentGeminiMdFilename,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
@@ -36,6 +29,7 @@ import {
|
|||||||
getPty,
|
getPty,
|
||||||
EDIT_TOOL_NAME,
|
EDIT_TOOL_NAME,
|
||||||
debugLogger,
|
debugLogger,
|
||||||
|
loadServerHierarchicalMemory,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { Settings } from './settings.js';
|
import type { Settings } from './settings.js';
|
||||||
|
|
||||||
@@ -47,10 +41,7 @@ import { appEvents } from '../utils/events.js';
|
|||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||||
import { createPolicyEngineConfig } from './policy.js';
|
import { createPolicyEngineConfig } from './policy.js';
|
||||||
import { ExtensionManager } from './extension-manager.js';
|
import { ExtensionManager } from './extension-manager.js';
|
||||||
import type {
|
import type { ExtensionEvents } from '@google/gemini-cli-core/src/utils/extensionLoader.js';
|
||||||
ExtensionEvents,
|
|
||||||
ExtensionLoader,
|
|
||||||
} from '@google/gemini-cli-core/src/utils/extensionLoader.js';
|
|
||||||
import { requestConsentNonInteractive } from './extensions/consent.js';
|
import { requestConsentNonInteractive } from './extensions/consent.js';
|
||||||
import { promptForSetting } from './extensions/extensionSettings.js';
|
import { promptForSetting } from './extensions/extensionSettings.js';
|
||||||
import type { EventEmitter } from 'node:stream';
|
import type { EventEmitter } from 'node:stream';
|
||||||
@@ -297,49 +288,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
|||||||
return result as unknown as CliArgs;
|
return result as unknown as CliArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is now a thin wrapper around the server's implementation.
|
|
||||||
// It's kept in the CLI for now as App.tsx directly calls it for memory refresh.
|
|
||||||
// TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself.
|
|
||||||
export async function loadHierarchicalGeminiMemory(
|
|
||||||
currentWorkingDirectory: string,
|
|
||||||
includeDirectoriesToReadGemini: readonly string[] = [],
|
|
||||||
debugMode: boolean,
|
|
||||||
fileService: FileDiscoveryService,
|
|
||||||
settings: Settings,
|
|
||||||
extensionLoader: ExtensionLoader,
|
|
||||||
folderTrust: boolean,
|
|
||||||
memoryImportFormat: 'flat' | 'tree' = 'tree',
|
|
||||||
fileFilteringOptions?: FileFilteringOptions,
|
|
||||||
): Promise<{ memoryContent: string; fileCount: number; filePaths: string[] }> {
|
|
||||||
// FIX: Use real, canonical paths for a reliable comparison to handle symlinks.
|
|
||||||
const realCwd = fs.realpathSync(path.resolve(currentWorkingDirectory));
|
|
||||||
const realHome = fs.realpathSync(path.resolve(homedir()));
|
|
||||||
const isHomeDirectory = realCwd === realHome;
|
|
||||||
|
|
||||||
// If it is the home directory, pass an empty string to the core memory
|
|
||||||
// function to signal that it should skip the workspace search.
|
|
||||||
const effectiveCwd = isHomeDirectory ? '' : currentWorkingDirectory;
|
|
||||||
|
|
||||||
if (debugMode) {
|
|
||||||
debugLogger.debug(
|
|
||||||
`CLI: Delegating hierarchical memory load to server for CWD: ${currentWorkingDirectory} (memoryImportFormat: ${memoryImportFormat})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directly call the server function with the corrected path.
|
|
||||||
return loadServerHierarchicalMemory(
|
|
||||||
effectiveCwd,
|
|
||||||
includeDirectoriesToReadGemini,
|
|
||||||
debugMode,
|
|
||||||
fileService,
|
|
||||||
extensionLoader,
|
|
||||||
folderTrust,
|
|
||||||
memoryImportFormat,
|
|
||||||
fileFilteringOptions,
|
|
||||||
settings.context?.discoveryMaxDirs,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a filter function to determine if a tool should be excluded.
|
* Creates a filter function to determine if a tool should be excluded.
|
||||||
*
|
*
|
||||||
@@ -437,18 +385,18 @@ export async function loadCliConfig(
|
|||||||
|
|
||||||
// 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 { memoryContent, fileCount, filePaths } =
|
||||||
await loadHierarchicalGeminiMemory(
|
await loadServerHierarchicalMemory(
|
||||||
cwd,
|
cwd,
|
||||||
settings.context?.loadMemoryFromIncludeDirectories
|
settings.context?.loadMemoryFromIncludeDirectories
|
||||||
? includeDirectories
|
? includeDirectories
|
||||||
: [],
|
: [],
|
||||||
debugMode,
|
debugMode,
|
||||||
fileService,
|
fileService,
|
||||||
settings,
|
|
||||||
extensionManager,
|
extensionManager,
|
||||||
trustedFolder,
|
trustedFolder,
|
||||||
memoryImportFormat,
|
memoryImportFormat,
|
||||||
memoryFileFiltering,
|
memoryFileFiltering,
|
||||||
|
settings.context?.discoveryMaxDirs,
|
||||||
);
|
);
|
||||||
|
|
||||||
const question = argv.promptInteractive || argv.prompt || '';
|
const question = argv.promptInteractive || argv.prompt || '';
|
||||||
|
|||||||
@@ -48,10 +48,11 @@ import {
|
|||||||
debugLogger,
|
debugLogger,
|
||||||
coreEvents,
|
coreEvents,
|
||||||
CoreEvent,
|
CoreEvent,
|
||||||
|
refreshServerHierarchicalMemory,
|
||||||
type ModelChangedPayload,
|
type ModelChangedPayload,
|
||||||
|
type MemoryChangedPayload,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { validateAuthMethod } from '../config/auth.js';
|
import { validateAuthMethod } from '../config/auth.js';
|
||||||
import { loadHierarchicalGeminiMemory } from '../config/config.js';
|
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { useHistory } from './hooks/useHistoryManager.js';
|
import { useHistory } from './hooks/useHistoryManager.js';
|
||||||
import { useMemoryMonitor } from './hooks/useMemoryMonitor.js';
|
import { useMemoryMonitor } from './hooks/useMemoryMonitor.js';
|
||||||
@@ -160,9 +161,6 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
const [showDebugProfiler, setShowDebugProfiler] = useState(false);
|
const [showDebugProfiler, setShowDebugProfiler] = useState(false);
|
||||||
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
|
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
|
||||||
|
|
||||||
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(
|
|
||||||
initializationResult.geminiMdFileCount,
|
|
||||||
);
|
|
||||||
const [shellModeActive, setShellModeActive] = useState(false);
|
const [shellModeActive, setShellModeActive] = useState(false);
|
||||||
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
|
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
@@ -569,7 +567,6 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
refreshStatic,
|
refreshStatic,
|
||||||
toggleVimEnabled,
|
toggleVimEnabled,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
setGeminiMdFileCount,
|
|
||||||
slashCommandActions,
|
slashCommandActions,
|
||||||
extensionsUpdateStateInternal,
|
extensionsUpdateStateInternal,
|
||||||
isConfigInitialized,
|
isConfigInitialized,
|
||||||
@@ -584,26 +581,8 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
Date.now(),
|
Date.now(),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const { memoryContent, fileCount, filePaths } =
|
const { memoryContent, fileCount } =
|
||||||
await loadHierarchicalGeminiMemory(
|
await refreshServerHierarchicalMemory(config);
|
||||||
process.cwd(),
|
|
||||||
settings.merged.context?.loadMemoryFromIncludeDirectories
|
|
||||||
? config.getWorkspaceContext().getDirectories()
|
|
||||||
: [],
|
|
||||||
config.getDebugMode(),
|
|
||||||
config.getFileService(),
|
|
||||||
settings.merged,
|
|
||||||
config.getExtensionLoader(),
|
|
||||||
config.isTrustedFolder(),
|
|
||||||
settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree'
|
|
||||||
config.getFileFilteringOptions(),
|
|
||||||
);
|
|
||||||
|
|
||||||
config.setUserMemory(memoryContent);
|
|
||||||
config.setGeminiMdFileCount(fileCount);
|
|
||||||
config.setGeminiMdFilePaths(filePaths);
|
|
||||||
|
|
||||||
setGeminiMdFileCount(fileCount);
|
|
||||||
|
|
||||||
historyManager.addItem(
|
historyManager.addItem(
|
||||||
{
|
{
|
||||||
@@ -635,7 +614,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
);
|
);
|
||||||
debugLogger.warn('Error refreshing memory:', error);
|
debugLogger.warn('Error refreshing memory:', error);
|
||||||
}
|
}
|
||||||
}, [config, historyManager, settings.merged]);
|
}, [config, historyManager]);
|
||||||
|
|
||||||
const cancelHandlerRef = useRef<() => void>(() => {});
|
const cancelHandlerRef = useRef<() => void>(() => {});
|
||||||
|
|
||||||
@@ -1248,6 +1227,19 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
|||||||
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
|
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(
|
||||||
|
config.getGeminiMdFileCount(),
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMemoryChanged = (result: MemoryChangedPayload) => {
|
||||||
|
setGeminiMdFileCount(result.fileCount);
|
||||||
|
};
|
||||||
|
coreEvents.on(CoreEvent.MemoryChanged, handleMemoryChanged);
|
||||||
|
return () => {
|
||||||
|
coreEvents.off(CoreEvent.MemoryChanged, handleMemoryChanged);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const uiState: UIState = useMemo(
|
const uiState: UIState = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
history: historyManager.history,
|
history: historyManager.history,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { CommandKind } from './types.js';
|
|||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { loadServerHierarchicalMemory } from '@google/gemini-cli-core';
|
import { refreshServerHierarchicalMemory } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
export function expandHomeDir(p: string): string {
|
export function expandHomeDir(p: string): string {
|
||||||
if (!p) {
|
if (!p) {
|
||||||
@@ -94,25 +94,7 @@ export const directoryCommand: SlashCommand = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (config.shouldLoadMemoryFromIncludeDirectories()) {
|
if (config.shouldLoadMemoryFromIncludeDirectories()) {
|
||||||
const { memoryContent, fileCount } =
|
await refreshServerHierarchicalMemory(config);
|
||||||
await loadServerHierarchicalMemory(
|
|
||||||
config.getWorkingDir(),
|
|
||||||
[
|
|
||||||
...config.getWorkspaceContext().getDirectories(),
|
|
||||||
...pathsToAdd,
|
|
||||||
],
|
|
||||||
config.getDebugMode(),
|
|
||||||
config.getFileService(),
|
|
||||||
config.getExtensionLoader(),
|
|
||||||
config.getFolderTrust(),
|
|
||||||
context.services.settings.merged.context?.importFormat ||
|
|
||||||
'tree', // Use setting or default to 'tree'
|
|
||||||
config.getFileFilteringOptions(),
|
|
||||||
context.services.settings.merged.context?.discoveryMaxDirs,
|
|
||||||
);
|
|
||||||
config.setUserMemory(memoryContent);
|
|
||||||
config.setGeminiMdFileCount(fileCount);
|
|
||||||
context.ui.setGeminiMdFileCount(fileCount);
|
|
||||||
}
|
}
|
||||||
addItem(
|
addItem(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import { MessageType } from '../types.js';
|
|||||||
import type { LoadedSettings } from '../../config/settings.js';
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
import {
|
import {
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
|
refreshServerHierarchicalMemory,
|
||||||
SimpleExtensionLoader,
|
SimpleExtensionLoader,
|
||||||
type FileDiscoveryService,
|
type FileDiscoveryService,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { LoadServerHierarchicalMemoryResponse } from '@google/gemini-cli-core/index.js';
|
import type { LoadServerHierarchicalMemoryResponse } from '@google/gemini-cli-core/index.js';
|
||||||
import { loadHierarchicalGeminiMemory } from '../../config/config.js';
|
|
||||||
|
|
||||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
const original =
|
const original =
|
||||||
@@ -28,19 +28,12 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|||||||
if (error instanceof Error) return error.message;
|
if (error instanceof Error) return error.message;
|
||||||
return String(error);
|
return String(error);
|
||||||
}),
|
}),
|
||||||
|
refreshServerHierarchicalMemory: vi.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../config/config.js', async (importOriginal) => {
|
const mockRefreshServerHierarchicalMemory =
|
||||||
const original =
|
refreshServerHierarchicalMemory as Mock;
|
||||||
await importOriginal<typeof import('../../config/config.js')>();
|
|
||||||
return {
|
|
||||||
...original,
|
|
||||||
loadHierarchicalGeminiMemory: vi.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockLoadHierarchicalGeminiMemory = loadHierarchicalGeminiMemory as Mock;
|
|
||||||
|
|
||||||
describe('memoryCommand', () => {
|
describe('memoryCommand', () => {
|
||||||
let mockContext: CommandContext;
|
let mockContext: CommandContext;
|
||||||
@@ -203,11 +196,8 @@ describe('memoryCommand', () => {
|
|||||||
},
|
},
|
||||||
} as unknown as LoadedSettings,
|
} as unknown as LoadedSettings,
|
||||||
},
|
},
|
||||||
ui: {
|
|
||||||
setGeminiMdFileCount: vi.fn(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
mockLoadHierarchicalGeminiMemory.mockClear();
|
mockRefreshServerHierarchicalMemory.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display success message when memory is refreshed with content', async () => {
|
it('should display success message when memory is refreshed with content', async () => {
|
||||||
@@ -218,7 +208,7 @@ describe('memoryCommand', () => {
|
|||||||
fileCount: 2,
|
fileCount: 2,
|
||||||
filePaths: ['/path/one/GEMINI.md', '/path/two/GEMINI.md'],
|
filePaths: ['/path/one/GEMINI.md', '/path/two/GEMINI.md'],
|
||||||
};
|
};
|
||||||
mockLoadHierarchicalGeminiMemory.mockResolvedValue(refreshResult);
|
mockRefreshServerHierarchicalMemory.mockResolvedValue(refreshResult);
|
||||||
|
|
||||||
await refreshCommand.action(mockContext, '');
|
await refreshCommand.action(mockContext, '');
|
||||||
|
|
||||||
@@ -230,19 +220,7 @@ describe('memoryCommand', () => {
|
|||||||
expect.any(Number),
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockLoadHierarchicalGeminiMemory).toHaveBeenCalledOnce();
|
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||||
expect(mockSetUserMemory).toHaveBeenCalledWith(
|
|
||||||
refreshResult.memoryContent,
|
|
||||||
);
|
|
||||||
expect(mockSetGeminiMdFileCount).toHaveBeenCalledWith(
|
|
||||||
refreshResult.fileCount,
|
|
||||||
);
|
|
||||||
expect(mockSetGeminiMdFilePaths).toHaveBeenCalledWith(
|
|
||||||
refreshResult.filePaths,
|
|
||||||
);
|
|
||||||
expect(mockContext.ui.setGeminiMdFileCount).toHaveBeenCalledWith(
|
|
||||||
refreshResult.fileCount,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
@@ -257,14 +235,11 @@ describe('memoryCommand', () => {
|
|||||||
if (!refreshCommand.action) throw new Error('Command has no action');
|
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||||
|
|
||||||
const refreshResult = { memoryContent: '', fileCount: 0, filePaths: [] };
|
const refreshResult = { memoryContent: '', fileCount: 0, filePaths: [] };
|
||||||
mockLoadHierarchicalGeminiMemory.mockResolvedValue(refreshResult);
|
mockRefreshServerHierarchicalMemory.mockResolvedValue(refreshResult);
|
||||||
|
|
||||||
await refreshCommand.action(mockContext, '');
|
await refreshCommand.action(mockContext, '');
|
||||||
|
|
||||||
expect(mockLoadHierarchicalGeminiMemory).toHaveBeenCalledOnce();
|
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||||
expect(mockSetUserMemory).toHaveBeenCalledWith('');
|
|
||||||
expect(mockSetGeminiMdFileCount).toHaveBeenCalledWith(0);
|
|
||||||
expect(mockSetGeminiMdFilePaths).toHaveBeenCalledWith([]);
|
|
||||||
|
|
||||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
{
|
{
|
||||||
@@ -279,11 +254,11 @@ describe('memoryCommand', () => {
|
|||||||
if (!refreshCommand.action) throw new Error('Command has no action');
|
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||||
|
|
||||||
const error = new Error('Failed to read memory files.');
|
const error = new Error('Failed to read memory files.');
|
||||||
mockLoadHierarchicalGeminiMemory.mockRejectedValue(error);
|
mockRefreshServerHierarchicalMemory.mockRejectedValue(error);
|
||||||
|
|
||||||
await refreshCommand.action(mockContext, '');
|
await refreshCommand.action(mockContext, '');
|
||||||
|
|
||||||
expect(mockLoadHierarchicalGeminiMemory).toHaveBeenCalledOnce();
|
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||||
expect(mockSetUserMemory).not.toHaveBeenCalled();
|
expect(mockSetUserMemory).not.toHaveBeenCalled();
|
||||||
expect(mockSetGeminiMdFileCount).not.toHaveBeenCalled();
|
expect(mockSetGeminiMdFileCount).not.toHaveBeenCalled();
|
||||||
expect(mockSetGeminiMdFilePaths).not.toHaveBeenCalled();
|
expect(mockSetGeminiMdFilePaths).not.toHaveBeenCalled();
|
||||||
@@ -318,7 +293,7 @@ describe('memoryCommand', () => {
|
|||||||
expect.any(Number),
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockLoadHierarchicalGeminiMemory).not.toHaveBeenCalled();
|
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getErrorMessage } from '@google/gemini-cli-core';
|
import {
|
||||||
|
getErrorMessage,
|
||||||
|
refreshServerHierarchicalMemory,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
import { loadHierarchicalGeminiMemory } from '../../config/config.js';
|
|
||||||
import type { SlashCommand, SlashCommandActionReturn } from './types.js';
|
import type { SlashCommand, SlashCommandActionReturn } from './types.js';
|
||||||
import { CommandKind } from './types.js';
|
import { CommandKind } from './types.js';
|
||||||
|
|
||||||
@@ -80,26 +82,9 @@ export const memoryCommand: SlashCommand = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const config = await context.services.config;
|
const config = await context.services.config;
|
||||||
const settings = context.services.settings;
|
|
||||||
if (config) {
|
if (config) {
|
||||||
const { memoryContent, fileCount, filePaths } =
|
const { memoryContent, fileCount } =
|
||||||
await loadHierarchicalGeminiMemory(
|
await refreshServerHierarchicalMemory(config);
|
||||||
config.getWorkingDir(),
|
|
||||||
config.shouldLoadMemoryFromIncludeDirectories()
|
|
||||||
? config.getWorkspaceContext().getDirectories()
|
|
||||||
: [],
|
|
||||||
config.getDebugMode(),
|
|
||||||
config.getFileService(),
|
|
||||||
settings.merged,
|
|
||||||
config.getExtensionLoader(),
|
|
||||||
config.isTrustedFolder(),
|
|
||||||
settings.merged.context?.importFormat || 'tree',
|
|
||||||
config.getFileFilteringOptions(),
|
|
||||||
);
|
|
||||||
config.setUserMemory(memoryContent);
|
|
||||||
config.setGeminiMdFileCount(fileCount);
|
|
||||||
config.setGeminiMdFilePaths(filePaths);
|
|
||||||
context.ui.setGeminiMdFileCount(fileCount);
|
|
||||||
|
|
||||||
const successMessage =
|
const successMessage =
|
||||||
memoryContent.length > 0
|
memoryContent.length > 0
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export interface CommandContext {
|
|||||||
toggleCorgiMode: () => void;
|
toggleCorgiMode: () => void;
|
||||||
toggleDebugProfiler: () => void;
|
toggleDebugProfiler: () => void;
|
||||||
toggleVimEnabled: () => Promise<boolean>;
|
toggleVimEnabled: () => Promise<boolean>;
|
||||||
setGeminiMdFileCount: (count: number) => void;
|
|
||||||
reloadCommands: () => void;
|
reloadCommands: () => void;
|
||||||
extensionsUpdateState: Map<string, ExtensionUpdateStatus>;
|
extensionsUpdateState: Map<string, ExtensionUpdateStatus>;
|
||||||
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
|
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
vi.fn(), // refreshStatic
|
vi.fn(), // refreshStatic
|
||||||
vi.fn(), // toggleVimEnabled
|
vi.fn(), // toggleVimEnabled
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
vi.fn(), // setGeminiMdFileCount
|
|
||||||
{
|
{
|
||||||
openAuthDialog: mockOpenAuthDialog,
|
openAuthDialog: mockOpenAuthDialog,
|
||||||
openThemeDialog: mockOpenThemeDialog,
|
openThemeDialog: mockOpenThemeDialog,
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export const useSlashCommandProcessor = (
|
|||||||
refreshStatic: () => void,
|
refreshStatic: () => void,
|
||||||
toggleVimEnabled: () => Promise<boolean>,
|
toggleVimEnabled: () => Promise<boolean>,
|
||||||
setIsProcessing: (isProcessing: boolean) => void,
|
setIsProcessing: (isProcessing: boolean) => void,
|
||||||
setGeminiMdFileCount: (count: number) => void,
|
|
||||||
actions: SlashCommandProcessorActions,
|
actions: SlashCommandProcessorActions,
|
||||||
extensionsUpdateState: Map<string, ExtensionUpdateStatus>,
|
extensionsUpdateState: Map<string, ExtensionUpdateStatus>,
|
||||||
isConfigInitialized: boolean,
|
isConfigInitialized: boolean,
|
||||||
@@ -207,7 +206,6 @@ export const useSlashCommandProcessor = (
|
|||||||
toggleCorgiMode: actions.toggleCorgiMode,
|
toggleCorgiMode: actions.toggleCorgiMode,
|
||||||
toggleDebugProfiler: actions.toggleDebugProfiler,
|
toggleDebugProfiler: actions.toggleDebugProfiler,
|
||||||
toggleVimEnabled,
|
toggleVimEnabled,
|
||||||
setGeminiMdFileCount,
|
|
||||||
reloadCommands,
|
reloadCommands,
|
||||||
extensionsUpdateState,
|
extensionsUpdateState,
|
||||||
dispatchExtensionStateUpdate: actions.dispatchExtensionStateUpdate,
|
dispatchExtensionStateUpdate: actions.dispatchExtensionStateUpdate,
|
||||||
@@ -234,7 +232,6 @@ export const useSlashCommandProcessor = (
|
|||||||
setPendingItem,
|
setPendingItem,
|
||||||
toggleVimEnabled,
|
toggleVimEnabled,
|
||||||
sessionShellAllowlist,
|
sessionShellAllowlist,
|
||||||
setGeminiMdFileCount,
|
|
||||||
reloadCommands,
|
reloadCommands,
|
||||||
extensionsUpdateState,
|
extensionsUpdateState,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export function createNonInteractiveUI(): CommandContext['ui'] {
|
|||||||
toggleCorgiMode: () => {},
|
toggleCorgiMode: () => {},
|
||||||
toggleDebugProfiler: () => {},
|
toggleDebugProfiler: () => {},
|
||||||
toggleVimEnabled: async () => false,
|
toggleVimEnabled: async () => false,
|
||||||
setGeminiMdFileCount: (_count) => {},
|
|
||||||
reloadCommands: () => {},
|
reloadCommands: () => {},
|
||||||
extensionsUpdateState: new Map(),
|
extensionsUpdateState: new Map(),
|
||||||
dispatchExtensionStateUpdate: (_action: ExtensionUpdateAction) => {},
|
dispatchExtensionStateUpdate: (_action: ExtensionUpdateAction) => {},
|
||||||
|
|||||||
@@ -266,6 +266,8 @@ export interface ConfigParameters {
|
|||||||
folderTrust?: boolean;
|
folderTrust?: boolean;
|
||||||
ideMode?: boolean;
|
ideMode?: boolean;
|
||||||
loadMemoryFromIncludeDirectories?: boolean;
|
loadMemoryFromIncludeDirectories?: boolean;
|
||||||
|
importFormat?: 'tree' | 'flat';
|
||||||
|
discoveryMaxDirs?: number;
|
||||||
compressionThreshold?: number;
|
compressionThreshold?: number;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
trustedFolder?: boolean;
|
trustedFolder?: boolean;
|
||||||
@@ -369,6 +371,8 @@ export class Config {
|
|||||||
| undefined;
|
| undefined;
|
||||||
private readonly experimentalZedIntegration: boolean = false;
|
private readonly experimentalZedIntegration: boolean = false;
|
||||||
private readonly loadMemoryFromIncludeDirectories: boolean = false;
|
private readonly loadMemoryFromIncludeDirectories: boolean = false;
|
||||||
|
private readonly importFormat: 'tree' | 'flat';
|
||||||
|
private readonly discoveryMaxDirs: number;
|
||||||
private readonly compressionThreshold: number | undefined;
|
private readonly compressionThreshold: number | undefined;
|
||||||
private readonly interactive: boolean;
|
private readonly interactive: boolean;
|
||||||
private readonly ptyInfo: string;
|
private readonly ptyInfo: string;
|
||||||
@@ -479,6 +483,8 @@ export class Config {
|
|||||||
this.ideMode = params.ideMode ?? false;
|
this.ideMode = params.ideMode ?? false;
|
||||||
this.loadMemoryFromIncludeDirectories =
|
this.loadMemoryFromIncludeDirectories =
|
||||||
params.loadMemoryFromIncludeDirectories ?? false;
|
params.loadMemoryFromIncludeDirectories ?? false;
|
||||||
|
this.importFormat = params.importFormat ?? 'tree';
|
||||||
|
this.discoveryMaxDirs = params.discoveryMaxDirs ?? 200;
|
||||||
this.compressionThreshold = params.compressionThreshold;
|
this.compressionThreshold = params.compressionThreshold;
|
||||||
this.interactive = params.interactive ?? false;
|
this.interactive = params.interactive ?? false;
|
||||||
this.ptyInfo = params.ptyInfo ?? 'child_process';
|
this.ptyInfo = params.ptyInfo ?? 'child_process';
|
||||||
@@ -707,6 +713,14 @@ export class Config {
|
|||||||
return this.loadMemoryFromIncludeDirectories;
|
return this.loadMemoryFromIncludeDirectories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getImportFormat(): 'tree' | 'flat' {
|
||||||
|
return this.importFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiscoveryMaxDirs(): number {
|
||||||
|
return this.discoveryMaxDirs;
|
||||||
|
}
|
||||||
|
|
||||||
getContentGeneratorConfig(): ContentGeneratorConfig {
|
getContentGeneratorConfig(): ContentGeneratorConfig {
|
||||||
return this.contentGeneratorConfig;
|
return this.contentGeneratorConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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.
|
||||||
@@ -53,13 +54,26 @@ export interface ModelChangedPayload {
|
|||||||
model: string;
|
model: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload for the 'memory-changed' event.
|
||||||
|
*/
|
||||||
|
export type MemoryChangedPayload = LoadServerHierarchicalMemoryResponse;
|
||||||
|
|
||||||
export enum CoreEvent {
|
export enum CoreEvent {
|
||||||
UserFeedback = 'user-feedback',
|
UserFeedback = 'user-feedback',
|
||||||
FallbackModeChanged = 'fallback-mode-changed',
|
FallbackModeChanged = 'fallback-mode-changed',
|
||||||
ModelChanged = 'model-changed',
|
ModelChanged = 'model-changed',
|
||||||
|
MemoryChanged = 'memory-changed',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CoreEventEmitter extends EventEmitter {
|
export interface CoreEvents {
|
||||||
|
[CoreEvent.UserFeedback]: [UserFeedbackPayload];
|
||||||
|
[CoreEvent.FallbackModeChanged]: [FallbackModeChangedPayload];
|
||||||
|
[CoreEvent.ModelChanged]: [ModelChangedPayload];
|
||||||
|
[CoreEvent.MemoryChanged]: [MemoryChangedPayload];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
||||||
private _feedbackBacklog: UserFeedbackPayload[] = [];
|
private _feedbackBacklog: UserFeedbackPayload[] = [];
|
||||||
private static readonly MAX_BACKLOG_SIZE = 10000;
|
private static readonly MAX_BACKLOG_SIZE = 10000;
|
||||||
|
|
||||||
@@ -116,63 +130,6 @@ export class CoreEventEmitter extends EventEmitter {
|
|||||||
this.emit(CoreEvent.UserFeedback, payload);
|
this.emit(CoreEvent.UserFeedback, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override on(
|
|
||||||
event: CoreEvent.UserFeedback,
|
|
||||||
listener: (payload: UserFeedbackPayload) => void,
|
|
||||||
): this;
|
|
||||||
override on(
|
|
||||||
event: CoreEvent.FallbackModeChanged,
|
|
||||||
listener: (payload: FallbackModeChangedPayload) => void,
|
|
||||||
): this;
|
|
||||||
override on(
|
|
||||||
event: CoreEvent.ModelChanged,
|
|
||||||
listener: (payload: ModelChangedPayload) => void,
|
|
||||||
): this;
|
|
||||||
override on(
|
|
||||||
event: string | symbol,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
listener: (...args: any[]) => void,
|
|
||||||
): this {
|
|
||||||
return super.on(event, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
override off(
|
|
||||||
event: CoreEvent.UserFeedback,
|
|
||||||
listener: (payload: UserFeedbackPayload) => void,
|
|
||||||
): this;
|
|
||||||
override off(
|
|
||||||
event: CoreEvent.FallbackModeChanged,
|
|
||||||
listener: (payload: FallbackModeChangedPayload) => void,
|
|
||||||
): this;
|
|
||||||
override off(
|
|
||||||
event: CoreEvent.ModelChanged,
|
|
||||||
listener: (payload: ModelChangedPayload) => void,
|
|
||||||
): this;
|
|
||||||
override off(
|
|
||||||
event: string | symbol,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
listener: (...args: any[]) => void,
|
|
||||||
): this {
|
|
||||||
return super.off(event, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
override emit(
|
|
||||||
event: CoreEvent.UserFeedback,
|
|
||||||
payload: UserFeedbackPayload,
|
|
||||||
): boolean;
|
|
||||||
override emit(
|
|
||||||
event: CoreEvent.FallbackModeChanged,
|
|
||||||
payload: FallbackModeChangedPayload,
|
|
||||||
): boolean;
|
|
||||||
override emit(
|
|
||||||
event: CoreEvent.ModelChanged,
|
|
||||||
payload: ModelChangedPayload,
|
|
||||||
): boolean;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
override emit(event: string | symbol, ...args: any[]): boolean {
|
|
||||||
return super.emit(event, ...args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const coreEvents = new CoreEventEmitter();
|
export const coreEvents = new CoreEventEmitter();
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ import { SimpleExtensionLoader } from './extensionLoader.js';
|
|||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import { type McpClientManager } from '../tools/mcp-client-manager.js';
|
import { type McpClientManager } from '../tools/mcp-client-manager.js';
|
||||||
|
|
||||||
|
const mockRefreshServerHierarchicalMemory = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock('./memoryDiscovery.js', async (importActual) => {
|
||||||
|
const actual = await importActual<typeof import('./memoryDiscovery.js')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
refreshServerHierarchicalMemory: mockRefreshServerHierarchicalMemory,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('SimpleExtensionLoader', () => {
|
describe('SimpleExtensionLoader', () => {
|
||||||
let mockConfig: Config;
|
let mockConfig: Config;
|
||||||
let extensionReloadingEnabled: boolean;
|
let extensionReloadingEnabled: boolean;
|
||||||
@@ -79,10 +89,14 @@ describe('SimpleExtensionLoader', () => {
|
|||||||
).toHaveBeenCalledExactlyOnceWith(activeExtension);
|
).toHaveBeenCalledExactlyOnceWith(activeExtension);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([true, false])(
|
describe.each([true, false])(
|
||||||
'should only call `start` and `stop` if extension reloading is enabled ($i)',
|
'when enableExtensionReloading === $i',
|
||||||
async (reloadingEnabled) => {
|
(reloadingEnabled) => {
|
||||||
|
beforeEach(() => {
|
||||||
extensionReloadingEnabled = reloadingEnabled;
|
extensionReloadingEnabled = reloadingEnabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should ${reloadingEnabled ? '' : 'not '}reload extension features`, async () => {
|
||||||
const loader = new SimpleExtensionLoader([]);
|
const loader = new SimpleExtensionLoader([]);
|
||||||
await loader.start(mockConfig);
|
await loader.start(mockConfig);
|
||||||
expect(mockMcpClientManager.startExtension).not.toHaveBeenCalled();
|
expect(mockMcpClientManager.startExtension).not.toHaveBeenCalled();
|
||||||
@@ -91,17 +105,43 @@ describe('SimpleExtensionLoader', () => {
|
|||||||
expect(
|
expect(
|
||||||
mockMcpClientManager.startExtension,
|
mockMcpClientManager.startExtension,
|
||||||
).toHaveBeenCalledExactlyOnceWith(activeExtension);
|
).toHaveBeenCalledExactlyOnceWith(activeExtension);
|
||||||
|
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||||
} else {
|
} else {
|
||||||
expect(mockMcpClientManager.startExtension).not.toHaveBeenCalled();
|
expect(mockMcpClientManager.startExtension).not.toHaveBeenCalled();
|
||||||
|
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
mockRefreshServerHierarchicalMemory.mockClear();
|
||||||
|
|
||||||
await loader.unloadExtension(activeExtension);
|
await loader.unloadExtension(activeExtension);
|
||||||
if (reloadingEnabled) {
|
if (reloadingEnabled) {
|
||||||
expect(
|
expect(
|
||||||
mockMcpClientManager.stopExtension,
|
mockMcpClientManager.stopExtension,
|
||||||
).toHaveBeenCalledExactlyOnceWith(activeExtension);
|
).toHaveBeenCalledExactlyOnceWith(activeExtension);
|
||||||
|
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||||
} else {
|
} else {
|
||||||
expect(mockMcpClientManager.stopExtension).not.toHaveBeenCalled();
|
expect(mockMcpClientManager.stopExtension).not.toHaveBeenCalled();
|
||||||
|
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.runIf(reloadingEnabled)(
|
||||||
|
'Should only reload memory once all extensions are done',
|
||||||
|
async () => {
|
||||||
|
const anotherExtension = {
|
||||||
|
...activeExtension,
|
||||||
|
name: 'another-extension',
|
||||||
|
};
|
||||||
|
const loader = new SimpleExtensionLoader([]);
|
||||||
|
await loader.loadExtension(activeExtension);
|
||||||
|
await loader.start(mockConfig);
|
||||||
|
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||||
|
await Promise.all([
|
||||||
|
loader.unloadExtension(activeExtension),
|
||||||
|
loader.loadExtension(anotherExtension),
|
||||||
|
]);
|
||||||
|
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import type { EventEmitter } from 'node:events';
|
import type { EventEmitter } from 'node:events';
|
||||||
import type { Config, GeminiCLIExtension } from '../config/config.js';
|
import type { Config, GeminiCLIExtension } from '../config/config.js';
|
||||||
|
import { refreshServerHierarchicalMemory } from './memoryDiscovery.js';
|
||||||
|
|
||||||
export abstract class ExtensionLoader {
|
export abstract class ExtensionLoader {
|
||||||
// Assigned in `start`.
|
// Assigned in `start`.
|
||||||
@@ -18,6 +19,9 @@ export abstract class ExtensionLoader {
|
|||||||
protected stoppingCount: number = 0;
|
protected stoppingCount: number = 0;
|
||||||
protected stopCompletedCount: number = 0;
|
protected stopCompletedCount: number = 0;
|
||||||
|
|
||||||
|
// Whether or not we are currently executing `start`
|
||||||
|
private isStarting: boolean = false;
|
||||||
|
|
||||||
constructor(private readonly eventEmitter?: EventEmitter<ExtensionEvents>) {}
|
constructor(private readonly eventEmitter?: EventEmitter<ExtensionEvents>) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,6 +36,8 @@ export abstract class ExtensionLoader {
|
|||||||
* McpClientManager, PromptRegistry, and GeminiChat set up.
|
* McpClientManager, PromptRegistry, and GeminiChat set up.
|
||||||
*/
|
*/
|
||||||
async start(config: Config): Promise<void> {
|
async start(config: Config): Promise<void> {
|
||||||
|
this.isStarting = true;
|
||||||
|
try {
|
||||||
if (!this.config) {
|
if (!this.config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
} else {
|
} else {
|
||||||
@@ -42,6 +48,9 @@ export abstract class ExtensionLoader {
|
|||||||
.filter((e) => e.isActive)
|
.filter((e) => e.isActive)
|
||||||
.map(this.startExtension.bind(this)),
|
.map(this.startExtension.bind(this)),
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
this.isStarting = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,12 +73,15 @@ export abstract class ExtensionLoader {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await this.config.getMcpClientManager()!.startExtension(extension);
|
await this.config.getMcpClientManager()!.startExtension(extension);
|
||||||
|
// Note: Context files are loaded only once all extensions are done
|
||||||
|
// loading/unloading to reduce churn, see the `maybeRefreshMemories` call
|
||||||
|
// below.
|
||||||
|
|
||||||
// TODO: Update custom command updating away from the event based system
|
// TODO: Update custom command updating away from the event based system
|
||||||
// and call directly into a custom command manager here. See the
|
// and call directly into a custom command manager here. See the
|
||||||
// useSlashCommandProcessor hook which responds to events fired here today.
|
// useSlashCommandProcessor hook which responds to events fired here today.
|
||||||
|
|
||||||
// TODO: Move all enablement of extension features here, including at least:
|
// TODO: Move all enablement of extension features here, including at least:
|
||||||
// - context file loading
|
|
||||||
// - excluded tool configuration
|
// - excluded tool configuration
|
||||||
} finally {
|
} finally {
|
||||||
this.startCompletedCount++;
|
this.startCompletedCount++;
|
||||||
@@ -81,6 +93,25 @@ export abstract class ExtensionLoader {
|
|||||||
this.startingCount = 0;
|
this.startingCount = 0;
|
||||||
this.startCompletedCount = 0;
|
this.startCompletedCount = 0;
|
||||||
}
|
}
|
||||||
|
await this.maybeRefreshMemories();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async maybeRefreshMemories(): Promise<void> {
|
||||||
|
if (!this.config) {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot refresh gemini memories prior to calling `start`.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.isStarting && // Don't refresh memories on the first call to `start`.
|
||||||
|
this.startingCount === this.startCompletedCount &&
|
||||||
|
this.stoppingCount === this.stopCompletedCount
|
||||||
|
) {
|
||||||
|
// Wait until all extensions are done starting and stopping before we
|
||||||
|
// reload memory, this is somewhat expensive and also busts the context
|
||||||
|
// cache, we want to only do it once.
|
||||||
|
await refreshServerHierarchicalMemory(this.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,12 +150,15 @@ export abstract class ExtensionLoader {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.config.getMcpClientManager()!.stopExtension(extension);
|
await this.config.getMcpClientManager()!.stopExtension(extension);
|
||||||
|
// Note: Context files are loaded only once all extensions are done
|
||||||
|
// loading/unloading to reduce churn, see the `maybeRefreshMemories` call
|
||||||
|
// below.
|
||||||
|
|
||||||
// TODO: Update custom command updating away from the event based system
|
// TODO: Update custom command updating away from the event based system
|
||||||
// and call directly into a custom command manager here. See the
|
// and call directly into a custom command manager here. See the
|
||||||
// useSlashCommandProcessor hook which responds to events fired here today.
|
// useSlashCommandProcessor hook which responds to events fired here today.
|
||||||
|
|
||||||
// TODO: Remove all extension features here, including at least:
|
// TODO: Remove all extension features here, including at least:
|
||||||
// - context files
|
|
||||||
// - excluded tools
|
// - excluded tools
|
||||||
} finally {
|
} finally {
|
||||||
this.stopCompletedCount++;
|
this.stopCompletedCount++;
|
||||||
@@ -136,6 +170,7 @@ export abstract class ExtensionLoader {
|
|||||||
this.stoppingCount = 0;
|
this.stoppingCount = 0;
|
||||||
this.stopCompletedCount = 0;
|
this.stopCompletedCount = 0;
|
||||||
}
|
}
|
||||||
|
await this.maybeRefreshMemories();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
loadGlobalMemory,
|
loadGlobalMemory,
|
||||||
loadEnvironmentMemory,
|
loadEnvironmentMemory,
|
||||||
loadJitSubdirectoryMemory,
|
loadJitSubdirectoryMemory,
|
||||||
|
refreshServerHierarchicalMemory,
|
||||||
} from './memoryDiscovery.js';
|
} from './memoryDiscovery.js';
|
||||||
import {
|
import {
|
||||||
setGeminiMdFilename,
|
setGeminiMdFilename,
|
||||||
@@ -20,8 +21,10 @@ import {
|
|||||||
} from '../tools/memoryTool.js';
|
} from '../tools/memoryTool.js';
|
||||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||||
import { GEMINI_DIR } from './paths.js';
|
import { GEMINI_DIR } from './paths.js';
|
||||||
import type { GeminiCLIExtension } from '../config/config.js';
|
import { Config, type GeminiCLIExtension } from '../config/config.js';
|
||||||
|
import { Storage } from '../config/storage.js';
|
||||||
import { SimpleExtensionLoader } from './extensionLoader.js';
|
import { SimpleExtensionLoader } from './extensionLoader.js';
|
||||||
|
import { CoreEvent, coreEvents } from './events.js';
|
||||||
|
|
||||||
vi.mock('os', async (importOriginal) => {
|
vi.mock('os', async (importOriginal) => {
|
||||||
const actualOs = await importOriginal<typeof os>();
|
const actualOs = await importOriginal<typeof os>();
|
||||||
@@ -876,4 +879,58 @@ included directory memory
|
|||||||
expect(result.files.find((f) => f.path === outerMemory)).toBeUndefined();
|
expect(result.files.find((f) => f.path === outerMemory)).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('refreshServerHierarchicalMemory should refresh memory and update config', async () => {
|
||||||
|
const extensionLoader = new SimpleExtensionLoader([]);
|
||||||
|
const config = new Config({
|
||||||
|
sessionId: '1',
|
||||||
|
targetDir: cwd,
|
||||||
|
cwd,
|
||||||
|
debugMode: false,
|
||||||
|
model: 'fake-model',
|
||||||
|
extensionLoader,
|
||||||
|
});
|
||||||
|
const result = await loadServerHierarchicalMemory(
|
||||||
|
config.getWorkingDir(),
|
||||||
|
config.shouldLoadMemoryFromIncludeDirectories()
|
||||||
|
? config.getWorkspaceContext().getDirectories()
|
||||||
|
: [],
|
||||||
|
config.getDebugMode(),
|
||||||
|
config.getFileService(),
|
||||||
|
config.getExtensionLoader(),
|
||||||
|
config.isTrustedFolder(),
|
||||||
|
config.getImportFormat(),
|
||||||
|
);
|
||||||
|
expect(result.fileCount).equals(0);
|
||||||
|
|
||||||
|
// Now add an extension with a memory file
|
||||||
|
const extensionsDir = new Storage(homedir).getExtensionsDir();
|
||||||
|
const extensionPath = path.join(extensionsDir, 'new-extension');
|
||||||
|
const contextFilePath = path.join(extensionPath, 'CustomContext.md');
|
||||||
|
await fsPromises.mkdir(extensionPath, { recursive: true });
|
||||||
|
await fsPromises.writeFile(contextFilePath, 'Really cool custom context!');
|
||||||
|
await extensionLoader.loadExtension({
|
||||||
|
name: 'new-extension',
|
||||||
|
isActive: true,
|
||||||
|
contextFiles: [contextFilePath],
|
||||||
|
version: '1.0.0',
|
||||||
|
id: '1234',
|
||||||
|
path: extensionPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockEventListener = vi.fn();
|
||||||
|
coreEvents.on(CoreEvent.MemoryChanged, mockEventListener);
|
||||||
|
const refreshResult = await refreshServerHierarchicalMemory(config);
|
||||||
|
expect(refreshResult.fileCount).equals(1);
|
||||||
|
expect(config.getGeminiMdFileCount()).equals(refreshResult.fileCount);
|
||||||
|
expect(refreshResult.memoryContent).toContain(
|
||||||
|
'Really cool custom context!',
|
||||||
|
);
|
||||||
|
expect(config.getUserMemory()).equals(refreshResult.memoryContent);
|
||||||
|
expect(refreshResult.filePaths[0]).toContain(
|
||||||
|
path.join(extensionPath, 'CustomContext.md'),
|
||||||
|
);
|
||||||
|
expect(config.getGeminiMdFilePaths()).equals(refreshResult.filePaths);
|
||||||
|
expect(mockEventListener).toHaveBeenCalledExactlyOnceWith(refreshResult);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } from '../config/constants.js';
|
|||||||
import { GEMINI_DIR } from './paths.js';
|
import { GEMINI_DIR } from './paths.js';
|
||||||
import type { ExtensionLoader } from './extensionLoader.js';
|
import type { ExtensionLoader } from './extensionLoader.js';
|
||||||
import { debugLogger } from './debugLogger.js';
|
import { debugLogger } from './debugLogger.js';
|
||||||
|
import type { Config } from '../config/config.js';
|
||||||
|
import { CoreEvent, coreEvents } from './events.js';
|
||||||
|
|
||||||
// Simple console logger, similar to the one previously in CLI's config.ts
|
// Simple console logger, similar to the one previously in CLI's config.ts
|
||||||
// TODO: Integrate with a more robust server-side logger if available/appropriate.
|
// TODO: Integrate with a more robust server-side logger if available/appropriate.
|
||||||
@@ -481,6 +483,15 @@ export async function loadServerHierarchicalMemory(
|
|||||||
fileFilteringOptions?: FileFilteringOptions,
|
fileFilteringOptions?: FileFilteringOptions,
|
||||||
maxDirs: number = 200,
|
maxDirs: number = 200,
|
||||||
): Promise<LoadServerHierarchicalMemoryResponse> {
|
): Promise<LoadServerHierarchicalMemoryResponse> {
|
||||||
|
// FIX: Use real, canonical paths for a reliable comparison to handle symlinks.
|
||||||
|
const realCwd = await fs.realpath(path.resolve(currentWorkingDirectory));
|
||||||
|
const realHome = await fs.realpath(path.resolve(homedir()));
|
||||||
|
const isHomeDirectory = realCwd === realHome;
|
||||||
|
|
||||||
|
// If it is the home directory, pass an empty string to the core memory
|
||||||
|
// function to signal that it should skip the workspace search.
|
||||||
|
currentWorkingDirectory = isHomeDirectory ? '' : currentWorkingDirectory;
|
||||||
|
|
||||||
if (debugMode)
|
if (debugMode)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`,
|
`Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`,
|
||||||
@@ -538,6 +549,33 @@ export async function loadServerHierarchicalMemory(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the hierarchical memory and resets the state of `config` as needed such
|
||||||
|
* that it reflects the new memory.
|
||||||
|
*
|
||||||
|
* Returns the result of the call to `loadHierarchicalGeminiMemory`.
|
||||||
|
*/
|
||||||
|
export async function refreshServerHierarchicalMemory(config: Config) {
|
||||||
|
const result = await loadServerHierarchicalMemory(
|
||||||
|
config.getWorkingDir(),
|
||||||
|
config.shouldLoadMemoryFromIncludeDirectories()
|
||||||
|
? config.getWorkspaceContext().getDirectories()
|
||||||
|
: [],
|
||||||
|
config.getDebugMode(),
|
||||||
|
config.getFileService(),
|
||||||
|
config.getExtensionLoader(),
|
||||||
|
config.isTrustedFolder(),
|
||||||
|
config.getImportFormat(),
|
||||||
|
config.getFileFilteringOptions(),
|
||||||
|
config.getDiscoveryMaxDirs(),
|
||||||
|
);
|
||||||
|
config.setUserMemory(result.memoryContent);
|
||||||
|
config.setGeminiMdFileCount(result.fileCount);
|
||||||
|
config.setGeminiMdFilePaths(result.filePaths);
|
||||||
|
coreEvents.emit(CoreEvent.MemoryChanged, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadJitSubdirectoryMemory(
|
export async function loadJitSubdirectoryMemory(
|
||||||
targetPath: string,
|
targetPath: string,
|
||||||
trustedRoots: string[],
|
trustedRoots: string[],
|
||||||
|
|||||||
Reference in New Issue
Block a user