From 2fed5371caae2bcd2e9f7b240c6a5b38ddc12a85 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 1 Aug 2026 10:44:53 +0200 Subject: [PATCH] Refresh runtime settings without opening Preferences. --- README.md | 2 +- crates/bds-ui/src/app.rs | 114 +++++++++++++++++++++++---------------- specs/events.allium | 5 +- 3 files changed, 72 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 598de65..7fcd07d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The project is under active development. Core blogging workflows are broadly ava - Read-only in-app browsers in the Help menu for the bundled global `DOCUMENTATION.md`, the generated Lua API reference with public types and runnable examples, the [CLI/server/TUI documentation](CLI.md), and the [MCP server documentation](MCP.md), with safe GFM rendering and confirmed external links. - A localized Tags workspace manages tags and category settings; its category table includes the main language and every configured translation, whose titles are used by matching category archives and menu entries. - A localized OPML menu editor manages pages, submenus, and category archives with protected Home ordering, keyboard-accessible tree controls, drag-and-drop, and bDS2-compatible persistence. -- Project-scoped typed domain events synchronize desktop views with shared-engine and future CLI mutations; persisted CLI notifications are consumed once, and the selected UI language is shared through settings. +- Project-scoped typed domain events synchronize desktop views and cached runtime settings with shared-engine and CLI mutations even when Preferences is closed; persisted CLI notifications are consumed once, and the selected UI language is shared through settings. - Headless `bds-cli` automation for rebuild/repair/render, publishing and Git sync, post/media/gallery creation, effective shared settings with secret-presence redaction, projects, utility Lua tasks, JSON I/O, airplane-mode AI routing, and guarded launcher installation from Settings → Data or `bds-cli install`. - Local MCP automation over stdio or a localhost-only stateless HTTP endpoint, with project resources, read/search/count tools, uniquely identified inert write proposals, clean duplicate-pending rejection, explicit desktop approval, and opt-in Claude Code/Copilot configuration. - A fully localized Ratatui terminal workspace, available locally through `bds-cli tui`/`BDS_MODE=tui` and remotely through authenticated SSH shell sessions, with shared post/template/script editing and publishing, project/search/command overlays, settings, tags, Git, reports, task progress, live multi-client locale updates, and airplane-mode AI gating. diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index f8876b2..ef7c480 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -2954,24 +2954,7 @@ impl BdsApp { ); return Task::none(); } - self.offline_mode = mode; - let models = self.chat_model_options(); - let selected = self.db.as_ref().and_then(|db| { - ai::load_ai_settings(db.conn(), mode) - .ok() - .map(|settings| settings.active().endpoint.model.clone()) - .filter(|model| !model.trim().is_empty()) - }); - for (id, state) in &mut self.chat_editors { - state.model_options = models.clone(); - if let Some(model) = selected.as_ref() { - state.conversation.model = Some(model.clone()); - if let Some(db) = &self.db { - let _ = engine::chat::set_conversation_model(db.conn(), id, model); - } - } - } - self.sync_menu_state(); + self.apply_offline_mode(mode); Task::none() } Message::SetUiLocale(locale) => { @@ -4179,6 +4162,27 @@ impl BdsApp { self.persist_project_ui_state(); } + fn apply_offline_mode(&mut self, mode: bool) { + self.offline_mode = mode; + let models = self.chat_model_options(); + let selected = self.db.as_ref().and_then(|db| { + ai::load_ai_settings(db.conn(), mode) + .ok() + .map(|settings| settings.active().endpoint.model.clone()) + .filter(|model| !model.trim().is_empty()) + }); + for (id, state) in &mut self.chat_editors { + state.model_options = models.clone(); + if let Some(model) = selected.as_ref() { + state.conversation.model = Some(model.clone()); + if let Some(db) = &self.db { + let _ = engine::chat::set_conversation_model(db.conn(), id, model); + } + } + } + self.sync_menu_state(); + } + fn apply_ui_locale(&mut self, locale: UiLocale) { self.ui_locale = locale; self.locale_dropdown_open = false; @@ -4294,21 +4298,21 @@ impl BdsApp { { self.apply_ui_locale(normalize_language(&language)); } - if self - .tabs - .iter() - .any(|tab| tab.tab_type == TabType::Settings) + if key == engine::settings::AIRPLANE_MODE_KEY + && let Some(db) = &self.db + && let Ok(mode) = engine::settings::airplane_mode(db.conn()) { - let active_section = self - .settings_state - .as_ref() - .and_then(|state| state.active_section.clone()); - let mut state = self.hydrate_settings_state(); - if let Some(section) = active_section { - state.focus_section(section); - } - self.settings_state = Some(state); + self.apply_offline_mode(mode); } + let active_section = self + .settings_state + .as_ref() + .and_then(|state| state.active_section.clone()); + let mut state = self.hydrate_settings_state(); + if let Some(section) = active_section { + state.focus_section(section); + } + self.settings_state = Some(state); false } DomainEvent::EntityChanged { @@ -12474,33 +12478,33 @@ mod tests { } #[test] - fn settings_change_rehydrates_ai_configuration() { + fn settings_change_rehydrates_configuration_without_opening_settings() { let (db, project, tmp) = setup(); + let mut app = make_app(db, project, &tmp); bds_core::engine::settings::set( - db.conn(), + app.db.as_ref().unwrap().conn(), "ai.endpoint.online.url", "http://127.0.0.1:9000/v1", ) .unwrap(); bds_core::engine::settings::set( - db.conn(), + app.db.as_ref().unwrap().conn(), "ai.endpoint.online.model", "mlx-community--gemma-4-12B-8bit", ) .unwrap(); - bds_core::engine::settings::set(db.conn(), "ai.endpoint.online.api_key_configured", "true") - .unwrap(); - let mut app = make_app(db, project, &tmp); - app.tabs.push(Tab { - id: "settings".to_string(), - tab_type: TabType::Settings, - title: "Settings".to_string(), - is_transient: false, - is_dirty: false, - }); - let mut settings_state = SettingsViewState::default(); - settings_state.focus_section(SettingsSection::AI); - app.settings_state = Some(settings_state); + bds_core::engine::settings::set( + app.db.as_ref().unwrap().conn(), + "ai.endpoint.online.api_key_configured", + "true", + ) + .unwrap(); + app.settings_state + .as_mut() + .unwrap() + .focus_section(SettingsSection::AI); + + assert!(app.tabs.iter().all(|tab| tab.tab_type != TabType::Settings)); app.handle_domain_event(DomainEvent::SettingsChanged { project_id: None, @@ -12517,6 +12521,22 @@ mod tests { assert_eq!(state.active_section, Some(SettingsSection::AI)); } + #[test] + fn airplane_setting_change_updates_runtime_mode_without_opening_settings() { + let (db, project, tmp) = setup(); + let mut app = make_app(db, project, &tmp); + assert!(app.offline_mode); + + bds_core::engine::settings::set_airplane_mode(app.db.as_ref().unwrap().conn(), false) + .unwrap(); + app.handle_domain_event(DomainEvent::SettingsChanged { + project_id: None, + key: bds_core::engine::settings::AIRPLANE_MODE_KEY.to_string(), + }); + + assert!(!app.offline_mode); + } + #[test] fn ai_unavailable_chat_errors_are_localized() { let raw = "validation error: AI unavailable - configure online endpoint in Settings"; diff --git a/specs/events.allium b/specs/events.allium index ff87466..dfa5c7c 100644 --- a/specs/events.allium +++ b/specs/events.allium @@ -49,7 +49,10 @@ rule ClientSynchronization { when: ClientSubscribed() -- Every shell subscribes on mount. Each subscriber receives events in -- publish order and refreshes only the affected project; deleted entities - -- close their open tabs. A refresh never writes the mutation again. + -- close their open tabs. Global and active-project setting changes refresh + -- every cached setting consumer even when the Settings surface is closed; + -- separately cached runtime controls such as airplane mode update too. A + -- refresh never writes the mutation again. ensures: ClientRefreshOnEvent() }