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