Fix missing resume hint on zero-token exits (#16987)

Addresses #16421

Problem: Resumed interactive sessions exited before new token usage
skipped all footer lines, hiding the `codex resume` continuation
command.

It's not clear whether this was an intentional design choice, but I
think it's reasonable to expect this message under these circumstances.

Solution: Compose token usage and resume hints independently so
resumable sessions still print the continuation command with zero usage.
This commit is contained in:
Eric Traut authored and GitHub committed 2026-04-07 09:34:04 -07:00
1 parent cd591dc457
commit 2b9bf5d3d4
2 files changed
+25 -18

No files matched your search

+6 -7
View File
@@ -438,15 +438,14 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<Stri
.. ..
} = exit_info; } = exit_info;
if token_usage.is_zero() { let mut lines = Vec::new();
return Vec::new(); if !token_usage.is_zero() {
lines.push(format!(
"{}",
codex_protocol::protocol::FinalOutput::from(token_usage)
));
} }
let mut lines = vec![format!(
"{}",
codex_protocol::protocol::FinalOutput::from(token_usage)
)];
if let Some(resume_cmd) = if let Some(resume_cmd) =
codex_core::util::resume_command(thread_name.as_deref(), conversation_id) codex_core::util::resume_command(thread_name.as_deref(), conversation_id)
{ {
+19 -11
View File
@@ -316,12 +316,13 @@ fn session_summary(
thread_id: Option<ThreadId>, thread_id: Option<ThreadId>,
thread_name: Option<String>, thread_name: Option<String>,
) -> Option<SessionSummary> { ) -> Option<SessionSummary> {
if token_usage.is_zero() { let usage_line = (!token_usage.is_zero()).then(|| FinalOutput::from(token_usage).to_string());
let resume_command = codex_core::util::resume_command(thread_name.as_deref(), thread_id);
if usage_line.is_none() && resume_command.is_none() {
return None; return None;
} }
let usage_line = FinalOutput::from(token_usage).to_string();
let resume_command = codex_core::util::resume_command(thread_name.as_deref(), thread_id);
Some(SessionSummary { Some(SessionSummary {
usage_line, usage_line,
resume_command, resume_command,
@@ -486,7 +487,7 @@ fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &Config) {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
struct SessionSummary { struct SessionSummary {
usage_line: String, usage_line: Option<String>,
resume_command: Option<String>, resume_command: Option<String>,
} }
@@ -3301,7 +3302,10 @@ impl App {
"Failed to attach to fresh app-server thread: {err}" "Failed to attach to fresh app-server thread: {err}"
)); ));
} else if let Some(summary) = summary { } else if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> = vec![summary.usage_line.clone().into()]; let mut lines: Vec<Line<'static>> = Vec::new();
if let Some(usage_line) = summary.usage_line {
lines.push(usage_line.into());
}
if let Some(command) = summary.resume_command { if let Some(command) = summary.resume_command {
let spans = vec!["To continue this session, run ".into(), command.cyan()]; let spans = vec!["To continue this session, run ".into(), command.cyan()];
lines.push(spans.into()); lines.push(spans.into());
@@ -4114,8 +4118,10 @@ impl App {
{ {
Ok(()) => { Ok(()) => {
if let Some(summary) = summary { if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> = let mut lines: Vec<Line<'static>> = Vec::new();
vec![summary.usage_line.clone().into()]; if let Some(usage_line) = summary.usage_line {
lines.push(usage_line.into());
}
if let Some(command) = summary.resume_command { if let Some(command) = summary.resume_command {
let spans = vec![ let spans = vec![
"To continue this session, run ".into(), "To continue this session, run ".into(),
@@ -4174,8 +4180,10 @@ impl App {
{ {
Ok(()) => { Ok(()) => {
if let Some(summary) = summary { if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> = let mut lines: Vec<Line<'static>> = Vec::new();
vec![summary.usage_line.clone().into()]; if let Some(usage_line) = summary.usage_line {
lines.push(usage_line.into());
}
if let Some(command) = summary.resume_command { if let Some(command) = summary.resume_command {
let spans = vec![ let spans = vec![
"To continue this session, run ".into(), "To continue this session, run ".into(),
@@ -10897,7 +10905,7 @@ guardian_approval = true
} }
#[tokio::test] #[tokio::test]
async fn session_summary_skip_zero_usage() { async fn session_summary_skips_when_no_usage_or_resume_hint() {
assert!( assert!(
session_summary( session_summary(
TokenUsage::default(), TokenUsage::default(),
@@ -10922,7 +10930,7 @@ guardian_approval = true
session_summary(usage, Some(conversation), /*thread_name*/ None).expect("summary"); session_summary(usage, Some(conversation), /*thread_name*/ None).expect("summary");
assert_eq!( assert_eq!(
summary.usage_line, summary.usage_line,
"Token usage: total=12 input=10 output=2" Some("Token usage: total=12 input=10 output=2".to_string())
); );
assert_eq!( assert_eq!(
summary.resume_command, summary.resume_command,