Skip to main content

aster/tools/
plan_mode_tool.rs

1// =============================================================================
2// Plan Mode Tools
3// =============================================================================
4//
5// 计划模式工具,基于 Claude Agent SDK 的 planmode.ts 实现
6// 提供 EnterPlanModeTool 和 ExitPlanModeTool 功能
7//
8// 功能特性:
9// - 进入计划模式进行复杂任务规划
10// - 只读模式,禁止文件修改(除计划文件外)
11// - 计划持久化存储
12// - 用户权限确认机制
13
14use crate::tools::{
15    base::{PermissionCheckResult, Tool},
16    context::{ToolContext, ToolOptions, ToolResult},
17    error::ToolError,
18};
19use async_trait::async_trait;
20use serde::{Deserialize, Serialize};
21use serde_json::{json, Value};
22use std::fs;
23use std::path::{Path, PathBuf};
24use std::sync::{Arc, Mutex};
25use std::time::{Duration, SystemTime, UNIX_EPOCH};
26use uuid::Uuid;
27
28// =============================================================================
29// 计划模式状态管理
30// =============================================================================
31
32/// 计划模式状态
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct PlanModeState {
35    pub active: bool,
36    pub plan_file: String,
37    pub plan_id: String,
38}
39
40/// 工具权限上下文
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ToolPermissionContext {
43    pub mode: String, // "normal", "plan", "delegate"
44}
45
46/// 应用状态(简化版本)
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct AppState {
49    pub tool_permission_context: ToolPermissionContext,
50    pub plan_mode: Option<PlanModeState>,
51}
52
53impl Default for AppState {
54    fn default() -> Self {
55        Self {
56            tool_permission_context: ToolPermissionContext {
57                mode: "normal".to_string(),
58            },
59            plan_mode: None,
60        }
61    }
62}
63
64/// 全局状态管理器
65pub struct GlobalStateManager {
66    state: Arc<Mutex<AppState>>,
67}
68
69impl GlobalStateManager {
70    pub fn new() -> Self {
71        Self {
72            state: Arc::new(Mutex::new(AppState::default())),
73        }
74    }
75}
76
77impl Default for GlobalStateManager {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl GlobalStateManager {
84    pub fn get_state(&self) -> AppState {
85        self.state.lock().unwrap().clone()
86    }
87
88    pub fn update_state<F>(&self, updater: F)
89    where
90        F: FnOnce(&mut AppState),
91    {
92        let mut state = self.state.lock().unwrap();
93        updater(&mut state);
94    }
95
96    pub fn is_plan_mode_active(&self) -> bool {
97        let state = self.state.lock().unwrap();
98        state.tool_permission_context.mode == "plan"
99    }
100
101    pub fn get_plan_file(&self) -> Option<String> {
102        let state = self.state.lock().unwrap();
103        state.plan_mode.as_ref().map(|pm| pm.plan_file.clone())
104    }
105
106    pub fn get_current_plan_id(&self) -> Option<String> {
107        let state = self.state.lock().unwrap();
108        state.plan_mode.as_ref().map(|pm| pm.plan_id.clone())
109    }
110
111    pub fn set_plan_mode(&self, active: bool, plan_file: Option<String>, plan_id: Option<String>) {
112        self.update_state(|state| {
113            if active {
114                let plan_file = plan_file.unwrap_or_else(|| {
115                    std::env::current_dir()
116                        .unwrap_or_else(|_| PathBuf::from("."))
117                        .join("PLAN.md")
118                        .to_string_lossy()
119                        .to_string()
120                });
121                let plan_id = plan_id.unwrap_or_else(|| Uuid::new_v4().to_string());
122
123                state.tool_permission_context.mode = "plan".to_string();
124                state.plan_mode = Some(PlanModeState {
125                    active: true,
126                    plan_file,
127                    plan_id,
128                });
129            } else {
130                state.tool_permission_context.mode = "normal".to_string();
131                state.plan_mode = None;
132            }
133        });
134    }
135}
136
137// 全局状态管理器实例
138lazy_static::lazy_static! {
139    static ref GLOBAL_STATE: GlobalStateManager = GlobalStateManager::new();
140}
141
142// =============================================================================
143// 计划持久化管理
144// =============================================================================
145
146/// 保存的计划结构
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SavedPlan {
149    pub metadata: PlanMetadata,
150    pub summary: String,
151    pub requirements_analysis: RequirementsAnalysis,
152    pub architectural_decisions: Vec<ArchitecturalDecision>,
153    pub steps: Vec<PlanStep>,
154    pub critical_files: Vec<CriticalFile>,
155    pub risks: Vec<Risk>,
156    pub alternatives: Vec<Alternative>,
157    pub estimated_complexity: String,
158    pub content: String,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct PlanMetadata {
163    pub id: String,
164    pub title: String,
165    pub description: String,
166    pub status: String, // "draft", "pending", "approved", "rejected"
167    pub created_at: u64,
168    pub updated_at: u64,
169    pub working_directory: String,
170    pub version: u32,
171    pub priority: String, // "low", "medium", "high"
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct RequirementsAnalysis {
176    pub functional_requirements: Vec<String>,
177    pub non_functional_requirements: Vec<String>,
178    pub technical_constraints: Vec<String>,
179    pub success_criteria: Vec<String>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ArchitecturalDecision {
184    pub title: String,
185    pub description: String,
186    pub rationale: String,
187    pub alternatives: Vec<String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct PlanStep {
192    pub step: u32,
193    pub description: String,
194    pub files: Vec<String>,
195    pub complexity: String, // "low", "medium", "high"
196    pub dependencies: Vec<u32>,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct CriticalFile {
201    pub path: String,
202    pub reason: String,
203    pub importance: u32, // 1-5
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct Risk {
208    pub category: String, // "technical", "business", "security"
209    pub level: String,    // "low", "medium", "high"
210    pub description: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct Alternative {
215    pub title: String,
216    pub description: String,
217    pub pros: Vec<String>,
218    pub cons: Vec<String>,
219}
220
221/// 计划持久化管理器
222pub struct PlanPersistenceManager;
223
224impl PlanPersistenceManager {
225    /// 生成计划 ID
226    pub fn generate_plan_id() -> String {
227        Uuid::new_v4().to_string()
228    }
229
230    /// 获取计划存储目录
231    pub fn get_plans_dir() -> PathBuf {
232        let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
233        PathBuf::from(home).join(".aster").join("plans")
234    }
235
236    /// 保存计划到持久化存储
237    pub fn save_plan(plan: &SavedPlan) -> Result<bool, ToolError> {
238        let plans_dir = Self::get_plans_dir();
239        if let Err(e) = fs::create_dir_all(&plans_dir) {
240            return Err(ToolError::ExecutionFailed(format!(
241                "Failed to create plans directory: {}",
242                e
243            )));
244        }
245
246        let plan_file = plans_dir.join(format!("{}.json", plan.metadata.id));
247        let plan_json = serde_json::to_string_pretty(plan)
248            .map_err(|e| ToolError::ExecutionFailed(format!("Failed to serialize plan: {}", e)))?;
249
250        fs::write(&plan_file, plan_json)
251            .map_err(|e| ToolError::ExecutionFailed(format!("Failed to write plan file: {}", e)))?;
252
253        Ok(true)
254    }
255
256    /// 从持久化存储加载计划
257    pub fn load_plan(plan_id: &str) -> Result<SavedPlan, ToolError> {
258        let plans_dir = Self::get_plans_dir();
259        let plan_file = plans_dir.join(format!("{}.json", plan_id));
260
261        if !plan_file.exists() {
262            return Err(ToolError::NotFound(format!("Plan not found: {}", plan_id)));
263        }
264
265        let plan_json = fs::read_to_string(&plan_file)
266            .map_err(|e| ToolError::ExecutionFailed(format!("Failed to read plan file: {}", e)))?;
267
268        let plan: SavedPlan = serde_json::from_str(&plan_json).map_err(|e| {
269            ToolError::ExecutionFailed(format!("Failed to deserialize plan: {}", e))
270        })?;
271
272        Ok(plan)
273    }
274}
275
276// =============================================================================
277// EnterPlanModeTool 实现
278// =============================================================================
279
280/// 进入计划模式工具
281///
282/// 基于 Claude Agent SDK 的 EnterPlanModeTool 完全复刻
283/// 用于复杂任务的规划和探索阶段
284pub struct EnterPlanModeTool;
285
286impl EnterPlanModeTool {
287    pub fn new() -> Self {
288        Self
289    }
290}
291
292impl Default for EnterPlanModeTool {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298#[async_trait]
299impl Tool for EnterPlanModeTool {
300    fn name(&self) -> &str {
301        "EnterPlanMode"
302    }
303
304    fn description(&self) -> &str {
305        r#"Use this tool when you encounter a complex task that requires careful planning and exploration before implementation.
306
307## When to Use This Tool
308
309**Prefer using EnterPlanMode** for implementation tasks unless they're simple. Use it when ANY of these conditions apply:
310
3111. **New Feature Implementation**: Adding meaningful new functionality
312   - Example: "Add a logout button" - where should it go? What should happen on click?
313   - Example: "Add form validation" - what rules? What error messages?
314
3152. **Multiple Valid Approaches**: The task can be solved in several different ways
316   - Example: "Add caching to the API" - could use Redis, in-memory, file-based, etc.
317   - Example: "Improve performance" - many optimization strategies possible
318
3193. **Code Modifications**: Changes that affect existing behavior or structure
320   - Example: "Update the login flow" - what exactly should change?
321   - Example: "Refactor this component" - what's the target architecture?
322
3234. **Architectural Decisions**: The task requires choosing between patterns or technologies
324   - Example: "Add real-time updates" - WebSockets vs SSE vs polling
325   - Example: "Implement state management" - Redux vs Context vs custom solution
326
3275. **Multi-File Changes**: The task will likely touch more than 2-3 files
328   - Example: "Refactor the authentication system"
329   - Example: "Add a new API endpoint with tests"
330
3316. **Unclear Requirements**: You need to explore before understanding the full scope
332   - Example: "Make the app faster" - need to profile and identify bottlenecks
333   - Example: "Fix the bug in checkout" - need to investigate root cause
334
3357. **User Preferences Matter**: The implementation could reasonably go multiple ways
336   - If you would use AskUserQuestion to clarify the approach, use EnterPlanMode instead
337   - Plan mode lets you explore first, then present options with context
338
339## When NOT to Use This Tool
340
341Only skip EnterPlanMode for simple tasks:
342- Single-line or few-line fixes (typos, obvious bugs, small tweaks)
343- Adding a single function with clear requirements
344- Tasks where the user has given very specific, detailed instructions
345- Pure research/exploration tasks (use the Task tool with explore agent instead)
346
347## What Happens in Plan Mode
348
349In plan mode, you'll:
3501. Thoroughly explore the codebase using Glob, Grep, and Read tools
3512. Understand existing patterns and architecture
3523. Design an implementation approach
3534. Present your plan to the user for approval
3545. Use AskUserQuestion if you need to clarify approaches
3556. Exit plan mode with ExitPlanMode when ready to implement
356
357## Examples
358
359### GOOD - Use EnterPlanMode:
360User: "Add user authentication to the app"
361- Requires architectural decisions (session vs JWT, where to store tokens, middleware structure)
362
363User: "Optimize the database queries"
364- Multiple approaches possible, need to profile first, significant impact
365
366User: "Implement dark mode"
367- Architectural decision on theme system, affects many components
368
369User: "Add a delete button to the user profile"
370- Seems simple but involves: where to place it, confirmation dialog, API call, error handling, state updates
371
372User: "Update the error handling in the API"
373- Affects multiple files, user should approve the approach
374
375### BAD - Don't use EnterPlanMode:
376User: "Fix the typo in the README"
377- Straightforward, no planning needed
378
379User: "Add a console.log to debug this function"
380- Simple, obvious implementation
381
382User: "What files handle routing?"
383- Research task, not implementation planning
384
385## Important Notes
386
387- This tool REQUIRES user approval - they must consent to entering plan mode
388- If unsure whether to use it, err on the side of planning - it's better to get alignment upfront than to redo work
389- Users appreciate being consulted before significant changes are made to their codebase"#
390    }
391
392    fn input_schema(&self) -> Value {
393        json!({
394            "type": "object",
395            "properties": {},
396            "required": []
397        })
398    }
399
400    fn options(&self) -> ToolOptions {
401        ToolOptions::new()
402            .with_base_timeout(Duration::from_secs(30))
403            .with_max_retries(0)
404    }
405
406    async fn check_permissions(
407        &self,
408        _params: &Value,
409        _context: &ToolContext,
410    ) -> PermissionCheckResult {
411        PermissionCheckResult::ask("Enter plan mode?")
412    }
413
414    async fn execute(
415        &self,
416        _params: Value,
417        _context: &ToolContext,
418    ) -> Result<ToolResult, ToolError> {
419        // 检查是否已经在计划模式中
420        if GLOBAL_STATE.is_plan_mode_active() {
421            return Ok(ToolResult::error(
422                "Already in plan mode. Use ExitPlanMode to exit first.",
423            ));
424        }
425
426        // 生成计划 ID 和文件路径
427        let plan_id = PlanPersistenceManager::generate_plan_id();
428        let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
429        let plan_path = current_dir.join("PLAN.md");
430
431        // 创建初始计划到持久化存储
432        let now = SystemTime::now()
433            .duration_since(UNIX_EPOCH)
434            .unwrap()
435            .as_millis() as u64;
436
437        let initial_plan = SavedPlan {
438            metadata: PlanMetadata {
439                id: plan_id.clone(),
440                title: "Untitled Plan".to_string(),
441                description: "Plan created in plan mode".to_string(),
442                status: "draft".to_string(),
443                created_at: now,
444                updated_at: now,
445                working_directory: current_dir.to_string_lossy().to_string(),
446                version: 1,
447                priority: "medium".to_string(),
448            },
449            summary: "Plan in progress".to_string(),
450            requirements_analysis: RequirementsAnalysis {
451                functional_requirements: vec![],
452                non_functional_requirements: vec![],
453                technical_constraints: vec![],
454                success_criteria: vec![],
455            },
456            architectural_decisions: vec![],
457            steps: vec![],
458            critical_files: vec![],
459            risks: vec![],
460            alternatives: vec![],
461            estimated_complexity: "moderate".to_string(),
462            content: "# Implementation Plan\n\n(Building plan...)".to_string(),
463        };
464
465        // 保存到持久化存储
466        PlanPersistenceManager::save_plan(&initial_plan)?;
467
468        // 更新全局状态:设置计划模式
469        GLOBAL_STATE.set_plan_mode(
470            true,
471            Some(plan_path.to_string_lossy().to_string()),
472            Some(plan_id.clone()),
473        );
474
475        let output = format!(
476            r#"Entered plan mode.
477
478Plan ID: {}
479
480=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
481This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
482- Creating new files (no Write, touch, or file creation of any kind) EXCEPT the plan file
483- Modifying existing files (no Edit operations) EXCEPT the plan file
484- Deleting files (no rm or deletion)
485- Moving or copying files (no mv or cp)
486- Creating temporary files anywhere, including /tmp
487- Using redirect operators (>, >>, |) or heredocs to write to files
488- Running ANY commands that change system state
489
490Your role is EXCLUSIVELY to explore the codebase and design implementation plans. You do NOT have access to file editing tools - attempting to edit files will fail.
491
492## Plan File Info:
493No plan file exists yet. You should create your plan at {} using the Write tool.
494You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
495
496The plan will be automatically saved to the persistent storage (~/.aster/plans/{}.json) when you exit plan mode.
497
498In plan mode, you should:
4991. Thoroughly explore the codebase to understand existing patterns
5002. Identify similar features and architectural approaches
5013. Consider multiple approaches and their trade-offs
5024. Use AskUserQuestion if you need to clarify the approach
5035. Design a concrete implementation strategy
5046. When ready, use ExitPlanMode to present your plan for approval
505
506Focus on understanding the problem before proposing solutions."#,
507            plan_id,
508            plan_path.display(),
509            plan_id
510        );
511
512        Ok(ToolResult::success(output)
513            .with_metadata("plan_id", json!(plan_id))
514            .with_metadata("plan_file", json!(plan_path.to_string_lossy()))
515            .with_metadata("mode", json!("plan")))
516    }
517}
518
519// =============================================================================
520// ExitPlanModeTool 实现
521// =============================================================================
522
523/// 退出计划模式工具输入
524#[derive(Debug, Serialize, Deserialize)]
525pub struct ExitPlanModeInput {}
526
527/// 退出计划模式工具
528///
529/// 基于 Claude Agent SDK 的 ExitPlanModeTool 完全复刻
530/// 用于完成计划并等待用户批准
531pub struct ExitPlanModeTool;
532
533impl ExitPlanModeTool {
534    pub fn new() -> Self {
535        Self
536    }
537
538    /// 解析计划内容为 SavedPlan 结构
539    fn parse_plan_content(&self, plan_id: &str, content: &str) -> SavedPlan {
540        let now = SystemTime::now()
541            .duration_since(UNIX_EPOCH)
542            .unwrap()
543            .as_millis() as u64;
544
545        // 从第一个标题提取标题
546        let title = content
547            .lines()
548            .find(|line| line.starts_with("# "))
549            .map(|line| line.trim_start_matches("# ").trim())
550            .unwrap_or("Untitled Plan")
551            .to_string();
552
553        SavedPlan {
554            metadata: PlanMetadata {
555                id: plan_id.to_string(),
556                title: title.clone(),
557                description: title,
558                status: "pending".to_string(),
559                created_at: now,
560                updated_at: now,
561                working_directory: std::env::current_dir()
562                    .unwrap_or_else(|_| PathBuf::from("."))
563                    .to_string_lossy()
564                    .to_string(),
565                version: 1,
566                priority: "medium".to_string(),
567            },
568            summary: self.extract_summary(content),
569            requirements_analysis: RequirementsAnalysis {
570                functional_requirements: self
571                    .extract_requirements(content, "Functional Requirements"),
572                non_functional_requirements: self
573                    .extract_requirements(content, "Non-Functional Requirements"),
574                technical_constraints: self.extract_requirements(content, "Technical Constraints"),
575                success_criteria: self.extract_requirements(content, "Success Criteria"),
576            },
577            architectural_decisions: vec![],
578            steps: self.extract_steps(content),
579            critical_files: self.extract_critical_files(content),
580            risks: self.extract_risks(content),
581            alternatives: vec![],
582            estimated_complexity: "moderate".to_string(),
583            content: content.to_string(),
584        }
585    }
586
587    fn extract_summary(&self, content: &str) -> String {
588        // 查找 ## Summary 部分
589        if let Some(start) = content.find("## Summary") {
590            let after_header = content.get(start..).unwrap_or("");
591            if let Some(content_start) = after_header.find('\n') {
592                let content_part = after_header.get(content_start + 1..).unwrap_or("");
593                if let Some(end) = content_part.find("\n##") {
594                    return content_part.get(..end).unwrap_or("").trim().to_string();
595                } else {
596                    return content_part.trim().to_string();
597                }
598            }
599        }
600        "No summary provided".to_string()
601    }
602
603    fn extract_requirements(&self, content: &str, section: &str) -> Vec<String> {
604        let section_header = format!("### {}", section);
605        if let Some(start) = content.find(&section_header) {
606            let after_header = content.get(start..).unwrap_or("");
607            if let Some(content_start) = after_header.find('\n') {
608                let content_part = after_header.get(content_start + 1..).unwrap_or("");
609                let end = content_part
610                    .find("\n###")
611                    .or_else(|| content_part.find("\n##"))
612                    .unwrap_or(content_part.len());
613                let section_content = content_part.get(..end).unwrap_or("");
614
615                return section_content
616                    .lines()
617                    .filter_map(|line| {
618                        let trimmed = line.trim();
619                        trimmed
620                            .strip_prefix("- ")
621                            .map(|stripped| stripped.trim().to_string())
622                    })
623                    .filter(|line| !line.is_empty())
624                    .collect();
625            }
626        }
627        vec![]
628    }
629
630    fn extract_steps(&self, content: &str) -> Vec<PlanStep> {
631        let mut steps = vec![];
632
633        // 使用简单的字符串匹配而不是正则表达式
634        for line in content.lines() {
635            if line.starts_with("### Step ") {
636                if let Some(colon_pos) = line.find(": ") {
637                    let step_part = line.get(9..colon_pos).unwrap_or(""); // "### Step ".len() = 9
638                    if let Ok(step_number) = step_part.parse::<u32>() {
639                        let description = line.get(colon_pos + 2..).unwrap_or("");
640                        steps.push(PlanStep {
641                            step: step_number,
642                            description: description.to_string(),
643                            files: vec![],
644                            complexity: "medium".to_string(),
645                            dependencies: vec![],
646                        });
647                    }
648                }
649            }
650        }
651
652        steps
653    }
654
655    fn extract_critical_files(&self, content: &str) -> Vec<CriticalFile> {
656        let mut files = vec![];
657
658        if let Some(start) = content.find("### Critical Files") {
659            let after_header = content.get(start..).unwrap_or("");
660            if let Some(content_start) = after_header.find('\n') {
661                let content_part = after_header.get(content_start + 1..).unwrap_or("");
662                let end = content_part.find("\n##").unwrap_or(content_part.len());
663                let section_content = content_part.get(..end).unwrap_or("");
664
665                for line in section_content.lines() {
666                    let trimmed = line.trim();
667                    if let Some(content_line) = trimmed.strip_prefix("- ") {
668                        if let Some(dash_pos) = content_line.find(" - ") {
669                            let path = content_line.get(..dash_pos).unwrap_or("").trim();
670                            let reason = content_line.get(dash_pos + 3..).unwrap_or("").trim();
671                            files.push(CriticalFile {
672                                path: path.to_string(),
673                                reason: reason.to_string(),
674                                importance: 3,
675                            });
676                        }
677                    }
678                }
679            }
680        }
681
682        files
683    }
684
685    fn extract_risks(&self, content: &str) -> Vec<Risk> {
686        let mut risks = vec![];
687
688        if let Some(start) = content.find("## Risks") {
689            let after_header = content.get(start..).unwrap_or("");
690            if let Some(content_start) = after_header.find('\n') {
691                let content_part = after_header.get(content_start + 1..).unwrap_or("");
692                // 寻找下一个 ## 标题,如果没有找到就读取到末尾
693                let end = content_part.find("\n## ").unwrap_or(content_part.len());
694                let section_content = content_part.get(..end).unwrap_or("");
695
696                // 按 ### 分割风险块
697                let risk_blocks: Vec<&str> = section_content.split("### ").collect();
698                for block in risk_blocks {
699                    if block.trim().is_empty() {
700                        continue;
701                    }
702
703                    let lines: Vec<&str> = block.lines().collect();
704                    if let Some(first_line) = lines.first() {
705                        let description = first_line.trim();
706                        if !description.is_empty() {
707                            // 移除数字前缀(如 "1. Performance Risk" -> "Performance Risk")
708                            let clean_description = if let Some(dot_pos) = description.find(". ") {
709                                description.get(dot_pos + 2..).unwrap_or(description)
710                            } else {
711                                description
712                            };
713
714                            risks.push(Risk {
715                                category: "technical".to_string(),
716                                level: "medium".to_string(),
717                                description: clean_description.to_string(),
718                            });
719                        }
720                    }
721                }
722            }
723        }
724
725        risks
726    }
727}
728
729impl Default for ExitPlanModeTool {
730    fn default() -> Self {
731        Self::new()
732    }
733}
734
735#[async_trait]
736impl Tool for ExitPlanModeTool {
737    fn name(&self) -> &str {
738        "ExitPlanMode"
739    }
740
741    fn description(&self) -> &str {
742        r#"Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.
743
744## How This Tool Works
745- You should have already written your plan to the plan file specified in the plan mode system message
746- This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote
747- This tool simply signals that you're done planning and ready for the user to review and approve
748- The user will see the contents of your plan file when they review it
749
750## When to Use This Tool
751IMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.
752
753## Handling Ambiguity in Plans
754Before using this tool, ensure your plan is clear and unambiguous. If there are multiple valid approaches or unclear requirements:
7551. Use the AskUserQuestion tool to clarify with the user
7562. Ask about specific implementation choices (e.g., architectural patterns, which library to use)
7573. Clarify any assumptions that could affect the implementation
7584. Edit your plan file to incorporate user feedback
7595. Only proceed with ExitPlanMode after resolving ambiguities and updating the plan file
760
761## Examples
762
7631. Initial task: "Search for and understand the implementation of vim mode in the codebase" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.
7642. Initial task: "Help me implement yank mode for vim" - Use the exit plan mode tool after you have finished planning the implementation steps of the task.
7653. Initial task: "Add a new feature to handle user authentication" - If unsure about auth method (OAuth, JWT, etc.), use AskUserQuestion first, then use exit plan mode tool after clarifying the approach."#
766    }
767
768    fn input_schema(&self) -> Value {
769        json!({
770            "type": "object",
771            "properties": {},
772            "required": []
773        })
774    }
775
776    fn options(&self) -> ToolOptions {
777        ToolOptions::new()
778            .with_base_timeout(Duration::from_secs(30))
779            .with_max_retries(0)
780    }
781
782    async fn check_permissions(
783        &self,
784        _params: &Value,
785        _context: &ToolContext,
786    ) -> PermissionCheckResult {
787        PermissionCheckResult::ask("Exit plan mode?")
788    }
789
790    async fn execute(
791        &self,
792        _params: Value,
793        _context: &ToolContext,
794    ) -> Result<ToolResult, ToolError> {
795        // 检查是否在计划模式中
796        if !GLOBAL_STATE.is_plan_mode_active() {
797            return Ok(ToolResult::error(
798                "Not in plan mode. Use EnterPlanMode first.",
799            ));
800        }
801
802        // 获取计划文件信息
803        let plan_file = GLOBAL_STATE.get_plan_file();
804        let plan_id = GLOBAL_STATE.get_current_plan_id();
805
806        let mut plan_content = String::new();
807        if let Some(ref plan_file_path) = plan_file {
808            if Path::new(plan_file_path).exists() {
809                plan_content = fs::read_to_string(plan_file_path).unwrap_or_default();
810            }
811        }
812
813        // 解析并保存计划到持久化存储
814        let mut saved_plan_path: Option<String> = None;
815        if let (Some(plan_id), false) = (&plan_id, plan_content.is_empty()) {
816            let plan = self.parse_plan_content(plan_id, &plan_content);
817            match PlanPersistenceManager::save_plan(&plan) {
818                Ok(_) => {
819                    saved_plan_path = Some(format!("~/.aster/plans/{}.json", plan_id));
820                }
821                Err(e) => {
822                    eprintln!("Failed to save plan to persistence: {}", e);
823                }
824            }
825        }
826
827        // 更新全局状态:退出计划模式
828        GLOBAL_STATE.set_plan_mode(false, None, None);
829
830        let output = if let Some(ref plan_file_path) = plan_file {
831            format!(
832                r#"Exited plan mode.
833
834Your plan has been saved to:
835- Working file: {}{}
836{}
837
838Awaiting user approval to proceed with implementation.
839
840## Approved Plan:
841{}"#,
842                plan_file_path,
843                saved_plan_path
844                    .as_ref()
845                    .map(|p| format!("\n- Persistent storage: {}", p))
846                    .unwrap_or_default(),
847                plan_id
848                    .as_ref()
849                    .map(|id| format!("\nPlan ID: {}", id))
850                    .unwrap_or_default(),
851                plan_content
852            )
853        } else {
854            "Exited plan mode. Awaiting user approval to proceed with implementation.".to_string()
855        };
856
857        Ok(ToolResult::success(output)
858            .with_metadata("plan_id", json!(plan_id))
859            .with_metadata("plan_file", json!(plan_file))
860            .with_metadata("saved_plan_path", json!(saved_plan_path))
861            .with_metadata("mode", json!("normal")))
862    }
863}
864
865#[cfg(test)]
866mod tests {
867    use super::*;
868    use crate::tools::{context::ToolContext, PermissionBehavior};
869    use serde_json::json;
870    use std::collections::HashMap;
871    use tempfile::TempDir;
872    use tokio;
873
874    fn create_test_context() -> ToolContext {
875        ToolContext {
876            working_directory: std::env::current_dir().unwrap(),
877            session_id: "test-session".to_string(),
878            user: Some("test-user".to_string()),
879            environment: HashMap::new(),
880            cancellation_token: None,
881        }
882    }
883
884    #[test]
885    fn test_enter_plan_mode_tool_creation() {
886        let tool = EnterPlanModeTool::new();
887        assert_eq!(tool.name(), "EnterPlanMode");
888        assert!(tool.description().contains("complex task"));
889    }
890
891    #[test]
892    fn test_exit_plan_mode_tool_creation() {
893        let tool = ExitPlanModeTool::new();
894        assert_eq!(tool.name(), "ExitPlanMode");
895        assert!(tool.description().contains("finished writing your plan"));
896    }
897
898    #[test]
899    fn test_global_state_manager() {
900        let manager = GlobalStateManager::new();
901
902        // 初始状态
903        assert!(!manager.is_plan_mode_active());
904        assert!(manager.get_plan_file().is_none());
905        assert!(manager.get_current_plan_id().is_none());
906
907        // 设置计划模式
908        manager.set_plan_mode(
909            true,
910            Some("test.md".to_string()),
911            Some("test-id".to_string()),
912        );
913        assert!(manager.is_plan_mode_active());
914        assert_eq!(manager.get_plan_file(), Some("test.md".to_string()));
915        assert_eq!(manager.get_current_plan_id(), Some("test-id".to_string()));
916
917        // 退出计划模式
918        manager.set_plan_mode(false, None, None);
919        assert!(!manager.is_plan_mode_active());
920        assert!(manager.get_plan_file().is_none());
921        assert!(manager.get_current_plan_id().is_none());
922    }
923
924    #[test]
925    fn test_plan_persistence_manager() {
926        let _temp_dir = TempDir::new().unwrap();
927        let _plans_dir = _temp_dir.path().join("plans");
928
929        // 创建测试计划
930        let plan_id = "test-plan-id";
931        let now = SystemTime::now()
932            .duration_since(UNIX_EPOCH)
933            .unwrap()
934            .as_millis() as u64;
935
936        let _plan = SavedPlan {
937            metadata: PlanMetadata {
938                id: plan_id.to_string(),
939                title: "Test Plan".to_string(),
940                description: "A test plan".to_string(),
941                status: "draft".to_string(),
942                created_at: now,
943                updated_at: now,
944                working_directory: "/tmp".to_string(),
945                version: 1,
946                priority: "medium".to_string(),
947            },
948            summary: "Test summary".to_string(),
949            requirements_analysis: RequirementsAnalysis {
950                functional_requirements: vec!["Req 1".to_string()],
951                non_functional_requirements: vec![],
952                technical_constraints: vec![],
953                success_criteria: vec![],
954            },
955            architectural_decisions: vec![],
956            steps: vec![],
957            critical_files: vec![],
958            risks: vec![],
959            alternatives: vec![],
960            estimated_complexity: "low".to_string(),
961            content: "# Test Plan\n\nThis is a test plan.".to_string(),
962        };
963
964        // 注意:这个测试需要修改 PlanPersistenceManager 来支持自定义目录
965        // 或者我们可以测试 ID 生成功能
966        let generated_id = PlanPersistenceManager::generate_plan_id();
967        assert!(!generated_id.is_empty());
968        assert!(generated_id.len() > 10); // UUID 应该比较长
969    }
970
971    #[tokio::test]
972    async fn test_enter_plan_mode_permissions() {
973        let tool = EnterPlanModeTool::new();
974        let context = create_test_context();
975        let input = json!({});
976
977        let result = tool.check_permissions(&input, &context).await;
978        assert!(matches!(result.behavior, PermissionBehavior::Ask));
979        assert_eq!(result.message, Some("Enter plan mode?".to_string()));
980    }
981
982    #[tokio::test]
983    async fn test_exit_plan_mode_permissions() {
984        let tool = ExitPlanModeTool::new();
985        let context = create_test_context();
986        let input = json!({});
987
988        let result = tool.check_permissions(&input, &context).await;
989        assert!(matches!(result.behavior, PermissionBehavior::Ask));
990        assert_eq!(result.message, Some("Exit plan mode?".to_string()));
991    }
992
993    #[tokio::test]
994    async fn test_enter_plan_mode_execution() {
995        let tool = EnterPlanModeTool::new();
996        let context = create_test_context();
997        let input = json!({});
998
999        // 确保不在计划模式中
1000        GLOBAL_STATE.set_plan_mode(false, None, None);
1001
1002        let result = tool.execute(input, &context).await.unwrap();
1003        assert!(result.success);
1004        assert!(result.output.is_some());
1005        assert!(result
1006            .output
1007            .as_ref()
1008            .unwrap()
1009            .contains("Entered plan mode"));
1010        assert!(!result.metadata.is_empty());
1011
1012        // 验证状态已更新
1013        assert!(GLOBAL_STATE.is_plan_mode_active());
1014    }
1015
1016    #[tokio::test]
1017    async fn test_exit_plan_mode_execution_not_in_plan_mode() {
1018        let tool = ExitPlanModeTool::new();
1019        let context = create_test_context();
1020        let input = json!({});
1021
1022        // 确保不在计划模式中
1023        GLOBAL_STATE.set_plan_mode(false, None, None);
1024
1025        let result = tool.execute(input, &context).await.unwrap();
1026        assert!(!result.success);
1027        assert!(result.error.is_some());
1028        assert!(result.error.as_ref().unwrap().contains("Not in plan mode"));
1029    }
1030
1031    #[tokio::test]
1032    async fn test_exit_plan_mode_execution_in_plan_mode() {
1033        let tool = ExitPlanModeTool::new();
1034        let context = create_test_context();
1035        let input = json!({});
1036
1037        // 设置计划模式
1038        GLOBAL_STATE.set_plan_mode(
1039            true,
1040            Some("test.md".to_string()),
1041            Some("test-id".to_string()),
1042        );
1043
1044        let result = tool.execute(input, &context).await.unwrap();
1045        assert!(result.success);
1046        assert!(result.output.is_some());
1047        assert!(result.output.as_ref().unwrap().contains("Exited plan mode"));
1048
1049        // 验证状态已更新
1050        assert!(!GLOBAL_STATE.is_plan_mode_active());
1051    }
1052
1053    #[test]
1054    fn test_plan_content_parsing() {
1055        let tool = ExitPlanModeTool::new();
1056        let content = r#"# Test Implementation Plan
1057
1058## Summary
1059
1060This is a test plan for implementing a new feature.
1061
1062### Functional Requirements
1063
1064- Requirement 1
1065- Requirement 2
1066
1067### Technical Constraints
1068
1069- Constraint 1
1070
1071### Step 1: Initial Setup
1072
1073Set up the basic structure.
1074
1075### Step 2: Implementation
1076
1077Implement the core functionality.
1078
1079### Critical Files
1080
1081- src/main.rs - Main entry point
1082- src/lib.rs - Core library
1083
1084## Risks
1085
1086### 1. Performance Risk
1087
1088The implementation might be slow.
1089
1090### 2. Security Risk
1091
1092Need to validate inputs properly.
1093"#;
1094
1095        let plan = tool.parse_plan_content("test-id", content);
1096
1097        assert_eq!(plan.metadata.title, "Test Implementation Plan");
1098        assert_eq!(
1099            plan.summary,
1100            "This is a test plan for implementing a new feature."
1101        );
1102        assert_eq!(plan.requirements_analysis.functional_requirements.len(), 2);
1103        assert_eq!(plan.requirements_analysis.technical_constraints.len(), 1);
1104        assert_eq!(plan.steps.len(), 2);
1105        assert_eq!(plan.critical_files.len(), 2);
1106        assert_eq!(plan.risks.len(), 2);
1107        assert_eq!(plan.content, content);
1108    }
1109
1110    #[test]
1111    fn test_tool_definitions() {
1112        let enter_tool = EnterPlanModeTool::new();
1113        let exit_tool = ExitPlanModeTool::new();
1114
1115        let enter_def = enter_tool.get_definition();
1116        let exit_def = exit_tool.get_definition();
1117
1118        assert_eq!(enter_def.name, "EnterPlanMode");
1119        assert_eq!(exit_def.name, "ExitPlanMode");
1120
1121        // 验证输入模式
1122        assert!(enter_def.input_schema.get("type").is_some());
1123        assert!(exit_def.input_schema.get("type").is_some());
1124    }
1125
1126    #[test]
1127    fn test_tool_options() {
1128        let enter_tool = EnterPlanModeTool::new();
1129        let exit_tool = ExitPlanModeTool::new();
1130
1131        let enter_options = enter_tool.options();
1132        let exit_options = exit_tool.options();
1133
1134        assert_eq!(enter_options.base_timeout, Duration::from_secs(30));
1135        assert_eq!(enter_options.max_retries, 0);
1136        assert_eq!(exit_options.base_timeout, Duration::from_secs(30));
1137        assert_eq!(exit_options.max_retries, 0);
1138    }
1139}