1use crate::app_config::AppType;
2use crate::config::{get_app_config_dir, home_dir};
3use crate::error::AppError;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fs;
7use std::path::PathBuf;
8use std::sync::{OnceLock, RwLock};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "camelCase")]
12pub struct VisibleApps {
13 #[serde(default = "default_visible_app_claude")]
14 pub claude: bool,
15 #[serde(default = "default_visible_app_codex")]
16 pub codex: bool,
17 #[serde(default = "default_visible_app_gemini")]
18 pub gemini: bool,
19 #[serde(default = "default_visible_app_opencode")]
20 pub opencode: bool,
21 #[serde(default = "default_visible_app_openclaw")]
22 pub openclaw: bool,
23 #[serde(default = "default_visible_app_hermes")]
24 pub hermes: bool,
25}
26
27fn default_visible_app_claude() -> bool {
28 true
29}
30
31fn default_visible_app_codex() -> bool {
32 true
33}
34
35fn default_visible_app_gemini() -> bool {
36 false
37}
38
39fn default_visible_app_opencode() -> bool {
40 true
41}
42
43fn default_visible_app_openclaw() -> bool {
44 true
45}
46
47fn default_visible_app_hermes() -> bool {
48 true
49}
50
51pub fn default_visible_apps() -> VisibleApps {
52 VisibleApps {
53 claude: true,
54 codex: true,
55 gemini: false,
56 opencode: true,
57 openclaw: true,
58 hermes: true,
59 }
60}
61
62impl Default for VisibleApps {
63 fn default() -> Self {
64 default_visible_apps()
65 }
66}
67
68impl VisibleApps {
69 pub fn ordered_enabled(&self) -> Vec<AppType> {
70 app_order()
71 .into_iter()
72 .filter(|app_type| self.is_enabled_for(app_type))
73 .collect()
74 }
75
76 pub fn is_enabled_for(&self, app_type: &AppType) -> bool {
77 match app_type {
78 AppType::Claude => self.claude,
79 AppType::Codex => self.codex,
80 AppType::Gemini => self.gemini,
81 AppType::OpenCode => self.opencode,
82 AppType::OpenClaw => self.openclaw,
83 AppType::Hermes => self.hermes,
84 }
85 }
86
87 pub fn set_enabled_for(&mut self, app_type: &AppType, enabled: bool) {
88 match app_type {
89 AppType::Claude => self.claude = enabled,
90 AppType::Codex => self.codex = enabled,
91 AppType::Gemini => self.gemini = enabled,
92 AppType::OpenCode => self.opencode = enabled,
93 AppType::OpenClaw => self.openclaw = enabled,
94 AppType::Hermes => self.hermes = enabled,
95 }
96 }
97
98 pub fn normalize(&mut self) {
99 if self.ordered_enabled().is_empty() {
100 *self = default_visible_apps();
101 }
102 }
103
104 pub fn validate(&self) -> Result<(), AppError> {
105 if self.ordered_enabled().is_empty() {
106 return Err(AppError::InvalidInput(
107 "At least one app must remain visible".to_string(),
108 ));
109 }
110
111 Ok(())
112 }
113}
114
115fn app_order() -> [AppType; 6] {
116 [
117 AppType::Claude,
118 AppType::Codex,
119 AppType::Gemini,
120 AppType::OpenCode,
121 AppType::OpenClaw,
122 AppType::Hermes,
123 ]
124}
125
126pub fn next_visible_app(
127 visible: &VisibleApps,
128 current: &AppType,
129 direction: i8,
130) -> Option<AppType> {
131 let ordered = app_order();
132 if ordered
133 .iter()
134 .all(|app_type| !visible.is_enabled_for(app_type))
135 {
136 return None;
137 }
138
139 let current_index = ordered.iter().position(|app_type| app_type == current)?;
140 let step = if direction < 0 { -1 } else { 1 };
141 let len = ordered.len() as isize;
142
143 for offset in 1..=ordered.len() {
144 let index = (current_index as isize + step * offset as isize).rem_euclid(len) as usize;
145 let candidate = &ordered[index];
146 if visible.is_enabled_for(candidate) {
147 return Some(candidate.clone());
148 }
149 }
150
151 None
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct CustomEndpoint {
158 pub url: String,
159 pub added_at: i64,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub last_used: Option<i64>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, Default)]
165#[serde(rename_all = "camelCase")]
166pub struct SecurityAuthSettings {
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub selected_type: Option<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172#[serde(rename_all = "camelCase")]
173pub struct SecuritySettings {
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub auth: Option<SecurityAuthSettings>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
179#[serde(rename_all = "camelCase")]
180pub struct WebDavSyncStatus {
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub last_sync_at: Option<i64>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub last_error: Option<String>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub last_error_source: Option<String>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub last_remote_etag: Option<String>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub last_local_manifest_hash: Option<String>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub last_remote_manifest_hash: Option<String>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
196#[serde(rename_all = "camelCase")]
197pub struct WebDavSyncSettings {
198 #[serde(default)]
199 pub enabled: bool,
200 #[serde(default)]
201 pub base_url: String,
202 #[serde(default = "default_webdav_remote_root")]
203 pub remote_root: String,
204 #[serde(default = "default_webdav_profile")]
205 pub profile: String,
206 #[serde(default)]
207 pub username: String,
208 #[serde(default)]
209 pub password: String,
210 #[serde(default)]
211 pub auto_sync: bool,
212 #[serde(default)]
213 pub status: WebDavSyncStatus,
214}
215
216fn default_webdav_remote_root() -> String {
217 "cc-switch-sync".to_string()
218}
219
220fn default_webdav_profile() -> String {
221 "default".to_string()
222}
223
224const JIANGUOYUN_WEBDAV_BASE_URL: &str = "https://dav.jianguoyun.com/dav";
225
226impl Default for WebDavSyncSettings {
227 fn default() -> Self {
228 Self {
229 enabled: false,
230 base_url: String::new(),
231 remote_root: default_webdav_remote_root(),
232 profile: default_webdav_profile(),
233 username: String::new(),
234 password: String::new(),
235 auto_sync: false,
236 status: WebDavSyncStatus::default(),
237 }
238 }
239}
240
241impl WebDavSyncSettings {
242 pub fn jianguoyun_preset(username: &str, password: &str) -> Self {
243 let mut settings = Self {
244 enabled: true,
245 base_url: JIANGUOYUN_WEBDAV_BASE_URL.to_string(),
246 remote_root: default_webdav_remote_root(),
247 profile: default_webdav_profile(),
248 username: username.to_string(),
249 password: password.to_string(),
250 ..Self::default()
251 };
252 settings.normalize();
253 settings
254 }
255
256 pub fn normalize(&mut self) {
257 self.base_url = self.base_url.trim().trim_end_matches('/').to_string();
258 self.remote_root = sanitize_path_segment(&self.remote_root);
259 self.profile = sanitize_path_segment(&self.profile);
260 self.username = self.username.trim().to_string();
261 self.password = self.password.trim().to_string();
262 }
263
264 pub fn validate(&self) -> Result<(), AppError> {
265 if !self.enabled && self.base_url.is_empty() {
266 return Ok(());
267 }
268 if self.base_url.is_empty() {
269 return Err(AppError::InvalidInput(
270 "WebDAV base_url 不能为空".to_string(),
271 ));
272 }
273 crate::services::webdav::parse_base_url(&self.base_url)?;
274 if self.remote_root.is_empty() || self.profile.is_empty() {
275 return Err(AppError::InvalidInput(
276 "WebDAV remote_root/profile 不能为空".to_string(),
277 ));
278 }
279 if self.remote_root.contains("..") || self.profile.contains("..") {
280 return Err(AppError::InvalidInput(
281 "WebDAV remote_root/profile 不能包含 '..'".to_string(),
282 ));
283 }
284 Ok(())
285 }
286}
287
288fn sanitize_path_segment(raw: &str) -> String {
289 raw.trim()
290 .trim_matches('/')
291 .split('/')
292 .filter(|s| !s.is_empty())
293 .collect::<Vec<_>>()
294 .join("/")
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct AppSettings {
301 #[serde(default = "default_show_in_tray")]
302 pub show_in_tray: bool,
303 #[serde(default = "default_minimize_to_tray_on_close")]
304 pub minimize_to_tray_on_close: bool,
305 #[serde(default)]
307 pub enable_claude_plugin_integration: bool,
308 #[serde(default)]
310 pub skip_claude_onboarding: bool,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub claude_config_dir: Option<String>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub codex_config_dir: Option<String>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub gemini_config_dir: Option<String>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub opencode_config_dir: Option<String>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub openclaw_config_dir: Option<String>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub hermes_config_dir: Option<String>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub current_provider_claude: Option<String>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub current_provider_codex: Option<String>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub current_provider_gemini: Option<String>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub current_provider_opencode: Option<String>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub current_provider_openclaw: Option<String>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub current_provider_hermes: Option<String>,
335 #[serde(default = "default_visible_apps")]
336 pub visible_apps: VisibleApps,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub language: Option<String>,
339 #[serde(default)]
341 pub launch_on_startup: bool,
342 #[serde(default)]
344 pub skill_sync_method: crate::services::skill::SyncMethod,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub security: Option<SecuritySettings>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub webdav_sync: Option<WebDavSyncSettings>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub backup_retain_count: Option<u32>,
351 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
353 pub custom_endpoints_claude: HashMap<String, CustomEndpoint>,
354 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
356 pub custom_endpoints_codex: HashMap<String, CustomEndpoint>,
357}
358
359fn default_show_in_tray() -> bool {
360 true
361}
362
363fn default_minimize_to_tray_on_close() -> bool {
364 true
365}
366
367impl Default for AppSettings {
368 fn default() -> Self {
369 Self {
370 show_in_tray: true,
371 minimize_to_tray_on_close: true,
372 enable_claude_plugin_integration: false,
373 skip_claude_onboarding: false,
374 claude_config_dir: None,
375 codex_config_dir: None,
376 gemini_config_dir: None,
377 opencode_config_dir: None,
378 openclaw_config_dir: None,
379 hermes_config_dir: None,
380 current_provider_claude: None,
381 current_provider_codex: None,
382 current_provider_gemini: None,
383 current_provider_opencode: None,
384 current_provider_openclaw: None,
385 current_provider_hermes: None,
386 visible_apps: default_visible_apps(),
387 language: None,
388 launch_on_startup: false,
389 skill_sync_method: crate::services::skill::SyncMethod::default(),
390 security: None,
391 webdav_sync: None,
392 backup_retain_count: None,
393 custom_endpoints_claude: HashMap::new(),
394 custom_endpoints_codex: HashMap::new(),
395 }
396 }
397}
398
399impl AppSettings {
400 fn settings_path() -> PathBuf {
401 get_app_config_dir().join("settings.json")
404 }
405
406 fn normalize_common(&mut self) {
407 self.claude_config_dir = self
408 .claude_config_dir
409 .as_ref()
410 .map(|s| s.trim())
411 .filter(|s| !s.is_empty())
412 .map(|s| s.to_string());
413
414 self.codex_config_dir = self
415 .codex_config_dir
416 .as_ref()
417 .map(|s| s.trim())
418 .filter(|s| !s.is_empty())
419 .map(|s| s.to_string());
420
421 self.gemini_config_dir = self
422 .gemini_config_dir
423 .as_ref()
424 .map(|s| s.trim())
425 .filter(|s| !s.is_empty())
426 .map(|s| s.to_string());
427
428 self.opencode_config_dir = self
429 .opencode_config_dir
430 .as_ref()
431 .map(|s| s.trim())
432 .filter(|s| !s.is_empty())
433 .map(|s| s.to_string());
434
435 self.openclaw_config_dir = self
436 .openclaw_config_dir
437 .as_ref()
438 .map(|s| s.trim())
439 .filter(|s| !s.is_empty())
440 .map(|s| s.to_string());
441
442 self.hermes_config_dir = self
443 .hermes_config_dir
444 .as_ref()
445 .map(|s| s.trim())
446 .filter(|s| !s.is_empty())
447 .map(|s| s.to_string());
448
449 self.language = self
450 .language
451 .as_ref()
452 .map(|s| s.trim())
453 .filter(|s| matches!(*s, "en" | "zh"))
454 .map(|s| s.to_string());
455
456 if let Some(webdav) = self.webdav_sync.as_mut() {
457 webdav.normalize();
458 }
459 }
460
461 fn normalize_loaded(&mut self) {
462 self.normalize_common();
463 self.visible_apps.normalize();
464 }
465
466 fn validate(&self) -> Result<(), AppError> {
467 self.visible_apps.validate()
468 }
469
470 pub fn load() -> Self {
471 let path = Self::settings_path();
472 if let Ok(content) = fs::read_to_string(&path) {
473 match serde_json::from_str::<AppSettings>(&content) {
474 Ok(mut settings) => {
475 settings.normalize_loaded();
476 settings
477 }
478 Err(err) => {
479 log::warn!(
480 "解析设置文件失败,将使用默认设置。路径: {}, 错误: {}",
481 path.display(),
482 err
483 );
484 Self::default()
485 }
486 }
487 } else {
488 Self::default()
489 }
490 }
491
492 pub fn save(&self) -> Result<(), AppError> {
493 let mut normalized = self.clone();
494 normalized.normalize_common();
495 normalized.validate()?;
496 let path = Self::settings_path();
497
498 if let Some(parent) = path.parent() {
499 fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
500 }
501
502 let json = serde_json::to_string_pretty(&normalized)
503 .map_err(|e| AppError::JsonSerialize { source: e })?;
504 fs::write(&path, json).map_err(|e| AppError::io(&path, e))?;
505 Ok(())
506 }
507}
508
509fn settings_store() -> &'static RwLock<AppSettings> {
510 static STORE: OnceLock<RwLock<AppSettings>> = OnceLock::new();
511 STORE.get_or_init(|| RwLock::new(AppSettings::load()))
512}
513
514pub fn reload_settings() -> Result<(), AppError> {
515 let fresh_settings = AppSettings::load();
516 let mut guard = settings_store().write().expect("写入设置锁失败");
517 *guard = fresh_settings;
518 Ok(())
519}
520
521#[cfg(test)]
522pub(crate) fn reload_test_settings() {
523 let mut guard = settings_store().write().expect("写入设置锁失败");
524 *guard = AppSettings::load();
525}
526
527fn resolve_override_path(raw: &str) -> PathBuf {
528 if raw == "~" {
529 if let Some(home) = home_dir() {
530 return home;
531 }
532 } else if let Some(stripped) = raw.strip_prefix("~/") {
533 if let Some(home) = home_dir() {
534 return home.join(stripped);
535 }
536 } else if let Some(stripped) = raw.strip_prefix("~\\") {
537 if let Some(home) = home_dir() {
538 return home.join(stripped);
539 }
540 }
541
542 PathBuf::from(raw)
543}
544
545pub fn get_settings() -> AppSettings {
546 settings_store().read().expect("读取设置锁失败").clone()
547}
548
549pub fn update_settings(mut new_settings: AppSettings) -> Result<(), AppError> {
550 new_settings.normalize_common();
551 new_settings.validate()?;
552 new_settings.save()?;
553
554 let mut guard = settings_store().write().expect("写入设置锁失败");
555 *guard = new_settings;
556 Ok(())
557}
558
559pub fn ensure_security_auth_selected_type(selected_type: &str) -> Result<(), AppError> {
560 let mut settings = get_settings();
561 let current = settings
562 .security
563 .as_ref()
564 .and_then(|sec| sec.auth.as_ref())
565 .and_then(|auth| auth.selected_type.as_deref());
566
567 if current == Some(selected_type) {
568 return Ok(());
569 }
570
571 let mut security = settings.security.unwrap_or_default();
572 let mut auth = security.auth.unwrap_or_default();
573 auth.selected_type = Some(selected_type.to_string());
574 security.auth = Some(auth);
575 settings.security = Some(security);
576
577 update_settings(settings)
578}
579
580pub fn get_claude_override_dir() -> Option<PathBuf> {
581 let settings = settings_store().read().ok()?;
582 settings
583 .claude_config_dir
584 .as_ref()
585 .map(|p| resolve_override_path(p))
586}
587
588pub fn get_codex_override_dir() -> Option<PathBuf> {
589 let settings = settings_store().read().ok()?;
590 settings
591 .codex_config_dir
592 .as_ref()
593 .map(|p| resolve_override_path(p))
594}
595
596pub fn get_gemini_override_dir() -> Option<PathBuf> {
597 let settings = settings_store().read().ok()?;
598 settings
599 .gemini_config_dir
600 .as_ref()
601 .map(|p| resolve_override_path(p))
602}
603
604pub fn get_opencode_override_dir() -> Option<PathBuf> {
605 let settings = settings_store().read().ok()?;
606 settings
607 .opencode_config_dir
608 .as_ref()
609 .map(|p| resolve_override_path(p))
610}
611
612pub fn get_openclaw_override_dir() -> Option<PathBuf> {
613 let settings = settings_store().read().ok()?;
614 settings
615 .openclaw_config_dir
616 .as_ref()
617 .map(|p| resolve_override_path(p))
618}
619
620pub fn get_hermes_override_dir() -> Option<PathBuf> {
621 let settings = settings_store().read().ok()?;
622 settings
623 .hermes_config_dir
624 .as_ref()
625 .map(|p| resolve_override_path(p))
626}
627
628pub fn get_current_provider(app_type: &AppType) -> Option<String> {
629 let settings = settings_store().read().ok()?;
630 match app_type {
631 AppType::Claude => settings.current_provider_claude.clone(),
632 AppType::Codex => settings.current_provider_codex.clone(),
633 AppType::Gemini => settings.current_provider_gemini.clone(),
634 AppType::OpenCode => settings.current_provider_opencode.clone(),
635 AppType::OpenClaw => settings.current_provider_openclaw.clone(),
636 AppType::Hermes => settings.current_provider_hermes.clone(),
637 }
638}
639
640pub fn set_current_provider(app_type: &AppType, id: Option<&str>) -> Result<(), AppError> {
641 let mut settings = get_settings();
642
643 match app_type {
644 AppType::Claude => settings.current_provider_claude = id.map(|value| value.to_string()),
645 AppType::Codex => settings.current_provider_codex = id.map(|value| value.to_string()),
646 AppType::Gemini => settings.current_provider_gemini = id.map(|value| value.to_string()),
647 AppType::OpenCode => settings.current_provider_opencode = id.map(|value| value.to_string()),
648 AppType::OpenClaw => settings.current_provider_openclaw = id.map(|value| value.to_string()),
649 AppType::Hermes => settings.current_provider_hermes = id.map(|value| value.to_string()),
650 }
651
652 update_settings(settings)
653}
654
655pub fn get_visible_apps() -> VisibleApps {
656 settings_store()
657 .read()
658 .map(|settings| settings.visible_apps.clone())
659 .unwrap_or_else(|_| default_visible_apps())
660}
661
662pub fn set_visible_apps(visible_apps: VisibleApps) -> Result<(), AppError> {
663 visible_apps.validate()?;
664
665 let mut settings = get_settings();
666 settings.visible_apps = visible_apps;
667 update_settings(settings)
668}
669
670pub fn get_effective_current_provider(
671 db: &crate::database::Database,
672 app_type: &AppType,
673) -> Result<Option<String>, AppError> {
674 if let Some(local_id) = get_current_provider(app_type) {
675 let providers = db.get_all_providers(app_type.as_str())?;
676 if providers.contains_key(&local_id) {
677 return Ok(Some(local_id));
678 }
679
680 log::warn!(
681 "本地 settings 中的供应商 {} ({}) 在数据库中不存在,将清理并 fallback 到数据库",
682 local_id,
683 app_type.as_str()
684 );
685 let _ = set_current_provider(app_type, None);
686 }
687
688 db.get_current_provider(app_type.as_str())
689}
690
691pub fn get_skill_sync_method() -> crate::services::skill::SyncMethod {
692 settings_store()
693 .read()
694 .map(|s| s.skill_sync_method)
695 .unwrap_or_default()
696}
697
698pub fn effective_backup_retain_count() -> usize {
699 settings_store()
700 .read()
701 .map(|settings| {
702 settings
703 .backup_retain_count
704 .map(|count| usize::try_from(count).unwrap_or(usize::MAX).max(1))
705 .unwrap_or(10)
706 })
707 .unwrap_or(10)
708}
709
710pub fn set_skill_sync_method(method: crate::services::skill::SyncMethod) -> Result<(), AppError> {
711 let mut settings = get_settings();
712 settings.skill_sync_method = method;
713 update_settings(settings)
714}
715
716pub fn get_webdav_sync_settings() -> Option<WebDavSyncSettings> {
717 settings_store()
718 .read()
719 .ok()
720 .and_then(|s| s.webdav_sync.clone())
721}
722
723pub fn set_webdav_sync_settings(webdav_sync: Option<WebDavSyncSettings>) -> Result<(), AppError> {
724 let mut settings = get_settings();
725 settings.webdav_sync = match webdav_sync {
726 Some(mut cfg) => {
727 cfg.normalize();
728 cfg.validate()?;
729 Some(cfg)
730 }
731 None => None,
732 };
733 update_settings(settings)
734}
735
736pub fn update_webdav_sync_status(status: WebDavSyncStatus) -> Result<(), AppError> {
737 let mut settings = get_settings();
738 if let Some(ref mut webdav) = settings.webdav_sync {
739 webdav.status = status;
740 }
741 update_settings(settings)
742}
743
744pub fn webdav_jianguoyun_preset(username: &str, password: &str) -> WebDavSyncSettings {
745 WebDavSyncSettings::jianguoyun_preset(username, password)
746}
747
748pub fn get_skip_claude_onboarding() -> bool {
749 settings_store()
750 .read()
751 .map(|s| s.skip_claude_onboarding)
752 .unwrap_or(false)
753}
754
755pub fn get_enable_claude_plugin_integration() -> bool {
756 settings_store()
757 .read()
758 .map(|s| s.enable_claude_plugin_integration)
759 .unwrap_or(false)
760}
761
762pub fn set_enable_claude_plugin_integration(enabled: bool) -> Result<(), AppError> {
763 let mut settings = get_settings();
764 settings.enable_claude_plugin_integration = enabled;
765 update_settings(settings)
766}
767
768pub fn set_skip_claude_onboarding(enabled: bool) -> Result<(), AppError> {
769 if enabled {
770 crate::claude_mcp::set_has_completed_onboarding()?;
771 } else {
772 crate::claude_mcp::clear_has_completed_onboarding()?;
773 }
774
775 let mut settings = get_settings();
776 settings.skip_claude_onboarding = enabled;
777 update_settings(settings)
778}