mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
codex: persist availability NUX by model
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user