Show model availability NUX tips in tui

This commit is contained in:
Ahmed Ibrahim
2026-02-26 18:48:36 -08:00
parent 3cec1f2220
commit 0fd10af916
12 changed files with 331 additions and 169 deletions
+10 -10
View File
@@ -809,6 +809,16 @@
"Notice": {
"description": "Settings for notices we display to users via the tui and app-server clients (primarily the Codex IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.",
"properties": {
"availability_nux_display_counts": {
"additionalProperties": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"default": {},
"description": "Tracks how many times the TUI actually displayed each availability NUX.",
"type": "object"
},
"hide_full_access_warning": {
"description": "Tracks whether the user has acknowledged the full access warning prompt.",
"type": "boolean"
@@ -836,16 +846,6 @@
"default": {},
"description": "Tracks acknowledged model migrations as old->new model slug mappings.",
"type": "object"
},
"model_new_nux_display_counts": {
"additionalProperties": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"default": {},
"description": "Tracks how many times the TUI actually displayed the model-new startup tip.",
"type": "object"
}
},
"type": "object"
+16 -17
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 the model-new startup tip was displayed for a model.
RecordModelNewNuxDisplay { model: String },
/// Record that an availability NUX was displayed.
RecordAvailabilityNuxDisplay { id: String },
/// Replace the entire `[mcp_servers]` table.
ReplaceMcpServers(BTreeMap<String, McpServerConfig>),
/// Set or clear a skill config entry under `[[skills.config]]`.
@@ -333,19 +333,19 @@ impl ConfigDocument {
&[Notice::TABLE_KEY, "model_migrations", from.as_str()],
value(to.clone()),
)),
ConfigEdit::RecordModelNewNuxDisplay { model } => {
ConfigEdit::RecordAvailabilityNuxDisplay { id } => {
let resolved = self.scoped_segments(
Scope::Global,
&[Notice::TABLE_KEY, "model_new_nux_display_counts"],
&[Notice::TABLE_KEY, "availability_nux_display_counts"],
);
let Some(table) = self.descend(&resolved, TraversalMode::Create) else {
return Ok(false);
};
let current = table
.get(model.as_str())
.get(id.as_str())
.and_then(toml_edit::Item::as_integer)
.unwrap_or(0);
table[model.as_str()] = value(current + 1);
table[id.as_str()] = value(current + 1);
Ok(true)
}
ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged) => Ok(self.write_value(
@@ -805,10 +805,9 @@ impl ConfigEditsBuilder {
self
}
pub fn record_model_new_nux_display(mut self, model: &str) -> Self {
self.edits.push(ConfigEdit::RecordModelNewNuxDisplay {
model: model.to_string(),
});
pub fn record_availability_nux_display(mut self, id: &str) -> Self {
self.edits
.push(ConfigEdit::RecordAvailabilityNuxDisplay { id: id.to_string() });
self
}
@@ -1459,7 +1458,7 @@ gpt-5 = "gpt-5.1"
}
#[test]
fn blocking_record_model_new_nux_display_increments_count() {
fn blocking_record_availability_nux_display_increments_count() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
std::fs::write(
@@ -1467,16 +1466,16 @@ gpt-5 = "gpt-5.1"
r#"[notice]
existing = "value"
[notice.model_new_nux_display_counts]
gpt-5 = 1
[notice.availability_nux_display_counts]
try_spark = 1
"#,
)
.expect("seed");
apply_blocking(
codex_home,
None,
&[ConfigEdit::RecordModelNewNuxDisplay {
model: "gpt-5".to_string(),
&[ConfigEdit::RecordAvailabilityNuxDisplay {
id: "try_spark".to_string(),
}],
)
.expect("persist");
@@ -1486,8 +1485,8 @@ gpt-5 = 1
let expected = r#"[notice]
existing = "value"
[notice.model_new_nux_display_counts]
gpt-5 = 2
[notice.availability_nux_display_counts]
try_spark = 2
"#;
assert_eq!(contents, expected);
}
+2 -2
View File
@@ -722,9 +722,9 @@ pub struct Notice {
/// Tracks acknowledged model migrations as old->new model slug mappings.
#[serde(default)]
pub model_migrations: BTreeMap<String, String>,
/// Tracks how many times the TUI actually displayed the model-new startup tip.
/// Tracks how many times the TUI actually displayed each availability NUX.
#[serde(default)]
pub model_new_nux_display_counts: BTreeMap<String, u32>,
pub availability_nux_display_counts: BTreeMap<String, u32>,
}
impl Notice {
+25 -13
View File
@@ -2689,16 +2689,16 @@ impl App {
));
}
}
AppEvent::PersistModelNewNuxDisplayed { model } => {
AppEvent::PersistAvailabilityNuxDisplayed { id } => {
let count = self
.config
.notices
.model_new_nux_display_counts
.entry(model.clone())
.availability_nux_display_counts
.entry(id.clone())
.or_default();
*count += 1;
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
.record_model_new_nux_display(model.as_str())
.record_availability_nux_display(id.as_str())
.apply()
.await
{
@@ -3828,7 +3828,7 @@ mod tests {
app.chat_widget.current_model(),
event,
is_first,
None,
Vec::new(),
)) as Arc<dyn HistoryCell>
};
@@ -3874,7 +3874,7 @@ mod tests {
requested_model: &str,
actual_model: &str,
is_first_event: bool,
selected_startup_tip: Option<&str>,
startup_tips: &[&str],
) -> String {
let event = SessionConfiguredEvent {
session_id: ThreadId::new(),
@@ -3896,7 +3896,7 @@ mod tests {
requested_model,
event,
is_first_event,
selected_startup_tip.map(str::to_string),
startup_tips.iter().map(|tip| (*tip).to_string()).collect(),
);
cell.display_lines(80)
@@ -3929,20 +3929,32 @@ mod tests {
"gpt-test",
"gpt-test",
true,
Some(
"New You're using GPT Test. Fast, high-reliability coding model. Use /model to compare or switch anytime.",
),
&["New Spark is now available to you."],
);
assert_snapshot!("first_session_header_with_model_nux", rendered);
}
#[test]
fn first_session_header_with_multiple_model_nux_matches_snapshot() {
let rendered = render_session_info_snapshot(
"gpt-test",
"gpt-test",
true,
&[
"New Spark is now available to you.",
"New Canvas is now available to you.",
],
);
assert_snapshot!("first_session_header_with_multiple_model_nux", rendered);
}
#[test]
fn later_session_header_with_model_nux_matches_snapshot() {
let rendered = render_session_info_snapshot(
"gpt-test",
"gpt-test",
false,
Some("New You're using GPT Test. Use /model to compare or switch anytime."),
&["New Spark is now available to you."],
);
assert_snapshot!("later_session_header_with_model_nux", rendered);
}
@@ -3950,7 +3962,7 @@ mod tests {
#[test]
fn later_session_header_after_model_nux_exhaustion_matches_snapshot() {
let rendered =
render_session_info_snapshot("requested-model", "resolved-model", false, None);
render_session_info_snapshot("requested-model", "resolved-model", false, &[]);
assert_snapshot!("later_session_header_after_model_nux_exhaustion", rendered);
}
@@ -4396,7 +4408,7 @@ mod tests {
app.chat_widget.current_model(),
event,
is_first,
None,
Vec::new(),
)) as Arc<dyn HistoryCell>
};
+3 -3
View File
@@ -293,9 +293,9 @@ pub(crate) enum AppEvent {
to_model: String,
},
/// Persist that the model-new startup tip was displayed for a model.
PersistModelNewNuxDisplayed {
model: String,
/// Persist that an availability NUX was displayed.
PersistAvailabilityNuxDisplayed {
id: String,
},
/// Skip the next world-writable scan (one-shot) after a user-confirmed continue.
+39 -20
View File
@@ -1131,26 +1131,18 @@ impl ChatWidget {
self.last_copyable_output = None;
let forked_from_id = event.forked_from_id;
let model_for_header = event.model.clone();
let startup_tip = if self.config.show_tooltips {
let active_model = self
.models_manager
.try_list_models()
.ok()
.and_then(|models| {
models
.into_iter()
.find(|preset| preset.model == model_for_header)
});
crate::tooltips::get_startup_tip(
active_model.as_ref(),
&self.config.notices.model_new_nux_display_counts,
let startup_tips = if self.config.show_tooltips {
let models = self.models_manager.try_list_models().unwrap_or_default();
crate::tooltips::get_startup_tips(
&models,
&self.config.notices.availability_nux_display_counts,
self.auth_manager
.auth_cached()
.and_then(|auth| auth.account_plan_type()),
self.show_welcome_banner,
)
} else {
None
Default::default()
};
self.session_header.set_model(&model_for_header);
self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
@@ -1160,24 +1152,51 @@ impl ChatWidget {
);
self.refresh_model_display();
self.sync_personality_command_enabled();
let selected_startup_tip = startup_tip.clone().map(StartupTip::message);
let displayed_startup_tips = if self.show_welcome_banner {
startup_tips
.first_session_tips
.iter()
.map(StartupTip::message)
.collect()
} else {
startup_tips
.selected_tip
.iter()
.map(StartupTip::message)
.collect()
};
let session_info_cell = history_cell::new_session_info(
&requested_model,
event,
self.show_welcome_banner,
selected_startup_tip,
displayed_startup_tips,
);
self.apply_session_info_cell(session_info_cell);
if let Some(StartupTip::ModelNew { model, .. }) = startup_tip {
let displayed_availability_nux_ids: Vec<String> = if self.show_welcome_banner {
startup_tips
.first_session_tips
.iter()
.filter_map(StartupTip::availability_nux_id)
.map(str::to_string)
.collect()
} else {
startup_tips
.selected_tip
.iter()
.filter_map(StartupTip::availability_nux_id)
.map(str::to_string)
.collect()
};
for id in displayed_availability_nux_ids {
let count = self
.config
.notices
.model_new_nux_display_counts
.entry(model.clone())
.availability_nux_display_counts
.entry(id.clone())
.or_default();
*count += 1;
self.app_event_tx
.send(AppEvent::PersistModelNewNuxDisplayed { model });
.send(AppEvent::PersistAvailabilityNuxDisplayed { id });
}
if let Some(messages) = initial_messages {
+14 -8
View File
@@ -43,6 +43,7 @@ use codex_protocol::items::AgentMessageItem;
use codex_protocol::items::PlanItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::MessagePhase;
use codex_protocol::openai_models::ModelAvailabilityNux;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::ReasoningEffortPreset;
@@ -1845,7 +1846,12 @@ async fn session_configured_first_session_shows_model_nux_and_increments_display
let mut model_info = construct_model_info_offline("gpt-5.1", &chat.config);
model_info.display_name = "GPT Test".to_string();
model_info.description = Some("Fast, high-reliability coding model.".to_string());
model_info.show_nux = true;
model_info.availability_nux = Some(ModelAvailabilityNux {
id: "try_spark".to_string(),
message:
"*New* You're using **GPT Test**. Fast, high-reliability coding model. Use /model to compare or switch anytime."
.to_string(),
});
chat.models_manager = Arc::new(ModelsManager::new(
chat.config.codex_home.clone(),
chat.auth_manager.clone(),
@@ -1884,8 +1890,8 @@ async fn session_configured_first_session_shows_model_nux_and_increments_display
Some(&1),
chat.config
.notices
.model_new_nux_display_counts
.get("gpt-5.1")
.availability_nux_display_counts
.get("try_spark")
);
}
@@ -1893,7 +1899,7 @@ async fn session_configured_first_session_shows_model_nux_and_increments_display
async fn session_configured_ineligible_model_does_not_increment_display_count() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.1")).await;
let mut model_info = construct_model_info_offline("gpt-5.1", &chat.config);
model_info.show_nux = false;
model_info.availability_nux = None;
chat.models_manager = Arc::new(ModelsManager::new(
chat.config.codex_home.clone(),
chat.auth_manager.clone(),
@@ -1928,8 +1934,8 @@ async fn session_configured_ineligible_model_does_not_increment_display_count()
None,
chat.config
.notices
.model_new_nux_display_counts
.get("gpt-5.1")
.availability_nux_display_counts
.get("try_spark")
);
}
@@ -6116,7 +6122,7 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() {
is_default: false,
upgrade: None,
show_in_picker,
show_nux: false,
availability_nux: None,
supported_in_api: true,
input_modalities: default_input_modalities(),
};
@@ -6385,7 +6391,7 @@ async fn single_reasoning_option_skips_selection() {
is_default: false,
upgrade: None,
show_in_picker: true,
show_nux: false,
availability_nux: None,
supported_in_api: true,
input_modalities: default_input_modalities(),
};
+5 -5
View File
@@ -1038,7 +1038,7 @@ pub(crate) fn new_session_info(
requested_model: &str,
event: SessionConfiguredEvent,
is_first_event: bool,
selected_startup_tip: Option<String>,
startup_tips: Vec<String>,
) -> SessionInfoCell {
let SessionConfiguredEvent {
model,
@@ -1086,12 +1086,12 @@ pub(crate) fn new_session_info(
];
parts.push(Box::new(PlainHistoryCell { lines: help_lines }));
if let Some(selected_startup_tip) = selected_startup_tip {
parts.push(Box::new(TooltipHistoryCell::new(selected_startup_tip)));
for startup_tip in startup_tips {
parts.push(Box::new(TooltipHistoryCell::new(startup_tip)));
}
} else {
if let Some(selected_startup_tip) = selected_startup_tip {
parts.push(Box::new(TooltipHistoryCell::new(selected_startup_tip)));
for startup_tip in startup_tips {
parts.push(Box::new(TooltipHistoryCell::new(startup_tip)));
}
if requested_model != model {
let lines = vec![
@@ -17,5 +17,4 @@ expression: rendered
/model - choose what model and reasoning effort to use
/review - review any changes and find issues
Tip: New You're using GPT Test. Fast, high-reliability coding model. Use /
model to compare or switch anytime.
Tip: New Spark is now available to you.
@@ -0,0 +1,22 @@
---
source: tui/src/app.rs
expression: rendered
---
╭─────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ model: gpt-test high /model to change │
│ directory: /tmp/project │
╰─────────────────────────────────────────────╯
To get started, describe a task or try one of these commands:
/init - create an AGENTS.md file with instructions for Codex
/status - show current session configuration
/permissions - choose what Codex is allowed to do
/model - choose what model and reasoning effort to use
/review - review any changes and find issues
Tip: New Spark is now available to you.
Tip: New Canvas is now available to you.
@@ -9,4 +9,4 @@ expression: rendered
│ directory: /tmp/project │
╰─────────────────────────────────────────────╯
Tip: New You're using GPT Test. Use /model to compare or switch anytime.
Tip: New Spark is now available to you.
+193 -88
View File
@@ -1,9 +1,11 @@
use codex_core::features::FEATURES;
use codex_protocol::account::PlanType;
use codex_protocol::openai_models::ModelAvailabilityNux;
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";
@@ -51,27 +53,40 @@ fn experimental_tooltips() -> Vec<&'static str> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StartupTip {
Generic(String),
ModelNew { model: String, message: String },
AvailabilityNux { id: String, message: String },
}
impl StartupTip {
pub(crate) fn message(self) -> String {
pub(crate) fn message(&self) -> String {
match self {
Self::Generic(message) | Self::ModelNew { message, .. } => message,
Self::Generic(message) | Self::AvailabilityNux { message, .. } => message.clone(),
}
}
pub(crate) fn availability_nux_id(&self) -> Option<&str> {
match self {
Self::Generic(_) => None,
Self::AvailabilityNux { id, .. } => Some(id.as_str()),
}
}
}
pub(crate) fn get_startup_tip(
model: Option<&ModelPreset>,
model_new_nux_display_counts: &BTreeMap<String, u32>,
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct StartupTips {
pub(crate) first_session_tips: Vec<StartupTip>,
pub(crate) selected_tip: Option<StartupTip>,
}
pub(crate) fn get_startup_tips(
models: &[ModelPreset],
availability_nux_display_counts: &BTreeMap<String, u32>,
plan: Option<PlanType>,
is_first_session: bool,
) -> Option<StartupTip> {
) -> StartupTips {
let mut rng = rand::rng();
get_startup_tip_with_rng(
model,
model_new_nux_display_counts,
get_startup_tips_with_rng(
models,
availability_nux_display_counts,
plan,
is_first_session,
announcement::fetch_announcement_tip(),
@@ -79,39 +94,38 @@ pub(crate) fn get_startup_tip(
)
}
fn get_startup_tip_with_rng<R: Rng + ?Sized>(
model: Option<&ModelPreset>,
model_new_nux_display_counts: &BTreeMap<String, u32>,
fn get_startup_tips_with_rng<R: Rng + ?Sized>(
models: &[ModelPreset],
availability_nux_display_counts: &BTreeMap<String, u32>,
plan: Option<PlanType>,
is_first_session: bool,
announcement_tip: Option<String>,
rng: &mut R,
) -> Option<StartupTip> {
let model_new_tip = model.and_then(|preset| {
let display_count = model_new_nux_display_counts
.get(&preset.model)
.copied()
.unwrap_or(0);
(preset.show_nux && display_count < 4).then(|| StartupTip::ModelNew {
model: preset.model.clone(),
message: model_new_tooltip(preset),
})
});
) -> StartupTips {
let availability_nux_tips = availability_nux_tips(models, availability_nux_display_counts);
if is_first_session {
return model_new_tip;
return StartupTips {
first_session_tips: availability_nux_tips,
selected_tip: None,
};
}
let Some(model_new_tip) = model_new_tip else {
return get_generic_tooltip_with_rng(plan, announcement_tip, rng).map(StartupTip::Generic);
};
if availability_nux_tips.is_empty() {
return StartupTips {
first_session_tips: Vec::new(),
selected_tip: get_generic_tooltip_with_rng(plan, announcement_tip, rng)
.map(StartupTip::Generic),
};
}
let mut weighted_candidates = vec![
model_new_tip.clone(),
model_new_tip.clone(),
model_new_tip.clone(),
model_new_tip,
];
let mut weighted_candidates = Vec::new();
for availability_nux_tip in &availability_nux_tips {
weighted_candidates.push(availability_nux_tip.clone());
weighted_candidates.push(availability_nux_tip.clone());
weighted_candidates.push(availability_nux_tip.clone());
weighted_candidates.push(availability_nux_tip.clone());
}
if let Some(announcement_tip) = announcement_tip {
weighted_candidates.push(StartupTip::Generic(announcement_tip));
}
@@ -122,9 +136,12 @@ fn get_startup_tip_with_rng<R: Rng + ?Sized>(
weighted_candidates.push(StartupTip::Generic(random_tip.to_string()));
}
weighted_candidates
.get(rng.random_range(0..weighted_candidates.len()))
.cloned()
StartupTips {
first_session_tips: Vec::new(),
selected_tip: weighted_candidates
.get(rng.random_range(0..weighted_candidates.len()))
.cloned(),
}
}
fn get_generic_tooltip_with_rng<R: Rng + ?Sized>(
@@ -167,18 +184,31 @@ fn plan_tooltip(plan: Option<PlanType>) -> Option<&'static str> {
}
}
fn model_new_tooltip(model: &ModelPreset) -> String {
let description = model.description.trim();
if description.is_empty() {
format!(
"*New* You're using **{}**. Use /model to compare or switch anytime.",
model.display_name
)
} else {
format!(
"*New* You're using **{}**. {} Use /model to compare or switch anytime.",
model.display_name, description
)
fn availability_nux_tips(
models: &[ModelPreset],
availability_nux_display_counts: &BTreeMap<String, u32>,
) -> Vec<StartupTip> {
let mut seen_ids = HashSet::new();
models
.iter()
.filter_map(|preset| preset.availability_nux.as_ref())
.filter(|availability_nux| {
availability_nux_display_counts
.get(&availability_nux.id)
.copied()
.unwrap_or(0)
< 4
})
.filter(|availability_nux| seen_ids.insert(availability_nux.id.clone()))
.map(startup_tip_from_availability_nux)
.collect()
}
fn startup_tip_from_availability_nux(availability_nux: &ModelAvailabilityNux) -> StartupTip {
StartupTip::AvailabilityNux {
id: availability_nux.id.clone(),
message: availability_nux.message.clone(),
}
}
@@ -343,7 +373,7 @@ mod tests {
use rand::SeedableRng;
use rand::rngs::StdRng;
fn model_preset(show_nux: bool, description: &str) -> ModelPreset {
fn model_preset(availability_nux: Option<(&str, &str)>, description: &str) -> ModelPreset {
ModelPreset {
id: "gpt-test".to_string(),
model: "gpt-test".to_string(),
@@ -355,7 +385,10 @@ mod tests {
is_default: false,
upgrade: None,
show_in_picker: true,
show_nux,
availability_nux: availability_nux.map(|(id, message)| ModelAvailabilityNux {
id: id.to_string(),
message: message.to_string(),
}),
supported_in_api: true,
input_modalities: codex_protocol::openai_models::default_input_modalities(),
}
@@ -479,12 +512,15 @@ content = "This is a test announcement"
}
#[test]
fn first_session_eligible_model_returns_model_tip() {
let preset = model_preset(true, "Fast, high-reliability coding model.");
fn first_session_eligible_model_returns_all_availability_nux_tips() {
let preset = model_preset(
Some(("try_spark", "*New* Spark is now available to you.")),
"Fast, high-reliability coding model.",
);
let mut rng = StdRng::seed_from_u64(1);
let tip = get_startup_tip_with_rng(
Some(&preset),
let tips = get_startup_tips_with_rng(
&[preset],
&BTreeMap::new(),
Some(PlanType::Plus),
true,
@@ -493,21 +529,24 @@ content = "This is a test announcement"
);
assert_eq!(
Some(StartupTip::ModelNew {
model: "gpt-test".to_string(),
message: "*New* You're using **GPT Test**. Fast, high-reliability coding model. Use /model to compare or switch anytime.".to_string(),
}),
tip
StartupTips {
first_session_tips: vec![StartupTip::AvailabilityNux {
id: "try_spark".to_string(),
message: "*New* Spark is now available to you.".to_string(),
}],
selected_tip: None,
},
tips
);
}
#[test]
fn first_session_ineligible_model_skips_tip() {
let preset = model_preset(false, "");
let preset = model_preset(None, "");
let mut rng = StdRng::seed_from_u64(1);
let tip = get_startup_tip_with_rng(
Some(&preset),
let tips = get_startup_tips_with_rng(
&[preset],
&BTreeMap::new(),
Some(PlanType::Plus),
true,
@@ -515,16 +554,68 @@ content = "This is a test announcement"
&mut rng,
);
assert_eq!(None, tip);
assert_eq!(StartupTips::default(), tips);
}
#[test]
fn later_session_can_select_model_tip_from_weighted_pool() {
let preset = model_preset(true, "");
fn first_session_dedupes_multiple_availability_nuxes_by_id() {
let mut rng = StdRng::seed_from_u64(1);
let models = vec![
model_preset(
Some(("try_spark", "*New* Spark is now available to you.")),
"",
),
model_preset(
Some(("try_spark", "*New* Spark is now available to you.")),
"",
),
ModelPreset {
model: "gpt-other".to_string(),
availability_nux: Some(ModelAvailabilityNux {
id: "try_canvas".to_string(),
message: "*New* Canvas is now available to you.".to_string(),
}),
..model_preset(None, "")
},
];
let tips = get_startup_tips_with_rng(
&models,
&BTreeMap::new(),
Some(PlanType::Plus),
true,
Some("announcement".to_string()),
&mut rng,
);
assert_eq!(
StartupTips {
first_session_tips: vec![
StartupTip::AvailabilityNux {
id: "try_spark".to_string(),
message: "*New* Spark is now available to you.".to_string(),
},
StartupTip::AvailabilityNux {
id: "try_canvas".to_string(),
message: "*New* Canvas is now available to you.".to_string(),
},
],
selected_tip: None,
},
tips
);
}
#[test]
fn later_session_can_select_availability_nux_from_weighted_pool() {
let preset = model_preset(
Some(("try_spark", "*New* Spark is now available to you.")),
"",
);
let mut rng = StdRng::seed_from_u64(5);
let tip = get_startup_tip_with_rng(
Some(&preset),
let tips = get_startup_tips_with_rng(
&[preset],
&BTreeMap::new(),
Some(PlanType::Plus),
false,
@@ -533,24 +624,28 @@ content = "This is a test announcement"
);
assert_eq!(
Some(StartupTip::ModelNew {
model: "gpt-test".to_string(),
message:
"*New* You're using **GPT Test**. Use /model to compare or switch anytime."
.to_string(),
}),
tip
StartupTips {
first_session_tips: Vec::new(),
selected_tip: Some(StartupTip::AvailabilityNux {
id: "try_spark".to_string(),
message: "*New* Spark is now available to you.".to_string(),
}),
},
tips
);
}
#[test]
fn later_session_count_limit_disables_model_tip() {
let preset = model_preset(true, "");
let counts = BTreeMap::from([("gpt-test".to_string(), 4)]);
fn later_session_count_limit_disables_availability_nux() {
let preset = model_preset(
Some(("try_spark", "*New* Spark is now available to you.")),
"",
);
let counts = BTreeMap::from([("try_spark".to_string(), 4)]);
let mut rng = StdRng::seed_from_u64(5);
let tip = get_startup_tip_with_rng(
Some(&preset),
let tips = get_startup_tips_with_rng(
&[preset],
&counts,
Some(PlanType::Plus),
false,
@@ -558,27 +653,37 @@ content = "This is a test announcement"
&mut rng,
);
assert_eq!(Some(StartupTip::Generic("announcement".to_string())), tip);
assert_eq!(
StartupTips {
first_session_tips: Vec::new(),
selected_tip: Some(StartupTip::Generic("announcement".to_string())),
},
tips
);
}
#[test]
fn later_session_eligible_model_includes_announcement_and_generic_candidates() {
let preset = model_preset(true, "");
let preset = model_preset(
Some(("try_spark", "*New* Spark is now available to you.")),
"",
);
let mut saw_announcement = false;
let mut saw_plan_tip = false;
let mut saw_random_tip = false;
let mut saw_model_tip = false;
let mut saw_availability_nux_tip = false;
for seed in 0..64 {
let mut rng = StdRng::seed_from_u64(seed);
let tip = get_startup_tip_with_rng(
Some(&preset),
let tip = get_startup_tips_with_rng(
std::slice::from_ref(&preset),
&BTreeMap::new(),
Some(PlanType::Plus),
false,
Some("announcement".to_string()),
&mut rng,
)
.selected_tip
.expect("tip");
match tip {
@@ -593,8 +698,8 @@ content = "This is a test announcement"
StartupTip::Generic(_) => {
saw_random_tip = true;
}
StartupTip::ModelNew { .. } => {
saw_model_tip = true;
StartupTip::AvailabilityNux { .. } => {
saw_availability_nux_tip = true;
}
}
}
@@ -602,6 +707,6 @@ content = "This is a test announcement"
assert!(saw_announcement);
assert!(saw_plan_tip);
assert!(saw_random_tip);
assert!(saw_model_tip);
assert!(saw_availability_nux_tip);
}
}