From 235987843c3d6647c0819c1071f9b9f064673e9c Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Thu, 14 Aug 2025 19:32:45 -0400 Subject: [PATCH] add a timer to running exec commands (#2321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sometimes i switch back to codex and i don't know how long a command has been running. Screenshot 2025-08-14 at 3 30 07 PM --- codex-rs/tui/src/history_cell.rs | 84 ++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 19fdfc4a..ba645a7e 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -35,6 +35,7 @@ use std::collections::HashMap; use std::io::Cursor; use std::path::PathBuf; use std::time::Duration; +use std::time::Instant; use tracing::error; #[derive(Clone)] @@ -78,10 +79,16 @@ pub(crate) struct ExecCell { pub(crate) command: Vec, pub(crate) parsed: Vec, pub(crate) output: Option, + start_time: Option, } impl HistoryCell for ExecCell { fn display_lines(&self) -> Vec> { - exec_command_lines(&self.command, &self.parsed, self.output.as_ref()) + exec_command_lines( + &self.command, + &self.parsed, + self.output.as_ref(), + self.start_time, + ) } } @@ -199,49 +206,66 @@ pub(crate) fn new_active_exec_command( command: Vec, parsed: Vec, ) -> ExecCell { - new_exec_cell(command, parsed, None) + ExecCell { + command, + parsed, + output: None, + start_time: Some(Instant::now()), + } } pub(crate) fn new_completed_exec_command( command: Vec, parsed: Vec, output: CommandOutput, -) -> ExecCell { - new_exec_cell(command, parsed, Some(output)) -} - -fn new_exec_cell( - command: Vec, - parsed: Vec, - output: Option, ) -> ExecCell { ExecCell { command, parsed, - output, + output: Some(output), + start_time: None, } } +fn exec_duration(start: Instant) -> String { + format!("{}s", start.elapsed().as_secs()) +} + fn exec_command_lines( command: &[String], parsed: &[ParsedCommand], output: Option<&CommandOutput>, + start_time: Option, ) -> Vec> { match parsed.is_empty() { - true => new_exec_command_generic(command, output), - false => new_parsed_command(parsed, output), + true => new_exec_command_generic(command, output, start_time), + false => new_parsed_command(parsed, output, start_time), } } - fn new_parsed_command( parsed_commands: &[ParsedCommand], output: Option<&CommandOutput>, + start_time: Option, ) -> Vec> { - let mut lines: Vec = vec![match output { - None => Line::from("⚙︎ Working".magenta().bold()), - Some(o) if o.exit_code == 0 => Line::from("✓ Completed".green().bold()), - Some(o) => Line::from(format!("✗ Failed (exit {})", o.exit_code).red().bold()), - }]; + let mut lines: Vec = Vec::new(); + match output { + None => { + let mut spans = vec!["⚙︎ Working".magenta().bold()]; + if let Some(st) = start_time { + let dur = exec_duration(st); + spans.push(format!(" • {dur}").dim()); + } + lines.push(Line::from(spans)); + } + Some(o) if o.exit_code == 0 => { + lines.push(Line::from("✓ Completed".green().bold())); + } + Some(o) => { + lines.push(Line::from( + format!("✗ Failed (exit {})", o.exit_code).red().bold(), + )); + } + }; for (i, parsed) in parsed_commands.iter().enumerate() { let text = match parsed { @@ -282,17 +306,27 @@ fn new_parsed_command( fn new_exec_command_generic( command: &[String], output: Option<&CommandOutput>, + start_time: Option, ) -> Vec> { let mut lines: Vec> = Vec::new(); let command_escaped = strip_bash_lc_and_escape(command); let mut cmd_lines = command_escaped.lines(); if let Some(first) = cmd_lines.next() { - lines.push(Line::from(vec![ - "⚡ Running ".to_string().magenta(), - first.to_string().into(), - ])); + let mut spans: Vec = vec!["⚡ Running".magenta()]; + if let Some(st) = start_time { + let dur = exec_duration(st); + spans.push(format!(" • {dur}").dim()); + } + spans.push(" ".into()); + spans.push(first.to_string().into()); + lines.push(Line::from(spans)); } else { - lines.push(Line::from("⚡ Running".to_string().magenta())); + let mut spans: Vec = vec!["⚡ Running".magenta()]; + if let Some(st) = start_time { + let dur = exec_duration(st); + spans.push(format!(" • {dur}").dim()); + } + lines.push(Line::from(spans)); } for cont in cmd_lines { lines.push(Line::from(cont.to_string())); @@ -866,7 +900,7 @@ mod tests { let parsed = vec![ParsedCommand::Unknown { cmd: "printf 'foo\nbar'".to_string(), }]; - let lines = exec_command_lines(&[], &parsed, None); + let lines = exec_command_lines(&[], &parsed, None, None); assert!(lines.len() >= 3); assert_eq!(lines[1].spans[0].content, " └ "); assert_eq!(lines[2].spans[0].content, " ");