diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c618fb51..71681aa5 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -638,9 +638,11 @@ dependencies = [ "codex-protocol", "codex-protocol-ts", "codex-tui", + "owo-colors", "predicates", "pretty_assertions", "serde_json", + "supports-color", "tempfile", "tokio", "tracing", @@ -931,7 +933,6 @@ dependencies = [ "libc", "mcp-types", "once_cell", - "owo-colors", "path-clean", "pathdiff", "pretty_assertions", diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 0d151a90..c410e09a 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -26,8 +26,11 @@ codex-exec = { workspace = true } codex-login = { workspace = true } codex-mcp-server = { workspace = true } codex-protocol = { workspace = true } +codex-protocol-ts = { workspace = true } codex-tui = { workspace = true } +owo-colors = { workspace = true } serde_json = { workspace = true } +supports-color = { workspace = true } tokio = { workspace = true, features = [ "io-std", "macros", @@ -37,7 +40,6 @@ tokio = { workspace = true, features = [ ] } tracing = { workspace = true } tracing-subscriber = { workspace = true } -codex-protocol-ts = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index e66855fe..df757b0c 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -14,8 +14,11 @@ use codex_cli::login::run_logout; use codex_cli::proto; use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; +use codex_tui::AppExitInfo; use codex_tui::Cli as TuiCli; +use owo_colors::OwoColorize; use std::path::PathBuf; +use supports_color::Stream; mod mcp_cmd; @@ -156,6 +159,41 @@ struct GenerateTsCommand { prettier: Option, } +fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec { + let AppExitInfo { + token_usage, + conversation_id, + } = exit_info; + + if token_usage.is_zero() { + return Vec::new(); + } + + let mut lines = vec![format!( + "{}", + codex_core::protocol::FinalOutput::from(token_usage) + )]; + + if let Some(session_id) = conversation_id { + let resume_cmd = format!("codex resume {session_id}"); + let command = if color_enabled { + resume_cmd.cyan().to_string() + } else { + resume_cmd + }; + lines.push(format!("To continue this session, run {command}.")); + } + + lines +} + +fn print_exit_messages(exit_info: AppExitInfo) { + let color_enabled = supports_color::on(Stream::Stdout).is_some(); + for line in format_exit_messages(exit_info, color_enabled) { + println!("{line}"); + } +} + fn main() -> anyhow::Result<()> { arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { cli_main(codex_linux_sandbox_exe).await?; @@ -176,13 +214,8 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() &mut interactive.config_overrides, root_config_overrides.clone(), ); - let usage = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; - if !usage.token_usage.is_zero() { - println!( - "{}", - codex_core::protocol::FinalOutput::from(usage.token_usage) - ); - } + let exit_info = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; + print_exit_messages(exit_info); } Some(Subcommand::Exec(mut exec_cli)) => { prepend_config_flags( @@ -372,6 +405,8 @@ fn print_completion(cmd: CompletionCommand) { #[cfg(test)] mod tests { use super::*; + use codex_core::protocol::TokenUsage; + use codex_protocol::mcp_protocol::ConversationId; fn finalize_from_args(args: &[&str]) -> TuiCli { let cli = MultitoolCli::try_parse_from(args).expect("parse"); @@ -393,6 +428,52 @@ mod tests { finalize_resume_interactive(interactive, root_overrides, session_id, last, resume_cli) } + fn sample_exit_info(conversation: Option<&str>) -> AppExitInfo { + let token_usage = TokenUsage { + output_tokens: 2, + total_tokens: 2, + ..Default::default() + }; + AppExitInfo { + token_usage, + conversation_id: conversation + .map(ConversationId::from_string) + .map(Result::unwrap), + } + } + + #[test] + fn format_exit_messages_skips_zero_usage() { + let exit_info = AppExitInfo { + token_usage: TokenUsage::default(), + conversation_id: None, + }; + let lines = format_exit_messages(exit_info, false); + assert!(lines.is_empty()); + } + + #[test] + fn format_exit_messages_includes_resume_hint_without_color() { + let exit_info = sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000")); + let lines = format_exit_messages(exit_info, false); + assert_eq!( + lines, + vec![ + "Token usage: total=2 input=0 output=2".to_string(), + "To continue this session, run codex resume 123e4567-e89b-12d3-a456-426614174000." + .to_string(), + ] + ); + } + + #[test] + fn format_exit_messages_applies_color_when_enabled() { + let exit_info = sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000")); + let lines = format_exit_messages(exit_info, true); + assert_eq!(lines.len(), 2); + assert!(lines[1].contains("\u{1b}[36m")); + } + #[test] fn resume_model_flag_applies_when_no_root_flags() { let interactive = finalize_from_args(["codex", "resume", "-m", "gpt-5-test"].as_ref()); diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index b029f722..38546d0b 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -85,7 +85,6 @@ unicode-segmentation = { workspace = true } unicode-width = { workspace = true } url = { workspace = true } pathdiff = { workspace = true } -owo-colors = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index a9b426fe..50ea95f1 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -3,8 +3,6 @@ use codex_arg0::arg0_dispatch_or_else; use codex_common::CliConfigOverrides; use codex_tui::Cli; use codex_tui::run_main; -use owo_colors::OwoColorize; -use supports_color::Stream; #[derive(Parser, Debug)] struct TopCli { @@ -25,19 +23,8 @@ fn main() -> anyhow::Result<()> { .splice(0..0, top_cli.config_overrides.raw_overrides); let exit_info = run_main(inner, codex_linux_sandbox_exe).await?; let token_usage = exit_info.token_usage; - let conversation_id = exit_info.conversation_id; if !token_usage.is_zero() { println!("{}", codex_core::protocol::FinalOutput::from(token_usage),); - if let Some(session_id) = conversation_id { - let command = format!("codex resume {session_id}"); - let prefix = "To continue this session, run "; - let suffix = "."; - if supports_color::on(Stream::Stdout).is_some() { - println!("{}{}{}", prefix, command.cyan(), suffix); - } else { - println!("{prefix}{command}{suffix}"); - } - } } Ok(()) })