Merge pull request #2368 from huww98/fix/memory-show-multi-file-support

fix(cli): `/memory show --project` and `--global` now display all configured context files
This commit is contained in:
顾盼 2026-03-25 16:51:08 +08:00 committed by GitHub
commit b57b8ed5fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 159 additions and 44 deletions

View file

@ -168,6 +168,116 @@ describe('memoryCommand', () => {
expect.any(Number),
);
});
it('should fall back to AGENTS.md when QWEN.md does not exist for --project', async () => {
const projectCommand = showCommand.subCommands?.find(
(cmd) => cmd.name === '--project',
);
if (!projectCommand?.action) throw new Error('Command has no action');
setGeminiMdFilename(['QWEN.md', 'AGENTS.md']);
vi.spyOn(process, 'cwd').mockReturnValue('/test/project');
mockReadFile.mockImplementation(async (filePath: string) => {
if (filePath.endsWith('AGENTS.md')) return 'agents memory content';
throw new Error('ENOENT');
});
await projectCommand.action(mockContext, '');
const expectedPath = path.join('/test/project', 'AGENTS.md');
expect(mockReadFile).toHaveBeenCalledWith(expectedPath, 'utf-8');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: expect.stringContaining('agents memory content'),
},
expect.any(Number),
);
});
it('should fall back to AGENTS.md when QWEN.md does not exist for --global', async () => {
const globalCommand = showCommand.subCommands?.find(
(cmd) => cmd.name === '--global',
);
if (!globalCommand?.action) throw new Error('Command has no action');
setGeminiMdFilename(['QWEN.md', 'AGENTS.md']);
vi.spyOn(os, 'homedir').mockReturnValue('/home/user');
mockReadFile.mockImplementation(async (filePath: string) => {
if (filePath.endsWith('AGENTS.md')) return 'global agents memory';
throw new Error('ENOENT');
});
await globalCommand.action(mockContext, '');
const expectedPath = path.join('/home/user', QWEN_DIR, 'AGENTS.md');
expect(mockReadFile).toHaveBeenCalledWith(expectedPath, 'utf-8');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: expect.stringContaining('global agents memory'),
},
expect.any(Number),
);
});
it('should show content from both QWEN.md and AGENTS.md for --project when both exist', async () => {
const projectCommand = showCommand.subCommands?.find(
(cmd) => cmd.name === '--project',
);
if (!projectCommand?.action) throw new Error('Command has no action');
setGeminiMdFilename(['QWEN.md', 'AGENTS.md']);
vi.spyOn(process, 'cwd').mockReturnValue('/test/project');
mockReadFile.mockImplementation(async (filePath: string) => {
if (filePath.endsWith('QWEN.md')) return 'qwen memory';
if (filePath.endsWith('AGENTS.md')) return 'agents memory';
throw new Error('ENOENT');
});
await projectCommand.action(mockContext, '');
expect(mockReadFile).toHaveBeenCalledWith(
path.join('/test/project', 'QWEN.md'),
'utf-8',
);
expect(mockReadFile).toHaveBeenCalledWith(
path.join('/test/project', 'AGENTS.md'),
'utf-8',
);
const addItemCall = (mockContext.ui.addItem as Mock).mock.calls[0][0];
expect(addItemCall.text).toContain('qwen memory');
expect(addItemCall.text).toContain('agents memory');
});
it('should show content from both files for --global when both exist', async () => {
const globalCommand = showCommand.subCommands?.find(
(cmd) => cmd.name === '--global',
);
if (!globalCommand?.action) throw new Error('Command has no action');
setGeminiMdFilename(['QWEN.md', 'AGENTS.md']);
vi.spyOn(os, 'homedir').mockReturnValue('/home/user');
mockReadFile.mockImplementation(async (filePath: string) => {
if (filePath.endsWith('QWEN.md')) return 'global qwen memory';
if (filePath.endsWith('AGENTS.md')) return 'global agents memory';
throw new Error('ENOENT');
});
await globalCommand.action(mockContext, '');
expect(mockReadFile).toHaveBeenCalledWith(
path.join('/home/user', QWEN_DIR, 'QWEN.md'),
'utf-8',
);
expect(mockReadFile).toHaveBeenCalledWith(
path.join('/home/user', QWEN_DIR, 'AGENTS.md'),
'utf-8',
);
const addItemCall = (mockContext.ui.addItem as Mock).mock.calls[0][0];
expect(addItemCall.text).toContain('global qwen memory');
expect(addItemCall.text).toContain('global agents memory');
});
});
describe('/memory add', () => {

View file

@ -6,7 +6,7 @@
import {
getErrorMessage,
getCurrentGeminiMdFilename,
getAllGeminiMdFilenames,
loadServerHierarchicalMemory,
QWEN_DIR,
} from '@qwen-code/qwen-code-core';
@ -18,6 +18,28 @@ import type { SlashCommand, SlashCommandActionReturn } from './types.js';
import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
/**
* Read all existing memory files from the configured filenames in a directory.
* Returns an array of found files with their paths and contents.
*/
async function findAllExistingMemoryFiles(
dir: string,
): Promise<Array<{ filePath: string; content: string }>> {
const results: Array<{ filePath: string; content: string }> = [];
for (const filename of getAllGeminiMdFilenames()) {
const filePath = path.join(dir, filename);
try {
const content = await fs.readFile(filePath, 'utf-8');
if (content.trim().length > 0) {
results.push({ filePath, content });
}
} catch {
// File doesn't exist, try next
}
}
return results;
}
export const memoryCommand: SlashCommand = {
name: 'memory',
get description() {
@ -56,37 +78,27 @@ export const memoryCommand: SlashCommand = {
},
kind: CommandKind.BUILT_IN,
action: async (context) => {
try {
const workingDir =
context.services.config?.getWorkingDir?.() ?? process.cwd();
const projectMemoryPath = path.join(
workingDir,
getCurrentGeminiMdFilename(),
);
const memoryContent = await fs.readFile(
projectMemoryPath,
'utf-8',
);
const messageContent =
memoryContent.trim().length > 0
? t(
'Project memory content from {{path}}:\n\n---\n{{content}}\n---',
{
path: projectMemoryPath,
content: memoryContent,
},
)
: t('Project memory is currently empty.');
const workingDir =
context.services.config?.getWorkingDir?.() ?? process.cwd();
const results = await findAllExistingMemoryFiles(workingDir);
if (results.length > 0) {
const combined = results
.map((r) =>
t(
'Project memory content from {{path}}:\n\n---\n{{content}}\n---',
{ path: r.filePath, content: r.content },
),
)
.join('\n\n');
context.ui.addItem(
{
type: MessageType.INFO,
text: messageContent,
text: combined,
},
Date.now(),
);
} catch (_error) {
} else {
context.ui.addItem(
{
type: MessageType.INFO,
@ -106,32 +118,25 @@ export const memoryCommand: SlashCommand = {
},
kind: CommandKind.BUILT_IN,
action: async (context) => {
try {
const globalMemoryPath = path.join(
os.homedir(),
QWEN_DIR,
getCurrentGeminiMdFilename(),
);
const globalMemoryContent = await fs.readFile(
globalMemoryPath,
'utf-8',
);
const messageContent =
globalMemoryContent.trim().length > 0
? t('Global memory content:\n\n---\n{{content}}\n---', {
content: globalMemoryContent,
})
: t('Global memory is currently empty.');
const globalDir = path.join(os.homedir(), QWEN_DIR);
const results = await findAllExistingMemoryFiles(globalDir);
if (results.length > 0) {
const combined = results
.map((r) =>
t('Global memory content:\n\n---\n{{content}}\n---', {
content: r.content,
}),
)
.join('\n\n');
context.ui.addItem(
{
type: MessageType.INFO,
text: messageContent,
text: combined,
},
Date.now(),
);
} catch (_error) {
} else {
context.ui.addItem(
{
type: MessageType.INFO,