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
17 changes: 9 additions & 8 deletions docs/users/features/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ Slash commands are used to manage Qwen Code sessions, interface, and basic behav

These commands help you save, restore, and summarize work progress.

| Command | Description | Usage Examples |
| ----------- | --------------------------------------------------------- | ------------------------------------ |
| `/init` | Analyze current directory and create initial context file | `/init` |
| `/summary` | Generate project summary based on conversation history | `/summary` |
| `/compress` | Replace chat history with summary to save Tokens | `/compress` |
| `/resume` | Resume a previous conversation session | `/resume` |
| `/recap` | Generate a one-line session recap now | `/recap` |
| `/restore` | Restore files to state before tool execution | `/restore` (list) or `/restore <ID>` |
| Command | Description | Usage Examples |
| ---------------- | ------------------------------------------------------------------------ | ------------------------------------ |
| `/init` | Analyze current directory and create initial context file | `/init` |
| `/summary` | Generate project summary based on conversation history | `/summary` |
| `/compress` | Replace chat history with summary to save Tokens | `/compress` |
| `/compress-fast` | Fast compression without AI — strips old tool outputs and thinking parts | `/compress-fast` |
| `/resume` | Resume a previous conversation session | `/resume` |
| `/recap` | Generate a one-line session recap now | `/recap` |
| `/restore` | Restore files to state before tool execution | `/restore` (list) or `/restore <ID>` |

### 1.2 Interface and Workspace Control

Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ export default {
'Clear conversation history and free up context',
'Compresses the context by replacing it with a summary.':
'Compresses the context by replacing it with a summary.',
'Fast context compression without AI. Strips old tool outputs and thinking parts.':
'Fast context compression without AI. Strips old tool outputs and thinking parts.',
'open full Qwen Code documentation in your browser':
'open full Qwen Code documentation in your browser',
'Configuration not available.': 'Configuration not available.',
Expand Down Expand Up @@ -2025,6 +2027,7 @@ export default {
'Loading suggestions...': 'Loading suggestions...',
'Show per-item context usage breakdown.':
'Show per-item context usage breakdown.',
'No compression needed.': 'No compression needed.',

// ============================================================================
// Stats
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ export default {
'Clear conversation history and free up context': '清除對話歷史並釋放上下文',
'Compresses the context by replacing it with a summary.':
'通過摘要替換來壓縮上下文',
'Fast context compression without AI. Strips old tool outputs and thinking parts.':
'無需 AI 的快速上下文壓縮。清理舊工具輸出並剝離思考過程。',
'open full Qwen Code documentation in your browser':
'在瀏覽器中打開完整的 Qwen Code 文檔',
'Configuration not available.': '配置不可用',
Expand Down Expand Up @@ -1726,4 +1728,5 @@ export default {
// === Same-as-English optimization ===
' (not in model registry)': '(不在模型註冊表中)',
'start server': '啟動伺服器',
'No compression needed.': '無需壓縮。',
};
3 changes: 3 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ export default {
'Clear conversation history and free up context': '清除对话历史并释放上下文',
'Compresses the context by replacing it with a summary.':
'通过摘要替换来压缩上下文',
'Fast context compression without AI. Strips old tool outputs and thinking parts.':
'无需 AI 的快速上下文压缩。清理旧工具输出并剥离思考过程。',
'open full Qwen Code documentation in your browser':
'在浏览器中打开完整的 Qwen Code 文档',
'Configuration not available.': '配置不可用',
Expand Down Expand Up @@ -1926,4 +1928,5 @@ export default {
'中国 (China)': '中国',
'中国 (China) - 阿里云百炼': '中国 - 阿里云百炼',
'阿里云百炼 (aliyun.com)': '阿里云百炼(aliyun.com)',
'No compression needed.': '无需压缩。',
};
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { bugCommand } from '../ui/commands/bugCommand.js';
import { clearCommand } from '../ui/commands/clearCommand.js';
import { deleteCommand } from '../ui/commands/deleteCommand.js';
import { compressCommand } from '../ui/commands/compressCommand.js';
import { compressFastCommand } from '../ui/commands/compressFastCommand.js';
import { contextCommand } from '../ui/commands/contextCommand.js';
import { copyCommand } from '../ui/commands/copyCommand.js';
import { docsCommand } from '../ui/commands/docsCommand.js';
Expand Down Expand Up @@ -107,6 +108,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
bugCommand,
clearCommand,
compressCommand,
compressFastCommand,
contextCommand,
copyCommand,
diffCommand,
Expand Down
178 changes: 178 additions & 0 deletions packages/cli/src/ui/commands/compressFastCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import {
CompressionStatus,
type ChatCompressionInfo,
type GeminiClient,
} from '@qwen-code/qwen-code-core';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { compressFastCommand } from './compressFastCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';

describe('compressFastCommand', () => {
let context: ReturnType<typeof createMockCommandContext>;
let mockTryCompressChatFast: ReturnType<typeof vi.fn>;

beforeEach(() => {
mockTryCompressChatFast = vi.fn();
context = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
},
},
});
});

it('should do nothing if a compression is already pending', async () => {
context.ui.pendingItem = {
type: MessageType.COMPRESSION,
compression: {
isPending: true,
originalTokenCount: null,
newTokenCount: null,
compressionStatus: null,
},
};
await compressFastCommand.action!(context, '');
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Already compressing, wait for previous request to complete',
}),
expect.any(Number),
);
expect(mockTryCompressChatFast).not.toHaveBeenCalled();
});

