fix(config): persist cost currency from /config
Summary: - Let /config cost_currency cny --save persist the setting. - Show the live cost currency via /config cost_currency. - Add cost currency to the interactive config document and apply path. - Add regression tests for command persistence and config UI currency round-tripping. Test plan: - cargo test -p deepseek-tui config_command_cost_currency_save_persists_value --locked - cargo test -p deepseek-tui build_document_reflects_cost_currency_from_settings --locked - cargo test -p deepseek-tui session_only_apply_keeps_runtime_overrides_and_skips_reload --locked - cargo fmt --all -- --check - git diff --check Supersedes #956. Fixes #932.
This commit is contained in:
@@ -44,7 +44,7 @@ pub fn show_config(_app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
/// - `/config tui` / `/config web` / `/config native` — open a specific
|
||||
/// editor mode (web requires the `web` build feature).
|
||||
/// - `/config <key>` — shows the current value of a setting.
|
||||
/// - `/config <key> <value>` — sets a runtime value (session only, no --save).
|
||||
/// - `/config <key> <value>` — sets a runtime value (session only, add --save to persist).
|
||||
pub fn config_command(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
let raw = arg.map(str::trim).unwrap_or("");
|
||||
if raw.is_empty() {
|
||||
@@ -63,8 +63,18 @@ pub fn config_command(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
// `/config <key>` — show current value
|
||||
show_single_setting(app, token)
|
||||
} else {
|
||||
// `/config <key> <value>` — set value
|
||||
set_config_value(app, parts[0], parts[1], false)
|
||||
// `/config <key> <value> [--save|-s]` — set value, optionally persist
|
||||
let raw_value = parts[1];
|
||||
let persist = raw_value.ends_with(" --save") || raw_value.ends_with(" -s");
|
||||
let value = if persist {
|
||||
raw_value
|
||||
.strip_suffix(" --save")
|
||||
.or_else(|| raw_value.strip_suffix(" -s"))
|
||||
.unwrap_or(raw_value)
|
||||
} else {
|
||||
raw_value
|
||||
};
|
||||
set_config_value(app, parts[0], value, persist)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +137,13 @@ fn show_single_setting(app: &App, key: &str) -> CommandResult {
|
||||
"transcript_spacing" | "spacing" => {
|
||||
Some(spacing_display(app.transcript_spacing).to_string())
|
||||
}
|
||||
"cost_currency" | "currency" => Some(
|
||||
match app.cost_currency {
|
||||
crate::pricing::CostCurrency::Usd => "usd",
|
||||
crate::pricing::CostCurrency::Cny => "cny",
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
_ => {
|
||||
let known = Settings::available_settings()
|
||||
.iter()
|
||||
@@ -1216,6 +1233,33 @@ mod tests {
|
||||
assert!(saved.contains("default_mode = \"agent\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_command_cost_currency_save_persists_value() {
|
||||
let _lock = lock_test_env();
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let temp_root = env::temp_dir().join(format!(
|
||||
"deepseek-tui-cost-currency-test-{}-{}",
|
||||
std::process::id(),
|
||||
nanos
|
||||
));
|
||||
fs::create_dir_all(&temp_root).unwrap();
|
||||
let _guard = EnvGuard::new(&temp_root);
|
||||
|
||||
let mut app = create_test_app();
|
||||
let result = config_command(&mut app, Some("cost_currency cny --save"));
|
||||
let msg = result.message.unwrap();
|
||||
|
||||
assert_eq!(msg, "cost_currency = cny (saved)");
|
||||
assert_eq!(app.cost_currency, crate::pricing::CostCurrency::Cny);
|
||||
|
||||
let settings_path = Settings::path().unwrap();
|
||||
let saved = fs::read_to_string(settings_path).unwrap();
|
||||
assert!(saved.contains("cost_currency = \"cny\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_approval_mode_valid_values() {
|
||||
let mut app = create_test_app();
|
||||
|
||||
@@ -67,6 +67,7 @@ pub struct SettingsSection {
|
||||
pub sidebar_focus: SidebarFocusValue,
|
||||
#[schemars(range(min = 0))]
|
||||
pub max_history: usize,
|
||||
pub cost_currency: CostCurrencyValue,
|
||||
pub default_model: Option<String>,
|
||||
}
|
||||
|
||||
@@ -181,6 +182,13 @@ pub enum DefaultModeValue {
|
||||
Yolo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CostCurrencyValue {
|
||||
Usd,
|
||||
Cny,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SidebarFocusValue {
|
||||
@@ -267,6 +275,7 @@ pub fn build_document(app: &App, config: &Config) -> Result<ConfigUiDocument> {
|
||||
sidebar_width: settings.sidebar_width_percent,
|
||||
sidebar_focus: settings.sidebar_focus.as_str().into(),
|
||||
max_history: settings.max_input_history,
|
||||
cost_currency: CostCurrencyValue::from_setting(&settings.cost_currency)?,
|
||||
default_model,
|
||||
},
|
||||
config: ConfigSection {
|
||||
@@ -428,6 +437,7 @@ pub fn apply_document(
|
||||
("sidebar_width", &doc.settings.sidebar_width.to_string()),
|
||||
("sidebar_focus", doc.settings.sidebar_focus.as_setting()),
|
||||
("max_history", &doc.settings.max_history.to_string()),
|
||||
("cost_currency", doc.settings.cost_currency.as_setting()),
|
||||
("mcp_config_path", doc.config.mcp_config_path.as_str()),
|
||||
] {
|
||||
let result = commands::set_config_value(app, key, value, persist);
|
||||
@@ -664,6 +674,25 @@ impl DefaultModeValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl CostCurrencyValue {
|
||||
fn from_setting(value: &str) -> Result<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"usd" => Ok(Self::Usd),
|
||||
"cny" | "rmb" | "yuan" => Ok(Self::Cny),
|
||||
other => {
|
||||
anyhow::bail!("Invalid cost_currency '{other}': expected usd, cny, rmb, or yuan")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_setting(self) -> &'static str {
|
||||
match self {
|
||||
Self::Usd => "usd",
|
||||
Self::Cny => "cny",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarFocusValue {
|
||||
fn as_setting(self) -> &'static str {
|
||||
match self {
|
||||
@@ -859,6 +888,50 @@ mod tests {
|
||||
assert_eq!(doc.config.reasoning_effort, ReasoningEffortValue::Max);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_document_reflects_cost_currency_from_settings() {
|
||||
let _lock = lock_test_env();
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("clock")
|
||||
.as_nanos();
|
||||
let temp_root = std::env::temp_dir().join(format!(
|
||||
"deepseek-config-ui-cost-currency-{}-{}",
|
||||
std::process::id(),
|
||||
nanos
|
||||
));
|
||||
fs::create_dir_all(temp_root.join(".deepseek")).expect("config dir");
|
||||
let config_path = temp_root.join(".deepseek").join("config.toml");
|
||||
fs::write(&config_path, "").expect("seed config");
|
||||
fs::write(
|
||||
temp_root.join(".deepseek").join("settings.toml"),
|
||||
r#"
|
||||
cost_currency = "cny"
|
||||
"#,
|
||||
)
|
||||
.expect("seed settings");
|
||||
|
||||
let old_config_path = std::env::var_os("DEEPSEEK_CONFIG_PATH");
|
||||
// Safety: test-only environment mutation guarded by a module mutex.
|
||||
unsafe {
|
||||
std::env::set_var("DEEPSEEK_CONFIG_PATH", &config_path);
|
||||
}
|
||||
|
||||
let app = app();
|
||||
let config = Config::default();
|
||||
let doc = build_document(&app, &config).expect("document");
|
||||
|
||||
assert_eq!(doc.settings.cost_currency, CostCurrencyValue::Cny);
|
||||
// Safety: restore the guarded test-only environment mutation above.
|
||||
unsafe {
|
||||
if let Some(value) = old_config_path {
|
||||
std::env::set_var("DEEPSEEK_CONFIG_PATH", value);
|
||||
} else {
|
||||
std::env::remove_var("DEEPSEEK_CONFIG_PATH");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_contains_typed_enums() {
|
||||
let schema = build_schema();
|
||||
@@ -920,6 +993,7 @@ mcp_config_path = "disk-mcp.json"
|
||||
doc.runtime.model = "deepseek-v4-flash".to_string();
|
||||
doc.config.reasoning_effort = ReasoningEffortValue::Low;
|
||||
doc.config.mcp_config_path = "session-mcp.json".to_string();
|
||||
doc.settings.cost_currency = CostCurrencyValue::Cny;
|
||||
|
||||
let outcome = apply_document(doc, &mut app, &mut config, false).expect("apply");
|
||||
|
||||
@@ -928,6 +1002,7 @@ mcp_config_path = "disk-mcp.json"
|
||||
assert_eq!(app.model, "deepseek-v4-flash");
|
||||
assert_eq!(app.reasoning_effort, ReasoningEffort::Low);
|
||||
assert_eq!(app.mcp_config_path, PathBuf::from("session-mcp.json"));
|
||||
assert_eq!(app.cost_currency, crate::pricing::CostCurrency::Cny);
|
||||
assert_eq!(
|
||||
config.reasoning_effort.as_deref(),
|
||||
Some(ReasoningEffort::Low.as_setting())
|
||||
|
||||
Reference in New Issue
Block a user