Skip to content
Open
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
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
56 changes: 53 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,60 @@ 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 (_context, partialArg) => {
const suggestions = [
{
value: '--history',
description: t(
'Clear dialogue history (keep system prompt + memory + context)',
),
},
{
value: '--all',
description: t('Complete reset (like a new session)'),
},
];
const filtered = suggestions.filter((s) => s.value.startsWith(partialArg));
return filtered.length > 0 ? filtered : null;
},
action: async (context, args): Promise<void | SlashCommandActionReturn> => {
const tokens = args.trim().split(/\s+/).filter(Boolean);
// --all is a superset of --history; if both are provided, treat as --all
const isAll = tokens.includes('--all');
const isHistory = !isAll && tokens.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 +93,12 @@ export const clearCommand: SlashCommand = {
skillTool.clearLoadedSkills();
}

if (isAll) {
// --history preserves IDE/editor context.
// --all also clears the IDE context store.
ideContextStore.clear();
}

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