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