From 20084facfe4755ef4100e4b0b4cc537e5d469b60 Mon Sep 17 00:00:00 2001 From: Ed Bayes Date: Thu, 7 Aug 2025 01:45:04 -0700 Subject: [PATCH] Add spinner animation to TUI status indicator (#1917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add a pulsing dot loader before the shimmering `Working` label in the status indicator widget and include a small test asserting the spinner character is rendered - also fix a small bug in the ran command header by adding a space between the ⚑ and `Ran command` https://github.com/user-attachments/assets/6768c9d2-e094-49cb-ad51-44bcac10aa6f ## Testing - `just fmt` - `just fix` *(failed: E0658 `let` expressions in core/src/client.rs)* - `cargo test --all-features` *(failed: E0658 `let` expressions in core/src/client.rs)* ------ https://chatgpt.com/codex/tasks/task_i_68941bffdb948322b0f4190bc9dbe7f6 --------- Co-authored-by: aibrahim-oai --- codex-rs/tui/src/history_cell.rs | 4 +-- codex-rs/tui/src/status_indicator_widget.rs | 32 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index d7a06fa5..b87525a0 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -262,7 +262,7 @@ impl HistoryCell { let mut lines: Vec> = Vec::new(); let command_escaped = strip_bash_lc_and_escape(&command); lines.push(Line::from(vec![ - "⚑Ran command ".magenta(), + "⚑ Ran command ".magenta(), command_escaped.into(), ])); @@ -556,7 +556,7 @@ impl HistoryCell { let mut header: Vec = Vec::new(); header.push(Span::raw("πŸ“‹")); header.push(Span::styled( - "Updated", + " Updated", Style::default().add_modifier(Modifier::BOLD).magenta(), )); header.push(Span::raw(" to do list [")); diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index fca9a23b..dcb8a5fd 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -213,9 +213,20 @@ impl WidgetRef for StatusIndicatorWidget { // Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback. let inner_width = area.width as usize; - // Compose a single status line like: "β–Œ Working (Xs β€’ Ctrl c to interrupt) " let mut spans: Vec> = Vec::new(); spans.push(Span::styled("β–Œ ", Style::default().fg(Color::Cyan))); + + // Simple dim spinner to the left of the header. + let spinner_frames = ['Β·', 'β€’', '●', 'β€’']; + const SPINNER_SLOWDOWN: usize = 2; + let spinner_ch = spinner_frames[(idx / SPINNER_SLOWDOWN) % spinner_frames.len()]; + spans.push(Span::styled( + spinner_ch.to_string(), + Style::default().fg(Color::DarkGray), + )); + spans.push(Span::raw(" ")); + + // Space after header // Animated header after the left bar spans.extend(animated_spans); // Space between header and bracket block @@ -324,4 +335,23 @@ mod tests { } assert!(row.contains("Working"), "expected Working header: {row:?}"); } + + #[test] + fn spinner_is_rendered() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut w = StatusIndicatorWidget::new(tx); + w.restart_with_text("Hello".to_string()); + std::thread::sleep(std::time::Duration::from_millis(120)); + + let area = ratatui::layout::Rect::new(0, 0, 30, 1); + let mut buf = ratatui::buffer::Buffer::empty(area); + w.render_ref(area, &mut buf); + + let ch = buf[(2, 0)].symbol().chars().next().unwrap_or(' '); + assert!( + matches!(ch, 'Β·' | 'β€’' | '●'), + "expected spinner char at col 2: {ch:?}" + ); + } }