Refresh runtime settings without opening Preferences.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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,21 +4298,21 @@ 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)
|
|
||||||
{
|
{
|
||||||
let active_section = self
|
self.apply_offline_mode(mode);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
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
|
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(
|
||||||
.unwrap();
|
app.db.as_ref().unwrap().conn(),
|
||||||
let mut app = make_app(db, project, &tmp);
|
"ai.endpoint.online.api_key_configured",
|
||||||
app.tabs.push(Tab {
|
"true",
|
||||||
id: "settings".to_string(),
|
)
|
||||||
tab_type: TabType::Settings,
|
.unwrap();
|
||||||
title: "Settings".to_string(),
|
app.settings_state
|
||||||
is_transient: false,
|
.as_mut()
|
||||||
is_dirty: false,
|
.unwrap()
|
||||||
});
|
.focus_section(SettingsSection::AI);
|
||||||
let mut settings_state = SettingsViewState::default();
|
|
||||||
settings_state.focus_section(SettingsSection::AI);
|
assert!(app.tabs.iter().all(|tab| tab.tab_type != TabType::Settings));
|
||||||
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";
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user