mirror of
https://github.com/openai/codex.git
synced 2026-10-21 07:42:01 +00:00
Add Astra sparkle effects to the TUI composer (#42842)
## What changed - Render sparse, fading stars across the composer when using an Astra model in a true-color terminal. - Preserve composer content, cursor state, effort effects, and terminal colors, and pause the animation while popups are open. - Add the default-on `tui.whimsy` setting so decorative effects can be disabled independently. Sparkles also respect `tui.animations`. ## Testing - Cover model and setting eligibility, terminal color handling, protected composer cells, popup behavior, effort effects, and layout stability. GitOrigin-RevId: fe0471ebef0bca21c44e1d8f731d46959eba0165
This commit is contained in:
@@ -731,6 +731,11 @@ pub struct Tui {
|
||||
#[serde(default = "default_true")]
|
||||
pub animations: bool,
|
||||
|
||||
/// Enable decorative effects such as Astra composer stars. Also requires animations.
|
||||
/// Defaults to `true`.
|
||||
#[serde(default = "default_true")]
|
||||
pub whimsy: bool,
|
||||
|
||||
/// Show startup tooltips in the TUI welcome screen.
|
||||
/// Defaults to `true`.
|
||||
#[serde(default = "default_true")]
|
||||
|
||||
@@ -4150,6 +4150,11 @@
|
||||
"default": false,
|
||||
"description": "Start the composer in Vim mode (`Normal`) by default. Defaults to `false`.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"whimsy": {
|
||||
"default": true,
|
||||
"description": "Enable decorative effects such as Astra composer stars. Also requires animations. Defaults to `true`.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -1256,6 +1256,7 @@ fn config_toml_deserializes_model_availability_nux() {
|
||||
Tui {
|
||||
notification_settings: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
whimsy: true,
|
||||
show_tooltips: true,
|
||||
auto_recap: true,
|
||||
disable_paste_burst: None,
|
||||
@@ -4269,6 +4270,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
|
||||
Tui {
|
||||
notification_settings: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
whimsy: true,
|
||||
show_tooltips: true,
|
||||
auto_recap: true,
|
||||
disable_paste_burst: None,
|
||||
|
||||
@@ -740,6 +740,9 @@ pub struct Config {
|
||||
/// Enable ASCII animations and shimmer effects in the TUI.
|
||||
pub animations: bool,
|
||||
|
||||
/// Enable decorative TUI effects such as Astra composer stars.
|
||||
pub tui_whimsy: bool,
|
||||
|
||||
/// Show startup tooltips in the TUI welcome screen.
|
||||
pub show_tooltips: bool,
|
||||
|
||||
@@ -4343,6 +4346,7 @@ impl Config {
|
||||
.map(|t| t.notification_settings.clone())
|
||||
.unwrap_or_default(),
|
||||
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
|
||||
tui_whimsy: cfg.tui.as_ref().map(|t| t.whimsy).unwrap_or(true),
|
||||
show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true),
|
||||
tui_auto_recap: cfg.tui.as_ref().map(|t| t.auto_recap).unwrap_or(/*default*/ true),
|
||||
model_availability_nux: cfg
|
||||
|
||||
@@ -225,6 +225,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
notify: None,
|
||||
tui_notifications: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
tui_whimsy: true,
|
||||
show_tooltips: true,
|
||||
tui_auto_recap: true,
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
|
||||
@@ -316,6 +316,7 @@ mod history_search;
|
||||
mod popup_state;
|
||||
mod reconnect;
|
||||
mod slash_input;
|
||||
mod sparkle;
|
||||
mod vim_history;
|
||||
mod vim_search;
|
||||
|
||||
@@ -526,6 +527,7 @@ pub(crate) struct ChatComposer {
|
||||
effort_tier: Option<EffortTier>,
|
||||
effort_animation_style: Option<IgnitionStyle>,
|
||||
effort_ignition: Option<EffortIgnition>,
|
||||
astra_sparkle: Option<sparkle::Sparkle>,
|
||||
effort_status_line_transition: Option<EffortStatusLineTransition>,
|
||||
effort_observed: bool,
|
||||
luna_reserve_active: bool,
|
||||
@@ -706,6 +708,7 @@ impl ChatComposer {
|
||||
effort_tier: None,
|
||||
effort_animation_style: None,
|
||||
effort_ignition: None,
|
||||
astra_sparkle: None,
|
||||
effort_status_line_transition: None,
|
||||
effort_observed: false,
|
||||
luna_reserve_active: false,
|
||||
@@ -5063,6 +5066,14 @@ impl ChatComposer {
|
||||
frame_requester.schedule_frame_in(IGNITION_FRAME_TICK);
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
if self.astra_sparkle.is_some() {
|
||||
self.render_sparkle(
|
||||
composer_rect,
|
||||
self.cursor_pos_with_textarea_right_reserve(area, textarea_right_reserve),
|
||||
buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer/sparkle_tests.rs
|
||||
assertion_line: 48
|
||||
expression: rows
|
||||
---
|
||||
|
||||
› Explore the night sky
|
||||
|
||||
100% context left
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Sparse Astra stars on the existing composer surface, fading with the terminal's colors.
|
||||
//! `enabled_foreground` owns eligibility, including late-arriving terminal colors.
|
||||
//! Rendering owns frame scheduling, so hidden composers do not keep animating.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use codex_config::types::Tui;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::buffer::CellDiffOption;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use regex_lite::Regex;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::ChatComposer;
|
||||
use super::popup_state::ActivePopup;
|
||||
use crate::bottom_pane::BottomPane;
|
||||
use crate::color::blend;
|
||||
use crate::terminal_palette::StdoutColorLevel;
|
||||
use crate::terminal_palette::default_fg;
|
||||
use crate::terminal_palette::effective_stdout_color_level;
|
||||
use crate::terminal_palette::rgb_color;
|
||||
|
||||
const FRAME_TICK: Duration = Duration::from_millis(150);
|
||||
const DOTS: [&str; 8] = ["⠁", "⠂", "⠄", "⠈", "⠐", "⠠", "⡀", "⢀"];
|
||||
static ASTRA_MODEL: LazyLock<Regex> = LazyLock::new(|| match Regex::new(r"(?i)\bastra\b") {
|
||||
Ok(regex) => regex,
|
||||
Err(error) => panic!("invalid Astra model regex: {error}"),
|
||||
});
|
||||
|
||||
pub(super) struct Sparkle {
|
||||
model: String,
|
||||
whimsy: bool,
|
||||
animations: bool,
|
||||
started: Instant,
|
||||
}
|
||||
|
||||
impl Sparkle {
|
||||
fn enabled_foreground(&self) -> Option<(u8, u8, u8)> {
|
||||
if !self.whimsy
|
||||
|| !self.animations
|
||||
|| !ASTRA_MODEL.is_match(&self.model)
|
||||
|| effective_stdout_color_level() != StdoutColorLevel::TrueColor
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Read the cached palette here: Windows may populate it after widget creation.
|
||||
default_fg()
|
||||
}
|
||||
}
|
||||
|
||||
impl BottomPane {
|
||||
pub(crate) fn set_astra_sparkle(&mut self, model: &str, settings: &Tui) {
|
||||
let started = self
|
||||
.composer
|
||||
.astra_sparkle
|
||||
.as_ref()
|
||||
.map_or_else(Instant::now, |sparkle| sparkle.started);
|
||||
self.composer.astra_sparkle = Some(Sparkle {
|
||||
model: model.to_owned(),
|
||||
whimsy: settings.whimsy,
|
||||
animations: settings.animations,
|
||||
started,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatComposer {
|
||||
pub(super) fn render_sparkle(&self, area: Rect, cursor: Option<(u16, u16)>, buf: &mut Buffer) {
|
||||
if area.is_empty() || !matches!(self.popups.active, ActivePopup::None) {
|
||||
return;
|
||||
}
|
||||
if let Some(sparkle) = &self.astra_sparkle
|
||||
&& let Some(foreground) = sparkle.enabled_foreground()
|
||||
{
|
||||
render_stars(area, cursor, sparkle.started.elapsed(), foreground, buf);
|
||||
if let Some(requester) = &self.frame_requester {
|
||||
requester.schedule_frame_in(FRAME_TICK);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_stars(
|
||||
area: Rect,
|
||||
cursor: Option<(u16, u16)>,
|
||||
elapsed: Duration,
|
||||
foreground: (u8, u8, u8),
|
||||
buf: &mut Buffer,
|
||||
) {
|
||||
let time = elapsed.as_secs_f32();
|
||||
for y in area.y..area.bottom() {
|
||||
let mut occupied_until = area.x;
|
||||
for x in area.x..area.right() {
|
||||
let cell = &buf[(x, y)];
|
||||
if x < occupied_until {
|
||||
continue;
|
||||
}
|
||||
if cell.symbol() != " " {
|
||||
occupied_until = x.saturating_add(cell.symbol().width() as u16);
|
||||
continue;
|
||||
}
|
||||
// Preserve the cursor, selections, and pixels from the effort bursts.
|
||||
if cursor == Some((x, y))
|
||||
|| !cell.modifier.is_empty()
|
||||
|| cell.diff_option != CellDiffOption::None
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Color::Rgb(r, g, b) = cell.bg else {
|
||||
continue;
|
||||
};
|
||||
// A stable coordinate hash gives each star its own dot, period, and phase.
|
||||
let mut hash = u64::from(y - area.y) * 65537 + u64::from(x - area.x);
|
||||
hash = (hash ^ (hash >> 16)).wrapping_mul(0x45d9f3b);
|
||||
hash = (hash ^ (hash >> 16)).wrapping_mul(0x45d9f3b);
|
||||
hash ^= hash >> 16;
|
||||
if hash % 5 != 0 {
|
||||
continue;
|
||||
}
|
||||
let phase =
|
||||
(time / (4.0 + (hash % 31) as f32 / 10.0) + (hash % 997) as f32 / 997.0).fract();
|
||||
let brightness = (phase * std::f32::consts::PI).sin().powi(12) * 0.55;
|
||||
if brightness < 0.04 {
|
||||
continue;
|
||||
}
|
||||
buf[(x, y)]
|
||||
.set_symbol(DOTS[(hash / 161 % 8) as usize])
|
||||
.set_fg(rgb_color(blend(foreground, (r, g, b), brightness)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sparkle_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,314 @@
|
||||
use super::super::tests::new_test_composer;
|
||||
use super::*;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::terminal_palette::with_test_default_colors;
|
||||
use crate::terminal_probe::DefaultColors;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::style::Style;
|
||||
|
||||
fn new_test_sparkle() -> Sparkle {
|
||||
Sparkle {
|
||||
model: "astra".to_string(),
|
||||
whimsy: true,
|
||||
animations: true,
|
||||
started: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparkle_keeps_the_existing_composer_layout() {
|
||||
with_test_default_colors(
|
||||
DefaultColors {
|
||||
fg: (230, 216, 255),
|
||||
bg: (36, 27, 53),
|
||||
},
|
||||
|| {
|
||||
let (mut composer, _rx) = new_test_composer();
|
||||
composer.set_text_content("Explore the night sky".to_string(), Vec::new(), Vec::new());
|
||||
let area = Rect::new(
|
||||
/*x*/ 0,
|
||||
/*y*/ 0,
|
||||
/*width*/ 60,
|
||||
composer.desired_height(/*width*/ 60),
|
||||
);
|
||||
let [surface, _, _, _] = composer.layout_areas(area);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
composer.render(area, &mut buffer);
|
||||
render_stars(
|
||||
surface,
|
||||
composer.cursor_pos(area),
|
||||
Duration::ZERO,
|
||||
/*foreground*/ (230, 216, 255),
|
||||
&mut buffer,
|
||||
);
|
||||
// Snapshot the composer layout without freezing the star distribution.
|
||||
let rows = (area.y..area.bottom())
|
||||
.map(|y| {
|
||||
(area.x..area.right())
|
||||
.map(|x| {
|
||||
let symbol = buffer[(x, y)].symbol();
|
||||
if DOTS.contains(&symbol) { " " } else { symbol }
|
||||
})
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
insta::assert_snapshot!("astra_current_composer", rows);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparkle_preserves_content_cursor_and_background() {
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 3,
|
||||
);
|
||||
let mut before = Buffer::empty(area);
|
||||
before.set_style(area, Style::default().bg(rgb_color((36, 27, 53))));
|
||||
before[(0, 0)].set_symbol("界");
|
||||
before[(2, 0)].set_symbol("!");
|
||||
before[(4, 0)].set_symbol("✦");
|
||||
before[(5, 0)].set_style(Style::default().reversed());
|
||||
before[(6, 0)].set_diff_option(CellDiffOption::Skip);
|
||||
let protected = [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0)];
|
||||
let mut seen = std::collections::HashMap::new();
|
||||
for tick in 0..80 {
|
||||
let mut after = before.clone();
|
||||
render_stars(
|
||||
area,
|
||||
Some((3, 0)),
|
||||
FRAME_TICK * tick,
|
||||
/*foreground*/ (230, 216, 255),
|
||||
&mut after,
|
||||
);
|
||||
assert_eq!(
|
||||
protected.map(|p| after[p].clone()),
|
||||
protected.map(|p| before[p].clone())
|
||||
);
|
||||
for (index, cell) in after.content.iter().enumerate() {
|
||||
assert_eq!(cell.bg, before.content[index].bg);
|
||||
if DOTS.contains(&cell.symbol())
|
||||
&& let Some(previous) = seen.insert(index, cell.symbol().to_string())
|
||||
{
|
||||
assert_eq!(cell.symbol(), previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(!seen.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stars_fade_using_the_custom_terminal_foreground() {
|
||||
for colors in [
|
||||
DefaultColors {
|
||||
fg: (230, 216, 255),
|
||||
bg: (36, 27, 53),
|
||||
},
|
||||
DefaultColors {
|
||||
fg: (101, 123, 131),
|
||||
bg: (253, 246, 227),
|
||||
},
|
||||
] {
|
||||
with_test_default_colors(colors, || {
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 3,
|
||||
);
|
||||
let mut shades: std::collections::HashMap<usize, std::collections::HashSet<Color>> =
|
||||
std::collections::HashMap::new();
|
||||
for tick in 0..40 {
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_style(area, Style::default().bg(rgb_color(colors.bg)));
|
||||
render_stars(
|
||||
area,
|
||||
/*cursor*/ None,
|
||||
FRAME_TICK * tick,
|
||||
colors.fg,
|
||||
&mut buffer,
|
||||
);
|
||||
for (index, cell) in buffer.content.iter().enumerate() {
|
||||
if DOTS.contains(&cell.symbol()) {
|
||||
let Color::Rgb(r, g, b) = cell.fg else {
|
||||
panic!("expected RGB fade")
|
||||
};
|
||||
shades.entry(index).or_default().insert(cell.fg);
|
||||
for (actual, fg, bg) in [
|
||||
(r, colors.fg.0, colors.bg.0),
|
||||
(g, colors.fg.1, colors.bg.1),
|
||||
(b, colors.fg.2, colors.bg.2),
|
||||
] {
|
||||
assert!((fg.min(bg)..=fg.max(bg)).contains(&actual));
|
||||
}
|
||||
assert_eq!(cell.bg, rgb_color(colors.bg));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(shades.values().any(|colors| colors.len() > 3));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_changes_and_disable_setting_control_sparkle() {
|
||||
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut pane = BottomPane::new(crate::bottom_pane::BottomPaneParams {
|
||||
app_event_tx: crate::app_event_sender::AppEventSender::new(tx),
|
||||
frame_requester: crate::tui::FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: String::new(),
|
||||
disable_paste_burst: true,
|
||||
animations_enabled: true,
|
||||
skills: None,
|
||||
});
|
||||
with_test_default_colors(
|
||||
DefaultColors {
|
||||
fg: (230, 216, 255),
|
||||
bg: (36, 27, 53),
|
||||
},
|
||||
|| {
|
||||
let mut settings = Tui {
|
||||
whimsy: true,
|
||||
animations: true,
|
||||
..Tui::default()
|
||||
};
|
||||
pane.set_astra_sparkle("gpt-6-astra", &settings);
|
||||
let started = pane
|
||||
.composer
|
||||
.astra_sparkle
|
||||
.as_ref()
|
||||
.map(|sparkle| sparkle.started);
|
||||
for model in ["astra", "ASTRA-preview", "openai/astra-2026-09-01"] {
|
||||
pane.set_astra_sparkle(model, &settings);
|
||||
assert_eq!(
|
||||
pane.composer
|
||||
.astra_sparkle
|
||||
.as_ref()
|
||||
.and_then(Sparkle::enabled_foreground),
|
||||
Some((230, 216, 255)),
|
||||
);
|
||||
assert_eq!(
|
||||
pane.composer
|
||||
.astra_sparkle
|
||||
.as_ref()
|
||||
.map(|sparkle| sparkle.started),
|
||||
started
|
||||
);
|
||||
}
|
||||
for model in [
|
||||
"gpt-5.6-sol",
|
||||
"astral",
|
||||
"castrated",
|
||||
"astra2",
|
||||
"astra_preview",
|
||||
] {
|
||||
pane.set_astra_sparkle(model, &settings);
|
||||
assert_eq!(
|
||||
pane.composer
|
||||
.astra_sparkle
|
||||
.as_ref()
|
||||
.and_then(Sparkle::enabled_foreground),
|
||||
None,
|
||||
"{model}",
|
||||
);
|
||||
}
|
||||
for (whimsy, animations) in [(false, true), (true, false), (false, false), (true, true)]
|
||||
{
|
||||
settings.whimsy = whimsy;
|
||||
settings.animations = animations;
|
||||
pane.set_astra_sparkle("astra", &settings);
|
||||
assert_eq!(
|
||||
pane.composer
|
||||
.astra_sparkle
|
||||
.as_ref()
|
||||
.and_then(Sparkle::enabled_foreground),
|
||||
(whimsy && animations).then_some((230, 216, 255)),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparkle_renders_with_effort_bursts_and_pauses_for_popups() {
|
||||
with_test_default_colors(
|
||||
DefaultColors {
|
||||
fg: (230, 216, 255),
|
||||
bg: (36, 27, 53),
|
||||
},
|
||||
|| {
|
||||
let (mut composer, _rx) = new_test_composer();
|
||||
composer.astra_sparkle = Some(new_test_sparkle());
|
||||
composer.set_text_content("hello 界".to_string(), Vec::new(), Vec::new());
|
||||
composer.set_active_reasoning_effort_baseline(Some(&ReasoningEffort::High));
|
||||
let area = Rect::new(
|
||||
/*x*/ 0,
|
||||
/*y*/ 0,
|
||||
/*width*/ 80,
|
||||
composer.desired_height(/*width*/ 80),
|
||||
);
|
||||
for effort in [ReasoningEffort::Max, ReasoningEffort::Ultra] {
|
||||
composer
|
||||
.set_active_reasoning_effort(Some(&effort), /*animations_enabled*/ true);
|
||||
let started = composer.astra_sparkle.take();
|
||||
let mut before = Buffer::empty(area);
|
||||
composer.render(area, &mut before);
|
||||
composer.astra_sparkle = started;
|
||||
let mut after = Buffer::empty(area);
|
||||
composer.render(area, &mut after);
|
||||
let text = after
|
||||
.content
|
||||
.iter()
|
||||
.map(ratatui::buffer::Cell::symbol)
|
||||
.collect::<String>();
|
||||
assert!(text.contains("hello 界"));
|
||||
assert!(
|
||||
after
|
||||
.content
|
||||
.iter()
|
||||
.any(|cell| DOTS.contains(&cell.symbol()))
|
||||
);
|
||||
assert_eq!(
|
||||
composer.cursor_pos(area).map(|p| after[p].clone()),
|
||||
composer.cursor_pos(area).map(|p| before[p].clone())
|
||||
);
|
||||
assert!(composer.effort_ignition.is_some());
|
||||
}
|
||||
composer.set_text_content("/mod".to_string(), Vec::new(), Vec::new());
|
||||
composer.draft.textarea.set_cursor(/*pos*/ 4);
|
||||
composer.sync_popups();
|
||||
assert!(!matches!(composer.popups.active, ActivePopup::None));
|
||||
let mut buffer = Buffer::empty(area);
|
||||
composer.render_sparkle(area, /*cursor*/ None, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::empty(area));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sparkle_waits_for_terminal_colors() {
|
||||
let (mut composer, _rx) = new_test_composer();
|
||||
composer.astra_sparkle = Some(new_test_sparkle());
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 3,
|
||||
);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
composer.render_sparkle(area, /*cursor*/ None, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::empty(area));
|
||||
with_test_default_colors(
|
||||
DefaultColors {
|
||||
fg: (230, 216, 255),
|
||||
bg: (36, 27, 53),
|
||||
},
|
||||
|| {
|
||||
buffer.set_style(area, Style::default().bg(rgb_color((36, 27, 53))));
|
||||
composer.render_sparkle(area, /*cursor*/ None, &mut buffer);
|
||||
assert!(
|
||||
buffer
|
||||
.content
|
||||
.iter()
|
||||
.any(|cell| DOTS.contains(&cell.symbol()))
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -309,6 +309,10 @@ impl ChatWidget {
|
||||
.bottom_pane
|
||||
.set_token_activity_command_enabled(widget.has_codex_backend_auth);
|
||||
widget.refresh_status_surfaces();
|
||||
widget.bottom_pane.set_astra_sparkle(
|
||||
widget.effective_collaboration_mode().model(),
|
||||
&widget.local_settings.tui,
|
||||
);
|
||||
|
||||
widget
|
||||
}
|
||||
|
||||
@@ -442,6 +442,8 @@ impl ChatWidget {
|
||||
pub(super) fn refresh_model_display(&mut self) {
|
||||
let effective = self.effective_collaboration_mode();
|
||||
self.session_header.set_model(effective.model());
|
||||
self.bottom_pane
|
||||
.set_astra_sparkle(effective.model(), &self.local_settings.tui);
|
||||
// Keep composer paste affordances aligned with the currently effective model.
|
||||
self.sync_image_paste_enabled();
|
||||
self.sync_service_tier_commands();
|
||||
|
||||
@@ -27,6 +27,7 @@ impl From<&Config> for LocalSettings {
|
||||
tui: Tui {
|
||||
notification_settings: config.tui_notifications.clone(),
|
||||
animations: config.animations,
|
||||
whimsy: config.tui_whimsy,
|
||||
show_tooltips: config.show_tooltips,
|
||||
auto_recap: config.tui_auto_recap,
|
||||
disable_paste_burst: Some(config.disable_paste_burst),
|
||||
|
||||
@@ -12,6 +12,7 @@ async fn local_load_preserves_defaults_and_resolved_overrides() -> anyhow::Resul
|
||||
r#"
|
||||
[tui]
|
||||
animations = false
|
||||
whimsy = false
|
||||
show_tooltips = false
|
||||
auto_recap = false
|
||||
vim_mode_default = true
|
||||
@@ -41,6 +42,7 @@ fast_default_opt_out = true
|
||||
expected.session_picker_view = Some(SessionPickerViewMode::Dense);
|
||||
if !config_text.is_empty() {
|
||||
expected.animations = false;
|
||||
expected.whimsy = false;
|
||||
expected.show_tooltips = false;
|
||||
expected.auto_recap = false;
|
||||
expected.vim_mode_default = true;
|
||||
|
||||
Reference in New Issue
Block a user