Skip to main content

deepseek_rust_cli/agent/
commands.rs

1use std::{fs, path::PathBuf};
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 = PathBuf::from(".deep/history").join(format!("{}.json", 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            let filename = format!("export_{}.md", agent.session_id);
190            fs::write(&filename, export)?;
191            Ok(Some(format!("Session exported to {}", filename)))
192        }
193        "/retry" => Ok(Some("RETRY".to_string())),
194        "/config" => {
195            if parts.len() > 1 {
196                let key = parts[1].to_lowercase();
197                if parts.len() > 2 {
198                    let val = parts[2];
199                    match key.as_str() {
200                        "model" => agent.config.model = val.to_string(),
201                        "url" | "base_url" => agent.config.base_url = val.to_string(),
202                        "temp" | "temperature" => {
203                            if let Ok(v) = val.parse() {
204                                agent.config.temperature = v
205                            } else {
206                                return Ok(Some("Invalid float".into()));
207                            }
208                        }
209                        "top_p" => {
210                            if let Ok(v) = val.parse() {
211                                agent.config.top_p = v
212                            } else {
213                                return Ok(Some("Invalid float".into()));
214                            }
215                        }
216                        "presence" | "presence_penalty" => {
217                            if let Ok(v) = val.parse() {
218                                agent.config.presence_penalty = v
219                            } else {
220                                return Ok(Some("Invalid float".into()));
221                            }
222                        }
223                        "frequency" | "frequency_penalty" => {
224                            if let Ok(v) = val.parse() {
225                                agent.config.frequency_penalty = v
226                            } else {
227                                return Ok(Some("Invalid float".into()));
228                            }
229                        }
230                        "max_tokens" => {
231                            if let Ok(v) = val.parse() {
232                                agent.config.max_tokens = v
233                            } else {
234                                return Ok(Some("Invalid integer".into()));
235                            }
236                        }
237                        "max_iterations" => {
238                            if let Ok(v) = val.parse() {
239                                agent.config.max_iterations = v
240                            } else {
241                                return Ok(Some("Invalid integer".into()));
242                            }
243                        }
244                        "max_context_chars" | "context_chars" | "max_context" => {
245                            if let Ok(v) = val.parse() {
246                                agent.config.max_context_chars = v
247                            } else {
248                                return Ok(Some("Invalid integer".into()));
249                            }
250                        }
251                        "max_tool_output_chars" | "tool_output_chars" | "max_tool_output" => {
252                            if let Ok(v) = val.parse() {
253                                agent.config.max_tool_output_chars = v
254                            } else {
255                                return Ok(Some("Invalid integer".into()));
256                            }
257                        }
258                        "tokens" | "show_token_usage" => {
259                            agent.config.show_token_usage =
260                                val.to_lowercase() == "true" || val == "1"
261                        }
262                        "short" | "concise_reasoning" => {
263                            agent.config.concise_reasoning =
264                                val.to_lowercase() == "true" || val == "1"
265                        }
266                        "debug" => agent.config.debug = val.to_lowercase() == "true" || val == "1",
267                        _ => return Ok(Some(format!("Unknown config key: {}", key))),
268                    }
269                    let _ = agent.config.save();
270                    Ok(Some(format!("Config {} set to {}", key, val)))
271                } else {
272                    let val = match key.as_str() {
273                        "model" => agent.config.model.clone(),
274                        "url" | "base_url" => agent.config.base_url.clone(),
275                        "temp" | "temperature" => agent.config.temperature.to_string(),
276                        "top_p" => agent.config.top_p.to_string(),
277                        "presence" | "presence_penalty" => {
278                            agent.config.presence_penalty.to_string()
279                        }
280                        "frequency" | "frequency_penalty" => {
281                            agent.config.frequency_penalty.to_string()
282                        }
283                        "max_tokens" => agent.config.max_tokens.to_string(),
284                        "max_iterations" => agent.config.max_iterations.to_string(),
285                        "max_context_chars" | "context_chars" | "max_context" => {
286                            agent.config.max_context_chars.to_string()
287                        }
288                        "max_tool_output_chars" | "tool_output_chars" | "max_tool_output" => {
289                            agent.config.max_tool_output_chars.to_string()
290                        }
291                        "tokens" | "show_token_usage" => agent.config.show_token_usage.to_string(),
292                        "short" | "concise_reasoning" => agent.config.concise_reasoning.to_string(),
293                        "debug" => agent.config.debug.to_string(),
294                        _ => format!("Unknown config key: {}", key),
295                    };
296                    Ok(Some(format!("{} = {}", key, val)))
297                }
298            } else {
299                let conf = format!(
300                    "Current Configuration:\n- model: {}\n- base_url: {}\n- temperature: {}\n- \
301                     top_p: {}\n- presence_penalty: {}\n- frequency_penalty: {}\n- max_tokens: \
302                     {}\n- max_iterations: {}\n- max_context_chars: {}\n- max_tool_output_chars: \
303                     {}\n- show_token_usage: {}\n- concise_reasoning: {}\n- debug: {}",
304                    agent.config.model,
305                    agent.config.base_url,
306                    agent.config.temperature,
307                    agent.config.top_p,
308                    agent.config.presence_penalty,
309                    agent.config.frequency_penalty,
310                    agent.config.max_tokens,
311                    agent.config.max_iterations,
312                    agent.config.max_context_chars,
313                    agent.config.max_tool_output_chars,
314                    agent.config.show_token_usage,
315                    agent.config.concise_reasoning,
316                    agent.config.debug
317                );
318                Ok(Some(conf))
319            }
320        }
321        "/help" => {
322            let help = r#"
323Available Commands:
324  /model [name]    - Show or switch current model (v4-flash, v4-pro)
325  /thinking [on|off|high|max] - Toggle thinking mode or set reasoning effort
326  /clear           - Clear current conversation history
327  /forget          - Delete session history from disk
328  /undo            - Undo last file/shell operation
329  /tokens          - Show current session token usage
330  /temperature [v] - Show or set model temperature
331  /auto            - Toggle auto-approval for tools
332  /info            - Show detailed session info
333  /sessions        - List all saved sessions
334  /resume <id>     - Switch to a different session
335  /savemem <text>  - Save a note to memory.md
336  /export          - Export session to a Markdown file
337  /retry           - Regenerate last assistant response
338  /config          - Show or set configuration values
339  /update          - Check for and install updates
340  /help            - Show this help message
341  /exit, /quit     - Exit the application (also 'exit' or 'quit')
342"#;
343            Ok(Some(help.trim().to_string()))
344        }
345        "/update" => crate::updater::run_update().map(Some),
346        _ => {
347            if cmd.starts_with('/') {
348                Ok(Some(suggest_command(&cmd)))
349            } else {
350                Ok(None)
351            }
352        }
353    }
354}
355
356const COMMANDS: &[&str] = &[
357    "/model",
358    "/thinking",
359    "/clear",
360    "/forget",
361    "/undo",
362    "/tokens",
363    "/temperature",
364    "/auto",
365    "/info",
366    "/sessions",
367    "/resume",
368    "/savemem",
369    "/export",
370    "/retry",
371    "/config",
372    "/update",
373    "/help",
374    "/exit",
375    "/quit",
376];
377
378fn levenshtein_distance(a: &str, b: &str) -> usize {
379    let a_chars: Vec<char> = a.chars().collect();
380    let b_chars: Vec<char> = b.chars().collect();
381    let len_a = a_chars.len();
382    let len_b = b_chars.len();
383
384    let mut row: Vec<usize> = (0..=len_b).collect();
385    for i in 1..=len_a {
386        let mut prev_diag = row[0];
387        row[0] = i;
388        for j in 1..=len_b {
389            let temp = row[j];
390            if a_chars[i - 1] == b_chars[j - 1] {
391                row[j] = prev_diag;
392            } else {
393                row[j] = 1 + std::cmp::min(row[j], std::cmp::min(row[j - 1], prev_diag));
394            }
395            prev_diag = temp;
396        }
397    }
398    row[len_b]
399}
400
401fn suggest_command(cmd: &str) -> String {
402    let mut best_match = None;
403    let mut best_dist = usize::MAX;
404
405    for &c in COMMANDS {
406        let dist = levenshtein_distance(cmd, c);
407        if dist < best_dist {
408            best_dist = dist;
409            best_match = Some(c);
410        }
411    }
412
413    if let Some(m) = best_match {
414        if best_dist <= 3 {
415            return format!("❌ Unknown command: {}. Did you mean `{}`?", cmd, m);
416        }
417    }
418    format!(
419        "❌ Unknown command: {}. Type `/help` to see available commands.",
420        cmd
421    )
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn test_levenshtein_distance() {
430        assert_eq!(levenshtein_distance("cat", "cat"), 0);
431        assert_eq!(levenshtein_distance("cat", "cut"), 1);
432        assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
433    }
434
435    #[test]
436    fn test_suggest_command() {
437        assert!(suggest_command("/toke").contains("Did you mean `/tokens`?"));
438        assert!(suggest_command("/conf").contains("Did you mean `/config`?"));
439        assert!(suggest_command("/abcdef").contains("Type `/help` to see available commands"));
440    }
441}