it('should call tryCompressChatFast without arguments', async () => {
mockTryCompressChatFast.mockResolvedValue({
originalTokenCount: 200,
newTokenCount: 100,
compressionStatus: CompressionStatus.COMPRESSED,
} satisfies ChatCompressionInfo);

await compressFastCommand.action!(context, '');

expect(mockTryCompressChatFast).toHaveBeenCalledWith();
});

it('should display compression result on success (interactive)', async () => {
mockTryCompressChatFast.mockResolvedValue({
originalTokenCount: 200,
newTokenCount: 100,
compressionStatus: CompressionStatus.COMPRESSED,
} satisfies ChatCompressionInfo);

await compressFastCommand.action!(context, '');

expect(context.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.COMPRESSION,
compression: {
isPending: false,
originalTokenCount: 200,
newTokenCount: 100,
compressionStatus: CompressionStatus.COMPRESSED,
},
},
expect.any(Number),
);
expect(context.ui.setPendingItem).toHaveBeenLastCalledWith(null);
});

it('should show "No compression needed" when tokens unchanged', async () => {
mockTryCompressChatFast.mockResolvedValue({
originalTokenCount: 100,
newTokenCount: 100,
compressionStatus: CompressionStatus.NOOP,
} satisfies ChatCompressionInfo);

await compressFastCommand.action!(context, '');

expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: 'No compression needed.',
}),
expect.any(Number),
);
});

it('should handle errors gracefully', async () => {
const error = new Error('Compression failed');
mockTryCompressChatFast.mockRejectedValue(error);

await compressFastCommand.action!(context, '');

expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: `Failed to compress chat history: ${error.message}`,
}),
expect.any(Number),
);
expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
});

it('should return error message in non-interactive mode', async () => {
const error = new Error('Compression failed');
mockTryCompressChatFast.mockRejectedValue(error);

const ctx = createMockCommandContext({
executionMode: 'non_interactive',
services: {
config: {
getGeminiClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
},
},
});

const result = await compressFastCommand.action!(ctx, '');

expect(result).toEqual({
type: 'message',
messageType: 'error',
content: `Failed to compress chat history: ${error.message}`,
});
});

it('should return info message in non-interactive mode on success', async () => {
mockTryCompressChatFast.mockResolvedValue({
originalTokenCount: 200,
newTokenCount: 100,
compressionStatus: CompressionStatus.COMPRESSED,
} satisfies ChatCompressionInfo);

const ctx = createMockCommandContext({
executionMode: 'non_interactive',
services: {
config: {
getGeminiClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
},
},
});

const result = await compressFastCommand.action!(ctx, '');

expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Context compressed (200 -> 100).',
});
});
});
Loading
Loading