1use crate::agent::commands::{TokenUsage, SLASH_COMMANDS};
12use crate::agent::{AgentError, AgentResult, ProviderType};
13use crate::agent::ui::ansi;
14use crate::config::{load_agent_config, save_agent_config};
15use colored::Colorize;
16use std::io::{self, Write};
17use std::path::Path;
18
19const ROBOT: &str = "🤖";
20
21pub fn get_available_models(provider: ProviderType) -> Vec<(&'static str, &'static str)> {
23 match provider {
24 ProviderType::OpenAI => vec![
25 ("gpt-5.2", "GPT-5.2 - Latest reasoning model (Dec 2025)"),
26 ("gpt-5.2-mini", "GPT-5.2 Mini - Fast and affordable"),
27 ("gpt-4o", "GPT-4o - Multimodal workhorse"),
28 ("o1-preview", "o1-preview - Advanced reasoning"),
29 ],
30 ProviderType::Anthropic => vec![
31 ("claude-sonnet-4-20250514", "Claude 4 Sonnet - Latest (May 2025)"),
32 ("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet - Previous gen"),
33 ("claude-3-opus-latest", "Claude 3 Opus - Most capable"),
34 ("claude-3-haiku-latest", "Claude 3 Haiku - Fast and cheap"),
35 ],
36 }
37}
38
39pub struct ChatSession {
41 pub provider: ProviderType,
42 pub model: String,
43 pub project_path: std::path::PathBuf,
44 pub history: Vec<(String, String)>, pub token_usage: TokenUsage,
46}
47
48impl ChatSession {
49 pub fn new(project_path: &Path, provider: ProviderType, model: Option<String>) -> Self {
50 let default_model = match provider {
51 ProviderType::OpenAI => "gpt-5.2".to_string(),
52 ProviderType::Anthropic => "claude-sonnet-4-20250514".to_string(),
53 };
54
55 Self {
56 provider,
57 model: model.unwrap_or(default_model),
58 project_path: project_path.to_path_buf(),
59 history: Vec::new(),
60 token_usage: TokenUsage::new(),
61 }
62 }
63
64 pub fn has_api_key(provider: ProviderType) -> bool {
66 let env_key = match provider {
68 ProviderType::OpenAI => std::env::var("OPENAI_API_KEY").ok(),
69 ProviderType::Anthropic => std::env::var("ANTHROPIC_API_KEY").ok(),
70 };
71
72 if env_key.is_some() {
73 return true;
74 }
75
76 let agent_config = load_agent_config();
78 match provider {
79 ProviderType::OpenAI => agent_config.openai_api_key.is_some(),
80 ProviderType::Anthropic => agent_config.anthropic_api_key.is_some(),
81 }
82 }
83
84 pub fn load_api_key_to_env(provider: ProviderType) {
86 let env_var = match provider {
87 ProviderType::OpenAI => "OPENAI_API_KEY",
88 ProviderType::Anthropic => "ANTHROPIC_API_KEY",
89 };
90
91 if std::env::var(env_var).is_ok() {
93 return;
94 }
95
96 let agent_config = load_agent_config();
98 let key = match provider {
99 ProviderType::OpenAI => agent_config.openai_api_key,
100 ProviderType::Anthropic => agent_config.anthropic_api_key,
101 };
102
103 if let Some(key) = key {
104 unsafe {
106 std::env::set_var(env_var, &key);
107 }
108 }
109 }
110
111 pub fn get_configured_providers() -> Vec<ProviderType> {
113 let mut providers = Vec::new();
114 if Self::has_api_key(ProviderType::OpenAI) {
115 providers.push(ProviderType::OpenAI);
116 }
117 if Self::has_api_key(ProviderType::Anthropic) {
118 providers.push(ProviderType::Anthropic);
119 }
120 providers
121 }
122
123 pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
125 let env_var = match provider {
126 ProviderType::OpenAI => "OPENAI_API_KEY",
127 ProviderType::Anthropic => "ANTHROPIC_API_KEY",
128 };
129
130 println!("\n{}", format!("🔑 No API key found for {}", provider).yellow());
131 println!("Please enter your {} API key:", provider);
132 print!("> ");
133 io::stdout().flush().unwrap();
134
135 let mut key = String::new();
136 io::stdin().read_line(&mut key).map_err(|e| AgentError::ToolError(e.to_string()))?;
137 let key = key.trim().to_string();
138
139 if key.is_empty() {
140 return Err(AgentError::MissingApiKey(env_var.to_string()));
141 }
142
143 unsafe {
146 std::env::set_var(env_var, &key);
147 }
148
149 let mut agent_config = load_agent_config();
151 match provider {
152 ProviderType::OpenAI => agent_config.openai_api_key = Some(key.clone()),
153 ProviderType::Anthropic => agent_config.anthropic_api_key = Some(key.clone()),
154 }
155
156 if let Err(e) = save_agent_config(&agent_config) {
157 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
158 } else {
159 println!("{}", "✓ API key saved to ~/.syncable.toml".green());
160 }
161
162 Ok(key)
163 }
164
165 pub fn handle_model_command(&mut self) -> AgentResult<()> {
167 let models = get_available_models(self.provider);
168
169 println!("\n{}", format!("📋 Available models for {}:", self.provider).cyan().bold());
170 println!();
171
172 for (i, (id, desc)) in models.iter().enumerate() {
173 let marker = if *id == self.model { "→ " } else { " " };
174 let num = format!("[{}]", i + 1);
175 println!(" {} {} {} - {}", marker, num.dimmed(), id.white().bold(), desc.dimmed());
176 }
177
178 println!();
179 println!("Enter number to select, or press Enter to keep current:");
180 print!("> ");
181 io::stdout().flush().unwrap();
182
183 let mut input = String::new();
184 io::stdin().read_line(&mut input).ok();
185 let input = input.trim();
186
187 if input.is_empty() {
188 println!("{}", format!("Keeping model: {}", self.model).dimmed());
189 return Ok(());
190 }
191
192 if let Ok(num) = input.parse::<usize>() {
193 if num >= 1 && num <= models.len() {
194 let (id, desc) = models[num - 1];
195 self.model = id.to_string();
196 println!("{}", format!("✓ Switched to {} - {}", id, desc).green());
197 } else {
198 println!("{}", "Invalid selection".red());
199 }
200 } else {
201 self.model = input.to_string();
203 println!("{}", format!("✓ Set model to: {}", input).green());
204 }
205
206 Ok(())
207 }
208
209 pub fn handle_provider_command(&mut self) -> AgentResult<()> {
211 let providers = [ProviderType::OpenAI, ProviderType::Anthropic];
212
213 println!("\n{}", "🔄 Available providers:".cyan().bold());
214 println!();
215
216 for (i, provider) in providers.iter().enumerate() {
217 let marker = if *provider == self.provider { "→ " } else { " " };
218 let has_key = if Self::has_api_key(*provider) {
219 "✓ API key configured".green()
220 } else {
221 "⚠ No API key".yellow()
222 };
223 let num = format!("[{}]", i + 1);
224 println!(" {} {} {} - {}", marker, num.dimmed(), provider.to_string().white().bold(), has_key);
225 }
226
227 println!();
228 println!("Enter number to select:");
229 print!("> ");
230 io::stdout().flush().unwrap();
231
232 let mut input = String::new();
233 io::stdin().read_line(&mut input).ok();
234 let input = input.trim();
235
236 if let Ok(num) = input.parse::<usize>() {
237 if num >= 1 && num <= providers.len() {
238 let new_provider = providers[num - 1];
239
240 if !Self::has_api_key(new_provider) {
242 Self::prompt_api_key(new_provider)?;
243 }
244
245 self.provider = new_provider;
246
247 let default_model = match new_provider {
249 ProviderType::OpenAI => "gpt-5.2",
250 ProviderType::Anthropic => "claude-sonnet-4-20250514",
251 };
252 self.model = default_model.to_string();
253
254 println!("{}", format!("✓ Switched to {} with model {}", new_provider, default_model).green());
255 } else {
256 println!("{}", "Invalid selection".red());
257 }
258 }
259
260 Ok(())
261 }
262
263 pub fn print_help() {
265 println!();
266 println!(" {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET);
267 println!(" {}📖 Available Commands{}", ansi::PURPLE, ansi::RESET);
268 println!(" {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET);
269 println!();
270
271 for cmd in SLASH_COMMANDS.iter() {
272 let alias = cmd.alias.map(|a| format!(" ({})", a)).unwrap_or_default();
273 println!(" {}/{:<12}{}{} - {}{}{}",
274 ansi::CYAN, cmd.name, alias, ansi::RESET,
275 ansi::DIM, cmd.description, ansi::RESET
276 );
277 }
278
279 println!();
280 println!(" {}Tip: Type / to see interactive command picker!{}", ansi::DIM, ansi::RESET);
281 println!();
282 }
283
284
285 pub fn print_logo() {
287 let purple = "\x1b[38;5;141m"; let orange = "\x1b[38;5;216m"; let pink = "\x1b[38;5;212m"; let magenta = "\x1b[38;5;207m"; let reset = "\x1b[0m";
298
299 println!();
300 println!(
301 "{} ███████╗{}{} ██╗ ██╗{}{}███╗ ██╗{}{} ██████╗{}{} █████╗ {}{}██████╗ {}{}██╗ {}{}███████╗{}",
302 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
303 );
304 println!(
305 "{} ██╔════╝{}{} ╚██╗ ██╔╝{}{}████╗ ██║{}{} ██╔════╝{}{} ██╔══██╗{}{}██╔══██╗{}{}██║ {}{}██╔════╝{}",
306 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
307 );
308 println!(
309 "{} ███████╗{}{} ╚████╔╝ {}{}██╔██╗ ██║{}{} ██║ {}{} ███████║{}{}██████╔╝{}{}██║ {}{}█████╗ {}",
310 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
311 );
312 println!(
313 "{} ╚════██║{}{} ╚██╔╝ {}{}██║╚██╗██║{}{} ██║ {}{} ██╔══██║{}{}██╔══██╗{}{}██║ {}{}██╔══╝ {}",
314 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
315 );
316 println!(
317 "{} ███████║{}{} ██║ {}{}██║ ╚████║{}{} ╚██████╗{}{} ██║ ██║{}{}██████╔╝{}{}███████╗{}{}███████╗{}",
318 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
319 );
320 println!(
321 "{} ╚══════╝{}{} ╚═╝ {}{}╚═╝ ╚═══╝{}{} ╚═════╝{}{} ╚═╝ ╚═╝{}{}╚═════╝ {}{}╚══════╝{}{}╚══════╝{}",
322 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
323 );
324 println!();
325 }
326
327 pub fn print_banner(&self) {
329 Self::print_logo();
331
332 println!(
334 " {} {}",
335 "🚀".dimmed(),
336 "Want to deploy? Deploy instantly from Syncable Platform → https://syncable.dev".dimmed()
337 );
338 println!();
339
340 println!(
342 " {} {} powered by {}: {}",
343 ROBOT,
344 "Syncable Agent".white().bold(),
345 self.provider.to_string().cyan(),
346 self.model.cyan()
347 );
348 println!(
349 " {}",
350 "Your AI-powered code analysis assistant".dimmed()
351 );
352 println!();
353 println!(
354 " {} Type your questions. Use {} to exit.\n",
355 "→".cyan(),
356 "exit".yellow().bold()
357 );
358 }
359
360
361 pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
363 let cmd = input.trim().to_lowercase();
364
365 if cmd == "/" {
368 Self::print_help();
369 return Ok(true);
370 }
371
372 match cmd.as_str() {
373 "/exit" | "/quit" | "/q" => {
374 println!("\n{}", "👋 Goodbye!".green());
375 return Ok(false);
376 }
377 "/help" | "/h" | "/?" => {
378 Self::print_help();
379 }
380 "/model" | "/m" => {
381 self.handle_model_command()?;
382 }
383 "/provider" | "/p" => {
384 self.handle_provider_command()?;
385 }
386 "/cost" => {
387 self.token_usage.print_report(&self.model);
388 }
389 "/clear" | "/c" => {
390 self.history.clear();
391 println!("{}", "✓ Conversation history cleared".green());
392 }
393 _ => {
394 if cmd.starts_with('/') {
395 println!("{}", format!("Unknown command: {}. Type /help for available commands.", cmd).yellow());
397 }
398 }
399 }
400
401 Ok(true)
402 }
403
404 pub fn is_command(input: &str) -> bool {
406 input.trim().starts_with('/')
407 }
408
409 pub fn read_input(&self) -> io::Result<String> {
412 use crate::agent::ui::input::{read_input_with_file_picker, InputResult};
413
414 match read_input_with_file_picker("You:", &self.project_path) {
415 InputResult::Submit(text) => {
416 let trimmed = text.trim();
417 if trimmed.starts_with('/') && trimmed.contains(" ") {
420 if let Some(cmd) = trimmed.split_whitespace().next() {
422 return Ok(cmd.to_string());
423 }
424 }
425 Ok(trimmed.to_string())
426 }
427 InputResult::Cancel => Ok("exit".to_string()), InputResult::Exit => Ok("exit".to_string()),
429 }
430 }
431}