Files
codex/codex-rs/tui/src/main.rs
T
Eric Traut 9d88e299a7 Print TUI session info on fatal exits (#27417)
## Summary

TUI exits printed the resume/session summary only after checking the
exit reason. On fatal exits, both CLI wrappers wrote the error and
called `process::exit(1)` immediately, so an active session that ended
on a fatal error could skip the session information entirely.

This change prints the normal exit summary before returning the fatal
nonzero exit code. If a fatal exit has a known thread id but no
resumable rollout hint, it prints `Session ID: <id>` instead of staying
silent. It also flushes stdout before `process::exit(1)` so the summary
line is not lost during process teardown.

## Implementation

- Apply the fatal-exit ordering fix in both `codex` and standalone
`codex-tui`.
- Keep normal user-requested exit behavior unchanged.
- Preserve the existing resume hint when a rollout is resumable, and use
the raw thread id only as a fatal-exit fallback.
2026-06-11 09:56:09 -07:00

84 lines
2.3 KiB
Rust

use clap::Parser;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_config::LoaderOverrides;
use codex_tui::AppExitInfo;
use codex_tui::Cli;
use codex_tui::ExitReason;
use codex_tui::run_main;
use codex_utils_cli::CliConfigOverrides;
use std::io::Write;
use supports_color::Stream;
fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<String> {
let is_fatal = matches!(&exit_info.exit_reason, ExitReason::Fatal(_));
let AppExitInfo {
token_usage,
thread_id,
resume_hint,
..
} = exit_info;
let mut lines = Vec::new();
if !token_usage.is_zero() {
lines.push(token_usage.to_string());
}
if let Some(resume_cmd) = resume_hint {
let command = if color_enabled {
format!("\u{1b}[36m{resume_cmd}\u{1b}[39m")
} else {
resume_cmd
};
lines.push(format!("To continue this session, run {command}"));
} else if is_fatal && let Some(thread_id) = thread_id {
lines.push(format!("Session ID: {thread_id}"));
}
lines
}
#[derive(Parser, Debug)]
struct TopCli {
#[clap(flatten)]
config_overrides: CliConfigOverrides,
#[clap(flatten)]
inner: Cli,
}
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
let top_cli = TopCli::parse();
let mut inner = top_cli.inner;
inner
.config_overrides
.raw_overrides
.splice(0..0, top_cli.config_overrides.raw_overrides);
let exit_info = run_main(
inner,
arg0_paths,
LoaderOverrides::default(),
/*explicit_remote_endpoint*/ None,
)
.await?;
let is_fatal = match &exit_info.exit_reason {
ExitReason::Fatal(message) => {
eprintln!("ERROR: {message}");
true
}
ExitReason::UserRequested => false,
};
let color_enabled = supports_color::on(Stream::Stdout).is_some();
for line in format_exit_messages(exit_info, color_enabled) {
println!("{line}");
}
if is_fatal {
std::io::stdout().flush()?;
std::process::exit(1);
}
Ok(())
})
}