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
---
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(())
})