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