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-opus-4-5-20251101", "Claude Opus 4.5 - Most capable (Nov 2025)"),
32 ("claude-sonnet-4-5-20250929", "Claude Sonnet 4.5 - Balanced (Sep 2025)"),
33 ("claude-haiku-4-5-20251001", "Claude Haiku 4.5 - Fast (Oct 2025)"),
34 ("claude-sonnet-4-20250514", "Claude Sonnet 4 - Previous gen"),
35 ],
36 ProviderType::Bedrock => vec![
38 ("global.anthropic.claude-opus-4-5-20251101-v1:0", "Claude Opus 4.5 - Most capable (Nov 2025)"),
39 ("global.anthropic.claude-sonnet-4-5-20250929-v1:0", "Claude Sonnet 4.5 - Balanced (Sep 2025)"),
40 ("global.anthropic.claude-haiku-4-5-20251001-v1:0", "Claude Haiku 4.5 - Fast (Oct 2025)"),
41 ("global.anthropic.claude-sonnet-4-20250514-v1:0", "Claude Sonnet 4 - Previous gen"),
42 ],
43 }
44}
45
46pub struct ChatSession {
48 pub provider: ProviderType,
49 pub model: String,
50 pub project_path: std::path::PathBuf,
51 pub history: Vec<(String, String)>, pub token_usage: TokenUsage,
53}
54
55impl ChatSession {
56 pub fn new(project_path: &Path, provider: ProviderType, model: Option<String>) -> Self {
57 let default_model = match provider {
58 ProviderType::OpenAI => "gpt-5.2".to_string(),
59 ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
60 ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(),
61 };
62
63 Self {
64 provider,
65 model: model.unwrap_or(default_model),
66 project_path: project_path.to_path_buf(),
67 history: Vec::new(),
68 token_usage: TokenUsage::new(),
69 }
70 }
71
72 pub fn has_api_key(provider: ProviderType) -> bool {
74 let env_key = match provider {
76 ProviderType::OpenAI => std::env::var("OPENAI_API_KEY").ok(),
77 ProviderType::Anthropic => std::env::var("ANTHROPIC_API_KEY").ok(),
78 ProviderType::Bedrock => {
79 if std::env::var("AWS_ACCESS_KEY_ID").is_ok() && std::env::var("AWS_SECRET_ACCESS_KEY").is_ok() {
81 return true;
82 }
83 if std::env::var("AWS_PROFILE").is_ok() {
84 return true;
85 }
86 None
87 }
88 };
89
90 if env_key.is_some() {
91 return true;
92 }
93
94 let agent_config = load_agent_config();
96
97 if let Some(profile_name) = &agent_config.active_profile {
99 if let Some(profile) = agent_config.profiles.get(profile_name) {
100 match provider {
101 ProviderType::OpenAI => {
102 if profile.openai.as_ref().map(|o| !o.api_key.is_empty()).unwrap_or(false) {
103 return true;
104 }
105 }
106 ProviderType::Anthropic => {
107 if profile.anthropic.as_ref().map(|a| !a.api_key.is_empty()).unwrap_or(false) {
108 return true;
109 }
110 }
111 ProviderType::Bedrock => {
112 if let Some(bedrock) = &profile.bedrock {
113 if bedrock.profile.is_some() ||
114 (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some()) {
115 return true;
116 }
117 }
118 }
119 }
120 }
121 }
122
123 for profile in agent_config.profiles.values() {
125 match provider {
126 ProviderType::OpenAI => {
127 if profile.openai.as_ref().map(|o| !o.api_key.is_empty()).unwrap_or(false) {
128 return true;
129 }
130 }
131 ProviderType::Anthropic => {
132 if profile.anthropic.as_ref().map(|a| !a.api_key.is_empty()).unwrap_or(false) {
133 return true;
134 }
135 }
136 ProviderType::Bedrock => {
137 if let Some(bedrock) = &profile.bedrock {
138 if bedrock.profile.is_some() ||
139 (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some()) {
140 return true;
141 }
142 }
143 }
144 }
145 }
146
147 match provider {
149 ProviderType::OpenAI => agent_config.openai_api_key.is_some(),
150 ProviderType::Anthropic => agent_config.anthropic_api_key.is_some(),
151 ProviderType::Bedrock => {
152 if let Some(bedrock) = &agent_config.bedrock {
153 bedrock.profile.is_some() ||
154 (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some())
155 } else {
156 agent_config.bedrock_configured.unwrap_or(false)
157 }
158 }
159 }
160 }
161
162 pub fn load_api_key_to_env(provider: ProviderType) {
164 let agent_config = load_agent_config();
165
166 let active_profile = agent_config.active_profile.as_ref()
168 .and_then(|name| agent_config.profiles.get(name));
169
170 match provider {
171 ProviderType::OpenAI => {
172 if std::env::var("OPENAI_API_KEY").is_ok() {
173 return;
174 }
175 if let Some(key) = active_profile
177 .and_then(|p| p.openai.as_ref())
178 .map(|o| o.api_key.clone())
179 .filter(|k| !k.is_empty())
180 {
181 unsafe { std::env::set_var("OPENAI_API_KEY", &key); }
182 return;
183 }
184 if let Some(key) = &agent_config.openai_api_key {
186 unsafe { std::env::set_var("OPENAI_API_KEY", key); }
187 }
188 }
189 ProviderType::Anthropic => {
190 if std::env::var("ANTHROPIC_API_KEY").is_ok() {
191 return;
192 }
193 if let Some(key) = active_profile
195 .and_then(|p| p.anthropic.as_ref())
196 .map(|a| a.api_key.clone())
197 .filter(|k| !k.is_empty())
198 {
199 unsafe { std::env::set_var("ANTHROPIC_API_KEY", &key); }
200 return;
201 }
202 if let Some(key) = &agent_config.anthropic_api_key {
204 unsafe { std::env::set_var("ANTHROPIC_API_KEY", key); }
205 }
206 }
207 ProviderType::Bedrock => {
208 let bedrock_config = active_profile
210 .and_then(|p| p.bedrock.as_ref())
211 .or(agent_config.bedrock.as_ref());
212
213 if let Some(bedrock) = bedrock_config {
214 if std::env::var("AWS_REGION").is_err() {
216 if let Some(region) = &bedrock.region {
217 unsafe { std::env::set_var("AWS_REGION", region); }
218 }
219 }
220 if let Some(profile) = &bedrock.profile {
222 if std::env::var("AWS_PROFILE").is_err() {
223 unsafe { std::env::set_var("AWS_PROFILE", profile); }
224 }
225 } else if let (Some(key_id), Some(secret)) = (&bedrock.access_key_id, &bedrock.secret_access_key) {
226 if std::env::var("AWS_ACCESS_KEY_ID").is_err() {
227 unsafe { std::env::set_var("AWS_ACCESS_KEY_ID", key_id); }
228 }
229 if std::env::var("AWS_SECRET_ACCESS_KEY").is_err() {
230 unsafe { std::env::set_var("AWS_SECRET_ACCESS_KEY", secret); }
231 }
232 }
233 }
234 }
235 }
236 }
237
238 pub fn get_configured_providers() -> Vec<ProviderType> {
240 let mut providers = Vec::new();
241 if Self::has_api_key(ProviderType::OpenAI) {
242 providers.push(ProviderType::OpenAI);
243 }
244 if Self::has_api_key(ProviderType::Anthropic) {
245 providers.push(ProviderType::Anthropic);
246 }
247 providers
248 }
249
250 fn run_bedrock_setup_wizard() -> AgentResult<String> {
252 use crate::config::types::BedrockConfig as BedrockConfigType;
253
254 println!();
255 println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
256 println!("{}", " 🔧 AWS Bedrock Setup Wizard".cyan().bold());
257 println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
258 println!();
259 println!("AWS Bedrock provides access to Claude models via AWS.");
260 println!("You'll need an AWS account with Bedrock access enabled.");
261 println!();
262
263 println!("{}", "Step 1: Choose authentication method".white().bold());
265 println!();
266 println!(" {} Use AWS Profile (from ~/.aws/credentials)", "[1]".cyan());
267 println!(" {}", "Best for: AWS CLI users, SSO, multiple accounts".dimmed());
268 println!();
269 println!(" {} Enter Access Keys directly", "[2]".cyan());
270 println!(" {}", "Best for: Quick setup, CI/CD environments".dimmed());
271 println!();
272 println!(" {} Use existing environment variables", "[3]".cyan());
273 println!(" {}", "Best for: Already configured AWS_* env vars".dimmed());
274 println!();
275 print!("Enter choice [1-3]: ");
276 io::stdout().flush().unwrap();
277
278 let mut choice = String::new();
279 io::stdin().read_line(&mut choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
280 let choice = choice.trim();
281
282 let mut bedrock_config = BedrockConfigType::default();
283
284 match choice {
285 "1" => {
286 println!();
288 println!("{}", "Step 2: Enter AWS Profile".white().bold());
289 println!("{}", "Press Enter for 'default' profile".dimmed());
290 print!("Profile name: ");
291 io::stdout().flush().unwrap();
292
293 let mut profile = String::new();
294 io::stdin().read_line(&mut profile).map_err(|e| AgentError::ToolError(e.to_string()))?;
295 let profile = profile.trim();
296 let profile = if profile.is_empty() { "default" } else { profile };
297
298 bedrock_config.profile = Some(profile.to_string());
299
300 unsafe { std::env::set_var("AWS_PROFILE", profile); }
302 println!("{}", format!("✓ Using profile: {}", profile).green());
303 }
304 "2" => {
305 println!();
307 println!("{}", "Step 2: Enter AWS Access Keys".white().bold());
308 println!("{}", "Get these from AWS Console → IAM → Security credentials".dimmed());
309 println!();
310
311 print!("AWS Access Key ID: ");
312 io::stdout().flush().unwrap();
313 let mut access_key = String::new();
314 io::stdin().read_line(&mut access_key).map_err(|e| AgentError::ToolError(e.to_string()))?;
315 let access_key = access_key.trim().to_string();
316
317 if access_key.is_empty() {
318 return Err(AgentError::MissingApiKey("AWS_ACCESS_KEY_ID".to_string()));
319 }
320
321 print!("AWS Secret Access Key: ");
322 io::stdout().flush().unwrap();
323 let mut secret_key = String::new();
324 io::stdin().read_line(&mut secret_key).map_err(|e| AgentError::ToolError(e.to_string()))?;
325 let secret_key = secret_key.trim().to_string();
326
327 if secret_key.is_empty() {
328 return Err(AgentError::MissingApiKey("AWS_SECRET_ACCESS_KEY".to_string()));
329 }
330
331 bedrock_config.access_key_id = Some(access_key.clone());
332 bedrock_config.secret_access_key = Some(secret_key.clone());
333
334 unsafe {
336 std::env::set_var("AWS_ACCESS_KEY_ID", &access_key);
337 std::env::set_var("AWS_SECRET_ACCESS_KEY", &secret_key);
338 }
339 println!("{}", "✓ Access keys configured".green());
340 }
341 "3" => {
342 if std::env::var("AWS_ACCESS_KEY_ID").is_err()
344 && std::env::var("AWS_PROFILE").is_err()
345 {
346 println!("{}", "⚠ No AWS credentials found in environment!".yellow());
347 println!("Set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY or AWS_PROFILE");
348 return Err(AgentError::MissingApiKey("AWS credentials".to_string()));
349 }
350 println!("{}", "✓ Using existing environment variables".green());
351 }
352 _ => {
353 println!("{}", "Invalid choice, using environment variables".yellow());
354 }
355 }
356
357 if bedrock_config.region.is_none() {
359 println!();
360 println!("{}", "Step 2: Select AWS Region".white().bold());
361 println!("{}", "Bedrock is available in select regions. Common choices:".dimmed());
362 println!();
363 println!(" {} us-east-1 (N. Virginia) - Most models", "[1]".cyan());
364 println!(" {} us-west-2 (Oregon)", "[2]".cyan());
365 println!(" {} eu-west-1 (Ireland)", "[3]".cyan());
366 println!(" {} ap-northeast-1 (Tokyo)", "[4]".cyan());
367 println!();
368 print!("Enter choice [1-4] or region name: ");
369 io::stdout().flush().unwrap();
370
371 let mut region_choice = String::new();
372 io::stdin().read_line(&mut region_choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
373 let region = match region_choice.trim() {
374 "1" | "" => "us-east-1",
375 "2" => "us-west-2",
376 "3" => "eu-west-1",
377 "4" => "ap-northeast-1",
378 other => other,
379 };
380
381 bedrock_config.region = Some(region.to_string());
382 unsafe { std::env::set_var("AWS_REGION", region); }
383 println!("{}", format!("✓ Region: {}", region).green());
384 }
385
386 println!();
388 println!("{}", "Step 3: Select Default Model".white().bold());
389 println!();
390 let models = get_available_models(ProviderType::Bedrock);
391 for (i, (id, desc)) in models.iter().enumerate() {
392 let marker = if i == 0 { "→ " } else { " " };
393 println!(" {} {} {}", marker, format!("[{}]", i + 1).cyan(), desc);
394 println!(" {}", id.dimmed());
395 }
396 println!();
397 print!("Enter choice [1-{}] (default: 1): ", models.len());
398 io::stdout().flush().unwrap();
399
400 let mut model_choice = String::new();
401 io::stdin().read_line(&mut model_choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
402 let model_idx: usize = model_choice.trim().parse().unwrap_or(1);
403 let model_idx = model_idx.saturating_sub(1).min(models.len() - 1);
404 let selected_model = models[model_idx].0.to_string();
405
406 bedrock_config.default_model = Some(selected_model.clone());
407 println!("{}", format!("✓ Default model: {}", models[model_idx].1.split(" - ").next().unwrap_or(&selected_model)).green());
408
409 let mut agent_config = load_agent_config();
411 agent_config.bedrock = Some(bedrock_config);
412 agent_config.bedrock_configured = Some(true);
413
414 if let Err(e) = save_agent_config(&agent_config) {
415 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
416 } else {
417 println!();
418 println!("{}", "✓ Configuration saved to ~/.syncable.toml".green());
419 }
420
421 println!();
422 println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
423 println!("{}", " ✅ AWS Bedrock setup complete!".green().bold());
424 println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
425 println!();
426
427 Ok(selected_model)
428 }
429
430 pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
432 if matches!(provider, ProviderType::Bedrock) {
434 return Self::run_bedrock_setup_wizard();
435 }
436
437 let env_var = match provider {
438 ProviderType::OpenAI => "OPENAI_API_KEY",
439 ProviderType::Anthropic => "ANTHROPIC_API_KEY",
440 ProviderType::Bedrock => unreachable!(), };
442
443 println!("\n{}", format!("🔑 No API key found for {}", provider).yellow());
444 println!("Please enter your {} API key:", provider);
445 print!("> ");
446 io::stdout().flush().unwrap();
447
448 let mut key = String::new();
449 io::stdin().read_line(&mut key).map_err(|e| AgentError::ToolError(e.to_string()))?;
450 let key = key.trim().to_string();
451
452 if key.is_empty() {
453 return Err(AgentError::MissingApiKey(env_var.to_string()));
454 }
455
456 unsafe {
459 std::env::set_var(env_var, &key);
460 }
461
462 let mut agent_config = load_agent_config();
464 match provider {
465 ProviderType::OpenAI => agent_config.openai_api_key = Some(key.clone()),
466 ProviderType::Anthropic => agent_config.anthropic_api_key = Some(key.clone()),
467 ProviderType::Bedrock => unreachable!(), }
469
470 if let Err(e) = save_agent_config(&agent_config) {
471 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
472 } else {
473 println!("{}", "✓ API key saved to ~/.syncable.toml".green());
474 }
475
476 Ok(key)
477 }
478
479 pub fn handle_model_command(&mut self) -> AgentResult<()> {
481 let models = get_available_models(self.provider);
482
483 println!("\n{}", format!("📋 Available models for {}:", self.provider).cyan().bold());
484 println!();
485
486 for (i, (id, desc)) in models.iter().enumerate() {
487 let marker = if *id == self.model { "→ " } else { " " };
488 let num = format!("[{}]", i + 1);
489 println!(" {} {} {} - {}", marker, num.dimmed(), id.white().bold(), desc.dimmed());
490 }
491
492 println!();
493 println!("Enter number to select, or press Enter to keep current:");
494 print!("> ");
495 io::stdout().flush().unwrap();
496
497 let mut input = String::new();
498 io::stdin().read_line(&mut input).ok();
499 let input = input.trim();
500
501 if input.is_empty() {
502 println!("{}", format!("Keeping model: {}", self.model).dimmed());
503 return Ok(());
504 }
505
506 if let Ok(num) = input.parse::<usize>() {
507 if num >= 1 && num <= models.len() {
508 let (id, desc) = models[num - 1];
509 self.model = id.to_string();
510
511 let mut agent_config = load_agent_config();
513 agent_config.default_model = Some(id.to_string());
514 if let Err(e) = save_agent_config(&agent_config) {
515 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
516 }
517
518 println!("{}", format!("✓ Switched to {} - {}", id, desc).green());
519 } else {
520 println!("{}", "Invalid selection".red());
521 }
522 } else {
523 self.model = input.to_string();
525
526 let mut agent_config = load_agent_config();
528 agent_config.default_model = Some(input.to_string());
529 if let Err(e) = save_agent_config(&agent_config) {
530 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
531 }
532
533 println!("{}", format!("✓ Set model to: {}", input).green());
534 }
535
536 Ok(())
537 }
538
539 pub fn handle_provider_command(&mut self) -> AgentResult<()> {
541 let providers = [ProviderType::OpenAI, ProviderType::Anthropic, ProviderType::Bedrock];
542
543 println!("\n{}", "🔄 Available providers:".cyan().bold());
544 println!();
545
546 for (i, provider) in providers.iter().enumerate() {
547 let marker = if *provider == self.provider { "→ " } else { " " };
548 let has_key = if Self::has_api_key(*provider) {
549 "✓ API key configured".green()
550 } else {
551 "⚠ No API key".yellow()
552 };
553 let num = format!("[{}]", i + 1);
554 println!(" {} {} {} - {}", marker, num.dimmed(), provider.to_string().white().bold(), has_key);
555 }
556
557 println!();
558 println!("Enter number to select:");
559 print!("> ");
560 io::stdout().flush().unwrap();
561
562 let mut input = String::new();
563 io::stdin().read_line(&mut input).ok();
564 let input = input.trim();
565
566 if let Ok(num) = input.parse::<usize>() {
567 if num >= 1 && num <= providers.len() {
568 let new_provider = providers[num - 1];
569
570 if !Self::has_api_key(new_provider) {
572 Self::prompt_api_key(new_provider)?;
573 }
574
575 Self::load_api_key_to_env(new_provider);
578
579 self.provider = new_provider;
580
581 let default_model = match new_provider {
583 ProviderType::OpenAI => "gpt-5.2".to_string(),
584 ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
585 ProviderType::Bedrock => {
586 let agent_config = load_agent_config();
588 agent_config.bedrock
589 .and_then(|b| b.default_model)
590 .unwrap_or_else(|| "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string())
591 }
592 };
593 self.model = default_model.clone();
594
595 let mut agent_config = load_agent_config();
597 agent_config.default_provider = new_provider.to_string();
598 agent_config.default_model = Some(default_model.clone());
599 if let Err(e) = save_agent_config(&agent_config) {
600 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
601 }
602
603 println!("{}", format!("✓ Switched to {} with model {}", new_provider, default_model).green());
604 } else {
605 println!("{}", "Invalid selection".red());
606 }
607 }
608
609 Ok(())
610 }
611
612 pub fn handle_reset_command(&mut self) -> AgentResult<()> {
614 let providers = [ProviderType::OpenAI, ProviderType::Anthropic, ProviderType::Bedrock];
615
616 println!("\n{}", "🔄 Reset Provider Credentials".cyan().bold());
617 println!();
618
619 for (i, provider) in providers.iter().enumerate() {
620 let status = if Self::has_api_key(*provider) {
621 "✓ configured".green()
622 } else {
623 "○ not configured".dimmed()
624 };
625 let num = format!("[{}]", i + 1);
626 println!(" {} {} - {}", num.dimmed(), provider.to_string().white().bold(), status);
627 }
628 println!(" {} All providers", "[4]".dimmed());
629 println!();
630 println!("Select provider to reset (or press Enter to cancel):");
631 print!("> ");
632 io::stdout().flush().unwrap();
633
634 let mut input = String::new();
635 io::stdin().read_line(&mut input).ok();
636 let input = input.trim();
637
638 if input.is_empty() {
639 println!("{}", "Cancelled".dimmed());
640 return Ok(());
641 }
642
643 let mut agent_config = load_agent_config();
644
645 match input {
646 "1" => {
647 agent_config.openai_api_key = None;
648 unsafe { std::env::remove_var("OPENAI_API_KEY"); }
650 println!("{}", "✓ OpenAI credentials cleared".green());
651 }
652 "2" => {
653 agent_config.anthropic_api_key = None;
654 unsafe { std::env::remove_var("ANTHROPIC_API_KEY"); }
655 println!("{}", "✓ Anthropic credentials cleared".green());
656 }
657 "3" => {
658 agent_config.bedrock = None;
659 agent_config.bedrock_configured = Some(false);
660 unsafe {
662 std::env::remove_var("AWS_PROFILE");
663 std::env::remove_var("AWS_ACCESS_KEY_ID");
664 std::env::remove_var("AWS_SECRET_ACCESS_KEY");
665 std::env::remove_var("AWS_REGION");
666 }
667 println!("{}", "✓ Bedrock credentials cleared".green());
668 }
669 "4" => {
670 agent_config.openai_api_key = None;
671 agent_config.anthropic_api_key = None;
672 agent_config.bedrock = None;
673 agent_config.bedrock_configured = Some(false);
674 unsafe {
676 std::env::remove_var("OPENAI_API_KEY");
677 std::env::remove_var("ANTHROPIC_API_KEY");
678 std::env::remove_var("AWS_PROFILE");
679 std::env::remove_var("AWS_ACCESS_KEY_ID");
680 std::env::remove_var("AWS_SECRET_ACCESS_KEY");
681 std::env::remove_var("AWS_REGION");
682 }
683 println!("{}", "✓ All provider credentials cleared".green());
684 }
685 _ => {
686 println!("{}", "Invalid selection".red());
687 return Ok(());
688 }
689 }
690
691 if let Err(e) = save_agent_config(&agent_config) {
693 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
694 } else {
695 println!("{}", "Configuration saved to ~/.syncable.toml".dimmed());
696 }
697
698 let current_cleared = match input {
700 "1" => self.provider == ProviderType::OpenAI,
701 "2" => self.provider == ProviderType::Anthropic,
702 "3" => self.provider == ProviderType::Bedrock,
703 "4" => true,
704 _ => false,
705 };
706
707 if current_cleared {
708 println!();
709 println!("{}", "Current provider credentials were cleared.".yellow());
710 println!("Use {} to reconfigure or {} to switch providers.", "/provider".cyan(), "/p".cyan());
711 }
712
713 Ok(())
714 }
715
716 pub fn handle_profile_command(&mut self) -> AgentResult<()> {
718 use crate::config::types::{Profile, OpenAIProfile, AnthropicProfile};
719
720 let mut agent_config = load_agent_config();
721
722 println!("\n{}", "👤 Profile Management".cyan().bold());
723 println!();
724
725 self.list_profiles(&agent_config);
727
728 println!(" {} Create new profile", "[1]".cyan());
729 println!(" {} Switch active profile", "[2]".cyan());
730 println!(" {} Configure provider in profile", "[3]".cyan());
731 println!(" {} Delete a profile", "[4]".cyan());
732 println!();
733 println!("Select action (or press Enter to cancel):");
734 print!("> ");
735 io::stdout().flush().unwrap();
736
737 let mut input = String::new();
738 io::stdin().read_line(&mut input).ok();
739 let input = input.trim();
740
741 if input.is_empty() {
742 println!("{}", "Cancelled".dimmed());
743 return Ok(());
744 }
745
746 match input {
747 "1" => {
748 println!("\n{}", "Create Profile".white().bold());
750 print!("Profile name (e.g., work, personal): ");
751 io::stdout().flush().unwrap();
752 let mut name = String::new();
753 io::stdin().read_line(&mut name).ok();
754 let name = name.trim().to_string();
755
756 if name.is_empty() {
757 println!("{}", "Profile name cannot be empty".red());
758 return Ok(());
759 }
760
761 if agent_config.profiles.contains_key(&name) {
762 println!("{}", format!("Profile '{}' already exists", name).yellow());
763 return Ok(());
764 }
765
766 print!("Description (optional): ");
767 io::stdout().flush().unwrap();
768 let mut desc = String::new();
769 io::stdin().read_line(&mut desc).ok();
770 let desc = desc.trim();
771
772 let profile = Profile {
773 description: if desc.is_empty() { None } else { Some(desc.to_string()) },
774 default_provider: None,
775 default_model: None,
776 openai: None,
777 anthropic: None,
778 bedrock: None,
779 };
780
781 agent_config.profiles.insert(name.clone(), profile);
782
783 if agent_config.active_profile.is_none() {
785 agent_config.active_profile = Some(name.clone());
786 }
787
788 if let Err(e) = save_agent_config(&agent_config) {
789 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
790 }
791
792 println!("{}", format!("✓ Profile '{}' created", name).green());
793 println!("{}", "Use option [3] to configure providers for this profile".dimmed());
794 }
795 "2" => {
796 if agent_config.profiles.is_empty() {
798 println!("{}", "No profiles configured. Create one first with option [1].".yellow());
799 return Ok(());
800 }
801
802 print!("Enter profile name to activate: ");
803 io::stdout().flush().unwrap();
804 let mut name = String::new();
805 io::stdin().read_line(&mut name).ok();
806 let name = name.trim().to_string();
807
808 if name.is_empty() {
809 println!("{}", "Cancelled".dimmed());
810 return Ok(());
811 }
812
813 if !agent_config.profiles.contains_key(&name) {
814 println!("{}", format!("Profile '{}' not found", name).red());
815 return Ok(());
816 }
817
818 agent_config.active_profile = Some(name.clone());
819
820 if let Some(profile) = agent_config.profiles.get(&name) {
822 if let Some(openai) = &profile.openai {
824 unsafe { std::env::set_var("OPENAI_API_KEY", &openai.api_key); }
825 }
826 if let Some(anthropic) = &profile.anthropic {
827 unsafe { std::env::set_var("ANTHROPIC_API_KEY", &anthropic.api_key); }
828 }
829 if let Some(bedrock) = &profile.bedrock {
830 if let Some(region) = &bedrock.region {
831 unsafe { std::env::set_var("AWS_REGION", region); }
832 }
833 if let Some(aws_profile) = &bedrock.profile {
834 unsafe { std::env::set_var("AWS_PROFILE", aws_profile); }
835 } else if let (Some(key_id), Some(secret)) = (&bedrock.access_key_id, &bedrock.secret_access_key) {
836 unsafe {
837 std::env::set_var("AWS_ACCESS_KEY_ID", key_id);
838 std::env::set_var("AWS_SECRET_ACCESS_KEY", secret);
839 }
840 }
841 }
842
843 if let Some(default_provider) = &profile.default_provider {
845 if let Ok(p) = default_provider.parse() {
846 self.provider = p;
847 }
848 }
849 }
850
851 if let Err(e) = save_agent_config(&agent_config) {
852 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
853 }
854
855 println!("{}", format!("✓ Switched to profile '{}'", name).green());
856 }
857 "3" => {
858 let profile_name = if let Some(name) = &agent_config.active_profile {
860 name.clone()
861 } else if agent_config.profiles.is_empty() {
862 println!("{}", "No profiles configured. Create one first with option [1].".yellow());
863 return Ok(());
864 } else {
865 print!("Enter profile name to configure: ");
866 io::stdout().flush().unwrap();
867 let mut name = String::new();
868 io::stdin().read_line(&mut name).ok();
869 name.trim().to_string()
870 };
871
872 if profile_name.is_empty() {
873 println!("{}", "Cancelled".dimmed());
874 return Ok(());
875 }
876
877 if !agent_config.profiles.contains_key(&profile_name) {
878 println!("{}", format!("Profile '{}' not found", profile_name).red());
879 return Ok(());
880 }
881
882 println!("\n{}", format!("Configure provider for '{}':", profile_name).white().bold());
883 println!(" {} OpenAI", "[1]".cyan());
884 println!(" {} Anthropic", "[2]".cyan());
885 println!(" {} AWS Bedrock", "[3]".cyan());
886 print!("> ");
887 io::stdout().flush().unwrap();
888
889 let mut provider_choice = String::new();
890 io::stdin().read_line(&mut provider_choice).ok();
891
892 match provider_choice.trim() {
893 "1" => {
894 print!("OpenAI API Key: ");
896 io::stdout().flush().unwrap();
897 let mut api_key = String::new();
898 io::stdin().read_line(&mut api_key).ok();
899 let api_key = api_key.trim().to_string();
900
901 if api_key.is_empty() {
902 println!("{}", "API key cannot be empty".red());
903 return Ok(());
904 }
905
906 if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
907 profile.openai = Some(OpenAIProfile {
908 api_key,
909 description: None,
910 default_model: None,
911 });
912 }
913 println!("{}", format!("✓ OpenAI configured for profile '{}'", profile_name).green());
914 }
915 "2" => {
916 print!("Anthropic API Key: ");
918 io::stdout().flush().unwrap();
919 let mut api_key = String::new();
920 io::stdin().read_line(&mut api_key).ok();
921 let api_key = api_key.trim().to_string();
922
923 if api_key.is_empty() {
924 println!("{}", "API key cannot be empty".red());
925 return Ok(());
926 }
927
928 if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
929 profile.anthropic = Some(AnthropicProfile {
930 api_key,
931 description: None,
932 default_model: None,
933 });
934 }
935 println!("{}", format!("✓ Anthropic configured for profile '{}'", profile_name).green());
936 }
937 "3" => {
938 println!("{}", "Running Bedrock setup...".dimmed());
940 let selected_model = Self::run_bedrock_setup_wizard()?;
941
942 let fresh_config = load_agent_config();
944 if let Some(bedrock) = fresh_config.bedrock.clone() {
945 if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
946 profile.bedrock = Some(bedrock);
947 profile.default_model = Some(selected_model);
948 }
949 }
950 println!("{}", format!("✓ Bedrock configured for profile '{}'", profile_name).green());
951 }
952 _ => {
953 println!("{}", "Invalid selection".red());
954 return Ok(());
955 }
956 }
957
958 if let Err(e) = save_agent_config(&agent_config) {
959 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
960 }
961 }
962 "4" => {
963 if agent_config.profiles.is_empty() {
965 println!("{}", "No profiles to delete.".yellow());
966 return Ok(());
967 }
968
969 print!("Enter profile name to delete: ");
970 io::stdout().flush().unwrap();
971 let mut name = String::new();
972 io::stdin().read_line(&mut name).ok();
973 let name = name.trim().to_string();
974
975 if name.is_empty() {
976 println!("{}", "Cancelled".dimmed());
977 return Ok(());
978 }
979
980 if agent_config.profiles.remove(&name).is_some() {
981 if agent_config.active_profile.as_deref() == Some(name.as_str()) {
983 agent_config.active_profile = None;
984 }
985
986 if let Err(e) = save_agent_config(&agent_config) {
987 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
988 }
989
990 println!("{}", format!("✓ Deleted profile '{}'", name).green());
991 } else {
992 println!("{}", format!("Profile '{}' not found", name).red());
993 }
994 }
995 _ => {
996 println!("{}", "Invalid selection".red());
997 }
998 }
999
1000 Ok(())
1001 }
1002
1003 fn list_profiles(&self, config: &crate::config::types::AgentConfig) {
1005 let active = config.active_profile.as_deref();
1006
1007 if config.profiles.is_empty() {
1008 println!("{}", " No profiles configured yet.".dimmed());
1009 println!();
1010 return;
1011 }
1012
1013 println!("{}", "📋 Profiles:".cyan());
1014 for (name, profile) in &config.profiles {
1015 let marker = if Some(name.as_str()) == active { "→ " } else { " " };
1016 let desc = profile.description.as_deref().unwrap_or("");
1017 let desc_fmt = if desc.is_empty() { String::new() } else { format!(" - {}", desc) };
1018
1019 let mut providers = Vec::new();
1021 if profile.openai.is_some() { providers.push("OpenAI"); }
1022 if profile.anthropic.is_some() { providers.push("Anthropic"); }
1023 if profile.bedrock.is_some() { providers.push("Bedrock"); }
1024
1025 let providers_str = if providers.is_empty() {
1026 "(no providers configured)".to_string()
1027 } else {
1028 format!("[{}]", providers.join(", "))
1029 };
1030
1031 println!(" {} {}{} {}", marker, name.white().bold(), desc_fmt.dimmed(), providers_str.dimmed());
1032 }
1033 println!();
1034 }
1035
1036 pub fn print_help() {
1038 println!();
1039 println!(" {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET);
1040 println!(" {}📖 Available Commands{}", ansi::PURPLE, ansi::RESET);
1041 println!(" {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET);
1042 println!();
1043
1044 for cmd in SLASH_COMMANDS.iter() {
1045 let alias = cmd.alias.map(|a| format!(" ({})", a)).unwrap_or_default();
1046 println!(" {}/{:<12}{}{} - {}{}{}",
1047 ansi::CYAN, cmd.name, alias, ansi::RESET,
1048 ansi::DIM, cmd.description, ansi::RESET
1049 );
1050 }
1051
1052 println!();
1053 println!(" {}Tip: Type / to see interactive command picker!{}", ansi::DIM, ansi::RESET);
1054 println!();
1055 }
1056
1057
1058 pub fn print_logo() {
1060 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";
1071
1072 println!();
1073 println!(
1074 "{} ███████╗{}{} ██╗ ██╗{}{}███╗ ██╗{}{} ██████╗{}{} █████╗ {}{}██████╗ {}{}██╗ {}{}███████╗{}",
1075 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1076 );
1077 println!(
1078 "{} ██╔════╝{}{} ╚██╗ ██╔╝{}{}████╗ ██║{}{} ██╔════╝{}{} ██╔══██╗{}{}██╔══██╗{}{}██║ {}{}██╔════╝{}",
1079 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1080 );
1081 println!(
1082 "{} ███████╗{}{} ╚████╔╝ {}{}██╔██╗ ██║{}{} ██║ {}{} ███████║{}{}██████╔╝{}{}██║ {}{}█████╗ {}",
1083 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1084 );
1085 println!(
1086 "{} ╚════██║{}{} ╚██╔╝ {}{}██║╚██╗██║{}{} ██║ {}{} ██╔══██║{}{}██╔══██╗{}{}██║ {}{}██╔══╝ {}",
1087 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1088 );
1089 println!(
1090 "{} ███████║{}{} ██║ {}{}██║ ╚████║{}{} ╚██████╗{}{} ██║ ██║{}{}██████╔╝{}{}███████╗{}{}███████╗{}",
1091 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1092 );
1093 println!(
1094 "{} ╚══════╝{}{} ╚═╝ {}{}╚═╝ ╚═══╝{}{} ╚═════╝{}{} ╚═╝ ╚═╝{}{}╚═════╝ {}{}╚══════╝{}{}╚══════╝{}",
1095 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1096 );
1097 println!();
1098 }
1099
1100 pub fn print_banner(&self) {
1102 Self::print_logo();
1104
1105 println!(
1107 " {} {}",
1108 "🚀".dimmed(),
1109 "Want to deploy? Deploy instantly from Syncable Platform → https://syncable.dev".dimmed()
1110 );
1111 println!();
1112
1113 println!(
1115 " {} {} powered by {}: {}",
1116 ROBOT,
1117 "Syncable Agent".white().bold(),
1118 self.provider.to_string().cyan(),
1119 self.model.cyan()
1120 );
1121 println!(
1122 " {}",
1123 "Your AI-powered code analysis assistant".dimmed()
1124 );
1125 println!();
1126 println!(
1127 " {} Type your questions. Use {} to exit.\n",
1128 "→".cyan(),
1129 "exit".yellow().bold()
1130 );
1131 }
1132
1133
1134 pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
1136 let cmd = input.trim().to_lowercase();
1137
1138 if cmd == "/" {
1141 Self::print_help();
1142 return Ok(true);
1143 }
1144
1145 match cmd.as_str() {
1146 "/exit" | "/quit" | "/q" => {
1147 println!("\n{}", "👋 Goodbye!".green());
1148 return Ok(false);
1149 }
1150 "/help" | "/h" | "/?" => {
1151 Self::print_help();
1152 }
1153 "/model" | "/m" => {
1154 self.handle_model_command()?;
1155 }
1156 "/provider" | "/p" => {
1157 self.handle_provider_command()?;
1158 }
1159 "/cost" => {
1160 self.token_usage.print_report(&self.model);
1161 }
1162 "/clear" | "/c" => {
1163 self.history.clear();
1164 println!("{}", "✓ Conversation history cleared".green());
1165 }
1166 "/reset" | "/r" => {
1167 self.handle_reset_command()?;
1168 }
1169 "/profile" => {
1170 self.handle_profile_command()?;
1171 }
1172 _ => {
1173 if cmd.starts_with('/') {
1174 println!("{}", format!("Unknown command: {}. Type /help for available commands.", cmd).yellow());
1176 }
1177 }
1178 }
1179
1180 Ok(true)
1181 }
1182
1183 pub fn is_command(input: &str) -> bool {
1185 input.trim().starts_with('/')
1186 }
1187
1188 fn strip_file_references(input: &str) -> String {
1192 let mut result = String::with_capacity(input.len());
1193 let chars: Vec<char> = input.chars().collect();
1194 let mut i = 0;
1195
1196 while i < chars.len() {
1197 if chars[i] == '@' {
1198 let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
1200
1201 if is_valid_trigger {
1202 let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
1204
1205 if has_path {
1206 i += 1;
1208 continue;
1209 }
1210 }
1211 }
1212 result.push(chars[i]);
1213 i += 1;
1214 }
1215
1216 result
1217 }
1218
1219 pub fn read_input(&self) -> io::Result<String> {
1222 use crate::agent::ui::input::{read_input_with_file_picker, InputResult};
1223
1224 match read_input_with_file_picker("You:", &self.project_path) {
1225 InputResult::Submit(text) => {
1226 let trimmed = text.trim();
1227 if trimmed.starts_with('/') && trimmed.contains(" ") {
1230 if let Some(cmd) = trimmed.split_whitespace().next() {
1232 return Ok(cmd.to_string());
1233 }
1234 }
1235 Ok(Self::strip_file_references(trimmed))
1238 }
1239 InputResult::Cancel => Ok("exit".to_string()), InputResult::Exit => Ok("exit".to_string()),
1241 }
1242 }
1243}