Skip to main content

deepseek_rust_cli/agent/
commands.rs

1use std::fs;
2
3use anyhow::Result;
4
5use crate::agent::{agent::DeepSeekAgent, history::load_history};
6
7pub async fn process_command(agent: &mut DeepSeekAgent, text: &str) -> Result<Option<String>> {
8    let parts: Vec<&str> = text.split_whitespace().collect();
9    if parts.is_empty() {
10        return Ok(None);
11    }
12
13    let cmd = parts[0].to_lowercase();
14    match cmd.as_str() {
15        "/model" => {
16            if parts.len() > 1 {
17                agent.model = parts[1].to_string();
18                agent.config.model = agent.model.clone();
19                let _ = agent.config.save();
20                Ok(Some(format!("Model switched to {}", agent.model)))
21            } else {
22                Ok(Some(format!("Current model: {}", agent.model)))
23            }
24        }
25        "/thinking" => {
26            if parts.len() > 1 {
27                match parts[1].to_lowercase().as_str() {
28                    "on" | "enable" | "enabled" | "true" | "1" => {
29                        agent.config.thinking_enabled = true;
30                        let _ = agent.config.save();
31                        return Ok(Some("Thinking mode enabled.".to_string()));
32                    }
33                    "off" | "disable" | "disabled" | "false" | "0" => {
34                        agent.config.thinking_enabled = false;
35                        let _ = agent.config.save();
36                        return Ok(Some("Thinking mode disabled.".to_string()));
37                    }
38                    "high" | "max" => {
39                        agent.config.reasoning_effort = Some(parts[1].to_string());
40                        let _ = agent.config.save();
41                        return Ok(Some(format!("Reasoning effort set to {}", parts[1])));
42                    }
43                    _ => return Ok(Some("Usage /thinking [on|off|high|max]".to_string())),
44                }
45            }
46            let status = if agent.config.thinking_enabled {
47                "enabled"
48            } else {
49                "disabled"
50            };
51            let effort = agent.config.reasoning_effort.as_deref().unwrap_or("high");
52            Ok(Some(format!(
53                "Thinking mode: {}, effort: {}",
54                status, effort
55            )))
56        }
57        "/clear" => {
58            agent.messages.truncate(1);
59            agent.save();
60            Ok(Some("History cleared.".to_string()))
61        }
62        "/forget" => {
63            agent.messages.truncate(1);
64            let path = crate::agent::history::get_history_path(&agent.session_id);
65            let _ = fs::remove_file(path);
66            Ok(Some(
67                "Session history forgotten and deleted from disk.".to_string(),
68            ))
69        }
70        "/undo" => Ok(Some(agent.undo())),
71        "/tokens" => Ok(Some(format!(
72            "Token Usage: {} prompt, {} completion (Total: {})",
73            agent.token_usage.prompt_tokens,
74            agent.token_usage.completion_tokens,
75            agent.token_usage.prompt_tokens + agent.token_usage.completion_tokens
76        ))),
77        "/temperature" => {
78            if parts.len() > 1 {
79                if let Ok(temp) = parts[1].parse::<f32>() {
80                    agent.config.temperature = temp;
81                    let _ = agent.config.save();
82                    Ok(Some(format!("Temperature set to {}", temp)))
83                } else {
84                    Ok(Some("Invalid temperature value.".to_string()))
85                }
86            } else {
87                Ok(Some(format!(
88                    "Current temperature: {}",
89                    agent.config.temperature
90                )))
91            }
92        }
93        "/auto" => {
94            agent.auto_approve = !agent.auto_approve;
95            let status = if agent.auto_approve {
96                "enabled"
97            } else {
98                "disabled"
99            };
100            Ok(Some(format!("Auto-approve is now {}", status)))
101        }
102        "/info" => {
103            let info = format!(
104                "Session ID: {}\nModel: {}\nTemperature: {}\nAuto-approve: {}\nHistory length: {} \
105                 messages\nTokens: P:{} C:{} T:{}",
106                agent.session_id,
107                agent.model,
108                agent.config.temperature,
109                agent.auto_approve,
110                agent.messages.len(),
111                agent.token_usage.prompt_tokens,
112                agent.token_usage.completion_tokens,
113                agent.token_usage.prompt_tokens + agent.token_usage.completion_tokens
114            );
115            Ok(Some(info))
116        }
117        "/sessions" => {
118            let mut sessions = Vec::new();
119            if let Ok(entries) = fs::read_dir(".deep/history") {
120                for entry in entries.flatten() {
121                    if let Some(name) = entry.file_name().to_str().filter(|n| n.ends_with(".json"))
122                    {
123                        sessions.push(name.trim_end_matches(".json").to_string());
124                    }
125                }
126            }
127            if sessions.is_empty() {
128                Ok(Some("No saved sessions found.".to_string()))
129            } else {
130                Ok(Some(format!(
131                    "Available sessions:\n- {}",
132                    sessions.join("\n- ")
133                )))
134            }
135        }
136        "/resume" => {
137            if parts.len() > 1 {
138                let new_sid = parts[1].to_string();
139                agent.session_id = new_sid.clone();
140                agent.messages = load_history(&new_sid);
141                if agent.messages.is_empty() {
142                    let full_sys = format!(
143                        "{}\n{}",
144                        agent.config.system_prompt,
145                        crate::agent::context::get_project_context()
146                    );
147                    agent.messages.push(crate::api::types::Message {
148                        role: "system".to_string(),
149                        content: Some(full_sys),
150                        reasoning_content: None,
151                        tool_calls: None,
152                        tool_call_id: None,
153                    });
154                    agent.save();
155                    Ok(Some(format!(
156                        "Session {} not found, started new session with system prompt.",
157                        new_sid
158                    )))
159                } else {
160                    Ok(Some(format!("Resumed session: {}", new_sid)))
161                }
162            } else {
163                Ok(Some("Usage: /resume <session_id>".to_string()))
164            }
165        }
166        "/savemem" => {
167            if parts.len() > 1 {
168                let note = text.trim_start_matches("/savemem").trim();
169                let mut memory = fs::read_to_string(".deep/memory.md").unwrap_or_default();
170                memory.push_str(&format!("\n- {}\n", note));
171                fs::write(".deep/memory.md", memory)?;
172                Ok(Some("Note saved to memory.md".to_string()))
173            } else {
174                Ok(Some("Usage: /savemem <note content>".to_string()))
175            }
176        }
177        "/export" => {
178            let mut export = format!(
179                "# DeepSeek Session Export\n\n- **Session:** {}\n- **Model:** {}\n\n---\n\n",
180                agent.session_id, agent.model
181            );
182            for msg in &agent.messages {
183                export.push_str(&format!(
184                    "### {}\n{}\n\n",
185                    msg.role.to_uppercase(),
186                    msg.content.as_deref().unwrap_or("(No content)")
187                ));
188            }
189            // Sanitize session_id to prevent directory traversal in filename
190            let safe_sid: String = agent
191                .session_id
192                .chars()
193                .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
194                .collect();
195            let filename = format!("export_{}.md", safe_sid);
196            fs::write(&filename, export)?;
197            Ok(Some(format!("Session exported to {}", filename)))
198        }
199        "/retry" => Ok(Some("RETRY".to_string())),
200        "/config" => {
201            if parts.len() > 1 {
202                let key = parts[1].to_lowercase();
203                if parts.len() > 2 {
204                    let val = parts[2];
205                    match key.as_str() {
206                        "model" => agent.config.model = val.to_string(),
207                        "url" | "base_url" => agent.config.base_url = val.to_string(),
208                        "temp" | "temperature" => {
209                            if let Ok(v) = val.parse() {
210                                agent.config.temperature = v
211                            } else {
212                                return Ok(Some("Invalid float".into()));
213                            }
214                        }
215                        "top_p" => {
216                            if let Ok(v) = val.parse() {
217                                agent.config.top_p = v
218                            } else {
219                                return Ok(Some("Invalid float".into()));
220                            }
221                        }
222                        "presence" | "presence_penalty" => {
223                            if let Ok(v) = val.parse() {
224                                agent.config.presence_penalty = v
225                            } else {
226                                return Ok(Some("Invalid float".into()));
227                            }
228                        }
229                        "frequency" | "frequency_penalty" => {
230                            if let Ok(v) = val.parse() {
231                                agent.config.frequency_penalty = v
232                            } else {
233                                return Ok(Some("Invalid float".into()));
234                            }
235                        }
236                        "max_tokens" => {
237                            if let Ok(v) = val.parse() {
238                                agent.config.max_tokens = v
239                            } else {
240                                return Ok(Some("Invalid integer".into()));
241                            }
242                        }
243                        "max_iterations" => {
244                            if let Ok(v) = val.parse() {
245                                agent.config.max_iterations = v
246                            } else {
247                                return Ok(Some("Invalid integer".into()));
248                            }
249                        }
250                        "max_context_chars" | "context_chars" | "max_context" => {
251                            if let Ok(v) = val.parse() {
252                                agent.config.max_context_chars = v
253                            } else {
254                                return Ok(Some("Invalid integer".into()));
255                            }
256                        }
257                        "max_tool_output_chars" | "tool_output_chars" | "max_tool_output" => {
258                            if let Ok(v) = val.parse() {
259                                agent.config.max_tool_output_chars = v
260                            } else {
261                                return Ok(Some("Invalid integer".into()));
262                            }
263                        }
264                        "tokens" | "show_token_usage" => {
265                            agent.config.show_token_usage =
266                                val.to_lowercase() == "true" || val == "1"
267                        }
268                        "short" | "concise_reasoning" => {
269                            agent.config.concise_reasoning =
270                                val.to_lowercase() == "true" || val == "1"
271                        }
272                        "debug" => agent.config.debug = val.to_lowercase() == "true" || val == "1",
273                        _ => return Ok(Some(format!("Unknown config key: {}", key))),
274                    }
275                    let _ = agent.config.save();
276                    Ok(Some(format!("Config {} set to {}", key, val)))
277                } else {
278                    let val = match key.as_str() {
279                        "model" => agent.config.model.clone(),
280                        "url" | "base_url" => agent.config.base_url.clone(),
281                        "temp" | "temperature" => agent.config.temperature.to_string(),
282                        "top_p" => agent.config.top_p.to_string(),
283                        "presence" | "presence_penalty" => {
284                            agent.config.presence_penalty.to_string()
285                        }
286                        "frequency" | "frequency_penalty" => {
287                            agent.config.frequency_penalty.to_string()
288                        }
289                        "max_tokens" => agent.config.max_tokens.to_string(),
290                        "max_iterations" => agent.config.max_iterations.to_string(),
291                        "max_context_chars" | "context_chars" | "max_context" => {
292                            agent.config.max_context_chars.to_string()
293                        }
294                        "max_tool_output_chars" | "tool_output_chars" | "max_tool_output" => {
295                            agent.config.max_tool_output_chars.to_string()
296                        }
297                        "tokens" | "show_token_usage" => agent.config.show_token_usage.to_string(),
298                        "short" | "concise_reasoning" => agent.config.concise_reasoning.to_string(),
299                        "debug" => agent.config.debug.to_string(),
300                        _ => format!("Unknown config key: {}", key),
301                    };
302                    Ok(Some(format!("{} = {}", key, val)))
303                }
304            } else {
305                let conf = format!(
306                    "Current Configuration:\n- model: {}\n- base_url: {}\n- temperature: {}\n- \
307                     top_p: {}\n- presence_penalty: {}\n- frequency_penalty: {}\n- max_tokens: \
308                     {}\n- max_iterations: {}\n- max_context_chars: {}\n- max_tool_output_chars: \
309                     {}\n- show_token_usage: {}\n- concise_reasoning: {}\n- debug: {}",
310                    agent.config.model,
311                    agent.config.base_url,
312                    agent.config.temperature,
313                    agent.config.top_p,
314                    agent.config.presence_penalty,
315                    agent.config.frequency_penalty,
316                    agent.config.max_tokens,
317                    agent.config.max_iterations,
318                    agent.config.max_context_chars,
319                    agent.config.max_tool_output_chars,
320                    agent.config.show_token_usage,
321                    agent.config.concise_reasoning,
322                    agent.config.debug
323                );
324                Ok(Some(conf))
325            }
326        }
327        "/help" => {
328            let help = r#"
329Available Commands:
330  /model [name]    - Show or switch current model (v4-flash, v4-pro)
331  /thinking [on|off|high|max] - Toggle thinking mode or set reasoning effort
332  /clear           - Clear current conversation history
333  /forget          - Delete session history from disk
334  /undo            - Undo last file/shell operation
335  /tokens          - Show current session token usage
336  /temperature [v] - Show or set model temperature
337  /auto            - Toggle auto-approval for tools
338  /info            - Show detailed session info
339  /sessions        - List all saved sessions
340  /resume <id>     - Switch to a different session
341  /savemem <text>  - Save a note to memory.md
342  /export          - Export session to a Markdown file
343  /retry           - Regenerate last assistant response
344  /config          - Show or set configuration values
345  /update          - Check for and install updates
346  /help            - Show this help message
347  /exit, /quit     - Exit the application (also 'exit' or 'quit')
348"#;
349            Ok(Some(help.trim().to_string()))
350        }
351        "/update" => crate::updater::run_update().map(Some),
352        _ => {
353            if cmd.starts_with('/') {
354                Ok(Some(suggest_command(&cmd)))
355            } else {
356                Ok(None)
357            }
358        }
359    }
360}
361
362const COMMANDS: &[&str] = &[
363    "/model",
364    "/thinking",
365    "/clear",
366    "/forget",
367    "/undo",
368    "/tokens",
369    "/temperature",
370    "/auto",
371    "/info",
372    "/sessions",
373    "/resume",
374    "/savemem",
375    "/export",
376    "/retry",
377    "/config",
378    "/update",
379    "/help",
380    "/exit",
381    "/quit",
382];
383
384fn levenshtein_distance(a: &str, b: &str) -> usize {
385    let a_chars: Vec<char> = a.chars().collect();
386    let b_chars: Vec<char> = b.chars().collect();
387    let len_a = a_chars.len();
388    let len_b = b_chars.len();
389
390    let mut row: Vec<usize> = (0..=len_b).collect();
391    for i in 1..=len_a {
392        let mut prev_diag = row[0];
393        row[0] = i;
394        for j in 1..=len_b {
395            let temp = row[j];
396            if a_chars[i - 1] == b_chars[j - 1] {
397                row[j] = prev_diag;
398            } else {
399                row[j] = 1 + std::cmp::min(row[j], std::cmp::min(row[j - 1], prev_diag));
400            }
401            prev_diag = temp;
402        }
403    }
404    row[len_b]
405}
406
407fn suggest_command(cmd: &str) -> String {
408    let mut best_match = None;
409    let mut best_dist = usize::MAX;
410
411    for &c in COMMANDS {
412        let dist = levenshtein_distance(cmd, c);
413        if dist < best_dist {
414            best_dist = dist;
415            best_match = Some(c);
416        }
417    }
418
419    if let Some(m) = best_match {
420        if best_dist <= 3 {
421            return format!("❌ Unknown command: {}. Did you mean `{}`?", cmd, m);
422        }
423    }
424    format!(
425        "❌ Unknown command: {}. Type `/help` to see available commands.",
426        cmd
427    )
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_levenshtein_distance() {
436        assert_eq!(levenshtein_distance("cat", "cat"), 0);
437        assert_eq!(levenshtein_distance("cat", "cut"), 1);
438        assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
439    }
440
441    #[test]
442    fn test_suggest_command() {
443        assert!(suggest_command("/toke").contains("Did you mean `/tokens`?"));
444        assert!(suggest_command("/conf").contains("Did you mean `/config`?"));
445        assert!(suggest_command("/abcdef").contains("Type `/help` to see available commands"));
446    }
447}