Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use codex_exec::Cli as ExecCli;
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
use codex_tui::AppExitInfo;
use codex_tui::Cli as TuiCli;
use codex_tui::UpdateAction;
use codex_tui::updates::UpdateAction;
use owo_colors::OwoColorize;
use std::path::PathBuf;
use supports_color::Stream;
Expand Down
21 changes: 19 additions & 2 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::UpdateAction;
use crate::app_backtrack::BacktrackState;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
Expand All @@ -13,6 +12,7 @@ use crate::render::highlight::highlight_bash_to_lines;
use crate::resume_picker::ResumeSelection;
use crate::tui;
use crate::tui::TuiEvent;
use crate::updates::UpdateAction;
use codex_ansi_escape::ansi_escape_line;
use codex_core::AuthManager;
use codex_core::ConversationManager;
Expand All @@ -38,7 +38,9 @@ use std::thread;
use std::time::Duration;
use tokio::select;
use tokio::sync::mpsc::unbounded_channel;
// use uuid::Uuid;

#[cfg(not(debug_assertions))]
use crate::history_cell::UpdateAvailableHistoryCell;

#[derive(Debug, Clone)]
pub struct AppExitInfo {
Expand Down Expand Up @@ -79,6 +81,7 @@ pub(crate) struct App {
}

impl App {
#[allow(clippy::too_many_arguments)]
pub async fn run(
tui: &mut tui::Tui,
auth_manager: Arc<AuthManager>,
Expand Down Expand Up @@ -141,6 +144,8 @@ impl App {
};

let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
#[cfg(not(debug_assertions))]
let upgrade_version = crate::updates::get_upgrade_version(&config);

let mut app = Self {
server: conversation_manager,
Expand All @@ -160,6 +165,18 @@ impl App {
pending_update_action: None,
};

#[cfg(not(debug_assertions))]
if let Some(latest_version) = upgrade_version {
app.handle_event(
tui,
AppEvent::InsertHistoryCell(Box::new(UpdateAvailableHistoryCell::new(
latest_version,
crate::updates::get_update_action(),
))),
)
.await?;
}

let tui_events = tui.event_stream();
tokio::pin!(tui_events);

Expand Down
63 changes: 63 additions & 0 deletions codex-rs/tui/src/history_cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::style::user_message_style;
use crate::text_formatting::format_and_truncate_tool_result;
use crate::text_formatting::truncate_text;
use crate::ui_consts::LIVE_PREFIX_COLS;
use crate::updates::UpdateAction;
use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_line;
use crate::wrapping::word_wrap_lines;
Expand Down Expand Up @@ -270,6 +271,68 @@ impl HistoryCell for PlainHistoryCell {
}
}

#[cfg_attr(debug_assertions, allow(dead_code))]
#[derive(Debug)]
pub(crate) struct UpdateAvailableHistoryCell {
latest_version: String,
update_action: Option<UpdateAction>,
}

#[cfg_attr(debug_assertions, allow(dead_code))]
impl UpdateAvailableHistoryCell {
pub(crate) fn new(latest_version: String, update_action: Option<UpdateAction>) -> Self {
Self {
latest_version,
update_action,
}
}
}

impl HistoryCell for UpdateAvailableHistoryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
use ratatui::style::Stylize as _;
use ratatui::text::Line;

let update_instruction = if let Some(update_action) = self.update_action {
Line::from(vec![
"Run ".into(),
update_action.command_str().cyan(),
" to update.".into(),
])
} else {
Line::from(vec![
"See ".into(),
"https://github.com/openai/codex".cyan().underlined(),
" for installation options.".into(),
])
};

let current_version = env!("CARGO_PKG_VERSION");
let content_lines: Vec<Line<'static>> = vec![
Line::from(vec![
padded_emoji("✨").bold().cyan(),
"Update available!".bold().cyan(),
" ".into(),
format!("{current_version} -> {}", self.latest_version).bold(),
]),
update_instruction,
Line::from(""),
Line::from("See full release notes:"),
Line::from(
"https://github.com/openai/codex/releases/latest"
.cyan()
.underlined(),
),
];

let line_max_width = content_lines.iter().map(Line::width).max().unwrap_or(0);
let inner_width = line_max_width
.min(usize::from(width.saturating_sub(4)))
.max(1);
with_border_with_inner_width(content_lines, inner_width)
}
}

#[derive(Debug)]
pub(crate) struct PrefixedWrappedHistoryCell {
text: Text<'static>,
Expand Down
124 changes: 1 addition & 123 deletions codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,45 +68,14 @@ mod text_formatting;
mod tui;
mod ui_consts;
mod update_prompt;
pub mod updates;
mod version;

/// Update action the CLI should perform after the TUI exits.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateAction {
/// Update via `npm install -g @openai/codex@latest`.
NpmGlobalLatest,
/// Update via `bun install -g @openai/codex@latest`.
BunGlobalLatest,
/// Update via `brew upgrade codex`.
BrewUpgrade,
}

impl UpdateAction {
/// Returns the list of command-line arguments for invoking the update.
pub fn command_args(&self) -> (&'static str, &'static [&'static str]) {
match self {
UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex@latest"]),
UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex@latest"]),
UpdateAction::BrewUpgrade => ("brew", &["upgrade", "codex"]),
}
}

/// Returns string representation of the command-line arguments for invoking the update.
pub fn command_str(&self) -> String {
let (command, args) = self.command_args();
let args_str = args.join(" ");
format!("{command} {args_str}")
}
}

mod wrapping;

#[cfg(test)]
pub mod test_backend;

#[cfg(not(debug_assertions))]
mod updates;

use crate::onboarding::TrustDirectorySelection;
use crate::onboarding::WSL_INSTRUCTIONS;
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
Expand Down Expand Up @@ -304,56 +273,6 @@ async fn run_ratatui_app(
}
}

