Skip to content

Commit c5630e0

Browse files
wip
1 parent 4b01f0f commit c5630e0

File tree

6 files changed

+152
-129
lines changed

6 files changed

+152
-129
lines changed

codex-rs/cli/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use codex_exec::Cli as ExecCli;
1919
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
2020
use codex_tui::AppExitInfo;
2121
use codex_tui::Cli as TuiCli;
22-
use codex_tui::UpdateAction;
22+
use codex_tui::updates::UpdateAction;
2323
use owo_colors::OwoColorize;
2424
use std::path::PathBuf;
2525
use supports_color::Stream;

codex-rs/tui/src/app.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::UpdateAction;
21
use crate::app_backtrack::BacktrackState;
32
use crate::app_event::AppEvent;
43
use crate::app_event_sender::AppEventSender;
@@ -13,6 +12,7 @@ use crate::render::highlight::highlight_bash_to_lines;
1312
use crate::resume_picker::ResumeSelection;
1413
use crate::tui;
1514
use crate::tui::TuiEvent;
15+
use crate::updates::UpdateAction;
1616
use codex_ansi_escape::ansi_escape_line;
1717
use codex_core::AuthManager;
1818
use codex_core::ConversationManager;
@@ -38,7 +38,9 @@ use std::thread;
3838
use std::time::Duration;
3939
use tokio::select;
4040
use tokio::sync::mpsc::unbounded_channel;
41-
// use uuid::Uuid;
41+
42+
#[cfg(not(debug_assertions))]
43+
use crate::history_cell::UpdateAvailableHistoryCell;
4244

