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/// Information about an incomplete plan
22#[derive(Debug, Clone)]
23pub struct IncompletePlan {
24    pub path: String,
25    pub filename: String,
26    pub done: usize,
27    pub pending: usize,
28    pub total: usize,
29}
30
31/// Find incomplete plans in the plans/ directory
32pub fn find_incomplete_plans(project_path: &std::path::Path) -> Vec<IncompletePlan> {
33    use regex::Regex;
34
35    let plans_dir = project_path.join("plans");
36    if !plans_dir.exists() {
37        return Vec::new();
38    }
39
40    let task_regex = Regex::new(r"^\s*-\s*\[([ x~!])\]").unwrap();
41    let mut incomplete = Vec::new();
42
43    if let Ok(entries) = std::fs::read_dir(&plans_dir) {
44        for entry in entries.flatten() {
45            let path = entry.path();
46            if path.extension().map(|e| e == "md").unwrap_or(false) {
47                if let Ok(content) = std::fs::read_to_string(&path) {
48                    let mut done = 0;
49                    let mut pending = 0;
50                    let mut in_progress = 0;
51
52                    for line in content.lines() {
53                        if let Some(caps) = task_regex.captures(line) {
54                            match caps.get(1).map(|m| m.as_str()) {
55                                Some("x") => done += 1,
56                                Some(" ") => pending += 1,
57                                Some("~") => in_progress += 1,
58                                Some("!") => done += 1, // Failed counts as "attempted"
59                                _ => {}
60                            }
61                        }
62                    }
63
64                    let total = done + pending + in_progress;
65                    if total > 0 && (pending > 0 || in_progress > 0) {
66                        let rel_path = path.strip_prefix(project_path)
67                            .map(|p| p.display().to_string())
68                            .unwrap_or_else(|_| path.display().to_string());
69
70                        incomplete.push(IncompletePlan {
71                            path: rel_path,
72                            filename: path.file_name()
73                                .map(|n| n.to_string_lossy().to_string())
74                                .unwrap_or_default(),
75                            done,
76                            pending: pending + in_progress,
77                            total,
78                        });
79                    }
80                }
81            }
82        }
83    }
84
85    // Sort by most recently modified (newest first)
86    incomplete.sort_by(|a, b| b.filename.cmp(&a.filename));
87    incomplete
88}
89
90/// Planning mode state - toggles between standard and plan mode
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum PlanMode {
93    /// Standard mode - all tools available, normal operation
94    #[default]
95    Standard,
96    /// Planning mode - read-only exploration, no file modifications
97    Planning,
98}
99
100impl PlanMode {
101    /// Toggle between Standard and Planning mode
102    pub fn toggle(&self) -> Self {
103        match self {
104            PlanMode::Standard => PlanMode::Planning,
105            PlanMode::Planning => PlanMode::Standard,
106        }
107    }
108
109    /// Check if in planning mode
110    pub fn is_planning(&self) -> bool {
111        matches!(self, PlanMode::Planning)
112    }
113
114    /// Get display name for the mode
115    pub fn display_name(&self) -> &'static str {
116        match self {
117            PlanMode::Standard => "standard mode",
118            PlanMode::Planning => "plan mode",
119        }
120    }
121}
122
123/// Available models per provider
124pub fn get_available_models(provider: ProviderType) -> Vec<(&'static str, &'static str)> {
125    match provider {
126        ProviderType::OpenAI => vec![
127            ("gpt-5.2", "GPT-5.2 - Latest reasoning model (Dec 2025)"),
128            ("gpt-5.2-mini", "GPT-5.2 Mini - Fast and affordable"),
129            ("gpt-4o", "GPT-4o - Multimodal workhorse"),
130            ("o1-preview", "o1-preview - Advanced reasoning"),
131        ],
132        ProviderType::Anthropic => vec![
133            ("claude-opus-4-5-20251101", "Claude Opus 4.5 - Most capable (Nov 2025)"),
134            ("claude-sonnet-4-5-20250929", "Claude Sonnet 4.5 - Balanced (Sep 2025)"),
135            ("claude-haiku-4-5-20251001", "Claude Haiku 4.5 - Fast (Oct 2025)"),
136            ("claude-sonnet-4-20250514", "Claude Sonnet 4 - Previous gen"),
137        ],
138        // Bedrock models - use cross-region inference profile format (global. prefix)
139        ProviderType::Bedrock => vec![
140            ("global.anthropic.claude-opus-4-5-20251101-v1:0", "Claude Opus 4.5 - Most capable (Nov 2025)"),
141            ("global.anthropic.claude-sonnet-4-5-20250929-v1:0", "Claude Sonnet 4.5 - Balanced (Sep 2025)"),
142            ("global.anthropic.claude-haiku-4-5-20251001-v1:0", "Claude Haiku 4.5 - Fast (Oct 2025)"),
143            ("global.anthropic.claude-sonnet-4-20250514-v1:0", "Claude Sonnet 4 - Previous gen"),
144        ],
145    }
146}
147
148/// Chat session state
149pub struct ChatSession {
150    pub provider: ProviderType,
151    pub model: String,
152    pub project_path: std::path::PathBuf,
153    pub history: Vec<(String, String)>, // (role, content)
154    pub token_usage: TokenUsage,
155    /// Current planning mode state
156    pub plan_mode: PlanMode,
157}
158
159impl ChatSession {
160    pub fn new(project_path: &Path, provider: ProviderType, model: Option<String>) -> Self {
161        let default_model = match provider {
162            ProviderType::OpenAI => "gpt-5.2".to_string(),
163            ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
164            ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(),
165        };
166
167        Self {
168            provider,
169            model: model.unwrap_or(default_model),
170            project_path: project_path.to_path_buf(),
171            history: Vec::new(),
172            token_usage: TokenUsage::new(),
173            plan_mode: PlanMode::default(),
174        }
175    }
176
177    /// Toggle planning mode and return the new mode
178    pub fn toggle_plan_mode(&mut self) -> PlanMode {
179        self.plan_mode = self.plan_mode.toggle();
180        self.plan_mode
181    }
182
183    /// Check if currently in planning mode
184    pub fn is_planning(&self) -> bool {
185        self.plan_mode.is_planning()
186    }
187
188    /// Check if API key is configured for a provider (env var OR config file)
189    pub fn has_api_key(provider: ProviderType) -> bool {
190        // Check environment variable first
191        let env_key = match provider {
192            ProviderType::OpenAI => std::env::var("OPENAI_API_KEY").ok(),
193            ProviderType::Anthropic => std::env::var("ANTHROPIC_API_KEY").ok(),
194            ProviderType::Bedrock => {
195                // Check for AWS credentials from env vars
196                if std::env::var("AWS_ACCESS_KEY_ID").is_ok() && std::env::var("AWS_SECRET_ACCESS_KEY").is_ok() {
197                    return true;
198                }
199                if std::env::var("AWS_PROFILE").is_ok() {
200                    return true;
201                }
202                None
203            }
204        };
205
206        if env_key.is_some() {
207            return true;
208        }
209
210        // Check config file - first try active global profile
211        let agent_config = load_agent_config();
212
213        // Check active global profile first
214        if let Some(profile_name) = &agent_config.active_profile {
215            if let Some(profile) = agent_config.profiles.get(profile_name) {
216                match provider {
217                    ProviderType::OpenAI => {
218                        if profile.openai.as_ref().map(|o| !o.api_key.is_empty()).unwrap_or(false) {
219                            return true;
220                        }
221                    }
222                    ProviderType::Anthropic => {
223                        if profile.anthropic.as_ref().map(|a| !a.api_key.is_empty()).unwrap_or(false) {
224                            return true;
225                        }
226                    }
227                    ProviderType::Bedrock => {
228                        if let Some(bedrock) = &profile.bedrock {
229                            if bedrock.profile.is_some() ||
230                               (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some()) {
231                                return true;
232                            }
233                        }
234                    }
235                }
236            }
237        }
238
239        // Check any profile that has this provider configured
240        for profile in agent_config.profiles.values() {
241            match provider {
242                ProviderType::OpenAI => {
243                    if profile.openai.as_ref().map(|o| !o.api_key.is_empty()).unwrap_or(false) {
244                        return true;
245                    }
246                }
247                ProviderType::Anthropic => {
248                    if profile.anthropic.as_ref().map(|a| !a.api_key.is_empty()).unwrap_or(false) {
249                        return true;
250                    }
251                }
252                ProviderType::Bedrock => {
253                    if let Some(bedrock) = &profile.bedrock {
254                        if bedrock.profile.is_some() ||
255                           (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some()) {
256                            return true;
257                        }
258                    }
259                }
260            }
261        }
262
263        // Fall back to legacy config
264        match provider {
265            ProviderType::OpenAI => agent_config.openai_api_key.is_some(),
266            ProviderType::Anthropic => agent_config.anthropic_api_key.is_some(),
267            ProviderType::Bedrock => {
268                if let Some(bedrock) = &agent_config.bedrock {
269                    bedrock.profile.is_some() ||
270                    (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some())
271                } else {
272                    agent_config.bedrock_configured.unwrap_or(false)
273                }
274            }
275        }
276    }
277    
278    /// Load API key from config if not in env, and set it in env for use
279    pub fn load_api_key_to_env(provider: ProviderType) {
280        let agent_config = load_agent_config();
281
282        // Try to get credentials from active global profile first
283        let active_profile = agent_config.active_profile.as_ref()
284            .and_then(|name| agent_config.profiles.get(name));
285
286        match provider {
287            ProviderType::OpenAI => {
288                if std::env::var("OPENAI_API_KEY").is_ok() {
289                    return;
290                }
291                // Check active global profile
292                if let Some(key) = active_profile
293                    .and_then(|p| p.openai.as_ref())
294                    .map(|o| o.api_key.clone())
295                    .filter(|k| !k.is_empty())
296                {
297                    unsafe { std::env::set_var("OPENAI_API_KEY", &key); }
298                    return;
299                }
300                // Fall back to legacy key
301                if let Some(key) = &agent_config.openai_api_key {
302                    unsafe { std::env::set_var("OPENAI_API_KEY", key); }
303                }
304            }
305            ProviderType::Anthropic => {
306                if std::env::var("ANTHROPIC_API_KEY").is_ok() {
307                    return;
308                }
309                // Check active global profile
310                if let Some(key) = active_profile
311                    .and_then(|p| p.anthropic.as_ref())
312                    .map(|a| a.api_key.clone())
313                    .filter(|k| !k.is_empty())
314                {
315                    unsafe { std::env::set_var("ANTHROPIC_API_KEY", &key); }
316                    return;
317                }
318                // Fall back to legacy key
319                if let Some(key) = &agent_config.anthropic_api_key {
320                    unsafe { std::env::set_var("ANTHROPIC_API_KEY", key); }
321                }
322            }
323            ProviderType::Bedrock => {
324                // Check active global profile first
325                let bedrock_config = active_profile
326                    .and_then(|p| p.bedrock.as_ref())
327                    .or(agent_config.bedrock.as_ref());
328
329                if let Some(bedrock) = bedrock_config {
330                    // Load region
331                    if std::env::var("AWS_REGION").is_err() {
332                        if let Some(region) = &bedrock.region {
333                            unsafe { std::env::set_var("AWS_REGION", region); }
334                        }
335                    }
336                    // Load profile OR access keys (profile takes precedence)
337                    if let Some(profile) = &bedrock.profile {
338                        if std::env::var("AWS_PROFILE").is_err() {
339                            unsafe { std::env::set_var("AWS_PROFILE", profile); }
340                        }
341                    } else if let (Some(key_id), Some(secret)) = (&bedrock.access_key_id, &bedrock.secret_access_key) {
342                        if std::env::var("AWS_ACCESS_KEY_ID").is_err() {
343                            unsafe { std::env::set_var("AWS_ACCESS_KEY_ID", key_id); }
344                        }
345                        if std::env::var("AWS_SECRET_ACCESS_KEY").is_err() {
346                            unsafe { std::env::set_var("AWS_SECRET_ACCESS_KEY", secret); }
347                        }
348                    }
349                }
350            }
351        }
352    }
353
354    /// Get configured providers (those with API keys)
355    pub fn get_configured_providers() -> Vec<ProviderType> {
356        let mut providers = Vec::new();
357        if Self::has_api_key(ProviderType::OpenAI) {
358            providers.push(ProviderType::OpenAI);
359        }
360        if Self::has_api_key(ProviderType::Anthropic) {
361            providers.push(ProviderType::Anthropic);
362        }
363        providers
364    }
365
366    /// Interactive wizard to set up AWS Bedrock credentials
367    fn run_bedrock_setup_wizard() -> AgentResult<String> {
368        use crate::config::types::BedrockConfig as BedrockConfigType;
369
370        println!();
371        println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
372        println!("{}", "  🔧 AWS Bedrock Setup Wizard".cyan().bold());
373        println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
374        println!();
375        println!("AWS Bedrock provides access to Claude models via AWS.");
376        println!("You'll need an AWS account with Bedrock access enabled.");
377        println!();
378
379        // Step 1: Choose authentication method
380        println!("{}", "Step 1: Choose authentication method".white().bold());
381        println!();
382        println!("  {} Use AWS Profile (from ~/.aws/credentials)", "[1]".cyan());
383        println!("      {}", "Best for: AWS CLI users, SSO, multiple accounts".dimmed());
384        println!();
385        println!("  {} Enter Access Keys directly", "[2]".cyan());
386        println!("      {}", "Best for: Quick setup, CI/CD environments".dimmed());
387        println!();
388        println!("  {} Use existing environment variables", "[3]".cyan());
389        println!("      {}", "Best for: Already configured AWS_* env vars".dimmed());
390        println!();
391        print!("Enter choice [1-3]: ");
392        io::stdout().flush().unwrap();
393
394        let mut choice = String::new();
395        io::stdin().read_line(&mut choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
396        let choice = choice.trim();
397
398        let mut bedrock_config = BedrockConfigType::default();
399
400        match choice {
401            "1" => {
402                // AWS Profile
403                println!();
404                println!("{}", "Step 2: Enter AWS Profile".white().bold());
405                println!("{}", "Press Enter for 'default' profile".dimmed());
406                print!("Profile name: ");
407                io::stdout().flush().unwrap();
408
409                let mut profile = String::new();
410                io::stdin().read_line(&mut profile).map_err(|e| AgentError::ToolError(e.to_string()))?;
411                let profile = profile.trim();
412                let profile = if profile.is_empty() { "default" } else { profile };
413
414                bedrock_config.profile = Some(profile.to_string());
415
416                // Set in env for current session
417                unsafe { std::env::set_var("AWS_PROFILE", profile); }
418                println!("{}", format!("✓ Using profile: {}", profile).green());
419            }
420            "2" => {
421                // Access Keys
422                println!();
423                println!("{}", "Step 2: Enter AWS Access Keys".white().bold());
424                println!("{}", "Get these from AWS Console → IAM → Security credentials".dimmed());
425                println!();
426
427                print!("AWS Access Key ID: ");
428                io::stdout().flush().unwrap();
429                let mut access_key = String::new();
430                io::stdin().read_line(&mut access_key).map_err(|e| AgentError::ToolError(e.to_string()))?;
431                let access_key = access_key.trim().to_string();
432
433                if access_key.is_empty() {
434                    return Err(AgentError::MissingApiKey("AWS_ACCESS_KEY_ID".to_string()));
435                }
436
437                print!("AWS Secret Access Key: ");
438                io::stdout().flush().unwrap();
439                let mut secret_key = String::new();
440                io::stdin().read_line(&mut secret_key).map_err(|e| AgentError::ToolError(e.to_string()))?;
441                let secret_key = secret_key.trim().to_string();
442
443                if secret_key.is_empty() {
444                    return Err(AgentError::MissingApiKey("AWS_SECRET_ACCESS_KEY".to_string()));
445                }
446
447                bedrock_config.access_key_id = Some(access_key.clone());
448                bedrock_config.secret_access_key = Some(secret_key.clone());
449
450                // Set in env for current session
451                unsafe {
452                    std::env::set_var("AWS_ACCESS_KEY_ID", &access_key);
453                    std::env::set_var("AWS_SECRET_ACCESS_KEY", &secret_key);
454                }
455                println!("{}", "✓ Access keys configured".green());
456            }
457            "3" => {
458                // Use existing env vars
459                if std::env::var("AWS_ACCESS_KEY_ID").is_err()
460                    && std::env::var("AWS_PROFILE").is_err()
461                {
462                    println!("{}", "⚠ No AWS credentials found in environment!".yellow());
463                    println!("Set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY or AWS_PROFILE");
464                    return Err(AgentError::MissingApiKey("AWS credentials".to_string()));
465                }
466                println!("{}", "✓ Using existing environment variables".green());
467            }
468            _ => {
469                println!("{}", "Invalid choice, using environment variables".yellow());
470            }
471        }
472
473        // Step 2: Region selection
474        if bedrock_config.region.is_none() {
475            println!();
476            println!("{}", "Step 2: Select AWS Region".white().bold());
477            println!("{}", "Bedrock is available in select regions. Common choices:".dimmed());
478            println!();
479            println!("  {} us-east-1     (N. Virginia) - Most models", "[1]".cyan());
480            println!("  {} us-west-2     (Oregon)", "[2]".cyan());
481            println!("  {} eu-west-1     (Ireland)", "[3]".cyan());
482            println!("  {} ap-northeast-1 (Tokyo)", "[4]".cyan());
483            println!();
484            print!("Enter choice [1-4] or region name: ");
485            io::stdout().flush().unwrap();
486
487            let mut region_choice = String::new();
488            io::stdin().read_line(&mut region_choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
489            let region = match region_choice.trim() {
490                "1" | "" => "us-east-1",
491                "2" => "us-west-2",
492                "3" => "eu-west-1",
493                "4" => "ap-northeast-1",
494                other => other,
495            };
496
497            bedrock_config.region = Some(region.to_string());
498            unsafe { std::env::set_var("AWS_REGION", region); }
499            println!("{}", format!("✓ Region: {}", region).green());
500        }
501
502        // Step 3: Model selection
503        println!();
504        println!("{}", "Step 3: Select Default Model".white().bold());
505        println!();
506        let models = get_available_models(ProviderType::Bedrock);
507        for (i, (id, desc)) in models.iter().enumerate() {
508            let marker = if i == 0 { "→ " } else { "  " };
509            println!("  {} {} {}", marker, format!("[{}]", i + 1).cyan(), desc);
510            println!("      {}", id.dimmed());
511        }
512        println!();
513        print!("Enter choice [1-{}] (default: 1): ", models.len());
514        io::stdout().flush().unwrap();
515
516        let mut model_choice = String::new();
517        io::stdin().read_line(&mut model_choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
518        let model_idx: usize = model_choice.trim().parse().unwrap_or(1);
519        let model_idx = model_idx.saturating_sub(1).min(models.len() - 1);
520        let selected_model = models[model_idx].0.to_string();
521
522        bedrock_config.default_model = Some(selected_model.clone());
523        println!("{}", format!("✓ Default model: {}", models[model_idx].1.split(" - ").next().unwrap_or(&selected_model)).green());
524
525        // Save configuration
526        let mut agent_config = load_agent_config();
527        agent_config.bedrock = Some(bedrock_config);
528        agent_config.bedrock_configured = Some(true);
529
530        if let Err(e) = save_agent_config(&agent_config) {
531            eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
532        } else {
533            println!();
534            println!("{}", "✓ Configuration saved to ~/.syncable.toml".green());
535        }
536
537        println!();
538        println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
539        println!("{}", "  ✅ AWS Bedrock setup complete!".green().bold());
540        println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
541        println!();
542
543        Ok(selected_model)
544    }
545
546    /// Prompt user to enter API key for a provider
547    pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
548        // Bedrock uses AWS credential chain - run setup wizard
549        if matches!(provider, ProviderType::Bedrock) {
550            return Self::run_bedrock_setup_wizard();
551        }
552
553        let env_var = match provider {
554            ProviderType::OpenAI => "OPENAI_API_KEY",
555            ProviderType::Anthropic => "ANTHROPIC_API_KEY",
556            ProviderType::Bedrock => unreachable!(),  // Handled above
557        };
558
559        println!("\n{}", format!("🔑 No API key found for {}", provider).yellow());
560        println!("Please enter your {} API key:", provider);
561        print!("> ");
562        io::stdout().flush().unwrap();
563
564        let mut key = String::new();
565        io::stdin().read_line(&mut key).map_err(|e| AgentError::ToolError(e.to_string()))?;
566        let key = key.trim().to_string();
567
568        if key.is_empty() {
569            return Err(AgentError::MissingApiKey(env_var.to_string()));
570        }
571
572        // Set for current session
573        // SAFETY: We're in a single-threaded CLI context during initialization
574        unsafe {
575            std::env::set_var(env_var, &key);
576        }
577
578        // Save to config file for persistence
579        let mut agent_config = load_agent_config();
580        match provider {
581            ProviderType::OpenAI => agent_config.openai_api_key = Some(key.clone()),
582            ProviderType::Anthropic => agent_config.anthropic_api_key = Some(key.clone()),
583            ProviderType::Bedrock => unreachable!(),  // Handled above
584        }
585
586        if let Err(e) = save_agent_config(&agent_config) {
587            eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
588        } else {
589            println!("{}", "✓ API key saved to ~/.syncable.toml".green());
590        }
591
592        Ok(key)
593    }
594
595    /// Handle /model command - interactive model selection
596    pub fn handle_model_command(&mut self) -> AgentResult<()> {
597        let models = get_available_models(self.provider);
598        
599        println!("\n{}", format!("📋 Available models for {}:", self.provider).cyan().bold());
600        println!();
601        
602        for (i, (id, desc)) in models.iter().enumerate() {
603            let marker = if *id == self.model { "→ " } else { "  " };
604            let num = format!("[{}]", i + 1);
605            println!("  {} {} {} - {}", marker, num.dimmed(), id.white().bold(), desc.dimmed());
606        }
607        
608        println!();
609        println!("Enter number to select, or press Enter to keep current:");
610        print!("> ");
611        io::stdout().flush().unwrap();
612        
613        let mut input = String::new();
614        io::stdin().read_line(&mut input).ok();
615        let input = input.trim();
616        
617        if input.is_empty() {
618            println!("{}", format!("Keeping model: {}", self.model).dimmed());
619            return Ok(());
620        }
621        
622        if let Ok(num) = input.parse::<usize>() {
623            if num >= 1 && num <= models.len() {
624                let (id, desc) = models[num - 1];
625                self.model = id.to_string();
626
627                // Save model choice to config for persistence
628                let mut agent_config = load_agent_config();
629                agent_config.default_model = Some(id.to_string());
630                if let Err(e) = save_agent_config(&agent_config) {
631                    eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
632                }
633
634                println!("{}", format!("✓ Switched to {} - {}", id, desc).green());
635            } else {
636                println!("{}", "Invalid selection".red());
637            }
638        } else {
639            // Allow direct model name input
640            self.model = input.to_string();
641
642            // Save model choice to config for persistence
643            let mut agent_config = load_agent_config();
644            agent_config.default_model = Some(input.to_string());
645            if let Err(e) = save_agent_config(&agent_config) {
646                eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
647            }
648
649            println!("{}", format!("✓ Set model to: {}", input).green());
650        }
651
652        Ok(())
653    }
654
655    /// Handle /provider command - switch provider with API key prompt if needed
656    pub fn handle_provider_command(&mut self) -> AgentResult<()> {
657        let providers = [ProviderType::OpenAI, ProviderType::Anthropic, ProviderType::Bedrock];
658        
659        println!("\n{}", "🔄 Available providers:".cyan().bold());
660        println!();
661        
662        for (i, provider) in providers.iter().enumerate() {
663            let marker = if *provider == self.provider { "→ " } else { "  " };
664            let has_key = if Self::has_api_key(*provider) {
665                "✓ API key configured".green()
666            } else {
667                "⚠ No API key".yellow()
668            };
669            let num = format!("[{}]", i + 1);
670            println!("  {} {} {} - {}", marker, num.dimmed(), provider.to_string().white().bold(), has_key);
671        }
672        
673        println!();
674        println!("Enter number to select:");
675        print!("> ");
676        io::stdout().flush().unwrap();
677        
678        let mut input = String::new();
679        io::stdin().read_line(&mut input).ok();
680        let input = input.trim();
681        
682        if let Ok(num) = input.parse::<usize>() {
683            if num >= 1 && num <= providers.len() {
684                let new_provider = providers[num - 1];
685
686                // Check if API key exists, prompt if not
687                if !Self::has_api_key(new_provider) {
688                    Self::prompt_api_key(new_provider)?;
689                }
690
691                // Load API key/credentials from config to environment
692                // This is essential for Bedrock bearer token auth!
693                Self::load_api_key_to_env(new_provider);
694
695                self.provider = new_provider;
696
697                // Set default model for new provider (check saved config for Bedrock)
698                let default_model = match new_provider {
699                    ProviderType::OpenAI => "gpt-5.2".to_string(),
700                    ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
701                    ProviderType::Bedrock => {
702                        // Use saved model preference if available
703                        let agent_config = load_agent_config();
704                        agent_config.bedrock
705                            .and_then(|b| b.default_model)
706                            .unwrap_or_else(|| "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string())
707                    }
708                };
709                self.model = default_model.clone();
710
711                // Save provider choice to config for persistence
712                let mut agent_config = load_agent_config();
713                agent_config.default_provider = new_provider.to_string();
714                agent_config.default_model = Some(default_model.clone());
715                if let Err(e) = save_agent_config(&agent_config) {
716                    eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
717                }
718
719                println!("{}", format!("✓ Switched to {} with model {}", new_provider, default_model).green());
720            } else {
721                println!("{}", "Invalid selection".red());
722            }
723        }
724        
725        Ok(())
726    }
727
728    /// Handle /reset command - reset provider credentials
729    pub fn handle_reset_command(&mut self) -> AgentResult<()> {
730        let providers = [ProviderType::OpenAI, ProviderType::Anthropic, ProviderType::Bedrock];
731
732        println!("\n{}", "🔄 Reset Provider Credentials".cyan().bold());
733        println!();
734
735        for (i, provider) in providers.iter().enumerate() {
736            let status = if Self::has_api_key(*provider) {
737                "✓ configured".green()
738            } else {
739                "○ not configured".dimmed()
740            };
741            let num = format!("[{}]", i + 1);
742            println!("  {} {} - {}", num.dimmed(), provider.to_string().white().bold(), status);
743        }
744        println!("  {} All providers", "[4]".dimmed());
745        println!();
746        println!("Select provider to reset (or press Enter to cancel):");
747        print!("> ");
748        io::stdout().flush().unwrap();
749
750        let mut input = String::new();
751        io::stdin().read_line(&mut input).ok();
752        let input = input.trim();
753
754        if input.is_empty() {
755            println!("{}", "Cancelled".dimmed());
756            return Ok(());
757        }
758
759        let mut agent_config = load_agent_config();
760
761        match input {
762            "1" => {
763                agent_config.openai_api_key = None;
764                // SAFETY: Single-threaded CLI context during command handling
765                unsafe { std::env::remove_var("OPENAI_API_KEY"); }
766                println!("{}", "✓ OpenAI credentials cleared".green());
767            }
768            "2" => {
769                agent_config.anthropic_api_key = None;
770                unsafe { std::env::remove_var("ANTHROPIC_API_KEY"); }
771                println!("{}", "✓ Anthropic credentials cleared".green());
772            }
773            "3" => {
774                agent_config.bedrock = None;
775                agent_config.bedrock_configured = Some(false);
776                // SAFETY: Single-threaded CLI context during command handling
777                unsafe {
778                    std::env::remove_var("AWS_PROFILE");
779                    std::env::remove_var("AWS_ACCESS_KEY_ID");
780                    std::env::remove_var("AWS_SECRET_ACCESS_KEY");
781                    std::env::remove_var("AWS_REGION");
782                }
783                println!("{}", "✓ Bedrock credentials cleared".green());
784            }
785            "4" => {
786                agent_config.openai_api_key = None;
787                agent_config.anthropic_api_key = None;
788                agent_config.bedrock = None;
789                agent_config.bedrock_configured = Some(false);
790                // SAFETY: Single-threaded CLI context during command handling
791                unsafe {
792                    std::env::remove_var("OPENAI_API_KEY");
793                    std::env::remove_var("ANTHROPIC_API_KEY");
794                    std::env::remove_var("AWS_PROFILE");
795                    std::env::remove_var("AWS_ACCESS_KEY_ID");
796                    std::env::remove_var("AWS_SECRET_ACCESS_KEY");
797                    std::env::remove_var("AWS_REGION");
798                }
799                println!("{}", "✓ All provider credentials cleared".green());
800            }
801            _ => {
802                println!("{}", "Invalid selection".red());
803                return Ok(());
804            }
805        }
806
807        // Save updated config
808        if let Err(e) = save_agent_config(&agent_config) {
809            eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
810        } else {
811            println!("{}", "Configuration saved to ~/.syncable.toml".dimmed());
812        }
813
814        // Prompt to reconfigure if current provider was reset
815        let current_cleared = match input {
816            "1" => self.provider == ProviderType::OpenAI,
817            "2" => self.provider == ProviderType::Anthropic,
818            "3" => self.provider == ProviderType::Bedrock,
819            "4" => true,
820            _ => false,
821        };
822
823        if current_cleared {
824            println!();
825            println!("{}", "Current provider credentials were cleared.".yellow());
826            println!("Use {} to reconfigure or {} to switch providers.", "/provider".cyan(), "/p".cyan());
827        }
828
829        Ok(())
830    }
831
832    /// Handle /profile command - manage global profiles
833    pub fn handle_profile_command(&mut self) -> AgentResult<()> {
834        use crate::config::types::{Profile, OpenAIProfile, AnthropicProfile};
835
836        let mut agent_config = load_agent_config();
837
838        println!("\n{}", "👤 Profile Management".cyan().bold());
839        println!();
840
841        // Show current profiles
842        self.list_profiles(&agent_config);
843
844        println!("  {} Create new profile", "[1]".cyan());
845        println!("  {} Switch active profile", "[2]".cyan());
846        println!("  {} Configure provider in profile", "[3]".cyan());
847        println!("  {} Delete a profile", "[4]".cyan());
848        println!();
849        println!("Select action (or press Enter to cancel):");
850        print!("> ");
851        io::stdout().flush().unwrap();
852
853        let mut input = String::new();
854        io::stdin().read_line(&mut input).ok();
855        let input = input.trim();
856
857        if input.is_empty() {
858            println!("{}", "Cancelled".dimmed());
859            return Ok(());
860        }
861
862        match input {
863            "1" => {
864                // Create new profile
865                println!("\n{}", "Create Profile".white().bold());
866                print!("Profile name (e.g., work, personal): ");
867                io::stdout().flush().unwrap();
868                let mut name = String::new();
869                io::stdin().read_line(&mut name).ok();
870                let name = name.trim().to_string();
871
872                if name.is_empty() {
873                    println!("{}", "Profile name cannot be empty".red());
874                    return Ok(());
875                }
876
877                if agent_config.profiles.contains_key(&name) {
878                    println!("{}", format!("Profile '{}' already exists", name).yellow());
879                    return Ok(());
880                }
881
882                print!("Description (optional): ");
883                io::stdout().flush().unwrap();
884                let mut desc = String::new();
885                io::stdin().read_line(&mut desc).ok();
886                let desc = desc.trim();
887
888                let profile = Profile {
889                    description: if desc.is_empty() { None } else { Some(desc.to_string()) },
890                    default_provider: None,
891                    default_model: None,
892                    openai: None,
893                    anthropic: None,
894                    bedrock: None,
895                };
896
897                agent_config.profiles.insert(name.clone(), profile);
898
899                // Set as active if it's the first profile
900                if agent_config.active_profile.is_none() {
901                    agent_config.active_profile = Some(name.clone());
902                }
903
904                if let Err(e) = save_agent_config(&agent_config) {
905                    eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
906                }
907
908                println!("{}", format!("✓ Profile '{}' created", name).green());
909                println!("{}", "Use option [3] to configure providers for this profile".dimmed());
910            }
911            "2" => {
912                // Switch active profile
913                if agent_config.profiles.is_empty() {
914                    println!("{}", "No profiles configured. Create one first with option [1].".yellow());
915                    return Ok(());
916                }
917
918                print!("Enter profile name to activate: ");
919                io::stdout().flush().unwrap();
920                let mut name = String::new();
921                io::stdin().read_line(&mut name).ok();
922                let name = name.trim().to_string();
923
924                if name.is_empty() {
925                    println!("{}", "Cancelled".dimmed());
926                    return Ok(());
927                }
928
929                if !agent_config.profiles.contains_key(&name) {
930                    println!("{}", format!("Profile '{}' not found", name).red());
931                    return Ok(());
932                }
933
934                agent_config.active_profile = Some(name.clone());
935
936                // Load credentials from the new profile
937                if let Some(profile) = agent_config.profiles.get(&name) {
938                    // Clear old env vars and load new ones
939                    if let Some(openai) = &profile.openai {
940                        unsafe { std::env::set_var("OPENAI_API_KEY", &openai.api_key); }
941                    }
942                    if let Some(anthropic) = &profile.anthropic {
943                        unsafe { std::env::set_var("ANTHROPIC_API_KEY", &anthropic.api_key); }
944                    }
945                    if let Some(bedrock) = &profile.bedrock {
946                        if let Some(region) = &bedrock.region {
947                            unsafe { std::env::set_var("AWS_REGION", region); }
948                        }
949                        if let Some(aws_profile) = &bedrock.profile {
950                            unsafe { std::env::set_var("AWS_PROFILE", aws_profile); }
951                        } else if let (Some(key_id), Some(secret)) = (&bedrock.access_key_id, &bedrock.secret_access_key) {
952                            unsafe {
953                                std::env::set_var("AWS_ACCESS_KEY_ID", key_id);
954                                std::env::set_var("AWS_SECRET_ACCESS_KEY", secret);
955                            }
956                        }
957                    }
958
959                    // Update current provider if profile has a default
960                    if let Some(default_provider) = &profile.default_provider {
961                        if let Ok(p) = default_provider.parse() {
962                            self.provider = p;
963                        }
964                    }
965                }
966
967                if let Err(e) = save_agent_config(&agent_config) {
968                    eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
969                }
970
971                println!("{}", format!("✓ Switched to profile '{}'", name).green());
972            }
973            "3" => {
974                // Configure provider in profile
975                let profile_name = if let Some(name) = &agent_config.active_profile {
976                    name.clone()
977                } else if agent_config.profiles.is_empty() {
978                    println!("{}", "No profiles configured. Create one first with option [1].".yellow());
979                    return Ok(());
980                } else {
981                    print!("Enter profile name to configure: ");
982                    io::stdout().flush().unwrap();
983                    let mut name = String::new();
984                    io::stdin().read_line(&mut name).ok();
985                    name.trim().to_string()
986                };
987
988                if profile_name.is_empty() {
989                    println!("{}", "Cancelled".dimmed());
990                    return Ok(());
991                }
992
993                if !agent_config.profiles.contains_key(&profile_name) {
994                    println!("{}", format!("Profile '{}' not found", profile_name).red());
995                    return Ok(());
996                }
997
998                println!("\n{}", format!("Configure provider for '{}':", profile_name).white().bold());
999                println!("  {} OpenAI", "[1]".cyan());
1000                println!("  {} Anthropic", "[2]".cyan());
1001                println!("  {} AWS Bedrock", "[3]".cyan());
1002                print!("> ");
1003                io::stdout().flush().unwrap();
1004
1005                let mut provider_choice = String::new();
1006                io::stdin().read_line(&mut provider_choice).ok();
1007
1008                match provider_choice.trim() {
1009                    "1" => {
1010                        // Configure OpenAI
1011                        print!("OpenAI API Key: ");
1012                        io::stdout().flush().unwrap();
1013                        let mut api_key = String::new();
1014                        io::stdin().read_line(&mut api_key).ok();
1015                        let api_key = api_key.trim().to_string();
1016
1017                        if api_key.is_empty() {
1018                            println!("{}", "API key cannot be empty".red());
1019                            return Ok(());
1020                        }
1021
1022                        if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
1023                            profile.openai = Some(OpenAIProfile {
1024                                api_key,
1025                                description: None,
1026                                default_model: None,
1027                            });
1028                        }
1029                        println!("{}", format!("✓ OpenAI configured for profile '{}'", profile_name).green());
1030                    }
1031                    "2" => {
1032                        // Configure Anthropic
1033                        print!("Anthropic API Key: ");
1034                        io::stdout().flush().unwrap();
1035                        let mut api_key = String::new();
1036                        io::stdin().read_line(&mut api_key).ok();
1037                        let api_key = api_key.trim().to_string();
1038
1039                        if api_key.is_empty() {
1040                            println!("{}", "API key cannot be empty".red());
1041                            return Ok(());
1042                        }
1043
1044                        if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
1045                            profile.anthropic = Some(AnthropicProfile {
1046                                api_key,
1047                                description: None,
1048                                default_model: None,
1049                            });
1050                        }
1051                        println!("{}", format!("✓ Anthropic configured for profile '{}'", profile_name).green());
1052                    }
1053                    "3" => {
1054                        // Configure Bedrock - use the wizard
1055                        println!("{}", "Running Bedrock setup...".dimmed());
1056                        let selected_model = Self::run_bedrock_setup_wizard()?;
1057
1058                        // Get the saved bedrock config and copy it to the profile
1059                        let fresh_config = load_agent_config();
1060                        if let Some(bedrock) = fresh_config.bedrock.clone() {
1061                            if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
1062                                profile.bedrock = Some(bedrock);
1063                                profile.default_model = Some(selected_model);
1064                            }
1065                        }
1066                        println!("{}", format!("✓ Bedrock configured for profile '{}'", profile_name).green());
1067                    }
1068                    _ => {
1069                        println!("{}", "Invalid selection".red());
1070                        return Ok(());
1071                    }
1072                }
1073
1074                if let Err(e) = save_agent_config(&agent_config) {
1075                    eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
1076                }
1077            }
1078            "4" => {
1079                // Delete profile
1080                if agent_config.profiles.is_empty() {
1081                    println!("{}", "No profiles to delete.".yellow());
1082                    return Ok(());
1083                }
1084
1085                print!("Enter profile name to delete: ");
1086                io::stdout().flush().unwrap();
1087                let mut name = String::new();
1088                io::stdin().read_line(&mut name).ok();
1089                let name = name.trim().to_string();
1090
1091                if name.is_empty() {
1092                    println!("{}", "Cancelled".dimmed());
1093                    return Ok(());
1094                }
1095
1096                if agent_config.profiles.remove(&name).is_some() {
1097                    // If this was the active profile, clear it
1098                    if agent_config.active_profile.as_deref() == Some(name.as_str()) {
1099                        agent_config.active_profile = None;
1100                    }
1101
1102                    if let Err(e) = save_agent_config(&agent_config) {
1103                        eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
1104                    }
1105
1106                    println!("{}", format!("✓ Deleted profile '{}'", name).green());
1107                } else {
1108                    println!("{}", format!("Profile '{}' not found", name).red());
1109                }
1110            }
1111            _ => {
1112                println!("{}", "Invalid selection".red());
1113            }
1114        }
1115
1116        Ok(())
1117    }
1118
1119    /// Handle /plans command - show incomplete plans and offer to continue
1120    pub fn handle_plans_command(&self) -> AgentResult<()> {
1121        let incomplete = find_incomplete_plans(&self.project_path);
1122
1123        if incomplete.is_empty() {
1124            println!("\n{}", "No incomplete plans found.".dimmed());
1125            println!("{}", "Create a plan using plan mode (Shift+Tab) and the plan_create tool.".dimmed());
1126            return Ok(());
1127        }
1128
1129        println!("\n{}", "📋 Incomplete Plans".cyan().bold());
1130        println!();
1131
1132        for (i, plan) in incomplete.iter().enumerate() {
1133            let progress = format!("{}/{}", plan.done, plan.total);
1134            let percent = if plan.total > 0 {
1135                (plan.done as f64 / plan.total as f64 * 100.0) as usize
1136            } else {
1137                0
1138            };
1139
1140            println!(
1141                "  {} {} {} ({} - {}%)",
1142                format!("[{}]", i + 1).cyan(),
1143                plan.filename.white().bold(),
1144                format!("({} pending)", plan.pending).yellow(),
1145                progress.dimmed(),
1146                percent
1147            );
1148            println!("      {}", plan.path.dimmed());
1149        }
1150
1151        println!();
1152        println!("{}", "To continue a plan, say:".dimmed());
1153        println!("  {}", "\"continue the plan at plans/FILENAME.md\"".cyan());
1154        println!("  {}", "or just \"continue\" to resume the most recent one".cyan());
1155        println!();
1156
1157        Ok(())
1158    }
1159
1160    /// List all profiles
1161    fn list_profiles(&self, config: &crate::config::types::AgentConfig) {
1162        let active = config.active_profile.as_deref();
1163
1164        if config.profiles.is_empty() {
1165            println!("{}", "  No profiles configured yet.".dimmed());
1166            println!();
1167            return;
1168        }
1169
1170        println!("{}", "📋 Profiles:".cyan());
1171        for (name, profile) in &config.profiles {
1172            let marker = if Some(name.as_str()) == active { "→ " } else { "  " };
1173            let desc = profile.description.as_deref().unwrap_or("");
1174            let desc_fmt = if desc.is_empty() { String::new() } else { format!(" - {}", desc) };
1175
1176            // Show which providers are configured
1177            let mut providers = Vec::new();
1178            if profile.openai.is_some() { providers.push("OpenAI"); }
1179            if profile.anthropic.is_some() { providers.push("Anthropic"); }
1180            if profile.bedrock.is_some() { providers.push("Bedrock"); }
1181
1182            let providers_str = if providers.is_empty() {
1183                "(no providers configured)".to_string()
1184            } else {
1185                format!("[{}]", providers.join(", "))
1186            };
1187
1188            println!("  {} {}{} {}", marker, name.white().bold(), desc_fmt.dimmed(), providers_str.dimmed());
1189        }
1190        println!();
1191    }
1192
1193    /// Handle /help command
1194    pub fn print_help() {
1195        println!();
1196        println!("  {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET);
1197        println!("  {}📖 Available Commands{}", ansi::PURPLE, ansi::RESET);
1198        println!("  {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET);
1199        println!();
1200        
1201        for cmd in SLASH_COMMANDS.iter() {
1202            let alias = cmd.alias.map(|a| format!(" ({})", a)).unwrap_or_default();
1203            println!("  {}/{:<12}{}{} - {}{}{}", 
1204                ansi::CYAN, cmd.name, alias, ansi::RESET,
1205                ansi::DIM, cmd.description, ansi::RESET
1206            );
1207        }
1208        
1209        println!();
1210        println!("  {}Tip: Type / to see interactive command picker!{}", ansi::DIM, ansi::RESET);
1211        println!();
1212    }
1213
1214
1215    /// Print session banner with colorful SYNCABLE ASCII art
1216    pub fn print_logo() {
1217    // Colors matching the logo gradient: purple → orange → pink
1218    // Using ANSI 256 colors for better gradient
1219
1220        // Purple shades for S, y
1221        let purple = "\x1b[38;5;141m";  // Light purple
1222        // Orange shades for n, c  
1223        let orange = "\x1b[38;5;216m";  // Peach/orange
1224        // Pink shades for a, b, l, e
1225        let pink = "\x1b[38;5;212m";    // Hot pink
1226        let magenta = "\x1b[38;5;207m"; // Magenta
1227        let reset = "\x1b[0m";
1228
1229        println!();
1230        println!(
1231            "{}  ███████╗{}{} ██╗   ██╗{}{}███╗   ██╗{}{} ██████╗{}{}  █████╗ {}{}██████╗ {}{}██╗     {}{}███████╗{}",
1232            purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1233        );
1234        println!(
1235            "{}  ██╔════╝{}{} ╚██╗ ██╔╝{}{}████╗  ██║{}{} ██╔════╝{}{} ██╔══██╗{}{}██╔══██╗{}{}██║     {}{}██╔════╝{}",
1236            purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1237        );
1238        println!(
1239            "{}  ███████╗{}{}  ╚████╔╝ {}{}██╔██╗ ██║{}{} ██║     {}{} ███████║{}{}██████╔╝{}{}██║     {}{}█████╗  {}",
1240            purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1241        );
1242        println!(
1243            "{}  ╚════██║{}{}   ╚██╔╝  {}{}██║╚██╗██║{}{} ██║     {}{} ██╔══██║{}{}██╔══██╗{}{}██║     {}{}██╔══╝  {}",
1244            purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1245        );
1246        println!(
1247            "{}  ███████║{}{}    ██║   {}{}██║ ╚████║{}{} ╚██████╗{}{} ██║  ██║{}{}██████╔╝{}{}███████╗{}{}███████╗{}",
1248            purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1249        );
1250        println!(
1251            "{}  ╚══════╝{}{}    ╚═╝   {}{}╚═╝  ╚═══╝{}{}  ╚═════╝{}{} ╚═╝  ╚═╝{}{}╚═════╝ {}{}╚══════╝{}{}╚══════╝{}",
1252            purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1253        );
1254        println!();
1255    }
1256
1257    /// Print the welcome banner
1258    pub fn print_banner(&self) {
1259        // Print the gradient ASCII logo
1260        Self::print_logo();
1261
1262        // Platform promo
1263        println!(
1264            "  {} {}",
1265            "🚀".dimmed(),
1266            "Want to deploy? Deploy instantly from Syncable Platform → https://syncable.dev".dimmed()
1267        );
1268        println!();
1269
1270        // Print agent info
1271        println!(
1272            "  {} {} powered by {}: {}",
1273            ROBOT,
1274            "Syncable Agent".white().bold(),
1275            self.provider.to_string().cyan(),
1276            self.model.cyan()
1277        );
1278        println!(
1279            "  {}",
1280            "Your AI-powered code analysis assistant".dimmed()
1281        );
1282
1283        // Check for incomplete plans and show a hint
1284        let incomplete_plans = find_incomplete_plans(&self.project_path);
1285        if !incomplete_plans.is_empty() {
1286            println!();
1287            if incomplete_plans.len() == 1 {
1288                let plan = &incomplete_plans[0];
1289                println!(
1290                    "  {} {} ({}/{} done)",
1291                    "📋 Incomplete plan:".yellow(),
1292                    plan.filename.white(),
1293                    plan.done,
1294                    plan.total
1295                );
1296                println!(
1297                    "     {} \"{}\" {}",
1298                    "→".cyan(),
1299                    "continue".cyan().bold(),
1300                    "to resume".dimmed()
1301                );
1302            } else {
1303                println!(
1304                    "  {} {} incomplete plans found. Use {} to see them.",
1305                    "📋".yellow(),
1306                    incomplete_plans.len(),
1307                    "/plans".cyan()
1308                );
1309            }
1310        }
1311
1312        println!();
1313        println!(
1314            "  {} Type your questions. Use {} to exit.\n",
1315            "→".cyan(),
1316            "exit".yellow().bold()
1317        );
1318    }
1319
1320
1321    /// Process a command (returns true if should continue, false if should exit)
1322    pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
1323        let cmd = input.trim().to_lowercase();
1324        
1325        // Handle bare "/" - now handled interactively in read_input
1326        // Just show help if they somehow got here
1327        if cmd == "/" {
1328            Self::print_help();
1329            return Ok(true);
1330        }
1331        
1332        match cmd.as_str() {
1333            "/exit" | "/quit" | "/q" => {
1334                println!("\n{}", "👋 Goodbye!".green());
1335                return Ok(false);
1336            }
1337            "/help" | "/h" | "/?" => {
1338                Self::print_help();
1339            }
1340            "/model" | "/m" => {
1341                self.handle_model_command()?;
1342            }
1343            "/provider" | "/p" => {
1344                self.handle_provider_command()?;
1345            }
1346            "/cost" => {
1347                self.token_usage.print_report(&self.model);
1348            }
1349            "/clear" | "/c" => {
1350                self.history.clear();
1351                println!("{}", "✓ Conversation history cleared".green());
1352            }
1353            "/reset" | "/r" => {
1354                self.handle_reset_command()?;
1355            }
1356            "/profile" => {
1357                self.handle_profile_command()?;
1358            }
1359            "/plans" => {
1360                self.handle_plans_command()?;
1361            }
1362            _ => {
1363                if cmd.starts_with('/') {
1364                    // Unknown command - interactive picker already handled in read_input
1365                    println!("{}", format!("Unknown command: {}. Type /help for available commands.", cmd).yellow());
1366                }
1367            }
1368        }
1369        
1370        Ok(true)
1371    }
1372
1373    /// Check if input is a command
1374    pub fn is_command(input: &str) -> bool {
1375        input.trim().starts_with('/')
1376    }
1377
1378    /// Strip @ prefix from file/folder references for AI consumption
1379    /// Keeps the path but removes the leading @ that was used for autocomplete
1380    /// e.g., "check @src/main.rs for issues" -> "check src/main.rs for issues"
1381    fn strip_file_references(input: &str) -> String {
1382        let mut result = String::with_capacity(input.len());
1383        let chars: Vec<char> = input.chars().collect();
1384        let mut i = 0;
1385
1386        while i < chars.len() {
1387            if chars[i] == '@' {
1388                // Check if this @ is at start or after whitespace (valid file reference trigger)
1389                let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
1390
1391                if is_valid_trigger {
1392                    // Check if there's a path after @ (not just @ followed by space/end)
1393                    let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
1394
1395                    if has_path {
1396                        // Skip the @ but keep the path
1397                        i += 1;
1398                        continue;
1399                    }
1400                }
1401            }
1402            result.push(chars[i]);
1403            i += 1;
1404        }
1405
1406        result
1407    }
1408
1409    /// Read user input with prompt - with interactive file picker support
1410    /// Uses custom terminal handling for @ file references and / commands
1411    /// Returns InputResult which the main loop should handle
1412    pub fn read_input(&self) -> io::Result<crate::agent::ui::input::InputResult> {
1413        use crate::agent::ui::input::read_input_with_file_picker;
1414
1415        Ok(read_input_with_file_picker("You:", &self.project_path, self.plan_mode.is_planning()))
1416    }
1417
1418    /// Process a submitted input text - strips @ references and handles suggestion format
1419    pub fn process_submitted_text(text: &str) -> String {
1420        let trimmed = text.trim();
1421        // Handle case where full suggestion was submitted (e.g., "/model        Description")
1422        // Extract just the command if it looks like a suggestion format
1423        if trimmed.starts_with('/') && trimmed.contains("  ") {
1424            // This looks like a suggestion format, extract just the command
1425            if let Some(cmd) = trimmed.split_whitespace().next() {
1426                return cmd.to_string();
1427            }
1428        }
1429        // Strip @ prefix from file references before sending to AI
1430        // The @ is for UI autocomplete, but the AI should see just the path
1431        Self::strip_file_references(trimmed)
1432    }
1433}