Skip to content

Commit 73213a2

Browse files
committed
feat(vscode-ui): port /init and /compress slash commands from CLI
Until now vscode-ui-plugin only supported user-defined .toml slash commands; the 52 built-in commands shipped in CLI were absent from the visual UI. This commit establishes a small built-in command framework on top of slashCommandService and lights up the two highest-value commands first: /init and /compress. Architecture: - SlashCommandInfo gains two optional fields: execution ('prompt' | 'side_effect') and sideEffect (currently 'compress'). TOML commands and built-in prompt commands continue to flow through processCommandPrompt and the existing 'webview turns prompt into user message' path, so all existing TOML behavior is preserved. - BUILT_IN_COMMANDS array is loaded BEFORE TOML directories, so a user's own ~/.deepv/commands/init.toml or compress.toml will override the built-in version (matches CLI command-resolution order). - getCommand() now also matches altNames (e.g. /summarize and /compact resolve to /compress), aligning with CLI compressCommand. /init implementation: - INIT_COMMAND_PROMPT cloned from packages/cli/src/ui/commands/prompts/initPrompt.ts (header comment in initPrompt.ts marks the intentional duplication; if it diverges it should be lifted into core). - Backend handler creates an empty DEEPV.md when the file is missing or zero-bytes (mirrors CLI initCommand). When DEEPV.md already exists with content it returns an explicit error rather than silently overwriting the user's notes - a safer MVP than the CLI's init-choice dialog. /compress implementation: - Side-effect command. The execute_custom_slash_command handler returns { sideEffect: 'compress', info } and MessageInput's slash dispatch detects this discriminator, suppresses the normal sendMessage path, and posts a builtin_compress message back to the extension. - The new builtin_compress handler resolves the current session's GeminiClient via sessionManager.getCurrentInitializedAIService() and calls geminiClient.tryCompressChat(promptId, signal, true) - byte-for-byte equivalent to packages/cli/src/ui/commands/compressCommand.ts. - Compression progress and result are surfaced via the existing show_notification channel; failures (compression already in progress, client not ready, etc.) are reported back to the user instead of being swallowed. Tests: npm run build green across all 3 workspaces (core/cli/vscode-ui-plugin). Manual test plan: type /init in webview with no DEEPV.md → file created + INIT_COMMAND_PROMPT submitted to AI; type /compress on a long session → tryCompressChat invoked, result surfaced as a notification, no 'phantom AI message' sent.
1 parent fa3afde commit 73213a2

7 files changed

