Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
9 changes: 9 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -2008,4 +2008,13 @@ export default {
'Raw mode not available. Please run in an interactive terminal.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n',
'Are you sure you want to completely reset the session?':
'Are you sure you want to completely reset the session?',
'Are you sure you want to clear the conversation history?':
'Are you sure you want to clear the conversation history?',
'Clear dialogue history (keep system prompt + memory + context)':
'Clear dialogue history (keep system prompt + memory + context)',
'Complete reset (like a new session)': 'Complete reset (like a new session)',
'Clear screen, or use --history/--all to clear conversation and reset session':
'Clear screen, or use --history/--all to clear conversation and reset session',
};
9 changes: 9 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -1813,4 +1813,13 @@ export default {
'原始模式不可用。请在交互式终端中运行。',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(使用 ↑ ↓ 箭头导航,Enter 选择,Ctrl+C 退出)\n',
'Are you sure you want to completely reset the session?':
'您确定要完全重置会话吗?',
'Are you sure you want to clear the conversation history?':
'您确定要清除对话历史吗?',
'Clear dialogue history (keep system prompt + memory + context)':
'清除对话历史(保留系统提示+记忆+上下文)',
'Complete reset (like a new session)': '完全重置(如同新会话)',
'Clear screen, or use --history/--all to clear conversation and reset session':
'清屏,或使用 --history/--all 清除对话并重置会话',
};
105 changes: 90 additions & 15 deletions packages/cli/src/ui/commands/clearCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
import {
SessionEndReason,
SessionStartSource,
ideContextStore,
} from '@qwen-code/qwen-code-core';

// Mock the telemetry service
Expand All @@ -21,6 +22,9 @@ vi.mock('@qwen-code/qwen-code-core', async () => {
uiTelemetryService: {
reset: vi.fn(),
},
ideContextStore: {
clear: vi.fn(),
},
};
});

Expand Down Expand Up @@ -68,39 +72,106 @@ describe('clearCommand', () => {
});
});

it('should set debug message, start a new session, reset chat, and clear UI when config is available', async () => {
it('should only clear UI when called without flags', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}

await clearCommand.action(mockContext, '');

expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
expect(mockResetChat).not.toHaveBeenCalled();
expect(mockStartNewSession).not.toHaveBeenCalled();
});

it('should return confirm_action when called with --history and not confirmed', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}

const result = await clearCommand.action(mockContext, '--history');

expect(result).toEqual({
type: 'confirm_action',
prompt: 'Are you sure you want to clear the conversation history?',
originalInvocation: { raw: '/clear --history' },
});
expect(mockContext.ui.clear).not.toHaveBeenCalled();
expect(mockResetChat).not.toHaveBeenCalled();
});

it('should return confirm_action when called with --all and not confirmed', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}

const result = await clearCommand.action(mockContext, '--all');

expect(result).toEqual({
type: 'confirm_action',
prompt: 'Are you sure you want to completely reset the session?',
originalInvocation: { raw: '/clear --all' },
});
expect(mockContext.ui.clear).not.toHaveBeenCalled();
expect(mockResetChat).not.toHaveBeenCalled();
});

it('should set debug message, start a new session, reset chat, and clear UI when confirmed with --history', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}

mockContext.overwriteConfirmed = true;
await clearCommand.action(mockContext, '--history');

expect(mockContext.ui.setDebugMessage).toHaveBeenCalledWith(
'Starting a new session, resetting chat, and clearing terminal.',
);
expect(mockContext.ui.setDebugMessage).toHaveBeenCalledTimes(1);

expect(mockStartNewSession).toHaveBeenCalledTimes(1);
expect(mockContext.session.startNewSession).toHaveBeenCalledWith(
'new-session-id',
);
expect(mockResetChat).toHaveBeenCalledTimes(1);
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
expect(ideContextStore.clear).not.toHaveBeenCalled();
});

// Check that all expected operations were called
expect(mockContext.ui.setDebugMessage).toHaveBeenCalled();
expect(mockStartNewSession).toHaveBeenCalled();
expect(mockContext.session.startNewSession).toHaveBeenCalled();
expect(mockResetChat).toHaveBeenCalled();
expect(mockContext.ui.clear).toHaveBeenCalled();
it('should completely reset session when confirmed with --all', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}

mockContext.overwriteConfirmed = true;
await clearCommand.action(mockContext, '--all');

expect(mockStartNewSession).toHaveBeenCalledTimes(1);
expect(mockResetChat).toHaveBeenCalledTimes(1);
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
expect(ideContextStore.clear).toHaveBeenCalledTimes(1);
});

