1use crate::ai::ollama::OllamaClient;
4use crate::core::config::{AiProfile, AiProvider, Config};
5use crate::core::error::{Error, Result};
6use dialoguer::{theme::ColorfulTheme, Input, Select};
7
8pub async fn run() -> Result<()> {
10 let mut config = Config::load()?;
11
12 println!();
14 println!(" Current: {}", format_current(&config));
15 println!();
16
17 let mut options = Vec::new();
19 let mut actions = Vec::new();
20
21 let mut profile_names: Vec<_> = config.ai.profiles.keys().cloned().collect();
23 profile_names.sort();
24
25 for name in &profile_names {
26 if let Some(profile) = config.ai.profiles.get(name) {
27 let label = format_profile_label(name, profile);
28 let is_active = is_profile_active(&config, name);
29 if is_active {
30 options.push(format!("{} ✓", label));
31 } else {
32 options.push(label);
33 }
34 actions.push(Action::SwitchProfile(name.clone()));
35 }
36 }
37
38 let has_claude = config.ai.anthropic_token.is_some();
40 let has_gemini = config.ai.google_token.is_some();
41
42 if has_claude {
43 let active = config.ai.provider == AiProvider::Claude;
44 let label = "Claude (Anthropic)".to_string();
45 if active {
46 options.push(format!("{} ✓", label));
47 } else {
48 options.push(label);
49 }
50 actions.push(Action::SwitchProvider(AiProvider::Claude));
51 }
52
53 if has_gemini {
54 let active = config.ai.provider == AiProvider::Gemini;
55 let label = "Gemini (Google)".to_string();
56 if active {
57 options.push(format!("{} ✓", label));
58 } else {
59 options.push(label);
60 }
61 actions.push(Action::SwitchProvider(AiProvider::Gemini));
62 }
63
64 let ollama_client = OllamaClient::new();
66 let ollama_available = ollama_client.is_available().await;
67
68 if ollama_available {
69 if let Ok(models) = ollama_client.list_models().await {
70 for model in models {
71 let is_active = config.ai.provider == AiProvider::Ollama
72 && config.ai.ollama_model == model.name;
73 let label = format!("Ollama: {}", model.name);
74 if is_active {
75 options.push(format!("{} ✓", label));
76 } else {
77 options.push(label);
78 }
79 actions.push(Action::SwitchOllama(model.name));
80 }
81 }
82 }
83
84 options.push("+ Add account (login)".to_string());
86 actions.push(Action::AddAccount);
87
88 options.push("+ Save as profile".to_string());
89 actions.push(Action::SaveProfile);
90
91 if !profile_names.is_empty() {
92 options.push("- Delete profile".to_string());
93 actions.push(Action::DeleteProfile);
94 }
95
96 let selection = Select::with_theme(&ColorfulTheme::default())
98 .with_prompt("Switch to")
99 .items(&options)
100 .default(0)
101 .interact()
102 .map_err(|e| Error::DaemonError {
103 message: format!("Selection failed: {}", e),
104 })?;
105
106 match &actions[selection] {
108 Action::SwitchProfile(name) => {
109 if let Some(profile) = config.ai.profiles.get(name).cloned() {
110 apply_profile(&mut config, &profile);
111 config.save()?;
112 println!("\n✓ Switched to '{}'", name);
113 }
114 }
115 Action::SwitchProvider(provider) => {
116 config.ai.provider = provider.clone();
117 config.save()?;
118 println!("\n✓ Switched to {}", format_provider_name(provider));
119 }
120 Action::SwitchOllama(model) => {
121 config.ai.provider = AiProvider::Ollama;
122 config.ai.ollama_model = model.clone();
123 config.save()?;
124 println!("\n✓ Switched to Ollama '{}'", model);
125 }
126 Action::SaveProfile => {
127 save_current_as_profile(&mut config).await?;
128 }
129 Action::AddAccount => {
130 drop(config);
132 crate::cli::login::run().await?;
133 }
134 Action::DeleteProfile => {
135 delete_profile(&mut config).await?;
136 }
137 }
138
139 Ok(())
140}
141
142#[derive(Debug)]
143enum Action {
144 SwitchProfile(String),
145 SwitchProvider(AiProvider),
146 SwitchOllama(String),
147 SaveProfile,
148 AddAccount,
149 DeleteProfile,
150}
151
152fn format_current(config: &Config) -> String {
153 match config.ai.provider {
154 AiProvider::Claude => "Claude (Anthropic)".to_string(),
155 AiProvider::Gemini => "Gemini (Google)".to_string(),
156 AiProvider::Ollama => format!("Ollama ({})", config.ai.ollama_model),
157 }
158}
159
160fn format_provider_name(provider: &AiProvider) -> &'static str {
161 match provider {
162 AiProvider::Claude => "Claude (Anthropic)",
163 AiProvider::Gemini => "Gemini (Google)",
164 AiProvider::Ollama => "Ollama",
165 }
166}
167
168fn format_profile_label(name: &str, profile: &AiProfile) -> String {
169 match profile.provider {
170 AiProvider::Claude => format!("[{}] Claude", name),
171 AiProvider::Gemini => format!("[{}] Gemini", name),
172 AiProvider::Ollama => {
173 let model = profile.ollama_model.as_deref().unwrap_or("default");
174 format!("[{}] Ollama: {}", name, model)
175 }
176 }
177}
178
179fn is_profile_active(config: &Config, name: &str) -> bool {
180 if let Some(profile) = config.ai.profiles.get(name) {
181 if config.ai.provider != profile.provider {
182 return false;
183 }
184 match config.ai.provider {
185 AiProvider::Ollama => profile.ollama_model.as_deref() == Some(&config.ai.ollama_model),
186 AiProvider::Claude => {
187 profile.anthropic_token.is_some()
188 && profile.anthropic_token == config.ai.anthropic_token
189 }
190 AiProvider::Gemini => {
191 profile.google_token.is_some() && profile.google_token == config.ai.google_token
192 }
193 }
194 } else {
195 false
196 }
197}
198
199fn apply_profile(config: &mut Config, profile: &AiProfile) {
200 config.ai.provider = profile.provider.clone();
201
202 if let Some(model) = &profile.ollama_model {
203 config.ai.ollama_model = model.clone();
204 }
205 if let Some(url) = &profile.ollama_url {
206 config.ai.ollama_url = url.clone();
207 }
208 if let Some(token) = &profile.google_token {
209 config.ai.google_token = Some(token.clone());
210 }
211 if let Some(token) = &profile.anthropic_token {
212 config.ai.anthropic_token = Some(token.clone());
213 }
214}
215
216async fn save_current_as_profile(config: &mut Config) -> Result<()> {
217 let name: String = Input::with_theme(&ColorfulTheme::default())
218 .with_prompt("Profile name")
219 .interact_text()
220 .map_err(|e| Error::DaemonError {
221 message: format!("Input failed: {}", e),
222 })?;
223
224 let profile = AiProfile {
225 provider: config.ai.provider.clone(),
226 ollama_model: if config.ai.provider == AiProvider::Ollama {
227 Some(config.ai.ollama_model.clone())
228 } else {
229 None
230 },
231 ollama_url: if config.ai.provider == AiProvider::Ollama {
232 Some(config.ai.ollama_url.clone())
233 } else {
234 None
235 },
236 google_token: if config.ai.provider == AiProvider::Gemini {
237 config.ai.google_token.clone()
238 } else {
239 None
240 },
241 anthropic_token: if config.ai.provider == AiProvider::Claude {
242 config.ai.anthropic_token.clone()
243 } else {
244 None
245 },
246 };
247
248 config.ai.profiles.insert(name.clone(), profile);
249 config.save()?;
250 println!("\n✓ Saved profile '{}'", name);
251
252 Ok(())
253}
254
255async fn delete_profile(config: &mut Config) -> Result<()> {
256 let profile_names: Vec<_> = config.ai.profiles.keys().cloned().collect();
257
258 if profile_names.is_empty() {
259 println!("No profiles to delete.");
260 return Ok(());
261 }
262
263 let selection = Select::with_theme(&ColorfulTheme::default())
264 .with_prompt("Delete which profile?")
265 .items(&profile_names)
266 .interact()
267 .map_err(|e| Error::DaemonError {
268 message: format!("Selection failed: {}", e),
269 })?;
270
271 let name = &profile_names[selection];
272 config.ai.profiles.remove(name);
273 config.save()?;
274 println!("\n✓ Deleted profile '{}'", name);
275
276 Ok(())
277}