mermaid_cli/agents/
mode_aware_executor.rs1use super::action_executor::execute_action;
2use super::types::{ActionResult, AgentAction};
3use crate::tui::OperationMode;
4use anyhow::Result;
5
6#[derive(Debug, Clone)]
8pub struct ModeAwareExecutor {
9 mode: OperationMode,
10 bypass_confirmed: bool,
11}
12
13impl ModeAwareExecutor {
14 pub fn new(mode: OperationMode) -> Self {
16 Self {
17 mode,
18 bypass_confirmed: false,
19 }
20 }
21
22 pub fn set_mode(&mut self, mode: OperationMode) {
24 self.mode = mode;
25 self.bypass_confirmed = false; }
27
28 pub fn needs_confirmation(&self, action: &AgentAction) -> bool {
30 if self.mode.is_planning_only() {
32 return false;
33 }
34
35 match action {
36 AgentAction::WriteFile { .. } | AgentAction::DeleteFile { .. } => {
38 !self.mode.auto_accept_files()
39 },
40
41 AgentAction::ExecuteCommand { .. } => !self.mode.auto_accept_commands(),
43
44 AgentAction::GitCommit { .. } => !self.mode.auto_accept_git(),
46
47 AgentAction::ReadFile { .. } | AgentAction::GitStatus | AgentAction::GitDiff { .. } => {
49 false
50 },
51
52 AgentAction::CreateDirectory { .. } => !self.mode.auto_accept_files(),
54
55 AgentAction::WebSearch { .. } => false,
57
58 AgentAction::ParallelRead { .. }
60 | AgentAction::ParallelWebSearch { .. }
61 | AgentAction::ParallelGitDiff { .. } => false,
62 }
63 }
64
65 pub fn is_destructive(&self, action: &AgentAction) -> bool {
67 match action {
68 AgentAction::DeleteFile { .. } => true,
69 AgentAction::ExecuteCommand { command, .. } => {
70 command.contains("rm")
71 || command.contains("del")
72 || command.contains("drop")
73 || command.contains("truncate")
74 },
75 _ => false,
76 }
77 }
78
79 pub async fn execute(&mut self, action: AgentAction) -> Result<ActionResult> {
81 if self.mode.is_planning_only() {
83 return Ok(ActionResult::Success {
84 output: format!("[PLANNED]: {}", self.describe_action(&action)),
85 });
86 }
87
88 if self.mode == OperationMode::BypassAll && self.is_destructive(&action) {
90 if !self.bypass_confirmed {
91 self.bypass_confirmed = true;
92 return Ok(ActionResult::Success {
93 output: format!(
94 "[WARNING] DESTRUCTIVE OPERATION in Bypass Mode: {}\n\
95 Press Enter to confirm or Esc to cancel.",
96 self.describe_action(&action)
97 ),
98 });
99 }
100 }
101
102 let result = execute_action(&action).await?;
104
105 if self.bypass_confirmed {
107 self.bypass_confirmed = false;
108 }
109
110 if self.mode != OperationMode::Normal {
112 match result {
113 ActionResult::Success { output } => Ok(ActionResult::Success {
114 output: format!("[{}] {}", self.mode.short_name(), output),
115 }),
116 other => Ok(other),
117 }
118 } else {
119 Ok(result)
120 }
121 }
122
123 pub fn describe_action(&self, action: &AgentAction) -> String {
125 match action {
126 AgentAction::ReadFile { path } => {
127 format!("Read file: {}", path)
128 },
129 AgentAction::WriteFile { path, content } => {
130 format!("Write file: {} ({} bytes)", path, content.len())
131 },
132 AgentAction::DeleteFile { path } => {
133 format!("Delete file: {}", path)
134 },
135 AgentAction::CreateDirectory { path } => {
136 format!("Create directory: {}", path)
137 },
138 AgentAction::ExecuteCommand {
139 command,
140 working_dir,
141 } => {
142 if let Some(dir) = working_dir {
143 format!("Execute command in {}: {}", dir, command)
144 } else {
145 format!("Execute command: {}", command)
146 }
147 },
148 AgentAction::GitDiff { path } => {
149 if let Some(p) = path {
150 format!("Git diff for: {}", p)
151 } else {
152 format!("Git diff (all files)")
153 }
154 },
155 AgentAction::GitStatus => "Git status".to_string(),
156 AgentAction::GitCommit { message, files } => {
157 if !files.is_empty() {
158 format!("Git commit ({} files): {}", files.len(), message)
159 } else {
160 format!("Git commit (all): {}", message)
161 }
162 },
163 AgentAction::WebSearch { query, result_count } => {
164 format!("Web search: '{}' ({} results)", query, result_count)
165 },
166 AgentAction::ParallelRead { paths } => {
167 format!("Read {} files in parallel", paths.len())
168 },
169 AgentAction::ParallelWebSearch { queries } => {
170 format!("Search web with {} queries in parallel", queries.len())
171 },
172 AgentAction::ParallelGitDiff { paths } => {
173 format!("Git diff for {} paths in parallel", paths.len())
174 },
175 }
176 }
177
178 pub fn mode(&self) -> OperationMode {
180 self.mode
181 }
182
183 pub fn is_bypass_confirmed(&self) -> bool {
185 self.bypass_confirmed
186 }
187
188 pub fn reset_bypass_confirmation(&mut self) {
190 self.bypass_confirmed = false;
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_needs_confirmation() {
200 let executor = ModeAwareExecutor::new(OperationMode::Normal);
201
202 assert!(executor.needs_confirmation(&AgentAction::WriteFile {
204 path: "test.txt".to_string(),
205 content: "test".to_string(),
206 }));
207
208 assert!(!executor.needs_confirmation(&AgentAction::ReadFile {
210 path: "test.txt".to_string(),
211 }));
212 }
213
214 #[test]
215 fn test_accept_edits_mode() {
216 let executor = ModeAwareExecutor::new(OperationMode::AcceptEdits);
217
218 assert!(!executor.needs_confirmation(&AgentAction::WriteFile {
220 path: "test.txt".to_string(),
221 content: "test".to_string(),
222 }));
223
224 assert!(executor.needs_confirmation(&AgentAction::ExecuteCommand {
226 command: "ls".to_string(),
227 working_dir: None,
228 }));
229 }
230
231 #[test]
232 fn test_bypass_all_mode() {
233 let executor = ModeAwareExecutor::new(OperationMode::BypassAll);
234
235 assert!(!executor.needs_confirmation(&AgentAction::WriteFile {
237 path: "test.txt".to_string(),
238 content: "test".to_string(),
239 }));
240
241 assert!(!executor.needs_confirmation(&AgentAction::ExecuteCommand {
242 command: "ls".to_string(),
243 working_dir: None,
244 }));
245
246 assert!(!executor.needs_confirmation(&AgentAction::GitCommit {
247 message: "test".to_string(),
248 files: vec![],
249 }));
250 }
251
252 #[test]
253 fn test_destructive_detection() {
254 let executor = ModeAwareExecutor::new(OperationMode::Normal);
255
256 assert!(executor.is_destructive(&AgentAction::DeleteFile {
258 path: "test.txt".to_string(),
259 }));
260
261 assert!(executor.is_destructive(&AgentAction::ExecuteCommand {
263 command: "rm -rf /".to_string(),
264 working_dir: None,
265 }));
266
267 assert!(!executor.is_destructive(&AgentAction::ExecuteCommand {
269 command: "ls -la".to_string(),
270 working_dir: None,
271 }));
272 }
273}