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