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