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