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. - 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 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. - 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`. - 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. - 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. - 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(); return Task::none();
} }
self.offline_mode = mode; self.apply_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();
Task::none() Task::none()
} }
Message::SetUiLocale(locale) => { Message::SetUiLocale(locale) => {
@@ -4179,6 +4162,27 @@ impl BdsApp {
self.persist_project_ui_state(); 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) { fn apply_ui_locale(&mut self, locale: UiLocale) {
self.ui_locale = locale; self.ui_locale = locale;
self.locale_dropdown_open = false; self.locale_dropdown_open = false;
@@ -4294,11 +4298,12 @@ impl BdsApp {
{ {
self.apply_ui_locale(normalize_language(&language)); self.apply_ui_locale(normalize_language(&language));
} }
if self if key == engine::settings::AIRPLANE_MODE_KEY
.tabs && let Some(db) = &self.db
.iter() && let Ok(mode) = engine::settings::airplane_mode(db.conn())
.any(|tab| tab.tab_type == TabType::Settings)
{ {
self.apply_offline_mode(mode);
}
let active_section = self let active_section = self
.settings_state .settings_state
.as_ref() .as_ref()
@@ -4308,7 +4313,6 @@ impl BdsApp {
state.focus_section(section); state.focus_section(section);
} }
self.settings_state = Some(state); self.settings_state = Some(state);
}
false false
} }
DomainEvent::EntityChanged { DomainEvent::EntityChanged {
@@ -12474,33 +12478,33 @@ mod tests {
} }
#[test] #[test]
fn settings_change_rehydrates_ai_configuration() { fn settings_change_rehydrates_configuration_without_opening_settings() {
let (db, project, tmp) = setup(); let (db, project, tmp) = setup();
let mut app = make_app(db, project, &tmp);
bds_core::engine::settings::set( bds_core::engine::settings::set(
db.conn(), app.db.as_ref().unwrap().conn(),
"ai.endpoint.online.url", "ai.endpoint.online.url",
"http://127.0.0.1:9000/v1", "http://127.0.0.1:9000/v1",
) )
.unwrap(); .unwrap();
bds_core::engine::settings::set( bds_core::engine::settings::set(
db.conn(), app.db.as_ref().unwrap().conn(),
"ai.endpoint.online.model", "ai.endpoint.online.model",
"mlx-community--gemma-4-12B-8bit", "mlx-community--gemma-4-12B-8bit",
) )
.unwrap(); .unwrap();
bds_core::engine::settings::set(db.conn(), "ai.endpoint.online.api_key_configured", "true") bds_core::engine::settings::set(
app.db.as_ref().unwrap().conn(),
"ai.endpoint.online.api_key_configured",
"true",
)
.unwrap(); .unwrap();
let mut app = make_app(db, project, &tmp); app.settings_state
app.tabs.push(Tab { .as_mut()
id: "settings".to_string(), .unwrap()
tab_type: TabType::Settings, .focus_section(SettingsSection::AI);
title: "Settings".to_string(),
is_transient: false, assert!(app.tabs.iter().all(|tab| tab.tab_type != TabType::Settings));
is_dirty: false,
});
let mut settings_state = SettingsViewState::default();
settings_state.focus_section(SettingsSection::AI);
app.settings_state = Some(settings_state);
app.handle_domain_event(DomainEvent::SettingsChanged { app.handle_domain_event(DomainEvent::SettingsChanged {
project_id: None, project_id: None,
@@ -12517,6 +12521,22 @@ mod tests {
assert_eq!(state.active_section, Some(SettingsSection::AI)); 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] #[test]
fn ai_unavailable_chat_errors_are_localized() { fn ai_unavailable_chat_errors_are_localized() {
let raw = "validation error: AI unavailable - configure online endpoint in Settings"; let raw = "validation error: AI unavailable - configure online endpoint in Settings";

View File

@@ -49,7 +49,10 @@ rule ClientSynchronization {
when: ClientSubscribed() when: ClientSubscribed()
-- Every shell subscribes on mount. Each subscriber receives events in -- Every shell subscribes on mount. Each subscriber receives events in
-- publish order and refreshes only the affected project; deleted entities -- 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() ensures: ClientRefreshOnEvent()
} }