Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions codex-rs/tui/src/chatwidget/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ use crate::bottom_pane::custom_prompt_view::CustomPromptView;
use crate::history_cell;
use crate::key_hint;
use crate::legacy_core::config::Config;
use crate::motion::MotionMode;
use crate::motion::shimmer_text;
use crate::onboarding::mark_url_hyperlink;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use crate::shimmer::shimmer_spans;
use crate::tui::FrameRequester;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::MarketplaceRemoveResponse;
Expand Down Expand Up @@ -100,7 +101,10 @@ impl Renderable for DelayedLoadingHeader {
} else if self.animations_enabled {
self.frame_requester
.schedule_frame_in(LOADING_ANIMATION_INTERVAL);
lines.push(Line::from(shimmer_spans(self.loading_text.as_str())));
lines.push(Line::from(shimmer_text(
self.loading_text.as_str(),
MotionMode::Animated,
)));
} else {
lines.push(Line::from(self.loading_text.as_str().dim()));
}
Expand Down
1 change: 0 additions & 1 deletion codex-rs/tui/src/exec_cell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ pub(crate) use render::OutputLinesParams;
pub(crate) use render::TOOL_CALL_MAX_LINES;
pub(crate) use render::new_active_exec_command;
pub(crate) use render::output_lines;
pub(crate) use render::spinner;
58 changes: 41 additions & 17 deletions codex-rs/tui/src/exec_cell/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ use super::model::ExecCall;
use super::model::ExecCell;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::history_cell::HistoryCell;
use crate::motion::MotionMode;
use crate::motion::ReducedMotionIndicator;
use crate::motion::activity_indicator;
use crate::render::highlight::highlight_bash_to_lines;
use crate::render::line_utils::prefix_lines;
use crate::render::line_utils::push_owned_lines;
use crate::shimmer::shimmer_spans;
use crate::wrapping::RtOptions;
use crate::wrapping::adaptive_wrap_line;
use crate::wrapping::adaptive_wrap_lines;
Expand Down Expand Up @@ -180,20 +182,13 @@ pub(crate) fn output_lines(
}
}

pub(crate) fn spinner(start_time: Option<Instant>, animations_enabled: bool) -> Span<'static> {
if !animations_enabled {
return "•".dim();
}
let elapsed = start_time.map(|st| st.elapsed()).unwrap_or_default();
if supports_color::on_cached(supports_color::Stream::Stdout)
.map(|level| level.has_16m)
.unwrap_or(false)
{
shimmer_spans("•")[0].clone()
} else {
let blink_on = (elapsed.as_millis() / 600).is_multiple_of(2);
if blink_on { "•".into() } else { "◦".dim() }
}
fn activity_marker(start_time: Option<Instant>, animations_enabled: bool) -> Span<'static> {
activity_indicator(
start_time,
MotionMode::from_animations_enabled(animations_enabled),
ReducedMotionIndicator::StaticBullet,
)
.unwrap_or_else(|| "•".dim())
}

