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 pub fn handle_resume_command(&self) -> AgentResult<()> {
1426 use crate::agent::persistence::{SessionSelector, browse_sessions, format_relative_time};
1427
1428 let selector = SessionSelector::new(&self.project_path);
1429 let sessions = selector.list_sessions();
1430
1431 if sessions.is_empty() {
1432 println!(
1433 "\n{}",
1434 "No previous sessions found for this project.".yellow()
1435 );
1436 println!(
1437 "{}",
1438 "Sessions are automatically saved during conversations.".dimmed()
1439 );
1440 return Ok(());
1441 }
1442
1443 if let Some(selected) = browse_sessions(&self.project_path) {
1445 let time = format_relative_time(selected.last_updated);
1447 println!(
1448 "\n{} Selected: {} ({}, {} messages)",
1449 "โ".green(),
1450 selected.display_name.white().bold(),
1451 time.dimmed(),
1452 selected.message_count
1453 );
1454 println!(
1455 "{}",
1456 "Note: To load the session, restart with: sync-ctl chat --resume <ID>".dimmed()
1457 );
1458 println!(" Session ID: {}", selected.id.cyan());
1459 }
1460
1461 Ok(())
1462 }
1463
1464 pub fn handle_list_sessions_command(&self) {
1466 use crate::agent::persistence::{SessionSelector, format_relative_time};
1467
1468 let selector = SessionSelector::new(&self.project_path);
1469 let sessions = selector.list_sessions();
1470
1471 if sessions.is_empty() {
1472 println!(
1473 "\n{}",
1474 "No previous sessions found for this project.".yellow()
1475 );
1476 return;
1477 }
1478
1479 println!(
1480 "\n{}",
1481 format!("๐ Sessions ({})", sessions.len()).cyan().bold()
1482 );
1483 println!();
1484
1485 for session in &sessions {
1486 let time = format_relative_time(session.last_updated);
1487 println!(
1488 " {} {} {}",
1489 format!("[{}]", session.index).cyan(),
1490 session.display_name.white(),
1491 format!("({})", time).dimmed()
1492 );
1493 println!(
1494 " {} messages ยท ID: {}",
1495 session.message_count.to_string().dimmed(),
1496 session.id[..8].to_string().dimmed()
1497 );
1498 }
1499
1500 println!();
1501 println!("{}", "To resume a session:".dimmed());
1502 println!(
1503 " {} or {}",
1504 "/resume".cyan(),
1505 "sync-ctl chat --resume <NUMBER|ID>".cyan()
1506 );
1507 println!();
1508 }
1509
1510 fn list_profiles(&self, config: &crate::config::types::AgentConfig) {
1512 let active = config.active_profile.as_deref();
1513
1514 if config.profiles.is_empty() {
1515 println!("{}", " No profiles configured yet.".dimmed());
1516 println!();
1517 return;
1518 }
1519
1520 println!("{}", "๐ Profiles:".cyan());
1521 for (name, profile) in &config.profiles {
1522 let marker = if Some(name.as_str()) == active {
1523 "โ "
1524 } else {
1525 " "
1526 };
1527 let desc = profile.description.as_deref().unwrap_or("");
1528 let desc_fmt = if desc.is_empty() {
1529 String::new()
1530 } else {
1531 format!(" - {}", desc)
1532 };
1533
1534 let mut providers = Vec::new();
1536 if profile.openai.is_some() {
1537 providers.push("OpenAI");
1538 }
1539 if profile.anthropic.is_some() {
1540 providers.push("Anthropic");
1541 }
1542 if profile.bedrock.is_some() {
1543 providers.push("Bedrock");
1544 }
1545
1546 let providers_str = if providers.is_empty() {
1547 "(no providers configured)".to_string()
1548 } else {
1549 format!("[{}]", providers.join(", "))
1550 };
1551
1552 println!(
1553 " {} {}{} {}",
1554 marker,
1555 name.white().bold(),
1556 desc_fmt.dimmed(),
1557 providers_str.dimmed()
1558 );
1559 }
1560 println!();
1561 }
1562
1563 pub fn print_help() {
1565 println!();
1566 println!(
1567 " {}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ{}",
1568 ansi::PURPLE,
1569 ansi::RESET
1570 );
1571 println!(" {}๐ Available Commands{}", ansi::PURPLE, ansi::RESET);
1572 println!(
1573 " {}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ{}",
1574 ansi::PURPLE,
1575 ansi::RESET
1576 );
1577 println!();
1578
1579 for cmd in SLASH_COMMANDS.iter() {
1580 let alias = cmd.alias.map(|a| format!(" ({})", a)).unwrap_or_default();
1581 println!(
1582 " {}/{:<12}{}{} - {}{}{}",
1583 ansi::CYAN,
1584 cmd.name,
1585 alias,
1586 ansi::RESET,
1587 ansi::DIM,
1588 cmd.description,
1589 ansi::RESET
1590 );
1591 }
1592
1593 println!();
1594 println!(
1595 " {}Tip: Type / to see interactive command picker!{}",
1596 ansi::DIM,
1597 ansi::RESET
1598 );
1599 println!();
1600 }
1601
1602 pub fn print_logo() {
1604 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";
1615
1616 println!();
1617 println!(
1618 "{} โโโโโโโโ{}{} โโโ โโโ{}{}โโโโ โโโ{}{} โโโโโโโ{}{} โโโโโโ {}{}โโโโโโโ {}{}โโโ {}{}โโโโโโโโ{}",
1619 purple,
1620 reset,
1621 purple,
1622 reset,
1623 orange,
1624 reset,
1625 orange,
1626 reset,
1627 pink,
1628 reset,
1629 pink,
1630 reset,
1631 magenta,
1632 reset,
1633 magenta,
1634 reset
1635 );
1636 println!(
1637 "{} โโโโโโโโ{}{} โโโโ โโโโ{}{}โโโโโ โโโ{}{} โโโโโโโโ{}{} โโโโโโโโ{}{}โโโโโโโโ{}{}โโโ {}{}โโโโโโโโ{}",
1638 purple,
1639 reset,
1640 purple,
1641 reset,
1642 orange,
1643 reset,
1644 orange,
1645 reset,
1646 pink,
1647 reset,
1648 pink,
1649 reset,
1650 magenta,
1651 reset,
1652 magenta,
1653 reset
1654 );
1655 println!(
1656 "{} โโโโโโโโ{}{} โโโโโโโ {}{}โโโโโโ โโโ{}{} โโโ {}{} โโโโโโโโ{}{}โโโโโโโโ{}{}โโโ {}{}โโโโโโ {}",
1657 purple,
1658 reset,
1659 purple,
1660 reset,
1661 orange,
1662 reset,
1663 orange,
1664 reset,
1665 pink,
1666 reset,
1667 pink,
1668 reset,
1669 magenta,
1670 reset,
1671 magenta,
1672 reset
1673 );
1674 println!(
1675 "{} โโโโโโโโ{}{} โโโโโ {}{}โโโโโโโโโโ{}{} โโโ {}{} โโโโโโโโ{}{}โโโโโโโโ{}{}โโโ {}{}โโโโโโ {}",
1676 purple,
1677 reset,
1678 purple,
1679 reset,
1680 orange,
1681 reset,
1682 orange,
1683 reset,
1684 pink,
1685 reset,
1686 pink,
1687 reset,
1688 magenta,
1689 reset,
1690 magenta,
1691 reset
1692 );
1693 println!(
1694 "{} โโโโโโโโ{}{} โโโ {}{}โโโ โโโโโโ{}{} โโโโโโโโ{}{} โโโ โโโ{}{}โโโโโโโโ{}{}โโโโโโโโ{}{}โโโโโโโโ{}",
1695 purple,
1696 reset,
1697 purple,
1698 reset,
1699 orange,
1700 reset,
1701 orange,
1702 reset,
1703 pink,
1704 reset,
1705 pink,
1706 reset,
1707 magenta,
1708 reset,
1709 magenta,
1710 reset
1711 );
1712 println!(
1713 "{} โโโโโโโโ{}{} โโโ {}{}โโโ โโโโโ{}{} โโโโโโโ{}{} โโโ โโโ{}{}โโโโโโโ {}{}โโโโโโโโ{}{}โโโโโโโโ{}",
1714 purple,
1715 reset,
1716 purple,
1717 reset,
1718 orange,
1719 reset,
1720 orange,
1721 reset,
1722 pink,
1723 reset,
1724 pink,
1725 reset,
1726 magenta,
1727 reset,
1728 magenta,
1729 reset
1730 );
1731 println!();
1732 }
1733
1734 pub fn print_banner(&self) {
1736 Self::print_logo();
1738
1739 println!(
1741 " {} {}",
1742 "๐".dimmed(),
1743 "Want to deploy? Deploy instantly from Syncable Platform โ https://syncable.dev"
1744 .dimmed()
1745 );
1746 println!();
1747
1748 println!(
1750 " {} {} powered by {}: {}",
1751 ROBOT,
1752 "Syncable Agent".white().bold(),
1753 self.provider.to_string().cyan(),
1754 self.model.cyan()
1755 );
1756 println!(" {}", "Your AI-powered code analysis assistant".dimmed());
1757
1758 let incomplete_plans = find_incomplete_plans(&self.project_path);
1760 if !incomplete_plans.is_empty() {
1761 println!();
1762 if incomplete_plans.len() == 1 {
1763 let plan = &incomplete_plans[0];
1764 println!(
1765 " {} {} ({}/{} done)",
1766 "๐ Incomplete plan:".yellow(),
1767 plan.filename.white(),
1768 plan.done,
1769 plan.total
1770 );
1771 println!(
1772 " {} \"{}\" {}",
1773 "โ".cyan(),
1774 "continue".cyan().bold(),
1775 "to resume".dimmed()
1776 );
1777 } else {
1778 println!(
1779 " {} {} incomplete plans found. Use {} to see them.",
1780 "๐".yellow(),
1781 incomplete_plans.len(),
1782 "/plans".cyan()
1783 );
1784 }
1785 }
1786
1787 println!();
1788 println!(
1789 " {} Type your questions. Use {} to exit.\n",
1790 "โ".cyan(),
1791 "exit".yellow().bold()
1792 );
1793 }
1794
1795 pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
1797 let cmd = input.trim().to_lowercase();
1798
1799 if cmd == "/" {
1802 Self::print_help();
1803 return Ok(true);
1804 }
1805
1806 match cmd.as_str() {
1807 "/exit" | "/quit" | "/q" => {
1808 println!("\n{}", "๐ Goodbye!".green());
1809 return Ok(false);
1810 }
1811 "/help" | "/h" | "/?" => {
1812 Self::print_help();
1813 }
1814 "/model" | "/m" => {
1815 self.handle_model_command()?;
1816 }
1817 "/provider" | "/p" => {
1818 self.handle_provider_command()?;
1819 }
1820 "/cost" => {
1821 self.token_usage.print_report(&self.model);
1822 }
1823 "/clear" | "/c" => {
1824 self.history.clear();
1825 println!("{}", "โ Conversation history cleared".green());
1826 }
1827 "/reset" | "/r" => {
1828 self.handle_reset_command()?;
1829 }
1830 "/profile" => {
1831 self.handle_profile_command()?;
1832 }
1833 "/plans" => {
1834 self.handle_plans_command()?;
1835 }
1836 "/resume" | "/s" => {
1837 self.handle_resume_command()?;
1840 }
1841 "/sessions" | "/ls" => {
1842 self.handle_list_sessions_command();
1843 }
1844 _ => {
1845 if cmd.starts_with('/') {
1846 println!(
1848 "{}",
1849 format!(
1850 "Unknown command: {}. Type /help for available commands.",
1851 cmd
1852 )
1853 .yellow()
1854 );
1855 }
1856 }
1857 }
1858
1859 Ok(true)
1860 }
1861
1862 pub fn is_command(input: &str) -> bool {
1864 input.trim().starts_with('/')
1865 }
1866
1867 fn strip_file_references(input: &str) -> String {
1871 let mut result = String::with_capacity(input.len());
1872 let chars: Vec<char> = input.chars().collect();
1873 let mut i = 0;
1874
1875 while i < chars.len() {
1876 if chars[i] == '@' {
1877 let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
1879
1880 if is_valid_trigger {
1881 let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
1883
1884 if has_path {
1885 i += 1;
1887 continue;
1888 }
1889 }
1890 }
1891 result.push(chars[i]);
1892 i += 1;
1893 }
1894
1895 result
1896 }
1897
1898 pub fn read_input(&self) -> io::Result<crate::agent::ui::input::InputResult> {
1902 use crate::agent::ui::input::read_input_with_file_picker;
1903
1904 Ok(read_input_with_file_picker(
1905 ">",
1906 &self.project_path,
1907 self.plan_mode.is_planning(),
1908 ))
1909 }
1910
1911 pub fn process_submitted_text(text: &str) -> String {
1913 let trimmed = text.trim();
1914 if trimmed.starts_with('/') && trimmed.contains(" ") {
1917 if let Some(cmd) = trimmed.split_whitespace().next() {
1919 return cmd.to_string();
1920 }
1921 }
1922 Self::strip_file_references(trimmed)
1925 }
1926}