Lines changed: 450 additions & 11 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @license
3+
* Copyright 2025 DeepV Code team
4+
* https://github.com/OrionStarAI/DeepVCode
5+
* SPDX-License-Identifier: Apache-2.0
6+
*/
7+
8+
/**
9+
* VSCode UI 端 /init 命令使用的提示词。
10+
*
11+
* ⚠️ 与 CLI 端 packages/cli/src/ui/commands/prompts/initPrompt.ts 的
12+
* INIT_COMMAND_PROMPT **保持文本一致**,避免两端行为漂移。如果以后修改,
13+
* 两边都要改(或者后续再抽到 core 共享)。
14+
*
15+
* 该提示词由 /init 命令在 webview 端作为普通 user message 发送给 AI,
16+
* AI 收到后会扫描整个工程并写入 DEEPV.md。
17+
*/
18+
19+
export const INIT_COMMAND_PROMPT = `
20+
You are a DeepV Code AI assistant that brings the power of AI directly into the terminal. Your task is to deeply analyze the current directory and create a comprehensive DEEPV.md project context file.
21+
22+
**Analysis Process:**
23+
24+
1. **Initial Exploration:**
25+
* Start by listing the files and directories to get a high-level overview of the structure.
26+
* Read the README file (e.g., \`README.md\`, \`README.txt\`) if it exists. This is often the best place to start.
27+
28+
2. **Launch Code Analysis Expert:**
29+
* Use the \`task\` tool to launch a specialized code analysis expert
30+
* Provide detailed instructions for comprehensive project analysis
31+
* Let the expert systematically explore the codebase, understand architecture, and analyze patterns
32+
* The expert should examine:
33+
- Configuration files (\`package.json\`, \`tsconfig.json\`, \`webpack.config.js\`, etc.)
34+
- Main source files, key modules, and entry points
35+
- Documentation files, build scripts, and deployment configurations
36+
- Directory structures and project architecture
37+
- Test files to understand testing patterns and coverage
38+
- Dependencies and external integrations
39+
40+
3. **Identify Project Type:**
41+
* **Code Project:** Look for clues like \`package.json\`, \`requirements.txt\`, \`pom.xml\`, \`go.mod\`, \`Cargo.toml\`, \`build.gradle\`, or a \`src\` directory. If you find them, this is likely a software project.
42+
* **Non-Code Project:** If you don't find code-related files, this might be a directory for documentation, research papers, notes, or something else.
43+
44+
**Final Steps:**
45+
46+
1. **Create DEEPV.md File:**
47+
Based on the code analysis expert's findings and your own exploration, use the \`write_file\` tool to create a comprehensive DEEPV.md file in the current directory. The file should contain:
48+
49+
**For a Code Project:**
50+
* Project overview and purpose
51+
* Main technologies and framework architecture
52+
* Key build/run/test commands (from \`package.json\`, \`Makefile\`, etc.)
53+
* Development conventions and coding standards
54+
* Detailed file/directory structure explanation
55+
* Dependencies and external integrations
56+
* Special configurations or patterns
57+
* Testing strategies and CI/CD setup
58+
59+
**For a Non-Code Project:**
60+
* Directory purpose and contents overview
61+
* Key files and their purposes
62+
* Usage patterns and workflows
63+
* Organization structure
64+
65+
2. **MANDATORY: Record Generation Info:**
66+
After successfully creating the DEEPV.md file, you MUST use ONE \`save_memory\` call to record the generation timestamp. Use the current date and time:
67+
\`\`\`
68+
save_memory(fact="DEEPV.md generated by /init command on YYYY-MM-DD HH:mm:ss")
69+
\`\`\`
70+
Replace YYYY-MM-DD HH:mm:ss with the actual current date and time when you execute this.
71+
72+
**Important Notes:**
73+
- This is for DeepV Code, a customized AI programming assistant
74+
- Be thorough in your analysis - explore the entire project comprehensively
75+
- Create a detailed, well-structured DEEPV.md file using write_file tool
76+
- Format the content as clear Markdown that will help future AI interactions
77+
- **CRITICAL**: You MUST use save_memory exactly once at the end to record the generation timestamp - this is mandatory and cannot be skipped
78+
`;

packages/vscode-ui-plugin/src/extension.ts

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2958,7 +2958,94 @@ function setupSlashCommandHandlers() {
29582958
return;
29592959
}
29602960

