codex: persist availability NUX by model

This commit is contained in:
Ahmed Ibrahim
2026-02-26 19:47:00 -08:00
parent 3a753e0adf
commit fc242eee29
6 changed files with 88 additions and 56 deletions
+10 -10
View File
@@ -39,8 +39,8 @@ pub enum ConfigEdit {
SetNoticeHideModelMigrationPrompt(String, bool),
/// Record that a migration prompt was shown for an old->new model mapping.
RecordModelMigrationSeen { from: String, to: String },
/// Record that an availability NUX message was displayed.
RecordAvailabilityNuxDisplay { message: String },
/// Record that an availability NUX was displayed for a model.
RecordAvailabilityNuxDisplay { model: String },
/// Replace the entire `[mcp_servers]` table.
ReplaceMcpServers(BTreeMap<String, McpServerConfig>),
/// Set or clear a skill config entry under `[[skills.config]]`.
@@ -333,7 +333,7 @@ impl ConfigDocument {
&[Notice::TABLE_KEY, "model_migrations", from.as_str()],
value(to.clone()),
)),
ConfigEdit::RecordAvailabilityNuxDisplay { message } => {
ConfigEdit::RecordAvailabilityNuxDisplay { model } => {
let resolved = self.scoped_segments(
Scope::Global,
&[Notice::TABLE_KEY, "availability_nux_display_counts"],
@@ -342,10 +342,10 @@ impl ConfigDocument {
return Ok(false);
};
let current = table
.get(message.as_str())
.get(model.as_str())
.and_then(toml_edit::Item::as_integer)
.unwrap_or(0);
table[message.as_str()] = value(current + 1);
table[model.as_str()] = value(current + 1);
Ok(true)
}
ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged) => Ok(self.write_value(
@@ -805,9 +805,9 @@ impl ConfigEditsBuilder {
self
}
pub fn record_availability_nux_display(mut self, message: &str) -> Self {
pub fn record_availability_nux_display(mut self, model: &str) -> Self {
self.edits.push(ConfigEdit::RecordAvailabilityNuxDisplay {
message: message.to_string(),
model: model.to_string(),
});
self
}
@@ -1468,7 +1468,7 @@ gpt-5 = "gpt-5.1"
existing = "value"
[notice.availability_nux_display_counts]
"*New* Spark is now available to you." = 1
"spark" = 1
"#,
)
.expect("seed");
@@ -1476,7 +1476,7 @@ existing = "value"
codex_home,
None,
&[ConfigEdit::RecordAvailabilityNuxDisplay {
message: "*New* Spark is now available to you.".to_string(),
model: "spark".to_string(),
}],
)
.expect("persist");
@@ -1487,7 +1487,7 @@ existing = "value"
existing = "value"
[notice.availability_nux_display_counts]
"*New* Spark is now available to you." = 2
"spark" = 2
"#;
assert_eq!(contents, expected);
}
+3 -3
View File
@@ -2743,16 +2743,16 @@ impl App {
));
}
}
AppEvent::PersistAvailabilityNuxDisplayed { message } => {
AppEvent::PersistAvailabilityNuxDisplayed { model } => {
let count = self
.config
.notices
.availability_nux_display_counts
.entry(message.clone())
.entry(model.clone())
.or_default();
*count += 1;
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.record_availability_nux_display(message.as_str())
.record_availability_nux_display(model.as_str())
.apply()
.await
{
+1 -1
View File
@@ -337,7 +337,7 @@ pub(crate) enum AppEvent {
/// Persist that an availability NUX was displayed.
PersistAvailabilityNuxDisplayed {
message: String,
model: String,
},
/// Skip the next world-writable scan (one-shot) after a user-confirmed continue.
+6 -6
View File
@@ -1179,31 +1179,31 @@ impl ChatWidget {
displayed_startup_tips,
);
self.apply_session_info_cell(session_info_cell);
let displayed_availability_nux_messages: Vec<String> = if self.show_welcome_banner {
let displayed_availability_nux_models: Vec<String> = if self.show_welcome_banner {
startup_tips
.first_session_tips
.iter()
.filter_map(StartupTip::availability_nux_message)
.filter_map(StartupTip::availability_nux_model)
.map(str::to_string)
.collect()
} else {
startup_tips
.selected_tip
.iter()
.filter_map(StartupTip::availability_nux_message)
.filter_map(StartupTip::availability_nux_model)
.map(str::to_string)
.collect()
};
for message in displayed_availability_nux_messages {
for model in displayed_availability_nux_models {
let count = self
.config
.notices
.availability_nux_display_counts
.entry(message.clone())
.entry(model.clone())
.or_default();
*count += 1;
self.app_event_tx
.send(AppEvent::PersistAvailabilityNuxDisplayed { message });
.send(AppEvent::PersistAvailabilityNuxDisplayed { model });
}
if let Some(messages) = initial_messages {
+2 -4
View File
@@ -1892,9 +1892,7 @@ async fn session_configured_first_session_shows_model_nux_and_increments_display
chat.config
.notices
.availability_nux_display_counts
.get(
"*New* You're using **GPT Test**. Fast, high-reliability coding model. Use /model to compare or switch anytime.",
)
.get("gpt-5.1")
);
}
@@ -1938,7 +1936,7 @@ async fn session_configured_ineligible_model_does_not_increment_display_count()
chat.config
.notices
.availability_nux_display_counts
.get("*New* Spark is now available to you.")
.get("gpt-5.1")
);
}
+66 -32
View File
@@ -5,7 +5,6 @@ use codex_protocol::openai_models::ModelPreset;
use lazy_static::lazy_static;
use rand::Rng;
use std::collections::BTreeMap;
use std::collections::HashSet;
const ANNOUNCEMENT_TIP_URL: &str =
"https://raw.githubusercontent.com/openai/codex/main/announcement_tip.toml";
@@ -53,20 +52,20 @@ fn experimental_tooltips() -> Vec<&'static str> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StartupTip {
Generic(String),
AvailabilityNux { message: String },
AvailabilityNux { model: String, message: String },
}
impl StartupTip {
pub(crate) fn message(&self) -> String {
match self {
Self::Generic(message) | Self::AvailabilityNux { message } => message.clone(),
Self::Generic(message) | Self::AvailabilityNux { message, .. } => message.clone(),
}
}
pub(crate) fn availability_nux_message(&self) -> Option<&str> {
pub(crate) fn availability_nux_model(&self) -> Option<&str> {
match self {
Self::Generic(_) => None,
Self::AvailabilityNux { message } => Some(message.as_str()),
Self::AvailabilityNux { model, .. } => Some(model.as_str()),
}
}
}
@@ -188,25 +187,31 @@ fn availability_nux_tips(
models: &[ModelPreset],
availability_nux_display_counts: &BTreeMap<String, u32>,
) -> Vec<StartupTip> {
let mut seen_messages = HashSet::new();
models
.iter()
.filter_map(|preset| preset.availability_nux.as_ref())
.filter(|availability_nux| {
.filter_map(|preset| {
preset
.availability_nux
.as_ref()
.map(|availability_nux| (preset.model.as_str(), availability_nux))
})
.filter(|(model, _)| {
availability_nux_display_counts
.get(&availability_nux.message)
.get(*model)
.copied()
.unwrap_or(0)
< 4
})
.filter(|availability_nux| seen_messages.insert(availability_nux.message.clone()))
.map(startup_tip_from_availability_nux)
.map(|(model, availability_nux)| startup_tip_from_availability_nux(model, availability_nux))
.collect()
}
fn startup_tip_from_availability_nux(availability_nux: &ModelAvailabilityNux) -> StartupTip {
fn startup_tip_from_availability_nux(
model: &str,
availability_nux: &ModelAvailabilityNux,
) -> StartupTip {
StartupTip::AvailabilityNux {
model: model.to_string(),
message: availability_nux.message.clone(),
}
}
@@ -372,11 +377,16 @@ mod tests {
use rand::SeedableRng;
use rand::rngs::StdRng;
fn model_preset(availability_nux: Option<&str>, description: &str) -> ModelPreset {
fn model_preset(
model: &str,
display_name: &str,
availability_nux: Option<&str>,
description: &str,
) -> ModelPreset {
ModelPreset {
id: "gpt-test".to_string(),
model: "gpt-test".to_string(),
display_name: "GPT Test".to_string(),
id: model.to_string(),
model: model.to_string(),
display_name: display_name.to_string(),
description: description.to_string(),
default_reasoning_effort: codex_protocol::openai_models::ReasoningEffort::Medium,
supported_reasoning_efforts: vec![],
@@ -512,6 +522,8 @@ content = "This is a test announcement"
#[test]
fn first_session_eligible_model_returns_all_availability_nux_tips() {
let preset = model_preset(
"gpt-test",
"GPT Test",
Some("*New* Spark is now available to you."),
"Fast, high-reliability coding model.",
);
@@ -529,6 +541,7 @@ content = "This is a test announcement"
assert_eq!(
StartupTips {
first_session_tips: vec![StartupTip::AvailabilityNux {
model: "gpt-test".to_string(),
message: "*New* Spark is now available to you.".to_string(),
}],
selected_tip: None,
@@ -539,7 +552,7 @@ content = "This is a test announcement"
#[test]
fn first_session_ineligible_model_skips_tip() {
let preset = model_preset(None, "");
let preset = model_preset("gpt-test", "GPT Test", None, "");
let mut rng = StdRng::seed_from_u64(1);
let tips = get_startup_tips_with_rng(
@@ -555,18 +568,21 @@ content = "This is a test announcement"
}
#[test]
fn first_session_dedupes_multiple_availability_nuxes_by_message() {
fn first_session_returns_multiple_availability_nuxes() {
let mut rng = StdRng::seed_from_u64(1);
let models = vec![
model_preset(Some("*New* Spark is now available to you."), ""),
model_preset(Some("*New* Spark is now available to you."), ""),
ModelPreset {
model: "gpt-other".to_string(),
availability_nux: Some(ModelAvailabilityNux {
message: "*New* Canvas is now available to you.".to_string(),
}),
..model_preset(None, "")
},
model_preset(
"spark",
"Spark",
Some("*New* Spark is now available to you."),
"",
),
model_preset(
"canvas",
"Canvas",
Some("*New* Canvas is now available to you."),
"",
),
];
let tips = get_startup_tips_with_rng(
@@ -582,9 +598,11 @@ content = "This is a test announcement"
StartupTips {
first_session_tips: vec![
StartupTip::AvailabilityNux {
model: "spark".to_string(),
message: "*New* Spark is now available to you.".to_string(),
},
StartupTip::AvailabilityNux {
model: "canvas".to_string(),
message: "*New* Canvas is now available to you.".to_string(),
},
],
@@ -596,7 +614,12 @@ content = "This is a test announcement"
#[test]
fn later_session_can_select_availability_nux_from_weighted_pool() {
let preset = model_preset(Some("*New* Spark is now available to you."), "");
let preset = model_preset(
"gpt-test",
"GPT Test",
Some("*New* Spark is now available to you."),
"",
);
let mut rng = StdRng::seed_from_u64(5);
let tips = get_startup_tips_with_rng(
@@ -612,6 +635,7 @@ content = "This is a test announcement"
StartupTips {
first_session_tips: Vec::new(),
selected_tip: Some(StartupTip::AvailabilityNux {
model: "gpt-test".to_string(),
message: "*New* Spark is now available to you.".to_string(),
}),
},
@@ -621,8 +645,13 @@ content = "This is a test announcement"
#[test]
fn later_session_count_limit_disables_availability_nux() {
let preset = model_preset(Some("*New* Spark is now available to you."), "");
let counts = BTreeMap::from([("*New* Spark is now available to you.".to_string(), 4)]);
let preset = model_preset(
"gpt-test",
"GPT Test",
Some("*New* Spark is now available to you."),
"",
);
let counts = BTreeMap::from([("gpt-test".to_string(), 4)]);
let mut rng = StdRng::seed_from_u64(5);
let tips = get_startup_tips_with_rng(
@@ -645,7 +674,12 @@ content = "This is a test announcement"
#[test]
fn later_session_eligible_model_includes_announcement_and_generic_candidates() {
let preset = model_preset(Some("*New* Spark is now available to you."), "");
let preset = model_preset(
"gpt-test",
"GPT Test",
Some("*New* Spark is now available to you."),
"",
);
let mut saw_announcement = false;
let mut saw_plan_tip = false;
let mut saw_random_tip = false;