1use crate::agent::commands::{TokenUsage, SLASH_COMMANDS};
12use crate::agent::{AgentError, AgentResult, ProviderType};
13use crate::agent::ui::ansi;
14use crate::config::{load_agent_config, save_agent_config};
15use colored::Colorize;
16use std::io::{self, Write};
17use std::path::Path;
18
19const ROBOT: &str = "🤖";
20
21#[derive(Debug, Clone)]
23pub struct IncompletePlan {
24 pub path: String,
25 pub filename: String,
26 pub done: usize,
27 pub pending: usize,
28 pub total: usize,
29}
30
31pub fn find_incomplete_plans(project_path: &std::path::Path) -> Vec<IncompletePlan> {
33 use regex::Regex;
34
35 let plans_dir = project_path.join("plans");
36 if !plans_dir.exists() {
37 return Vec::new();
38 }
39
40 let task_regex = Regex::new(r"^\s*-\s*\[([ x~!])\]").unwrap();
41 let mut incomplete = Vec::new();
42
43 if let Ok(entries) = std::fs::read_dir(&plans_dir) {
44 for entry in entries.flatten() {
45 let path = entry.path();
46 if path.extension().map(|e| e == "md").unwrap_or(false) {
47 if let Ok(content) = std::fs::read_to_string(&path) {
48 let mut done = 0;
49 let mut pending = 0;
50 let mut in_progress = 0;
51
52 for line in content.lines() {
53 if let Some(caps) = task_regex.captures(line) {
54 match caps.get(1).map(|m| m.as_str()) {
55 Some("x") => done += 1,
56 Some(" ") => pending += 1,
57 Some("~") => in_progress += 1,
58 Some("!") => done += 1, _ => {}
60 }
61 }
62 }
63
64 let total = done + pending + in_progress;
65 if total > 0 && (pending > 0 || in_progress > 0) {
66 let rel_path = path.strip_prefix(project_path)
67 .map(|p| p.display().to_string())
68 .unwrap_or_else(|_| path.display().to_string());
69
70 incomplete.push(IncompletePlan {
71 path: rel_path,
72 filename: path.file_name()
73 .map(|n| n.to_string_lossy().to_string())
74 .unwrap_or_default(),
75 done,
76 pending: pending + in_progress,
77 total,
78 });
79 }
80 }
81 }
82 }
83 }
84
85 incomplete.sort_by(|a, b| b.filename.cmp(&a.filename));
87 incomplete
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum PlanMode {
93 #[default]
95 Standard,
96 Planning,
98}
99
100impl PlanMode {
101 pub fn toggle(&self) -> Self {
103 match self {
104 PlanMode::Standard => PlanMode::Planning,
105 PlanMode::Planning => PlanMode::Standard,
106 }
107 }
108
109 pub fn is_planning(&self) -> bool {
111 matches!(self, PlanMode::Planning)
112 }
113
114 pub fn display_name(&self) -> &'static str {
116 match self {
117 PlanMode::Standard => "standard mode",
118 PlanMode::Planning => "plan mode",
119 }
120 }
121}
122
123pub fn get_available_models(provider: ProviderType) -> Vec<(&'static str, &'static str)> {
125 match provider {
126 ProviderType::OpenAI => vec![
127 ("gpt-5.2", "GPT-5.2 - Latest reasoning model (Dec 2025)"),
128 ("gpt-5.2-mini", "GPT-5.2 Mini - Fast and affordable"),
129 ("gpt-4o", "GPT-4o - Multimodal workhorse"),
130 ("o1-preview", "o1-preview - Advanced reasoning"),
131 ],
132 ProviderType::Anthropic => vec![
133 ("claude-opus-4-5-20251101", "Claude Opus 4.5 - Most capable (Nov 2025)"),
134 ("claude-sonnet-4-5-20250929", "Claude Sonnet 4.5 - Balanced (Sep 2025)"),
135 ("claude-haiku-4-5-20251001", "Claude Haiku 4.5 - Fast (Oct 2025)"),
136 ("claude-sonnet-4-20250514", "Claude Sonnet 4 - Previous gen"),
137 ],
138 ProviderType::Bedrock => vec![
140 ("global.anthropic.claude-opus-4-5-20251101-v1:0", "Claude Opus 4.5 - Most capable (Nov 2025)"),
141 ("global.anthropic.claude-sonnet-4-5-20250929-v1:0", "Claude Sonnet 4.5 - Balanced (Sep 2025)"),
142 ("global.anthropic.claude-haiku-4-5-20251001-v1:0", "Claude Haiku 4.5 - Fast (Oct 2025)"),
143 ("global.anthropic.claude-sonnet-4-20250514-v1:0", "Claude Sonnet 4 - Previous gen"),
144 ],
145 }
146}
147
148pub struct ChatSession {
150 pub provider: ProviderType,
151 pub model: String,
152 pub project_path: std::path::PathBuf,
153 pub history: Vec<(String, String)>, pub token_usage: TokenUsage,
155 pub plan_mode: PlanMode,
157}
158
159impl ChatSession {
160 pub fn new(project_path: &Path, provider: ProviderType, model: Option<String>) -> Self {
161 let default_model = match provider {
162 ProviderType::OpenAI => "gpt-5.2".to_string(),
163 ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
164 ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string(),
165 };
166
167 Self {
168 provider,
169 model: model.unwrap_or(default_model),
170 project_path: project_path.to_path_buf(),
171 history: Vec::new(),
172 token_usage: TokenUsage::new(),
173 plan_mode: PlanMode::default(),
174 }
175 }
176
177 pub fn toggle_plan_mode(&mut self) -> PlanMode {
179 self.plan_mode = self.plan_mode.toggle();
180 self.plan_mode
181 }
182
183 pub fn is_planning(&self) -> bool {
185 self.plan_mode.is_planning()
186 }
187
188 pub fn has_api_key(provider: ProviderType) -> bool {
190 let env_key = match provider {
192 ProviderType::OpenAI => std::env::var("OPENAI_API_KEY").ok(),
193 ProviderType::Anthropic => std::env::var("ANTHROPIC_API_KEY").ok(),
194 ProviderType::Bedrock => {
195 if std::env::var("AWS_ACCESS_KEY_ID").is_ok() && std::env::var("AWS_SECRET_ACCESS_KEY").is_ok() {
197 return true;
198 }
199 if std::env::var("AWS_PROFILE").is_ok() {
200 return true;
201 }
202 None
203 }
204 };
205
206 if env_key.is_some() {
207 return true;
208 }
209
210 let agent_config = load_agent_config();
212
213 if let Some(profile_name) = &agent_config.active_profile {
215 if let Some(profile) = agent_config.profiles.get(profile_name) {
216 match provider {
217 ProviderType::OpenAI => {
218 if profile.openai.as_ref().map(|o| !o.api_key.is_empty()).unwrap_or(false) {
219 return true;
220 }
221 }
222 ProviderType::Anthropic => {
223 if profile.anthropic.as_ref().map(|a| !a.api_key.is_empty()).unwrap_or(false) {
224 return true;
225 }
226 }
227 ProviderType::Bedrock => {
228 if let Some(bedrock) = &profile.bedrock {
229 if bedrock.profile.is_some() ||
230 (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some()) {
231 return true;
232 }
233 }
234 }
235 }
236 }
237 }
238
239 for profile in agent_config.profiles.values() {
241 match provider {
242 ProviderType::OpenAI => {
243 if profile.openai.as_ref().map(|o| !o.api_key.is_empty()).unwrap_or(false) {
244 return true;
245 }
246 }
247 ProviderType::Anthropic => {
248 if profile.anthropic.as_ref().map(|a| !a.api_key.is_empty()).unwrap_or(false) {
249 return true;
250 }
251 }
252 ProviderType::Bedrock => {
253 if let Some(bedrock) = &profile.bedrock {
254 if bedrock.profile.is_some() ||
255 (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some()) {
256 return true;
257 }
258 }
259 }
260 }
261 }
262
263 match provider {
265 ProviderType::OpenAI => agent_config.openai_api_key.is_some(),
266 ProviderType::Anthropic => agent_config.anthropic_api_key.is_some(),
267 ProviderType::Bedrock => {
268 if let Some(bedrock) = &agent_config.bedrock {
269 bedrock.profile.is_some() ||
270 (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some())
271 } else {
272 agent_config.bedrock_configured.unwrap_or(false)
273 }
274 }
275 }
276 }
277
278 pub fn load_api_key_to_env(provider: ProviderType) {
280 let agent_config = load_agent_config();
281
282 let active_profile = agent_config.active_profile.as_ref()
284 .and_then(|name| agent_config.profiles.get(name));
285
286 match provider {
287 ProviderType::OpenAI => {
288 if std::env::var("OPENAI_API_KEY").is_ok() {
289 return;
290 }
291 if let Some(key) = active_profile
293 .and_then(|p| p.openai.as_ref())
294 .map(|o| o.api_key.clone())
295 .filter(|k| !k.is_empty())
296 {
297 unsafe { std::env::set_var("OPENAI_API_KEY", &key); }
298 return;
299 }
300 if let Some(key) = &agent_config.openai_api_key {
302 unsafe { std::env::set_var("OPENAI_API_KEY", key); }
303 }
304 }
305 ProviderType::Anthropic => {
306 if std::env::var("ANTHROPIC_API_KEY").is_ok() {
307 return;
308 }
309 if let Some(key) = active_profile
311 .and_then(|p| p.anthropic.as_ref())
312 .map(|a| a.api_key.clone())
313 .filter(|k| !k.is_empty())
314 {
315 unsafe { std::env::set_var("ANTHROPIC_API_KEY", &key); }
316 return;
317 }
318 if let Some(key) = &agent_config.anthropic_api_key {
320 unsafe { std::env::set_var("ANTHROPIC_API_KEY", key); }
321 }
322 }
323 ProviderType::Bedrock => {
324 let bedrock_config = active_profile
326 .and_then(|p| p.bedrock.as_ref())
327 .or(agent_config.bedrock.as_ref());
328
329 if let Some(bedrock) = bedrock_config {
330 if std::env::var("AWS_REGION").is_err() {
332 if let Some(region) = &bedrock.region {
333 unsafe { std::env::set_var("AWS_REGION", region); }
334 }
335 }
336 if let Some(profile) = &bedrock.profile {
338 if std::env::var("AWS_PROFILE").is_err() {
339 unsafe { std::env::set_var("AWS_PROFILE", profile); }
340 }
341 } else if let (Some(key_id), Some(secret)) = (&bedrock.access_key_id, &bedrock.secret_access_key) {
342 if std::env::var("AWS_ACCESS_KEY_ID").is_err() {
343 unsafe { std::env::set_var("AWS_ACCESS_KEY_ID", key_id); }
344 }
345 if std::env::var("AWS_SECRET_ACCESS_KEY").is_err() {
346 unsafe { std::env::set_var("AWS_SECRET_ACCESS_KEY", secret); }
347 }
348 }
349 }
350 }
351 }
352 }
353
354 pub fn get_configured_providers() -> Vec<ProviderType> {
356 let mut providers = Vec::new();
357 if Self::has_api_key(ProviderType::OpenAI) {
358 providers.push(ProviderType::OpenAI);
359 }
360 if Self::has_api_key(ProviderType::Anthropic) {
361 providers.push(ProviderType::Anthropic);
362 }
363 providers
364 }
365
366 fn run_bedrock_setup_wizard() -> AgentResult<String> {
368 use crate::config::types::BedrockConfig as BedrockConfigType;
369
370 println!();
371 println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
372 println!("{}", " 🔧 AWS Bedrock Setup Wizard".cyan().bold());
373 println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
374 println!();
375 println!("AWS Bedrock provides access to Claude models via AWS.");
376 println!("You'll need an AWS account with Bedrock access enabled.");
377 println!();
378
379 println!("{}", "Step 1: Choose authentication method".white().bold());
381 println!();
382 println!(" {} Use AWS Profile (from ~/.aws/credentials)", "[1]".cyan());
383 println!(" {}", "Best for: AWS CLI users, SSO, multiple accounts".dimmed());
384 println!();
385 println!(" {} Enter Access Keys directly", "[2]".cyan());
386 println!(" {}", "Best for: Quick setup, CI/CD environments".dimmed());
387 println!();
388 println!(" {} Use existing environment variables", "[3]".cyan());
389 println!(" {}", "Best for: Already configured AWS_* env vars".dimmed());
390 println!();
391 print!("Enter choice [1-3]: ");
392 io::stdout().flush().unwrap();
393
394 let mut choice = String::new();
395 io::stdin().read_line(&mut choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
396 let choice = choice.trim();
397
398 let mut bedrock_config = BedrockConfigType::default();
399
400 match choice {
401 "1" => {
402 println!();
404 println!("{}", "Step 2: Enter AWS Profile".white().bold());
405 println!("{}", "Press Enter for 'default' profile".dimmed());
406 print!("Profile name: ");
407 io::stdout().flush().unwrap();
408
409 let mut profile = String::new();
410 io::stdin().read_line(&mut profile).map_err(|e| AgentError::ToolError(e.to_string()))?;
411 let profile = profile.trim();
412 let profile = if profile.is_empty() { "default" } else { profile };
413
414 bedrock_config.profile = Some(profile.to_string());
415
416 unsafe { std::env::set_var("AWS_PROFILE", profile); }
418 println!("{}", format!("✓ Using profile: {}", profile).green());
419 }
420 "2" => {
421 println!();
423 println!("{}", "Step 2: Enter AWS Access Keys".white().bold());
424 println!("{}", "Get these from AWS Console → IAM → Security credentials".dimmed());
425 println!();
426
427 print!("AWS Access Key ID: ");
428 io::stdout().flush().unwrap();
429 let mut access_key = String::new();
430 io::stdin().read_line(&mut access_key).map_err(|e| AgentError::ToolError(e.to_string()))?;
431 let access_key = access_key.trim().to_string();
432
433 if access_key.is_empty() {
434 return Err(AgentError::MissingApiKey("AWS_ACCESS_KEY_ID".to_string()));
435 }
436
437 print!("AWS Secret Access Key: ");
438 io::stdout().flush().unwrap();
439 let mut secret_key = String::new();
440 io::stdin().read_line(&mut secret_key).map_err(|e| AgentError::ToolError(e.to_string()))?;
441 let secret_key = secret_key.trim().to_string();
442
443 if secret_key.is_empty() {
444 return Err(AgentError::MissingApiKey("AWS_SECRET_ACCESS_KEY".to_string()));
445 }
446
447 bedrock_config.access_key_id = Some(access_key.clone());
448 bedrock_config.secret_access_key = Some(secret_key.clone());
449
450 unsafe {
452 std::env::set_var("AWS_ACCESS_KEY_ID", &access_key);
453 std::env::set_var("AWS_SECRET_ACCESS_KEY", &secret_key);
454 }
455 println!("{}", "✓ Access keys configured".green());
456 }
457 "3" => {
458 if std::env::var("AWS_ACCESS_KEY_ID").is_err()
460 && std::env::var("AWS_PROFILE").is_err()
461 {
462 println!("{}", "⚠ No AWS credentials found in environment!".yellow());
463 println!("Set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY or AWS_PROFILE");
464 return Err(AgentError::MissingApiKey("AWS credentials".to_string()));
465 }
466 println!("{}", "✓ Using existing environment variables".green());
467 }
468 _ => {
469 println!("{}", "Invalid choice, using environment variables".yellow());
470 }
471 }
472
473 if bedrock_config.region.is_none() {
475 println!();
476 println!("{}", "Step 2: Select AWS Region".white().bold());
477 println!("{}", "Bedrock is available in select regions. Common choices:".dimmed());
478 println!();
479 println!(" {} us-east-1 (N. Virginia) - Most models", "[1]".cyan());
480 println!(" {} us-west-2 (Oregon)", "[2]".cyan());
481 println!(" {} eu-west-1 (Ireland)", "[3]".cyan());
482 println!(" {} ap-northeast-1 (Tokyo)", "[4]".cyan());
483 println!();
484 print!("Enter choice [1-4] or region name: ");
485 io::stdout().flush().unwrap();
486
487 let mut region_choice = String::new();
488 io::stdin().read_line(&mut region_choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
489 let region = match region_choice.trim() {
490 "1" | "" => "us-east-1",
491 "2" => "us-west-2",
492 "3" => "eu-west-1",
493 "4" => "ap-northeast-1",
494 other => other,
495 };
496
497 bedrock_config.region = Some(region.to_string());
498 unsafe { std::env::set_var("AWS_REGION", region); }
499 println!("{}", format!("✓ Region: {}", region).green());
500 }
501
502 println!();
504 println!("{}", "Step 3: Select Default Model".white().bold());
505 println!();
506 let models = get_available_models(ProviderType::Bedrock);
507 for (i, (id, desc)) in models.iter().enumerate() {
508 let marker = if i == 0 { "→ " } else { " " };
509 println!(" {} {} {}", marker, format!("[{}]", i + 1).cyan(), desc);
510 println!(" {}", id.dimmed());
511 }
512 println!();
513 print!("Enter choice [1-{}] (default: 1): ", models.len());
514 io::stdout().flush().unwrap();
515
516 let mut model_choice = String::new();
517 io::stdin().read_line(&mut model_choice).map_err(|e| AgentError::ToolError(e.to_string()))?;
518 let model_idx: usize = model_choice.trim().parse().unwrap_or(1);
519 let model_idx = model_idx.saturating_sub(1).min(models.len() - 1);
520 let selected_model = models[model_idx].0.to_string();
521
522 bedrock_config.default_model = Some(selected_model.clone());
523 println!("{}", format!("✓ Default model: {}", models[model_idx].1.split(" - ").next().unwrap_or(&selected_model)).green());
524
525 let mut agent_config = load_agent_config();
527 agent_config.bedrock = Some(bedrock_config);
528 agent_config.bedrock_configured = Some(true);
529
530 if let Err(e) = save_agent_config(&agent_config) {
531 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
532 } else {
533 println!();
534 println!("{}", "✓ Configuration saved to ~/.syncable.toml".green());
535 }
536
537 println!();
538 println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
539 println!("{}", " ✅ AWS Bedrock setup complete!".green().bold());
540 println!("{}", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan());
541 println!();
542
543 Ok(selected_model)
544 }
545
546 pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
548 if matches!(provider, ProviderType::Bedrock) {
550 return Self::run_bedrock_setup_wizard();
551 }
552
553 let env_var = match provider {
554 ProviderType::OpenAI => "OPENAI_API_KEY",
555 ProviderType::Anthropic => "ANTHROPIC_API_KEY",
556 ProviderType::Bedrock => unreachable!(), };
558
559 println!("\n{}", format!("🔑 No API key found for {}", provider).yellow());
560 println!("Please enter your {} API key:", provider);
561 print!("> ");
562 io::stdout().flush().unwrap();
563
564 let mut key = String::new();
565 io::stdin().read_line(&mut key).map_err(|e| AgentError::ToolError(e.to_string()))?;
566 let key = key.trim().to_string();
567
568 if key.is_empty() {
569 return Err(AgentError::MissingApiKey(env_var.to_string()));
570 }
571
572 unsafe {
575 std::env::set_var(env_var, &key);
576 }
577
578 let mut agent_config = load_agent_config();
580 match provider {
581 ProviderType::OpenAI => agent_config.openai_api_key = Some(key.clone()),
582 ProviderType::Anthropic => agent_config.anthropic_api_key = Some(key.clone()),
583 ProviderType::Bedrock => unreachable!(), }
585
586 if let Err(e) = save_agent_config(&agent_config) {
587 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
588 } else {
589 println!("{}", "✓ API key saved to ~/.syncable.toml".green());
590 }
591
592 Ok(key)
593 }
594
595 pub fn handle_model_command(&mut self) -> AgentResult<()> {
597 let models = get_available_models(self.provider);
598
599 println!("\n{}", format!("📋 Available models for {}:", self.provider).cyan().bold());
600 println!();
601
602 for (i, (id, desc)) in models.iter().enumerate() {
603 let marker = if *id == self.model { "→ " } else { " " };
604 let num = format!("[{}]", i + 1);
605 println!(" {} {} {} - {}", marker, num.dimmed(), id.white().bold(), desc.dimmed());
606 }
607
608 println!();
609 println!("Enter number to select, or press Enter to keep current:");
610 print!("> ");
611 io::stdout().flush().unwrap();
612
613 let mut input = String::new();
614 io::stdin().read_line(&mut input).ok();
615 let input = input.trim();
616
617 if input.is_empty() {
618 println!("{}", format!("Keeping model: {}", self.model).dimmed());
619 return Ok(());
620 }
621
622 if let Ok(num) = input.parse::<usize>() {
623 if num >= 1 && num <= models.len() {
624 let (id, desc) = models[num - 1];
625 self.model = id.to_string();
626
627 let mut agent_config = load_agent_config();
629 agent_config.default_model = Some(id.to_string());
630 if let Err(e) = save_agent_config(&agent_config) {
631 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
632 }
633
634 println!("{}", format!("✓ Switched to {} - {}", id, desc).green());
635 } else {
636 println!("{}", "Invalid selection".red());
637 }
638 } else {
639 self.model = input.to_string();
641
642 let mut agent_config = load_agent_config();
644 agent_config.default_model = Some(input.to_string());
645 if let Err(e) = save_agent_config(&agent_config) {
646 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
647 }
648
649 println!("{}", format!("✓ Set model to: {}", input).green());
650 }
651
652 Ok(())
653 }
654
655 pub fn handle_provider_command(&mut self) -> AgentResult<()> {
657 let providers = [ProviderType::OpenAI, ProviderType::Anthropic, ProviderType::Bedrock];
658
659 println!("\n{}", "🔄 Available providers:".cyan().bold());
660 println!();
661
662 for (i, provider) in providers.iter().enumerate() {
663 let marker = if *provider == self.provider { "→ " } else { " " };
664 let has_key = if Self::has_api_key(*provider) {
665 "✓ API key configured".green()
666 } else {
667 "⚠ No API key".yellow()
668 };
669 let num = format!("[{}]", i + 1);
670 println!(" {} {} {} - {}", marker, num.dimmed(), provider.to_string().white().bold(), has_key);
671 }
672
673 println!();
674 println!("Enter number to select:");
675 print!("> ");
676 io::stdout().flush().unwrap();
677
678 let mut input = String::new();
679 io::stdin().read_line(&mut input).ok();
680 let input = input.trim();
681
682 if let Ok(num) = input.parse::<usize>() {
683 if num >= 1 && num <= providers.len() {
684 let new_provider = providers[num - 1];
685
686 if !Self::has_api_key(new_provider) {
688 Self::prompt_api_key(new_provider)?;
689 }
690
691 Self::load_api_key_to_env(new_provider);
694
695 self.provider = new_provider;
696
697 let default_model = match new_provider {
699 ProviderType::OpenAI => "gpt-5.2".to_string(),
700 ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
701 ProviderType::Bedrock => {
702 let agent_config = load_agent_config();
704 agent_config.bedrock
705 .and_then(|b| b.default_model)
706 .unwrap_or_else(|| "global.anthropic.claude-sonnet-4-5-20250929-v1:0".to_string())
707 }
708 };
709 self.model = default_model.clone();
710
711 let mut agent_config = load_agent_config();
713 agent_config.default_provider = new_provider.to_string();
714 agent_config.default_model = Some(default_model.clone());
715 if let Err(e) = save_agent_config(&agent_config) {
716 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
717 }
718
719 println!("{}", format!("✓ Switched to {} with model {}", new_provider, default_model).green());
720 } else {
721 println!("{}", "Invalid selection".red());
722 }
723 }
724
725 Ok(())
726 }
727
728 pub fn handle_reset_command(&mut self) -> AgentResult<()> {
730 let providers = [ProviderType::OpenAI, ProviderType::Anthropic, ProviderType::Bedrock];
731
732 println!("\n{}", "🔄 Reset Provider Credentials".cyan().bold());
733 println!();
734
735 for (i, provider) in providers.iter().enumerate() {
736 let status = if Self::has_api_key(*provider) {
737 "✓ configured".green()
738 } else {
739 "○ not configured".dimmed()
740 };
741 let num = format!("[{}]", i + 1);
742 println!(" {} {} - {}", num.dimmed(), provider.to_string().white().bold(), status);
743 }
744 println!(" {} All providers", "[4]".dimmed());
745 println!();
746 println!("Select provider to reset (or press Enter to cancel):");
747 print!("> ");
748 io::stdout().flush().unwrap();
749
750 let mut input = String::new();
751 io::stdin().read_line(&mut input).ok();
752 let input = input.trim();
753
754 if input.is_empty() {
755 println!("{}", "Cancelled".dimmed());
756 return Ok(());
757 }
758
759 let mut agent_config = load_agent_config();
760
761 match input {
762 "1" => {
763 agent_config.openai_api_key = None;
764 unsafe { std::env::remove_var("OPENAI_API_KEY"); }
766 println!("{}", "✓ OpenAI credentials cleared".green());
767 }
768 "2" => {
769 agent_config.anthropic_api_key = None;
770 unsafe { std::env::remove_var("ANTHROPIC_API_KEY"); }
771 println!("{}", "✓ Anthropic credentials cleared".green());
772 }
773 "3" => {
774 agent_config.bedrock = None;
775 agent_config.bedrock_configured = Some(false);
776 unsafe {
778 std::env::remove_var("AWS_PROFILE");
779 std::env::remove_var("AWS_ACCESS_KEY_ID");
780 std::env::remove_var("AWS_SECRET_ACCESS_KEY");
781 std::env::remove_var("AWS_REGION");
782 }
783 println!("{}", "✓ Bedrock credentials cleared".green());
784 }
785 "4" => {
786 agent_config.openai_api_key = None;
787 agent_config.anthropic_api_key = None;
788 agent_config.bedrock = None;
789 agent_config.bedrock_configured = Some(false);
790 unsafe {
792 std::env::remove_var("OPENAI_API_KEY");
793 std::env::remove_var("ANTHROPIC_API_KEY");
794 std::env::remove_var("AWS_PROFILE");
795 std::env::remove_var("AWS_ACCESS_KEY_ID");
796 std::env::remove_var("AWS_SECRET_ACCESS_KEY");
797 std::env::remove_var("AWS_REGION");
798 }
799 println!("{}", "✓ All provider credentials cleared".green());
800 }
801 _ => {
802 println!("{}", "Invalid selection".red());
803 return Ok(());
804 }
805 }
806
807 if let Err(e) = save_agent_config(&agent_config) {
809 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
810 } else {
811 println!("{}", "Configuration saved to ~/.syncable.toml".dimmed());
812 }
813
814 let current_cleared = match input {
816 "1" => self.provider == ProviderType::OpenAI,
817 "2" => self.provider == ProviderType::Anthropic,
818 "3" => self.provider == ProviderType::Bedrock,
819 "4" => true,
820 _ => false,
821 };
822
823 if current_cleared {
824 println!();
825 println!("{}", "Current provider credentials were cleared.".yellow());
826 println!("Use {} to reconfigure or {} to switch providers.", "/provider".cyan(), "/p".cyan());
827 }
828
829 Ok(())
830 }
831
832 pub fn handle_profile_command(&mut self) -> AgentResult<()> {
834 use crate::config::types::{Profile, OpenAIProfile, AnthropicProfile};
835
836 let mut agent_config = load_agent_config();
837
838 println!("\n{}", "👤 Profile Management".cyan().bold());
839 println!();
840
841 self.list_profiles(&agent_config);
843
844 println!(" {} Create new profile", "[1]".cyan());
845 println!(" {} Switch active profile", "[2]".cyan());
846 println!(" {} Configure provider in profile", "[3]".cyan());
847 println!(" {} Delete a profile", "[4]".cyan());
848 println!();
849 println!("Select action (or press Enter to cancel):");
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 input.is_empty() {
858 println!("{}", "Cancelled".dimmed());
859 return Ok(());
860 }
861
862 match input {
863 "1" => {
864 println!("\n{}", "Create Profile".white().bold());
866 print!("Profile name (e.g., work, personal): ");
867 io::stdout().flush().unwrap();
868 let mut name = String::new();
869 io::stdin().read_line(&mut name).ok();
870 let name = name.trim().to_string();
871
872 if name.is_empty() {
873 println!("{}", "Profile name cannot be empty".red());
874 return Ok(());
875 }
876
877 if agent_config.profiles.contains_key(&name) {
878 println!("{}", format!("Profile '{}' already exists", name).yellow());
879 return Ok(());
880 }
881
882 print!("Description (optional): ");
883 io::stdout().flush().unwrap();
884 let mut desc = String::new();
885 io::stdin().read_line(&mut desc).ok();
886 let desc = desc.trim();
887
888 let profile = Profile {
889 description: if desc.is_empty() { None } else { Some(desc.to_string()) },
890 default_provider: None,
891 default_model: None,
892 openai: None,
893 anthropic: None,
894 bedrock: None,
895 };
896
897 agent_config.profiles.insert(name.clone(), profile);
898
899 if agent_config.active_profile.is_none() {
901 agent_config.active_profile = Some(name.clone());
902 }
903
904 if let Err(e) = save_agent_config(&agent_config) {
905 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
906 }
907
908 println!("{}", format!("✓ Profile '{}' created", name).green());
909 println!("{}", "Use option [3] to configure providers for this profile".dimmed());
910 }
911 "2" => {
912 if agent_config.profiles.is_empty() {
914 println!("{}", "No profiles configured. Create one first with option [1].".yellow());
915 return Ok(());
916 }
917
918 print!("Enter profile name to activate: ");
919 io::stdout().flush().unwrap();
920 let mut name = String::new();
921 io::stdin().read_line(&mut name).ok();
922 let name = name.trim().to_string();
923
924 if name.is_empty() {
925 println!("{}", "Cancelled".dimmed());
926 return Ok(());
927 }
928
929 if !agent_config.profiles.contains_key(&name) {
930 println!("{}", format!("Profile '{}' not found", name).red());
931 return Ok(());
932 }
933
934 agent_config.active_profile = Some(name.clone());
935
936 if let Some(profile) = agent_config.profiles.get(&name) {
938 if let Some(openai) = &profile.openai {
940 unsafe { std::env::set_var("OPENAI_API_KEY", &openai.api_key); }
941 }
942 if let Some(anthropic) = &profile.anthropic {
943 unsafe { std::env::set_var("ANTHROPIC_API_KEY", &anthropic.api_key); }
944 }
945 if let Some(bedrock) = &profile.bedrock {
946 if let Some(region) = &bedrock.region {
947 unsafe { std::env::set_var("AWS_REGION", region); }
948 }
949 if let Some(aws_profile) = &bedrock.profile {
950 unsafe { std::env::set_var("AWS_PROFILE", aws_profile); }
951 } else if let (Some(key_id), Some(secret)) = (&bedrock.access_key_id, &bedrock.secret_access_key) {
952 unsafe {
953 std::env::set_var("AWS_ACCESS_KEY_ID", key_id);
954 std::env::set_var("AWS_SECRET_ACCESS_KEY", secret);
955 }
956 }
957 }
958
959 if let Some(default_provider) = &profile.default_provider {
961 if let Ok(p) = default_provider.parse() {
962 self.provider = p;
963 }
964 }
965 }
966
967 if let Err(e) = save_agent_config(&agent_config) {
968 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
969 }
970
971 println!("{}", format!("✓ Switched to profile '{}'", name).green());
972 }
973 "3" => {
974 let profile_name = if let Some(name) = &agent_config.active_profile {
976 name.clone()
977 } else if agent_config.profiles.is_empty() {
978 println!("{}", "No profiles configured. Create one first with option [1].".yellow());
979 return Ok(());
980 } else {
981 print!("Enter profile name to configure: ");
982 io::stdout().flush().unwrap();
983 let mut name = String::new();
984 io::stdin().read_line(&mut name).ok();
985 name.trim().to_string()
986 };
987
988 if profile_name.is_empty() {
989 println!("{}", "Cancelled".dimmed());
990 return Ok(());
991 }
992
993 if !agent_config.profiles.contains_key(&profile_name) {
994 println!("{}", format!("Profile '{}' not found", profile_name).red());
995 return Ok(());
996 }
997
998 println!("\n{}", format!("Configure provider for '{}':", profile_name).white().bold());
999 println!(" {} OpenAI", "[1]".cyan());
1000 println!(" {} Anthropic", "[2]".cyan());
1001 println!(" {} AWS Bedrock", "[3]".cyan());
1002 print!("> ");
1003 io::stdout().flush().unwrap();
1004
1005 let mut provider_choice = String::new();
1006 io::stdin().read_line(&mut provider_choice).ok();
1007
1008 match provider_choice.trim() {
1009 "1" => {
1010 print!("OpenAI API Key: ");
1012 io::stdout().flush().unwrap();
1013 let mut api_key = String::new();
1014 io::stdin().read_line(&mut api_key).ok();
1015 let api_key = api_key.trim().to_string();
1016
1017 if api_key.is_empty() {
1018 println!("{}", "API key cannot be empty".red());
1019 return Ok(());
1020 }
1021
1022 if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
1023 profile.openai = Some(OpenAIProfile {
1024 api_key,
1025 description: None,
1026 default_model: None,
1027 });
1028 }
1029 println!("{}", format!("✓ OpenAI configured for profile '{}'", profile_name).green());
1030 }
1031 "2" => {
1032 print!("Anthropic API Key: ");
1034 io::stdout().flush().unwrap();
1035 let mut api_key = String::new();
1036 io::stdin().read_line(&mut api_key).ok();
1037 let api_key = api_key.trim().to_string();
1038
1039 if api_key.is_empty() {
1040 println!("{}", "API key cannot be empty".red());
1041 return Ok(());
1042 }
1043
1044 if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
1045 profile.anthropic = Some(AnthropicProfile {
1046 api_key,
1047 description: None,
1048 default_model: None,
1049 });
1050 }
1051 println!("{}", format!("✓ Anthropic configured for profile '{}'", profile_name).green());
1052 }
1053 "3" => {
1054 println!("{}", "Running Bedrock setup...".dimmed());
1056 let selected_model = Self::run_bedrock_setup_wizard()?;
1057
1058 let fresh_config = load_agent_config();
1060 if let Some(bedrock) = fresh_config.bedrock.clone() {
1061 if let Some(profile) = agent_config.profiles.get_mut(&profile_name) {
1062 profile.bedrock = Some(bedrock);
1063 profile.default_model = Some(selected_model);
1064 }
1065 }
1066 println!("{}", format!("✓ Bedrock configured for profile '{}'", profile_name).green());
1067 }
1068 _ => {
1069 println!("{}", "Invalid selection".red());
1070 return Ok(());
1071 }
1072 }
1073
1074 if let Err(e) = save_agent_config(&agent_config) {
1075 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
1076 }
1077 }
1078 "4" => {
1079 if agent_config.profiles.is_empty() {
1081 println!("{}", "No profiles to delete.".yellow());
1082 return Ok(());
1083 }
1084
1085 print!("Enter profile name to delete: ");
1086 io::stdout().flush().unwrap();
1087 let mut name = String::new();
1088 io::stdin().read_line(&mut name).ok();
1089 let name = name.trim().to_string();
1090
1091 if name.is_empty() {
1092 println!("{}", "Cancelled".dimmed());
1093 return Ok(());
1094 }
1095
1096 if agent_config.profiles.remove(&name).is_some() {
1097 if agent_config.active_profile.as_deref() == Some(name.as_str()) {
1099 agent_config.active_profile = None;
1100 }
1101
1102 if let Err(e) = save_agent_config(&agent_config) {
1103 eprintln!("{}", format!("Warning: Could not save config: {}", e).yellow());
1104 }
1105
1106 println!("{}", format!("✓ Deleted profile '{}'", name).green());
1107 } else {
1108 println!("{}", format!("Profile '{}' not found", name).red());
1109 }
1110 }
1111 _ => {
1112 println!("{}", "Invalid selection".red());
1113 }
1114 }
1115
1116 Ok(())
1117 }
1118
1119 pub fn handle_plans_command(&self) -> AgentResult<()> {
1121 let incomplete = find_incomplete_plans(&self.project_path);
1122
1123 if incomplete.is_empty() {
1124 println!("\n{}", "No incomplete plans found.".dimmed());
1125 println!("{}", "Create a plan using plan mode (Shift+Tab) and the plan_create tool.".dimmed());
1126 return Ok(());
1127 }
1128
1129 println!("\n{}", "📋 Incomplete Plans".cyan().bold());
1130 println!();
1131
1132 for (i, plan) in incomplete.iter().enumerate() {
1133 let progress = format!("{}/{}", plan.done, plan.total);
1134 let percent = if plan.total > 0 {
1135 (plan.done as f64 / plan.total as f64 * 100.0) as usize
1136 } else {
1137 0
1138 };
1139
1140 println!(
1141 " {} {} {} ({} - {}%)",
1142 format!("[{}]", i + 1).cyan(),
1143 plan.filename.white().bold(),
1144 format!("({} pending)", plan.pending).yellow(),
1145 progress.dimmed(),
1146 percent
1147 );
1148 println!(" {}", plan.path.dimmed());
1149 }
1150
1151 println!();
1152 println!("{}", "To continue a plan, say:".dimmed());
1153 println!(" {}", "\"continue the plan at plans/FILENAME.md\"".cyan());
1154 println!(" {}", "or just \"continue\" to resume the most recent one".cyan());
1155 println!();
1156
1157 Ok(())
1158 }
1159
1160 fn list_profiles(&self, config: &crate::config::types::AgentConfig) {
1162 let active = config.active_profile.as_deref();
1163
1164 if config.profiles.is_empty() {
1165 println!("{}", " No profiles configured yet.".dimmed());
1166 println!();
1167 return;
1168 }
1169
1170 println!("{}", "📋 Profiles:".cyan());
1171 for (name, profile) in &config.profiles {
1172 let marker = if Some(name.as_str()) == active { "→ " } else { " " };
1173 let desc = profile.description.as_deref().unwrap_or("");
1174 let desc_fmt = if desc.is_empty() { String::new() } else { format!(" - {}", desc) };
1175
1176 let mut providers = Vec::new();
1178 if profile.openai.is_some() { providers.push("OpenAI"); }
1179 if profile.anthropic.is_some() { providers.push("Anthropic"); }
1180 if profile.bedrock.is_some() { providers.push("Bedrock"); }
1181
1182 let providers_str = if providers.is_empty() {
1183 "(no providers configured)".to_string()
1184 } else {
1185 format!("[{}]", providers.join(", "))
1186 };
1187
1188 println!(" {} {}{} {}", marker, name.white().bold(), desc_fmt.dimmed(), providers_str.dimmed());
1189 }
1190 println!();
1191 }
1192
1193 pub fn print_help() {
1195 println!();
1196 println!(" {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET);
1197 println!(" {}📖 Available Commands{}", ansi::PURPLE, ansi::RESET);
1198 println!(" {}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}", ansi::PURPLE, ansi::RESET);
1199 println!();
1200
1201 for cmd in SLASH_COMMANDS.iter() {
1202 let alias = cmd.alias.map(|a| format!(" ({})", a)).unwrap_or_default();
1203 println!(" {}/{:<12}{}{} - {}{}{}",
1204 ansi::CYAN, cmd.name, alias, ansi::RESET,
1205 ansi::DIM, cmd.description, ansi::RESET
1206 );
1207 }
1208
1209 println!();
1210 println!(" {}Tip: Type / to see interactive command picker!{}", ansi::DIM, ansi::RESET);
1211 println!();
1212 }
1213
1214
1215 pub fn print_logo() {
1217 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";
1228
1229 println!();
1230 println!(
1231 "{} ███████╗{}{} ██╗ ██╗{}{}███╗ ██╗{}{} ██████╗{}{} █████╗ {}{}██████╗ {}{}██╗ {}{}███████╗{}",
1232 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1233 );
1234 println!(
1235 "{} ██╔════╝{}{} ╚██╗ ██╔╝{}{}████╗ ██║{}{} ██╔════╝{}{} ██╔══██╗{}{}██╔══██╗{}{}██║ {}{}██╔════╝{}",
1236 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1237 );
1238 println!(
1239 "{} ███████╗{}{} ╚████╔╝ {}{}██╔██╗ ██║{}{} ██║ {}{} ███████║{}{}██████╔╝{}{}██║ {}{}█████╗ {}",
1240 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1241 );
1242 println!(
1243 "{} ╚════██║{}{} ╚██╔╝ {}{}██║╚██╗██║{}{} ██║ {}{} ██╔══██║{}{}██╔══██╗{}{}██║ {}{}██╔══╝ {}",
1244 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1245 );
1246 println!(
1247 "{} ███████║{}{} ██║ {}{}██║ ╚████║{}{} ╚██████╗{}{} ██║ ██║{}{}██████╔╝{}{}███████╗{}{}███████╗{}",
1248 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1249 );
1250 println!(
1251 "{} ╚══════╝{}{} ╚═╝ {}{}╚═╝ ╚═══╝{}{} ╚═════╝{}{} ╚═╝ ╚═╝{}{}╚═════╝ {}{}╚══════╝{}{}╚══════╝{}",
1252 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
1253 );
1254 println!();
1255 }
1256
1257 pub fn print_banner(&self) {
1259 Self::print_logo();
1261
1262 println!(
1264 " {} {}",
1265 "🚀".dimmed(),
1266 "Want to deploy? Deploy instantly from Syncable Platform → https://syncable.dev".dimmed()
1267 );
1268 println!();
1269
1270 println!(
1272 " {} {} powered by {}: {}",
1273 ROBOT,
1274 "Syncable Agent".white().bold(),
1275 self.provider.to_string().cyan(),
1276 self.model.cyan()
1277 );
1278 println!(
1279 " {}",
1280 "Your AI-powered code analysis assistant".dimmed()
1281 );
1282
1283 let incomplete_plans = find_incomplete_plans(&self.project_path);
1285 if !incomplete_plans.is_empty() {
1286 println!();
1287 if incomplete_plans.len() == 1 {
1288 let plan = &incomplete_plans[0];
1289 println!(
1290 " {} {} ({}/{} done)",
1291 "📋 Incomplete plan:".yellow(),
1292 plan.filename.white(),
1293 plan.done,
1294 plan.total
1295 );
1296 println!(
1297 " {} \"{}\" {}",
1298 "→".cyan(),
1299 "continue".cyan().bold(),
1300 "to resume".dimmed()
1301 );
1302 } else {
1303 println!(
1304 " {} {} incomplete plans found. Use {} to see them.",
1305 "📋".yellow(),
1306 incomplete_plans.len(),
1307 "/plans".cyan()
1308 );
1309 }
1310 }
1311
1312 println!();
1313 println!(
1314 " {} Type your questions. Use {} to exit.\n",
1315 "→".cyan(),
1316 "exit".yellow().bold()
1317 );
1318 }
1319
1320
1321 pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
1323 let cmd = input.trim().to_lowercase();
1324
1325 if cmd == "/" {
1328 Self::print_help();
1329 return Ok(true);
1330 }
1331
1332 match cmd.as_str() {
1333 "/exit" | "/quit" | "/q" => {
1334 println!("\n{}", "👋 Goodbye!".green());
1335 return Ok(false);
1336 }
1337 "/help" | "/h" | "/?" => {
1338 Self::print_help();
1339 }
1340 "/model" | "/m" => {
1341 self.handle_model_command()?;
1342 }
1343 "/provider" | "/p" => {
1344 self.handle_provider_command()?;
1345 }
1346 "/cost" => {
1347 self.token_usage.print_report(&self.model);
1348 }
1349 "/clear" | "/c" => {
1350 self.history.clear();
1351 println!("{}", "✓ Conversation history cleared".green());
1352 }
1353 "/reset" | "/r" => {
1354 self.handle_reset_command()?;
1355 }
1356 "/profile" => {
1357 self.handle_profile_command()?;
1358 }
1359 "/plans" => {
1360 self.handle_plans_command()?;
1361 }
1362 _ => {
1363 if cmd.starts_with('/') {
1364 println!("{}", format!("Unknown command: {}. Type /help for available commands.", cmd).yellow());
1366 }
1367 }
1368 }
1369
1370 Ok(true)
1371 }
1372
1373 pub fn is_command(input: &str) -> bool {
1375 input.trim().starts_with('/')
1376 }
1377
1378 fn strip_file_references(input: &str) -> String {
1382 let mut result = String::with_capacity(input.len());
1383 let chars: Vec<char> = input.chars().collect();
1384 let mut i = 0;
1385
1386 while i < chars.len() {
1387 if chars[i] == '@' {
1388 let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
1390
1391 if is_valid_trigger {
1392 let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
1394
1395 if has_path {
1396 i += 1;
1398 continue;
1399 }
1400 }
1401 }
1402 result.push(chars[i]);
1403 i += 1;
1404 }
1405
1406 result
1407 }
1408
1409 pub fn read_input(&self) -> io::Result<crate::agent::ui::input::InputResult> {
1413 use crate::agent::ui::input::read_input_with_file_picker;
1414
1415 Ok(read_input_with_file_picker("You:", &self.project_path, self.plan_mode.is_planning()))
1416 }
1417
1418 pub fn process_submitted_text(text: &str) -> String {
1420 let trimmed = text.trim();
1421 if trimmed.starts_with('/') && trimmed.contains(" ") {
1424 if let Some(cmd) = trimmed.split_whitespace().next() {
1426 return cmd.to_string();
1427 }
1428 }
1429 Self::strip_file_references(trimmed)
1432 }
1433}