Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ dist/
.vscode/
*.tgz
*.log
.claude/
.mcp.json
.deepcode/settings.json
15 changes: 10 additions & 5 deletions src/mcp/mcp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
8 changes: 7 additions & 1 deletion src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -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;
}
11 changes: 9 additions & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getRuntimeContext,
getSystemPrompt,
getTools,
type PromptToolOptions,
type ToolDefinition,
} from "./prompt";
import {
Expand Down Expand Up @@ -333,6 +334,7 @@ export class SessionManager {
private readonly toolExecutor: ToolExecutor;
private readonly mcpManager = new McpManager();
private mcpToolDefinitions: ToolDefinition[] = [];
private planModeSessionIds = new Set<string>();

constructor(options: SessionManagerOptions) {
this.projectRoot = options.projectRoot;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1274,7 +1280,7 @@ ${skillMd}
{
model,
messages,
tools: getTools(this.getPromptToolOptions(), this.mcpToolDefinitions),
tools: getTools(this.getPromptToolOptions(sessionId), this.mcpToolDefinitions),
...thinkingOptions,
},
{ signal: sessionController.signal },
Expand Down Expand Up @@ -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,
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/tests/mcp-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/tests/slash-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => {
"undo",
"mcp",
"raw",
"plan",
"exit",
]);
});
Expand Down
9 changes: 8 additions & 1 deletion src/ui/core/slash-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export type SlashCommandKind =
| "undo"
| "mcp"
| "raw"
| "exit";
| "exit"
| "plan";

export type SlashCommandItem = {
kind: SlashCommandKind;
Expand Down Expand Up @@ -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",
Expand Down
49 changes: 49 additions & 0 deletions src/ui/views/App.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion src/ui/views/PromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
Expand Down
142 changes: 142 additions & 0 deletions templates/skills/plan-mode.md
Original file line number Diff line number Diff line change
@@ -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.
Loading