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}