fix: preserve active style across settings saves (#747)

Co-authored-by: Chris233 <h-chris233@outlook.com>
This commit is contained in:
HaoqianandChris233 authored and GitHub committed 2026-07-11 13:02:38 +08:00
1 parent 4242020ae9
commit f2c23723ee
3 files changed
+204 -11

No files matched your search

@@ -13,6 +13,14 @@ pub fn get_default_style_system_prompts() -> StyleSystemPrompts {
pub(crate) trait SettingsWriter {
fn read_settings(&self) -> UserPreferences;
fn write_settings(&self, prefs: UserPreferences) -> Result<(), String>;
fn write_settings_preserving_current_style_preferences(
&self,
mut prefs: UserPreferences,
) -> Result<(), String> {
let current = self.read_settings();
prefs.preserve_style_preferences_from(&current);
self.write_settings(prefs)
}
fn sync_active_asr_provider(&self, provider: &str) -> Result<(), String>;
fn refresh_dictation_hotkey(&self);
fn refresh_qa_hotkey(&self);
@@ -32,6 +40,15 @@ impl SettingsWriter for Coordinator {
self.prefs().set(prefs).map_err(|e| e.to_string())
}
fn write_settings_preserving_current_style_preferences(
&self,
prefs: UserPreferences,
) -> Result<(), String> {
self.prefs()
.set_preserving_current_style_preferences(prefs)
.map_err(|e| e.to_string())
}
fn sync_active_asr_provider(&self, provider: &str) -> Result<(), String> {
self.sync_active_asr_provider_to_vault(provider)
}
@@ -74,6 +91,13 @@ impl<T: SettingsWriter + ?Sized> SettingsWriter for Arc<T> {
(**self).write_settings(prefs)
}
fn write_settings_preserving_current_style_preferences(
&self,
prefs: UserPreferences,
) -> Result<(), String> {
(**self).write_settings_preserving_current_style_preferences(prefs)
}
fn sync_active_asr_provider(&self, provider: &str) -> Result<(), String> {
(**self).sync_active_asr_provider(provider)
}
@@ -162,7 +186,7 @@ pub(crate) fn persist_settings_with_keyboard_apply<T: SettingsWriter>(
}
}
if let Err(error) = coord.write_settings(prefs.clone()) {
if let Err(error) = coord.write_settings_preserving_current_style_preferences(prefs.clone()) {
if active_asr_provider_changed {
match coord.sync_active_asr_provider(&previous.active_asr_provider) {
Ok(()) => {
@@ -180,11 +204,13 @@ pub(crate) fn persist_settings_with_keyboard_apply<T: SettingsWriter>(
}
Err(rollback_error) => {
// ASR vault 无法回滚时 roll-forward prefs;键盘列表保持新状态,避免三者分叉。
coord.write_settings(prefs).map_err(|roll_forward_error| {
format!(
"{error}; additionally failed to restore active ASR provider: {rollback_error}; additionally failed to preserve active ASR provider consistency: {roll_forward_error}"
)
})?;
coord
.write_settings_preserving_current_style_preferences(prefs)
.map_err(|roll_forward_error| {
format!(
"{error}; additionally failed to restore active ASR provider: {rollback_error}; additionally failed to preserve active ASR provider consistency: {roll_forward_error}"
)
})?;
}
}
} else if windows_keyboard_list_changed {
@@ -241,7 +267,8 @@ pub fn set_settings(
// 广播给所有 webview。issue #205:QaPanel 跑在独立 webview,
// 没有 HotkeySettingsContext,必须靠事件感知录音键变化,否则面板可见时
// 用户改键会让浮窗里的 "{recordHotkey}" 文案一直停留在旧值。
persist_settings(&*coord, prefs.clone())?;
persist_settings(&*coord, prefs)?;
let prefs = coord.prefs().get();
#[cfg(target_os = "android")]
coord.apply_android_overlay_settings_change(&remote_prev, &prefs);
// refresh_tray_microphone_menu 内部会调用 NSStatusItem.set_menu,必须在主线程上跑。
@@ -284,7 +311,8 @@ pub fn set_settings(
let packs = coord.style_packs().list().map_err(|e| e.to_string())?;
sync_style_pack_preferences(&mut prefs, &packs);
prefs.android_overlay_trigger = prefs.android_overlay_trigger.normalized();
persist_settings(&*coord, prefs.clone())?;
persist_settings(&*coord, prefs)?;
let prefs = coord.prefs().get();
#[cfg(target_os = "android")]
coord.apply_android_overlay_settings_change(&previous, &prefs);
let _ = app.emit("prefs:changed", &prefs);
@@ -292,6 +320,109 @@ pub fn set_settings(
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Default)]
struct RaceSettingsWriter {
reads: Mutex<Vec<UserPreferences>>,
saved: Mutex<Option<UserPreferences>>,
}
impl SettingsWriter for RaceSettingsWriter {
fn read_settings(&self) -> UserPreferences {
let mut reads = self.reads.lock().unwrap();
if reads.is_empty() {
return self.saved.lock().unwrap().clone().unwrap_or_default();
}
reads.remove(0)
}
fn write_settings(&self, prefs: UserPreferences) -> Result<(), String> {
*self.saved.lock().unwrap() = Some(prefs);
Ok(())
}
fn sync_active_asr_provider(&self, _provider: &str) -> Result<(), String> {
Ok(())
}
fn refresh_dictation_hotkey(&self) {}
fn refresh_qa_hotkey(&self) {}
fn refresh_combo_hotkey(&self) {}
fn refresh_translation_hotkey(&self) {}
fn refresh_switch_style_hotkey(&self) {}
fn refresh_open_app_hotkey(&self) {}
fn refresh_coding_agent_hotkey(&self) {}
}
#[test]
fn settings_save_preserves_current_style_preferences_before_write() {
let packs = crate::types::builtin_style_packs();
let current = UserPreferences {
default_mode: PolishMode::Light,
active_style_pack_id: builtin_style_pack_id(PolishMode::Light).to_string(),
..UserPreferences::default()
};
let mut stale_settings_payload = UserPreferences {
default_mode: PolishMode::Formal,
active_style_pack_id: builtin_style_pack_id(PolishMode::Formal).to_string(),
..UserPreferences::default()
};
stale_settings_payload.preserve_style_preferences_from(&current);
sync_style_pack_preferences(&mut stale_settings_payload, &packs);
assert_eq!(
stale_settings_payload.active_style_pack_id,
builtin_style_pack_id(PolishMode::Light)
);
assert_eq!(stale_settings_payload.default_mode, PolishMode::Light);
}
#[test]
fn persist_settings_keeps_style_change_that_lands_before_write() {
let active_before_request = UserPreferences {
default_mode: PolishMode::Formal,
active_style_pack_id: builtin_style_pack_id(PolishMode::Formal).to_string(),
..UserPreferences::default()
};
let active_before_write = UserPreferences {
default_mode: PolishMode::Light,
active_style_pack_id: builtin_style_pack_id(PolishMode::Light).to_string(),
..UserPreferences::default()
};
let stale_payload = UserPreferences {
default_mode: PolishMode::Formal,
active_style_pack_id: builtin_style_pack_id(PolishMode::Formal).to_string(),
microphone_device_name: "External Mic".to_string(),
..UserPreferences::default()
};
let writer = RaceSettingsWriter {
reads: Mutex::new(vec![active_before_request, active_before_write]),
saved: Mutex::new(None),
};
persist_settings(&writer, stale_payload).unwrap();
let saved = writer.saved.lock().unwrap().clone().expect("prefs saved");
assert_eq!(
saved.active_style_pack_id,
builtin_style_pack_id(PolishMode::Light)
);
assert_eq!(saved.default_mode, PolishMode::Light);
assert_eq!(saved.microphone_device_name, "External Mic");
}
}
// ─────────────────────────── release channel (Beta opt-in) ───────────────────────────
//
// 渠道偏好的写入路径跟 set_settings 复用 persist_settings:保持热键兜底归一化
@@ -321,8 +452,8 @@ pub fn set_update_channel(
return Ok(());
}
prefs.update_channel = channel;
persist_settings(&*coord, prefs.clone())?;
let _ = app.emit("prefs:changed", &prefs);
persist_settings(&*coord, prefs)?;
let _ = app.emit("prefs:changed", &coord.prefs().get());
Ok(())
}
@@ -97,11 +97,25 @@ impl PreferencesStore {
*guard = prefs;
Ok(())
}
pub fn set_preserving_current_style_preferences(
&self,
mut prefs: UserPreferences,
) -> Result<()> {
let mut guard = self.state.lock();
prefs.preserve_style_preferences_from(&guard);
let json = serde_json::to_vec_pretty(&prefs).context("encode prefs failed")?;
atomic_write(&self.path, &json)?;
*guard = prefs;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::read_preferences;
use super::{read_preferences, PreferencesStore};
use crate::types::{builtin_style_pack_id, PolishMode, UserPreferences};
use parking_lot::Mutex;
use std::fs;
use std::path::PathBuf;
@@ -142,4 +156,42 @@ mod tests {
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn set_preserving_current_style_preferences_keeps_store_style_fields() {
let tmp: PathBuf =
std::env::temp_dir().join(format!("openless-prefs-test-{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&tmp).expect("create temp dir");
let path = tmp.join("preferences.json");
let current = UserPreferences {
default_mode: PolishMode::Light,
active_style_pack_id: "local.light-cleanup".to_string(),
..UserPreferences::default()
};
let store = PreferencesStore {
path,
state: Mutex::new(current),
};
let incoming = UserPreferences {
default_mode: PolishMode::Formal,
active_style_pack_id: builtin_style_pack_id(PolishMode::Formal).to_string(),
microphone_device_name: "External Mic".to_string(),
..UserPreferences::default()
};
store
.set_preserving_current_style_preferences(incoming)
.expect("save prefs");
let saved = store.get();
assert_eq!(saved.default_mode, PolishMode::Light);
assert_eq!(saved.active_style_pack_id, "local.light-cleanup");
assert_eq!(saved.microphone_device_name, "External Mic");
let saved_on_disk = read_preferences(&store.path).expect("read saved prefs");
assert_eq!(saved_on_disk.default_mode, PolishMode::Light);
assert_eq!(saved_on_disk.active_style_pack_id, "local.light-cleanup");
assert_eq!(saved_on_disk.microphone_device_name, "External Mic");
let _ = fs::remove_dir_all(&tmp);
}
}
+10
View File
@@ -879,6 +879,16 @@ pub struct UserPreferences {
pub android_overlay_size_dp: u32,
}
impl UserPreferences {
pub(crate) fn preserve_style_preferences_from(&mut self, current: &Self) {
self.default_mode = current.default_mode;
self.enabled_modes = current.enabled_modes.clone();
self.active_style_pack_id = current.active_style_pack_id.clone();
self.style_system_prompts = current.style_system_prompts.clone();
self.custom_style_prompts = current.custom_style_prompts.clone();
}
}
fn default_local_asr_model() -> String {
"qwen3-asr-0.6b".into()
}