use std::path::{Path, PathBuf};
use oino_ai::{ThinkingLevel, Transport};
use serde_json::Value;
use crate::error::{SettingsError, SettingsErrorKind, SettingsScope};
use crate::json::{JsonObject, get_path, migrate_settings, set_path, strip_bom};
use crate::paths::{SETTINGS_FILE, home_dir, lexical_normalize, project_dir};
use crate::storage::Storage;
use crate::types::{
CacheWarmingMode, PackageSource, ProjectTrustDefault, QueueMode, ResourceKind, UiDensity,
};
use crate::view::Settings;
#[derive(Debug)]
pub struct SettingsManager {
storage: Storage,
view: Settings,
project_trusted: bool,
global_load_failed: bool,
project_load_failed: bool,
errors: Vec<SettingsError>,
}
impl SettingsManager {
#[must_use]
pub fn create(cwd: &Path, agent_dir: &Path, project_trusted: bool) -> Self {
let absolute = |path: &Path| {
std::path::absolute(path)
.map_or_else(|_| path.to_path_buf(), |path| lexical_normalize(&path))
};
let storage = Storage::Files {
global: absolute(agent_dir).join(SETTINGS_FILE),
project: project_dir(&absolute(cwd)).join(SETTINGS_FILE),
};
Self::from_storage(storage, project_trusted)
}
#[must_use]
pub fn in_memory(initial: JsonObject) -> Self {
let mut initial = initial;
migrate_settings(&mut initial);
let content = serde_json::to_string_pretty(&Value::Object(initial)).unwrap_or_default();
Self::from_storage(
Storage::Memory {
global: Some(content),
project: None,
},
true,
)
}
fn from_storage(storage: Storage, project_trusted: bool) -> Self {
let mut errors = Vec::new();
let (global, global_load_failed) = match load_layer(&storage, SettingsScope::Global) {
Ok(global) => (global, false),
Err(kind) => {
errors.push(error(&storage, SettingsScope::Global, kind));
(JsonObject::new(), true)
}
};
let (project, project_load_failed) = if project_trusted {
match load_layer(&storage, SettingsScope::Project) {
Ok(project) => (project, false),
Err(kind) => {
errors.push(error(&storage, SettingsScope::Project, kind));
(JsonObject::new(), true)
}
}
} else {
(JsonObject::new(), false)
};
Self {
storage,
view: Settings::from_layers(global, project, JsonObject::new(), home_dir()),
project_trusted,
global_load_failed,
project_load_failed,
errors,
}
}
#[must_use]
pub fn settings(&self) -> &Settings {
&self.view
}
#[must_use]
pub fn snapshot(&self) -> Settings {
self.view.clone()
}
#[must_use]
pub fn path(&self, scope: SettingsScope) -> Option<&Path> {
self.storage.path(scope)
}
#[must_use]
pub fn is_project_trusted(&self) -> bool {
self.project_trusted
}
pub fn set_project_trusted(&mut self, trusted: bool) {
if self.project_trusted == trusted {
return;
}
self.project_trusted = trusted;
let project = if trusted {
match load_layer(&self.storage, SettingsScope::Project) {
Ok(project) => {
self.project_load_failed = false;
project
}
Err(kind) => {
self.project_load_failed = true;
self.record(SettingsScope::Project, kind);
JsonObject::new()
}
}
} else {
self.project_load_failed = false;
JsonObject::new()
};
self.rebuild(None, Some(project));
}
pub fn reload(&mut self) {
let global = match load_layer(&self.storage, SettingsScope::Global) {
Ok(global) => {
self.global_load_failed = false;
Some(global)
}
Err(kind) => {
self.global_load_failed = true;
self.record(SettingsScope::Global, kind);
None
}
};
let project = if self.project_trusted {
match load_layer(&self.storage, SettingsScope::Project) {
Ok(project) => {
self.project_load_failed = false;
Some(project)
}
Err(kind) => {
self.project_load_failed = true;
self.record(SettingsScope::Project, kind);
None
}
}
} else {
Some(JsonObject::new())
};
self.rebuild(global, project);
}
pub fn apply_overrides(&mut self, overrides: &JsonObject) {
let merged = crate::json::deep_merge_settings(self.view.overrides(), overrides);
self.view = Settings::from_layers(
self.view.global().clone(),
self.view.project().clone(),
merged,
self.view.home().map(Path::to_path_buf),
);
}
pub fn drain_errors(&mut self) -> Vec<SettingsError> {
std::mem::take(&mut self.errors)
}
pub fn set(
&mut self,
scope: SettingsScope,
path: &[&str],
value: Option<Value>,
) -> Result<(), SettingsError> {
self.set_many(scope, &[(path, value)])
}
pub fn set_many(
&mut self,
scope: SettingsScope,
edits: &[(&[&str], Option<Value>)],
) -> Result<(), SettingsError> {
if scope == SettingsScope::Project && !self.project_trusted {
return Err(self.error(scope, SettingsErrorKind::ProjectUntrusted));
}
let mut layer = self.view.layer(scope).clone();
for (path, value) in edits {
set_path(&mut layer, path, value.clone());
}
match scope {
SettingsScope::Global => self.rebuild(Some(layer.clone()), None),
SettingsScope::Project => self.rebuild(None, Some(layer.clone())),
}
let load_failed = match scope {
SettingsScope::Global => self.global_load_failed,
SettingsScope::Project => self.project_load_failed,
};
if load_failed {
return Ok(());
}
self.storage
.update(scope, |current| {
let mut file = match current {
Some(content) if !content.is_empty() => parse_layer(content)?,
_ => JsonObject::new(),
};
for (path, _) in edits {
set_path(&mut file, path, get_path(&layer, path).cloned());
}
serde_json::to_string_pretty(&Value::Object(file))
.map_err(|e| SettingsErrorKind::Write(e.to_string()))
})
.map_err(|kind| self.error(scope, kind))
}
pub fn set_default_provider(&mut self, provider: &str) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["defaultProvider"],
Some(provider.into()),
)
}
pub fn set_default_model(&mut self, model_id: &str) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["defaultModel"],
Some(model_id.into()),
)
}
pub fn set_default_model_and_provider(
&mut self,
provider: &str,
model_id: &str,
) -> Result<(), SettingsError> {
self.set_many(
SettingsScope::Global,
&[
(&["defaultProvider"], Some(provider.into())),
(&["defaultModel"], Some(model_id.into())),
],
)
}
pub fn set_default_thinking_level(
&mut self,
level: ThinkingLevel,
) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["defaultThinkingLevel"],
Some(to_value(level)),
)
}
pub fn set_model_thinking_level(
&mut self,
provider: &str,
model_id: &str,
level: ThinkingLevel,
) -> Result<(), SettingsError> {
let mut levels = self.global_object("modelThinkingLevels");
levels.insert(format!("{provider}/{model_id}"), to_value(level));
self.set(
SettingsScope::Global,
&["modelThinkingLevels"],
Some(Value::Object(levels)),
)
}
pub fn remove_model_thinking_level(
&mut self,
provider: &str,
model_id: &str,
) -> Result<(), SettingsError> {
if !self.view.global().contains_key("modelThinkingLevels") {
return Ok(());
}
let mut levels = self.global_object("modelThinkingLevels");
levels.shift_remove(&format!("{provider}/{model_id}"));
let value = (!levels.is_empty()).then_some(Value::Object(levels));
self.set(SettingsScope::Global, &["modelThinkingLevels"], value)
}
pub fn set_steering_mode(&mut self, mode: QueueMode) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["steeringMode"],
Some(to_value(mode)),
)
}
pub fn set_follow_up_mode(&mut self, mode: QueueMode) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["followUpMode"],
Some(to_value(mode)),
)
}
pub fn set_transport(&mut self, transport: Transport) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["transport"],
Some(to_value(transport)),
)
}
pub fn set_compaction_enabled(&mut self, enabled: bool) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["compaction", "enabled"],
Some(enabled.into()),
)
}
pub fn set_retry_enabled(&mut self, enabled: bool) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["retry", "enabled"],
Some(enabled.into()),
)
}
pub fn set_http_idle_timeout_ms(&mut self, timeout_ms: u64) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["httpIdleTimeoutMs"],
Some(timeout_ms.into()),
)
}
pub fn set_cache_warming_mode(&mut self, mode: CacheWarmingMode) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["cacheWarming"],
Some(to_value(mode)),
)
}
pub fn set_show_cache_miss_notices(&mut self, show: bool) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["showCacheMissNotices"],
Some(show.into()),
)
}
pub fn set_shell_path(&mut self, path: Option<&str>) -> Result<(), SettingsError> {
self.set(SettingsScope::Global, &["shellPath"], path.map(Value::from))
}
pub fn set_shell_command_prefix(&mut self, prefix: Option<&str>) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["shellCommandPrefix"],
prefix.map(Value::from),
)
}
pub fn set_default_project_trust(
&mut self,
trust: ProjectTrustDefault,
) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["defaultProjectTrust"],
Some(to_value(trust)),
)
}
pub fn set_enable_skill_commands(&mut self, enabled: bool) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["enableSkillCommands"],
Some(enabled.into()),
)
}
pub fn set_enabled_models(&mut self, patterns: Option<&[String]>) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["enabledModels"],
patterns.map(Into::into),
)
}
pub fn set_image_auto_resize(&mut self, enabled: bool) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["images", "autoResize"],
Some(enabled.into()),
)
}
pub fn set_block_images(&mut self, blocked: bool) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["images", "blockImages"],
Some(blocked.into()),
)
}
pub fn set_packages(
&mut self,
scope: SettingsScope,
packages: &[PackageSource],
) -> Result<(), SettingsError> {
let value = serde_json::to_value(packages)
.map_err(|e| self.error(scope, SettingsErrorKind::Write(e.to_string())))?;
self.set(scope, &["packages"], Some(value))
}
pub fn set_resource_paths(
&mut self,
scope: SettingsScope,
kind: ResourceKind,
paths: &[String],
) -> Result<(), SettingsError> {
self.set(scope, &[kind.key()], Some(paths.into()))
}
pub fn get_or_create_device_id(&mut self) -> Result<String, SettingsError> {
if let Some(id) = self.view.device_id() {
return Ok(id.to_owned());
}
let id = uuid::Uuid::new_v4().to_string();
self.set(
SettingsScope::Global,
&["deviceId"],
Some(id.clone().into()),
)?;
Ok(id)
}
pub fn set_extension_setting(
&mut self,
scope: SettingsScope,
namespace: &str,
key: &str,
value: Option<Value>,
) -> Result<(), SettingsError> {
self.set(scope, &["extensionSettings", namespace, key], value)
}
pub fn set_extension_settings(
&mut self,
scope: SettingsScope,
namespace: &str,
value: Option<Value>,
) -> Result<(), SettingsError> {
self.set(scope, &["extensionSettings", namespace], value)
}
pub fn set_disabled_extensions(&mut self, ids: &[String]) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["disabledExtensions"],
Some(ids.into()),
)
}
pub fn set_ui_density(&mut self, density: UiDensity) -> Result<(), SettingsError> {
self.set(
SettingsScope::Global,
&["ui", "density"],
Some(to_value(density)),
)
}
pub fn set_frontend(&mut self, key: &str, value: Option<Value>) -> Result<(), SettingsError> {
self.set(SettingsScope::Global, &[key], value)
}
fn global_object(&self, key: &str) -> JsonObject {
self.view
.global()
.get(key)
.and_then(Value::as_object)
.cloned()
.unwrap_or_default()
}
fn rebuild(&mut self, global: Option<JsonObject>, project: Option<JsonObject>) {
let global = global.unwrap_or_else(|| self.view.global().clone());
let project = project.unwrap_or_else(|| self.view.project().clone());
self.view = Settings::from_layers(
global,
project,
self.view.overrides().clone(),
self.view.home().map(Path::to_path_buf),
);
}
fn error(&self, scope: SettingsScope, kind: SettingsErrorKind) -> SettingsError {
error(&self.storage, scope, kind)
}
fn record(&mut self, scope: SettingsScope, kind: SettingsErrorKind) {
let error = self.error(scope, kind);
self.errors.push(error);
}
}
fn error(storage: &Storage, scope: SettingsScope, kind: SettingsErrorKind) -> SettingsError {
SettingsError {
scope,
path: storage.path(scope).map(PathBuf::from),
kind,
}
}
fn load_layer(storage: &Storage, scope: SettingsScope) -> Result<JsonObject, SettingsErrorKind> {
match storage.read(scope)? {
Some(content) if !content.is_empty() => parse_layer(&content),
_ => Ok(JsonObject::new()),
}
}
fn parse_layer(content: &str) -> Result<JsonObject, SettingsErrorKind> {
match serde_json::from_str(strip_bom(content)) {
Ok(Value::Object(mut object)) => {
migrate_settings(&mut object);
Ok(object)
}
Ok(_) => Err(SettingsErrorKind::NotAnObject),
Err(error) => Err(SettingsErrorKind::Parse(error.to_string())),
}
}
fn to_value<T: serde::Serialize>(value: T) -> Value {
serde_json::to_value(value).unwrap_or(Value::Null)
}