diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index fdc572fcbe..9bf6a2caaf 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -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', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 357a4ccd32..8664a415c7 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -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 清除对话并重置会话', }; diff --git a/packages/cli/src/ui/commands/clearCommand.test.ts b/packages/cli/src/ui/commands/clearCommand.test.ts index 61e66b53e2..f84b0d29b9 100644 --- a/packages/cli/src/ui/commands/clearCommand.test.ts +++ b/packages/cli/src/ui/commands/clearCommand.test.ts @@ -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 @@ -21,6 +22,9 @@ vi.mock('@qwen-code/qwen-code-core', async () => { uiTelemetryService: { reset: vi.fn(), }, + ideContextStore: { + clear: vi.fn(), + }, }; }); @@ -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( @@ -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); @@ -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'); @@ -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); @@ -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.', diff --git a/packages/cli/src/ui/commands/clearCommand.ts b/packages/cli/src/ui/commands/clearCommand.ts index 571ee5c6ce..127021050c 100644 --- a/packages/cli/src/ui/commands/clearCommand.ts +++ b/packages/cli/src/ui/commands/clearCommand.ts @@ -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 { @@ -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 => { + 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) { @@ -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); }