// Show update banner in terminal history (instead of stderr) so it is visible
// within the TUI scrollback. Building spans keeps styling consistent.
#[cfg(not(debug_assertions))]
if let Some(latest_version) = updates::get_upgrade_version(&initial_config) {
use crate::history_cell::padded_emoji;
use crate::history_cell::with_border_with_inner_width;
use ratatui::style::Stylize as _;
use ratatui::text::Line;

let current_version = env!("CARGO_PKG_VERSION");

let mut content_lines: Vec<Line<'static>> = vec![
Line::from(vec![
padded_emoji("✨").bold().cyan(),
"Update available!".bold().cyan(),
" ".into(),
format!("{current_version} -> {latest_version}.").bold(),
]),
Line::from(""),
Line::from("See full release notes:"),
Line::from(""),
Line::from(
"https://github.com/openai/codex/releases/latest"
.cyan()
.underlined(),
),
Line::from(""),
];

if let Some(update_action) = get_update_action() {
content_lines.push(Line::from(vec![
"Run ".into(),
update_action.command_str().cyan(),
" to update.".into(),
]));
} else {
content_lines.push(Line::from(vec![
"See ".into(),
"https://github.com/openai/codex".cyan().underlined(),
" for installation options.".into(),
]));
}

let viewport_width = tui.terminal.viewport_area.width as usize;
let inner_width = viewport_width.saturating_sub(4).max(1);
let mut lines = with_border_with_inner_width(content_lines, inner_width);
lines.push("".into());
tui.insert_history_lines(lines);
}

// Initialize high-fidelity session event logging if enabled.
session_log::maybe_init(&initial_config);

Expand Down Expand Up @@ -472,47 +391,6 @@ async fn run_ratatui_app(
app_result
}

/// Get the update action from the environment.
/// Returns `None` if not managed by npm, bun, or brew.
#[cfg(not(debug_assertions))]
pub(crate) fn get_update_action() -> Option<UpdateAction> {
let exe = std::env::current_exe().unwrap_or_default();
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
if managed_by_npm {
Some(UpdateAction::NpmGlobalLatest)
} else if managed_by_bun {
Some(UpdateAction::BunGlobalLatest)
} else if cfg!(target_os = "macos")
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
{
Some(UpdateAction::BrewUpgrade)
} else {
None
}
}

#[test]
#[cfg(not(debug_assertions))]
fn test_get_update_action() {
let prev = std::env::var_os("CODEX_MANAGED_BY_NPM");

// First: no npm var -> expect None (we do not run from brew in CI)
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
assert_eq!(get_update_action(), None);

// Then: with npm var -> expect NpmGlobalLatest
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", "1") };
assert_eq!(get_update_action(), Some(UpdateAction::NpmGlobalLatest));

// Restore prior value to avoid leaking state
if let Some(v) = prev {
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", v) };
} else {
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
}
}

#[expect(
clippy::print_stderr,
reason = "TUI should no longer be displayed, so we can write to stderr."
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/update_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(crate) async fn run_update_prompt_if_needed(
let Some(latest_version) = updates::get_upgrade_version_for_popup(config) else {
return Ok(UpdatePromptOutcome::Continue);
};
let Some(update_action) = crate::get_update_action() else {
let Some(update_action) = crate::updates::get_update_action() else {
return Ok(UpdatePromptOutcome::Continue);
};

Expand Down
69 changes: 67 additions & 2 deletions codex-rs/tui/src/updates.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![cfg(any(not(debug_assertions), test))]

use chrono::DateTime;
use chrono::Duration;
use chrono::Utc;
Expand Down Expand Up @@ -142,6 +140,53 @@ fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
Some((maj, min, pat))
}

/// Update action the CLI should perform after the TUI exits.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateAction {
/// Update via `npm install -g @openai/codex@latest`.
NpmGlobalLatest,
/// Update via `bun install -g @openai/codex@latest`.
BunGlobalLatest,
/// Update via `brew upgrade codex`.
BrewUpgrade,
}

#[cfg(any(not(debug_assertions), test))]
pub(crate) fn get_update_action() -> Option<UpdateAction> {
let exe = std::env::current_exe().unwrap_or_default();
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
if managed_by_npm {
Some(UpdateAction::NpmGlobalLatest)
} else if managed_by_bun {
Some(UpdateAction::BunGlobalLatest)
} else if cfg!(target_os = "macos")
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
{
Some(UpdateAction::BrewUpgrade)
} else {
None
}
}

impl UpdateAction {
/// Returns the list of command-line arguments for invoking the update.
pub fn command_args(self) -> (&'static str, &'static [&'static str]) {
match self {
UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex@latest"]),
UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex@latest"]),
UpdateAction::BrewUpgrade => ("brew", &["upgrade", "codex"]),
}
}

/// Returns string representation of the command-line arguments for invoking the update.
pub fn command_str(self) -> String {
let (command, args) = self.command_args();
let args_str = args.join(" ");
format!("{command} {args_str}")
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -165,4 +210,24 @@ mod tests {
assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3)));
assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true));
}

#[test]
fn test_get_update_action() {
let prev = std::env::var_os("CODEX_MANAGED_BY_NPM");

// First: no npm var -> expect None (we do not run from brew in CI)
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
assert_eq!(get_update_action(), None);

// Then: with npm var -> expect NpmGlobalLatest
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", "1") };
assert_eq!(get_update_action(), Some(UpdateAction::NpmGlobalLatest));

// Restore prior value to avoid leaking state
if let Some(v) = prev {
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", v) };
} else {
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
}
}
}
Loading