[codex-tui] exit when terminal is dumb (#9293)
Using terminal with TERM=dumb specifically mean that TUIs and the like don't work. Ensure that codex doesn't run in these environments and exit with odd errors like crossterm's "Error: The cursor position could not be read within a normal duration" --------- Co-authored-by: Josh McKinney <joshka@openai.com>
This commit is contained in:
parent
80f80181c2
commit
c14e6813fb
3 changed files with 60 additions and 1 deletions
|
|
@ -28,6 +28,7 @@ use codex_tui::ExitReason;
|
|||
use codex_tui::update_action::UpdateAction;
|
||||
use codex_tui2 as tui2;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
use supports_color::Stream;
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ use codex_core::features::Feature;
|
|||
use codex_core::features::FeatureOverrides;
|
||||
use codex_core::features::Features;
|
||||
use codex_core::features::is_known_feature_key;
|
||||
use codex_core::terminal::TerminalName;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
/// Codex CLI
|
||||
|
|
@ -735,6 +737,25 @@ async fn run_interactive_tui(
|
|||
// Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state.
|
||||
interactive.prompt = Some(prompt.replace("\r\n", "\n").replace('\r', "\n"));
|
||||
}
|
||||
|
||||
let terminal_info = codex_core::terminal::terminal_info();
|
||||
if terminal_info.name == TerminalName::Dumb {
|
||||
if !(std::io::stdin().is_terminal() && std::io::stderr().is_terminal()) {
|
||||
return Ok(AppExitInfo::fatal(
|
||||
"TERM is set to \"dumb\". Refusing to start the interactive TUI because no terminal is available for a confirmation prompt (stdin/stderr is not a TTY). Run in a supported terminal or unset TERM.",
|
||||
));
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"WARNING: TERM is set to \"dumb\". Codex's interactive TUI may not work in this terminal."
|
||||
);
|
||||
if !confirm("Continue anyway? [y/N]: ")? {
|
||||
return Ok(AppExitInfo::fatal(
|
||||
"Refusing to start the interactive TUI because TERM is set to \"dumb\". Run in a supported terminal or unset TERM.",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if is_tui2_enabled(&interactive).await? {
|
||||
let result = tui2::run_main(interactive.into(), codex_linux_sandbox_exe).await?;
|
||||
Ok(result.into())
|
||||
|
|
@ -743,6 +764,15 @@ async fn run_interactive_tui(
|
|||
}
|
||||
}
|
||||
|
||||
fn confirm(prompt: &str) -> std::io::Result<bool> {
|
||||
eprintln!("{prompt}");
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let answer = input.trim();
|
||||
Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes"))
|
||||
}
|
||||
|
||||
/// Returns `Ok(true)` when the resolved configuration enables the `tui2` feature flag.
|
||||
///
|
||||
/// This performs a lightweight config load (honoring the same precedence as the lower-level TUI
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ pub enum TerminalName {
|
|||
Vte,
|
||||
/// Windows Terminal emulator.
|
||||
WindowsTerminal,
|
||||
/// Dumb terminal (TERM=dumb).
|
||||
Dumb,
|
||||
/// Unknown or missing terminal identification.
|
||||
Unknown,
|
||||
}
|
||||
|
|
@ -131,7 +133,12 @@ impl TerminalInfo {
|
|||
|
||||
/// Creates terminal metadata from a `TERM` capability value.
|
||||
fn from_term(term: String, multiplexer: Option<Multiplexer>) -> Self {
|
||||
Self::new(TerminalName::Unknown, None, None, Some(term), multiplexer)
|
||||
let name = if term == "dumb" {
|
||||
TerminalName::Dumb
|
||||
} else {
|
||||
TerminalName::Unknown
|
||||
};
|
||||
Self::new(name, None, None, Some(term), multiplexer)
|
||||
}
|
||||
|
||||
/// Creates terminal metadata for unknown terminals.
|
||||
|
|
@ -166,6 +173,7 @@ impl TerminalInfo {
|
|||
TerminalName::GnomeTerminal => "gnome-terminal".to_string(),
|
||||
TerminalName::Vte => format_terminal_version("VTE", &self.version),
|
||||
TerminalName::WindowsTerminal => "WindowsTerminal".to_string(),
|
||||
TerminalName::Dumb => "dumb".to_string(),
|
||||
TerminalName::Unknown => "unknown".to_string(),
|
||||
}
|
||||
};
|
||||
|
|
@ -435,6 +443,7 @@ fn terminal_name_from_term_program(value: &str) -> Option<TerminalName> {
|
|||
"gnometerminal" => Some(TerminalName::GnomeTerminal),
|
||||
"vte" => Some(TerminalName::Vte),
|
||||
"windowsterminal" => Some(TerminalName::WindowsTerminal),
|
||||
"dumb" => Some(TerminalName::Dumb),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -1136,6 +1145,15 @@ mod tests {
|
|||
"term_fallback_user_agent"
|
||||
);
|
||||
|
||||
let env = FakeEnvironment::new().with_var("TERM", "dumb");
|
||||
let terminal = detect_terminal_info_from_env(&env);
|
||||
assert_eq!(
|
||||
terminal,
|
||||
terminal_info(TerminalName::Dumb, None, None, Some("dumb"), None),
|
||||
"dumb_term_info"
|
||||
);
|
||||
assert_eq!(terminal.user_agent_token(), "dumb", "dumb_term_user_agent");
|
||||
|
||||
let env = FakeEnvironment::new();
|
||||
let terminal = detect_terminal_info_from_env(&env);
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -84,6 +84,17 @@ pub struct AppExitInfo {
|
|||
pub exit_reason: ExitReason,
|
||||
}
|
||||
|
||||
impl AppExitInfo {
|
||||
pub fn fatal(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
token_usage: TokenUsage::default(),
|
||||
thread_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::Fatal(message.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AppRunControl {
|
||||
Continue,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue