Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions packages/cli/src/ui/commands/dreamCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,21 @@ import { dreamCommand } from './dreamCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';

describe('dreamCommand', () => {
it('submits a consolidation prompt with the project-scoped transcript directory', async () => {
it('declares acp in supportedModes', () => {
expect(dreamCommand.supportedModes).toEqual(['interactive', 'acp']);
});

it('returns error when config is not loaded', async () => {
const context = createMockCommandContext({ services: { config: null } });
const result = await dreamCommand.action?.(context, '');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: expect.stringContaining('Config'),
});
});

it('submits a consolidation prompt in interactive mode without eager metadata write', async () => {
const projectRoot = path.join('tmp', 'dream-project');
const buildConsolidationPrompt = vi.fn().mockReturnValue('dream prompt');
const writeDreamManualRun = vi.fn();
Expand Down Expand Up @@ -43,8 +57,29 @@ describe('dreamCommand', () => {
expect.any(String),
expectedTranscriptDir,
);
expect(expectedTranscriptDir).not.toContain(
`${path.sep}.qwen${path.sep}tmp${path.sep}`,
);
// In interactive mode, writeDreamManualRun is deferred to onComplete
expect(writeDreamManualRun).not.toHaveBeenCalled();
});

it('calls writeDreamManualRun eagerly in ACP mode', async () => {
const projectRoot = path.join('tmp', 'dream-project');
const buildConsolidationPrompt = vi.fn().mockReturnValue('dream prompt');
const writeDreamManualRun = vi.fn();
const context = createMockCommandContext({
executionMode: 'acp',
services: {
config: {
getProjectRoot: vi.fn().mockReturnValue(projectRoot),
getMemoryManager: vi.fn().mockReturnValue({
buildConsolidationPrompt,
writeDreamManualRun,
}),
getSessionId: vi.fn().mockReturnValue('session-1'),
},
},
});

await dreamCommand.action?.(context, '');
expect(writeDreamManualRun).toHaveBeenCalledWith(projectRoot, 'session-1');
});
});
53 changes: 37 additions & 16 deletions packages/cli/src/ui/commands/dreamCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const dreamCommand: SlashCommand = {
return t('Consolidate managed auto-memory topic files.');
},
kind: CommandKind.BUILT_IN,
supportedModes: ['interactive', 'acp'] as const,
Comment thread
doudouOUC marked this conversation as resolved.
action: async (context) => {
const config = context.services.config;
if (!config) {
Expand All @@ -26,25 +27,45 @@ export const dreamCommand: SlashCommand = {
};
}

const projectRoot = config.getProjectRoot();
const memoryRoot = getAutoMemoryRoot(projectRoot);
const transcriptDir = path.join(
new Storage(projectRoot).getProjectDir(),
'chats',
);
try {
const projectRoot = config.getProjectRoot();
const memoryRoot = getAutoMemoryRoot(projectRoot);
const transcriptDir = path.join(
new Storage(projectRoot).getProjectDir(),
'chats',
);

const prompt = config
.getMemoryManager()
.buildConsolidationPrompt(memoryRoot, transcriptDir);
const prompt = config
.getMemoryManager()
.buildConsolidationPrompt(memoryRoot, transcriptDir);

return {
type: 'submit_prompt',
content: prompt,
onComplete: async () => {
await config
const recordDream = async () =>
config
.getMemoryManager()
.writeDreamManualRun(projectRoot, config.getSessionId());
},
};

// In ACP mode, onComplete is never invoked — record eagerly.
if (context.executionMode === 'acp') {
try {
await recordDream();
} catch {
// Best-effort: dream dedup recording must not block prompt submission.
}
}

return {
type: 'submit_prompt',
content: prompt,
onComplete: recordDream,
};
} catch (error) {
return {
type: 'message',
messageType: 'error',
content: t('Failed to process /dream: {{message}}', {
message: error instanceof Error ? error.message : String(error),
}),
};
}
},
};
100 changes: 100 additions & 0 deletions packages/cli/src/ui/commands/forgetCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it, vi } from 'vitest';
import { forgetCommand } from './forgetCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';

describe('forgetCommand', () => {
it('returns error when no argument is given', async () => {
const context = createMockCommandContext();
const result = await forgetCommand.action?.(context, '');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: expect.stringContaining('/forget'),
});
});

it('returns error when config is not loaded', async () => {
const context = createMockCommandContext({ services: { config: null } });
const result = await forgetCommand.action?.(context, 'something');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: expect.stringContaining('Config'),
});
});

it('returns info message on successful forget', async () => {
const context = createMockCommandContext({
services: {
config: {
getProjectRoot: vi.fn().mockReturnValue('/tmp/test-project'),
getMemoryManager: vi.fn().mockReturnValue({
selectForgetCandidates: vi
.fn()
.mockResolvedValue({ matches: [{ id: '1' }] }),
forgetMatches: vi
.fn()
.mockResolvedValue({ systemMessage: 'Forgot 1 entry.' }),
}),
},
},
});
const result = await forgetCommand.action?.(context, 'old preference');
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Forgot 1 entry.',
});
});

it('returns fallback message when no entries match', async () => {
const context = createMockCommandContext({
services: {
config: {
getProjectRoot: vi.fn().mockReturnValue('/tmp/test-project'),
getMemoryManager: vi.fn().mockReturnValue({
selectForgetCandidates: vi.fn().mockResolvedValue({ matches: [] }),
forgetMatches: vi.fn().mockResolvedValue({ systemMessage: null }),
}),
},
},
});
const result = await forgetCommand.action?.(context, 'nonexistent');
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: expect.stringContaining('nonexistent'),
});
});

it('returns error message when memory manager throws', async () => {
const context = createMockCommandContext({
services: {
config: {
getProjectRoot: vi.fn().mockReturnValue('/tmp/test-project'),
getMemoryManager: vi.fn().mockReturnValue({
selectForgetCandidates: vi
.fn()
.mockRejectedValue(new Error('EACCES: permission denied')),
}),
},
},
});
const result = await forgetCommand.action?.(context, 'something');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: expect.stringContaining('EACCES: permission denied'),
});
});

it('declares acp in supportedModes', () => {
expect(forgetCommand.supportedModes).toEqual(['interactive', 'acp']);
});
});
37 changes: 24 additions & 13 deletions packages/cli/src/ui/commands/forgetCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const forgetCommand: SlashCommand = {
return t('Remove matching entries from managed auto-memory.');
},
kind: CommandKind.BUILT_IN,
supportedModes: ['interactive', 'acp'] as const,
action: async (context, args) => {
const query = args.trim();

Expand All @@ -34,19 +35,29 @@ export const forgetCommand: SlashCommand = {
};
}

const selection = await config
.getMemoryManager()
.selectForgetCandidates(config.getProjectRoot(), query, { config });
try {
const selection = await config
.getMemoryManager()
.selectForgetCandidates(config.getProjectRoot(), query, { config });

const result = await config
.getMemoryManager()
.forgetMatches(config.getProjectRoot(), selection.matches);
return {
type: 'message',
messageType: 'info',
content:
result.systemMessage ??
t('No managed auto-memory entries matched: {{query}}', { query }),
};
const result = await config
.getMemoryManager()
.forgetMatches(config.getProjectRoot(), selection.matches);
return {
type: 'message',
messageType: 'info',
content:
result.systemMessage ??
t('No managed auto-memory entries matched: {{query}}', { query }),
};
} catch (error) {
return {
type: 'message',
messageType: 'error',
content: t('Failed to process /forget: {{message}}', {
message: error instanceof Error ? error.message : String(error),
}),
};
}
},
};
67 changes: 67 additions & 0 deletions packages/cli/src/ui/commands/rememberCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it, vi } from 'vitest';
import { rememberCommand } from './rememberCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';

describe('rememberCommand', () => {
it('returns error when no argument is given', () => {
const context = createMockCommandContext();
const result = rememberCommand.action?.(context, '');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: expect.stringContaining('/remember'),
});
});

it('returns error when config is not loaded', () => {
const context = createMockCommandContext({ services: { config: null } });
const result = rememberCommand.action?.(context, 'something');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: expect.stringContaining('Config'),
});
});

it('returns submit_prompt for managed auto-memory', () => {
const context = createMockCommandContext({
services: {
config: {
getManagedAutoMemoryEnabled: vi.fn().mockReturnValue(true),
getProjectRoot: vi.fn().mockReturnValue('/tmp/test-project'),
},
},
});
const result = rememberCommand.action?.(context, 'user prefers dark mode');
expect(result).toMatchObject({
type: 'submit_prompt',
content: expect.stringContaining('user prefers dark mode'),
});
});

it('returns submit_prompt for non-managed memory (QWEN.md fallback)', () => {
const context = createMockCommandContext({
services: {
config: {
getManagedAutoMemoryEnabled: vi.fn().mockReturnValue(false),
getProjectRoot: vi.fn().mockReturnValue('/tmp/test-project'),
},
},
});
const result = rememberCommand.action?.(context, 'some fact');
expect(result).toMatchObject({
type: 'submit_prompt',
content: expect.stringContaining('some fact'),
});
});

it('declares acp in supportedModes', () => {
expect(rememberCommand.supportedModes).toEqual(['interactive', 'acp']);
});
});
21 changes: 11 additions & 10 deletions packages/cli/src/ui/commands/rememberCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const rememberCommand: SlashCommand = {
return t('Save a durable memory to the memory system.');
},
kind: CommandKind.BUILT_IN,
supportedModes: ['interactive', 'acp'] as const,
action: (context: CommandContext, args): SlashCommandActionReturn | void => {
const fact = args.trim();
Comment thread
doudouOUC marked this conversation as resolved.
if (!fact) {
Expand All @@ -30,17 +31,17 @@ export const rememberCommand: SlashCommand = {
}

const config = context.services.config;
const useManagedMemory = config?.getManagedAutoMemoryEnabled() ?? false;
if (!config) {
return {
type: 'message',
messageType: 'error',
content: t('Config not loaded.'),
};
}

if (useManagedMemory) {
// In managed auto-memory mode the save_memory tool is not registered.
// Submit a prompt so the main agent writes the per-entry file directly,
// choosing the appropriate type (user / feedback / project / reference)
// based on the content, following the instructions in buildManagedAutoMemoryPrompt.
const memoryDir = config
? getAutoMemoryRoot(config.getProjectRoot())
: undefined;
const dirHint = memoryDir ? ` Save it to \`${memoryDir}\`.` : '';
if (config.getManagedAutoMemoryEnabled()) {
const memoryDir = getAutoMemoryRoot(config.getProjectRoot());
const dirHint = ` Save it to \`${memoryDir}\`.`;
return {
type: 'submit_prompt',
content: `Please save the following to your memory system.${dirHint} Choose the most appropriate memory type (user, feedback, project, or reference) based on the content:\n\n${fact}`,
Expand Down
Loading