mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Add independent controls for TUI visual effects (#46832)
## What changed Replace `tui.whimsy` with `[tui.effects]` preferences for `starfield`, `shimmer`, `welcome`, `effort`, `progress`, and `title`. Each defaults to `true` and remains subordinate to the `tui.animations` master switch. Wire the preferences into composer effects, welcome artwork, status and loading text, progress indicators, and terminal-title animations. For example, disable shimmering text while keeping progress animations: ```toml [tui.effects] shimmer = false ``` Keep retired `tui.whimsy` entries loadable but ignore their value. Update the configuration schema to expose the new preferences. ## Testing Add and extend coverage for independent shimmer and progress effects, master-switch precedence, welcome and starfield controls, terminal-title blinking, and strict configuration loading with the retired setting. GitOrigin-RevId: 28b37ba52872133b8954543cabe9ff424147f1c5
This commit is contained in:
@@ -42,6 +42,7 @@ mod state;
|
||||
mod strict_config;
|
||||
pub mod test_support;
|
||||
mod thread_config;
|
||||
mod tui_effects;
|
||||
mod tui_keymap;
|
||||
pub mod types;
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//! Individual TUI effect preferences, subordinate to the animation master switch.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Optional visual effects. Disabling an effect preserves its underlying activity.
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct TuiEffects {
|
||||
/// Animate the composer starfield.
|
||||
pub starfield: bool,
|
||||
/// Shimmer status and loading text.
|
||||
pub shimmer: bool,
|
||||
/// Animate the welcome artwork.
|
||||
pub welcome: bool,
|
||||
/// Animate reasoning-effort changes in the composer and footer.
|
||||
pub effort: bool,
|
||||
/// Animate activity bullets and loading spinners.
|
||||
pub progress: bool,
|
||||
/// Blink the terminal-title indicator when user action is required.
|
||||
pub title: bool,
|
||||
}
|
||||
|
||||
impl Default for TuiEffects {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
starfield: true,
|
||||
shimmer: true,
|
||||
welcome: true,
|
||||
effort: true,
|
||||
progress: true,
|
||||
title: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub use crate::tui_effects::TuiEffects;
|
||||
pub use crate::tui_keymap::KeybindingSpec;
|
||||
pub use crate::tui_keymap::KeybindingsSpec;
|
||||
pub use crate::tui_keymap::MAX_FUNCTION_KEY;
|
||||
@@ -753,10 +754,9 @@ pub struct Tui {
|
||||
/// Records the one-time screen-reader detection attempt. Either value skips detection.
|
||||
pub screen_reader_detection_done: Option<bool>,
|
||||
|
||||
/// Enable decorative effects such as Astra composer stars. Also requires animations.
|
||||
/// Defaults to `true`.
|
||||
#[serde(default = "default_true")]
|
||||
pub whimsy: bool,
|
||||
/// Individual visual effects. Each also requires animations to be enabled.
|
||||
#[serde(default)]
|
||||
pub effects: TuiEffects,
|
||||
|
||||
/// Show startup tooltips in the TUI welcome screen.
|
||||
/// Defaults to `true`.
|
||||
|
||||
@@ -4210,6 +4210,22 @@
|
||||
"description": "When true, disables burst-paste detection for typed input entirely. All characters are inserted as they are received, and no buffering or placeholder replacement will occur for fast keypress bursts. Overrides the legacy top-level `disable_paste_burst` setting. Defaults to `false`.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"effects": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TuiEffects"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"effort": true,
|
||||
"progress": true,
|
||||
"shimmer": true,
|
||||
"starfield": true,
|
||||
"title": true,
|
||||
"welcome": true
|
||||
},
|
||||
"description": "Individual visual effects. Each also requires animations to be enabled."
|
||||
},
|
||||
"keymap": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4527,11 +4543,6 @@
|
||||
"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"
|
||||
@@ -4971,6 +4982,43 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TuiEffects": {
|
||||
"additionalProperties": false,
|
||||
"description": "Optional visual effects. Disabling an effect preserves its underlying activity.",
|
||||
"properties": {
|
||||
"effort": {
|
||||
"default": true,
|
||||
"description": "Animate reasoning-effort changes in the composer and footer.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"progress": {
|
||||
"default": true,
|
||||
"description": "Animate activity bullets and loading spinners.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"shimmer": {
|
||||
"default": true,
|
||||
"description": "Shimmer status and loading text.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"starfield": {
|
||||
"default": true,
|
||||
"description": "Animate the composer starfield.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"title": {
|
||||
"default": true,
|
||||
"description": "Blink the terminal-title indicator when user action is required.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"welcome": {
|
||||
"default": true,
|
||||
"description": "Animate the welcome artwork.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"TuiGlobalKeymap": {
|
||||
"additionalProperties": false,
|
||||
"description": "Global keybindings. These are used when a context does not define an override.",
|
||||
|
||||
@@ -1253,7 +1253,7 @@ fn config_toml_deserializes_model_availability_nux() {
|
||||
notification_settings: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
screen_reader_detection_done: None,
|
||||
whimsy: true,
|
||||
effects: Default::default(),
|
||||
show_tooltips: true,
|
||||
show_server_version_notice: true,
|
||||
auto_recap: true,
|
||||
@@ -4380,7 +4380,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
|
||||
notification_settings: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
screen_reader_detection_done: None,
|
||||
whimsy: true,
|
||||
effects: Default::default(),
|
||||
show_tooltips: true,
|
||||
show_server_version_notice: true,
|
||||
auto_recap: true,
|
||||
|
||||
@@ -748,8 +748,8 @@ 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,
|
||||
/// Individual TUI effects, subordinate to the animation master switch.
|
||||
pub tui_effects: codex_config::types::TuiEffects,
|
||||
|
||||
/// Show startup tooltips in the TUI welcome screen.
|
||||
pub show_tooltips: bool,
|
||||
@@ -4382,7 +4382,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),
|
||||
tui_effects: cfg.tui.as_ref().map(|t| t.effects).unwrap_or_default(),
|
||||
show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true),
|
||||
tui_show_server_version_notice: cfg
|
||||
.tui
|
||||
|
||||
@@ -227,7 +227,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
notify: None,
|
||||
tui_notifications: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
tui_whimsy: true,
|
||||
tui_effects: Default::default(),
|
||||
show_tooltips: true,
|
||||
tui_show_server_version_notice: true,
|
||||
tui_auto_recap: true,
|
||||
|
||||
@@ -261,6 +261,14 @@ async fn empty_state_animation_preserves_header_draft_cursor_and_footer() -> Res
|
||||
let disabled = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal);
|
||||
assert_eq!(disabled, &before);
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.effects.welcome = false;
|
||||
draw(&mut app, &mut tui, size)?;
|
||||
assert_eq!(
|
||||
crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal),
|
||||
&before
|
||||
);
|
||||
app.local_settings.tui.effects.welcome = true;
|
||||
app.local_settings.tui.effects.shimmer = false;
|
||||
draw(&mut app, &mut tui, size)?;
|
||||
assert!(has_logo(
|
||||
crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal)
|
||||
|
||||
@@ -43,9 +43,16 @@ impl App {
|
||||
screen_size: Size,
|
||||
) -> Result<Rect> {
|
||||
self.chat_widget.sync_warnings(&self.transcript_cells);
|
||||
let motion = MotionMode::from_animations_enabled(self.local_settings.tui.animations);
|
||||
let motion = MotionMode::from_animations_enabled(
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.shimmer,
|
||||
);
|
||||
let focused = tui.is_terminal_focused();
|
||||
let logo = self.empty_state_presentation(motion, focused);
|
||||
let logo = self.empty_state_presentation(
|
||||
MotionMode::from_animations_enabled(
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.welcome,
|
||||
),
|
||||
focused,
|
||||
);
|
||||
let latest_navigation = if self.enter_returns_to_latest() {
|
||||
"enter/esc latest"
|
||||
} else {
|
||||
|
||||
@@ -59,7 +59,7 @@ async fn review_regression_agents_overview_creation_is_fresh_but_returning_is_no
|
||||
trust_launch_folder(&mut app);
|
||||
app.cli_kv_overrides.extend([
|
||||
("tui.animations".into(), TomlValue::Boolean(true)),
|
||||
("tui.whimsy".into(), TomlValue::Boolean(true)),
|
||||
("tui.effects.starfield".into(), TomlValue::Boolean(true)),
|
||||
]);
|
||||
app.harness_overrides.model = Some(model.into());
|
||||
let mut server = start_config_write_test_app_server(&app).await?;
|
||||
|
||||
@@ -95,7 +95,7 @@ async fn only_a_confirmed_empty_new_task_shows_the_sparkle() -> Result<()> {
|
||||
] {
|
||||
let (mut app, _events, _ops) = make_test_app_with_channels().await;
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.whimsy = true;
|
||||
app.local_settings.tui.effects.starfield = true;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
let mut thread = started("gpt-6-astra");
|
||||
let presentation = if scenario == "empty_resume" {
|
||||
@@ -142,7 +142,7 @@ async fn disconnected_sparkle_input_routed_by_app_consumes_the_opportunity_witho
|
||||
for edited in [false, true] {
|
||||
let (mut app, _events, _ops) = make_test_app_with_channels().await;
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.whimsy = true;
|
||||
app.local_settings.tui.effects.starfield = true;
|
||||
let mut server = start_config_write_test_app_server(&app).await?;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
app.replace_chat_widget_with_app_server_thread(
|
||||
@@ -197,7 +197,7 @@ async fn commands_can_precede_the_sparkle_but_inserted_or_typed_drafts_cannot()
|
||||
] {
|
||||
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.whimsy = true;
|
||||
app.local_settings.tui.effects.starfield = true;
|
||||
app.local_settings.tui.disable_paste_burst = Some(true);
|
||||
let mut server = start_config_write_test_app_server(&app).await?;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
@@ -281,7 +281,7 @@ async fn commands_can_precede_the_sparkle_but_inserted_or_typed_drafts_cannot()
|
||||
async fn overlays_shortcuts_and_key_chords_leave_the_main_sparkle_untouched() -> Result<()> {
|
||||
let (mut app, _events, _ops) = make_test_app_with_channels().await;
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.whimsy = true;
|
||||
app.local_settings.tui.effects.starfield = true;
|
||||
let mut server = start_config_write_test_app_server(&app).await?;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
app.replace_chat_widget_with_app_server_thread(
|
||||
@@ -346,7 +346,7 @@ async fn a_command_pending_on_fresh_astra_start_can_finish_before_the_sparkle()
|
||||
for (command, visible_after) in [("/status", true), ("/mention", false)] {
|
||||
let (mut app, _events, _ops) = make_test_app_with_channels().await;
|
||||
app.chat_widget.local_settings.tui.animations = true;
|
||||
app.chat_widget.local_settings.tui.whimsy = true;
|
||||
app.chat_widget.local_settings.tui.effects.starfield = true;
|
||||
app.pending_startup_thread_start = true;
|
||||
app.chat_widget.handle_paste(command.into());
|
||||
let mut server = crate::start_embedded_app_server_for_picker(&app.config).await?;
|
||||
@@ -378,7 +378,7 @@ async fn only_confirmed_picker_model_changes_can_arm_the_sparkle() -> Result<()>
|
||||
] {
|
||||
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.whimsy = true;
|
||||
app.local_settings.tui.effects.starfield = true;
|
||||
app.local_settings.tui.disable_paste_burst = Some(true);
|
||||
let mut server = start_config_write_test_app_server(&app).await?;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
@@ -489,7 +489,7 @@ async fn astra_picker_confirms_the_model_at_application_after_an_automatic_updat
|
||||
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
|
||||
app.config.model = Some("gpt-5.5".into());
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.whimsy = true;
|
||||
app.local_settings.tui.effects.starfield = true;
|
||||
let mut server = start_config_write_test_app_server(&app).await?;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
let thread = server.start_thread(&app.config).await?;
|
||||
@@ -577,7 +577,7 @@ async fn session_only_astra_picker_shows_stars_only_on_an_untouched_task() -> Re
|
||||
app.config.model = Some("gpt-5.5".into());
|
||||
app.config.model_reasoning_effort = Some(ReasoningEffortConfig::Medium);
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.whimsy = true;
|
||||
app.local_settings.tui.effects.starfield = true;
|
||||
app.local_settings.tui.disable_paste_burst = Some(true);
|
||||
let config_path = app.config.codex_home.join("config.toml");
|
||||
let defaults = "model = 'gpt-5.5'\nmodel_reasoning_effort = 'medium'\n";
|
||||
@@ -665,7 +665,7 @@ async fn session_only_astra_picker_shows_stars_only_on_an_untouched_task() -> Re
|
||||
async fn a_read_only_command_can_return_to_active_stars_but_mention_ends_them() -> Result<()> {
|
||||
let (mut app, _events, _ops) = make_test_app_with_channels().await;
|
||||
app.local_settings.tui.animations = true;
|
||||
app.local_settings.tui.whimsy = true;
|
||||
app.local_settings.tui.effects.starfield = true;
|
||||
app.local_settings.tui.disable_paste_burst = Some(true);
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
app.replace_chat_widget_with_app_server_thread(
|
||||
@@ -695,7 +695,7 @@ async fn early_input_and_real_work_consume_the_sparkle() -> Result<()> {
|
||||
for scenario in ["untouched", "typed_then_erased", "paste", "turn", "review"] {
|
||||
let (mut app, _events, _ops) = make_test_app_with_channels().await;
|
||||
app.chat_widget.local_settings.tui.animations = true;
|
||||
app.chat_widget.local_settings.tui.whimsy = true;
|
||||
app.chat_widget.local_settings.tui.effects.starfield = true;
|
||||
app.pending_startup_thread_start = true;
|
||||
match scenario {
|
||||
"typed_then_erased" => {
|
||||
|
||||
@@ -197,6 +197,7 @@ fn startup_bottom_pane() -> (BottomPane, UnboundedReceiver<AppEvent>) {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: true,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: None,
|
||||
}),
|
||||
app_event_rx,
|
||||
|
||||
@@ -363,7 +363,7 @@ impl App {
|
||||
{
|
||||
tui.set_overlay_input(tui::OverlayInput::Transcript)?;
|
||||
t.motion = crate::motion::MotionMode::from_animations_enabled(
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.shimmer,
|
||||
);
|
||||
let active_key = self.chat_widget.active_cell_transcript_key();
|
||||
let chat_widget = &self.chat_widget;
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
//! focus loss hide the field without restarting that deadline. Rendering runs after the textarea,
|
||||
//! placeholder, effort ignition, and voice strip, and draws only in eligible blank cells without
|
||||
//! overwriting the placeholder or normal cursor. Hidden frames do not schedule animation redraws;
|
||||
//! motion settings, whimsy settings, and true-color support also gate the effect.
|
||||
//! motion settings, the starfield preference, and true-color support also gate the effect.
|
||||
//!
|
||||
//! # Large Paste Placeholders
|
||||
//!
|
||||
|
||||
@@ -30,6 +30,7 @@ fn draft_and_voice_composer_snapshots() {
|
||||
speaker_history: vec![255, 146, 110, 73, 37, 0],
|
||||
activity: "listening",
|
||||
animations: false,
|
||||
progress: true,
|
||||
}),
|
||||
FrameRequester::test_dummy(),
|
||||
);
|
||||
|
||||
@@ -97,7 +97,7 @@ impl BottomPane {
|
||||
SparkleDraft::Untouched | SparkleDraft::Command
|
||||
) && self.composer.sparkle.phase.get() == Phase::Unarmed
|
||||
&& settings.animations
|
||||
&& settings.whimsy
|
||||
&& settings.effects.starfield
|
||||
&& Self::is_sparkle_model(model)
|
||||
{
|
||||
self.composer.sparkle.phase.set(Phase::Waiting);
|
||||
@@ -112,7 +112,7 @@ impl BottomPane {
|
||||
}
|
||||
|
||||
pub(crate) fn stop_ineligible_sparkle(&self, model: &str, settings: &Tui) {
|
||||
if !settings.animations || !settings.whimsy || !Self::is_sparkle_model(model) {
|
||||
if !settings.animations || !settings.effects.starfield || !Self::is_sparkle_model(model) {
|
||||
self.composer.stop_visible_sparkle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ fn pane() -> BottomPane {
|
||||
placeholder_text: "Ask Codex to do anything".into(),
|
||||
disable_paste_burst: true,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: None,
|
||||
})
|
||||
}
|
||||
@@ -43,7 +44,7 @@ fn palette<T>(render: impl FnOnce() -> T) -> T {
|
||||
fn enabled() -> Tui {
|
||||
Tui {
|
||||
animations: true,
|
||||
whimsy: true,
|
||||
effects: Default::default(),
|
||||
..Tui::default()
|
||||
}
|
||||
}
|
||||
@@ -1055,7 +1056,10 @@ fn focus_startup_work_and_configuration_do_not_extend_a_started_deadline() {
|
||||
|
||||
for settings in [
|
||||
Tui {
|
||||
whimsy: false,
|
||||
effects: codex_config::types::TuiEffects {
|
||||
starfield: false,
|
||||
..Default::default()
|
||||
},
|
||||
..enabled()
|
||||
},
|
||||
Tui {
|
||||
|
||||
@@ -291,6 +291,7 @@ pub(crate) struct BottomPane {
|
||||
is_task_running: bool,
|
||||
esc_backtrack_hint: bool,
|
||||
animations_enabled: bool,
|
||||
effects: codex_config::types::TuiEffects,
|
||||
|
||||
/// Inline status indicator shown above the composer while a task is running.
|
||||
status: Option<StatusIndicatorWidget>,
|
||||
@@ -321,6 +322,7 @@ pub(crate) struct BottomPaneParams {
|
||||
pub(crate) placeholder_text: String,
|
||||
pub(crate) disable_paste_burst: bool,
|
||||
pub(crate) animations_enabled: bool,
|
||||
pub(crate) effects: codex_config::types::TuiEffects,
|
||||
pub(crate) skills: Option<Vec<SkillMetadata>>,
|
||||
}
|
||||
|
||||
@@ -342,6 +344,7 @@ impl BottomPane {
|
||||
placeholder_text,
|
||||
disable_paste_burst,
|
||||
animations_enabled,
|
||||
effects,
|
||||
skills,
|
||||
} = params;
|
||||
let mut composer = ChatComposer::new_with_config(
|
||||
@@ -379,6 +382,7 @@ impl BottomPane {
|
||||
pending_thread_approvals: PendingThreadApprovals::new(),
|
||||
esc_backtrack_hint: false,
|
||||
animations_enabled,
|
||||
effects,
|
||||
context_window_percent: None,
|
||||
context_window_used_tokens: None,
|
||||
keymap,
|
||||
@@ -402,7 +406,7 @@ impl BottomPane {
|
||||
/// visible frame can play a one-shot Max/Ultra effect.
|
||||
pub(crate) fn set_active_reasoning_effort(&mut self, effort: Option<&ReasoningEffort>) {
|
||||
let animations_enabled = effort_ignition::effort_animation_enabled(
|
||||
self.animations_enabled,
|
||||
self.animations_enabled && self.effects.effort,
|
||||
effective_stdout_color_level(),
|
||||
);
|
||||
if self
|
||||
@@ -1283,6 +1287,7 @@ impl BottomPane {
|
||||
self.app_event_tx.clone(),
|
||||
self.frame_requester.clone(),
|
||||
self.animations_enabled,
|
||||
self.effects,
|
||||
));
|
||||
}
|
||||
if let Some(status) = self.status.as_mut() {
|
||||
@@ -1319,6 +1324,7 @@ impl BottomPane {
|
||||
self.app_event_tx.clone(),
|
||||
self.frame_requester.clone(),
|
||||
self.animations_enabled,
|
||||
self.effects,
|
||||
)
|
||||
});
|
||||
if let Some(status) = self.status.as_mut() {
|
||||
@@ -2107,7 +2113,7 @@ impl BottomPane {
|
||||
/*flex*/ 0,
|
||||
RenderableItem::Owned(Box::new(hook_status::HookStatus {
|
||||
message,
|
||||
animations_enabled: self.animations_enabled,
|
||||
animations_enabled: self.animations_enabled && self.effects.shimmer,
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -2362,6 +2368,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
})
|
||||
}
|
||||
@@ -2623,6 +2630,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: true,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
pane.push_approval_request(exec_request(), &features);
|
||||
@@ -2643,6 +2651,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: true,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
pane.insert_str("draft");
|
||||
@@ -2676,6 +2685,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3001,6 +3011,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3068,6 +3079,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3142,6 +3154,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3173,6 +3186,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3196,6 +3210,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3225,6 +3240,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3262,6 +3278,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3294,6 +3311,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3325,6 +3343,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3354,6 +3373,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3377,6 +3397,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(vec![SkillMetadata {
|
||||
name: "test-skill".to_string(),
|
||||
description: "test skill".to_string(),
|
||||
@@ -3425,6 +3446,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3493,6 +3515,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3529,6 +3552,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3577,6 +3601,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3702,6 +3727,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3750,6 +3776,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -3828,6 +3855,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ pub(crate) struct VoiceStripState {
|
||||
pub(crate) speaker_history: Vec<u8>,
|
||||
pub(crate) activity: &'static str,
|
||||
pub(crate) animations: bool,
|
||||
pub(crate) progress: bool,
|
||||
}
|
||||
|
||||
pub(super) struct VoiceStrip {
|
||||
@@ -84,7 +85,8 @@ impl Renderable for VoiceStrip {
|
||||
return;
|
||||
}
|
||||
let connecting = self.state.phase == VoiceStripPhase::Connecting;
|
||||
let mode = MotionMode::from_animations_enabled(self.state.animations);
|
||||
let mode =
|
||||
MotionMode::from_animations_enabled(self.state.animations && self.state.progress);
|
||||
if connecting && mode == MotionMode::Animated {
|
||||
self.frame_requester
|
||||
.schedule_frame_in(Duration::from_millis(100));
|
||||
|
||||
@@ -20,6 +20,7 @@ fn active_strip() -> VoiceStrip {
|
||||
speaker_history: vec![0, 37, 73, 110, 146, 219],
|
||||
activity: "listening",
|
||||
animations: true,
|
||||
progress: true,
|
||||
},
|
||||
FrameRequester::test_dummy(),
|
||||
)
|
||||
|
||||
@@ -1625,7 +1625,7 @@ impl ChatWidget {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.flush_active_cell();
|
||||
self.transcript.active_cell = Some(Box::new(history_cell::new_mcp_inventory_loading(
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
)));
|
||||
self.bump_active_cell_revision();
|
||||
self.request_redraw();
|
||||
|
||||
@@ -303,7 +303,7 @@ impl ChatWidget {
|
||||
parsed_cmd,
|
||||
source,
|
||||
/*interaction_input*/ None,
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
)));
|
||||
self.bump_active_cell_revision();
|
||||
}
|
||||
@@ -441,7 +441,7 @@ impl ChatWidget {
|
||||
parsed,
|
||||
source,
|
||||
/*interaction_input*/ None,
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
);
|
||||
let completed = orphan.complete_call(&id, output, duration);
|
||||
debug_assert!(completed, "new orphan exec cell should contain {id}");
|
||||
@@ -456,7 +456,7 @@ impl ChatWidget {
|
||||
parsed,
|
||||
source,
|
||||
/*interaction_input*/ None,
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
);
|
||||
let completed = cell.complete_call(&id, output, duration);
|
||||
debug_assert!(completed, "new exec cell should contain {id}");
|
||||
|
||||
@@ -110,6 +110,7 @@ impl ChatWidget {
|
||||
placeholder_text: placeholder.clone(),
|
||||
disable_paste_burst: local_settings.tui.disable_paste_burst.unwrap_or(false),
|
||||
animations_enabled: local_settings.tui.animations,
|
||||
effects: local_settings.tui.effects,
|
||||
skills: None,
|
||||
}),
|
||||
transcript: TranscriptState::new(active_cell),
|
||||
|
||||
@@ -128,6 +128,7 @@ impl ChatWidget {
|
||||
|
||||
pub(super) fn schedule_hook_timer_if_needed(&self) {
|
||||
if self.local_settings.tui.animations
|
||||
&& self.local_settings.tui.effects.shimmer
|
||||
&& self
|
||||
.active_hook_cell
|
||||
.as_ref()
|
||||
|
||||
@@ -343,7 +343,7 @@ impl ChatWidget {
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.shimmer,
|
||||
"Loading available plugins...".to_string(),
|
||||
Some("This updates when the marketplace list is ready.".to_string()),
|
||||
)),
|
||||
@@ -362,7 +362,7 @@ impl ChatWidget {
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.shimmer,
|
||||
"Adding marketplace...".to_string(),
|
||||
/*note*/ None,
|
||||
)),
|
||||
@@ -480,7 +480,7 @@ impl ChatWidget {
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.shimmer,
|
||||
loading_text.clone(),
|
||||
/*note*/ None,
|
||||
)),
|
||||
@@ -502,7 +502,7 @@ impl ChatWidget {
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.shimmer,
|
||||
format!("Loading details for {plugin_display_name}..."),
|
||||
/*note*/ None,
|
||||
)),
|
||||
|
||||
@@ -293,6 +293,7 @@ impl ChatWidget {
|
||||
.collect(),
|
||||
activity,
|
||||
animations: self.local_settings.tui.animations,
|
||||
progress: self.local_settings.tui.effects.progress,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ impl ChatWidget {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.flush_active_cell();
|
||||
self.transcript.active_cell = Some(Box::new(history_cell::ThreadRecapLoadingCell::new(
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
)));
|
||||
self.bump_active_cell_revision();
|
||||
self.request_redraw();
|
||||
|
||||
@@ -368,7 +368,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn action_required_terminal_title_prefix_at(&self, now: Instant) -> &'static str {
|
||||
if !self.local_settings.tui.animations {
|
||||
if !(self.local_settings.tui.animations && self.local_settings.tui.effects.title) {
|
||||
return TERMINAL_TITLE_ACTION_REQUIRED_PREFIX;
|
||||
}
|
||||
|
||||
@@ -396,6 +396,7 @@ impl ChatWidget {
|
||||
selections: &StatusSurfaceSelections,
|
||||
) -> Option<Duration> {
|
||||
if self.local_settings.tui.animations
|
||||
&& self.local_settings.tui.effects.progress
|
||||
&& self.status_state.thread_title_generation_pending
|
||||
&& (selections.status_line_items.iter().any(|item| {
|
||||
matches!(
|
||||
@@ -416,6 +417,7 @@ impl ChatWidget {
|
||||
return Some(TERMINAL_TITLE_SPINNER_INTERVAL);
|
||||
}
|
||||
if self.local_settings.tui.animations
|
||||
&& self.local_settings.tui.effects.title
|
||||
&& self.terminal_title_shows_action_required_with_selections(selections)
|
||||
{
|
||||
return Some(TERMINAL_TITLE_ACTION_REQUIRED_INTERVAL);
|
||||
@@ -1008,6 +1010,7 @@ impl ChatWidget {
|
||||
|
||||
pub(super) fn terminal_title_spinner_text_at(&self, now: Instant) -> Option<String> {
|
||||
let spinner = (self.local_settings.tui.animations
|
||||
&& self.local_settings.tui.effects.progress
|
||||
&& self.terminal_title_has_active_progress())
|
||||
.then(|| self.terminal_title_spinner_frame_at(now));
|
||||
if self.realtime_microphone_is_listening() {
|
||||
@@ -1049,12 +1052,15 @@ impl ChatWidget {
|
||||
|
||||
pub(super) fn should_animate_terminal_title_spinner(&self) -> bool {
|
||||
self.local_settings.tui.animations
|
||||
&& self.local_settings.tui.effects.progress
|
||||
&& self.terminal_title_uses_activity()
|
||||
&& self.terminal_title_has_active_progress()
|
||||
}
|
||||
|
||||
pub(super) fn should_animate_terminal_title_action_required(&self) -> bool {
|
||||
self.local_settings.tui.animations && self.terminal_title_shows_action_required()
|
||||
self.local_settings.tui.animations
|
||||
&& self.local_settings.tui.effects.title
|
||||
&& self.terminal_title_shows_action_required()
|
||||
}
|
||||
|
||||
fn should_animate_terminal_title_spinner_with_selections(
|
||||
@@ -1062,6 +1068,7 @@ impl ChatWidget {
|
||||
selections: &StatusSurfaceSelections,
|
||||
) -> bool {
|
||||
self.local_settings.tui.animations
|
||||
&& self.local_settings.tui.effects.progress
|
||||
&& selections
|
||||
.terminal_title_items
|
||||
.contains(&TerminalTitleItem::Spinner)
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn ordinary_and_prepared_image_submissions_consume_the_sparkle_before_rend
|
||||
let (mut chat, _events, mut ops) = make_chatwidget_manual(Some("gpt-6-astra")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.local_settings.tui.animations = true;
|
||||
chat.local_settings.tui.whimsy = true;
|
||||
chat.local_settings.tui.effects.starfield = true;
|
||||
chat.bottom_pane
|
||||
.mark_fresh_task_for_sparkle("gpt-6-astra", &chat.local_settings.tui);
|
||||
let untouched = chat.bottom_pane.composer_draft_snapshot().sparkle_draft;
|
||||
|
||||
@@ -172,7 +172,12 @@ async fn terminal_title_action_required_respects_spinner_setting() {
|
||||
#[tokio::test]
|
||||
async fn terminal_title_action_required_blinks_when_animations_are_enabled() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.local_settings.tui.effects.title = false;
|
||||
chat.bottom_pane.set_task_running(/*running*/ true);
|
||||
assert!(chat.should_animate_terminal_title_spinner());
|
||||
chat.local_settings.tui.effects.progress = false;
|
||||
assert!(!chat.should_animate_terminal_title_spinner());
|
||||
chat.local_settings.tui.effects.title = true;
|
||||
chat.refresh_terminal_title();
|
||||
|
||||
let request = ExecApprovalRequestEvent {
|
||||
@@ -201,6 +206,13 @@ async fn terminal_title_action_required_blinks_when_animations_are_enabled() {
|
||||
Some("[ . ] Action Required | project".to_string())
|
||||
);
|
||||
assert!(chat.should_animate_terminal_title_action_required());
|
||||
chat.local_settings.tui.effects.title = false;
|
||||
chat.refresh_terminal_title();
|
||||
assert_eq!(
|
||||
chat.last_terminal_title,
|
||||
Some("[ ! ] Action Required | project".into())
|
||||
);
|
||||
assert!(!chat.should_animate_terminal_title_action_required());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -37,11 +37,12 @@ impl ChatWidget {
|
||||
if !self.status_state.thread_title_generation_pending {
|
||||
return value;
|
||||
}
|
||||
let spinner = if self.local_settings.tui.animations {
|
||||
self.terminal_title_spinner_frame_at(now)
|
||||
} else {
|
||||
TERMINAL_TITLE_SPINNER_FRAMES[0]
|
||||
};
|
||||
let spinner =
|
||||
if self.local_settings.tui.animations && self.local_settings.tui.effects.progress {
|
||||
self.terminal_title_spinner_frame_at(now)
|
||||
} else {
|
||||
TERMINAL_TITLE_SPINNER_FRAMES[0]
|
||||
};
|
||||
Some(match value {
|
||||
Some(value) => format!("{value} {spinner}"),
|
||||
None => spinner.to_string(),
|
||||
@@ -51,6 +52,7 @@ impl ChatWidget {
|
||||
pub(crate) fn refresh_thread_title_progress_for_time_tick(&mut self) {
|
||||
if self.status_state.thread_title_generation_pending
|
||||
&& self.local_settings.tui.animations
|
||||
&& self.local_settings.tui.effects.progress
|
||||
&& self.configured_status_line_items().iter().any(|item| {
|
||||
matches!(
|
||||
item.parse::<StatusLineItem>(),
|
||||
|
||||
@@ -72,7 +72,7 @@ impl ChatWidget {
|
||||
self.transcript.active_cell = Some(Box::new(history_cell::new_active_web_search_call(
|
||||
call_id,
|
||||
String::new(),
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
)));
|
||||
self.bump_active_cell_revision();
|
||||
self.request_redraw();
|
||||
@@ -180,7 +180,7 @@ impl ChatWidget {
|
||||
let call = history_cell::new_active_mcp_tool_call(
|
||||
id,
|
||||
invocation,
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
);
|
||||
self.update_computer_activity(|cell| cell.start(call));
|
||||
self.bump_active_cell_revision();
|
||||
@@ -191,7 +191,7 @@ impl ChatWidget {
|
||||
self.transcript.active_cell = Some(Box::new(history_cell::new_active_mcp_tool_call(
|
||||
id,
|
||||
invocation,
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
)));
|
||||
self.bump_active_cell_revision();
|
||||
self.request_redraw();
|
||||
@@ -214,7 +214,7 @@ impl ChatWidget {
|
||||
let call = history_cell::new_active_mcp_tool_call(
|
||||
id,
|
||||
invocation,
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
);
|
||||
self.update_computer_activity(|cell| cell.complete(call, duration, result));
|
||||
self.bump_active_cell_revision();
|
||||
@@ -234,7 +234,7 @@ impl ChatWidget {
|
||||
let mut cell = history_cell::new_active_mcp_tool_call(
|
||||
id,
|
||||
invocation,
|
||||
self.local_settings.tui.animations,
|
||||
self.local_settings.tui.animations && self.local_settings.tui.effects.progress,
|
||||
);
|
||||
cell.complete(duration, result);
|
||||
self.transcript.active_cell = Some(Box::new(cell));
|
||||
|
||||
@@ -789,6 +789,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: false,
|
||||
effects: Default::default(),
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
(pane, tx, rx)
|
||||
|
||||
@@ -63,7 +63,7 @@ impl LocalSettings {
|
||||
notification_settings: config.tui_notifications.clone(),
|
||||
animations: animations && system_motion == crate::motion::MotionMode::Animated,
|
||||
screen_reader_detection_done: None,
|
||||
whimsy: config.tui_whimsy,
|
||||
effects: config.tui_effects,
|
||||
show_tooltips: config.show_tooltips,
|
||||
show_server_version_notice: config.tui_show_server_version_notice,
|
||||
auto_recap: config.tui_auto_recap,
|
||||
|
||||
@@ -54,7 +54,7 @@ async fn system_motion_suppresses_animations_without_changing_saved_preferences(
|
||||
|
||||
for configured in [true, false] {
|
||||
let home = tempfile::tempdir()?;
|
||||
let config_text = format!("[tui]\nanimations = {configured}\nwhimsy = true\n");
|
||||
let config_text = format!("[tui]\nanimations = {configured}\n");
|
||||
std::fs::write(home.path().join("config.toml"), &config_text)?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
@@ -94,13 +94,15 @@ async fn local_load_preserves_defaults_and_resolved_overrides() -> anyhow::Resul
|
||||
r#"
|
||||
[tui]
|
||||
animations = false
|
||||
whimsy = false
|
||||
whimsy = false # Retired: must not override effects or prevent strict loading.
|
||||
show_tooltips = false
|
||||
show_server_version_notice = false
|
||||
auto_recap = false
|
||||
vim_mode_default = true
|
||||
terminal_resize_reflow_max_rows = 0
|
||||
session_picker_view = "comfortable"
|
||||
[tui.effects]
|
||||
shimmer = false
|
||||
[history]
|
||||
persistence = "none"
|
||||
max_bytes = 4096
|
||||
@@ -112,6 +114,7 @@ fast_default_opt_out = true
|
||||
std::fs::write(home.path().join("config.toml"), config_text)?;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(home.path().to_path_buf())
|
||||
.strict_config(true)
|
||||
.loader_overrides(LoaderOverrides {
|
||||
ignore_project_config: true,
|
||||
..LoaderOverrides::without_managed_config_for_tests()
|
||||
@@ -119,13 +122,14 @@ fast_default_opt_out = true
|
||||
.cli_overrides(vec![("tui.disable_paste_burst".into(), true.into())])
|
||||
.build()
|
||||
.await?;
|
||||
assert_eq!(config.startup_warnings, Vec::<String>::new());
|
||||
let local = LocalSettings::from(&config);
|
||||
let mut expected: Tui = toml::from_str("")?;
|
||||
expected.disable_paste_burst = Some(true);
|
||||
expected.session_picker_view = Some(SessionPickerViewMode::Dense);
|
||||
if !config_text.is_empty() {
|
||||
expected.animations = false;
|
||||
expected.whimsy = false;
|
||||
expected.effects.shimmer = false;
|
||||
expected.show_tooltips = false;
|
||||
expected.show_server_version_notice = false;
|
||||
expected.auto_recap = false;
|
||||
|
||||
@@ -136,7 +136,7 @@ impl OnboardingScreen {
|
||||
steps.push(Step::Welcome(WelcomeWidget::new(
|
||||
!matches!(login_status, LoginStatus::NotAuthenticated),
|
||||
tui.frame_requester(),
|
||||
local_settings.tui.animations,
|
||||
local_settings.tui.animations && local_settings.tui.effects.welcome,
|
||||
)));
|
||||
#[cfg(target_os = "windows")]
|
||||
let show_windows_create_sandbox_hint = if show_trust_screen
|
||||
@@ -171,7 +171,8 @@ impl OnboardingScreen {
|
||||
app_server_request_handle,
|
||||
auth_config,
|
||||
bedrock_setup_enabled,
|
||||
animations_enabled: local_settings.tui.animations,
|
||||
animations_enabled: local_settings.tui.animations
|
||||
&& local_settings.tui.effects.shimmer,
|
||||
animations_suppressed: std::cell::Cell::new(false),
|
||||
}));
|
||||
} else {
|
||||
|
||||
@@ -129,8 +129,12 @@ async fn persisted_default_loads_and_renders_without_animation() -> anyhow::Resu
|
||||
assert!(!config.animations);
|
||||
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let (frame_requester, mut frames) = FrameRequester::test_channel();
|
||||
let mut status =
|
||||
StatusIndicatorWidget::new(AppEventSender::new(tx), frame_requester, config.animations);
|
||||
let mut status = StatusIndicatorWidget::new(
|
||||
AppEventSender::new(tx),
|
||||
frame_requester,
|
||||
config.animations,
|
||||
Default::default(),
|
||||
);
|
||||
status.update_header("Reading the terminal output".into());
|
||||
let mut timer = StatusTimer::default();
|
||||
timer.pause_at(std::time::Instant::now());
|
||||
|
||||
@@ -505,6 +505,7 @@ fn startup_draft_bottom_pane(
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: crate::system_motion::mode() == crate::motion::MotionMode::Animated,
|
||||
effects: Default::default(),
|
||||
skills: None,
|
||||
},
|
||||
ChatComposerConfig::plain_text(),
|
||||
|
||||
@@ -68,6 +68,7 @@ pub(crate) struct StatusIndicatorWidget {
|
||||
app_event_tx: AppEventSender,
|
||||
frame_requester: FrameRequester,
|
||||
animations_enabled: bool,
|
||||
effects: codex_config::types::TuiEffects,
|
||||
}
|
||||
|
||||
// Format elapsed seconds into a compact human-friendly form used by the status line.
|
||||
@@ -92,6 +93,7 @@ impl StatusIndicatorWidget {
|
||||
app_event_tx: AppEventSender,
|
||||
frame_requester: FrameRequester,
|
||||
animations_enabled: bool,
|
||||
effects: codex_config::types::TuiEffects,
|
||||
) -> Self {
|
||||
Self {
|
||||
header: String::from("Working"),
|
||||
@@ -105,6 +107,7 @@ impl StatusIndicatorWidget {
|
||||
app_event_tx,
|
||||
frame_requester,
|
||||
animations_enabled,
|
||||
effects,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,12 +227,15 @@ impl StatusIndicator<'_> {
|
||||
|started_at| now.saturating_duration_since(started_at),
|
||||
);
|
||||
let pretty_elapsed = fmt_elapsed_compact(elapsed_duration.as_secs());
|
||||
let motion_mode = MotionMode::from_animations_enabled(row.animations_enabled);
|
||||
let progress =
|
||||
MotionMode::from_animations_enabled(row.animations_enabled && row.effects.progress);
|
||||
let shimmer =
|
||||
MotionMode::from_animations_enabled(row.animations_enabled && row.effects.shimmer);
|
||||
|
||||
let mut spans = Vec::with_capacity(5);
|
||||
if let Some(indicator) = activity_indicator(
|
||||
Some(self.timer.last_resume_at),
|
||||
motion_mode,
|
||||
progress,
|
||||
ReducedMotionIndicator::Hidden,
|
||||
) {
|
||||
spans.push(indicator);
|
||||
@@ -238,7 +244,7 @@ impl StatusIndicator<'_> {
|
||||
spans.extend(summary_shimmer(
|
||||
&row.header,
|
||||
now.saturating_duration_since(row.header_started_at),
|
||||
motion_mode,
|
||||
shimmer,
|
||||
));
|
||||
if !spans.is_empty() {
|
||||
spans.push(" ".into());
|
||||
@@ -294,7 +300,9 @@ impl Renderable for StatusIndicator<'_> {
|
||||
return;
|
||||
}
|
||||
if self.row.animations_enabled || self.timer.display_started_at.is_some() {
|
||||
let interval_ms = if self.row.animations_enabled {
|
||||
let interval_ms = if self.row.animations_enabled
|
||||
&& (self.row.effects.progress || self.row.effects.shimmer)
|
||||
{
|
||||
32
|
||||
} else {
|
||||
1_000
|
||||
@@ -325,6 +333,7 @@ mod tests {
|
||||
AppEventSender::new(tx),
|
||||
FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ true,
|
||||
Default::default(),
|
||||
);
|
||||
let previous = Instant::now() - Duration::from_secs(/*secs*/ 1);
|
||||
row.header_started_at = previous;
|
||||
@@ -357,6 +366,7 @@ mod tests {
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ true,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
// Render into a fixed-size test terminal and snapshot the backend.
|
||||
@@ -376,6 +386,7 @@ mod tests {
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ true,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
// Render into a fixed-size test terminal and snapshot the backend.
|
||||
@@ -394,6 +405,7 @@ mod tests {
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ false,
|
||||
Default::default(),
|
||||
);
|
||||
w.update_details(
|
||||
Some("A man a plan a canal panama".to_string()),
|
||||
@@ -423,6 +435,7 @@ mod tests {
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ false,
|
||||
Default::default(),
|
||||
);
|
||||
let mut timer = StatusTimer::default();
|
||||
timer.pause_at(timer.last_resume_at);
|
||||
@@ -447,6 +460,7 @@ mod tests {
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ false,
|
||||
Default::default(),
|
||||
);
|
||||
w.set_interrupt_binding(Some(key_hint::plain(KeyCode::F(12)).into()));
|
||||
let mut timer = StatusTimer::default();
|
||||
@@ -466,6 +480,7 @@ mod tests {
|
||||
AppEventSender::new(tx),
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ false,
|
||||
Default::default(),
|
||||
);
|
||||
let mut timer = StatusTimer::default();
|
||||
timer.pause_at(timer.last_resume_at);
|
||||
@@ -513,6 +528,7 @@ mod tests {
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ true,
|
||||
Default::default(),
|
||||
);
|
||||
w.update_details(
|
||||
Some("abcd abcd abcd abcd".to_string()),
|
||||
@@ -537,6 +553,7 @@ mod tests {
|
||||
tx,
|
||||
crate::tui::FrameRequester::test_dummy(),
|
||||
/*animations_enabled*/ true,
|
||||
Default::default(),
|
||||
);
|
||||
w.update_details(
|
||||
Some("cargo test -p codex-core and then cargo test -p codex-tui".to_string()),
|
||||
@@ -560,3 +577,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "status_indicator_widget/effects_tests.rs"]
|
||||
mod effects_tests;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//! Independent status effects and master-switch precedence.
|
||||
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::terminal_palette::with_test_default_colors;
|
||||
use crate::terminal_probe::DefaultColors;
|
||||
|
||||
#[test]
|
||||
fn shimmer_and_progress_are_independent_and_obey_master_switch() {
|
||||
with_test_default_colors(
|
||||
DefaultColors {
|
||||
fg: (240, 240, 240),
|
||||
bg: (16, 16, 16),
|
||||
},
|
||||
|| {
|
||||
let mut snapshots = Vec::new();
|
||||
for (animations, shimmer, progress) in [
|
||||
(true, false, false),
|
||||
(true, false, true),
|
||||
(true, true, false),
|
||||
(false, true, true),
|
||||
] {
|
||||
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
|
||||
let mut row = StatusIndicatorWidget::new(
|
||||
AppEventSender::new(tx),
|
||||
FrameRequester::test_dummy(),
|
||||
animations,
|
||||
codex_config::types::TuiEffects {
|
||||
shimmer,
|
||||
progress,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// Pin the label timing and fallback bullet glyph to the first frame.
|
||||
let start = Instant::now() + Duration::from_secs(/*secs*/ 3600);
|
||||
row.header_started_at = start;
|
||||
let timer = StatusTimer {
|
||||
last_resume_at: start,
|
||||
..Default::default()
|
||||
};
|
||||
let lines = StatusIndicator {
|
||||
row: &row,
|
||||
timer: &timer,
|
||||
}
|
||||
.lines(/*width*/ 80);
|
||||
let header = &lines[0];
|
||||
// Omit the bullet's process-clock-driven styling from the snapshot.
|
||||
let label_start = if animations && progress { 2 } else { 0 };
|
||||
let label = &header.spans[label_start..];
|
||||
snapshots.push(format!(
|
||||
"animations={animations}, shimmer={shimmer}, progress={progress}\n{header}\n{label:?}"
|
||||
));
|
||||
}
|
||||
insta::assert_snapshot!(snapshots.join("\n\n"));
|
||||
},
|
||||
);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: tui/src/status_indicator_widget/effects_tests.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
animations=true, shimmer=false, progress=false
|
||||
Working (0s • esc to interrupt)
|
||||
[Span::from("Working"), Span::from(" "), Span::from("(0s • ").dim(), Span::from("esc").fg(Color::Rgb(240, 240, 240)).bold().not_dim(), Span::from(" to interrupt)").dim()]
|
||||
|
||||
animations=true, shimmer=false, progress=true
|
||||
• Working (0s • esc to interrupt)
|
||||
[Span::from("Working"), Span::from(" "), Span::from("(0s • ").dim(), Span::from("esc").fg(Color::Rgb(240, 240, 240)).bold().not_dim(), Span::from(" to interrupt)").dim()]
|
||||
|
||||
animations=true, shimmer=true, progress=false
|
||||
Working (0s • esc to interrupt)
|
||||
[Span::from("W").fg(Color::Rgb(128, 128, 128)), Span::from("o").fg(Color::Rgb(128, 128, 128)), Span::from("r").fg(Color::Rgb(128, 128, 128)), Span::from("k").fg(Color::Rgb(128, 128, 128)), Span::from("i").fg(Color::Rgb(128, 128, 128)), Span::from("n").fg(Color::Rgb(128, 128, 128)), Span::from("g").fg(Color::Rgb(128, 128, 128)), Span::from(" "), Span::from("(0s • ").dim(), Span::from("esc").fg(Color::Rgb(240, 240, 240)).bold().not_dim(), Span::from(" to interrupt)").dim()]
|
||||
|
||||
animations=false, shimmer=true, progress=true
|
||||
Working (0s • esc to interrupt)
|
||||
[Span::from("Working"), Span::from(" "), Span::from("(0s • ").dim(), Span::from("esc").fg(Color::Rgb(240, 240, 240)).bold().not_dim(), Span::from(" to interrupt)").dim()]
|
||||
Reference in New Issue
Block a user