1use 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#[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
31pub 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, _ => {}
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 incomplete.sort_by(|a, b| b.filename.cmp(&a.filename));
89 incomplete
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub enum PlanMode {
95 #[default]
97 Standard,
98 Planning,
100}
101
102impl PlanMode {
103 pub fn toggle(&self) -> Self {
105 match self {
106 PlanMode::Standard => PlanMode::Planning,
107 PlanMode::Planning => PlanMode::Standard,
108 }
109 }
110
111 pub fn is_planning(&self) -> bool {
113 matches!(self, PlanMode::Planning)
114 }
115
116 pub fn display_name(&self) -> &'static str {
118 match self {
119 PlanMode::Standard => "standard mode",
120 PlanMode::Planning => "plan mode",
121 }
122 }
123}
124
125pub 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 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
171pub struct ChatSession {
173 pub provider: ProviderType,
174 pub model: String,
175 pub project_path: std::path::PathBuf,
176 pub history: Vec<(String, String)>, pub token_usage: TokenUsage,
178 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 pub fn toggle_plan_mode(&mut self) -> PlanMode {
202 self.plan_mode = self.plan_mode.toggle();
203 self.plan_mode
204 }
205
206 pub fn is_planning(&self) -> bool {
208 self.plan_mode.is_planning()
209 }
210
211 pub fn has_api_key(provider: ProviderType) -> bool {
213 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 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 let agent_config = load_agent_config();
237
238 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 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 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 pub fn load_api_key_to_env(provider: ProviderType) {
327 let agent_config = load_agent_config();
328
329 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 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 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 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 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 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 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 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 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 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 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 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 unsafe {
511 std::env::set_var("AWS_PROFILE", profile);
512 }
513 println!("{}", format!("✓ Using profile: {}", profile).green());
514 }
515 "2" => {
516 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 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 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 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 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 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 pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
684 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!(), };
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 unsafe {
716 std::env::set_var(env_var, &key);
717 }
718
719 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!(), }
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 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 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 self.model = input.to_string();
799
800 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 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 if !Self::has_api_key(new_provider) {
863 Self::prompt_api_key(new_provider)?;
864 }
865
866 Self::load_api_key_to_env(new_provider);
869
870 self.provider = new_provider;
871
872 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if let Some(profile) = agent_config.profiles.get(&name) {
1159 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 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 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 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 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 println!("{}", "Running Bedrock setup...".dimmed());
1305 let selected_model = Self::run_bedrock_setup_wizard()?;
1306
1307 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 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 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 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 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 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 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 pub fn print_logo() {
1518 let purple = "\x1b[38;5;141m"; let orange = "\x1b[38;5;216m"; let pink = "\x1b[38;5;212m"; let magenta = "\x1b[38;5;207m"; 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 pub fn print_banner(&self) {
1650 Self::print_logo();
1652
1653 println!(
1655 " {} {}",
1656 "🚀".dimmed(),
1657 "Want to deploy? Deploy instantly from Syncable Platform → https://syncable.dev"
1658 .dimmed()
1659 );
1660 println!();
1661
1662 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 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 pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
1711 let cmd = input.trim().to_lowercase();
1712
1713 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 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 pub fn is_command(input: &str) -> bool {
1770 input.trim().starts_with('/')
1771 }
1772
1773 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 let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
1785
1786 if is_valid_trigger {
1787 let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
1789
1790 if has_path {
1791 i += 1;
1793 continue;
1794 }
1795 }
1796 }
1797 result.push(chars[i]);
1798 i += 1;
1799 }
1800
1801 result
1802 }
1803
1804 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 pub fn process_submitted_text(text: &str) -> String {
1819 let trimmed = text.trim();
1820 if trimmed.starts_with('/') && trimmed.contains(" ") {
1823 if let Some(cmd) = trimmed.split_whitespace().next() {
1825 return cmd.to_string();
1826 }
1827 }
1828 Self::strip_file_references(trimmed)
1831 }
1832}