diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index 22e6db168d..a6ab4f8983 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -353,7 +353,7 @@ impl App { &event, TuiEvent::Draw | TuiEvent::Resume | TuiEvent::Resize(_) | TuiEvent::FocusGained ) { - self.chat_widget.refresh_iterm_session_status(); + self.chat_widget.refresh_terminal_program_status(); } let width = tui.terminal.last_known_screen_size.width.max(/*other*/ 1); let footer = self.prompt_navigation_footer(width.saturating_sub(/*rhs*/ 2)); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 16b13efb89..ea0a5ffd54 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1182,16 +1182,16 @@ impl ChatWidget { } self.refresh_status_line_if_workspace_headline_due(); self.refresh_thread_usage_if_settlement_due(); - self.refresh_iterm_session_status(); + self.refresh_terminal_program_status(); } - pub(crate) fn refresh_iterm_session_status(&self) { - let status = self.desired_iterm_session_status(); - if let Err(err) = crate::iterm_session_status::set_iterm_session_status( + pub(crate) fn refresh_terminal_program_status(&self) { + let status = self.desired_program_status(); + if let Err(err) = crate::terminal_program_status::set_terminal_program_status( status, self.iterm_session_detail(status), ) { - tracing::debug!(error = %err, "failed to set iTerm2 session status"); + tracing::debug!(error = %err, "failed to set terminal program status"); } } diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 7eef820958..a3946abd13 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -8,12 +8,12 @@ use crate::bottom_pane::status_line_from_segments; use crate::branch_summary; use crate::chatwidget::limit_label_for_window; use crate::chatwidget::rate_limits::get_limits_duration; -use crate::iterm_session_status::ItermSessionStatus; use crate::legacy_core::config::Config; use crate::model_catalog::LUNA_RESERVE_MODEL; use crate::status::format_credit_micros; use crate::status::format_estimated_usd_micros; use crate::status::format_tokens_compact; +use crate::terminal_program_status::ProgramStatus; use codex_app_server_protocol::AskForApproval; use codex_config::ConfigLayerSource; use codex_config::os_host_name; @@ -322,18 +322,18 @@ impl ChatWidget { self.bottom_pane.terminal_title_requires_action() } - pub(super) fn desired_iterm_session_status(&self) -> ItermSessionStatus { + pub(super) fn desired_program_status(&self) -> ProgramStatus { if self.terminal_title_requires_action() { - ItermSessionStatus::Waiting + ProgramStatus::Blocked } else if self.bottom_pane.is_task_running() || !self.unified_exec_processes.is_empty() { - ItermSessionStatus::Working + ProgramStatus::Working } else { - ItermSessionStatus::Idle + ProgramStatus::Idle } } - pub(super) fn iterm_session_detail(&self, status: ItermSessionStatus) -> Option<&str> { - (status == ItermSessionStatus::Working && self.bottom_pane.is_task_running()) + pub(super) fn iterm_session_detail(&self, status: ProgramStatus) -> Option<&str> { + (status == ProgramStatus::Working && self.bottom_pane.is_task_running()) .then_some(self.status_state.current_status.header.as_str()) .filter(|detail| *detail != "Working") } diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 2928d3cfe6..d083177e00 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -257,8 +257,6 @@ mod history_projection; mod history_replay; #[path = "tests/home_cleanup_tests.rs"] mod home_cleanup_tests; -#[path = "tests/iterm_session_status.rs"] -mod iterm_session_status; #[path = "tests/luna_reserve_usage_tests.rs"] mod luna_reserve_usage_tests; mod mcp_startup; @@ -299,6 +297,8 @@ mod status_command_tests; mod status_surface_previews; #[path = "tests/subagent_activity_tests.rs"] mod subagent_activity; +#[path = "tests/terminal_program_status.rs"] +mod terminal_program_status; mod terminal_title; #[path = "tests/tool_activity_tests.rs"] mod tool_activity_tests; diff --git a/codex-rs/tui/src/chatwidget/tests/iterm_session_status.rs b/codex-rs/tui/src/chatwidget/tests/terminal_program_status.rs similarity index 78% rename from codex-rs/tui/src/chatwidget/tests/iterm_session_status.rs rename to codex-rs/tui/src/chatwidget/tests/terminal_program_status.rs index d63e1e4fcf..3911129ba3 100644 --- a/codex-rs/tui/src/chatwidget/tests/iterm_session_status.rs +++ b/codex-rs/tui/src/chatwidget/tests/terminal_program_status.rs @@ -1,16 +1,17 @@ use super::*; -use crate::iterm_session_status::ItermSessionStatus::Idle; -use crate::iterm_session_status::ItermSessionStatus::Waiting; -use crate::iterm_session_status::ItermSessionStatus::Working; use pretty_assertions::assert_eq; +use crate::terminal_program_status::ProgramStatus::Blocked; +use crate::terminal_program_status::ProgramStatus::Idle; +use crate::terminal_program_status::ProgramStatus::Working; + #[tokio::test] async fn session_status_follows_existing_lifecycle_state() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - let mut states = vec![chat.desired_iterm_session_status()]; + let mut states = vec![chat.desired_program_status()]; chat.bottom_pane.set_task_running(/*running*/ true); - states.push(chat.desired_iterm_session_status()); + states.push(chat.desired_program_status()); handle_exec_approval_request( &mut chat, @@ -31,13 +32,13 @@ async fn session_status_follows_existing_lifecycle_state() { available_decisions: None, }, ); - states.push(chat.desired_iterm_session_status()); + states.push(chat.desired_program_status()); chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); - states.push(chat.desired_iterm_session_status()); + states.push(chat.desired_program_status()); chat.bottom_pane.set_task_running(/*running*/ false); - states.push(chat.desired_iterm_session_status()); + states.push(chat.desired_program_status()); chat.unified_exec_processes.push(UnifiedExecProcessSummary { key: "background-process".to_string(), @@ -45,13 +46,13 @@ async fn session_status_follows_existing_lifecycle_state() { command_display: "sleep 5".to_string(), recent_chunks: Vec::new(), }); - states.push(chat.desired_iterm_session_status()); + states.push(chat.desired_program_status()); chat.unified_exec_processes.clear(); - states.push(chat.desired_iterm_session_status()); + states.push(chat.desired_program_status()); assert_eq!( states, - [Idle, Working, Waiting, Working, Idle, Working, Idle] + [Idle, Working, Blocked, Working, Idle, Working, Idle] ); } @@ -65,7 +66,7 @@ async fn session_status_detail_uses_only_foreground_activity() { let generic = detail(&chat, Working); chat.set_status_header("Reading source".to_string()); let working = detail(&chat, Working); - let waiting = detail(&chat, Waiting); + let waiting = detail(&chat, Blocked); chat.bottom_pane.set_task_running(/*running*/ false); let idle = detail(&chat, Idle); let background = detail(&chat, Working); diff --git a/codex-rs/tui/src/iterm_session_status_tests.rs b/codex-rs/tui/src/iterm_session_status_tests.rs index ac1e729d70..12f35136f6 100644 --- a/codex-rs/tui/src/iterm_session_status_tests.rs +++ b/codex-rs/tui/src/iterm_session_status_tests.rs @@ -6,26 +6,31 @@ use codex_terminal_detection::TerminalName; use crossterm::Command; use pretty_assertions::assert_eq; -use super::ClearItermSessionStatus; -use super::ItermSessionStatus; +use super::ClearTerminalProgramStatus; use super::NO_EMITTED_STATUS; +use super::ProgramStatus; use super::SessionStatusState; -use super::SetItermSessionStatus; -use super::iterm_session_status_supported; +use super::SetTerminalProgramStatus; +use super::TerminalProgramStatusSupport; use super::sanitize_iterm_session_detail; +use super::terminal_program_status_support; #[test] -fn encodes_session_status_fields() { +fn encodes_iterm_session_status_fields() { let mut encoded = Vec::new(); for (status, detail) in [ - (ItermSessionStatus::Idle, ""), - (ItermSessionStatus::Working, "Reading source"), - (ItermSessionStatus::Waiting, ""), + (ProgramStatus::Idle, ""), + (ProgramStatus::Working, "Reading source"), + (ProgramStatus::Blocked, ""), ] { let mut value = String::new(); - SetItermSessionStatus(status, detail) - .write_ansi(&mut value) - .expect("encode iTerm2 session status"); + SetTerminalProgramStatus { + status, + iterm_detail: detail, + emit_iterm_status: true, + } + .write_ansi(&mut value) + .expect("encode combined terminal program status"); encoded.push(value); } @@ -37,6 +42,20 @@ fn encodes_session_status_fields() { insta::assert_snapshot!("iterm_session_status_fields", snapshot); } +#[test] +fn encodes_generic_program_status() { + let mut encoded = String::new(); + SetTerminalProgramStatus { + status: ProgramStatus::Working, + iterm_detail: "ignored", + emit_iterm_status: false, + } + .write_ansi(&mut encoded) + .expect("encode generic terminal program status"); + + assert_eq!(encoded, "\x1b]7501;state=working:app=codex\x1b\\"); +} + #[test] fn detail_is_safe_bounded_osc_text() { assert_eq!( @@ -51,15 +70,24 @@ fn detail_is_safe_bounded_osc_text() { #[test] fn clear_empties_every_owned_field() { - let mut encoded = String::new(); - ClearItermSessionStatus - .write_ansi(&mut encoded) - .expect("encode iTerm2 session status clear"); + let mut combined = String::new(); + ClearTerminalProgramStatus { + emit_iterm_status: true, + } + .write_ansi(&mut combined) + .expect("encode combined terminal program status clear"); + let mut generic = String::new(); + ClearTerminalProgramStatus { + emit_iterm_status: false, + } + .write_ansi(&mut generic) + .expect("encode generic terminal program status clear"); assert_eq!( - encoded, - "\x1b]21337;status=;indicator=;status-color=;detail=\x07" + combined, + "\x1b]21337;status=;indicator=;status-color=;detail=\x07\x1b]7501;state=clear\x1b\\" ); + assert_eq!(generic, "\x1b]7501;state=clear\x1b\\"); } #[test] @@ -67,33 +95,33 @@ fn cache_changes_only_after_a_successful_write() { let state = SessionStatusState::new(); let mut emitted = Vec::new(); state - .update(ItermSessionStatus::Working as u8, "Thinking", || { + .update(ProgramStatus::Working as u8, "Thinking", || { Err(io::Error::other("write failed")) }) .expect_err("failed write should be returned"); state - .update(ItermSessionStatus::Working as u8, "Thinking", || { - emitted.push(ItermSessionStatus::Working as u8); + .update(ProgramStatus::Working as u8, "Thinking", || { + emitted.push(ProgramStatus::Working as u8); Ok(()) }) .expect("retry status write"); state - .update(ItermSessionStatus::Working as u8, "Thinking", || { - emitted.push(ItermSessionStatus::Waiting as u8); + .update(ProgramStatus::Working as u8, "Thinking", || { + emitted.push(ProgramStatus::Blocked as u8); Ok(()) }) .expect("deduplicate status write"); state - .update(ItermSessionStatus::Working as u8, "Reading", || { - emitted.push(ItermSessionStatus::Working as u8); + .update(ProgramStatus::Working as u8, "Reading", || { + emitted.push(ProgramStatus::Working as u8); Ok(()) }) .expect("emit changed detail"); state.invalidate(); state - .update(ItermSessionStatus::Working as u8, "Thinking", || { - emitted.push(ItermSessionStatus::Working as u8); + .update(ProgramStatus::Working as u8, "Thinking", || { + emitted.push(ProgramStatus::Working as u8); Ok(()) }) .expect("re-emit status after terminal handoff"); @@ -107,16 +135,16 @@ fn cache_changes_only_after_a_successful_write() { assert_eq!( emitted, [ - ItermSessionStatus::Working as u8, - ItermSessionStatus::Working as u8, - ItermSessionStatus::Working as u8, + ProgramStatus::Working as u8, + ProgramStatus::Working as u8, + ProgramStatus::Working as u8, NO_EMITTED_STATUS, ] ); } #[test] -fn supports_only_direct_iterm_ttys() { +fn selects_protocols_for_terminal_environment() { let direct_iterm = TerminalInfo { name: TerminalName::Iterm2, term_program: Some("iTerm.app".to_string()), @@ -124,36 +152,58 @@ fn supports_only_direct_iterm_ttys() { term: None, multiplexer: None, }; - assert!(iterm_session_status_supported( - &direct_iterm, - /*stdout_is_terminal*/ true, - /*screen_active*/ false, - )); - assert!(!iterm_session_status_supported( - &direct_iterm, - /*stdout_is_terminal*/ false, - /*screen_active*/ false, - )); - assert!(!iterm_session_status_supported( - &direct_iterm, - /*stdout_is_terminal*/ true, - /*screen_active*/ true, - )); + let both_protocols = TerminalProgramStatusSupport { + osc_7501: true, + osc_21337: true, + }; + let no_protocols = TerminalProgramStatusSupport { + osc_7501: false, + osc_21337: false, + }; + let generic_protocol = TerminalProgramStatusSupport { + osc_7501: true, + osc_21337: false, + }; + assert_eq!( + terminal_program_status_support( + &direct_iterm, + /*stdout_is_terminal*/ true, + /*screen_active*/ false, + ), + both_protocols + ); + assert_eq!( + terminal_program_status_support( + &direct_iterm, + /*stdout_is_terminal*/ false, + /*screen_active*/ false, + ), + no_protocols + ); - assert!(!iterm_session_status_supported( - &TerminalInfo { + for terminal in [ + TerminalInfo { multiplexer: Some(Multiplexer::Tmux { version: None }), ..direct_iterm.clone() }, - /*stdout_is_terminal*/ true, - /*screen_active*/ false, - )); - assert!(!iterm_session_status_supported( - &TerminalInfo { + TerminalInfo { name: TerminalName::Ghostty, - ..direct_iterm + ..direct_iterm.clone() }, - /*stdout_is_terminal*/ true, - /*screen_active*/ false, - )); + ] { + assert_eq!( + terminal_program_status_support( + &terminal, /*stdout_is_terminal*/ true, /*screen_active*/ false, + ), + generic_protocol + ); + } + assert_eq!( + terminal_program_status_support( + &direct_iterm, + /*stdout_is_terminal*/ true, + /*screen_active*/ true, + ), + generic_protocol + ); } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 4f7c5fa050..8edf8b61e4 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -162,9 +162,9 @@ mod hooks_rpc; mod ide_context; mod inline_visualization; pub(crate) mod insert_history; -mod iterm_session_status; mod managed_worktree_tool_specs; mod managed_worktree_tools; +mod terminal_program_status; pub use insert_history::insert_history_lines; mod footer_hint; mod key_hint; diff --git a/codex-rs/tui/src/snapshots/codex_tui__iterm_session_status__tests__iterm_session_status_fields.snap b/codex-rs/tui/src/snapshots/codex_tui__terminal_program_status__tests__iterm_session_status_fields.snap similarity index 59% rename from codex-rs/tui/src/snapshots/codex_tui__iterm_session_status__tests__iterm_session_status_fields.snap rename to codex-rs/tui/src/snapshots/codex_tui__terminal_program_status__tests__iterm_session_status_fields.snap index 9a5cd475f3..07001e76a5 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__iterm_session_status__tests__iterm_session_status_fields.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__terminal_program_status__tests__iterm_session_status_fields.snap @@ -1,8 +1,8 @@ --- source: tui/src/iterm_session_status_tests.rs -assertion_line: 39 +assertion_line: 42 expression: snapshot --- -\u{1b}]21337;status=Idle;indicator=#00d75f;status-color=;detail=\u{7} -\u{1b}]21337;status=Working;indicator=#ff9500;status-color=;detail=Reading source\u{7} -\u{1b}]21337;status=Waiting;indicator=#5f87ff;status-color=;detail=\u{7} +\u{1b}]21337;status=Idle;indicator=#00d75f;status-color=;detail=\u{7}\u{1b}]7501;state=idle:app=codex\u{1b}\\ +\u{1b}]21337;status=Working;indicator=#ff9500;status-color=;detail=Reading source\u{7}\u{1b}]7501;state=working:app=codex\u{1b}\\ +\u{1b}]21337;status=Waiting;indicator=#5f87ff;status-color=;detail=\u{7}\u{1b}]7501;state=blocked:app=codex\u{1b}\\ diff --git a/codex-rs/tui/src/iterm_session_status.rs b/codex-rs/tui/src/terminal_program_status.rs similarity index 54% rename from codex-rs/tui/src/iterm_session_status.rs rename to codex-rs/tui/src/terminal_program_status.rs index 73b23dc036..9379f960a6 100644 --- a/codex-rs/tui/src/iterm_session_status.rs +++ b/codex-rs/tui/src/terminal_program_status.rs @@ -1,9 +1,10 @@ -//! Emits coarse Codex lifecycle state through iTerm2's OSC 21337 protocol. +//! Emits coarse Codex lifecycle state through terminal status protocols. //! -//! This first integration is intentionally limited to direct iTerm2 sessions. -//! The payload includes bounded activity text from the existing TUI status. -//! A process-wide cache matches the terminal session's ownership and prevents -//! stale state when the active chat changes. +//! OSC 7501 is safe to send to any terminal and reports only the lifecycle +//! state and app name. Direct iTerm2 sessions also receive the existing OSC +//! 21337 status, including bounded activity text. A process-wide cache matches +//! the terminal session's ownership and prevents stale state when the active +//! chat changes. use std::fmt; use std::io; @@ -21,7 +22,7 @@ use ratatui::crossterm::execute; const NO_EMITTED_STATUS: u8 = 0; const INVALIDATED_EMITTED_STATUS: u8 = u8::MAX; static SESSION_STATUS_STATE: SessionStatusState = SessionStatusState::new(); -static ITERM_SESSION_STATUS_SUPPORTED: OnceLock = OnceLock::new(); +static TERMINAL_PROGRAM_STATUS_SUPPORT: OnceLock = OnceLock::new(); struct SessionStatusState { last_emitted: Mutex, @@ -75,58 +76,92 @@ impl SessionStatusState { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[repr(u8)] -pub(crate) enum ItermSessionStatus { - Idle = 1, - Working = 2, - Waiting = 3, +struct TerminalProgramStatusSupport { + osc_7501: bool, + osc_21337: bool, } -impl ItermSessionStatus { - fn label(self) -> &'static str { +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub(crate) enum ProgramStatus { + Idle = 1, + Working = 2, + Blocked = 3, +} + +impl ProgramStatus { + fn iterm_label(self) -> &'static str { match self { Self::Idle => "Idle", Self::Working => "Working", - Self::Waiting => "Waiting", + Self::Blocked => "Waiting", } } - fn indicator(self) -> &'static str { + fn iterm_indicator(self) -> &'static str { match self { Self::Idle => "#00d75f", Self::Working => "#ff9500", - Self::Waiting => "#5f87ff", + Self::Blocked => "#5f87ff", + } + } + + fn protocol_state(self) -> &'static str { + match self { + Self::Idle => "idle", + Self::Working => "working", + Self::Blocked => "blocked", } } } -pub(crate) fn set_iterm_session_status( - status: ItermSessionStatus, +pub(crate) fn set_terminal_program_status( + status: ProgramStatus, detail: Option<&str>, ) -> io::Result<()> { if cfg!(test) { return Ok(()); } - if !*ITERM_SESSION_STATUS_SUPPORTED.get_or_init(|| { - iterm_session_status_supported( - &terminal_info(), - /*stdout_is_terminal*/ stdout().is_terminal(), - /*screen_active*/ std::env::var_os("STY").is_some(), - ) - }) { + let support = current_terminal_program_status_support(); + if !support.osc_7501 { return Ok(()); } - let detail = sanitize_iterm_session_detail(detail.unwrap_or_default()); + let detail = if support.osc_21337 { + sanitize_iterm_session_detail(detail.unwrap_or_default()) + } else { + String::new() + }; SESSION_STATUS_STATE.update(status as u8, &detail, || { - execute!(stdout(), SetItermSessionStatus(status, &detail)) + execute!( + stdout(), + SetTerminalProgramStatus { + status, + iterm_detail: &detail, + emit_iterm_status: support.osc_21337, + } + ) }) } -pub(crate) fn clear_iterm_session_status() -> io::Result<()> { +pub(crate) fn clear_terminal_program_status() -> io::Result<()> { + if cfg!(test) { + return Ok(()); + } + + let support = current_terminal_program_status_support(); + if !support.osc_7501 { + return Ok(()); + } + SESSION_STATUS_STATE.update(NO_EMITTED_STATUS, "", || { - execute!(stdout(), ClearItermSessionStatus) + execute!( + stdout(), + ClearTerminalProgramStatus { + emit_iterm_status: support.osc_21337, + } + ) }) } @@ -134,7 +169,7 @@ pub(crate) fn clear_iterm_session_status() -> io::Result<()> { /// /// The next active draw will re-emit Codex's current status even if its /// lifecycle state did not change during the handoff. -pub(crate) fn invalidate_iterm_session_status() { +pub(crate) fn invalidate_terminal_program_status() { SESSION_STATUS_STATE.invalidate(); } @@ -153,35 +188,60 @@ fn sanitize_iterm_session_detail(detail: &str) -> String { escaped } -fn iterm_session_status_supported( +fn current_terminal_program_status_support() -> TerminalProgramStatusSupport { + *TERMINAL_PROGRAM_STATUS_SUPPORT.get_or_init(|| { + terminal_program_status_support( + &terminal_info(), + /*stdout_is_terminal*/ stdout().is_terminal(), + /*screen_active*/ std::env::var_os("STY").is_some(), + ) + }) +} + +fn terminal_program_status_support( terminal: &TerminalInfo, stdout_is_terminal: bool, screen_active: bool, -) -> bool { - stdout_is_terminal +) -> TerminalProgramStatusSupport { + let osc_21337 = stdout_is_terminal && terminal.name == TerminalName::Iterm2 && terminal.multiplexer.is_none() - && !screen_active + && !screen_active; + TerminalProgramStatusSupport { + osc_7501: stdout_is_terminal, + osc_21337, + } } #[derive(Clone, Copy, Debug)] -struct SetItermSessionStatus<'a>(ItermSessionStatus, &'a str); +struct SetTerminalProgramStatus<'a> { + status: ProgramStatus, + iterm_detail: &'a str, + emit_iterm_status: bool, +} -impl Command for SetItermSessionStatus<'_> { +impl Command for SetTerminalProgramStatus<'_> { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + if self.emit_iterm_status { + write!( + f, + "\x1b]21337;status={};indicator={};status-color=;detail={}\x07", + self.status.iterm_label(), + self.status.iterm_indicator(), + self.iterm_detail, + )?; + } write!( f, - "\x1b]21337;status={};indicator={};status-color=;detail={}\x07", - self.0.label(), - self.0.indicator(), - self.1, + "\x1b]7501;state={}:app=codex\x1b\\", + self.status.protocol_state(), ) } #[cfg(windows)] fn execute_winapi(&self) -> io::Result<()> { Err(io::Error::other( - "tried to set iTerm2 session status using WinAPI; use ANSI instead", + "tried to set terminal program status using WinAPI; use ANSI instead", )) } @@ -192,17 +252,22 @@ impl Command for SetItermSessionStatus<'_> { } #[derive(Clone, Copy, Debug)] -struct ClearItermSessionStatus; +struct ClearTerminalProgramStatus { + emit_iterm_status: bool, +} -impl Command for ClearItermSessionStatus { +impl Command for ClearTerminalProgramStatus { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, "\x1b]21337;status=;indicator=;status-color=;detail=\x07") + if self.emit_iterm_status { + write!(f, "\x1b]21337;status=;indicator=;status-color=;detail=\x07")?; + } + write!(f, "\x1b]7501;state=clear\x1b\\") } #[cfg(windows)] fn execute_winapi(&self) -> io::Result<()> { Err(io::Error::other( - "tried to clear iTerm2 session status using WinAPI; use ANSI instead", + "tried to clear terminal program status using WinAPI; use ANSI instead", )) } diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 3ce906fc18..434ac1140b 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -408,7 +408,7 @@ pub fn restore_after_exit() -> Result<()> { if let Err(err) = terminal_stderr::finish() { first_error.get_or_insert(err); } - if let Err(err) = crate::iterm_session_status::clear_iterm_session_status() { + if let Err(err) = crate::terminal_program_status::clear_terminal_program_status() { first_error.get_or_insert(err); } @@ -976,6 +976,9 @@ impl Tui { if let Err(err) = terminal_stderr::pause() { tracing::warn!("failed to restore terminal stderr before external program: {err}"); } + if let Err(err) = crate::terminal_program_status::clear_terminal_program_status() { + tracing::debug!(error = %err, "failed to clear terminal program status before external program"); + } let output = f().await; @@ -999,7 +1002,7 @@ impl Tui { let _ = self.enter_alt_screen(); } - crate::iterm_session_status::invalidate_iterm_session_status(); + crate::terminal_program_status::invalidate_terminal_program_status(); self.resume_events(); self.schedule_screen_size_recheck(Duration::ZERO); output diff --git a/codex-rs/tui/src/tui/job_control.rs b/codex-rs/tui/src/tui/job_control.rs index 56d099cc0f..4c5a8e8331 100644 --- a/codex-rs/tui/src/tui/job_control.rs +++ b/codex-rs/tui/src/tui/job_control.rs @@ -69,11 +69,11 @@ impl SuspendContext { } let y = self.suspend_cursor_y.load(Ordering::Relaxed); let _ = execute!(stdout(), MoveTo(0, y), Show); - if let Err(err) = crate::iterm_session_status::clear_iterm_session_status() { - tracing::debug!(error = %err, "failed to clear iTerm2 session status before suspend"); + if let Err(err) = crate::terminal_program_status::clear_terminal_program_status() { + tracing::debug!(error = %err, "failed to clear terminal program status before suspend"); } let suspend_result = suspend_process(); - crate::iterm_session_status::invalidate_iterm_session_status(); + crate::terminal_program_status::invalidate_terminal_program_status(); suspend_result?; super::reapply_raw_mode_after_resume()?;