diff --git a/.gitignore b/.gitignore index 8f054d4..58b8cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ dist/ .vscode/ *.tgz *.log +.claude/ +.mcp.json .deepcode/settings.json diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index 26a7a32..d2ef1c8 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -425,9 +425,11 @@ export function createMcpSpawnSpec( if (platform === "win32") { return { // On Windows, shell: true lets cmd.exe resolve the command via PATHEXT - // (npx -> npx.cmd, etc.). Pass one quoted command line with no spawn - // args to avoid Node 24 DEP0190. - command: [command, ...args].map(quoteWindowsShellArg).join(" "), + // (npx -> npx.cmd, etc.). Join command and args into a single string + // with empty spawn args to avoid Node 24 DEP0190. + // Only quote arguments that contain spaces or double-quotes to prevent + // double-wrapping by Node.js's own shell quoting. + command: [command, ...args].map(quoteWindowsArgIfNeeded).join(" "), args: [], shell: true, windowsHide: true, @@ -441,6 +443,9 @@ export function createMcpSpawnSpec( }; } -function quoteWindowsShellArg(arg: string): string { - return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, "$&$&")}"`; +function quoteWindowsArgIfNeeded(arg: string): string { + if (arg.includes(" ") || arg.includes('"')) { + return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, "$&$&")}"`; + } + return arg; } diff --git a/src/prompt.ts b/src/prompt.ts index 4bd1288..7083a06 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -94,9 +94,10 @@ const SYSTEM_PROMPT_BASE = `你是名叫Deep Code的交互式CLI工具,帮助 重要:严禁编造任何非编程相关的 URL。对于编程链接,仅限使用:1) 用户提供的上下文;2) 你确定的官方文档主域名。在输出前,必须自查该链接是否存在于你的上下文记忆中;若不存在,请明确说明无法提供。`; -type PromptToolOptions = { +export type PromptToolOptions = { model?: string; webSearchEnabled?: boolean; + planMode?: boolean; }; const DEFAULT_SKILL_TEMPLATES = ["karpathy-guidelines.md"]; @@ -550,5 +551,10 @@ export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDe tools.push(tool); } + if (_options.planMode) { + const allowedTools = new Set(["read", "AskUserQuestion", "UpdatePlan", "WebSearch"]); + return tools.filter((tool) => allowedTools.has(tool.function.name)); + } + return tools; } diff --git a/src/session.ts b/src/session.ts index 57108de..782ab9f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -15,6 +15,7 @@ import { getRuntimeContext, getSystemPrompt, getTools, + type PromptToolOptions, type ToolDefinition, } from "./prompt"; import { @@ -333,6 +334,7 @@ export class SessionManager { private readonly toolExecutor: ToolExecutor; private readonly mcpManager = new McpManager(); private mcpToolDefinitions: ToolDefinition[] = []; + private planModeSessionIds = new Set(); constructor(options: SessionManagerOptions) { this.projectRoot = options.projectRoot; @@ -1079,6 +1081,10 @@ ${skillMd} } } + if (userPrompt.skills?.some((skill) => skill.name === "plan-mode")) { + this.planModeSessionIds.add(sessionId); + } + this.activeSessionId = sessionId; await this.activateSession(sessionId, controller); return sessionId; @@ -1274,7 +1280,7 @@ ${skillMd} { model, messages, - tools: getTools(this.getPromptToolOptions(), this.mcpToolDefinitions), + tools: getTools(this.getPromptToolOptions(sessionId), this.mcpToolDefinitions), ...thinkingOptions, }, { signal: sessionController.signal }, @@ -1495,10 +1501,11 @@ ${skillMd} this.saveSessionMessages(sessionId, sessionMessages); } - private getPromptToolOptions(): { model: string; webSearchEnabled: boolean } { + private getPromptToolOptions(sessionId?: string): PromptToolOptions { return { model: this.getResolvedSettings().model, webSearchEnabled: true, + planMode: sessionId ? this.planModeSessionIds.has(sessionId) : false, }; } diff --git a/src/tests/mcp-client.test.ts b/src/tests/mcp-client.test.ts index e161aad..29151d3 100644 --- a/src/tests/mcp-client.test.ts +++ b/src/tests/mcp-client.test.ts @@ -10,9 +10,9 @@ test("createMcpSpawnSpec keeps non-Windows MCP launches shell-free", () => { }); }); -test("createMcpSpawnSpec avoids Windows shell args for Node 24", () => { +test("createMcpSpawnSpec joins args without quoting when spaces are absent (Windows)", () => { assert.deepEqual(createMcpSpawnSpec("npx", ["-y", "@playwright/mcp@latest"], "win32"), { - command: '"npx" "-y" "@playwright/mcp@latest"', + command: "npx -y @playwright/mcp@latest", args: [], shell: true, windowsHide: true, diff --git a/src/tests/slash-commands.test.ts b/src/tests/slash-commands.test.ts index 30d77ee..bae6ead 100644 --- a/src/tests/slash-commands.test.ts +++ b/src/tests/slash-commands.test.ts @@ -29,6 +29,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => { "undo", "mcp", "raw", + "plan", "exit", ]); }); diff --git a/src/ui/core/slash-commands.ts b/src/ui/core/slash-commands.ts index 04840ba..a0d3535 100644 --- a/src/ui/core/slash-commands.ts +++ b/src/ui/core/slash-commands.ts @@ -11,7 +11,8 @@ export type SlashCommandKind = | "undo" | "mcp" | "raw" - | "exit"; + | "exit" + | "plan"; export type SlashCommandItem = { kind: SlashCommandKind; @@ -78,6 +79,12 @@ export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [ args: ["lite", "normal", "raw-scrollback"], description: "Toggle display mode for viewing or collapsing reasoning content", }, + { + kind: "plan", + name: "plan", + label: "/plan", + description: "Create a detailed implementation plan without executing code", + }, { kind: "exit", name: "exit", diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index bef803e..3a812bb 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -1,9 +1,11 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; +import * as path from "path"; import { createOpenAIClient } from "../../common/openai-client"; import type { PermissionScope } from "../../settings"; import { type ModelConfigSelection } from "../../settings"; +import { getExtensionRoot } from "../../prompt"; import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptInput"; import { MessageView, RawModeExitPrompt } from "../components"; import { SessionList } from "./SessionList"; @@ -299,6 +301,53 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl return; } + if (submission.command === "plan") { + sessionManager.setActiveSessionId(null); + setMessages([]); + setStatusLine(""); + setErrorLine(null); + setRunningProcesses(null); + setActiveStatus(null); + setDismissedQuestionIds(new Set()); + setShowWelcome(false); + + const extensionRoot = getExtensionRoot(); + const planSkillPath = path.join(extensionRoot, "templates", "skills", "plan-mode.md"); + const planModeSkill: SkillInfo = { + name: "plan-mode", + path: planSkillPath, + description: "Analyze requirements and create a detailed implementation plan without executing code", + }; + + const planPrompt: UserPromptContent = { + text: submission.text || "Please create an implementation plan.", + skills: [planModeSkill], + }; + + if (planPrompt.text) { + setMessages((prev) => [...prev, buildSyntheticUserMessage(`/plan ${submission.text || ""}`, 0)]); + } + + setBusy(true); + setErrorLine(null); + setRunningProcesses(null); + setShowProcessStdout(false); + processStdoutRef.current.clear(); + try { + await sessionManager.handleUserPrompt(planPrompt); + await refreshSkills(); + refreshSessionsList(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setErrorLine(message); + } finally { + setBusy(false); + setStreamProgress(null); + setRunningProcesses(null); + } + return; + } + const prompt: UserPromptContent = { text: submission.text, imageUrls: submission.imageUrls, diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index b812a73..ee8d6f2 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -63,7 +63,7 @@ export type PromptSubmission = { selectedSkills?: SkillInfo[]; permissions?: UserToolPermission[]; alwaysAllows?: PermissionScope[]; - command?: "new" | "resume" | "continue" | "undo" | "mcp" | "exit"; + command?: "new" | "resume" | "continue" | "undo" | "mcp" | "exit" | "plan"; }; export type PromptDraft = { @@ -665,6 +665,17 @@ export const PromptInput = React.memo(function PromptInput({ resetPromptInput(); return; } + if (item.kind === "plan") { + const planPrefix = "/plan"; + const planDescription = buffer.text.startsWith(planPrefix) ? buffer.text.slice(planPrefix.length).trim() : ""; + onSubmit({ text: planDescription, imageUrls: [], command: "plan" }); + setBuffer(EMPTY_BUFFER); + clearUndoRedoStacks(); + setImageUrls([]); + setSelectedSkills([]); + setShowSkillsDropdown(false); + return; + } if (item.kind === "exit") { onSubmit({ text: "/exit", imageUrls: [], command: "exit" }); setBuffer(EMPTY_BUFFER); diff --git a/templates/skills/plan-mode.md b/templates/skills/plan-mode.md new file mode 100644 index 0000000..5ea5db7 --- /dev/null +++ b/templates/skills/plan-mode.md @@ -0,0 +1,142 @@ +--- +name: plan-mode +description: Analyze requirements and produce a detailed implementation plan without executing any code changes. Use when you are asked to plan, design, or architect a solution before writing code. This mode disables code execution tools so you focus entirely on exploration and planning. +--- + +# Plan Mode + +You are in plan-only mode. You MUST NOT execute any code or make any file changes. Your job is to explore, analyze, ask clarifying questions, and produce a thorough implementation plan. + +## Tool Restrictions + +### Allowed Tools + +These are the ONLY tools available to you in plan mode: + +- **Read** — Explore the codebase, read existing files, understand current architecture +- **AskUserQuestion** — Ask the user clarifying questions when requirements are ambiguous or multiple approaches exist +- **UpdatePlan** — Save and update the structured implementation plan as you iterate +- **WebSearch** — Search for external information (API docs, best practices, etc.) + +### Prohibited Tools + +The following tools are NOT available. Do not attempt to use them: + +- **Bash** — No shell commands. No package installation, no git operations, no running code. +- **Write** — No file creation. The plan document is the only artifact you produce. +- **Edit** — No file modifications. You are in read-only mode regarding the codebase. + +If you find yourself wanting to use a prohibited tool, stop and describe what you would do in the plan instead. + +## Planning Workflow + +### Step 1: Understand the Requirements + +Before exploring the codebase, make sure you understand what the user wants: + +1. **Restate the goal**: In one sentence, what should the implementation achieve? +2. **Identify unknowns**: What information is missing? Ambiguity? Conflicting requirements? +3. **Ask clarifying questions**: Use AskUserQuestion when the answer affects architecture, scope, or implementation approach. Do NOT ask about trivial details you can reasonably infer. + +Only ask questions that would change your plan. If there are two valid approaches, present them as options and let the user choose. + +### Step 2: Explore the Codebase + +Once requirements are clear, explore the project: + +1. **Find relevant files**: Use Read to examine files related to the feature area +2. **Understand existing patterns**: How are similar features implemented? What conventions does the project follow? +3. **Identify integration points**: Where does the new code need to connect? What interfaces exist? +4. **Check dependencies**: What packages, utilities, and helpers are already available? + +Read broadly first, then deeply on the most relevant files. Prefer reading multiple files in parallel when they are independent. + +### Step 3: Create the Plan + +Use UpdatePlan to output a structured implementation plan. The plan MUST include these sections: + +#### 1. Requirements Summary +- What problem does this solve? +- What are the acceptance criteria? +- What is explicitly OUT of scope? + +#### 2. Architecture Decisions +- High-level approach and rationale +- Alternative approaches considered and why they were rejected +- Key tradeoffs + +#### 3. Files to Change +- List every file that needs modification or creation +- For each file: purpose, key changes, and dependencies on other files +- Mark new files with [NEW] and existing files with [MODIFY] + +#### 4. Implementation Steps +- Each step is independently actionable +- Steps are ordered by dependency +- Each step includes: what to do, which files to touch, acceptance criteria +- Step granularity: each step should take 30 minutes to 2 hours + +#### 5. Data Flow / Component Interaction +- How data moves through the system +- Key interfaces, types, and function signatures +- Error handling strategy + +#### 6. Testing Strategy +- What tests need to be written or updated +- Test cases for the happy path and edge cases +- Manual verification steps + +#### 7. Risks and Mitigations +- What could go wrong? +- Which parts are most complex or uncertain? +- Rollback / revert strategy if applicable + +### Step 4: Iterate and Refine + +After creating the initial plan: + +1. **Review for completeness**: Are there gaps? Unhandled edge cases? +2. **Check for consistency**: Do the steps follow a logical order? Are dependencies clear? +3. **Add detail where needed**: Complex steps may need sub-steps or additional explanation +4. **Update the plan**: Use UpdatePlan again to reflect your refinements + +### Step 5: Final Review + +Before presenting the plan to the user: + +1. **Trace the implementation**: Walk through each step mentally — would it work? +2. **Verify scope**: Did you include anything outside what was requested? Remove it. +3. **Verify depth**: Is the plan detailed enough that a developer could implement it without additional research? +4. **Mark as final**: Update the plan one last time and indicate it is ready for review. + +## Plan Quality Standards + +A good plan is: + +- **Specific**: Names actual files, functions, and types — not vague descriptions +- **Ordered**: Steps are in dependency order, not random +- **Scoped**: Only what was requested, no bonus features +- **Grounded**: Based on the actual codebase, not assumptions +- **Actionable**: Each step has clear inputs, outputs, and acceptance criteria + +A bad plan: +- Vague descriptions like "update the UI" or "fix the bug" +- Missing dependencies between steps +- No file paths or wrong file paths +- Includes unrequested refactoring or cleanup +- Steps that are too large ("implement the entire feature" as one step) + +## Edge Cases + +- **No description provided**: If the user says just `/plan` with no details, use AskUserQuestion to ask what they want planned. Do NOT make assumptions. +- **Ambiguous requirements**: Ask. Do not guess. Present 2-3 options and let the user decide. +- **File does not exist yet**: Note in the plan that it is a new file. Describe its expected contents. +- **File paths are unclear**: Ask for the path. Do not infer file locations. +- **Multiple valid approaches**: Present tradeoffs. Recommend one with reasoning. +- **Plan is too large for one session**: Focus on the highest-priority parts first. Note deferred items explicitly. + +## Bottom Line + +**Do not write code. Do not modify files. Do not run commands.** + +Your sole output is a plan document. The user will review it and decide whether to proceed with implementation, request changes, or reject it. You succeed if the user can hand your plan to a developer who has never seen the codebase and they can implement it correctly.