impl HistoryCell for ExecCell {
Expand Down Expand Up @@ -263,7 +258,7 @@ impl ExecCell {
let mut out: Vec<Line<'static>> = Vec::new();
out.push(Line::from(vec![
if self.is_active() {
spinner(self.active_start_time(), self.animations_enabled())
activity_marker(self.active_start_time(), self.animations_enabled())
} else {
"•".dim()
},
Expand Down Expand Up @@ -371,7 +366,7 @@ impl ExecCell {
let bullet = match success {
Some(true) => "•".green().bold(),
Some(false) => "•".red().bold(),
None => spinner(call.start_time, self.animations_enabled()),
None => activity_marker(call.start_time, self.animations_enabled()),
};
let is_interaction = call.is_unified_exec_interaction();
let title = if is_interaction {
Expand Down Expand Up @@ -957,6 +952,35 @@ mod tests {
);
}

#[test]
fn active_command_without_animations_is_stable() {
let call = ExecCall {
call_id: "call-id".to_string(),
command: vec!["bash".into(), "-lc".into(), "echo done".into()],
parsed: Vec::new(),
output: None,
source: ExecCommandSource::Agent,
start_time: Some(Instant::now()),
duration: None,
interaction_input: None,
};

let cell = ExecCell::new(call, /*animations_enabled*/ false);
let first: Vec<String> = cell
.command_display_lines(/*width*/ 80)
.iter()
.map(render_line_text)
.collect();
let second: Vec<String> = cell
.command_display_lines(/*width*/ 80)
.iter()
.map(render_line_text)
.collect();

assert_eq!(first, second);
assert_eq!(first, vec!["• Running echo done".to_string()]);
}

#[test]
fn exploring_display_does_not_split_long_url_like_search_query() {
let url_like = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/with/a/very/long/path";
Expand Down
35 changes: 31 additions & 4 deletions codex-rs/tui/src/history_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ use crate::exec_cell::CommandOutput;
use crate::exec_cell::OutputLinesParams;
use crate::exec_cell::TOOL_CALL_MAX_LINES;
use crate::exec_cell::output_lines;
use crate::exec_cell::spinner;
use crate::exec_command::relativize_to_home;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::legacy_core::config::Config;
use crate::live_wrap::take_prefix_by_width;
use crate::markdown::append_markdown;
use crate::motion::MotionMode;
use crate::motion::ReducedMotionIndicator;
use crate::motion::activity_indicator;
use crate::render::line_utils::line_to_static;
use crate::render::line_utils::prefix_lines;
use crate::render::line_utils::push_owned_lines;
Expand Down Expand Up @@ -1668,7 +1670,12 @@ impl HistoryCell for McpToolCallCell {
let bullet = match status {
Some(true) => "•".green().bold(),
Some(false) => "•".red().bold(),
None => spinner(Some(self.start_time), self.animations_enabled),
None => activity_indicator(
Some(self.start_time),
MotionMode::from_animations_enabled(self.animations_enabled),
ReducedMotionIndicator::StaticBullet,
)
.unwrap_or_else(|| "•".dim()),
};
let header_text = if status.is_some() {
"Called"
Expand Down Expand Up @@ -1858,7 +1865,12 @@ impl HistoryCell for WebSearchCell {
let bullet = if self.completed {
"•".dim()
} else {
spinner(Some(self.start_time), self.animations_enabled)
activity_indicator(
Some(self.start_time),
MotionMode::from_animations_enabled(self.animations_enabled),
ReducedMotionIndicator::StaticBullet,
)
.unwrap_or_else(|| "•".dim())
};
let header = web_search_header(self.completed);
let detail = web_search_detail(self.action.as_ref(), &self.query);
Expand Down Expand Up @@ -2468,7 +2480,12 @@ impl HistoryCell for McpInventoryLoadingCell {
fn display_lines(&self, _width: u16) -> Vec<Line<'static>> {
vec![
vec![
spinner(Some(self.start_time), self.animations_enabled),
activity_indicator(
Some(self.start_time),
MotionMode::from_animations_enabled(self.animations_enabled),
ReducedMotionIndicator::StaticBullet,
)
.unwrap_or_else(|| "•".dim()),
" ".into(),
"Loading MCP inventory".bold(),
"…".dim(),
Expand Down Expand Up @@ -3966,6 +3983,16 @@ mod tests {
insta::assert_snapshot!(rendered);
}

#[test]
fn mcp_inventory_loading_without_animations_is_stable() {
let cell = new_mcp_inventory_loading(/*animations_enabled*/ false);
let first = render_lines(&cell.display_lines(/*width*/ 80));
let second = render_lines(&cell.display_lines(/*width*/ 80));

assert_eq!(first, second);
assert_eq!(first, vec!["• Loading MCP inventory…".to_string()]);
}

#[test]
fn completed_mcp_tool_call_success_snapshot() {
let invocation = McpInvocation {
Expand Down
48 changes: 41 additions & 7 deletions codex-rs/tui/src/history_cell/hook_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
//! first drawn.
//! 4. Completed runs only persist when they have output or a non-success status.
use super::HistoryCell;
use crate::exec_cell::spinner;
use crate::motion::MotionMode;
use crate::motion::ReducedMotionIndicator;
use crate::motion::activity_indicator;
use crate::motion::shimmer_text;
use crate::render::renderable::Renderable;
use crate::shimmer::shimmer_spans;
use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookOutputEntry;
use codex_app_server_protocol::HookOutputEntryKind;
Expand Down Expand Up @@ -626,11 +628,17 @@ fn push_running_hook_header(
status_message: Option<&str>,
animations_enabled: bool,
) {
let mut header = vec![spinner(start_time, animations_enabled), " ".into()];
if animations_enabled {
header.extend(shimmer_spans(hook_text));
} else {
header.push(hook_text.to_string().bold());
let mut header = Vec::new();
let motion_mode = MotionMode::from_animations_enabled(animations_enabled);
if let Some(indicator) =
activity_indicator(start_time, motion_mode, ReducedMotionIndicator::Hidden)
{
header.push(indicator);
header.push(" ".into());
}
header.extend(shimmer_text(hook_text, motion_mode));
if !animations_enabled && let Some(span) = header.last_mut() {
span.style = span.style.patch(Style::default().bold());
}
if let Some(status_message) = status_message
&& !status_message.is_empty()
Expand Down Expand Up @@ -761,6 +769,32 @@ mod tests {
assert_eq!(cell.transcript_animation_tick(), None);
}

#[test]
fn visible_hook_without_animations_omits_spinner() {
let mut cell = HookCell::new_active(
hook_run_summary("hook-1"),
/*animations_enabled*/ false,
);
cell.reveal_running_runs_now_for_test();
cell.advance_time(Instant::now());

let rendered: Vec<String> = cell
.display_lines(/*width*/ 80)
.iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
})
.collect();

assert_eq!(
rendered,
vec!["Running PostToolUse hook: checking output policy".to_string()]
);
}

fn hook_run_summary(id: &str) -> HookRunSummary {
HookRunSummary {
id: id.to_string(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ mod markdown_stream;
mod mention_codec;
mod model_catalog;
mod model_migration;
mod motion;
mod multi_agents;
mod notifications;
#[cfg(any(not(debug_assertions), test))]
Expand Down
Loading
Loading