Refresh runtime settings without opening Preferences.

This commit is contained in:
2026-08-01 10:44:53 +02:00
parent 81382b45ce
commit 2fed5371ca
3 changed files with 72 additions and 49 deletions

View File

@@ -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.

View File

@@ -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";

View File

@@ -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()
}