From 84a0ba9bf5121d288bee58ba865fc9b962b7f1d3 Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Thu, 18 Sep 2025 09:28:32 -0700 Subject: [PATCH] hint for codex resume on tui exit (#3757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-09-16 at 4 25 19 PM --- codex-rs/Cargo.lock | 1 + codex-rs/cli/src/main.rs | 7 +++++-- codex-rs/tui/Cargo.toml | 1 + codex-rs/tui/src/app.rs | 14 ++++++++++++-- codex-rs/tui/src/lib.rs | 10 +++++++--- codex-rs/tui/src/main.rs | 20 +++++++++++++++++--- 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 81fc8683..e3930b50 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -929,6 +929,7 @@ dependencies = [ "libc", "mcp-types", "once_cell", + "owo-colors", "path-clean", "pathdiff", "pretty_assertions", diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7ce98a39..e66855fe 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -177,8 +177,11 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() root_config_overrides.clone(), ); let usage = codex_tui::run_main(interactive, codex_linux_sandbox_exe).await?; - if !usage.is_zero() { - println!("{}", codex_core::protocol::FinalOutput::from(usage)); + if !usage.token_usage.is_zero() { + println!( + "{}", + codex_core::protocol::FinalOutput::from(usage.token_usage) + ); } } Some(Subcommand::Exec(mut exec_cli)) => { diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 06e892b7..c7a53158 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -85,6 +85,7 @@ unicode-segmentation = "1.12.0" unicode-width = "0.1" url = "2" pathdiff = "0.2" +owo-colors = "4.2.0" [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 205986c9..c28b2d27 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -15,6 +15,7 @@ use codex_core::config::persist_model_selection; use codex_core::model_family::find_family_for_model; use codex_core::protocol::TokenUsage; use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig; +use codex_protocol::mcp_protocol::ConversationId; use color_eyre::eyre::Result; use color_eyre::eyre::WrapErr; use crossterm::event::KeyCode; @@ -33,6 +34,12 @@ use tokio::select; use tokio::sync::mpsc::unbounded_channel; // use uuid::Uuid; +#[derive(Debug, Clone)] +pub struct AppExitInfo { + pub token_usage: TokenUsage, + pub conversation_id: Option, +} + pub(crate) struct App { pub(crate) server: Arc, pub(crate) app_event_tx: AppEventSender, @@ -70,7 +77,7 @@ impl App { initial_prompt: Option, initial_images: Vec, resume_selection: ResumeSelection, - ) -> Result { + ) -> Result { use tokio_stream::StreamExt; let (app_event_tx, mut app_event_rx) = unbounded_channel(); let app_event_tx = AppEventSender::new(app_event_tx); @@ -153,7 +160,10 @@ impl App { } } {} tui.terminal.clear()?; - Ok(app.token_usage()) + Ok(AppExitInfo { + token_usage: app.token_usage(), + conversation_id: app.chat_widget.conversation_id(), + }) } pub(crate) async fn handle_tui_event( diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 3f4d66ee..04ead750 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -4,6 +4,7 @@ #![deny(clippy::print_stdout, clippy::print_stderr)] #![deny(clippy::disallowed_methods)] use app::App; +pub use app::AppExitInfo; use codex_core::AuthManager; use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; use codex_core::CodexAuth; @@ -86,7 +87,7 @@ use codex_core::internal_storage::InternalStorage; pub async fn run_main( cli: Cli, codex_linux_sandbox_exe: Option, -) -> std::io::Result { +) -> std::io::Result { let (sandbox_mode, approval_policy) = if cli.full_auto { ( Some(SandboxMode::WorkspaceWrite), @@ -258,7 +259,7 @@ async fn run_ratatui_app( mut internal_storage: InternalStorage, active_profile: Option, should_show_trust_screen: bool, -) -> color_eyre::Result { +) -> color_eyre::Result { let mut config = config; color_eyre::install()?; @@ -370,7 +371,10 @@ async fn run_ratatui_app( resume_picker::ResumeSelection::Exit => { restore(); session_log::log_session_end(); - return Ok(codex_core::protocol::TokenUsage::default()); + return Ok(AppExitInfo { + token_usage: codex_core::protocol::TokenUsage::default(), + conversation_id: None, + }); } other => other, } diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index 2dbd797d..a9b426fe 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -3,6 +3,8 @@ 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 { @@ -21,9 +23,21 @@ fn main() -> anyhow::Result<()> { .config_overrides .raw_overrides .splice(0..0, top_cli.config_overrides.raw_overrides); - let usage = run_main(inner, codex_linux_sandbox_exe).await?; - if !usage.is_zero() { - println!("{}", codex_core::protocol::FinalOutput::from(usage)); + 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(()) })