mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
Add terminal title spinner while running
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
+65
-11
@@ -117,16 +117,17 @@ use self::pending_interactive_replay::PendingInteractiveReplayState;
|
||||
|
||||
const EXTERNAL_EDITOR_HINT: &str = "Save and close external editor to continue.";
|
||||
const THREAD_EVENT_CHANNEL_CAPACITY: usize = 32768;
|
||||
|
||||
enum ThreadInteractiveRequest {
|
||||
Approval(ApprovalRequest),
|
||||
McpServerElicitation(McpServerElicitationFormRequest),
|
||||
}
|
||||
const TITLE_SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
/// Baseline cadence for periodic stream commit animation ticks.
|
||||
///
|
||||
/// Smooth-mode streaming drains one line per tick, so this interval controls
|
||||
/// perceived typing speed for non-backlogged output.
|
||||
const COMMIT_ANIMATION_TICK: Duration = tui::TARGET_FRAME_INTERVAL;
|
||||
const TITLE_SPINNER_INTERVAL: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppExitInfo {
|
||||
@@ -729,12 +730,47 @@ fn normalize_harness_overrides_for_cwd(
|
||||
Ok(overrides)
|
||||
}
|
||||
|
||||
fn normalize_title_context(title_override: Option<String>) -> Option<String> {
|
||||
title_override
|
||||
fn compute_title_context(
|
||||
title_override: Option<String>,
|
||||
task_running: bool,
|
||||
tick: u128,
|
||||
) -> Option<String> {
|
||||
let context = title_override
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.map(ToString::to_string);
|
||||
|
||||
if !task_running {
|
||||
return context;
|
||||
}
|
||||
|
||||
let frame = TITLE_SPINNER_FRAMES[tick as usize % TITLE_SPINNER_FRAMES.len()];
|
||||
context
|
||||
.map(|context| format!("{frame} - {context}"))
|
||||
.or_else(|| Some(frame.to_string()))
|
||||
}
|
||||
|
||||
fn title_animation_tick() -> u128 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
/ TITLE_SPINNER_INTERVAL.as_millis()
|
||||
}
|
||||
|
||||
fn update_terminal_title(
|
||||
tui: &mut tui::Tui,
|
||||
title_override: Option<String>,
|
||||
task_running: bool,
|
||||
) -> Result<()> {
|
||||
let title_context = compute_title_context(title_override, task_running, title_animation_tick());
|
||||
tui.set_title_context(title_context.as_deref())?;
|
||||
if task_running {
|
||||
tui.frame_requester()
|
||||
.schedule_frame_in(TITLE_SPINNER_INTERVAL);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -1924,8 +1960,8 @@ impl App {
|
||||
primary_session_configured: None,
|
||||
pending_primary_events: VecDeque::new(),
|
||||
};
|
||||
let title_context = normalize_title_context(app.chat_widget.title_override());
|
||||
tui.set_title_context(title_context.as_deref())?;
|
||||
let task_running = app.chat_widget.is_task_running();
|
||||
update_terminal_title(tui, app.chat_widget.title_override(), task_running)?;
|
||||
|
||||
// On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -2025,8 +2061,8 @@ impl App {
|
||||
AppRunControl::Continue
|
||||
}
|
||||
};
|
||||
let title_context = normalize_title_context(app.chat_widget.title_override());
|
||||
tui.set_title_context(title_context.as_deref())?;
|
||||
let task_running = app.chat_widget.is_task_running();
|
||||
update_terminal_title(tui, app.chat_widget.title_override(), task_running)?;
|
||||
if App::should_stop_waiting_for_initial_session(
|
||||
waiting_for_initial_session_configured,
|
||||
app.primary_thread_id,
|
||||
@@ -3900,10 +3936,28 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_title_context_uses_manual_title_when_present() {
|
||||
fn title_context_adds_spinner_while_running() {
|
||||
assert_eq!(
|
||||
normalize_title_context(Some("Named thread".to_string())),
|
||||
Some("Named thread".to_string())
|
||||
compute_title_context(Some("Named thread".to_string()), true, 0),
|
||||
Some("⠋ - Named thread".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
compute_title_context(Some("Named thread".to_string()), true, 9),
|
||||
Some("⠏ - Named thread".to_string())
|
||||
);
|
||||
assert_eq!(compute_title_context(None, true, 0), Some("⠋".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_context_defaults_to_none_without_manual_title() {
|
||||
assert_eq!(compute_title_context(None, false, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_context_prefers_manual_title_when_idle() {
|
||||
assert_eq!(
|
||||
compute_title_context(Some("manual title".to_string()), false, 0),
|
||||
Some("manual title".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8545,6 +8545,10 @@ impl ChatWidget {
|
||||
self.title_override = title;
|
||||
}
|
||||
|
||||
pub(crate) fn is_task_running(&self) -> bool {
|
||||
self.bottom_pane.is_task_running()
|
||||
}
|
||||
|
||||
/// Returns the current thread's precomputed rollout path.
|
||||
///
|
||||
/// For fresh non-ephemeral threads this path may exist before the file is
|
||||
|
||||
@@ -61,6 +61,13 @@ pub(crate) const TARGET_FRAME_INTERVAL: Duration = frame_rate_limiter::MIN_FRAME
|
||||
pub type Terminal = CustomTerminal<CrosstermBackend<Stdout>>;
|
||||
const DEFAULT_TERMINAL_TITLE: &str = "Codex";
|
||||
|
||||
fn has_spinner_prefix(context: &str) -> bool {
|
||||
matches!(
|
||||
context.chars().next(),
|
||||
Some('⠋' | '⠙' | '⠹' | '⠸' | '⠼' | '⠴' | '⠦' | '⠧' | '⠇' | '⠏')
|
||||
)
|
||||
}
|
||||
|
||||
fn format_terminal_title(context: Option<&str>) -> String {
|
||||
let context = context
|
||||
.map(|text| {
|
||||
@@ -73,6 +80,9 @@ fn format_terminal_title(context: Option<&str>) -> String {
|
||||
.filter(|text| !text.is_empty());
|
||||
|
||||
match context {
|
||||
Some(context) if has_spinner_prefix(&context) => {
|
||||
format!("{DEFAULT_TERMINAL_TITLE} {context}")
|
||||
}
|
||||
Some(context) => format!("{DEFAULT_TERMINAL_TITLE} - {context}"),
|
||||
None => DEFAULT_TERMINAL_TITLE.to_string(),
|
||||
}
|
||||
@@ -608,6 +618,15 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_title_places_spinner_after_codex() {
|
||||
assert_eq!(format_terminal_title(Some("⠋")), "Codex ⠋");
|
||||
assert_eq!(
|
||||
format_terminal_title(Some("⠋ - fix title syncing")),
|
||||
"Codex ⠋ - fix title syncing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_title_strips_control_characters() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user