Move preferences from the database to a YAML config file

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Georg Bauer
2026-07-26 09:50:39 +02:00
parent 3f2c42513f
commit 5d441038bc
19 changed files with 571 additions and 964 deletions

View File

@@ -12,7 +12,8 @@ use preferences::PreferenceDraft;
#[cfg(test)]
use preferences::{parse_optional_gib, parse_streaming_cache};
use crate::database::{AppPreferences, Database, ProjectWithSessions, SessionState, StoredMessage};
use crate::config::{Config, EndpointConfig};
use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage};
#[cfg(target_os = "macos")]
use crate::engine::ChatTurn;
use crate::metrics::{KvCacheReport, Metrics, MetricsSnapshot};
@@ -20,9 +21,9 @@ use crate::model::{self, DownloadOutcome, DownloadProgress, ManagedArtifactId, M
#[cfg(target_os = "macos")]
use crate::runtime::{ActiveGeneration, CheckpointTarget, GenerationEvent, GenerationService};
use crate::settings::{
DEFAULT_KV_BUDGET_GIB, DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences,
KvCachePreferences, ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences,
SteeringPreferences, StreamingCacheBudget,
DiagnosticPreferences, ExecutionPreferences, GIB, GenerationPreferences, KvCachePreferences,
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
StreamingCacheBudget,
};
use iced::widget::{markdown, scrollable, text_input};
use iced::{Size, Subscription, Task, keyboard, mouse, window};
@@ -53,7 +54,7 @@ pub(crate) struct App {
pub(super) native_edit_commands: crate::native_edit::EditCommandQueue,
database: Option<Database>,
projects: Vec<ProjectWithSessions>,
preferences: AppPreferences,
config: Config,
preference_draft: PreferenceDraft,
preferences_open: bool,
preference_error: Option<String>,
@@ -99,7 +100,7 @@ pub(crate) struct App {
#[cfg(target_os = "macos")]
active_titling: Option<generation::TitleRequest>,
#[cfg(target_os = "macos")]
runtime_preferences: Arc<RwLock<AppPreferences>>,
runtime_config: Arc<RwLock<Config>>,
#[cfg(target_os = "macos")]
_endpoint: Option<crate::server::ServerHandle>,
error: Option<String>,
@@ -217,22 +218,24 @@ pub(crate) enum Message {
impl App {
pub(crate) fn load(main_window: window::Id) -> Self {
let config = match Config::load(&config_path()) {
Ok(config) => config,
Err(error) => return Self::failed(error, main_window),
};
let path = application_support_path().join("data.sqlite3");
match Database::open(&path) {
Ok(mut database) => match (database.load_projects(), database.load_preferences()) {
(Ok(projects), Ok(preferences)) => {
let preference_draft = match PreferenceDraft::from_saved(&preferences) {
Ok(draft) => draft,
Err(error) => return Self::failed(error, main_window),
};
Ok(mut database) => match database.load_projects() {
Ok(projects) => {
let preference_draft = PreferenceDraft::from_saved(&config);
// Before the counters are taken, so they measure what is
// actually reachable.
sweep_orphan_checkpoints(&kv_cache_path(), &projects);
#[cfg(target_os = "macos")]
crate::engine::sweep_transient_cache(&transient_cache_path());
let context_limit = preferences.context_tokens.max(0) as u32;
let context_limit = config.generation.context_tokens.max(0) as u32;
// Reopen on the project we left, with a fresh draft chat.
let last_project = preferences
let last_project = config
.interface
.last_project_id
.filter(|id| projects.iter().any(|item| item.project.id == *id));
let drafts = last_project
@@ -243,8 +246,8 @@ impl App {
Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
let metrics_snapshot = metrics.snapshot();
#[cfg(target_os = "macos")]
let (runtime_preferences, generation_service, endpoint, service_error) =
spawn_services(&preferences, Arc::clone(&metrics));
let (runtime_config, generation_service, endpoint, service_error) =
spawn_services(&config, Arc::clone(&metrics));
Self {
main_window,
model_manager_window: None,
@@ -255,7 +258,7 @@ impl App {
native_edit_commands: crate::native_edit::command_queue(),
database: Some(database),
projects,
preferences,
config,
preference_draft,
preferences_open: false,
preference_error: None,
@@ -294,7 +297,7 @@ impl App {
#[cfg(target_os = "macos")]
active_titling: None,
#[cfg(target_os = "macos")]
runtime_preferences,
runtime_config,
#[cfg(target_os = "macos")]
_endpoint: endpoint,
error: {
@@ -309,22 +312,27 @@ impl App {
},
}
}
(Err(error), _) | (_, Err(error)) => Self::failed(error, main_window),
Err(error) => Self::failed(
format!("Could not open the project database: {error}"),
main_window,
),
},
Err(error) => Self::failed(error, main_window),
Err(error) => Self::failed(
format!("Could not open the project database: {error}"),
main_window,
),
}
}
fn failed(error: String, main_window: window::Id) -> Self {
let preferences = AppPreferences::default();
let preference_draft = PreferenceDraft::from_saved(&preferences)
.expect("default preferences must use a supported model");
let context_limit = preferences.context_tokens.max(0) as u32;
let config = Config::default();
let preference_draft = PreferenceDraft::from_saved(&config);
let context_limit = config.generation.context_tokens.max(0) as u32;
let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
let metrics_snapshot = metrics.snapshot();
#[cfg(target_os = "macos")]
let (runtime_preferences, generation_service, endpoint, service_error) =
spawn_services(&preferences, Arc::clone(&metrics));
let (runtime_config, generation_service, endpoint, service_error) =
spawn_services(&config, Arc::clone(&metrics));
#[cfg(target_os = "macos")]
let startup_error = service_error;
#[cfg(not(target_os = "macos"))]
@@ -339,7 +347,7 @@ impl App {
native_edit_commands: crate::native_edit::command_queue(),
database: None,
projects: Vec::new(),
preferences,
config,
preference_draft,
preferences_open: false,
preference_error: None,
@@ -378,14 +386,12 @@ impl App {
#[cfg(target_os = "macos")]
active_titling: None,
#[cfg(target_os = "macos")]
runtime_preferences,
runtime_config,
#[cfg(target_os = "macos")]
_endpoint: endpoint,
error: Some(match startup_error {
Some(service_error) => {
format!("Could not open the project database: {error}. {service_error}")
}
None => format!("Could not open the project database: {error}"),
Some(service_error) => format!("{error} {service_error}"),
None => error,
}),
}
}
@@ -453,29 +459,19 @@ impl App {
Message::StartSidebarDrag => self.sidebar_drag = true,
Message::DragSidebar(x) => {
if self.sidebar_drag {
self.preferences.sidebar_width =
self.config.interface.sidebar_width =
(x.round() as i32).clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH);
}
}
Message::EndSidebarDrag => {
if self.sidebar_drag {
self.sidebar_drag = false;
if let Some(database) = self.database.as_mut()
&& let Err(error) =
database.set_sidebar_width(self.preferences.sidebar_width)
{
self.error = Some(error);
}
self.store_config();
}
}
Message::ToggleSidebar => {
let collapsed = !self.preferences.sidebar_collapsed;
self.preferences.sidebar_collapsed = collapsed;
if let Some(database) = self.database.as_mut()
&& let Err(error) = database.set_sidebar_collapsed(collapsed)
{
self.error = Some(error);
}
self.config.interface.sidebar_collapsed = !self.config.interface.sidebar_collapsed;
self.store_config();
}
Message::MetricsTick => self.sample_metrics(),
Message::PreferenceModelChanged(model) => {
@@ -829,9 +825,9 @@ impl App {
let _ = fs::remove_file(session_checkpoint_path(session_id));
}
self.drafts.remove(&project_id);
if self.preferences.last_project_id == Some(project_id) {
self.preferences.last_project_id = None;
let _ = database.set_last_project(None);
if self.config.interface.last_project_id == Some(project_id) {
self.config.interface.last_project_id = None;
let _ = self.config.save(&config_path());
}
if self.selected_project == Some(project_id) {
self.selected_project = None;
@@ -957,7 +953,7 @@ impl App {
self.context_limit = if limit > 0 {
limit as u32
} else {
self.preferences.context_tokens.max(0) as u32
self.config.generation.context_tokens.max(0) as u32
};
self.tokens_per_second = tokens_per_second;
self.error = None;
@@ -1128,6 +1124,13 @@ impl App {
self.finish_cache_change();
}
/// Writes the configuration file, surfacing a failure in the error banner.
fn store_config(&mut self) {
if let Err(error) = self.config.save(&config_path()) {
self.error = Some(error);
}
}
fn finish_cache_change(&mut self) {
self.metrics.rescan_cache(&kv_cache_path());
self.metrics_snapshot = self.metrics.snapshot();
@@ -1135,12 +1138,7 @@ impl App {
}
fn scan_kv_cache(&mut self) {
let budget = self
.preferences
.runtime()
.map_or(DEFAULT_KV_BUDGET_GIB * GIB, |runtime| {
runtime.kv_cache.settings().budget_bytes
});
let budget = self.config.runtime.kv_cache.settings().budget_bytes;
self.kv_cache_report = crate::metrics::kv_cache_report(&kv_cache_path(), budget);
self.last_cache_scan = Instant::now();
}
@@ -1148,34 +1146,34 @@ impl App {
#[cfg(target_os = "macos")]
fn spawn_services(
preferences: &AppPreferences,
config: &Config,
metrics: Arc<Metrics>,
) -> (
Arc<RwLock<AppPreferences>>,
Arc<RwLock<Config>>,
Option<GenerationService>,
Option<crate::server::ServerHandle>,
Option<String>,
) {
let runtime_preferences = Arc::new(RwLock::new(preferences.clone()));
let runtime_config = Arc::new(RwLock::new(config.clone()));
let generation = match GenerationService::spawn(Arc::clone(&metrics)) {
Ok(generation) => generation,
Err(error) => return (runtime_preferences, None, None, Some(error)),
Err(error) => return (runtime_config, None, None, Some(error)),
};
let endpoint = preferences.endpoint_enabled.then(|| {
let endpoint = config.endpoint.enabled.then(|| {
crate::server::ServerHandle::spawn(
generation.clone(),
Arc::clone(&runtime_preferences),
Arc::clone(&runtime_config),
models_path(),
transient_cache_path(),
u16::try_from(preferences.endpoint_port).unwrap_or(4000),
preferences.endpoint_cors,
u16::try_from(config.endpoint.port).unwrap_or(4000),
config.endpoint.cors,
metrics,
)
});
match endpoint {
Some(Ok(endpoint)) => (runtime_preferences, Some(generation), Some(endpoint), None),
Some(Err(error)) => (runtime_preferences, Some(generation), None, Some(error)),
None => (runtime_preferences, Some(generation), None, None),
Some(Ok(endpoint)) => (runtime_config, Some(generation), Some(endpoint), None),
Some(Err(error)) => (runtime_config, Some(generation), None, Some(error)),
None => (runtime_config, Some(generation), None, None),
}
}
@@ -1219,6 +1217,11 @@ fn models_path() -> PathBuf {
application_support_path().join("models")
}
/// The settings file, beside the project database.
fn config_path() -> PathBuf {
application_support_path().join("config.yaml")
}
pub(super) fn chat_scroll_id() -> scrollable::Id {
scrollable::Id::new("chat-transcript")
}