From 147137c1f4118175d3f8db92f2f33faf9c14f6d8 Mon Sep 17 00:00:00 2001 From: Ian MacLeod Date: Fri, 4 Sep 2026 19:45:14 +0000 Subject: [PATCH] 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 --- codex-rs/config/src/types.rs | 5 + codex-rs/core/config.schema.json | 5 + codex-rs/core/src/config/config_tests.rs | 2 + codex-rs/core/src/config/mod.rs | 4 + codex-rs/thread-manager-sample/src/main.rs | 1 + codex-rs/tui/src/bottom_pane/chat_composer.rs | 11 + ...parkle__tests__astra_current_composer.snap | 9 + .../src/bottom_pane/chat_composer/sparkle.rs | 138 ++++++++ .../chat_composer/sparkle_tests.rs | 314 ++++++++++++++++++ codex-rs/tui/src/chatwidget/constructor.rs | 4 + codex-rs/tui/src/chatwidget/settings.rs | 2 + codex-rs/tui/src/local_settings.rs | 1 + codex-rs/tui/src/local_settings_tests.rs | 2 + 13 files changed, 498 insertions(+) create mode 100644 codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__astra_current_composer.snap create mode 100644 codex-rs/tui/src/bottom_pane/chat_composer/sparkle.rs create mode 100644 codex-rs/tui/src/bottom_pane/chat_composer/sparkle_tests.rs diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index d0ed440f59..c668ee4e48 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -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")] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 2cfc7297ef..8be0a2f62a 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -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" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 83d1ec5e79..d84d47f1a2 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -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, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 0ae1b06cb1..a19fcc86d8 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -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 diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index e696ae1b22..461c2a4759 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -225,6 +225,7 @@ fn new_config(model: Option, 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(), diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 709db71cac..620cc161eb 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -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, effort_animation_style: Option, effort_ignition: Option, + astra_sparkle: Option, effort_status_line_transition: Option, 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, + ); + } } } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__astra_current_composer.snap b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__astra_current_composer.snap new file mode 100644 index 0000000000..da0f1cb3c2 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__sparkle__tests__astra_current_composer.snap @@ -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 diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/sparkle.rs b/codex-rs/tui/src/bottom_pane/chat_composer/sparkle.rs new file mode 100644 index 0000000000..8df42d97ef --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/chat_composer/sparkle.rs @@ -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 = 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; diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/sparkle_tests.rs b/codex-rs/tui/src/bottom_pane/chat_composer/sparkle_tests.rs new file mode 100644 index 0000000000..83a82902ea --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/chat_composer/sparkle_tests.rs @@ -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::() + }) + .collect::>() + .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> = + 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::(); + 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())) + ); + }, + ); +} diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index d6852cbbd0..945d85496a 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -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 } diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index cd6b08ab32..374d30d9b1 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -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(); diff --git a/codex-rs/tui/src/local_settings.rs b/codex-rs/tui/src/local_settings.rs index 4a593350a0..41c6099650 100644 --- a/codex-rs/tui/src/local_settings.rs +++ b/codex-rs/tui/src/local_settings.rs @@ -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), diff --git a/codex-rs/tui/src/local_settings_tests.rs b/codex-rs/tui/src/local_settings_tests.rs index f676f8dbd5..6205fa6847 100644 --- a/codex-rs/tui/src/local_settings_tests.rs +++ b/codex-rs/tui/src/local_settings_tests.rs @@ -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;