feat: Plan Mode implementation (Phase 2A)
- EnterPlanModeTool: switch to read-only mode with plan file path management - ExitPlanModeTool: restore permissions and validate plan content - PermissionRuleEngine: plan file edit exception in PLAN mode - SystemPromptBuilder: 5-phase plan workflow instructions injection - /plan command: toggle plan mode from command line - Plan file stored at ~/.claude/projects/[path]/PLAN.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
package com.claudecode.command.impl;
|
||||||
|
|
||||||
|
import com.claudecode.command.CommandContext;
|
||||||
|
import com.claudecode.command.SlashCommand;
|
||||||
|
import com.claudecode.permission.PermissionSettings;
|
||||||
|
import com.claudecode.permission.PermissionTypes.PermissionMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /plan 命令 —— 对应 claude-code/src/commands/plan/plan.tsx。
|
||||||
|
* <p>
|
||||||
|
* 切换计划模式开关。在计划模式下,AI只能分析不能修改。
|
||||||
|
*/
|
||||||
|
public class PlanCommand implements SlashCommand {
|
||||||
|
|
||||||
|
private final PermissionSettings permissionSettings;
|
||||||
|
|
||||||
|
public PlanCommand(PermissionSettings permissionSettings) {
|
||||||
|
this.permissionSettings = permissionSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "plan";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return "Toggle plan mode (analysis only, no file modifications)";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute(String args, CommandContext context) {
|
||||||
|
PermissionMode currentMode = permissionSettings.getCurrentMode();
|
||||||
|
|
||||||
|
if (currentMode == PermissionMode.PLAN) {
|
||||||
|
// Exit plan mode
|
||||||
|
permissionSettings.setCurrentMode(PermissionMode.DEFAULT);
|
||||||
|
return "📋 Exited plan mode. Normal permissions restored.\n" +
|
||||||
|
"All tools are now available.";
|
||||||
|
} else {
|
||||||
|
// Enter plan mode
|
||||||
|
permissionSettings.setCurrentMode(PermissionMode.PLAN);
|
||||||
|
return "📋 Entered plan mode.\n" +
|
||||||
|
"Only read-only tools are available. Use /plan again to exit.\n" +
|
||||||
|
"Or ask the AI to call EnterPlanMode for the full workflow.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,10 +79,11 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ToolRegistry toolRegistry(TaskManager taskManager, McpManager mcpManager,
|
public ToolRegistry toolRegistry(TaskManager taskManager, McpManager mcpManager,
|
||||||
ToolContext toolContext) {
|
ToolContext toolContext, PermissionSettings permissionSettings) {
|
||||||
// 将 TaskManager 和 McpManager 注册到 ToolContext 供工具使用
|
// 将 TaskManager 和 McpManager 注册到 ToolContext 供工具使用
|
||||||
toolContext.set("TASK_MANAGER", taskManager);
|
toolContext.set("TASK_MANAGER", taskManager);
|
||||||
toolContext.set("MCP_MANAGER", mcpManager);
|
toolContext.set("MCP_MANAGER", mcpManager);
|
||||||
|
toolContext.set("PERMISSION_SETTINGS", permissionSettings);
|
||||||
|
|
||||||
ToolRegistry registry = new ToolRegistry();
|
ToolRegistry registry = new ToolRegistry();
|
||||||
registry.registerAll(
|
registry.registerAll(
|
||||||
@@ -110,7 +111,9 @@ public class AppConfig {
|
|||||||
new ConfigTool(),
|
new ConfigTool(),
|
||||||
// P2: 实用工具
|
// P2: 实用工具
|
||||||
new SleepTool(),
|
new SleepTool(),
|
||||||
new ToolSearchTool()
|
new ToolSearchTool(),
|
||||||
|
new EnterPlanModeTool(),
|
||||||
|
new ExitPlanModeTool()
|
||||||
);
|
);
|
||||||
|
|
||||||
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
|
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
|
||||||
@@ -156,6 +159,7 @@ public class AppConfig {
|
|||||||
new FilesCommand(),
|
new FilesCommand(),
|
||||||
new PermissionsCommand(permissionSettings),
|
new PermissionsCommand(permissionSettings),
|
||||||
new TasksCommand(taskManager),
|
new TasksCommand(taskManager),
|
||||||
|
new PlanCommand(permissionSettings),
|
||||||
// P2 命令
|
// P2 命令
|
||||||
new HooksCommand(),
|
new HooksCommand(),
|
||||||
new ReviewCommand(),
|
new ReviewCommand(),
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ public class SystemPromptBuilder {
|
|||||||
private String skillsSummary;
|
private String skillsSummary;
|
||||||
private String gitSummary;
|
private String gitSummary;
|
||||||
private String languagePreference;
|
private String languagePreference;
|
||||||
|
private boolean planMode;
|
||||||
|
private String planFilePath;
|
||||||
|
private String sessionMemory;
|
||||||
|
|
||||||
public SystemPromptBuilder() {
|
public SystemPromptBuilder() {
|
||||||
this.workDir = System.getProperty("user.dir");
|
this.workDir = System.getProperty("user.dir");
|
||||||
@@ -67,6 +70,17 @@ public class SystemPromptBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SystemPromptBuilder planMode(boolean active, String planFilePath) {
|
||||||
|
this.planMode = active;
|
||||||
|
this.planFilePath = planFilePath;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SystemPromptBuilder sessionMemory(String sessionMemory) {
|
||||||
|
this.sessionMemory = sessionMemory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建完整的系统提示词。
|
* 构建完整的系统提示词。
|
||||||
*/
|
*/
|
||||||
@@ -130,6 +144,18 @@ public class SystemPromptBuilder {
|
|||||||
sb.append(customInstructions).append("\n\n");
|
sb.append(customInstructions).append("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 10. Plan Mode Instructions (对应 TS getPlanModeInstructions) ──
|
||||||
|
if (planMode) {
|
||||||
|
sb.append(getPlanModeSection());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 11. Session Memory (对应 TS SessionMemory) ──
|
||||||
|
if (sessionMemory != null && !sessionMemory.isBlank()) {
|
||||||
|
sb.append("# Session Memory\n");
|
||||||
|
sb.append("The following is a summary of key information from this conversation:\n");
|
||||||
|
sb.append(sessionMemory).append("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,4 +389,62 @@ public class SystemPromptBuilder {
|
|||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对应 TS getPlanModeInstructions()。
|
||||||
|
* 计划模式5阶段工作流指导。
|
||||||
|
*/
|
||||||
|
private String getPlanModeSection() {
|
||||||
|
String planPath = planFilePath != null ? planFilePath : "~/.claude/projects/PLAN.md";
|
||||||
|
return """
|
||||||
|
# Plan Mode Active
|
||||||
|
|
||||||
|
The user indicated they do NOT want execution yet. They want you to analyze and plan first.
|
||||||
|
YOU MUST NOT make any edits (except the plan file), run shell commands, or make changes.
|
||||||
|
|
||||||
|
## Plan File
|
||||||
|
Location: %s
|
||||||
|
This is the ONLY file you may create or edit in plan mode.
|
||||||
|
|
||||||
|
## Plan Workflow (5 Phases)
|
||||||
|
|
||||||
|
### Phase 1: Initial Understanding
|
||||||
|
- Use read-only tools (Read, Grep, Glob, ListFiles) to explore the codebase
|
||||||
|
- Find existing implementations and reusable patterns
|
||||||
|
- Understand the project structure and conventions
|
||||||
|
|
||||||
|
### Phase 2: Design
|
||||||
|
- Based on your understanding, design the implementation approach
|
||||||
|
- Consider multiple perspectives and trade-offs
|
||||||
|
- Identify potential risks and edge cases
|
||||||
|
|
||||||
|
### Phase 3: Review
|
||||||
|
- Read critical files you identified
|
||||||
|
- Ensure your plan aligns with the user's original request
|
||||||
|
- Use AskUserQuestion to clarify any ambiguous requirements
|
||||||
|
|
||||||
|
### Phase 4: Write the Plan
|
||||||
|
- Write your plan to the plan file ONLY
|
||||||
|
- Include these sections:
|
||||||
|
- **Context**: Why this change is needed (problem/need/outcome)
|
||||||
|
- **Recommended Approach**: Single recommended approach (not all alternatives)
|
||||||
|
- **File Paths**: Critical files to be modified
|
||||||
|
- **Existing Utilities**: Functions to reuse (with file paths)
|
||||||
|
- **Verification**: Command to test the changes end-to-end
|
||||||
|
- Keep the plan concise but detailed (~40 lines typical)
|
||||||
|
|
||||||
|
### Phase 5: Exit Plan Mode
|
||||||
|
- Call ExitPlanMode with a brief summary
|
||||||
|
- The user will review and approve the plan
|
||||||
|
- Do NOT use AskUserQuestion for plan approval — call ExitPlanMode instead
|
||||||
|
|
||||||
|
## Important Reminders
|
||||||
|
- You can ONLY use: Read, Grep, Glob, ListFiles, WebFetch, WebSearch, AskUserQuestion
|
||||||
|
- You can ONLY write to: %s
|
||||||
|
- Do NOT run Bash commands
|
||||||
|
- Do NOT edit any source files
|
||||||
|
- Do NOT use FileWrite or FileEdit except for the plan file
|
||||||
|
|
||||||
|
""".formatted(planPath, planPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.claudecode.permission;
|
package com.claudecode.permission;
|
||||||
|
|
||||||
import com.claudecode.permission.PermissionTypes.*;
|
import com.claudecode.permission.PermissionTypes.*;
|
||||||
|
import com.claudecode.tool.impl.EnterPlanModeTool;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -25,9 +26,13 @@ public class PermissionRuleEngine {
|
|||||||
private static final Set<String> FILE_EDIT_TOOLS = Set.of("Write", "Edit", "NotebookEdit");
|
private static final Set<String> FILE_EDIT_TOOLS = Set.of("Write", "Edit", "NotebookEdit");
|
||||||
private static final Set<String> READ_ONLY_TOOLS = Set.of(
|
private static final Set<String> READ_ONLY_TOOLS = Set.of(
|
||||||
"Read", "Glob", "Grep", "ListFiles", "WebFetch", "WebSearch",
|
"Read", "Glob", "Grep", "ListFiles", "WebFetch", "WebSearch",
|
||||||
"TodoRead", "TaskGet", "TaskList", "AskUserQuestion"
|
"TodoRead", "TaskGet", "TaskList", "AskUserQuestion",
|
||||||
|
"EnterPlanMode", "ExitPlanMode", "ToolSearch"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Tools allowed to operate on plan file during PLAN mode */
|
||||||
|
private static final Set<String> PLAN_FILE_TOOLS = Set.of("Write", "Edit");
|
||||||
|
|
||||||
private final PermissionSettings settings;
|
private final PermissionSettings settings;
|
||||||
|
|
||||||
public PermissionRuleEngine(PermissionSettings settings) {
|
public PermissionRuleEngine(PermissionSettings settings) {
|
||||||
@@ -43,6 +48,14 @@ public class PermissionRuleEngine {
|
|||||||
* @return 权限决策
|
* @return 权限决策
|
||||||
*/
|
*/
|
||||||
public PermissionDecision evaluate(String toolName, Map<String, Object> input, boolean isReadOnly) {
|
public PermissionDecision evaluate(String toolName, Map<String, Object> input, boolean isReadOnly) {
|
||||||
|
return evaluate(toolName, input, isReadOnly, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估工具调用的权限(带 ToolContext 用于 plan 模式检查)
|
||||||
|
*/
|
||||||
|
public PermissionDecision evaluate(String toolName, Map<String, Object> input,
|
||||||
|
boolean isReadOnly, Object toolContext) {
|
||||||
PermissionMode mode = settings.getCurrentMode();
|
PermissionMode mode = settings.getCurrentMode();
|
||||||
|
|
||||||
// BYPASS 模式:全部允许
|
// BYPASS 模式:全部允许
|
||||||
@@ -50,11 +63,15 @@ public class PermissionRuleEngine {
|
|||||||
return PermissionDecision.allow("Bypass mode enabled");
|
return PermissionDecision.allow("Bypass mode enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
// PLAN 模式:仅允许只读工具
|
// PLAN 模式:仅允许只读工具 + plan文件编辑
|
||||||
if (mode == PermissionMode.PLAN) {
|
if (mode == PermissionMode.PLAN) {
|
||||||
if (isReadOnly || READ_ONLY_TOOLS.contains(toolName)) {
|
if (isReadOnly || READ_ONLY_TOOLS.contains(toolName)) {
|
||||||
return PermissionDecision.allow("Read-only tool allowed in plan mode");
|
return PermissionDecision.allow("Read-only tool allowed in plan mode");
|
||||||
}
|
}
|
||||||
|
// Allow Write/Edit to plan file only
|
||||||
|
if (PLAN_FILE_TOOLS.contains(toolName) && isPlanFileOperation(input, toolContext)) {
|
||||||
|
return PermissionDecision.allow("Plan file edit allowed in plan mode");
|
||||||
|
}
|
||||||
return PermissionDecision.deny("Plan mode: execution disabled (analysis only)");
|
return PermissionDecision.deny("Plan mode: execution disabled (analysis only)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,4 +195,36 @@ public class PermissionRuleEngine {
|
|||||||
int space = trimmed.indexOf(' ');
|
int space = trimmed.indexOf(' ');
|
||||||
return space > 0 ? trimmed.substring(0, space) : trimmed;
|
return space > 0 ? trimmed.substring(0, space) : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件操作是否针对 plan 文件。
|
||||||
|
* 在 PLAN 模式中,只有 PLAN.md 可以被编辑。
|
||||||
|
*/
|
||||||
|
private boolean isPlanFileOperation(Map<String, Object> input, Object toolContext) {
|
||||||
|
if (input == null) return false;
|
||||||
|
String filePath = (String) input.get("file_path");
|
||||||
|
if (filePath == null) return false;
|
||||||
|
|
||||||
|
// Check if the file path ends with PLAN.md
|
||||||
|
if (filePath.endsWith("PLAN.md") || filePath.endsWith("plan.md")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have toolContext, check against stored plan file path
|
||||||
|
if (toolContext != null) {
|
||||||
|
try {
|
||||||
|
var ctx = (com.claudecode.tool.ToolContext) toolContext;
|
||||||
|
String planPath = ctx.get(EnterPlanModeTool.PLAN_FILE_PATH_KEY);
|
||||||
|
if (planPath != null) {
|
||||||
|
var targetPath = java.nio.file.Path.of(filePath).toAbsolutePath().normalize();
|
||||||
|
var planFilePath = java.nio.file.Path.of(planPath).toAbsolutePath().normalize();
|
||||||
|
return targetPath.equals(planFilePath);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore cast/access errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.claudecode.tool.impl;
|
||||||
|
|
||||||
|
import com.claudecode.permission.PermissionSettings;
|
||||||
|
import com.claudecode.permission.PermissionTypes.PermissionMode;
|
||||||
|
import com.claudecode.tool.ToolContext;
|
||||||
|
import com.claudecode.tool.Tool;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入计划模式工具 —— 对应 claude-code/src/tools/EnterPlanModeTool。
|
||||||
|
* <p>
|
||||||
|
* 将权限切换到只读模式,AI只能分析代码不能修改,
|
||||||
|
* 只有计划文件(PLAN.md)可以编辑。
|
||||||
|
*/
|
||||||
|
public class EnterPlanModeTool implements Tool {
|
||||||
|
|
||||||
|
public static final String PLAN_MODE_KEY = "PLAN_MODE_ACTIVE";
|
||||||
|
public static final String PLAN_FILE_PATH_KEY = "PLAN_FILE_PATH";
|
||||||
|
public static final String PRE_PLAN_MODE_KEY = "PRE_PLAN_MODE";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "EnterPlanMode";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return """
|
||||||
|
Enter plan mode to analyze the codebase and design an implementation plan WITHOUT making changes.
|
||||||
|
|
||||||
|
When to use:
|
||||||
|
- User asks you to "plan" or "think about" a change before implementing
|
||||||
|
- User wants to understand approach before committing to it
|
||||||
|
- Complex multi-file changes that need careful design
|
||||||
|
|
||||||
|
In plan mode:
|
||||||
|
- You can ONLY use read-only tools (Read, Grep, Glob, ListFiles, WebFetch, WebSearch)
|
||||||
|
- You can ONLY write to the plan file (PLAN.md)
|
||||||
|
- All other file modifications and shell commands are BLOCKED
|
||||||
|
- Use AskUserQuestion to clarify requirements
|
||||||
|
- Call ExitPlanMode when the plan is complete
|
||||||
|
|
||||||
|
The plan file location is determined automatically based on the project path.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String inputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Brief reason for entering plan mode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute(Map<String, Object> input, ToolContext context) {
|
||||||
|
// Check if already in plan mode
|
||||||
|
Boolean active = context.getOrDefault(PLAN_MODE_KEY, false);
|
||||||
|
if (active) {
|
||||||
|
String existingPlan = context.get(PLAN_FILE_PATH_KEY);
|
||||||
|
return "Already in plan mode. Plan file: " + existingPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine plan file path
|
||||||
|
Path workDir = context.getWorkDir();
|
||||||
|
Path planDir = getPlanDirectory(workDir);
|
||||||
|
Path planFile = planDir.resolve("PLAN.md");
|
||||||
|
|
||||||
|
// Save pre-plan mode for restoration
|
||||||
|
PermissionSettings permSettings = context.get("PERMISSION_SETTINGS");
|
||||||
|
if (permSettings != null) {
|
||||||
|
PermissionMode previousMode = permSettings.getCurrentMode();
|
||||||
|
context.set(PRE_PLAN_MODE_KEY, previousMode);
|
||||||
|
// Switch to PLAN mode
|
||||||
|
permSettings.setCurrentMode(PermissionMode.PLAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store plan state
|
||||||
|
context.set(PLAN_MODE_KEY, true);
|
||||||
|
context.set(PLAN_FILE_PATH_KEY, planFile.toString());
|
||||||
|
|
||||||
|
// Create plan directory if needed
|
||||||
|
try {
|
||||||
|
Files.createDirectories(planDir);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Non-fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = input != null ? (String) input.get("reason") : null;
|
||||||
|
boolean planExists = Files.exists(planFile);
|
||||||
|
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
result.append("✅ Entered plan mode.\n\n");
|
||||||
|
result.append("📋 Plan file: ").append(planFile).append("\n");
|
||||||
|
if (planExists) {
|
||||||
|
result.append("📄 Existing plan found — you can read and update it.\n");
|
||||||
|
} else {
|
||||||
|
result.append("📝 No existing plan — create one by writing to the plan file.\n");
|
||||||
|
}
|
||||||
|
result.append("\n");
|
||||||
|
result.append("Restrictions active:\n");
|
||||||
|
result.append(" • Only read-only tools allowed (Read, Grep, Glob, etc.)\n");
|
||||||
|
result.append(" • Only the plan file can be edited\n");
|
||||||
|
result.append(" • Shell commands are blocked\n");
|
||||||
|
result.append(" • Call ExitPlanMode when your plan is ready\n");
|
||||||
|
|
||||||
|
if (reason != null && !reason.isBlank()) {
|
||||||
|
result.append("\nReason: ").append(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
// This tool itself doesn't modify files
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String activityDescription(Map<String, Object> input) {
|
||||||
|
return "Entering plan mode...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plan directory for the given work directory.
|
||||||
|
* Uses ~/.claude/projects/[sanitized-path]/ structure.
|
||||||
|
*/
|
||||||
|
static Path getPlanDirectory(Path workDir) {
|
||||||
|
String sanitized = workDir.toAbsolutePath().toString()
|
||||||
|
.replace(":", "_")
|
||||||
|
.replace("\\", "_")
|
||||||
|
.replace("/", "_");
|
||||||
|
return Path.of(System.getProperty("user.home"))
|
||||||
|
.resolve(".claude")
|
||||||
|
.resolve("projects")
|
||||||
|
.resolve(sanitized);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package com.claudecode.tool.impl;
|
||||||
|
|
||||||
|
import com.claudecode.permission.PermissionSettings;
|
||||||
|
import com.claudecode.permission.PermissionTypes.PermissionMode;
|
||||||
|
import com.claudecode.tool.ToolContext;
|
||||||
|
import com.claudecode.tool.Tool;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出计划模式工具 —— 对应 claude-code/src/tools/ExitPlanModeTool。
|
||||||
|
* <p>
|
||||||
|
* 验证计划文件内容,恢复之前的权限模式。
|
||||||
|
*/
|
||||||
|
public class ExitPlanModeTool implements Tool {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "ExitPlanMode";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return """
|
||||||
|
Exit plan mode after completing your implementation plan.
|
||||||
|
|
||||||
|
When to use:
|
||||||
|
- You have finished writing the plan to the plan file
|
||||||
|
- The plan includes: context, approach, file paths, and verification steps
|
||||||
|
- You are ready for the user to review and approve the plan
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Restore normal permission mode (all tools available again)
|
||||||
|
- Present the plan to the user for review
|
||||||
|
- The user can then ask you to implement the plan
|
||||||
|
|
||||||
|
Do NOT call this tool until the plan is written to the plan file.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String inputSchema() {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Brief summary of the plan (1-2 sentences)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["summary"]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute(Map<String, Object> input, ToolContext context) {
|
||||||
|
// Check if in plan mode
|
||||||
|
Boolean active = context.getOrDefault(EnterPlanModeTool.PLAN_MODE_KEY, false);
|
||||||
|
if (!active) {
|
||||||
|
return "⚠️ Not currently in plan mode. Nothing to exit.";
|
||||||
|
}
|
||||||
|
|
||||||
|
String planFilePath = context.get(EnterPlanModeTool.PLAN_FILE_PATH_KEY);
|
||||||
|
String summary = input != null ? (String) input.get("summary") : null;
|
||||||
|
|
||||||
|
// Validate plan file exists and has content
|
||||||
|
String planContent = null;
|
||||||
|
if (planFilePath != null) {
|
||||||
|
Path planFile = Path.of(planFilePath);
|
||||||
|
if (Files.exists(planFile)) {
|
||||||
|
try {
|
||||||
|
planContent = Files.readString(planFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Non-fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore previous mode
|
||||||
|
PermissionSettings permSettings = context.get("PERMISSION_SETTINGS");
|
||||||
|
if (permSettings != null) {
|
||||||
|
PermissionMode previousMode = context.getOrDefault(
|
||||||
|
EnterPlanModeTool.PRE_PLAN_MODE_KEY, PermissionMode.DEFAULT);
|
||||||
|
permSettings.setCurrentMode(previousMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear plan mode state
|
||||||
|
context.set(EnterPlanModeTool.PLAN_MODE_KEY, false);
|
||||||
|
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
result.append("✅ Exited plan mode. Normal permissions restored.\n\n");
|
||||||
|
|
||||||
|
if (planContent != null && !planContent.isBlank()) {
|
||||||
|
int lines = (int) planContent.lines().count();
|
||||||
|
int chars = planContent.length();
|
||||||
|
result.append("📋 Plan file: ").append(planFilePath).append("\n");
|
||||||
|
result.append("📊 Plan size: ").append(lines).append(" lines, ")
|
||||||
|
.append(chars).append(" characters\n");
|
||||||
|
} else {
|
||||||
|
result.append("⚠️ Warning: Plan file is empty or missing.\n");
|
||||||
|
result.append(" Path: ").append(planFilePath).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary != null && !summary.isBlank()) {
|
||||||
|
result.append("\n📝 Summary: ").append(summary).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append("\nThe user can now review the plan and ask you to implement it.");
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
// This tool itself doesn't modify files, just state
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String activityDescription(Map<String, Object> input) {
|
||||||
|
return "Exiting plan mode...";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user