4345
#[derive(Debug, Clone)]
4446
pub struct AppExitInfo {
@@ -79,6 +81,7 @@ pub(crate) struct App {
7981
}
8082

8183
impl App {
84+
#[allow(clippy::too_many_arguments)]
8285
pub async fn run(
8386
tui: &mut tui::Tui,
8487
auth_manager: Arc<AuthManager>,
@@ -141,6 +144,8 @@ impl App {
141144
};
142145

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

145150
let mut app = Self {
146151
server: conversation_manager,
@@ -160,6 +165,18 @@ impl App {
160165
pending_update_action: None,
161166
};
162167

168+
#[cfg(not(debug_assertions))]
169+
if let Some(latest_version) = upgrade_version {
170+
app.handle_event(
171+
tui,
172+
AppEvent::InsertHistoryCell(Box::new(UpdateAvailableHistoryCell::new(
173+
latest_version,
174+
crate::updates::get_update_action(),
175+
))),
176+
)
177+
.await?;
178+
}
179+
163180
let tui_events = tui.event_stream();
164181
tokio::pin!(tui_events);
165182

codex-rs/tui/src/history_cell.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::style::user_message_style;
1616
use crate::text_formatting::format_and_truncate_tool_result;
1717
use crate::text_formatting::truncate_text;
1818
use crate::ui_consts::LIVE_PREFIX_COLS;
19+
use crate::updates::UpdateAction;
1920
use crate::wrapping::RtOptions;
2021
use crate::wrapping::word_wrap_line;
2122
use crate::wrapping::word_wrap_lines;
@@ -270,6 +271,68 @@ impl HistoryCell for PlainHistoryCell {
270271
}
271272
}
272273

274+
#[cfg_attr(debug_assertions, allow(dead_code))]
275+
#[derive(Debug)]
276+
pub(crate) struct UpdateAvailableHistoryCell {
277+
latest_version: String,
278+
update_action: Option<UpdateAction>,
279+
}
280+
281+
#[cfg_attr(debug_assertions, allow(dead_code))]
282+
impl UpdateAvailableHistoryCell {
283+
pub(crate) fn new(latest_version: String, update_action: Option<UpdateAction>) -> Self {
284+
Self {
285+
latest_version,
286+
update_action,
287+
}
288+
}
289+
}
290+
291+
impl HistoryCell for UpdateAvailableHistoryCell {
292+
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
293+
use ratatui::style::Stylize as _;
294+
use ratatui::text::Line;
295+
296+
let update_instruction = if let Some(update_action) = self.update_action {
297+
Line::from(vec![
298+
"Run ".into(),
299+
update_action.command_str().cyan(),
300+
" to update.".into(),
301+
])
302+
} else {
303+
Line::from(vec![
304+
"See ".into(),
305+
"https://github.com/openai/codex".cyan().underlined(),
306+
" for installation options.".into(),
307+
])
308+
};
309+
310+
let current_version = env!("CARGO_PKG_VERSION");
311+
let content_lines: Vec<Line<'static>> = vec![
312+
Line::from(vec![
313+
padded_emoji("✨").bold().cyan(),
314+
"Update available!".bold().cyan(),
315+
" ".into(),
316+
format!("{current_version} -> {}", self.latest_version).bold(),
317+
]),
318+
update_instruction,
319+
Line::from(""),
320+
Line::from("See full release notes:"),
321+
Line::from(
322+
"https://github.com/openai/codex/releases/latest"
323+
.cyan()
324+
.underlined(),
325+
),
326+
];
327+
328+
let line_max_width = content_lines.iter().map(Line::width).max().unwrap_or(0);
329+
let inner_width = line_max_width
330+
.min(usize::from(width.saturating_sub(4)))
331+
.max(1);
332+
with_border_with_inner_width(content_lines, inner_width)
333+
}
334+
}
335+
273336
#[derive(Debug)]
274337
pub(crate) struct PrefixedWrappedHistoryCell {
275338
text: Text<'static>,

codex-rs/tui/src/lib.rs

Lines changed: 1 addition & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -68,45 +68,14 @@ mod text_formatting;
6868
mod tui;
6969
mod ui_consts;
7070
mod update_prompt;
71+
pub mod updates;
7172
mod version;
7273

73-
/// Update action the CLI should perform after the TUI exits.
74-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75-
pub enum UpdateAction {
76-
/// Update via `npm install -g @openai/codex@latest`.
77-
NpmGlobalLatest,
78-
/// Update via `bun install -g @openai/codex@latest`.
79-
BunGlobalLatest,
80-
/// Update via `brew upgrade codex`.
81-
BrewUpgrade,
82-
}
83-
84-
impl UpdateAction {
85-
/// Returns the list of command-line arguments for invoking the update.
86-
pub fn command_args(&self) -> (&'static str, &'static [&'static str]) {
87-
match self {
88-
UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex@latest"]),
89-
UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex@latest"]),
90-
UpdateAction::BrewUpgrade => ("brew", &["upgrade", "codex"]),
91-
}
92-
}
93-
94-
/// Returns string representation of the command-line arguments for invoking the update.
95-
pub fn command_str(&self) -> String {
96-
let (command, args) = self.command_args();
97-
let args_str = args.join(" ");
98-
format!("{command} {args_str}")
99-
}
100-
}
101-
10274
mod wrapping;
10375

10476
#[cfg(test)]
10577
pub mod test_backend;
10678

107-
#[cfg(not(debug_assertions))]
108-
mod updates;
109-
11079
use crate::onboarding::TrustDirectorySelection;
11180
use crate::onboarding::WSL_INSTRUCTIONS;
11281
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
@@ -304,56 +273,6 @@ async fn run_ratatui_app(
304273
}
305274
}
306275

307-
// Show update banner in terminal history (instead of stderr) so it is visible
308-
// within the TUI scrollback. Building spans keeps styling consistent.
309-
#[cfg(not(debug_assertions))]
310-
if let Some(latest_version) = updates::get_upgrade_version(&initial_config) {
311-
use crate::history_cell::padded_emoji;
312-
use crate::history_cell::with_border_with_inner_width;
313-
use ratatui::style::Stylize as _;
314-
use ratatui::text::Line;
315-
316-
let current_version = env!("CARGO_PKG_VERSION");
317-
318-
let mut content_lines: Vec<Line<'static>> = vec![
319-
Line::from(vec![
320-
padded_emoji("✨").bold().cyan(),
321-
"Update available!".bold().cyan(),
322-
" ".into(),
323-
format!("{current_version} -> {latest_version}.").bold(),
324-
]),
325-
Line::from(""),
326-
Line::from("See full release notes:"),
327-
Line::from(""),
328-
Line::from(
329-
"https://github.com/openai/codex/releases/latest"
330-
.cyan()
331-
.underlined(),
332-
),
333-
Line::from(""),
334-
];
335-
336-
if let Some(update_action) = get_update_action() {
337-
content_lines.push(Line::from(vec![
338-
"Run ".into(),
339-
update_action.command_str().cyan(),
340-
" to update.".into(),
341-
]));
342-
} else {
343-
content_lines.push(Line::from(vec![
344-
"See ".into(),
345-
"https://github.com/openai/codex".cyan().underlined(),
346-
" for installation options.".into(),
347-
]));
348-
}
349-
350-
let viewport_width = tui.terminal.viewport_area.width as usize;
351-
let inner_width = viewport_width.saturating_sub(4).max(1);
352-
let mut lines = with_border_with_inner_width(content_lines, inner_width);
353-
lines.push("".into());
354-
tui.insert_history_lines(lines);
355-
}
356-
357276
// Initialize high-fidelity session event logging if enabled.
358277
session_log::maybe_init(&initial_config);
359278

@@ -472,47 +391,6 @@ async fn run_ratatui_app(
472391
app_result
473392
}
474393

475-
/// Get the update action from the environment.
476-
/// Returns `None` if not managed by npm, bun, or brew.
477-
#[cfg(not(debug_assertions))]
478-
pub(crate) fn get_update_action() -> Option<UpdateAction> {
479-
let exe = std::env::current_exe().unwrap_or_default();
480-
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
481-
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
482-
if managed_by_npm {
483-
Some(UpdateAction::NpmGlobalLatest)
484-
} else if managed_by_bun {
485-
Some(UpdateAction::BunGlobalLatest)
486-
} else if cfg!(target_os = "macos")
487-
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
488-
{
489-
Some(UpdateAction::BrewUpgrade)
490-
} else {
491-
None
492-
}
493-
}
494-
495-
#[test]
496-
#[cfg(not(debug_assertions))]
497-
fn test_get_update_action() {
498-
let prev = std::env::var_os("CODEX_MANAGED_BY_NPM");
499-
500-
// First: no npm var -> expect None (we do not run from brew in CI)
501-
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
502-
assert_eq!(get_update_action(), None);
503-
504-
// Then: with npm var -> expect NpmGlobalLatest
505-
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", "1") };
506-
assert_eq!(get_update_action(), Some(UpdateAction::NpmGlobalLatest));
507-
508-
// Restore prior value to avoid leaking state
509-
if let Some(v) = prev {
510-
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", v) };
511-
} else {
512-
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
513-
}
514-
}
515-
516394
#[expect(
517395
clippy::print_stderr,
518396
reason = "TUI should no longer be displayed, so we can write to stderr."

codex-rs/tui/src/update_prompt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub(crate) async fn run_update_prompt_if_needed(
3939
let Some(latest_version) = updates::get_upgrade_version_for_popup(config) else {
4040
return Ok(UpdatePromptOutcome::Continue);
4141
};
42-
let Some(update_action) = crate::get_update_action() else {
42+
let Some(update_action) = crate::updates::get_update_action() else {
4343
return Ok(UpdatePromptOutcome::Continue);
4444
};
4545

codex-rs/tui/src/updates.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#![cfg(any(not(debug_assertions), test))]
2-
31
use chrono::DateTime;
42
use chrono::Duration;
53
use chrono::Utc;
@@ -142,6 +140,53 @@ fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
142140
Some((maj, min, pat))
143141
}
144142

143+
/// Update action the CLI should perform after the TUI exits.
144+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145+
pub enum UpdateAction {
146+
/// Update via `npm install -g @openai/codex@latest`.
147+
NpmGlobalLatest,
148+
/// Update via `bun install -g @openai/codex@latest`.
149+
BunGlobalLatest,
150+
/// Update via `brew upgrade codex`.
151+
BrewUpgrade,
152+
}
153+
154+
#[cfg(any(not(debug_assertions), test))]
155+
pub(crate) fn get_update_action() -> Option<UpdateAction> {
156+
let exe = std::env::current_exe().unwrap_or_default();
157+
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
158+
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
159+
if managed_by_npm {
160+
Some(UpdateAction::NpmGlobalLatest)
161+
} else if managed_by_bun {
162+
Some(UpdateAction::BunGlobalLatest)
163+
} else if cfg!(target_os = "macos")
164+
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
165+
{
166+
Some(UpdateAction::BrewUpgrade)
167+
} else {
168+
None
169+
}
170+
}
171+
172+
impl UpdateAction {
173+
/// Returns the list of command-line arguments for invoking the update.
174+
pub fn command_args(self) -> (&'static str, &'static [&'static str]) {
175+
match self {
176+
UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex@latest"]),
177+
UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex@latest"]),
178+
UpdateAction::BrewUpgrade => ("brew", &["upgrade", "codex"]),
179+
}
180+
}
181+
182+
/// Returns string representation of the command-line arguments for invoking the update.
183+
pub fn command_str(self) -> String {
184+
let (command, args) = self.command_args();
185+
let args_str = args.join(" ");
186+
format!("{command} {args_str}")
187+
}
188+
}
189+
145190
#[cfg(test)]
146191
mod tests {
147192
use super::*;
@@ -165,4 +210,24 @@ mod tests {
165210
assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3)));
166211
assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true));
167212
}
213+
214+
#[test]
215+
fn test_get_update_action() {
216+
let prev = std::env::var_os("CODEX_MANAGED_BY_NPM");
217+
218+
// First: no npm var -> expect None (we do not run from brew in CI)
219+
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
220+
assert_eq!(get_update_action(), None);
221+
222+
// Then: with npm var -> expect NpmGlobalLatest
223+
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", "1") };
224+
assert_eq!(get_update_action(), Some(UpdateAction::NpmGlobalLatest));
225+
226+
// Restore prior value to avoid leaking state
227+
if let Some(v) = prev {
228+
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", v) };
229+
} else {
230+
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
231+
}
232+
}
168233
}

0 commit comments

Comments
 (0)