diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 6236805195..b5e839c32f 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -44,6 +44,7 @@ use codex_core::plugins::PluginsManager; use codex_core::web_search::web_search_detail; use codex_otel::RuntimeMetricsSummary; use codex_protocol::account::PlanType; +use codex_protocol::config_types::ServiceTier; use codex_protocol::mcp::Resource; use codex_protocol::mcp::ResourceTemplate; use codex_protocol::models::WebSearchAction; @@ -1099,7 +1100,12 @@ pub(crate) fn new_session_info( } else { if config.show_tooltips && let Some(tooltips) = tooltip_override - .or_else(|| tooltips::get_tooltip(auth_plan)) + .or_else(|| { + tooltips::get_tooltip( + auth_plan, + matches!(config.service_tier, Some(ServiceTier::Fast)), + ) + }) .map(TooltipHistoryCell::new) { parts.push(Box::new(tooltips)); diff --git a/codex-rs/tui/src/tooltips.rs b/codex-rs/tui/src/tooltips.rs index 0600bd4165..4cdfeced5b 100644 --- a/codex-rs/tui/src/tooltips.rs +++ b/codex-rs/tui/src/tooltips.rs @@ -7,9 +7,12 @@ const ANNOUNCEMENT_TIP_URL: &str = "https://raw.githubusercontent.com/openai/codex/main/announcement_tip.toml"; const IS_MACOS: bool = cfg!(target_os = "macos"); +const IS_WINDOWS: bool = cfg!(target_os = "windows"); const PAID_TOOLTIP: &str = "*New* Try the **Codex App** with 2x rate limits until *April 2nd*. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true"; +const PAID_TOOLTIP_WINDOWS: &str = "*New* Try the **Codex App**, now available on **Windows**, with 2x rate limits until *April 2nd*. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true"; const PAID_TOOLTIP_NON_MAC: &str = "*New* 2x rate limits until *April 2nd*."; +const FAST_TOOLTIP: &str = "*New* Use **/fast** to enable our fastest inference at 2X plan usage."; const OTHER_TOOLTIP: &str = "*New* Build faster with the **Codex App**. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true"; const OTHER_TOOLTIP_NON_MAC: &str = "*New* Build faster with Codex."; const FREE_GO_TOOLTIP: &str = @@ -25,7 +28,7 @@ lazy_static! { if line.is_empty() || line.starts_with('#') { return false; } - if !IS_MACOS && line.contains("codex app") { + if !IS_MACOS && !IS_WINDOWS && line.contains("codex app") { return false; } true @@ -47,7 +50,7 @@ fn experimental_tooltips() -> Vec<&'static str> { } /// Pick a random tooltip to show to the user when starting Codex. -pub(crate) fn get_tooltip(plan: Option) -> Option { +pub(crate) fn get_tooltip(plan: Option, fast_mode_enabled: bool) -> Option { let mut rng = rand::rng(); if let Some(announcement) = announcement::fetch_announcement_tip() { @@ -62,12 +65,7 @@ pub(crate) fn get_tooltip(plan: Option) -> Option { | Some(PlanType::Team) | Some(PlanType::Enterprise) | Some(PlanType::Pro) => { - let tooltip = if IS_MACOS { - PAID_TOOLTIP - } else { - PAID_TOOLTIP_NON_MAC - }; - return Some(tooltip.to_string()); + return Some(pick_paid_tooltip(&mut rng, fast_mode_enabled).to_string()); } Some(PlanType::Go) | Some(PlanType::Free) => { return Some(FREE_GO_TOOLTIP.to_string()); @@ -86,6 +84,28 @@ pub(crate) fn get_tooltip(plan: Option) -> Option { pick_tooltip(&mut rng).map(str::to_string) } +fn paid_app_tooltip() -> &'static str { + if IS_MACOS { + PAID_TOOLTIP + } else if IS_WINDOWS { + PAID_TOOLTIP_WINDOWS + } else { + PAID_TOOLTIP_NON_MAC + } +} + +/// Paid users spend most startup sessions in a dedicated promo slot rather than the +/// generic random tip pool. Keep this business logic explicit: we currently split +/// that slot between the app promo and Fast mode, but suppress the Fast promo once +/// the user already has Fast mode enabled. +fn pick_paid_tooltip(rng: &mut R, fast_mode_enabled: bool) -> &'static str { + if fast_mode_enabled || rng.random_bool(0.5) { + paid_app_tooltip() + } else { + FAST_TOOLTIP + } +} + fn pick_tooltip(rng: &mut R) -> Option<&'static str> { if ALL_TOOLTIPS.is_empty() { None @@ -264,6 +284,31 @@ mod tests { assert_eq!(expected, pick_tooltip(&mut rng)); } + #[test] + fn paid_tooltip_pool_rotates_between_promos() { + let mut seen = std::collections::BTreeSet::new(); + for seed in 0..32 { + let mut rng = StdRng::seed_from_u64(seed); + seen.insert(pick_paid_tooltip(&mut rng, false)); + } + + let expected = std::collections::BTreeSet::from([paid_app_tooltip(), FAST_TOOLTIP]); + assert_eq!(seen, expected); + } + + #[test] + fn paid_tooltip_pool_skips_fast_when_fast_mode_is_enabled() { + let mut seen = std::collections::BTreeSet::new(); + for seed in 0..8 { + let mut rng = StdRng::seed_from_u64(seed); + seen.insert(pick_paid_tooltip(&mut rng, true)); + } + + let expected = std::collections::BTreeSet::from([paid_app_tooltip()]); + assert_eq!(seen, expected); + assert!(!seen.contains(&FAST_TOOLTIP)); + } + #[test] fn announcement_tip_toml_picks_last_matching() { let toml = r#"