Skip to main content

nexus_core/app/
settings.rs

1// Casts here are on bounded values: token counts, byte sizes, and
2// selection indices — never on unbounded input. JSON-derived indices in
3// provider/tools go through try_from instead.
4#![allow(
5    clippy::cast_possible_truncation,
6    clippy::cast_possible_wrap,
7    clippy::cast_precision_loss,
8    clippy::cast_sign_loss
9)]
10use anyhow::{Result, bail};
11
12use super::{App, OCR_ENGINES, SEARCH_PROVIDERS, VERBOSITY_LEVELS};
13
14impl App {
15    /// Advance the OCR engine auto → tesseract → vlm → local → auto,
16    /// persisted. Cycling into "local" pulls the configured model via ollama
17    /// in the background (formerly the separate `/ocr-local` command) —
18    /// `ocr_local_install` itself flips the engine to "local" and persists it
19    /// once the pull actually succeeds, so a failed pull doesn't leave the
20    /// engine silently pointed at a model that was never fetched.
21    pub fn cycle_ocr_engine(&mut self) -> Result<()> {
22        let i = super::OCR_ENGINES
23            .iter()
24            .position(|&e| e == self.ocr_engine)
25            .unwrap_or(0);
26        let next = super::OCR_ENGINES[(i + 1) % super::OCR_ENGINES.len()];
27        if next == "local" {
28            self.ocr_local_install("");
29            return Ok(());
30        }
31        self.ocr_engine = next.to_string();
32        self.db.set_setting("ocr_engine", &self.ocr_engine)?;
33        Ok(())
34    }
35
36    /// Set one named setting by key, persisting it and applying it live —
37    /// the `SetSetting` command the host (and later the TUI) uses. Unlike
38    /// `load_settings` (which ignores unknown persisted rows), this fails
39    /// fast: an unknown key or an invalid value for a constrained key is an
40    /// error, never a silent no-op reported as success.
41    pub fn set_setting(&mut self, key: &str, value: &str) -> Result<()> {
42        if !SETTING_KEYS.contains(&key) {
43            bail!("unknown setting: {key}");
44        }
45        if !valid_setting_value(key, value) {
46            bail!("invalid value for {key}: {value:?}");
47        }
48        let embedding_changed =
49            key == "embedding_model" && self.embedding_model.trim() != value.trim();
50        self.apply_setting(key, value);
51        if embedding_changed {
52            // Vectors have no model id of their own, so retaining them after a
53            // model change can make semantic search silently skip or mis-rank
54            // every file. Rebuild them with the new model instead. Dropping
55            // the receiver also prevents an old in-flight result from being
56            // written back after the clear.
57            self.embed_rx = None;
58            self.db.clear_chunk_embeddings()?;
59        }
60        if key == "blocked_domains" {
61            // Per-space (not a db setting): lives next to the space's other
62            // config files so it travels with the space.
63            std::fs::write(
64                self.space.blocked_domains_path(&self.active_space.name),
65                value,
66            )?;
67        } else {
68            self.db.set_setting(key, value)?;
69        }
70        self.refresh_toolbox();
71        if key == "embedding_model" {
72            self.start_embedding();
73        }
74        self.push_status(format!("{key} set"));
75        Ok(())
76    }
77}
78
79/// Every key `set_setting` accepts — the same keys `apply_setting` (and the
80/// `blocked_domains` special case) can actually apply.
81const SETTING_KEYS: [&str; 21] = [
82    "show_stats",
83    "show_reasoning",
84    "hide_hints",
85    "usage_range",
86    "temperature",
87    "top_p",
88    "max_tokens",
89    "memory_model",
90    "transcriber_model",
91    "ocr_model",
92    "ocr_engine",
93    "local_ocr_model",
94    "embedding_model",
95    "image_gen_model",
96    "video_gen_model",
97    "compact_threshold",
98    "searxng_url",
99    "verbosity",
100    "langsearch_key",
101    "search_provider",
102    "blocked_domains",
103];
104
105/// Whether `value` is one `apply_setting` will actually apply for `key` —
106/// constrained keys must carry valid values, so a typo'd value can't be
107/// persisted as a no-op while reporting success.
108fn valid_setting_value(key: &str, value: &str) -> bool {
109    match key {
110        "show_stats" | "show_reasoning" | "hide_hints" => matches!(value, "0" | "1"),
111        "temperature" | "top_p" => value.parse::<f32>().is_ok(),
112        "max_tokens" => value.parse::<u32>().is_ok(),
113        "compact_threshold" => value.parse::<u8>().is_ok(),
114        "usage_range" => crate::db::UsageRange::CYCLE
115            .iter()
116            .any(|r| r.key() == value),
117        "ocr_engine" => OCR_ENGINES.contains(&value),
118        "verbosity" => VERBOSITY_LEVELS.contains(&value),
119        "search_provider" => SEARCH_PROVIDERS.contains(&value),
120        _ => true, // free-form strings
121    }
122}