mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
tmp
This commit is contained in:
@@ -117,7 +117,7 @@ describe('memoryCommand', () => {
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add <text to remember>',
|
||||
content: 'Usage: /memory add [--global|--project] <text to remember>',
|
||||
});
|
||||
|
||||
expect(mockContext.ui.addItem).not.toHaveBeenCalled();
|
||||
@@ -143,6 +143,61 @@ describe('memoryCommand', () => {
|
||||
toolArgs: { fact },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle --global flag and add scope to tool args', () => {
|
||||
if (!addCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const fact = 'remember this globally';
|
||||
const result = addCommand.action(mockContext, `--global ${fact}`);
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to memory (global): "${fact}"`,
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact, scope: 'global' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle --project flag and add scope to tool args', () => {
|
||||
if (!addCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const fact = 'remember this for project';
|
||||
const result = addCommand.action(mockContext, `--project ${fact}`);
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to memory (project): "${fact}"`,
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact, scope: 'project' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error if flag is provided but no fact follows', () => {
|
||||
if (!addCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const result = addCommand.action(mockContext, '--global ');
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add [--global|--project] <text to remember>',
|
||||
});
|
||||
|
||||
expect(mockContext.ui.addItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('/memory refresh', () => {
|
||||
|
||||
@@ -44,29 +44,66 @@ export const memoryCommand: SlashCommand = {
|
||||
},
|
||||
{
|
||||
name: 'add',
|
||||
description: 'Add content to the memory.',
|
||||
description:
|
||||
'Add content to the memory. Use --global for global memory or --project for project memory.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add <text to remember>',
|
||||
content:
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
};
|
||||
}
|
||||
|
||||
const trimmedArgs = args.trim();
|
||||
let scope: 'global' | 'project' | undefined;
|
||||
let fact: string;
|
||||
|
||||
// Check for scope flags
|
||||
if (trimmedArgs.startsWith('--global ')) {
|
||||
scope = 'global';
|
||||
fact = trimmedArgs.substring('--global '.length).trim();
|
||||
} else if (trimmedArgs.startsWith('--project ')) {
|
||||
scope = 'project';
|
||||
fact = trimmedArgs.substring('--project '.length).trim();
|
||||
} else if (trimmedArgs === '--global' || trimmedArgs === '--project') {
|
||||
// Flag provided but no text after it
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
};
|
||||
} else {
|
||||
// No scope specified, will be handled by the tool
|
||||
fact = trimmedArgs;
|
||||
}
|
||||
|
||||
if (!fact || fact.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
'Usage: /memory add [--global|--project] <text to remember>',
|
||||
};
|
||||
}
|
||||
|
||||
const scopeText = scope ? ` (${scope})` : '';
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Attempting to save to memory: "${args.trim()}"`,
|
||||
text: `Attempting to save to memory${scopeText}: "${fact}"`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
const toolArgs = scope ? { fact, scope } : { fact };
|
||||
return {
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact: args.trim() },
|
||||
toolArgs,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -425,13 +425,16 @@ describe('MemoryTool', () => {
|
||||
expect(result).not.toBe(false);
|
||||
|
||||
if (result && result.type === 'edit') {
|
||||
expect(result.title).toBe('Choose Memory Storage Location');
|
||||
expect(result.fileName).toBe('Memory Storage Options');
|
||||
expect(result.fileDiff).toContain('Choose where to save this memory');
|
||||
expect(result.title).toContain('Choose Memory Location');
|
||||
expect(result.title).toContain('GLOBAL');
|
||||
expect(result.title).toContain('PROJECT');
|
||||
expect(result.fileName).toBe('QWEN.md');
|
||||
expect(result.fileDiff).toContain('Test fact');
|
||||
expect(result.fileDiff).toContain('Global:');
|
||||
expect(result.fileDiff).toContain('Project:');
|
||||
expect(result.originalContent).toBe('');
|
||||
expect(result.fileDiff).toContain('--- QWEN.md');
|
||||
expect(result.fileDiff).toContain('+++ QWEN.md');
|
||||
expect(result.fileDiff).toContain('+- Test fact');
|
||||
expect(result.originalContent).toContain('scope: global');
|
||||
expect(result.originalContent).toContain('INSTRUCTIONS:');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -149,7 +149,12 @@ export class MemoryTool
|
||||
}
|
||||
|
||||
getDescription(params: SaveMemoryParams): string {
|
||||
const scope = params.scope || 'global';
|
||||
if (!params.scope) {
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
return `CHOOSE: ${globalPath} (global) OR ${projectPath} (project)`;
|
||||
}
|
||||
const scope = params.scope;
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
return `in ${tildeifyPath(memoryFilePath)} (${scope})`;
|
||||
}
|
||||
@@ -217,27 +222,43 @@ export class MemoryTool
|
||||
params: SaveMemoryParams,
|
||||
_abortSignal: AbortSignal,
|
||||
): Promise<ToolEditConfirmationDetails | false> {
|
||||
// If scope is not specified, prompt the user to choose
|
||||
// When scope is not specified, show a choice dialog defaulting to global
|
||||
if (!params.scope) {
|
||||
// Show preview of what would be added to global by default
|
||||
const defaultScope = 'global';
|
||||
const currentContent = await this.readMemoryFileContent(defaultScope);
|
||||
const newContent = this.computeNewContent(currentContent, params.fact);
|
||||
|
||||
const fileName = path.basename(getMemoryFilePath(defaultScope));
|
||||
const fileDiff = Diff.createPatch(
|
||||
fileName,
|
||||
currentContent,
|
||||
newContent,
|
||||
'Current',
|
||||
'Proposed (Global)',
|
||||
DEFAULT_DIFF_OPTIONS,
|
||||
);
|
||||
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
|
||||
const confirmationDetails: ToolEditConfirmationDetails = {
|
||||
type: 'edit',
|
||||
title: `Choose Memory Storage Location`,
|
||||
fileName: 'Memory Storage Options',
|
||||
filePath: '',
|
||||
fileDiff: `Choose where to save this memory:\n\n"${params.fact}"\n\nOptions:\n- Global: ${globalPath} (shared across all projects)\n- Project: ${projectPath} (current project only)\n\nPlease specify the scope parameter: "global" or "project"`,
|
||||
originalContent: '',
|
||||
newContent: `Memory to save: ${params.fact}\n\nScope options:\n- global: ${globalPath}\n- project: ${projectPath}`,
|
||||
title: `Choose Memory Location: GLOBAL (${globalPath}) or PROJECT (${projectPath})`,
|
||||
fileName,
|
||||
filePath: getMemoryFilePath(defaultScope),
|
||||
fileDiff,
|
||||
originalContent: `scope: global\n\n# INSTRUCTIONS:\n# - Click "Yes" to save to GLOBAL memory: ${globalPath}\n# - Click "Modify with external editor" and change "global" to "project" to save to PROJECT memory: ${projectPath}\n\n${currentContent}`,
|
||||
newContent: `scope: global\n\n# INSTRUCTIONS:\n# - Click "Yes" to save to GLOBAL memory: ${globalPath}\n# - Click "Modify with external editor" and change "global" to "project" to save to PROJECT memory: ${projectPath}\n\n${newContent}`,
|
||||
onConfirm: async (_outcome: ToolConfirmationOutcome) => {
|
||||
// This will be handled by the execution flow
|
||||
// Will be handled in createUpdatedParams
|
||||
},
|
||||
};
|
||||
return confirmationDetails;
|
||||
}
|
||||
|
||||
const scope = params.scope;
|
||||
// Only check allowlist when scope is specified
|
||||
const scope = params.scope!; // We know scope is specified at this point
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
const allowlistKey = `${memoryFilePath}_${scope}`;
|
||||
|
||||
@@ -362,17 +383,25 @@ export class MemoryTool
|
||||
};
|
||||
}
|
||||
|
||||
// If scope is not specified, prompt the user to choose
|
||||
if (!params.scope) {
|
||||
const errorMessage =
|
||||
'Please specify where to save this memory. Use scope parameter: "global" for user-level (~/.qwen/QWEN.md) or "project" for current project (./QWEN.md).';
|
||||
// If scope is not specified and user didn't modify content, return error prompting for choice
|
||||
if (!params.scope && !params.modified_by_user) {
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
const errorMessage = `Please specify where to save this memory:
|
||||
|
||||
Global: ${globalPath} (shared across all projects)
|
||||
Project: ${projectPath} (current project only)`;
|
||||
|
||||
return {
|
||||
llmContent: JSON.stringify({ success: false, error: errorMessage }),
|
||||
returnDisplay: `${errorMessage}\n\nGlobal: ${tildeifyPath(getMemoryFilePath('global'))}\nProject: ${tildeifyPath(getMemoryFilePath('project'))}`,
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
error: 'Please specify where to save this memory',
|
||||
}),
|
||||
returnDisplay: errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
const scope = params.scope;
|
||||
const scope = params.scope || 'global';
|
||||
const memoryFilePath = getMemoryFilePath(scope);
|
||||
|
||||
try {
|
||||
@@ -424,24 +453,88 @@ export class MemoryTool
|
||||
|
||||
getModifyContext(_abortSignal: AbortSignal): ModifyContext<SaveMemoryParams> {
|
||||
return {
|
||||
getFilePath: (params: SaveMemoryParams) =>
|
||||
getMemoryFilePath(params.scope || 'global'),
|
||||
getCurrentContent: async (params: SaveMemoryParams): Promise<string> =>
|
||||
this.readMemoryFileContent(params.scope || 'global'),
|
||||
getProposedContent: async (params: SaveMemoryParams): Promise<string> => {
|
||||
getFilePath: (params: SaveMemoryParams) => {
|
||||
// Determine scope from modified content or default
|
||||
let scope = params.scope || 'global';
|
||||
if (params.modified_content) {
|
||||
const scopeMatch = params.modified_content.match(
|
||||
/^scope:\s*(global|project)\s*\n/i,
|
||||
);
|
||||
if (scopeMatch) {
|
||||
scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
|
||||
}
|
||||
}
|
||||
return getMemoryFilePath(scope);
|
||||
},
|
||||
getCurrentContent: async (params: SaveMemoryParams): Promise<string> => {
|
||||
// Check if content starts with scope directive
|
||||
if (params.modified_content) {
|
||||
const scopeMatch = params.modified_content.match(
|
||||
/^scope:\s*(global|project)\s*\n/i,
|
||||
);
|
||||
if (scopeMatch) {
|
||||
const scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
|
||||
const content = await this.readMemoryFileContent(scope);
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${content}`;
|
||||
}
|
||||
}
|
||||
const scope = params.scope || 'global';
|
||||
const content = await this.readMemoryFileContent(scope);
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${content}`;
|
||||
},
|
||||
getProposedContent: async (params: SaveMemoryParams): Promise<string> => {
|
||||
let scope = params.scope || 'global';
|
||||
|
||||
// Check if modified content has scope directive
|
||||
if (params.modified_content) {
|
||||
const scopeMatch = params.modified_content.match(
|
||||
/^scope:\s*(global|project)\s*\n/i,
|
||||
);
|
||||
if (scopeMatch) {
|
||||
scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
|
||||
}
|
||||
}
|
||||
|
||||
const currentContent = await this.readMemoryFileContent(scope);
|
||||
return this.computeNewContent(currentContent, params.fact);
|
||||
const newContent = this.computeNewContent(currentContent, params.fact);
|
||||
const globalPath = tildeifyPath(getMemoryFilePath('global'));
|
||||
const projectPath = tildeifyPath(getMemoryFilePath('project'));
|
||||
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${newContent}`;
|
||||
},
|
||||
createUpdatedParams: (
|
||||
_oldContent: string,
|
||||
modifiedProposedContent: string,
|
||||
originalParams: SaveMemoryParams,
|
||||
): SaveMemoryParams => ({
|
||||
...originalParams,
|
||||
modified_by_user: true,
|
||||
modified_content: modifiedProposedContent,
|
||||
}),
|
||||
): SaveMemoryParams => {
|
||||
// Parse user's scope choice from modified content
|
||||
const scopeMatch = modifiedProposedContent.match(
|
||||
/^scope:\s*(global|project)/i,
|
||||
);
|
||||
const scope = scopeMatch
|
||||
? (scopeMatch[1].toLowerCase() as 'global' | 'project')
|
||||
: 'global';
|
||||
|
||||
// Strip out the scope directive and instruction lines, keep only the actual memory content
|
||||
const contentWithoutScope = modifiedProposedContent.replace(
|
||||
/^scope:\s*(global|project)\s*\n/,
|
||||
'',
|
||||
);
|
||||
const actualContent = contentWithoutScope
|
||||
.replace(/^#[^\n]*\n/gm, '')
|
||||
.replace(/^\s*\n/gm, '')
|
||||
.trim();
|
||||
|
||||
return {
|
||||
...originalParams,
|
||||
scope,
|
||||
modified_by_user: true,
|
||||
modified_content: actualContent,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user