Skip to content

Commit 3a1d44c

Browse files
committed
feat(cli): add history command
1 parent 5c3f1be commit 3a1d44c

6 files changed

Lines changed: 190 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ dvcode --session abc123
340340
| `/help` | 显示帮助信息和快速入门指南 |
341341
| `/doctor` | 运行 CLI 快速诊断 |
342342
| `/report` | 生成诊断报告(默认复制到剪切板) |
343+
| `/history` | 查看最近输入历史 |
343344
| `/help-ask` | AI 智能帮助助手,解答使用问题 |
344345
| `/issue <描述>` | 提交 GitHub Issue(自动附带错误日志) |
345346
| `/quit``/exit` | 退出应用,显示会话统计 |

docs/cli/commands.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ Slash commands provide meta-level control over the CLI itself.
5151
- **`/report`**
5252
- **Description:** Generate a diagnostic report for sharing. Copies to clipboard by default.
5353

54+
- **`/history`**
55+
- **Description:** Show recent input history. Supports `--limit`, `--type`, and search terms.
56+
5457
- **`/mcp`**
5558
- **Description:** List configured Model Context Protocol (MCP) servers, their connection status, server details, and available tools.
5659
- **Sub-commands:**

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { helpCommand } from '../ui/commands/helpCommand.js';
2525
import { helpAskCommand } from '../ui/commands/helpAskCommand.js';
2626
import { doctorCommand } from '../ui/commands/doctorCommand.js';
2727
import { reportCommand } from '../ui/commands/reportCommand.js';
28+
import { historyCommand } from '../ui/commands/historyCommand.js';
2829
import { ideCommand } from '../ui/commands/ideCommand.js';
2930
import { initCommand } from '../ui/commands/initCommand.js';
3031
// import { mcpCommand } from '../ui/commands/mcpCommand.js'; // 已删除
@@ -91,6 +92,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
9192
helpAskCommand,
9293
doctorCommand,
9394
reportCommand,
95+
historyCommand,
9496
hooksCommand,
9597
issueCommand,
9698
ideCommand(this.config),
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect } from 'vitest';
8+
import { historyCommand } from './historyCommand.js';
9+
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
10+
import { t } from '../utils/i18n.js';
11+
12+
describe('historyCommand', () => {
13+
it('should show recent user history by default', async () => {
14+
const context = createMockCommandContext({
15+
ui: {
16+
history: [
17+
{ id: 1, type: 'user', text: 'hello' },
18+
{ id: 2, type: 'gemini', text: 'hi' },
19+
],
20+
},
21+
});
22+
23+
if (!historyCommand.action) {
24+
throw new Error('history command must have an action');
25+
}
26+
27+
const result = await historyCommand.action(context, '');
28+
29+
if (!result || result.type !== 'message') {
30+
throw new Error('Expected message result');
31+
}
32+
expect(result.content).toContain(t('command.history.header'));
33+
expect(result.content).toContain('[user] hello');
34+
expect(result.content).not.toContain('[gemini]');
35+
});
36+
37+
it('should filter history by query', async () => {
38+
const context = createMockCommandContext({
39+
ui: {
40+
history: [
41+
{ id: 1, type: 'user', text: 'alpha test' },
42+
{ id: 2, type: 'user', text: 'beta item' },
43+
],
44+
},
45+
});
46+
47+
if (!historyCommand.action) {
48+
throw new Error('history command must have an action');
49+
}
50+
51+
const result = await historyCommand.action(context, 'beta');
52+
53+
if (!result || result.type !== 'message') {
54+
throw new Error('Expected message result');
55+
}
56+
expect(result.content).toContain('[user] beta item');
57+
expect(result.content).not.toContain('[user] alpha test');
58+
});
59+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { CommandKind, SlashCommand } from './types.js';
2+
import { t } from '../utils/i18n.js';
3+
import { HistoryItem } from '../types.js';
4+
5+
type HistoryOptions = {
6+
limit: number;
7+
type: 'user' | 'assistant' | 'error' | 'all';
8+
query: string;
9+
};
10+
11+
const parseHistoryArgs = (args: string): HistoryOptions => {
12+
const tokens = args.split(/\s+/).filter(Boolean);
13+
let limit = 20;
14+
let type: HistoryOptions['type'] = 'user';
15+
const queryParts: string[] = [];
16+
17+
for (let i = 0; i < tokens.length; i += 1) {
18+
const token = tokens[i];
19+
if (token === '--limit' && tokens[i + 1]) {
20+
limit = Number(tokens[i + 1]) || limit;
21+
i += 1;
22+
continue;
23+
}
24+
if (token.startsWith('--limit=')) {
25+
const value = token.split('=')[1];
26+
limit = Number(value) || limit;
27+
continue;
28+
}
29+
if (token === '--type' && tokens[i + 1]) {
30+
const value = tokens[i + 1] as HistoryOptions['type'];
31+
type = value;
32+
i += 1;
33+
continue;
34+
}
35+
if (token.startsWith('--type=')) {
36+
const value = token.split('=')[1] as HistoryOptions['type'];
37+
type = value;
38+
continue;
39+
}
40+
queryParts.push(token);
41+
}
42+
43+
return {
44+
limit: Math.max(1, Math.min(200, limit)),
45+
type,
46+
query: queryParts.join(' ').trim(),
47+
};
48+
};
49+
50+
const matchesType = (
51+
item: HistoryItem,
52+
type: HistoryOptions['type'],
53+
): boolean => {
54+
if (type === 'all') {
55+
return true;
56+
}
57+
if (type === 'user') {
58+
return item.type === 'user' || item.type === 'user_shell';
59+
}
60+
if (type === 'assistant') {
61+
return item.type === 'gemini' || item.type === 'gemini_content';
62+
}
63+
return item.type === 'error';
64+
};
65+
66+
const formatHistoryItem = (item: HistoryItem): string => {
67+
if (item.text && item.text.trim()) {
68+
return `[${item.type}] ${item.text}`;
69+
}
70+
return `[${item.type}] (no text)`;
71+
};
72+
73+
export const historyCommand: SlashCommand = {
74+
name: 'history',
75+
description: t('command.history.description'),
76+
kind: CommandKind.BUILT_IN,
77+
action: async (context, args) => {
78+
const options = parseHistoryArgs(args);
79+
const history = context.ui.history ?? [];
80+
81+
if (history.length === 0) {
82+
return {
83+
type: 'message',
84+
messageType: 'info',
85+
content: t('command.history.empty'),
86+
};
87+
}
88+
89+
const query = options.query.toLowerCase();
90+
const filtered = history.filter((item) => {
91+
if (!matchesType(item, options.type)) {
92+
return false;
93+
}
94+
if (!query) {
95+
return true;
96+
}
97+
return (item.text ?? '').toLowerCase().includes(query);
98+
});
99+
100+
const latest = filtered.slice(-options.limit).reverse();
101+
const lines = latest.map((item, index) =>
102+
`${index + 1}. ${formatHistoryItem(item)}`.trim(),
103+
);
104+
105+
if (lines.length === 0) {
106+
return {
107+
type: 'message',
108+
messageType: 'info',
109+
content: t('command.history.empty'),
110+
};
111+
}
112+
113+
return {
114+
type: 'message',
115+
messageType: 'info',
116+
content: [t('command.history.header'), ...lines].join('\n'),
117+
};
118+
},
119+
};

packages/cli/src/ui/utils/i18n.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,7 @@ export const translations = {
973973
'command.help.description': 'Get deepv-code help',
974974
'command.doctor.description': 'Run quick diagnostics for the CLI',
975975
'command.report.description': 'Generate a diagnostic report for sharing',
976+
'command.history.description': 'Show recent input history',
976977
'command.clear.description':
977978
'Clear terminal screen (keeps conversation context)',
978979
'command.queue.description': 'Manage prompt queue',
@@ -991,6 +992,8 @@ export const translations = {
991992
'command.issue.opening': 'Opening GitHub issue form in your browser...',
992993
'command.report.copied': 'Report copied to clipboard.',
993994
'command.report.copy_failed': 'Failed to copy report to clipboard:',
995+
'command.history.header': 'Recent history:',
996+
'command.history.empty': 'No matching history entries found.',
994997
'command.issue.open.manual':
995998
'Please open the following URL in your browser to submit the issue:\n{url}',
996999
'command.issue.open.failed': 'Failed to open the issue URL: {error}',
@@ -2576,6 +2579,7 @@ export const translations = {
25762579
'command.help.description': '获取 deepv-code 帮助',
25772580
'command.doctor.description': '运行 CLI 快速诊断',
25782581
'command.report.description': '生成可分享的诊断报告',
2582+
'command.history.description': '显示最近的输入历史',
25792583
'command.clear.description': '清除终端屏幕(保留对话上下文)',
25802584
'command.queue.description': '管理提示队列',
25812585
'command.queue.clear.description': '清空所有排队的提示',
@@ -2592,6 +2596,8 @@ export const translations = {
25922596
'command.issue.opening': '正在为你打开 GitHub Issue 提交页面...',
25932597
'command.report.copied': '报告已复制到剪切板。',
25942598
'command.report.copy_failed': '复制报告到剪切板失败:',
2599+
'command.history.header': '最近历史记录:',
2600+
'command.history.empty': '未找到匹配的历史记录。',
25952601
'command.issue.open.manual': '请在浏览器中打开以下链接提交 Issue:\n{url}',
25962602
'command.issue.open.failed': '打开 Issue 链接失败:{error}',
25972603
'command.about.description': '显示版本信息',

0 commit comments

Comments
 (0)