2961-
// 处理命令的 prompt
2961+
// ─────────────────────────────────────────────────────────────
2962+
// 内置 side-effect 命令分发:不返回 prompt,而是执行后端动作
2963+
// ─────────────────────────────────────────────────────────────
2964+
if (command.execution === 'side_effect') {
2965+
if (command.sideEffect === 'compress') {
2966+
// /compress:让 webview 走副作用分支自己驱动压缩,与 CLI compressCommand
2967+
// 行为对齐(CLI 内部也是直接调 geminiClient.tryCompressChat)。
2968+
// 这里只把"我是 compress 副作用"反馈回去;真正调用 tryCompressChat 的
2969+
// 路径已经存在于 MultiSessionApp 里(compression_confirmation 流),
2970+
// webview 收到后转发一条 builtin_compress 消息回 backend 实际执行。
2971+
communicationService.sendMessage({
2972+
type: 'slash_command_result',
2973+
payload: {
2974+
success: true,
2975+
sideEffect: 'compress',
2976+
info: 'Compressing conversation history…',
2977+
},
2978+
});
2979+
return;
2980+
}
2981+
// 未来扩展其它 side-effect 类型时在此追加分支
2982+
communicationService.sendMessage({
2983+
type: 'slash_command_result',
2984+
payload: { success: false, error: `Unsupported side effect: ${command.sideEffect}` },
2985+
});
2986+
return;
2987+
}
2988+
2989+
// ─────────────────────────────────────────────────────────────
2990+
// /init 内置命令的特殊预处理:在 workspace 创建空 DEEPV.md,再走 prompt 分支
2991+
// ─────────────────────────────────────────────────────────────
2992+
if (command.kind === 'built-in' && command.name === 'init') {
2993+
const workspaceFolders = vscode.workspace.workspaceFolders;
2994+
if (!workspaceFolders || workspaceFolders.length === 0) {
2995+
communicationService.sendMessage({
2996+
type: 'slash_command_result',
2997+
payload: { success: false, error: '/init requires an open workspace folder.' },
2998+
});
2999+
return;
3000+
}
3001+
const workspaceRoot = workspaceFolders[0].uri.fsPath;
3002+
const deepvMdPath = path.join(workspaceRoot, 'DEEPV.md');
3003+
try {
3004+
let info: string | undefined;
3005+
if (fs.existsSync(deepvMdPath)) {
3006+
const stats = fs.statSync(deepvMdPath);
3007+
if (stats.size === 0) {
3008+
// 空文件:当不存在处理(与 CLI initCommand 行为对齐)
3009+
info = `Empty DEEPV.md detected at ${deepvMdPath}, regenerating…`;
3010+
} else {
3011+
// 非空文件:MVP 阶段保守拒绝执行,避免覆盖用户内容。
3012+
// CLI 端在这里会弹 init-choice 对话框;webview 端先走拒绝路径,
3013+
// 后续如有用户呼声再补对话框。
3014+
const sizeKB = Math.round(stats.size / 1024 * 100) / 100;
3015+
communicationService.sendMessage({
3016+
type: 'slash_command_result',
3017+
payload: {
3018+
success: false,
3019+
error: `DEEPV.md already exists (${sizeKB}KB). Delete or rename it first if you want to regenerate via /init.`,
3020+
},
3021+
});
3022+
return;
3023+
}
3024+
} else {
3025+
// 不存在:创建空文件
3026+
fs.writeFileSync(deepvMdPath, '', 'utf8');
3027+
info = `Created empty DEEPV.md at ${deepvMdPath}`;
3028+
}
3029+
const processedPrompt = slashCommandService.processCommandPrompt(command, args);
3030+
communicationService.sendMessage({
3031+
type: 'slash_command_result',
3032+
payload: { success: true, prompt: processedPrompt, info },
3033+
});
3034+
return;
3035+
} catch (initErr) {
3036+
logger.error('Failed to prepare DEEPV.md for /init', initErr instanceof Error ? initErr : undefined);
3037+
communicationService.sendMessage({
3038+
type: 'slash_command_result',
3039+
payload: {
3040+
success: false,
3041+
error: `Failed to prepare DEEPV.md: ${initErr instanceof Error ? initErr.message : String(initErr)}`,
3042+
},
3043+
});
3044+
return;
3045+
}
3046+
}
3047+
3048+
// 默认 prompt 模式(TOML 命令 / 其它 built-in prompt 命令)
29623049
const processedPrompt = slashCommandService.processCommandPrompt(command, args);
29633050

29643051
communicationService.sendMessage({
@@ -2974,6 +3061,70 @@ function setupSlashCommandHandlers() {
29743061
}
29753062
});
29763063

3064+
// ─────────────────────────────────────────────────────────────────
3065+
// 内置 /compress 的实际执行入口:webview 收到 sideEffect:'compress' 后
3066+
// 会回发一条 builtin_compress 消息触发这里。我们直接调用 core 的
3067+
// tryCompressChat(与 CLI compressCommand 完全一致),并把结果
3068+
// 通过现有 system-notification / chat_compressed 通道反馈给 webview。
3069+
// ─────────────────────────────────────────────────────────────────
3070+
communicationService.addMessageHandler('builtin_compress', async () => {
3071+
try {
3072+
// 当前会话的 AIService —— 与 setupChatHandlers 内 send_message 的
3073+
// 取法(getCurrentInitializedAIService)保持一致。
3074+
const aiService = await sessionManager.getCurrentInitializedAIService();
3075+
const geminiClient = aiService?.getGeminiClient?.();
3076+
if (!geminiClient) {
3077+
logger.warn('[/compress] geminiClient not initialized');
3078+
communicationService.sendMessage({
3079+
type: 'slash_command_result',
3080+
payload: { success: false, error: 'AI client not ready. Please wait for initialization.' },
3081+
});
3082+
return;
3083+
}
3084+
3085+
if (geminiClient.isCompressionInProgress?.()) {
3086+
communicationService.sendMessage({
3087+
type: 'slash_command_result',
3088+
payload: { success: false, error: 'Compression already in progress, please wait.' },
3089+
});
3090+
return;
3091+
}
3092+
3093+
logger.info('[/compress] Manual compression triggered via slash command');
3094+
const promptId = `slash-compress-${Date.now()}`;
3095+
const result = await geminiClient.tryCompressChat(promptId, new AbortController().signal, true);
3096+
3097+
if (result) {
3098+
const before = (result as any).originalTokenCount;
3099+
const after = (result as any).newTokenCount;
3100+
logger.info(`[/compress] Compressed ${before}${after} tokens`);
3101+
const fmt = (n: any) => (typeof n === 'number' ? n.toLocaleString() : String(n));
3102+
communicationService.sendMessage({
3103+
type: 'slash_command_result',
3104+
payload: {
3105+
success: true,
3106+
info: `Compressed ${fmt(before)}${fmt(after)} tokens`,
3107+
},
3108+
});
3109+
} else {
3110+
logger.warn('[/compress] tryCompressChat returned falsy result');
3111+
communicationService.sendMessage({
3112+
type: 'slash_command_result',
3113+
payload: { success: false, error: 'Compression returned no result. The history may already be small enough.' },
3114+
});
3115+
}
3116+
} catch (error) {
3117+
logger.error('[/compress] Compression failed', error instanceof Error ? error : undefined);
3118+
communicationService.sendMessage({
3119+
type: 'slash_command_result',
3120+
payload: {
3121+
success: false,
3122+
error: `Compression failed: ${error instanceof Error ? error.message : String(error)}`,
3123+
},
3124+
});
3125+
}
3126+
});
3127+
29773128
logger.info('🎯 Slash command handlers registered');
29783129
}
29793130

