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 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}