mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
Show model availability NUX tips in tui
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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![
|
||||
|
||||
+1
-2
@@ -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.
|
||||
|
||||
+22
@@ -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.
|
||||
+1
-1
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user