packages/vscode-ui-plugin/src/services/slashCommandService.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@
1010
* Loads custom slash commands from .toml files, sharing the same configuration
1111
* paths as the CLI (~/.deepv/commands and <project>/.deepvcode/commands).
1212
*
13-
* This is a simplified version of CLI's FileCommandLoader, adapted for VSCode environment.
13+
* Also exposes a small set of **built-in** commands that mirror high-value
14+
* CLI counterparts (currently: /init, /compress).
15+
*
16+
* Built-in commands have two execution shapes:
17+
* 1. `prompt` — same as TOML commands: returns a processed prompt string,
18+
* the webview then sends it to the AI as a normal user message.
19+
* 2. `side_effect` — the command performs a backend action (e.g. compress
20+
* history) and the webview MUST NOT forward it to the AI. Side-effect
21+
* commands carry a discriminator (`sideEffect: 'compress' | …`) which
22+
* the webview switches on to dispatch the right local action.
1423
*/
1524

1625
import * as fs from 'fs/promises';
@@ -20,6 +29,7 @@ import * as toml from '@iarna/toml';
2029
import { glob } from 'glob';
2130
import { getUserCommandsDirs, getProjectCommandsDirs } from 'deepv-code-core';
2231
import { Logger } from '../utils/logger';
32+
import { INIT_COMMAND_PROMPT } from '../constants/initPrompt';
2333

2434
/**
2535
* Slash command info sent to webview (serializable)
@@ -31,8 +41,20 @@ export interface SlashCommandInfo {
3141
description: string;
3242
/** Command source: 'file' for custom commands, 'built-in' for hardcoded */
3343
kind: 'file' | 'built-in';
34-
/** The prompt template */
44+
/** The prompt template (TOML 命令直接是用户写的;built-in 'prompt' 命令是固定文本;side-effect 命令为空字符串) */
3545
prompt: string;
46+
/**
47+
* Built-in 命令的执行风格:
48+
* - undefined / 'prompt':和 TOML 命令一样,processCommandPrompt 返回字符串,
49+
* webview 拿去当 user message 发给 AI。
50+
* - 'side_effect':调用方应当走副作用分支(不发 AI),由 sideEffect 字段
51+
* 指明执行哪种动作。
52+
*/
53+
execution?: 'prompt' | 'side_effect';
54+
/** 副作用类型(execution === 'side_effect' 时必填) */
55+
sideEffect?: 'compress';
56+
/** 命令别名(webview 在 fuzzy 匹配 / 输入解析时会兼容这些别名) */
57+
altNames?: string[];
3658
}
3759

3860
/**
@@ -48,6 +70,36 @@ interface TomlCommandDef {
4870
*/
4971
const SHORTHAND_ARGS_PLACEHOLDER = '{{args}}';
5072

