1use koda_core::config::{KodaConfig, ProviderType};
10use koda_core::providers::LlmProvider;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13
14pub enum ReplAction {
16 Quit,
17 SwitchModel(String),
18 PickModel,
19 SetupProvider(ProviderType, String), PickProvider,
21 ShowHelp,
22 ListSessions,
23 ResumeSession(String),
24 DeleteSession(String),
25 InjectPrompt(String),
27 Compact,
29 Purge(Option<String>),
31 Expand(usize),
33 Verbose(Option<bool>),
35 ListAgents,
37 ShowDiff,
39 MemoryCommand(Option<String>),
41 Undo,
43 ListSkills(Option<String>),
45 ManageKeys,
47 #[allow(dead_code)]
48 Handled,
49 NotACommand,
50}
51
52pub async fn handle_command(
54 input: &str,
55 _config: &KodaConfig,
56 _provider: &Arc<RwLock<Box<dyn LlmProvider>>>,
57) -> ReplAction {
58 let parts: Vec<&str> = input.splitn(2, ' ').collect();
59 let cmd = parts[0];
60 let arg = parts.get(1).map(|s| s.trim());
61
62 match cmd {
63 "/exit" => ReplAction::Quit,
64
65 "/model" => match arg {
66 Some(model) => ReplAction::SwitchModel(model.to_string()),
67 None => ReplAction::PickModel,
68 },
69
70 "/provider" => match arg {
71 Some(name) => {
72 let ptype = ProviderType::from_url_or_name("", Some(name));
73 let base_url = ptype.default_base_url().to_string();
74 ReplAction::SetupProvider(ptype, base_url)
75 }
76 None => ReplAction::PickProvider,
77 },
78
79 "/help" => ReplAction::ShowHelp,
80
81 "/diff" => match arg {
82 Some("review") => {
83 let full_diff = get_git_diff();
84 ReplAction::InjectPrompt(format!(
85 "Review these uncommitted changes. Point out bugs, improvements, and concerns:\n\n```diff\n{full_diff}\n```"
86 ))
87 }
88 Some("commit") => {
89 let full_diff = get_git_diff();
90 ReplAction::InjectPrompt(format!(
91 "Write a conventional commit message for these changes. Use the format: type: description\n\nInclude a body with bullet points for each logical change.\n\n```diff\n{full_diff}\n```"
92 ))
93 }
94 _ => ReplAction::ShowDiff,
95 },
96
97 "/compact" => ReplAction::Compact,
98 "/purge" => ReplAction::Purge(arg.map(|s| s.to_string())),
99
100 "/expand" => {
101 let n: usize = arg.and_then(|s| s.parse().ok()).unwrap_or(1);
102 ReplAction::Expand(n)
103 }
104
105 "/verbose" => match arg {
106 Some("on") => ReplAction::Verbose(Some(true)),
107 Some("off") => ReplAction::Verbose(Some(false)),
108 _ => ReplAction::Verbose(None), },
110
111 "/agent" => ReplAction::ListAgents,
112
113 "/sessions" => match arg {
114 Some(sub) if sub.starts_with("delete ") => {
115 let id = sub.strip_prefix("delete ").unwrap().trim().to_string();
116 ReplAction::DeleteSession(id)
117 }
118 Some(sub) if sub.starts_with("resume ") => {
119 let id = sub.strip_prefix("resume ").unwrap().trim().to_string();
120 ReplAction::ResumeSession(id)
121 }
122 Some(id) if !id.is_empty() && id.chars().all(|c| c.is_ascii_hexdigit() || c == '-') => {
124 ReplAction::ResumeSession(id.to_string())
125 }
126 _ => ReplAction::ListSessions,
127 },
128
129 "/memory" => ReplAction::MemoryCommand(arg.map(|s| s.to_string())),
130
131 "/undo" => ReplAction::Undo,
132
133 "/skills" => ReplAction::ListSkills(arg.map(|s| s.to_string())),
134
135 "/key" | "/keys" => ReplAction::ManageKeys,
136
137 _ => ReplAction::NotACommand,
138 }
139}
140
141pub const PROVIDERS: &[(&str, &str)] = &[
146 ("lmstudio", "LM Studio"),
147 ("ollama", "Ollama"),
148 ("openai", "OpenAI"),
149 ("anthropic", "Anthropic"),
150 ("deepseek", "DeepSeek"),
151 ("gemini", "Google Gemini"),
152 ("groq", "Groq"),
153 ("grok", "Grok (xAI)"),
154 ("mistral", "Mistral"),
155 ("minimax", "MiniMax"),
156 ("openrouter", "OpenRouter"),
157 ("together", "Together"),
158 ("fireworks", "Fireworks"),
159 ("vllm", "vLLM"),
160];
161
162fn get_git_diff() -> String {
164 const MAX_DIFF_CHARS: usize = 30_000;
165
166 let unstaged = std::process::Command::new("git")
167 .args(["diff"])
168 .output()
169 .ok()
170 .filter(|o| o.status.success())
171 .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
172 .unwrap_or_default();
173
174 let staged = std::process::Command::new("git")
175 .args(["diff", "--cached"])
176 .output()
177 .ok()
178 .filter(|o| o.status.success())
179 .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
180 .unwrap_or_default();
181
182 let mut diff = String::new();
183 if !unstaged.is_empty() {
184 diff.push_str(&unstaged);
185 }
186 if !staged.is_empty() {
187 if !diff.is_empty() {
188 diff.push_str("\n# --- Staged changes ---\n\n");
189 }
190 diff.push_str(&staged);
191 }
192
193 if diff.len() > MAX_DIFF_CHARS {
194 let mut end = MAX_DIFF_CHARS;
195 while end > 0 && !diff.is_char_boundary(end) {
196 end -= 1;
197 }
198 format!(
199 "{}\n\n[TRUNCATED: diff was {} chars, showing first {}]",
200 &diff[..end],
201 diff.len(),
202 MAX_DIFF_CHARS
203 )
204 } else {
205 diff
206 }
207}