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 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub save_shortcut: Option<String>,
360}
361
362fn default_show_in_tray() -> bool {
363 true
364}
365
366fn default_minimize_to_tray_on_close() -> bool {
367 true
368}
369
370impl Default for AppSettings {
371 fn default() -> Self {
372 Self {
373 show_in_tray: true,
374 minimize_to_tray_on_close: true,
375 enable_claude_plugin_integration: false,
376 skip_claude_onboarding: false,
377 claude_config_dir: None,
378 codex_config_dir: None,
379 gemini_config_dir: None,
380 opencode_config_dir: None,
381 openclaw_config_dir: None,
382 hermes_config_dir: None,
383 current_provider_claude: None,
384 current_provider_codex: None,
385 current_provider_gemini: None,
386 current_provider_opencode: None,
387 current_provider_openclaw: None,
388 current_provider_hermes: None,
389 visible_apps: default_visible_apps(),
390 language: None,
391 launch_on_startup: false,
392 skill_sync_method: crate::services::skill::SyncMethod::default(),
393 security: None,
394 webdav_sync: None,
395 backup_retain_count: None,
396 custom_endpoints_claude: HashMap::new(),
397 custom_endpoints_codex: HashMap::new(),
398 save_shortcut: None,
399 }
400 }
401}
402
403impl AppSettings {
404 fn settings_path() -> PathBuf {
405 get_app_config_dir().join("settings.json")
408 }
409
410 fn normalize_common(&mut self) {
411 self.claude_config_dir = self
412 .claude_config_dir
413 .as_ref()
414 .map(|s| s.trim())
415 .filter(|s| !s.is_empty())
416 .map(|s| s.to_string());
417
418 self.codex_config_dir = self
419 .codex_config_dir
420 .as_ref()
421 .map(|s| s.trim())
422 .filter(|s| !s.is_empty())
423 .map(|s| s.to_string());
424
425 self.gemini_config_dir = self
426 .gemini_config_dir
427 .as_ref()
428 .map(|s| s.trim())
429 .filter(|s| !s.is_empty())
430 .map(|s| s.to_string());
431
432 self.opencode_config_dir = self
433 .opencode_config_dir
434 .as_ref()
435 .map(|s| s.trim())
436 .filter(|s| !s.is_empty())
437 .map(|s| s.to_string());
438
439 self.openclaw_config_dir = self
440 .openclaw_config_dir
441 .as_ref()
442 .map(|s| s.trim())
443 .filter(|s| !s.is_empty())
444 .map(|s| s.to_string());
445
446 self.hermes_config_dir = self
447 .hermes_config_dir
448 .as_ref()
449 .map(|s| s.trim())
450 .filter(|s| !s.is_empty())
451 .map(|s| s.to_string());
452
453 self.language = self
454 .language
455 .as_ref()
456 .map(|s| s.trim())
457 .filter(|s| matches!(*s, "en" | "zh"))
458 .map(|s| s.to_string());
459
460 if let Some(webdav) = self.webdav_sync.as_mut() {
461 webdav.normalize();
462 }
463 }
464
465 fn normalize_loaded(&mut self) {
466 self.normalize_common();
467 self.visible_apps.normalize();
468 }
469
470 fn validate(&self) -> Result<(), AppError> {
471 self.visible_apps.validate()
472 }
473
474 pub fn load() -> Self {
475 let path = Self::settings_path();
476 if let Ok(content) = fs::read_to_string(&path) {
477 match serde_json::from_str::<AppSettings>(&content) {
478 Ok(mut settings) => {
479 settings.normalize_loaded();
480 settings
481 }
482 Err(err) => {
483 log::warn!(
484 "解析设置文件失败,将使用默认设置。路径: {}, 错误: {}",
485 path.display(),
486 err
487 );
488 Self::default()
489 }
490 }
491 } else {
492 Self::default()
493 }
494 }
495
496 pub fn save(&self) -> Result<(), AppError> {
497 let mut normalized = self.clone();
498 normalized.normalize_common();
499 normalized.validate()?;
500 let path = Self::settings_path();
501
502 if let Some(parent) = path.parent() {
503 fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
504 }
505
506 let json = serde_json::to_string_pretty(&normalized)
507 .map_err(|e| AppError::JsonSerialize { source: e })?;
508 fs::write(&path, json).map_err(|e| AppError::io(&path, e))?;
509 Ok(())
510 }
511}
512
513fn settings_store() -> &'static RwLock<AppSettings> {
514 static STORE: OnceLock<RwLock<AppSettings>> = OnceLock::new();
515 STORE.get_or_init(|| RwLock::new(AppSettings::load()))
516}
517
518pub fn reload_settings() -> Result<(), AppError> {
519 let fresh_settings = AppSettings::load();
520 let mut guard = settings_store().write().expect("写入设置锁失败");
521 *guard = fresh_settings;
522 Ok(())
523}
524
525#[cfg(test)]
526pub(crate) fn reload_test_settings() {
527 let mut guard = settings_store().write().expect("写入设置锁失败");
528 *guard = AppSettings::load();
529}
530
531fn resolve_override_path(raw: &str) -> PathBuf {
532 if raw == "~" {
533 if let Some(home) = home_dir() {
534 return home;
535 }
536 } else if let Some(stripped) = raw.strip_prefix("~/") {
537 if let Some(home) = home_dir() {
538 return home.join(stripped);
539 }
540 } else if let Some(stripped) = raw.strip_prefix("~\\") {
541 if let Some(home) = home_dir() {
542 return home.join(stripped);
543 }
544 }
545
546 PathBuf::from(raw)
547}
548
549pub fn get_settings() -> AppSettings {
550 settings_store().read().expect("读取设置锁失败").clone()
551}
552
553pub fn update_settings(mut new_settings: AppSettings) -> Result<(), AppError> {
554 new_settings.normalize_common();
555 new_settings.validate()?;
556 new_settings.save()?;
557
558 let mut guard = settings_store().write().expect("写入设置锁失败");
559 *guard = new_settings;
560 Ok(())
561}
562
563pub fn ensure_security_auth_selected_type(selected_type: &str) -> Result<(), AppError> {
564 let mut settings = get_settings();
565 let current = settings
566 .security
567 .as_ref()
568 .and_then(|sec| sec.auth.as_ref())
569 .and_then(|auth| auth.selected_type.as_deref());
570
571 if current == Some(selected_type) {
572 return Ok(());
573 }
574
575 let mut security = settings.security.unwrap_or_default();
576 let mut auth = security.auth.unwrap_or_default();
577 auth.selected_type = Some(selected_type.to_string());
578 security.auth = Some(auth);
579 settings.security = Some(security);
580
581 update_settings(settings)
582}
583
584pub fn get_claude_override_dir() -> Option<PathBuf> {
585 let settings = settings_store().read().ok()?;
586 settings
587 .claude_config_dir
588 .as_ref()
589 .map(|p| resolve_override_path(p))
590}
591
592pub fn get_codex_override_dir() -> Option<PathBuf> {
593 let settings = settings_store().read().ok()?;
594 settings
595 .codex_config_dir
596 .as_ref()
597 .map(|p| resolve_override_path(p))
598}
599
600pub fn get_gemini_override_dir() -> Option<PathBuf> {
601 let settings = settings_store().read().ok()?;
602 settings
603 .gemini_config_dir
604 .as_ref()
605 .map(|p| resolve_override_path(p))
606}
607
608pub fn get_opencode_override_dir() -> Option<PathBuf> {
609 let settings = settings_store().read().ok()?;
610 settings
611 .opencode_config_dir
612 .as_ref()
613 .map(|p| resolve_override_path(p))
614}
615
616pub fn get_openclaw_override_dir() -> Option<PathBuf> {
617 let settings = settings_store().read().ok()?;
618 settings
619 .openclaw_config_dir
620 .as_ref()
621 .map(|p| resolve_override_path(p))
622}
623
624pub fn get_hermes_override_dir() -> Option<PathBuf> {
625 let settings = settings_store().read().ok()?;
626 settings
627 .hermes_config_dir
628 .as_ref()
629 .map(|p| resolve_override_path(p))
630}
631
632pub fn get_current_provider(app_type: &AppType) -> Option<String> {
633 let settings = settings_store().read().ok()?;
634 match app_type {
635 AppType::Claude => settings.current_provider_claude.clone(),
636 AppType::Codex => settings.current_provider_codex.clone(),
637 AppType::Gemini => settings.current_provider_gemini.clone(),
638 AppType::OpenCode => settings.current_provider_opencode.clone(),
639 AppType::OpenClaw => settings.current_provider_openclaw.clone(),
640 AppType::Hermes => settings.current_provider_hermes.clone(),
641 }
642}
643
644pub fn set_current_provider(app_type: &AppType, id: Option<&str>) -> Result<(), AppError> {
645 let mut settings = get_settings();
646
647 match app_type {
648 AppType::Claude => settings.current_provider_claude = id.map(|value| value.to_string()),
649 AppType::Codex => settings.current_provider_codex = id.map(|value| value.to_string()),
650 AppType::Gemini => settings.current_provider_gemini = id.map(|value| value.to_string()),
651 AppType::OpenCode => settings.current_provider_opencode = id.map(|value| value.to_string()),
652 AppType::OpenClaw => settings.current_provider_openclaw = id.map(|value| value.to_string()),
653 AppType::Hermes => settings.current_provider_hermes = id.map(|value| value.to_string()),
654 }
655
656 update_settings(settings)
657}
658
659pub fn get_visible_apps() -> VisibleApps {
660 settings_store()
661 .read()
662 .map(|settings| settings.visible_apps.clone())
663 .unwrap_or_else(|_| default_visible_apps())
664}
665
666pub fn set_visible_apps(visible_apps: VisibleApps) -> Result<(), AppError> {
667 visible_apps.validate()?;
668
669 let mut settings = get_settings();
670 settings.visible_apps = visible_apps;
671 update_settings(settings)
672}
673
674pub fn get_effective_current_provider(
675 db: &crate::database::Database,
676 app_type: &AppType,
677) -> Result<Option<String>, AppError> {
678 if let Some(local_id) = get_current_provider(app_type) {
679 let providers = db.get_all_providers(app_type.as_str())?;
680 if providers.contains_key(&local_id) {
681 return Ok(Some(local_id));
682 }
683
684 log::warn!(
685 "本地 settings 中的供应商 {} ({}) 在数据库中不存在,将清理并 fallback 到数据库",
686 local_id,
687 app_type.as_str()
688 );
689 let _ = set_current_provider(app_type, None);
690 }
691
692 db.get_current_provider(app_type.as_str())
693}
694
695pub fn get_skill_sync_method() -> crate::services::skill::SyncMethod {
696 settings_store()
697 .read()
698 .map(|s| s.skill_sync_method)
699 .unwrap_or_default()
700}
701
702pub fn effective_backup_retain_count() -> usize {
703 settings_store()
704 .read()
705 .map(|settings| {
706 settings
707 .backup_retain_count
708 .map(|count| usize::try_from(count).unwrap_or(usize::MAX).max(1))
709 .unwrap_or(10)
710 })
711 .unwrap_or(10)
712}
713
714pub fn set_skill_sync_method(method: crate::services::skill::SyncMethod) -> Result<(), AppError> {
715 let mut settings = get_settings();
716 settings.skill_sync_method = method;
717 update_settings(settings)
718}
719
720pub fn get_webdav_sync_settings() -> Option<WebDavSyncSettings> {
721 settings_store()
722 .read()
723 .ok()
724 .and_then(|s| s.webdav_sync.clone())
725}
726
727pub fn set_webdav_sync_settings(webdav_sync: Option<WebDavSyncSettings>) -> Result<(), AppError> {
728 let mut settings = get_settings();
729 settings.webdav_sync = match webdav_sync {
730 Some(mut cfg) => {
731 cfg.normalize();
732 cfg.validate()?;
733 Some(cfg)
734 }
735 None => None,
736 };
737 update_settings(settings)
738}
739
740pub fn update_webdav_sync_status(status: WebDavSyncStatus) -> Result<(), AppError> {
741 let mut settings = get_settings();
742 if let Some(ref mut webdav) = settings.webdav_sync {
743 webdav.status = status;
744 }
745 update_settings(settings)
746}
747
748pub fn get_save_shortcut() -> String {
749 get_settings()
750 .save_shortcut
751 .unwrap_or_else(|| "Ctrl+S".to_string())
752}
753
754pub fn webdav_jianguoyun_preset(username: &str, password: &str) -> WebDavSyncSettings {
755 WebDavSyncSettings::jianguoyun_preset(username, password)
756}
757
758pub fn get_skip_claude_onboarding() -> bool {
759 settings_store()
760 .read()
761 .map(|s| s.skip_claude_onboarding)
762 .unwrap_or(false)
763}
764
765pub fn get_enable_claude_plugin_integration() -> bool {
766 settings_store()
767 .read()
768 .map(|s| s.enable_claude_plugin_integration)
769 .unwrap_or(false)
770}
771
772pub fn set_enable_claude_plugin_integration(enabled: bool) -> Result<(), AppError> {
773 let mut settings = get_settings();
774 settings.enable_claude_plugin_integration = enabled;
775 update_settings(settings)
776}
777
778pub fn set_skip_claude_onboarding(enabled: bool) -> Result<(), AppError> {
779 if enabled {
780 crate::claude_mcp::set_has_completed_onboarding()?;
781 } else {
782 crate::claude_mcp::clear_has_completed_onboarding()?;
783 }
784
785 let mut settings = get_settings();
786 settings.skip_claude_onboarding = enabled;
787 update_settings(settings)
788}