73+
/**
74+
* 内置命令清单。**故意**和 CLI `packages/cli/src/ui/commands/*.ts` 保持
75+
* 字段语义一致:name / altNames / description 都从 CLI 复制过来,让用户
76+
* 在两端体验一致。
77+
*
78+
* 现仅注册 /init 与 /compress 两个最高频的命令;后续如需移植 /clear /memory
79+
* 等命令,只需在这里追加新条目并在 setupSlashCommandHandlers 中扩展副作用
80+
* 分支即可。
81+
*/
82+
const BUILT_IN_COMMANDS: SlashCommandInfo[] = [
83+
{
84+
name: 'init',
85+
description: 'Analyze the current workspace and create a DEEPV.md project context file',
86+
kind: 'built-in',
87+
execution: 'prompt',
88+
prompt: INIT_COMMAND_PROMPT,
89+
},
90+
{
91+
name: 'compress',
92+
altNames: ['summarize', 'compact'],
93+
description: 'Manually compress the conversation history to free up context (calls tryCompressChat)',
94+
kind: 'built-in',
95+
execution: 'side_effect',
96+
sideEffect: 'compress',
97+
// side-effect 命令不会被作为 prompt 发送,但保留一段说明文本,
98+
// 以便万一调用方误用 processCommandPrompt 时仍有合理 fallback。
99+
prompt: '[Built-in /compress invoked]',
100+
},
101+
];
102+
51103
/**
52104
* Service for managing custom slash commands in VSCode
53105
*/
@@ -91,10 +143,20 @@ export class SlashCommandService {
91143
}
92144

93145
/**
94-
* Get a specific command by name
146+
* Get a specific command by name. 同时匹配主名 + altNames(CLI 行为对齐)。
95147
*/
96148
getCommand(name: string): SlashCommandInfo | undefined {
97-
return this.commands.get(name);
149+
if (!name) return undefined;
150+
// 直接命中主名(最常见路径)
151+
const direct = this.commands.get(name);
152+
if (direct) return direct;
153+
// 退化:扫一次 altNames 命中(命令数量很少,O(n) 完全可接受)
154+
for (const cmd of this.commands.values()) {
155+
if (cmd.altNames && cmd.altNames.includes(name)) {
156+
return cmd;
157+
}
158+
}
159+
return undefined;
98160
}
99161

100162
/**
@@ -119,8 +181,21 @@ export class SlashCommandService {
119181

120182
/**
121183
* Load commands from both user and project directories
184+
*
185+
* 加载顺序(后者覆盖前者):
186+
* 1) Built-in 命令(/init, /compress …)
187+
* 2) 用户级 TOML 命令
188+
* 3) 项目级 TOML 命令
189+
*
190+
* 用户的 TOML 命令可以覆盖 built-in(比如想给 /compress 自定义行为),
191+
* 这与 CLI 的命令解析优先级保持一致。
122192
*/
123193
private async loadCommands(): Promise<void> {
194+
// 1) 先注册 built-in 命令
195+
for (const cmd of BUILT_IN_COMMANDS) {
196+
this.commands.set(cmd.name, cmd);
197+
}
198+
124199
const globOptions = {
125200
nodir: true,
126201
dot: true,

packages/vscode-ui-plugin/src/types/messages.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,16 @@ export type ExtensionToWebViewMessage =
500500
| { type: 'stream_recovery_end'; payload: { sessionId: string } }
501501
// 🎯 自定义斜杠命令相关
502502
| { type: 'slash_commands_list'; payload: { commands: SlashCommandInfo[] } }
503-
| { type: 'slash_command_result'; payload: { success: boolean; prompt?: string; error?: string } }
503+
| { type: 'slash_command_result'; payload: {
504+
success: boolean;
505+
/** Prompt 模式:返回处理后的 prompt 给 webview 转发为 user message */
506+
prompt?: string;
507+
/** Side-effect 模式:webview 检测到此字段则不发 AI,转发对应消息给 backend 触发动作 */
508+
sideEffect?: 'compress';
509+
/** 信息提示:用于在 webview 上显示一条系统通知(成功 / 跳过等) */
510+
info?: string;
511+
error?: string;
512+
} }
504513
// 🎯 模型切换压缩确认
505514
| { type: 'compression_confirmation_request'; payload: { requestId: string; sessionId: string; targetModel: string; currentTokens: number; targetTokenLimit: number; compressionThreshold: number; message: string } }
506515
// 🎯 Token使用情况更新(压缩后)

0 commit comments

Comments
 (0)