feat(tui): support custom background color (#1034)
This commit is contained in:
@@ -347,7 +347,7 @@ Key environment variables:
|
||||
| `NO_ANIMATIONS=1` | Force accessibility mode at startup |
|
||||
| `SSL_CERT_FILE` | Custom CA bundle for corporate proxies |
|
||||
|
||||
UI locale is separate from model language — set `locale` in `settings.toml`, use `/config locale zh-Hans`, or rely on `LC_ALL`/`LANG`. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) and [docs/MCP.md](docs/MCP.md).
|
||||
Set `locale` in `settings.toml`, use `/config locale zh-Hans`, or rely on `LC_ALL`/`LANG` to choose UI chrome and the default natural language sent to V4 models. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) and [docs/MCP.md](docs/MCP.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -119,6 +119,10 @@ fn show_single_setting(app: &App, key: &str) -> CommandResult {
|
||||
}
|
||||
"approval_mode" | "approval" => Some(app.approval_mode.label().to_string()),
|
||||
"locale" | "language" => Some(locale_display(app.ui_locale).to_string()),
|
||||
"background_color" | "background" | "bg" => {
|
||||
crate::palette::hex_rgb_string(app.ui_theme.surface_bg)
|
||||
.or_else(|| Some("(default)".to_string()))
|
||||
}
|
||||
"auto_compact" | "compact" => {
|
||||
Some(if app.auto_compact { "true" } else { "false" }.to_string())
|
||||
}
|
||||
@@ -383,6 +387,15 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
|
||||
app.ui_locale = resolve_locale(&settings.locale);
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"background_color" | "background" | "bg" => {
|
||||
let base_theme = crate::palette::UiTheme::detect();
|
||||
app.ui_theme = settings
|
||||
.background_color
|
||||
.as_deref()
|
||||
.and_then(crate::palette::parse_hex_rgb_color)
|
||||
.map_or(base_theme, |color| base_theme.with_background_color(color));
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"cost_currency" | "currency" => {
|
||||
app.cost_currency = crate::pricing::CostCurrency::from_setting(&settings.cost_currency)
|
||||
.unwrap_or(crate::pricing::CostCurrency::Usd);
|
||||
@@ -443,6 +456,10 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
|
||||
let display_value = match key.as_str() {
|
||||
"default_mode" | "mode" => settings.default_mode.clone(),
|
||||
"cost_currency" | "currency" => settings.cost_currency.clone(),
|
||||
"background_color" | "background" | "bg" => settings
|
||||
.background_color
|
||||
.clone()
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
_ => value.to_string(),
|
||||
};
|
||||
|
||||
|
||||
@@ -58,6 +58,11 @@ pub struct SettingsSection {
|
||||
pub show_thinking: bool,
|
||||
pub show_tool_details: bool,
|
||||
pub locale: UiLocale,
|
||||
#[schemars(
|
||||
title = "Background color",
|
||||
description = "Main TUI background color as #RRGGBB"
|
||||
)]
|
||||
pub background_color: Option<String>,
|
||||
pub composer_density: ComposerDensityValue,
|
||||
pub composer_border: bool,
|
||||
pub transcript_spacing: TranscriptSpacingValue,
|
||||
@@ -268,6 +273,7 @@ pub fn build_document(app: &App, config: &Config) -> Result<ConfigUiDocument> {
|
||||
show_thinking: settings.show_thinking,
|
||||
show_tool_details: settings.show_tool_details,
|
||||
locale: UiLocale::from_setting(&settings.locale)?,
|
||||
background_color: settings.background_color.clone(),
|
||||
composer_density: settings.composer_density.as_str().into(),
|
||||
composer_border: settings.composer_border,
|
||||
transcript_spacing: settings.transcript_spacing.as_str().into(),
|
||||
@@ -424,6 +430,13 @@ pub fn apply_document(
|
||||
bool_str(doc.settings.show_tool_details),
|
||||
),
|
||||
("locale", doc.settings.locale.as_setting()),
|
||||
(
|
||||
"background_color",
|
||||
doc.settings
|
||||
.background_color
|
||||
.as_deref()
|
||||
.unwrap_or("default"),
|
||||
),
|
||||
(
|
||||
"composer_density",
|
||||
doc.settings.composer_density.as_setting(),
|
||||
@@ -932,6 +945,48 @@ cost_currency = "cny"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_document_reflects_background_color_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-background-color-{}-{}",
|
||||
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##"
|
||||
background_color = "#1A1B26"
|
||||
"##,
|
||||
)
|
||||
.expect("seed settings");
|
||||
|
||||
let old_config_path = std::env::var_os("DEEPSEEK_CONFIG_PATH");
|
||||
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.background_color.as_deref(), Some("#1a1b26"));
|
||||
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();
|
||||
|
||||
@@ -287,6 +287,40 @@ impl UiTheme {
|
||||
pub fn detect() -> Self {
|
||||
Self::for_mode(PaletteMode::detect())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_background_color(mut self, color: Color) -> Self {
|
||||
self.surface_bg = color;
|
||||
self.header_bg = color;
|
||||
self.footer_bg = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn parse_hex_rgb_color(value: &str) -> Option<Color> {
|
||||
let hex = value.trim().strip_prefix('#').unwrap_or(value.trim());
|
||||
if hex.len() != 6 || !hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||
Some(Color::Rgb(r, g, b))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn normalize_hex_rgb_color(value: &str) -> Option<String> {
|
||||
hex_rgb_string(parse_hex_rgb_color(value)?)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn hex_rgb_string(color: Color) -> Option<String> {
|
||||
let Color::Rgb(r, g, b) = color else {
|
||||
return None;
|
||||
};
|
||||
Some(format!("#{r:02x}{g:02x}{b:02x}"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -612,9 +646,10 @@ mod tests {
|
||||
use super::{
|
||||
ACCENT_REASONING_LIVE, ColorDepth, DEEPSEEK_INK, DEEPSEEK_RED, DEEPSEEK_SKY,
|
||||
DEEPSEEK_SLATE, LIGHT_PANEL, LIGHT_SURFACE, LIGHT_TEXT_BODY, LIGHT_TEXT_HINT,
|
||||
LIGHT_UI_THEME, PaletteMode, SURFACE_REASONING, TEXT_HINT, adapt_bg,
|
||||
LIGHT_UI_THEME, PaletteMode, SURFACE_REASONING, TEXT_HINT, UI_THEME, adapt_bg,
|
||||
adapt_bg_for_palette_mode, adapt_color, adapt_fg_for_palette_mode, blend, nearest_ansi16,
|
||||
pulse_brightness, reasoning_surface_tint, rgb_to_ansi256,
|
||||
normalize_hex_rgb_color, parse_hex_rgb_color, pulse_brightness, reasoning_surface_tint,
|
||||
rgb_to_ansi256,
|
||||
};
|
||||
use ratatui::style::Color;
|
||||
|
||||
@@ -640,6 +675,32 @@ mod tests {
|
||||
assert_eq!(theme.text_body, LIGHT_TEXT_BODY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_theme_applies_custom_background_to_base_surfaces() {
|
||||
let custom = Color::Rgb(26, 27, 38);
|
||||
let theme = super::UiTheme::for_mode(PaletteMode::Dark).with_background_color(custom);
|
||||
|
||||
assert_eq!(theme.surface_bg, custom);
|
||||
assert_eq!(theme.header_bg, custom);
|
||||
assert_eq!(theme.footer_bg, custom);
|
||||
assert_eq!(
|
||||
theme.composer_bg, UI_THEME.composer_bg,
|
||||
"custom background must not erase panel contrast"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_rgb_color_parser_accepts_hashless_and_normalizes() {
|
||||
assert_eq!(parse_hex_rgb_color("#1a1B26"), Some(Color::Rgb(26, 27, 38)));
|
||||
assert_eq!(parse_hex_rgb_color("1a1b26"), Some(Color::Rgb(26, 27, 38)));
|
||||
assert_eq!(
|
||||
normalize_hex_rgb_color("#1A1B26").as_deref(),
|
||||
Some("#1a1b26")
|
||||
);
|
||||
assert_eq!(parse_hex_rgb_color("#123"), None);
|
||||
assert_eq!(parse_hex_rgb_color("#zzzzzz"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_palette_maps_dark_surfaces_and_text() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::{expand_path, normalize_model_name};
|
||||
use crate::localization::normalize_configured_locale;
|
||||
use crate::palette::normalize_hex_rgb_color;
|
||||
|
||||
// ============================================================================
|
||||
// TuiPrefs — ~/.deepseek/tui.toml
|
||||
@@ -185,6 +186,8 @@ pub struct Settings {
|
||||
pub show_tool_details: bool,
|
||||
/// UI locale: auto, en, ja, zh-Hans, pt-BR
|
||||
pub locale: String,
|
||||
/// Optional main TUI background color as a 6-digit hex RGB value.
|
||||
pub background_color: Option<String>,
|
||||
/// Composer layout density: compact, comfortable, spacious
|
||||
pub composer_density: String,
|
||||
/// Show a border around the composer input area
|
||||
@@ -233,6 +236,7 @@ impl Default for Settings {
|
||||
show_thinking: true,
|
||||
show_tool_details: true,
|
||||
locale: "auto".to_string(),
|
||||
background_color: None,
|
||||
composer_density: "comfortable".to_string(),
|
||||
composer_border: true,
|
||||
composer_vim_mode: "normal".to_string(),
|
||||
@@ -287,6 +291,7 @@ impl Settings {
|
||||
s.locale = normalize_configured_locale(&s.locale)
|
||||
.unwrap_or("en")
|
||||
.to_string();
|
||||
s.background_color = normalize_optional_background_color(s.background_color.as_deref());
|
||||
s.default_model = s.default_model.as_deref().and_then(normalize_default_model);
|
||||
s
|
||||
};
|
||||
@@ -358,6 +363,9 @@ impl Settings {
|
||||
};
|
||||
self.locale = locale.to_string();
|
||||
}
|
||||
"background_color" | "background" | "bg" => {
|
||||
self.background_color = normalize_background_color_setting(value)?;
|
||||
}
|
||||
"composer_density" | "composer" => {
|
||||
let normalized = normalize_composer_density(value);
|
||||
if !["compact", "comfortable", "spacious"].contains(&normalized) {
|
||||
@@ -491,6 +499,10 @@ impl Settings {
|
||||
lines.push(format!(" show_thinking: {}", self.show_thinking));
|
||||
lines.push(format!(" show_tool_details: {}", self.show_tool_details));
|
||||
lines.push(format!(" locale: {}", self.locale));
|
||||
lines.push(format!(
|
||||
" background_color: {}",
|
||||
self.background_color.as_deref().unwrap_or("(default)")
|
||||
));
|
||||
lines.push(format!(" composer_density: {}", self.composer_density));
|
||||
lines.push(format!(" composer_border: {}", self.composer_border));
|
||||
lines.push(format!(" composer_vim_mode: {}", self.composer_vim_mode));
|
||||
@@ -542,7 +554,11 @@ impl Settings {
|
||||
("show_tool_details", "Show detailed tool output: on/off"),
|
||||
(
|
||||
"locale",
|
||||
"UI locale: auto, en, ja, zh-Hans, pt-BR (model output is unchanged)",
|
||||
"UI locale and default model language: auto, en, ja, zh-Hans, pt-BR",
|
||||
),
|
||||
(
|
||||
"background_color",
|
||||
"Main TUI background color: #RRGGBB or default",
|
||||
),
|
||||
(
|
||||
"composer_density",
|
||||
@@ -621,6 +637,28 @@ fn normalize_transcript_spacing(value: &str) -> &str {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_optional_background_color(value: Option<&str>) -> Option<String> {
|
||||
value.and_then(|raw| normalize_background_color_setting(raw).ok().flatten())
|
||||
}
|
||||
|
||||
fn normalize_background_color_setting(value: &str) -> Result<Option<String>> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty()
|
||||
|| matches!(
|
||||
trimmed.to_ascii_lowercase().as_str(),
|
||||
"default" | "none" | "reset" | "off"
|
||||
)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
normalize_hex_rgb_color(trimmed).map(Some).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to update setting: invalid background_color '{value}'. Expected #RRGGBB, RRGGBB, or default."
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_sidebar_focus(value: &str) -> &str {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"plan" => "plan",
|
||||
@@ -705,6 +743,29 @@ mod tests {
|
||||
assert!(err.to_string().contains("invalid locale"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_color_normalizes_hex_and_accepts_default() {
|
||||
let mut settings = Settings::default();
|
||||
settings
|
||||
.set("background_color", "#1A1b26")
|
||||
.expect("set custom background");
|
||||
assert_eq!(settings.background_color.as_deref(), Some("#1a1b26"));
|
||||
|
||||
settings
|
||||
.set("background", "default")
|
||||
.expect("reset custom background");
|
||||
assert_eq!(settings.background_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_color_rejects_invalid_hex() {
|
||||
let mut settings = Settings::default();
|
||||
let err = settings
|
||||
.set("background_color", "#123")
|
||||
.expect_err("short hex should fail");
|
||||
assert!(err.to_string().contains("invalid background_color"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_currency_normalizes_yuan_aliases_and_rejects_unknowns() {
|
||||
let mut settings = Settings::default();
|
||||
|
||||
@@ -1148,7 +1148,14 @@ impl App {
|
||||
let sidebar_focus = SidebarFocus::from_setting(&settings.sidebar_focus);
|
||||
let max_input_history = settings.max_input_history;
|
||||
let use_paste_burst_detection = settings.paste_burst_detection;
|
||||
let ui_theme = palette::UiTheme::detect();
|
||||
let mut ui_theme = palette::UiTheme::detect();
|
||||
if let Some(background) = settings
|
||||
.background_color
|
||||
.as_deref()
|
||||
.and_then(palette::parse_hex_rgb_color)
|
||||
{
|
||||
ui_theme = ui_theme.with_background_color(background);
|
||||
}
|
||||
let model = settings.default_model.clone().unwrap_or(model);
|
||||
let auto_model = model.trim().eq_ignore_ascii_case("auto");
|
||||
let threshold_model = if auto_model {
|
||||
|
||||
@@ -20,7 +20,7 @@ use ratatui::{
|
||||
Frame, Terminal,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
prelude::Widget,
|
||||
style::{Color, Style},
|
||||
style::Style,
|
||||
text::Span,
|
||||
widgets::Block,
|
||||
};
|
||||
@@ -5107,8 +5107,8 @@ fn build_pending_input_preview(app: &App) -> PendingInputPreview {
|
||||
fn render(f: &mut Frame, app: &mut App) {
|
||||
let size = f.area();
|
||||
|
||||
// Clear entire area with terminal default background
|
||||
let background = Block::default().style(Style::default().bg(Color::Reset));
|
||||
// Clear entire area with the configured app background.
|
||||
let background = Block::default().style(Style::default().bg(app.ui_theme.surface_bg));
|
||||
f.render_widget(background, size);
|
||||
|
||||
// Show onboarding screen if needed
|
||||
@@ -5213,7 +5213,9 @@ fn render(f: &mut Frame, app: &mut App) {
|
||||
// background before any sub-widgets render, so cells that end up
|
||||
// uncovered by layout splits (e.g. after file-tree toggle or
|
||||
// resize) don't retain stale content from a previous frame.
|
||||
Block::default().render(chunks[1], f.buffer_mut());
|
||||
Block::default()
|
||||
.style(Style::default().bg(app.ui_theme.surface_bg))
|
||||
.render(chunks[1], f.buffer_mut());
|
||||
|
||||
let mut sidebar_area = None;
|
||||
|
||||
|
||||
@@ -599,6 +599,16 @@ impl ConfigView {
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Display,
|
||||
key: "background_color".to_string(),
|
||||
value: settings
|
||||
.background_color
|
||||
.clone()
|
||||
.unwrap_or_else(|| "(default)".to_string()),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Display,
|
||||
key: "calm_mode".to_string(),
|
||||
@@ -1005,6 +1015,7 @@ fn config_hint_for_key(key: &str) -> &'static str {
|
||||
| "paste_burst_detection" => "on/off, true/false, yes/no, 1/0",
|
||||
"composer_density" | "transcript_spacing" => "compact | comfortable | spacious",
|
||||
"locale" => "auto | en | ja | zh-Hans | pt-BR",
|
||||
"background_color" => "#RRGGBB | default",
|
||||
"default_mode" => "agent | plan | yolo",
|
||||
"sidebar_width" => "10..=50",
|
||||
"sidebar_focus" => "auto | plan | todos | tasks | agents",
|
||||
@@ -2017,6 +2028,7 @@ mod tests {
|
||||
assert!(keys.contains(&"model"));
|
||||
assert!(keys.contains(&"approval_mode"));
|
||||
assert!(keys.contains(&"locale"));
|
||||
assert!(keys.contains(&"background_color"));
|
||||
assert!(keys.contains(&"auto_compact"));
|
||||
assert!(keys.contains(&"composer_border"));
|
||||
assert!(keys.contains(&"mcp_config_path"));
|
||||
|
||||
@@ -54,6 +54,7 @@ pub struct ChatWidget {
|
||||
lines: Vec<Line<'static>>,
|
||||
scrollbar: Option<TranscriptScrollbar>,
|
||||
jump_to_latest_button: Option<Rect>,
|
||||
background: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -66,6 +67,7 @@ struct TranscriptScrollbar {
|
||||
impl ChatWidget {
|
||||
pub fn new(app: &mut App, area: Rect) -> Self {
|
||||
let content_area = area;
|
||||
let background = app.ui_theme.surface_bg;
|
||||
let visible_lines = content_area.height as usize;
|
||||
let render_options = app.transcript_render_options();
|
||||
|
||||
@@ -82,6 +84,7 @@ impl ChatWidget {
|
||||
lines,
|
||||
scrollbar: None,
|
||||
jump_to_latest_button: None,
|
||||
background,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -290,6 +293,7 @@ impl ChatWidget {
|
||||
lines,
|
||||
scrollbar,
|
||||
jump_to_latest_button,
|
||||
background,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,11 +323,11 @@ impl Renderable for ChatWidget {
|
||||
// gray on most user setups; an explicit ink fill keeps the chat
|
||||
// area on-brand.
|
||||
Block::default()
|
||||
.style(Style::default().bg(palette::DEEPSEEK_INK))
|
||||
.style(Style::default().bg(self.background))
|
||||
.render(area, buf);
|
||||
|
||||
let paragraph =
|
||||
Paragraph::new(self.lines.clone()).style(Style::default().bg(palette::DEEPSEEK_INK));
|
||||
Paragraph::new(self.lines.clone()).style(Style::default().bg(self.background));
|
||||
paragraph.render(area, buf);
|
||||
|
||||
if let Some(scrollbar) = self.scrollbar {
|
||||
@@ -342,7 +346,7 @@ impl Renderable for ChatWidget {
|
||||
}
|
||||
|
||||
if let Some(button_area) = self.jump_to_latest_button {
|
||||
render_jump_to_latest_button(button_area, buf);
|
||||
render_jump_to_latest_button(button_area, buf, self.background);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,12 +378,12 @@ fn jump_to_latest_button_rect(area: Rect, has_scrollbar: bool) -> Option<Rect> {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_jump_to_latest_button(area: Rect, buf: &mut Buffer) {
|
||||
fn render_jump_to_latest_button(area: Rect, buf: &mut Buffer, background: Color) {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(palette::BORDER_COLOR))
|
||||
.style(Style::default().bg(palette::DEEPSEEK_INK))
|
||||
.style(Style::default().bg(background))
|
||||
.render(area, buf);
|
||||
|
||||
let arrow_x = area.x.saturating_add(1);
|
||||
@@ -2613,6 +2617,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chat_widget_uses_configured_surface_background() {
|
||||
let mut app = create_test_app();
|
||||
let custom = ratatui::style::Color::Rgb(26, 27, 38);
|
||||
app.ui_theme = app.ui_theme.with_background_color(custom);
|
||||
app.add_message(HistoryCell::Assistant {
|
||||
content: "ready".to_string(),
|
||||
streaming: false,
|
||||
});
|
||||
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 30,
|
||||
height: 5,
|
||||
};
|
||||
let mut buf = Buffer::empty(area);
|
||||
let widget = ChatWidget::new(&mut app, area);
|
||||
widget.render(area, &mut buf);
|
||||
|
||||
assert_eq!(buf[(area.x, area.y)].bg, custom);
|
||||
assert_eq!(
|
||||
buf[(area.x + area.width - 1, area.y + area.height - 1)].bg,
|
||||
custom
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression: when the transcript scrollbar is visible, the rightmost
|
||||
/// content column must remain readable (the scrollbar gets its own
|
||||
/// 1-column gutter rather than overdrawing chat content).
|
||||
|
||||
@@ -287,8 +287,12 @@ Common settings keys:
|
||||
- `show_tool_details` (on/off)
|
||||
- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `pt-BR`; default `auto`): UI chrome
|
||||
locale. `auto` checks `LC_ALL`, `LC_MESSAGES`, then `LANG`; unsupported or
|
||||
missing locales fall back to English. This does not force model output
|
||||
language.
|
||||
missing locales fall back to English. The runtime also exposes the resolved
|
||||
locale in the system prompt so V4 models use it as the default natural
|
||||
language for reasoning and replies.
|
||||
- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI
|
||||
background color applied to the root, header, transcript, and footer
|
||||
surfaces while preserving panel contrast.
|
||||
- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,
|
||||
context panel, `/cost`, `/tokens`, and long-turn notification summaries. The
|
||||
aliases `rmb` and `yuan` normalize to `cny`.
|
||||
|
||||
@@ -37,7 +37,9 @@ Fallback:
|
||||
|
||||
- Missing or unsupported configured locales fall back to English.
|
||||
- `auto` falls back to English when no supported environment locale is detected.
|
||||
- UI locale is separate from model prompt language. Users still ask the model for a response language in the prompt.
|
||||
- The resolved locale is included in the system prompt and used as the default
|
||||
natural language for V4 reasoning and replies. Users can still switch
|
||||
languages mid-session by writing in a different language.
|
||||
|
||||
## Planned Global South QA Matrix
|
||||
|
||||
|
||||
Reference in New Issue
Block a user