syncable_cli/agent/
session.rs

1//! Interactive chat session with /model and /provider commands
2//!
3//! Provides a rich REPL experience similar to Claude Code with:
4//! - `/model` - Select from available models based on configured API keys
5//! - `/provider` - Switch provider (prompts for API key if not set)
6//! - `/cost` - Show token usage and estimated cost
7//! - `/help` - Show available commands
8//! - `/clear` - Clear conversation history
9//! - `/exit` or `/quit` - Exit the session
10
11use 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
21/// Available models per provider
22pub 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        // Bedrock models - use cross-region inference profile format (global. prefix)
37        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
46/// Chat session state
47pub struct ChatSession {
48    pub provider: ProviderType,
49    pub model: String,
50    pub project_path: std::path::PathBuf,
51    pub history: Vec<(String, String)>, // (role, content)
52    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    /// Check if API key is configured for a provider (env var OR config file)
73    pub fn has_api_key(provider: ProviderType) -> bool {
74        // Check environment variable first
75        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                // Check for AWS credentials from env vars
80                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        // Check config file - first try active global profile
95        let agent_config = load_agent_config();
96
97        // Check active global profile first
98        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        // Check any profile that has this provider configured
124        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        // Fall back to legacy config
148        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    /// Load API key from config if not in env, and set it in env for use
163    pub fn load_api_key_to_env(provider: ProviderType) {
164        let agent_config = load_agent_config();
165
166        // Try to get credentials from active global profile first
167        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                // Check active global profile
176                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                // Fall back to legacy key
185                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                // Check active global profile
194                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                // Fall back to legacy key
203                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                // Check active global profile first
209                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                    // Load region
215                    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                    // Load profile OR access keys (profile takes precedence)
221                    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    /// Get configured providers (those with API keys)
239    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    /// Interactive wizard to set up AWS Bedrock credentials
251    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        // Step 1: Choose authentication method
264        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                // AWS Profile
287                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                // Set in env for current session
301                unsafe { std::env::set_var("AWS_PROFILE", profile); }
302                println!("{}", format!("✓ Using profile: {}", profile).green());
303            }
304            "2" => {
305                // Access Keys
306                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                // Set in env for current session
335                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                // Use existing env vars
343                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        // Step 2: Region selection
358        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        // Step 3: Model selection
387        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        // Save configuration
410        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    /// Prompt user to enter API key for a provider
431    pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
432        // Bedrock uses AWS credential chain - run setup wizard
433        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!(),  // Handled above
441        };
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        // Set for current session
457        // SAFETY: We're in a single-threaded CLI context during initialization
458        unsafe {
459            std::env::set_var(env_var, &key);
460        }
461
462        // Save to config file for persistence
463        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!(),  // Handled above
468        }
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    /// Handle /model command - interactive model selection
480    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                // Save model choice to config for persistence
512                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            // Allow direct model name input
524            self.model = input.to_string();
525
526            // Save model choice to config for persistence
527            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    /// Handle /provider command - switch provider with API key prompt if needed
540    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                // Check if API key exists, prompt if not
571                if !Self::has_api_key(new_provider) {
572                    Self::prompt_api_key(new_provider)?;
573                }
574
575                // Load API key/credentials from config to environment
576                // This is essential for Bedrock bearer token auth!
577                Self::load_api_key_to_env(new_provider);
578
579                self.provider = new_provider;
580
581                // Set default model for new provider (check saved config for Bedrock)
582                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                        // Use saved model preference if available
587                        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                // Save provider choice to config for persistence
596                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    /// Handle /reset command - reset provider credentials
613    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                // SAFETY: Single-threaded CLI context during command handling
649                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                // SAFETY: Single-threaded CLI context during command handling
661                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                // SAFETY: Single-threaded CLI context during command handling
675                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        // Save updated config
692        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        // Prompt to reconfigure if current provider was reset
699        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    /// Handle /profile command - manage global profiles
717    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        // Show current profiles
726        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                // Create new profile
749                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                // Set as active if it's the first profile
784                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                // Switch active profile
797                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                // Load credentials from the new profile
821                if let Some(profile) = agent_config.profiles.get(&name) {
822                    // Clear old env vars and load new ones
823                    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                    // Update current provider if profile has a default
844                    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                // Configure provider in profile
859                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                        // Configure OpenAI
895                        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                        // Configure Anthropic
917                        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                        // Configure Bedrock - use the wizard
939                        println!("{}", "Running Bedrock setup...".dimmed());
940                        let selected_model = Self::run_bedrock_setup_wizard()?;
941
942                        // Get the saved bedrock config and copy it to the profile
943                        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                // Delete profile
964                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 this was the active profile, clear it
982                    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    /// List all profiles
1004    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            // Show which providers are configured
1020            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    /// Handle /help command
1037    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    /// Print session banner with colorful SYNCABLE ASCII art
1059    pub fn print_logo() {
1060    // Colors matching the logo gradient: purple → orange → pink
1061    // Using ANSI 256 colors for better gradient
1062
1063        // Purple shades for S, y
1064        let purple = "\x1b[38;5;141m";  // Light purple
1065        // Orange shades for n, c  
1066        let orange = "\x1b[38;5;216m";  // Peach/orange
1067        // Pink shades for a, b, l, e
1068        let pink = "\x1b[38;5;212m";    // Hot pink
1069        let magenta = "\x1b[38;5;207m"; // Magenta
1070        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    /// Print the welcome banner
1101    pub fn print_banner(&self) {
1102        // Print the gradient ASCII logo
1103        Self::print_logo();
1104
1105        // Platform promo
1106        println!(
1107            "  {} {}",
1108            "🚀".dimmed(),
1109            "Want to deploy? Deploy instantly from Syncable Platform → https://syncable.dev".dimmed()
1110        );
1111        println!();
1112
1113        // Print agent info
1114        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    /// Process a command (returns true if should continue, false if should exit)
1135    pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
1136        let cmd = input.trim().to_lowercase();
1137        
1138        // Handle bare "/" - now handled interactively in read_input
1139        // Just show help if they somehow got here
1140        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                    // Unknown command - interactive picker already handled in read_input
1175                    println!("{}", format!("Unknown command: {}. Type /help for available commands.", cmd).yellow());
1176                }
1177            }
1178        }
1179        
1180        Ok(true)
1181    }
1182
1183    /// Check if input is a command
1184    pub fn is_command(input: &str) -> bool {
1185        input.trim().starts_with('/')
1186    }
1187
1188    /// Strip @ prefix from file/folder references for AI consumption
1189    /// Keeps the path but removes the leading @ that was used for autocomplete
1190    /// e.g., "check @src/main.rs for issues" -> "check src/main.rs for issues"
1191    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                // Check if this @ is at start or after whitespace (valid file reference trigger)
1199                let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
1200
1201                if is_valid_trigger {
1202                    // Check if there's a path after @ (not just @ followed by space/end)
1203                    let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
1204
1205                    if has_path {
1206                        // Skip the @ but keep the path
1207                        i += 1;
1208                        continue;
1209                    }
1210                }
1211            }
1212            result.push(chars[i]);
1213            i += 1;
1214        }
1215
1216        result
1217    }
1218
1219    /// Read user input with prompt - with interactive file picker support
1220    /// Uses custom terminal handling for @ file references and / commands
1221    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                // Handle case where full suggestion was submitted (e.g., "/model        Description")
1228                // Extract just the command if it looks like a suggestion format
1229                if trimmed.starts_with('/') && trimmed.contains("  ") {
1230                    // This looks like a suggestion format, extract just the command
1231                    if let Some(cmd) = trimmed.split_whitespace().next() {
1232                        return Ok(cmd.to_string());
1233                    }
1234                }
1235                // Strip @ prefix from file references before sending to AI
1236                // The @ is for UI autocomplete, but the AI should see just the path
1237                Ok(Self::strip_file_references(trimmed))
1238            }
1239            InputResult::Cancel => Ok("exit".to_string()),  // Ctrl+C exits
1240            InputResult::Exit => Ok("exit".to_string()),
1241        }
1242    }
1243}