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