feat(tui/cards): set ANSI-16 card backgrounds

When PaletteMode::Ansi16, render tool cards with solid ANSI backgrounds and force high-contrast text (white on dark, black on light).

Mappings:
- Auto Drive: magenta / light magenta
- Agent (read): green / light green
- Agent (write): yellow / light yellow
- Browser: red / light red

Also extend /demo to accept 'auto drive card' for quickly previewing all ANSI-16 backgrounds.
This commit is contained in:
James Peter
2025-12-18 15:52:59 +10:00
parent bfc41e2815
commit 820991e9d6
5 changed files with 238 additions and 22 deletions
+1 -1
View File
@@ -2355,7 +2355,7 @@ impl App<'_> {
}
SlashCommand::Demo => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.handle_demo_command();
widget.handle_demo_command(command_args);
}
}
// Prompt-expanding commands should have been handled in submit_user_message
+105 -1
View File
@@ -13997,7 +13997,21 @@ impl ChatWidget<'_> {
self.request_redraw();
}
pub(crate) fn handle_demo_command(&mut self) {
pub(crate) fn handle_demo_command(&mut self, command_args: String) {
let trimmed_args = command_args.trim();
if !trimmed_args.is_empty() {
if self.handle_demo_auto_drive_card_background_palette(trimmed_args) {
self.request_redraw();
return;
}
self.history_push_plain_state(history_cell::new_warning_event(format!(
"demo: unknown args '{trimmed_args}' (try: /demo auto drive card)",
)));
self.request_redraw();
return;
}
use ratatui::style::Modifier as RtModifier;
use ratatui::style::Style as RtStyle;
use ratatui::text::Span;
@@ -14436,6 +14450,96 @@ impl ChatWidget<'_> {
self.request_redraw();
}
fn handle_demo_auto_drive_card_background_palette(&mut self, args: &str) -> bool {
if !Self::demo_command_is_auto_drive_card_backgrounds(args) {
return false;
}
let (r, g, b) = crate::colors::color_to_rgb(crate::colors::background());
let luminance = (0.2126 * r as f32 + 0.7152 * g as f32 + 0.0722 * b as f32) / 255.0;
let theme_label = if luminance < 0.5 { "dark" } else { "light" };
self.history_push_plain_state(history_cell::plain_message_state_from_lines(
vec![
ratatui::text::Line::from("Auto Drive card — ANSI-16 background palette"),
ratatui::text::Line::from(format!(
"Theme context: {theme_label} (based on current /theme background)",
)),
ratatui::text::Line::from(
"Tip: switch /theme (dark/light) and rerun to compare.".to_string(),
),
],
HistoryCellType::Notice,
));
use ratatui::style::Color;
const PALETTE: &[(Color, &str)] = &[
(Color::Black, "Black"),
(Color::Red, "Red"),
(Color::Green, "Green"),
(Color::Yellow, "Yellow"),
(Color::Blue, "Blue"),
(Color::Magenta, "Magenta"),
(Color::Cyan, "Cyan"),
(Color::Gray, "Gray"),
(Color::DarkGray, "DarkGray"),
(Color::LightRed, "LightRed"),
(Color::LightGreen, "LightGreen"),
(Color::LightYellow, "LightYellow"),
(Color::LightBlue, "LightBlue"),
(Color::LightMagenta, "LightMagenta"),
(Color::LightCyan, "LightCyan"),
(Color::White, "White"),
];
for (idx, (bg, name)) in PALETTE.iter().enumerate() {
let ordinal = idx + 1;
let goal = format!("ANSI-16 bg {ordinal:02}: {name}");
let mut auto_drive_card = history_cell::AutoDriveCardCell::new(Some(goal));
auto_drive_card.disable_reveal();
auto_drive_card.set_background_override(Some(*bg));
auto_drive_card.push_action(
"Queued smoke tests across agents",
AutoDriveActionKind::Info,
);
auto_drive_card.push_action(
"Warning: macOS shard flaked",
AutoDriveActionKind::Warning,
);
auto_drive_card.push_action(
"Action required: retry or pause run",
AutoDriveActionKind::Error,
);
auto_drive_card.set_status(AutoDriveStatus::Paused);
self.history_push(auto_drive_card);
}
true
}
fn demo_command_is_auto_drive_card_backgrounds(args: &str) -> bool {
let normalized = args.trim().to_ascii_lowercase();
let simplified = normalized.replace(['-', '_'], " ");
let tokens: std::collections::HashSet<&str> = simplified.split_whitespace().collect();
if tokens.is_empty() {
return false;
}
let wants_auto_drive = (tokens.contains("auto") && tokens.contains("drive"))
|| tokens.contains("autodrive")
|| tokens.contains("auto-drive");
let wants_card = tokens.contains("card") || tokens.contains("cards");
let wants_background = tokens.contains("bg")
|| tokens.contains("background")
|| tokens.contains("backgrounds")
|| tokens.contains("color")
|| tokens.contains("colors")
|| tokens.contains("colour")
|| tokens.contains("colours");
wants_auto_drive && (wants_card || wants_background)
}
fn add_perf_output(&mut self, text: String) {
let mut lines: Vec<ratatui::text::Line<'static>> = Vec::new();
lines.push(ratatui::text::Line::from("performance".dim()));
+61 -13
View File
@@ -15,7 +15,9 @@ use super::{HistoryCell, HistoryCellType, ToolCellStatus};
use crate::card_theme;
use crate::glitch_animation::{gradient_multi, mix_rgb};
use crate::gradient_background::{GradientBackground, RevealRender};
use crate::util::buffer::fill_rect;
use crate::colors;
use crate::theme::{palette_mode, PaletteMode};
use code_common::elapsed::format_duration_digital;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -97,6 +99,7 @@ pub(crate) struct AutoDriveCardCell {
actions: Vec<AutoDriveAction>,
cell_key: Option<String>,
signature: Option<String>,
background_override: Option<Color>,
reveal_started_at: Option<Instant>,
first_action_at: Option<Instant>,
completion_message: Option<String>,
@@ -105,16 +108,21 @@ pub(crate) struct AutoDriveCardCell {
impl AutoDriveCardCell {
pub(crate) fn new(goal: Option<String>) -> Self {
let reveal_started_at = active_auto_drive_theme()
.theme
.reveal
.map(|_| Instant::now());
let reveal_started_at = if palette_mode() == PaletteMode::Ansi16 {
None
} else {
active_auto_drive_theme()
.theme
.reveal
.map(|_| Instant::now())
};
let cell = Self {
goal: goal.and_then(Self::normalize_text),
status: AutoDriveStatus::Running,
actions: Vec::new(),
cell_key: None,
signature: None,
background_override: None,
reveal_started_at,
first_action_at: None,
completion_message: None,
@@ -152,6 +160,14 @@ impl AutoDriveCardCell {
self.status = status;
}
pub(crate) fn set_background_override(&mut self, background: Option<Color>) {
self.background_override = background;
}
pub(crate) fn disable_reveal(&mut self) {
self.reveal_started_at = None;
}
pub(crate) fn push_action(&mut self, text: impl Into<String>, kind: AutoDriveActionKind) {
let now = Instant::now();
let base = self.first_action_at.get_or_insert(now);
@@ -1038,6 +1054,15 @@ impl AutoDriveCardCell {
}
fn celebration_background_style() -> Style {
if palette_mode() == PaletteMode::Ansi16 {
let fg = if is_dark_theme_active() {
Color::White
} else {
Color::Black
};
return Style::default().fg(fg);
}
Style::default().fg(Color::Rgb(255, 255, 255))
}
@@ -1059,21 +1084,44 @@ impl AutoDriveCardCell {
}
let style = auto_drive_card_style();
let reveal = self.reveal_progress().and_then(|(progress, theme)| {
theme.theme.reveal.map(|config| RevealRender {
progress,
variant: config.variant,
intro_light: !is_dark_theme_active(),
})
});
let draw_width = area.width - 2;
let render_area = Rect {
width: draw_width,
..area
};
GradientBackground::render(buf, render_area, &style.gradient, style.text_primary, reveal);
if let Some(bg) = self.background_override {
fill_rect(
buf,
render_area,
Some(' '),
Style::default().fg(style.text_primary).bg(bg),
);
} else if palette_mode() == PaletteMode::Ansi16 {
fill_rect(
buf,
render_area,
Some(' '),
Style::default()
.fg(style.text_primary)
.bg(style.gradient.left),
);
} else {
let reveal = self.reveal_progress().and_then(|(progress, theme)| {
theme.theme.reveal.map(|config| RevealRender {
progress,
variant: config.variant,
intro_light: !is_dark_theme_active(),
})
});
GradientBackground::render(
buf,
render_area,
&style.gradient,
style.text_primary,
reveal,
);
}
let rows = self.build_card_rows(render_area.width, &style);
let lines = rows_to_lines(&rows, &style, render_area.width);
+69 -7
View File
@@ -7,6 +7,8 @@ use crate::card_theme;
use crate::card_theme::{CardThemeDefinition, GradientSpec};
use crate::colors;
use crate::gradient_background::GradientBackground;
use crate::theme::{palette_mode, PaletteMode};
use crate::util::buffer::fill_rect;
#[derive(Clone, Copy)]
pub(crate) struct CardStyle {
@@ -60,16 +62,34 @@ impl CardRow {
pub(crate) const CARD_ACCENT_WIDTH: usize = 2;
pub(crate) fn agent_card_style(_write_enabled: Option<bool>) -> CardStyle {
// Agent batches should share the calmer green theme regardless of write access so
// mixed read/write runs stay visually consistent.
pub(crate) fn agent_card_style(write_enabled: Option<bool>) -> CardStyle {
// Agent batches share the calmer green theme in full-color terminals.
// In ANSI-16 mode we swap to solid backgrounds for legibility (green for read,
// yellow when write mode is enabled).
let is_dark = is_dark_theme_active();
let definition = if is_dark {
card_theme::agent_read_only_dark_theme()
} else {
card_theme::agent_read_only_light_theme()
};
style_from_theme(definition, is_dark)
let mut style = style_from_theme(definition, is_dark);
if palette_mode() == PaletteMode::Ansi16 {
let bg = if write_enabled == Some(true) {
if is_dark {
Color::Yellow
} else {
Color::LightYellow
}
} else if is_dark {
Color::Green
} else {
Color::LightGreen
};
apply_ansi16_background(&mut style, bg, is_dark);
}
style
}
pub(crate) fn browser_card_style() -> CardStyle {
@@ -79,7 +99,14 @@ pub(crate) fn browser_card_style() -> CardStyle {
} else {
card_theme::browser_light_theme()
};
style_from_theme(definition, is_dark)
let mut style = style_from_theme(definition, is_dark);
if palette_mode() == PaletteMode::Ansi16 {
let bg = if is_dark { Color::Red } else { Color::LightRed };
apply_ansi16_background(&mut style, bg, is_dark);
}
style
}
pub(crate) fn auto_drive_card_style() -> CardStyle {
@@ -89,7 +116,28 @@ pub(crate) fn auto_drive_card_style() -> CardStyle {
} else {
card_theme::auto_drive_light_theme()
};
style_from_theme(definition, is_dark)
let mut style = style_from_theme(definition, is_dark);
if palette_mode() == PaletteMode::Ansi16 {
let bg = if is_dark { Color::Magenta } else { Color::LightMagenta };
apply_ansi16_background(&mut style, bg, is_dark);
}
style
}
fn apply_ansi16_background(style: &mut CardStyle, background: Color, is_dark: bool) {
style.gradient = GradientSpec {
left: background,
right: background,
bias: 0.0,
};
let text = if is_dark { Color::White } else { Color::Black };
style.accent_fg = text;
style.text_primary = text;
style.text_secondary = text;
style.title_text = text;
}
pub(crate) fn web_search_card_style() -> CardStyle {
@@ -152,7 +200,21 @@ fn adjust_gradient(gradient: GradientSpec, _is_dark: bool) -> GradientSpec {
}
pub(crate) fn fill_card_background(buf: &mut Buffer, area: Rect, style: &CardStyle) {
GradientBackground::render(buf, area, &style.gradient, style.text_primary, None);
if palette_mode() == PaletteMode::Ansi16
&& style.gradient.left == style.gradient.right
&& !matches!(style.gradient.left, Color::Rgb(_, _, _))
{
fill_rect(
buf,
area,
Some(' '),
Style::default()
.bg(style.gradient.left)
.fg(style.text_primary),
);
} else {
GradientBackground::render(buf, area, &style.gradient, style.text_primary, None);
}
}
pub(crate) fn pad_icon(icon: &str, width: usize) -> String {
+2
View File
@@ -99,6 +99,8 @@ typically start multiple agents. They require a task/problem description.
- `/demo`: populate the chat history with assorted sample cells (available in
dev and perf builds for UI testing).
- `/demo auto drive card`: render the Auto Drive card once for each ANSI-16
background color so you can compare theme contrast.
- `/test-approval`: test approval request (available in debug builds only).
Implementation Notes