feat(cli): add ansi_basic theme fallback and offline provider shim for limited‑color terminals

- Detect terminal color support and automatically switch to the new `ansi_basic` theme when only 16‑color support is available.
- Introduce `OfflineProvider` that supplies a placeholder model and friendly messages when no providers are reachable, keeping the TUI usable.
- Add `CONFIG_SCHEMA_VERSION` (`1.1.0`) with schema migration logic and default handling in `Config`.
- Update configuration saving to persist the schema version and ensure defaults.
- Register the `ansi_basic` theme in `theme.rs`.
- Extend `ChatApp` with `set_status_message` to display custom status lines.
- Update documentation (architecture, Vim mode state machine) to reflect new behavior.
- Add async‑trait and futures dependencies required for the offline provider implementation.
This commit is contained in:
2025-10-12 02:19:43 +02:00
parent 952e4819fe
commit 56de1170ee
8 changed files with 249 additions and 32 deletions

View File

@@ -10,9 +10,15 @@ use std::time::Duration;
/// Default location for the OWLEN configuration file
pub const DEFAULT_CONFIG_PATH: &str = "~/.config/owlen/config.toml";
/// Current schema version written to `config.toml`.
pub const CONFIG_SCHEMA_VERSION: &str = "1.1.0";
/// Core configuration shared by all OWLEN clients
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Schema version for on-disk configuration files
#[serde(default = "Config::default_schema_version")]
pub schema_version: String,
/// General application settings
pub general: GeneralSettings,
/// MCP (Multi-Client-Provider) settings
@@ -57,6 +63,7 @@ impl Default for Config {
);
Self {
schema_version: Self::default_schema_version(),
general: GeneralSettings::default(),
mcp: McpSettings::default(),
providers,
@@ -97,6 +104,10 @@ impl McpServerConfig {
}
impl Config {
fn default_schema_version() -> String {
CONFIG_SCHEMA_VERSION.to_string()
}
/// Load configuration from disk, falling back to defaults when missing
pub fn load(path: Option<&Path>) -> Result<Self> {
let path = match path {
@@ -106,10 +117,27 @@ impl Config {
if path.exists() {
let content = fs::read_to_string(&path)?;
let mut config: Config =
let parsed: toml::Value =
toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))?;
let previous_version = parsed
.get("schema_version")
.and_then(|value| value.as_str())
.unwrap_or("0.0.0")
.to_string();
if let Some(agent_table) = parsed.get("agent").and_then(|value| value.as_table()) {
if agent_table.contains_key("max_tool_calls") {
log::warn!(
"Configuration option agent.max_tool_calls is deprecated and ignored. \
The agent now uses agent.max_iterations."
);
}
}
let mut config: Config = parsed
.try_into()
.map_err(|e: toml::de::Error| crate::Error::Config(e.to_string()))?;
config.ensure_defaults();
config.mcp.apply_backward_compat();
config.apply_schema_migrations(&previous_version);
config.validate()?;
Ok(config)
} else {
@@ -130,8 +158,10 @@ impl Config {
fs::create_dir_all(dir)?;
}
let mut snapshot = self.clone();
snapshot.schema_version = Config::default_schema_version();
let content =
toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
toml::to_string_pretty(&snapshot).map_err(|e| crate::Error::Config(e.to_string()))?;
fs::write(path, content)?;
Ok(())
}
@@ -171,6 +201,9 @@ impl Config {
ensure_provider_config(self, "ollama");
ensure_provider_config(self, "ollama-cloud");
if self.schema_version.is_empty() {
self.schema_version = Self::default_schema_version();
}
}
/// Validate configuration invariants and surface actionable error messages.
@@ -181,6 +214,17 @@ impl Config {
Ok(())
}
fn apply_schema_migrations(&mut self, previous_version: &str) {
if previous_version != CONFIG_SCHEMA_VERSION {
log::info!(
"Upgrading configuration schema from '{}' to '{}'",
previous_version,
CONFIG_SCHEMA_VERSION
);
}
self.schema_version = CONFIG_SCHEMA_VERSION.to_string();
}
fn validate_default_provider(&self) -> Result<()> {
if self.general.default_provider.trim().is_empty() {
return Err(crate::Error::Config(