1use crate::constants::env::{ai, system};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9use std::sync::Mutex;
10
11use once_cell::sync::Lazy;
12
13static INSIDE_GET_CONFIG: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
16
17static GLOBAL_CONFIG_CACHE: Lazy<Mutex<GlobalConfigCache>> =
19 Lazy::new(|| Mutex::new(GlobalConfigCache::default()));
20
21#[derive(Default)]
22struct GlobalConfigCache {
23 config: Option<GlobalConfig>,
24 mtime: u64,
25}
26
27#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum InstallMethod {
31 #[default]
32 Unknown,
33 Local,
34 Native,
35 Global,
36}
37
38#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
40#[serde(rename_all = "lowercase")]
41pub enum ReleaseChannel {
42 #[default]
43 Stable,
44 Latest,
45}
46
47#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum DiffTool {
51 #[default]
52 Auto,
53 Terminal,
54}
55
56#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum EditorMode {
60 #[default]
61 Normal,
62 Emacs,
63 Vim,
64}
65
66#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
68#[serde(rename_all = "kebab-case")]
69pub enum NotificationChannel {
70 #[default]
71 Auto,
72 Terminal,
73 Native,
74}
75
76#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
78#[serde(rename_all = "lowercase")]
79pub enum ThemeSetting {
80 #[default]
81 Dark,
82 Light,
83 System,
84}
85
86#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
88pub struct AccountInfo {
89 #[serde(default)]
90 pub account_uuid: String,
91 #[serde(default)]
92 pub email_address: String,
93 pub organization_uuid: Option<String>,
94 pub organization_name: Option<String>,
95 pub organization_role: Option<String>,
96 pub workspace_role: Option<String>,
97 pub display_name: Option<String>,
98 pub has_extra_usage_enabled: Option<bool>,
99 pub billing_type: Option<String>,
100 pub account_created_at: Option<String>,
101 pub subscription_created_at: Option<String>,
102}
103
104#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
106pub struct McpServerConfig {
107 pub command: Option<String>,
108 pub args: Option<Vec<String>>,
109 pub env: Option<HashMap<String, String>>,
110}
111
112#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct ProjectConfig {
116 #[serde(default)]
117 pub allowed_tools: Vec<String>,
118 #[serde(default)]
119 pub mcp_context_uris: Vec<String>,
120 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
121 pub last_api_duration: Option<u64>,
122 pub last_api_duration_without_retries: Option<u64>,
123 pub last_tool_duration: Option<u64>,
124 pub last_cost: Option<f64>,
125 pub last_duration: Option<u64>,
126 pub last_lines_added: Option<u32>,
127 pub last_lines_removed: Option<u32>,
128 pub last_total_input_tokens: Option<u32>,
129 pub last_total_output_tokens: Option<u32>,
130 pub last_total_cache_creation_input_tokens: Option<u32>,
131 pub last_total_cache_read_input_tokens: Option<u32>,
132 pub last_total_web_search_requests: Option<u32>,
133 pub last_fps_average: Option<f64>,
134 pub last_fps_low_1_pct: Option<f64>,
135 pub last_session_id: Option<String>,
136 pub last_model_usage: Option<HashMap<String, ModelUsage>>,
137 pub last_session_metrics: Option<HashMap<String, f64>>,
138 pub example_files: Option<Vec<String>>,
139 pub example_files_generated_at: Option<u64>,
140 #[serde(default)]
141 pub has_trust_dialog_accepted: bool,
142 #[serde(default)]
143 pub has_completed_project_onboarding: bool,
144 #[serde(default)]
145 pub project_onboarding_seen_count: u32,
146 #[serde(default)]
147 pub has_claude_md_external_includes_approved: bool,
148 #[serde(default)]
149 pub has_claude_md_external_includes_warning_shown: bool,
150 pub enabled_mcpjson_servers: Option<Vec<String>>,
151 pub disabled_mcpjson_servers: Option<Vec<String>>,
152 pub enable_all_project_mcp_servers: Option<bool>,
153 pub disabled_mcp_servers: Option<Vec<String>>,
154 pub enabled_mcp_servers: Option<Vec<String>>,
155 pub active_worktree_session: Option<WorktreeSession>,
156 pub remote_control_spawn_mode: Option<String>,
157}
158
159#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
161pub struct ModelUsage {
162 #[serde(default)]
163 pub input_tokens: u32,
164 #[serde(default)]
165 pub output_tokens: u32,
166 #[serde(default)]
167 pub cache_read_input_tokens: u32,
168 #[serde(default)]
169 pub cache_creation_input_tokens: u32,
170 #[serde(default)]
171 pub web_search_requests: u32,
172 #[serde(default)]
173 pub cost_usd: f64,
174}
175
176#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
178pub struct WorktreeSession {
179 #[serde(default)]
180 pub original_cwd: String,
181 #[serde(default)]
182 pub worktree_path: String,
183 #[serde(default)]
184 pub worktree_name: String,
185 pub original_branch: Option<String>,
186 pub session_id: String,
187 pub hook_based: Option<bool>,
188}
189
190#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct GlobalConfig {
195 pub api_key_helper: Option<String>,
196 pub projects: Option<HashMap<String, ProjectConfig>>,
197 #[serde(default)]
198 pub num_startups: u32,
199 pub install_method: Option<InstallMethod>,
200 pub auto_updates: Option<bool>,
201 pub auto_updates_protected_for_native: Option<bool>,
202 pub doctor_shown_at_session: Option<u32>,
203 pub user_id: Option<String>,
204 #[serde(default)]
205 pub theme: Option<ThemeSetting>,
206 pub has_completed_onboarding: Option<bool>,
207 pub last_onboarding_version: Option<String>,
208 pub last_release_notes_seen: Option<String>,
209 pub changelog_last_fetched: Option<u64>,
210 pub cached_changelog: Option<String>,
211 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
212 pub claude_ai_mcp_ever_connected: Option<Vec<String>>,
213 #[serde(default)]
214 pub preferred_notif_channel: NotificationChannel,
215 pub custom_notify_command: Option<String>,
216 #[serde(default)]
217 pub verbose: Option<bool>,
218 pub custom_api_key_responses: Option<CustomApiKeyResponses>,
219 pub primary_api_key: Option<String>,
220 pub has_acknowledged_cost_threshold: Option<bool>,
221 pub has_seen_undercover_auto_notice: Option<bool>,
222 pub has_seen_ultraplan_terms: Option<bool>,
223 pub has_reset_auto_mode_opt_in_for_default_offer: Option<bool>,
224 pub oauth_account: Option<AccountInfo>,
225 pub iterm2_key_binding_installed: Option<bool>,
226 pub editor_mode: Option<EditorMode>,
227 pub bypass_permissions_mode_accepted: Option<bool>,
228 pub has_used_backslash_return: Option<bool>,
229 #[serde(default)]
230 pub auto_compact_enabled: bool,
231 #[serde(default)]
232 pub show_turn_duration: bool,
233 #[serde(default)]
234 pub env: HashMap<String, String>,
235 pub has_seen_tasks_hint: Option<bool>,
236 pub has_used_stash: Option<bool>,
237 pub has_used_background_task: Option<bool>,
238 pub queued_command_up_hint_count: Option<u32>,
239 pub diff_tool: Option<DiffTool>,
240 pub iterm2_setup_in_progress: Option<bool>,
241 pub iterm2_backup_path: Option<String>,
242 pub apple_terminal_backup_path: Option<String>,
243 pub apple_terminal_setup_in_progress: Option<bool>,
244 pub shift_enter_key_binding_installed: Option<bool>,
245 pub option_as_meta_key_installed: Option<bool>,
246 pub auto_connect_ide: Option<bool>,
247 pub auto_install_ide_extension: Option<bool>,
248 pub has_ide_onboarding_been_shown: Option<HashMap<String, bool>>,
249 pub ide_hint_shown_count: Option<u32>,
250 pub has_ide_auto_connect_dialog_been_shown: Option<bool>,
251 #[serde(default)]
252 pub tips_history: HashMap<String, u32>,
253 pub companion: Option<serde_json::Value>,
254 pub companion_muted: Option<bool>,
255 pub feedback_survey_state: Option<FeedbackSurveyState>,
256 pub transcript_share_dismissed: Option<bool>,
257 #[serde(default)]
258 pub memory_usage_count: u32,
259 pub has_shown_s1m_welcome_v2: Option<HashMap<String, bool>>,
260 pub s1m_access_cache: Option<HashMap<String, S1mAccessCacheEntry>>,
261 pub s1m_non_subscriber_access_cache: Option<HashMap<String, S1mAccessCacheEntry>>,
262 pub passes_eligibility_cache: Option<HashMap<String, serde_json::Value>>,
263 pub grove_config_cache: Option<HashMap<String, GroveConfigCacheEntry>>,
264 pub passes_upsell_seen_count: Option<u32>,
265 pub has_visited_passes: Option<bool>,
266 pub passes_last_seen_remaining: Option<u32>,
267 pub overage_credit_grant_cache: Option<HashMap<String, OverageCreditCacheEntry>>,
268 pub overage_credit_upsell_seen_count: Option<u32>,
269 pub has_visited_extra_usage: Option<bool>,
270 pub voice_notice_seen_count: Option<u32>,
271 pub voice_lang_hint_shown_count: Option<u32>,
272 pub voice_lang_hint_last_language: Option<String>,
273 pub voice_footer_hint_seen_count: Option<u32>,
274 pub opus_1m_merge_notice_seen_count: Option<u32>,
275 pub experiment_notices_seen_count: Option<HashMap<String, u32>>,
276 pub has_shown_opus_plan_welcome: Option<HashMap<String, bool>>,
277 #[serde(default)]
278 pub prompt_queue_use_count: u32,
279 #[serde(default)]
280 pub btw_use_count: u32,
281 pub last_plan_mode_use: Option<u64>,
282 pub subscription_notice_count: Option<u32>,
283 pub has_available_subscription: Option<bool>,
284 pub subscription_upsell_seen_count: Option<u32>,
285 pub recommended_subscription: Option<String>,
286 #[serde(default)]
287 pub todo_feature_enabled: bool,
288 pub show_expanded_todos: Option<bool>,
289 pub show_spinner_tree: Option<bool>,
290 pub first_start_time: Option<String>,
291 #[serde(default)]
292 pub message_idle_notif_threshold_ms: u64,
293 pub github_action_setup_count: Option<u32>,
294 pub slack_app_install_count: Option<u32>,
295 #[serde(default)]
296 pub file_checkpointing_enabled: bool,
297 #[serde(default)]
298 pub terminal_progress_bar_enabled: bool,
299 pub show_status_in_terminal_tab: Option<bool>,
300 pub task_complete_notif_enabled: Option<bool>,
301 pub input_needed_notif_enabled: Option<bool>,
302 pub agent_push_notif_enabled: Option<bool>,
303 pub claude_code_first_token_date: Option<String>,
304 pub model_switch_callout_dismissed: Option<bool>,
305 pub model_switch_callout_last_shown: Option<u64>,
306 pub model_switch_callout_version: Option<String>,
307 pub effort_callout_dismissed: Option<bool>,
308 pub effort_callout_v2_dismissed: Option<bool>,
309 pub remote_dialog_seen: Option<bool>,
310 pub bridge_oauth_dead_expires_at: Option<u64>,
311 pub bridge_oauth_dead_fail_count: Option<u32>,
312 pub desktop_upsell_seen_count: Option<u32>,
313 pub desktop_upsell_dismissed: Option<bool>,
314 pub idle_return_dismissed: Option<bool>,
315 pub opus_pro_migration_complete: Option<bool>,
316 pub opus_pro_migration_timestamp: Option<u64>,
317 pub sonnet_1m_45_migration_complete: Option<bool>,
318 pub legacy_opus_migration_timestamp: Option<u64>,
319 pub sonnet_45_to_46_migration_timestamp: Option<u64>,
320 #[serde(default)]
321 pub cached_statsig_gates: HashMap<String, bool>,
322 pub cached_dynamic_configs: Option<HashMap<String, serde_json::Value>>,
323 pub cached_growth_book_features: Option<HashMap<String, serde_json::Value>>,
324 pub growth_book_overrides: Option<HashMap<String, serde_json::Value>>,
325 pub last_shown_emergency_tip: Option<String>,
326 #[serde(default)]
327 pub respect_gitignore: bool,
328 #[serde(default)]
329 pub copy_full_response: bool,
330 pub copy_on_select: Option<bool>,
331 pub github_repo_paths: Option<HashMap<String, Vec<String>>>,
332 pub deep_link_terminal: Option<String>,
333 pub iterm2_it2_setup_complete: Option<bool>,
334 pub prefer_tmux_over_iterm2: Option<bool>,
335 pub skill_usage: Option<HashMap<String, SkillUsageEntry>>,
336 pub official_marketplace_auto_install_attempted: Option<bool>,
337 pub official_marketplace_auto_installed: Option<bool>,
338 pub official_marketplace_auto_install_fail_reason: Option<String>,
339 pub official_marketplace_auto_install_retry_count: Option<u32>,
340 pub official_marketplace_auto_install_last_attempt_time: Option<u64>,
341 pub official_marketplace_auto_install_next_retry_time: Option<u64>,
342 pub has_completed_claude_in_chrome_onboarding: Option<bool>,
343 pub claude_in_chrome_default_enabled: Option<bool>,
344 pub cached_chrome_extension_installed: Option<bool>,
345 pub chrome_extension: Option<ChromeExtensionState>,
346 pub lsp_recommendation_disabled: Option<bool>,
347 pub lsp_recommendation_never_plugins: Option<Vec<String>>,
348 pub lsp_recommendation_ignored_count: Option<u32>,
349 pub claude_code_hints: Option<ClaudeCodeHints>,
350 pub permission_explainer_enabled: Option<bool>,
351 pub teammate_mode: Option<String>,
352 pub teammate_default_model: Option<String>,
353 pub pr_status_footer_enabled: Option<bool>,
354 pub tungsten_panel_visible: Option<bool>,
355 pub penguin_mode_org_enabled: Option<bool>,
356 pub startup_prefetched_at: Option<u64>,
357 pub remote_control_at_startup: Option<bool>,
358 pub cached_extra_usage_disabled_reason: Option<String>,
359 pub auto_permissions_notification_count: Option<u32>,
360 pub speculation_enabled: Option<bool>,
361 pub client_data_cache: Option<serde_json::Value>,
362 pub additional_model_options_cache: Option<Vec<serde_json::Value>>,
363 pub metrics_status_cache: Option<MetricsStatusCache>,
364 pub migration_version: Option<u32>,
365}
366
367#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
369pub struct FeedbackSurveyState {
370 pub last_shown_time: Option<u64>,
371}
372
373#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
375pub struct S1mAccessCacheEntry {
376 pub has_access: bool,
377 pub has_access_not_as_default: Option<bool>,
378 pub timestamp: u64,
379}
380
381#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
383pub struct GroveConfigCacheEntry {
384 pub grove_enabled: bool,
385 pub timestamp: u64,
386}
387
388#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
390pub struct OverageCreditCacheEntry {
391 pub info: OverageCreditInfo,
392 pub timestamp: u64,
393}
394
395#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
397pub struct OverageCreditInfo {
398 pub available: bool,
399 pub eligible: bool,
400 pub granted: bool,
401 pub amount_minor_units: Option<i64>,
402 pub currency: Option<String>,
403}
404
405#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
407pub struct SkillUsageEntry {
408 pub usage_count: u32,
409 pub last_used_at: u64,
410}
411
412#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
414pub struct ChromeExtensionState {
415 pub paired_device_id: Option<String>,
416 pub paired_device_name: Option<String>,
417}
418
419#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
421pub struct ClaudeCodeHints {
422 pub plugin: Option<Vec<String>>,
423 pub disabled: Option<bool>,
424}
425
426#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
428pub struct MetricsStatusCache {
429 pub enabled: bool,
430 pub timestamp: u64,
431}
432
433#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
435pub struct CustomApiKeyResponses {
436 pub approved: Option<Vec<String>>,
437 pub rejected: Option<Vec<String>>,
438}
439
440impl Default for GlobalConfig {
441 fn default() -> Self {
442 GlobalConfig {
443 num_startups: 0,
444 install_method: None,
445 auto_updates: None,
446 theme: Some(ThemeSetting::Dark),
447 preferred_notif_channel: NotificationChannel::Auto,
448 verbose: Some(false),
449 editor_mode: Some(EditorMode::Normal),
450 auto_compact_enabled: true,
451 show_turn_duration: true,
452 queued_command_up_hint_count: Some(0),
453 diff_tool: Some(DiffTool::Auto),
454 custom_api_key_responses: Some(CustomApiKeyResponses {
455 approved: Some(vec![]),
456 rejected: Some(vec![]),
457 }),
458 env: HashMap::new(),
459 tips_history: HashMap::new(),
460 memory_usage_count: 0,
461 prompt_queue_use_count: 0,
462 btw_use_count: 0,
463 todo_feature_enabled: true,
464 show_expanded_todos: Some(false),
465 message_idle_notif_threshold_ms: 60000,
466 auto_connect_ide: Some(false),
467 auto_install_ide_extension: Some(true),
468 file_checkpointing_enabled: true,
469 terminal_progress_bar_enabled: true,
470 cached_statsig_gates: HashMap::new(),
471 cached_dynamic_configs: Some(HashMap::new()),
472 cached_growth_book_features: Some(HashMap::new()),
473 respect_gitignore: true,
474 copy_full_response: false,
475 api_key_helper: None,
477 projects: None,
478 auto_updates_protected_for_native: None,
479 doctor_shown_at_session: None,
480 user_id: None,
481 has_completed_onboarding: None,
482 last_onboarding_version: None,
483 last_release_notes_seen: None,
484 changelog_last_fetched: None,
485 cached_changelog: None,
486 mcp_servers: None,
487 claude_ai_mcp_ever_connected: None,
488 custom_notify_command: None,
489 primary_api_key: None,
490 has_acknowledged_cost_threshold: None,
491 has_seen_undercover_auto_notice: None,
492 has_seen_ultraplan_terms: None,
493 has_reset_auto_mode_opt_in_for_default_offer: None,
494 oauth_account: None,
495 iterm2_key_binding_installed: None,
496 bypass_permissions_mode_accepted: None,
497 has_used_backslash_return: None,
498 has_seen_tasks_hint: None,
499 has_used_stash: None,
500 has_used_background_task: None,
501 iterm2_setup_in_progress: None,
502 iterm2_backup_path: None,
503 apple_terminal_backup_path: None,
504 apple_terminal_setup_in_progress: None,
505 shift_enter_key_binding_installed: None,
506 option_as_meta_key_installed: None,
507 has_ide_onboarding_been_shown: None,
508 ide_hint_shown_count: None,
509 has_ide_auto_connect_dialog_been_shown: None,
510 companion: None,
511 companion_muted: None,
512 feedback_survey_state: None,
513 transcript_share_dismissed: None,
514 has_shown_s1m_welcome_v2: None,
515 s1m_access_cache: None,
516 s1m_non_subscriber_access_cache: None,
517 passes_eligibility_cache: None,
518 grove_config_cache: None,
519 passes_upsell_seen_count: None,
520 has_visited_passes: None,
521 passes_last_seen_remaining: None,
522 overage_credit_grant_cache: None,
523 overage_credit_upsell_seen_count: None,
524 has_visited_extra_usage: None,
525 voice_notice_seen_count: None,
526 voice_lang_hint_shown_count: None,
527 voice_lang_hint_last_language: None,
528 voice_footer_hint_seen_count: None,
529 opus_1m_merge_notice_seen_count: None,
530 experiment_notices_seen_count: None,
531 has_shown_opus_plan_welcome: None,
532 last_plan_mode_use: None,
533 subscription_notice_count: None,
534 has_available_subscription: None,
535 subscription_upsell_seen_count: None,
536 recommended_subscription: None,
537 show_spinner_tree: None,
538 first_start_time: None,
539 github_action_setup_count: None,
540 slack_app_install_count: None,
541 show_status_in_terminal_tab: None,
542 task_complete_notif_enabled: None,
543 input_needed_notif_enabled: None,
544 agent_push_notif_enabled: None,
545 claude_code_first_token_date: None,
546 model_switch_callout_dismissed: None,
547 model_switch_callout_last_shown: None,
548 model_switch_callout_version: None,
549 effort_callout_dismissed: None,
550 effort_callout_v2_dismissed: None,
551 remote_dialog_seen: None,
552 bridge_oauth_dead_expires_at: None,
553 bridge_oauth_dead_fail_count: None,
554 desktop_upsell_seen_count: None,
555 desktop_upsell_dismissed: None,
556 idle_return_dismissed: None,
557 opus_pro_migration_complete: None,
558 opus_pro_migration_timestamp: None,
559 sonnet_1m_45_migration_complete: None,
560 legacy_opus_migration_timestamp: None,
561 sonnet_45_to_46_migration_timestamp: None,
562 growth_book_overrides: None,
563 last_shown_emergency_tip: None,
564 copy_on_select: None,
565 github_repo_paths: None,
566 deep_link_terminal: None,
567 iterm2_it2_setup_complete: None,
568 prefer_tmux_over_iterm2: None,
569 skill_usage: None,
570 official_marketplace_auto_install_attempted: None,
571 official_marketplace_auto_installed: None,
572 official_marketplace_auto_install_fail_reason: None,
573 official_marketplace_auto_install_retry_count: None,
574 official_marketplace_auto_install_last_attempt_time: None,
575 official_marketplace_auto_install_next_retry_time: None,
576 has_completed_claude_in_chrome_onboarding: None,
577 claude_in_chrome_default_enabled: None,
578 cached_chrome_extension_installed: None,
579 chrome_extension: None,
580 lsp_recommendation_disabled: None,
581 lsp_recommendation_never_plugins: None,
582 lsp_recommendation_ignored_count: None,
583 claude_code_hints: None,
584 permission_explainer_enabled: None,
585 teammate_mode: None,
586 teammate_default_model: None,
587 pr_status_footer_enabled: None,
588 tungsten_panel_visible: None,
589 penguin_mode_org_enabled: None,
590 startup_prefetched_at: None,
591 remote_control_at_startup: None,
592 cached_extra_usage_disabled_reason: None,
593 auto_permissions_notification_count: None,
594 speculation_enabled: None,
595 client_data_cache: None,
596 additional_model_options_cache: None,
597 metrics_status_cache: None,
598 migration_version: None,
599 }
600 }
601}
602
603pub fn get_global_config_path() -> PathBuf {
605 let config_dir = std::env::var(ai::CONFIG_DIR)
607 .or_else(|_| std::env::var(ai::CLAUDE_CONFIG_DIR))
608 .unwrap_or_else(|_| {
609 dirs::home_dir()
610 .map(|h| h.join(".ai").to_string_lossy().to_string())
611 .unwrap_or_else(|| "~/.ai".to_string())
612 });
613
614 dirs::home_dir()
615 .map(|h| h.join(".ai.json"))
616 .unwrap_or_else(|| PathBuf::from(".ai.json"))
617}
618
619pub fn get_global_config() -> GlobalConfig {
621 let path = get_global_config_path();
622
623 if !path.exists() {
624 return GlobalConfig::default();
625 }
626
627 match fs::read_to_string(&path) {
628 Ok(content) => match serde_json::from_str::<GlobalConfig>(&content) {
629 Ok(config) => {
630 let mut default_config = GlobalConfig::default();
632 merge_config(&mut default_config, config);
633 default_config
634 }
635 Err(e) => {
636 eprintln!("Failed to parse config: {}", e);
637 GlobalConfig::default()
638 }
639 },
640 Err(e) => {
641 eprintln!("Failed to read config file: {}", e);
642 GlobalConfig::default()
643 }
644 }
645}
646
647fn normalize_path_for_config_key(path: &str) -> String {
651 path.replace('\\', "/")
652}
653
654fn find_git_root(path: &str) -> Option<String> {
656 let mut current = std::path::Path::new(path);
657 loop {
658 if current.join(".git").exists() {
659 return Some(current.to_string_lossy().to_string());
660 }
661 match current.parent() {
662 Some(p) => current = p,
663 None => return None,
664 }
665 }
666}
667
668fn resolve_canonical_git_root(git_root: &str) -> Option<String> {
670 let git_dir = std::path::Path::new(git_root).join(".git");
671 let git_dir_contents = std::fs::read_to_string(&git_dir).ok()?;
672 for line in git_dir_contents.lines() {
673 if line.starts_with("gitdir: ") {
674 let common_dir = &line[8..].trim_end_matches('/');
675 if let Some(worktrees_idx) = common_dir.find("/worktrees/") {
678 return Some(common_dir[..worktrees_idx].to_string());
679 }
680 let normalized = std::path::Path::new(common_dir)
682 .parent()?
683 .to_string_lossy()
684 .to_string();
685 return Some(normalized);
686 }
687 }
688 Some(git_root.to_string())
689}
690
691fn find_canonical_git_root(start_path: &str) -> Option<String> {
695 let root = find_git_root(start_path)?;
696 Some(resolve_canonical_git_root(&root).unwrap_or(root))
697}
698
699fn get_project_path_for_config() -> String {
701 use crate::utils::cwd::get_original_cwd;
702
703 let original_cwd = get_original_cwd();
704 let original_cwd_str = original_cwd.to_string_lossy();
705
706 if let Some(canonical) = find_canonical_git_root(&original_cwd_str) {
708 return normalize_path_for_config_key(&canonical);
709 }
710
711 normalize_path_for_config_key(&original_cwd_str)
713}
714
715static SESSION_TRUST_ACCEPTED: std::sync::atomic::AtomicBool =
717 std::sync::atomic::AtomicBool::new(false);
718
719pub fn check_has_trust_dialog_accepted() -> bool {
722 if SESSION_TRUST_ACCEPTED.load(std::sync::atomic::Ordering::SeqCst) {
724 return true;
725 }
726
727 if std::env::var("AI_CODE_SESSION_TRUST_ACCEPTED").as_deref() == Ok("1") {
731 SESSION_TRUST_ACCEPTED.store(true, std::sync::atomic::Ordering::SeqCst);
732 return true;
733 }
734
735 let config = get_global_config();
736
737 let project_path = get_project_path_for_config();
740 if let Some(projects) = &config.projects {
741 if let Some(project_config) = projects.get(&project_path) {
742 if project_config.has_trust_dialog_accepted {
743 SESSION_TRUST_ACCEPTED.store(true, std::sync::atomic::Ordering::SeqCst);
744 return true;
745 }
746 }
747 }
748
749 let cwd = crate::utils::cwd::get_cwd();
751 let cwd_str = cwd.to_string_lossy();
752 let mut current_path = normalize_path_for_config_key(&cwd_str);
753
754 loop {
755 if let Some(projects) = &config.projects {
756 if let Some(project_config) = projects.get(¤t_path) {
757 if project_config.has_trust_dialog_accepted {
758 SESSION_TRUST_ACCEPTED.store(true, std::sync::atomic::Ordering::SeqCst);
759 return true;
760 }
761 }
762 }
763
764 let parent_path = std::path::Path::new(¤t_path)
766 .parent()
767 .map(|p| normalize_path_for_config_key(&p.to_string_lossy()));
768
769 match parent_path {
770 Some(parent) if parent != current_path => current_path = parent,
771 _ => break,
772 }
773 }
774
775 false
776}
777
778fn merge_config(default: &mut GlobalConfig, loaded: GlobalConfig) {
780 macro_rules! merge_option {
784 ($field:ident) => {
785 if let Some(v) = loaded.$field {
786 default.$field = Some(v);
787 }
788 };
789 }
790
791 macro_rules! merge_hashmap {
792 ($field:ident) => {
793 if let Some(v) = loaded.$field {
794 default.$field = v;
795 }
796 };
797 }
798
799 merge_option!(api_key_helper);
800 merge_option!(projects);
801 default.num_startups = loaded.num_startups;
802 merge_option!(install_method);
803 merge_option!(auto_updates);
804 merge_option!(auto_updates_protected_for_native);
805 merge_option!(doctor_shown_at_session);
806 merge_option!(user_id);
807 default.theme = loaded.theme;
808 merge_option!(has_completed_onboarding);
809 merge_option!(last_onboarding_version);
810 merge_option!(last_release_notes_seen);
811 merge_option!(changelog_last_fetched);
812 merge_option!(cached_changelog);
813 merge_option!(mcp_servers);
814 merge_option!(claude_ai_mcp_ever_connected);
815 default.preferred_notif_channel = loaded.preferred_notif_channel;
816 merge_option!(custom_notify_command);
817 merge_option!(verbose);
818 merge_option!(custom_api_key_responses);
819 merge_option!(primary_api_key);
820 merge_option!(has_acknowledged_cost_threshold);
821 merge_option!(has_seen_undercover_auto_notice);
822 merge_option!(has_seen_ultraplan_terms);
823 merge_option!(has_reset_auto_mode_opt_in_for_default_offer);
824 merge_option!(oauth_account);
825 merge_option!(iterm2_key_binding_installed);
826 merge_option!(editor_mode);
827 merge_option!(bypass_permissions_mode_accepted);
828 merge_option!(has_used_backslash_return);
829 default.auto_compact_enabled = loaded.auto_compact_enabled;
830 default.show_turn_duration = loaded.show_turn_duration;
831 default.env = loaded.env;
832 merge_option!(has_seen_tasks_hint);
833 merge_option!(has_used_stash);
834 merge_option!(has_used_background_task);
835 merge_option!(queued_command_up_hint_count);
836 merge_option!(diff_tool);
837 merge_option!(iterm2_setup_in_progress);
838 merge_option!(iterm2_backup_path);
839 merge_option!(apple_terminal_backup_path);
840 merge_option!(apple_terminal_setup_in_progress);
841 merge_option!(shift_enter_key_binding_installed);
842 merge_option!(option_as_meta_key_installed);
843 merge_option!(auto_connect_ide);
844 merge_option!(auto_install_ide_extension);
845 merge_option!(has_ide_onboarding_been_shown);
846 merge_option!(ide_hint_shown_count);
847 merge_option!(has_ide_auto_connect_dialog_been_shown);
848 default.tips_history = loaded.tips_history;
849 merge_option!(companion);
850 merge_option!(companion_muted);
851 merge_option!(feedback_survey_state);
852 merge_option!(transcript_share_dismissed);
853 default.memory_usage_count = loaded.memory_usage_count;
854 merge_option!(has_shown_s1m_welcome_v2);
855 merge_option!(s1m_access_cache);
856 merge_option!(s1m_non_subscriber_access_cache);
857 merge_option!(passes_eligibility_cache);
858 merge_option!(grove_config_cache);
859 merge_option!(passes_upsell_seen_count);
860 merge_option!(has_visited_passes);
861 merge_option!(passes_last_seen_remaining);
862 merge_option!(overage_credit_grant_cache);
863 merge_option!(overage_credit_upsell_seen_count);
864 merge_option!(has_visited_extra_usage);
865 merge_option!(voice_notice_seen_count);
866 merge_option!(voice_lang_hint_shown_count);
867 merge_option!(voice_lang_hint_last_language);
868 merge_option!(voice_footer_hint_seen_count);
869 merge_option!(opus_1m_merge_notice_seen_count);
870 merge_option!(experiment_notices_seen_count);
871 merge_option!(has_shown_opus_plan_welcome);
872 default.prompt_queue_use_count = loaded.prompt_queue_use_count;
873 default.btw_use_count = loaded.btw_use_count;
874 merge_option!(last_plan_mode_use);
875 merge_option!(subscription_notice_count);
876 merge_option!(has_available_subscription);
877 merge_option!(subscription_upsell_seen_count);
878 merge_option!(recommended_subscription);
879 default.todo_feature_enabled = loaded.todo_feature_enabled;
880 merge_option!(show_expanded_todos);
881 merge_option!(show_spinner_tree);
882 merge_option!(first_start_time);
883 default.message_idle_notif_threshold_ms = loaded.message_idle_notif_threshold_ms;
884 merge_option!(github_action_setup_count);
885 merge_option!(slack_app_install_count);
886 default.file_checkpointing_enabled = loaded.file_checkpointing_enabled;
887 default.terminal_progress_bar_enabled = loaded.terminal_progress_bar_enabled;
888 merge_option!(show_status_in_terminal_tab);
889 merge_option!(task_complete_notif_enabled);
890 merge_option!(input_needed_notif_enabled);
891 merge_option!(agent_push_notif_enabled);
892 merge_option!(claude_code_first_token_date);
893 merge_option!(model_switch_callout_dismissed);
894 merge_option!(model_switch_callout_last_shown);
895 merge_option!(model_switch_callout_version);
896 merge_option!(effort_callout_dismissed);
897 merge_option!(effort_callout_v2_dismissed);
898 merge_option!(remote_dialog_seen);
899 merge_option!(bridge_oauth_dead_expires_at);
900 merge_option!(bridge_oauth_dead_fail_count);
901 merge_option!(desktop_upsell_seen_count);
902 merge_option!(desktop_upsell_dismissed);
903 merge_option!(idle_return_dismissed);
904 merge_option!(opus_pro_migration_complete);
905 merge_option!(opus_pro_migration_timestamp);
906 merge_option!(sonnet_1m_45_migration_complete);
907 merge_option!(legacy_opus_migration_timestamp);
908 merge_option!(sonnet_45_to_46_migration_timestamp);
909 default.cached_statsig_gates = loaded.cached_statsig_gates;
910 merge_option!(cached_dynamic_configs);
911 merge_option!(cached_growth_book_features);
912 merge_option!(growth_book_overrides);
913 merge_option!(last_shown_emergency_tip);
914 default.respect_gitignore = loaded.respect_gitignore;
915 default.copy_full_response = loaded.copy_full_response;
916 merge_option!(copy_on_select);
917 merge_option!(github_repo_paths);
918 merge_option!(deep_link_terminal);
919 merge_option!(iterm2_it2_setup_complete);
920 merge_option!(prefer_tmux_over_iterm2);
921 merge_option!(skill_usage);
922 merge_option!(official_marketplace_auto_install_attempted);
923 merge_option!(official_marketplace_auto_installed);
924 merge_option!(official_marketplace_auto_install_fail_reason);
925 merge_option!(official_marketplace_auto_install_retry_count);
926 merge_option!(official_marketplace_auto_install_last_attempt_time);
927 merge_option!(official_marketplace_auto_install_next_retry_time);
928 merge_option!(has_completed_claude_in_chrome_onboarding);
929 merge_option!(claude_in_chrome_default_enabled);
930 merge_option!(cached_chrome_extension_installed);
931 merge_option!(chrome_extension);
932 merge_option!(lsp_recommendation_disabled);
933 merge_option!(lsp_recommendation_never_plugins);
934 merge_option!(lsp_recommendation_ignored_count);
935 merge_option!(claude_code_hints);
936 merge_option!(permission_explainer_enabled);
937 merge_option!(teammate_mode);
938 merge_option!(teammate_default_model);
939 merge_option!(pr_status_footer_enabled);
940 merge_option!(tungsten_panel_visible);
941 merge_option!(penguin_mode_org_enabled);
942 merge_option!(startup_prefetched_at);
943 merge_option!(remote_control_at_startup);
944 merge_option!(cached_extra_usage_disabled_reason);
945 merge_option!(auto_permissions_notification_count);
946 merge_option!(speculation_enabled);
947 merge_option!(client_data_cache);
948 merge_option!(additional_model_options_cache);
949 merge_option!(metrics_status_cache);
950 merge_option!(migration_version);
951}
952
953fn remove_nulls(json: &str) -> String {
955 let value: serde_json::Value = match serde_json::from_str(json) {
956 Ok(v) => v,
957 Err(_) => return json.to_string(),
958 };
959 let cleaned = remove_defaults_impl(&value);
960 serde_json::to_string_pretty(&cleaned)
961 .map(|s| s.to_string())
962 .unwrap_or_else(|_| json.to_string())
963}
964
965fn remove_defaults_impl(value: &serde_json::Value) -> serde_json::Value {
966 match value {
967 serde_json::Value::Object(map) => {
968 let filtered: serde_json::Map<String, serde_json::Value> = map
969 .iter()
970 .filter(|(_, v)| !is_default_value(v))
971 .map(|(k, v)| (k.clone(), remove_defaults_impl(v)))
972 .collect();
973 serde_json::Value::Object(filtered)
974 }
975 serde_json::Value::Array(arr) => {
976 serde_json::Value::Array(arr.iter().map(remove_defaults_impl).collect())
977 }
978 _ => value.clone(),
979 }
980}
981
982fn is_default_value(value: &serde_json::Value) -> bool {
984 match value {
985 serde_json::Value::Null => true,
987 serde_json::Value::Bool(b) if !b => true,
989 serde_json::Value::Number(n) if n.as_i64() == Some(0) => true,
991 serde_json::Value::String(s) if s.is_empty() => true,
993 serde_json::Value::Array(arr) if arr.is_empty() => true,
995 serde_json::Value::Object(obj) if obj.is_empty() => true,
997 _ => false,
998 }
999}
1000
1001pub fn save_global_config(config: &GlobalConfig) -> Result<(), String> {
1003 let path = get_global_config_path();
1004
1005 if let Some(parent) = path.parent() {
1007 fs::create_dir_all(parent)
1008 .map_err(|e| format!("Failed to create config directory: {}", e))?;
1009 }
1010
1011 let json = serde_json::to_string_pretty(config)
1012 .map_err(|e| format!("Failed to serialize config: {}", e))?;
1013
1014 let json = remove_nulls(&json);
1016
1017 fs::write(&path, json).map_err(|e| format!("Failed to write config file: {}", e))?;
1018
1019 if let Ok(mut cache) = GLOBAL_CONFIG_CACHE.lock() {
1021 cache.config = Some(config.clone());
1022 cache.mtime = std::time::SystemTime::now()
1023 .duration_since(std::time::UNIX_EPOCH)
1024 .map(|d| d.as_millis() as u64)
1025 .unwrap_or(0);
1026 }
1027
1028 Ok(())
1029}
1030
1031pub fn get_current_project_config() -> ProjectConfig {
1033 let global_config = get_global_config();
1034
1035 let project_path = std::env::var(ai::PROJECT_PATH)
1037 .or_else(|_| std::env::var(ai::CLAUDE_PROJECT_PATH))
1038 .unwrap_or_else(|_err| {
1039 std::env::current_dir()
1040 .map(|p| p.to_string_lossy().to_string())
1041 .unwrap_or_default()
1042 });
1043
1044 global_config
1045 .projects
1046 .and_then(|p| p.get(&project_path).cloned())
1047 .unwrap_or_default()
1048}
1049
1050pub fn save_current_project_config(config: ProjectConfig) -> Result<(), String> {
1052 let project_path = std::env::var(ai::PROJECT_PATH)
1053 .or_else(|_| std::env::var(ai::CLAUDE_PROJECT_PATH))
1054 .unwrap_or_else(|_| {
1055 std::env::current_dir()
1056 .map(|p| p.to_string_lossy().to_string())
1057 .unwrap_or_default()
1058 });
1059
1060 let mut global_config = get_global_config();
1061
1062 let projects = global_config.projects.get_or_insert_with(HashMap::new);
1063 projects.insert(project_path, config);
1064
1065 save_global_config(&global_config)
1066}
1067
1068pub fn get_or_create_user_id() -> String {
1070 let mut config = get_global_config();
1071
1072 if let Some(user_id) = &config.user_id {
1073 return user_id.clone();
1074 }
1075
1076 let user_id = uuid::Uuid::new_v4().to_string();
1078 config.user_id = Some(user_id.clone());
1079
1080 let _ = save_global_config(&config);
1081
1082 user_id
1083}
1084
1085#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1087#[serde(tag = "type", content = "envVar")]
1088pub enum AutoUpdaterDisabledReason {
1089 Development,
1090 Env { env_var: String },
1091 Config,
1092}
1093
1094pub fn get_auto_updater_disabled_reason() -> Option<AutoUpdaterDisabledReason> {
1096 if std::env::var(system::NODE_ENV)
1098 .map(|v| v == "development")
1099 .unwrap_or(false)
1100 {
1101 return Some(AutoUpdaterDisabledReason::Development);
1102 }
1103
1104 if std::env::var(ai::DISABLE_AUTOUPDATER)
1106 .or_else(|_| std::env::var(system::DISABLE_AUTOUPDATER))
1107 .map(|v| !v.is_empty() && v != "false")
1108 .unwrap_or(false)
1109 {
1110 return Some(AutoUpdaterDisabledReason::Env {
1111 env_var: "DISABLE_AUTOUPDATER".to_string(),
1112 });
1113 }
1114
1115 let config = get_global_config();
1117 if config.auto_updates == Some(false)
1118 && (config.install_method != Some(InstallMethod::Native)
1119 || config.auto_updates_protected_for_native != Some(true))
1120 {
1121 return Some(AutoUpdaterDisabledReason::Config);
1122 }
1123
1124 None
1125}
1126
1127pub fn is_auto_updater_disabled() -> bool {
1129 get_auto_updater_disabled_reason().is_some()
1130}
1131
1132pub fn get_custom_api_key_status(truncated_api_key: &str) -> &'static str {
1134 let config = get_global_config();
1135
1136 if let Some(responses) = &config.custom_api_key_responses {
1137 if let Some(approved) = &responses.approved {
1138 if approved.contains(&truncated_api_key.to_string()) {
1139 return "approved";
1140 }
1141 }
1142 if let Some(rejected) = &responses.rejected {
1143 if rejected.contains(&truncated_api_key.to_string()) {
1144 return "rejected";
1145 }
1146 }
1147 }
1148
1149 "new"
1150}
1151
1152pub fn record_first_start_time() {
1154 let mut config = get_global_config();
1155
1156 if config.first_start_time.is_none() {
1157 config.first_start_time = Some(chrono::Utc::now().to_rfc3339());
1158 let _ = save_global_config(&config);
1159 }
1160}
1161
1162pub fn complete_onboarding(version: &str) {
1164 let mut config = get_global_config();
1165 config.has_completed_onboarding = Some(true);
1166 config.last_onboarding_version = Some(version.to_string());
1167 let _ = save_global_config(&config);
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172 use super::*;
1173
1174 #[test]
1175 fn test_default_config() {
1176 let config = GlobalConfig::default();
1177 assert_eq!(config.num_startups, 0);
1178 assert_eq!(config.theme, Some(ThemeSetting::Dark));
1179 assert_eq!(config.verbose, Some(false));
1180 assert!(config.auto_compact_enabled);
1181 }
1182
1183 #[test]
1184 fn test_get_global_config_path() {
1185 let path = get_global_config_path();
1186 assert!(path.to_string_lossy().contains(".ai"));
1187 }
1188
1189 #[test]
1190 fn test_is_auto_updater_disabled() {
1191 let _ = is_auto_updater_disabled();
1193 }
1194
1195 #[test]
1196 fn test_get_custom_api_key_status() {
1197 assert_eq!(get_custom_api_key_status("test-key"), "new");
1198 }
1199}