diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 68ce1ac7a..26c381d50 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -575,6 +575,7 @@ pub struct ConfigView { filter: String, status: Option, locale: Locale, + effective_cost_currency: String, last_visible_rows: Cell, last_row_hitboxes: RefCell>, } @@ -819,6 +820,7 @@ impl ConfigView { filter: String::new(), status: None, locale: app.ui_locale, + effective_cost_currency: cost_currency_config_value(app), last_visible_rows: Cell::new(0), last_row_hitboxes: RefCell::new(Vec::new()), } @@ -841,7 +843,7 @@ impl ConfigView { let section = row.section.label().to_lowercase(); let key = row.key.to_lowercase(); - let value = row.value.to_lowercase(); + let value = self.row_display_value(row).to_lowercase(); let scope = row.scope.label().to_lowercase(); filter.split_whitespace().all(|term| { @@ -1120,6 +1122,27 @@ impl ConfigView { self.update_filter(|filter| filter.clear()); } + + fn row_display_value(&self, row: &ConfigRow) -> String { + if row.key == "cost_currency" && row.scope == ConfigScope::Saved { + let saved_cost_currency = crate::pricing::CostCurrency::from_setting(&row.value); + let effective_cost_currency = + crate::pricing::CostCurrency::from_setting(&self.effective_cost_currency); + if saved_cost_currency != effective_cost_currency { + return format!("{} (effective {})", row.value, self.effective_cost_currency); + } + } + + row.value.clone() + } +} + +fn cost_currency_config_value(app: &App) -> String { + match app.cost_currency { + crate::pricing::CostCurrency::Usd => "usd", + crate::pricing::CostCurrency::Cny => "cny", + } + .to_string() } fn config_hint_for_key(key: &str) -> &'static str { @@ -1137,6 +1160,7 @@ fn config_hint_for_key(key: &str) -> &'static str { "theme" => "system | dark | light | grayscale", "locale" => "auto | en | ja | zh-Hans | pt-BR", "background_color" => "#RRGGBB | default", + "cost_currency" => "usd | cny", "default_mode" => "agent | plan | yolo", "sidebar_width" => "10..=50", "sidebar_focus" => "auto | work | tasks | agents | context | hidden", @@ -1444,7 +1468,10 @@ impl ModalView for ConfigView { } else { Style::default().fg(palette::TEXT_PRIMARY) }; - let value = truncate_view_text(&row.value, CONFIG_VALUE_COLUMN_WIDTH); + let value = truncate_view_text( + &self.row_display_value(row), + CONFIG_VALUE_COLUMN_WIDTH, + ); let mut line = Line::from(format!( " {:, + _lock: MutexGuard<'static, ()>, + } + + impl ConfigSettingsEnvGuard { + fn new(settings_toml: &str) -> Self { + let lock = crate::test_support::lock_test_env(); + let tmp = TempDir::new().expect("settings tempdir"); + let config_path = tmp.path().join(".deepseek").join("config.toml"); + let settings_path = config_path + .parent() + .expect("settings parent") + .join("settings.toml"); + std::fs::create_dir_all(config_path.parent().expect("config parent")) + .expect("config dir"); + std::fs::write(&settings_path, settings_toml).expect("settings file"); + let previous_config_path = std::env::var_os("DEEPSEEK_CONFIG_PATH"); + unsafe { + std::env::set_var("DEEPSEEK_CONFIG_PATH", &config_path); + } + Self { + _tmp: tmp, + previous_config_path, + _lock: lock, + } + } + } + + impl Drop for ConfigSettingsEnvGuard { + fn drop(&mut self) { + unsafe { + match self.previous_config_path.take() { + Some(previous) => std::env::set_var("DEEPSEEK_CONFIG_PATH", previous), + None => std::env::remove_var("DEEPSEEK_CONFIG_PATH"), + } + } + } + } fn create_test_app() -> App { let options = TuiOptions { @@ -2051,6 +2122,26 @@ mod tests { App::new(options, &Config::default()) } + fn cost_currency_row_for_settings( + settings_toml: &str, + ) -> (String, String, crate::pricing::CostCurrency, Locale) { + let _guard = ConfigSettingsEnvGuard::new(settings_toml); + let app = create_test_app(); + let view = ConfigView::new_for_app(&app); + let row = view + .rows + .iter() + .find(|row| row.key == "cost_currency") + .expect("cost_currency row"); + + ( + row.value.clone(), + view.row_display_value(row), + app.cost_currency, + app.ui_locale, + ) + } + fn type_filter(view: &mut ConfigView, text: &str) { for ch in text.chars() { let action = view.handle_key(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); @@ -2231,6 +2322,62 @@ mod tests { assert_eq!(row.value, "https://ui-config-view.local/v1"); } + #[test] + fn config_view_cost_currency_shows_saved_and_effective_runtime_currency() { + let _guard = ConfigSettingsEnvGuard::new("locale = \"zh-Hans\"\ncost_currency = \"usd\"\n"); + let app = create_test_app(); + assert_eq!(app.ui_locale, Locale::ZhHans); + assert_eq!(app.cost_currency, crate::pricing::CostCurrency::Cny); + + let view = ConfigView::new_for_app(&app); + let row = view + .rows + .iter() + .find(|row| row.key == "cost_currency") + .expect("cost_currency row"); + + assert_eq!(row.value, "usd"); + assert_eq!(view.row_display_value(row), "usd (effective cny)"); + assert_eq!(Settings::load().expect("settings").cost_currency, "usd"); + } + + #[test] + fn config_view_cost_currency_aliases_matching_effective_currency_are_silent() { + for alias in ["rmb", "yuan", "¥"] { + let (saved_value, display_value, effective_currency, locale) = + cost_currency_row_for_settings(&format!( + "locale = \"zh-Hans\"\ncost_currency = \"{alias}\"\n" + )); + + assert_eq!(locale, Locale::ZhHans); + assert_eq!(effective_currency, crate::pricing::CostCurrency::Cny); + assert_eq!(saved_value, alias); + assert_eq!(display_value, alias); + } + } + + #[test] + fn config_view_cost_currency_matching_cny_setting_is_silent() { + let (saved_value, display_value, effective_currency, locale) = + cost_currency_row_for_settings("locale = \"zh-Hans\"\ncost_currency = \"cny\"\n"); + + assert_eq!(locale, Locale::ZhHans); + assert_eq!(effective_currency, crate::pricing::CostCurrency::Cny); + assert_eq!(saved_value, "cny"); + assert_eq!(display_value, "cny"); + } + + #[test] + fn config_view_cost_currency_non_zh_hans_locale_uses_saved_currency() { + let (saved_value, display_value, effective_currency, locale) = + cost_currency_row_for_settings("locale = \"en\"\ncost_currency = \"cny\"\n"); + + assert_eq!(locale, Locale::En); + assert_eq!(effective_currency, crate::pricing::CostCurrency::Cny); + assert_eq!(saved_value, "cny"); + assert_eq!(display_value, "cny"); + } + #[test] fn config_view_exposes_all_available_saved_settings() { let app = create_test_app();