1use anyhow::Context;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8#[serde(rename_all = "camelCase")]
9pub struct Settings {
10 #[serde(default)]
11 pub default_provider: Option<String>,
12
13 #[serde(default)]
14 pub default_model: Option<String>,
15
16 #[serde(default)]
17 pub default_thinking_level: Option<String>,
18
19 #[serde(default)]
20 pub tools: Vec<String>,
21
22 #[serde(default)]
23 pub exclude_tools: Vec<String>,
24
25 #[serde(default)]
26 pub theme: Option<String>,
27
28 #[serde(default)]
29 pub verbose: bool,
30
31 #[serde(default, rename = "hideThinkingBlock")]
33 pub hide_thinking: Option<bool>,
34
35 #[serde(default, rename = "collapseToolOutput")]
37 pub collapse_tool_output: Option<bool>,
38}
39
40impl Settings {
41 pub fn load(cwd: &std::path::Path) -> anyhow::Result<Self> {
43 let global_path = Self::global_path()?;
44 Self::load_from(global_path, cwd)
45 }
46
47 pub fn load_from(
49 global_path: std::path::PathBuf,
50 cwd: &std::path::Path,
51 ) -> anyhow::Result<Self> {
52 let global = Self::load_file(&global_path)?;
53 let project = Self::load_file(&cwd.join(".rab").join("settings.json")).unwrap_or_default();
54 Ok(Self::merge(global, project))
55 }
56
57 fn global_path() -> anyhow::Result<PathBuf> {
58 let dir = directories::BaseDirs::new().context("Could not determine home directory")?;
59 Ok(dir
60 .home_dir()
61 .join(".rab")
62 .join("agent")
63 .join("settings.json"))
64 }
65
66 fn load_file(path: &std::path::Path) -> anyhow::Result<Settings> {
67 if !path.exists() {
68 return Ok(Settings::default());
69 }
70 let content = std::fs::read_to_string(path)
71 .with_context(|| format!("Failed to read {}", path.display()))?;
72 serde_json::from_str(&content)
73 .with_context(|| format!("Failed to parse {}", path.display()))
74 }
75
76 fn merge(global: Settings, project: Settings) -> Self {
78 Self {
79 default_provider: project.default_provider.or(global.default_provider),
80 default_model: project.default_model.or(global.default_model),
81 default_thinking_level: project
82 .default_thinking_level
83 .or(global.default_thinking_level),
84 tools: if project.tools.is_empty() {
85 global.tools
86 } else {
87 project.tools
88 },
89 exclude_tools: if project.exclude_tools.is_empty() {
90 global.exclude_tools
91 } else {
92 project.exclude_tools
93 },
94 theme: project.theme.or(global.theme),
95 verbose: project.verbose || global.verbose,
96 hide_thinking: project.hide_thinking.or(global.hide_thinking),
97 collapse_tool_output: project.collapse_tool_output.or(global.collapse_tool_output),
98 }
99 }
100
101 pub fn save(&self) -> anyhow::Result<()> {
103 let path = Self::global_path()?;
104 self.save_to(path)
105 }
106
107 pub fn save_to(&self, path: std::path::PathBuf) -> anyhow::Result<()> {
109 if let Some(parent) = path.parent() {
110 std::fs::create_dir_all(parent)?;
111 }
112 let content = serde_json::to_string_pretty(self)
113 .with_context(|| format!("Failed to serialize settings to {}", path.display()))?;
114 std::fs::write(&path, &content)
115 .with_context(|| format!("Failed to write {}", path.display()))?;
116 Ok(())
117 }
118
119 pub fn model(&self) -> &str {
121 self.default_model.as_deref().unwrap_or("deepseek-v4-flash")
122 }
123}
124
125pub fn save_field(key: &str, value: impl serde::Serialize) -> anyhow::Result<()> {
128 let path = Settings::global_path()?;
129 if let Some(parent) = path.parent() {
130 std::fs::create_dir_all(parent)?;
131 }
132
133 let mut data: serde_json::Value = if path.exists() {
134 let content = std::fs::read_to_string(&path)
135 .with_context(|| format!("Failed to read {}", path.display()))?;
136 serde_json::from_str(&content)
137 .with_context(|| format!("Failed to parse {}", path.display()))?
138 } else {
139 serde_json::Value::Object(serde_json::Map::new())
140 };
141
142 if let serde_json::Value::Object(ref mut map) = data {
143 let json_val = serde_json::to_value(value)
144 .with_context(|| format!("Failed to serialize field {}", key))?;
145 map.insert(key.to_string(), json_val);
146 }
147
148 let content = serde_json::to_string_pretty(&data)
149 .with_context(|| format!("Failed to serialize {}", path.display()))?;
150 std::fs::write(&path, &content)
151 .with_context(|| format!("Failed to write {}", path.display()))?;
152 Ok(())
153}