it('should fire SessionEnd event before clearing and SessionStart event after clearing', async () => {
it('should treat --all as superset when both --history and --all are provided', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}

await clearCommand.action(mockContext, '');
mockContext.overwriteConfirmed = true;
await clearCommand.action(mockContext, '--history --all');

expect(mockStartNewSession).toHaveBeenCalledTimes(1);
expect(mockResetChat).toHaveBeenCalledTimes(1);
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
// --all should trigger ideContextStore.clear
expect(ideContextStore.clear).toHaveBeenCalledTimes(1);
});

it('should fire SessionEnd event before clearing and SessionStart event after clearing when confirmed', async () => {
if (!clearCommand.action) {
throw new Error('clearCommand must have an action.');
}

mockContext.overwriteConfirmed = true;
await clearCommand.action(mockContext, '--history');

expect(mockGetHookSystem).toHaveBeenCalled();
expect(mockFireSessionEndEvent).toHaveBeenCalledWith(
Expand Down Expand Up @@ -132,7 +203,8 @@ describe('clearCommand', () => {
new Error('SessionStart hook failed'),
);

await clearCommand.action(mockContext, '');
mockContext.overwriteConfirmed = true;
await clearCommand.action(mockContext, '--history');

// Should still complete the clear operation despite hook errors
expect(mockStartNewSession).toHaveBeenCalledTimes(1);
Expand All @@ -155,7 +227,8 @@ describe('clearCommand', () => {
callOrder.push('resetChat');
});

await clearCommand.action(mockContext, '');
mockContext.overwriteConfirmed = true;
await clearCommand.action(mockContext, '--history');

// ui.clear should be called before resetChat for immediate UI feedback
const clearIndex = callOrder.indexOf('ui.clear');
Expand Down Expand Up @@ -192,7 +265,8 @@ describe('clearCommand', () => {
}),
);

await clearCommand.action(mockContext, '');
mockContext.overwriteConfirmed = true;
await clearCommand.action(mockContext, '--history');

// The action should complete immediately without waiting for hooks
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
Expand All @@ -217,9 +291,10 @@ describe('clearCommand', () => {
session: {
startNewSession: vi.fn(),
},
overwriteConfirmed: true,
});

await clearCommand.action(nullConfigContext, '');
await clearCommand.action(nullConfigContext, '--history');

expect(nullConfigContext.ui.setDebugMessage).toHaveBeenCalledWith(
'Starting a new session and clearing.',
Expand Down
49 changes: 46 additions & 3 deletions packages/cli/src/ui/commands/clearCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type { SlashCommand } from './types.js';
import type { SlashCommand, SlashCommandActionReturn } from './types.js';
import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
import {
Expand All @@ -14,16 +14,53 @@ import {
ToolNames,
SkillTool,
type PermissionMode,
ideContextStore,
} from '@qwen-code/qwen-code-core';

export const clearCommand: SlashCommand = {
name: 'clear',
altNames: ['reset', 'new'],
get description() {
return t('Clear conversation history and free up context');
return t(
'Clear screen, or use --history/--all to clear conversation and reset session',
);
},
kind: CommandKind.BUILT_IN,
action: async (context, _args) => {
completion: async () => [
{
value: '--history',
description: t(
'Clear dialogue history (keep system prompt + memory + context)',
),
},
{ value: '--all', description: t('Complete reset (like a new session)') },
],
action: async (context, args): Promise<void | SlashCommandActionReturn> => {
const hasAll = args.includes('--all');
// --all is a superset of --history; if both are provided, treat as --all
const isAll = hasAll;
const isHistory = !hasAll && args.includes('--history');

if (!isHistory && !isAll) {
// Clear UI only for immediate responsiveness
context.ui.clear();
return;
}

if (!context.overwriteConfirmed) {
return {
type: 'confirm_action',
prompt: isAll
? t('Are you sure you want to completely reset the session?')
: t('Are you sure you want to clear the conversation history?'),
originalInvocation: {
raw:
context.invocation?.raw ||
`/clear ${isAll ? '--all' : '--history'}`,
},
};
}

const { config } = context.services;

if (config) {
Expand All @@ -49,6 +86,12 @@ export const clearCommand: SlashCommand = {
skillTool.clearLoadedSkills();
}

if (isAll) {
// --history preserves IDE context (system prompt + memory + context);
// --all performs a full reset including IDE context store
ideContextStore.clear();
}

if (newSessionId && context.session.startNewSession) {
context.session.startNewSession(newSessionId);
}
Expand Down
Loading