Add model availability NUX tooltips (#13021)

- override startup tooltips with model availability NUX and persist
per-model show counts in config
- stop showing each model after four exposures and fall back to normal
tooltips
This commit is contained in:
Ahmed Ibrahim
2026-02-27 17:14:06 -08:00
committed by GitHub
parent ff5cbfd7d4
commit ec6f6aacbf
11 changed files with 657 additions and 6 deletions
+47
View File
@@ -8,6 +8,7 @@ use codex_protocol::config_types::Personality;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::openai_models::ReasoningEffort;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use tokio::task;
@@ -75,6 +76,27 @@ pub fn status_line_items_edit(items: &[String]) -> ConfigEdit {
}
}
pub fn model_availability_nux_count_edits(shown_count: &HashMap<String, u32>) -> Vec<ConfigEdit> {
let mut shown_count_entries: Vec<_> = shown_count.iter().collect();
shown_count_entries.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
let mut edits = vec![ConfigEdit::ClearPath {
segments: vec!["tui".to_string(), "model_availability_nux".to_string()],
}];
for (model_slug, count) in shown_count_entries {
edits.push(ConfigEdit::SetPath {
segments: vec![
"tui".to_string(),
"model_availability_nux".to_string(),
model_slug.clone(),
],
value: value(i64::from(*count)),
});
}
edits
}
// TODO(jif) move to a dedicated file
mod document_helpers {
use crate::config::types::McpServerConfig;
@@ -799,6 +821,12 @@ impl ConfigEditsBuilder {
self
}
pub fn set_model_availability_nux_count(mut self, shown_count: &HashMap<String, u32>) -> Self {
self.edits
.extend(model_availability_nux_count_edits(shown_count));
self
}
pub fn replace_mcp_servers(mut self, servers: &BTreeMap<String, McpServerConfig>) -> Self {
self.edits
.push(ConfigEdit::ReplaceMcpServers(servers.clone()));
@@ -963,6 +991,25 @@ model_reasoning_effort = "high"
assert_eq!(contents, "enabled = true\n");
}
#[test]
fn set_model_availability_nux_count_writes_shown_count() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
let shown_count = HashMap::from([("gpt-foo".to_string(), 4)]);
ConfigEditsBuilder::new(codex_home)
.set_model_availability_nux_count(&shown_count)
.apply_blocking()
.expect("persist");
let contents =
std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"[tui.model_availability_nux]
gpt-foo = 4
"#;
assert_eq!(contents, expected);
}
#[test]
fn set_skill_config_writes_disabled_entry() {
let tmp = tempdir().expect("tmpdir");
+60
View File
@@ -9,6 +9,7 @@ use crate::config::types::McpServerDisabledReason;
use crate::config::types::McpServerTransportConfig;
use crate::config::types::MemoriesConfig;
use crate::config::types::MemoriesToml;
use crate::config::types::ModelAvailabilityNuxConfig;
use crate::config::types::Notice;
use crate::config::types::NotificationMethod;
use crate::config::types::Notifications;
@@ -276,6 +277,9 @@ pub struct Config {
/// Show startup tooltips in the TUI welcome screen.
pub show_tooltips: bool,
/// Persisted startup availability NUX state for model tooltips.
pub model_availability_nux: ModelAvailabilityNuxConfig,
/// Start the TUI in the specified collaboration mode (plan/default).
/// Controls whether the TUI uses the terminal's alternate screen buffer.
@@ -2213,6 +2217,11 @@ impl Config {
.unwrap_or_default(),
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true),
model_availability_nux: cfg
.tui
.as_ref()
.map(|t| t.model_availability_nux.clone())
.unwrap_or_default(),
tui_alternate_screen: cfg
.tui
.as_ref()
@@ -2401,6 +2410,7 @@ mod tests {
use crate::config::types::McpServerTransportConfig;
use crate::config::types::MemoriesConfig;
use crate::config::types::MemoriesToml;
use crate::config::types::ModelAvailabilityNuxConfig;
use crate::config::types::NotificationMethod;
use crate::config::types::Notifications;
use crate::config_loader::RequirementSource;
@@ -2539,6 +2549,51 @@ phase_2_model = "gpt-5"
);
}
#[test]
fn config_toml_deserializes_model_availability_nux() {
let toml = r#"
[tui.model_availability_nux]
"gpt-foo" = 2
"gpt-bar" = 4
"#;
let cfg: ConfigToml =
toml::from_str(toml).expect("TOML deserialization should succeed for TUI NUX");
assert_eq!(
cfg.tui.expect("tui config should deserialize"),
Tui {
notifications: Notifications::default(),
notification_method: NotificationMethod::default(),
animations: true,
show_tooltips: true,
alternate_screen: AltScreenMode::default(),
status_line: None,
theme: None,
model_availability_nux: ModelAvailabilityNuxConfig {
shown_count: HashMap::from([
("gpt-bar".to_string(), 4),
("gpt-foo".to_string(), 2),
]),
},
}
);
}
#[test]
fn runtime_config_defaults_model_availability_nux() {
let cfg = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
tempdir().expect("tempdir").path().to_path_buf(),
)
.expect("load config");
assert_eq!(
cfg.model_availability_nux,
ModelAvailabilityNuxConfig::default()
);
}
#[test]
fn config_toml_deserializes_permissions_network() {
let toml = r#"
@@ -2673,6 +2728,7 @@ theme = "dracula"
alternate_screen: AltScreenMode::Auto,
status_line: None,
theme: None,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
}
);
}
@@ -4884,6 +4940,7 @@ model_verbosity = "high"
tui_notification_method: Default::default(),
animations: true,
show_tooltips: true,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
analytics_enabled: Some(true),
feedback_enabled: true,
tui_alternate_screen: AltScreenMode::Auto,
@@ -5011,6 +5068,7 @@ model_verbosity = "high"
tui_notification_method: Default::default(),
animations: true,
show_tooltips: true,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
analytics_enabled: Some(true),
feedback_enabled: true,
tui_alternate_screen: AltScreenMode::Auto,
@@ -5136,6 +5194,7 @@ model_verbosity = "high"
tui_notification_method: Default::default(),
animations: true,
show_tooltips: true,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
analytics_enabled: Some(false),
feedback_enabled: true,
tui_alternate_screen: AltScreenMode::Auto,
@@ -5247,6 +5306,7 @@ model_verbosity = "high"
tui_notification_method: Default::default(),
animations: true,
show_tooltips: true,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
analytics_enabled: Some(true),
feedback_enabled: true,
tui_alternate_screen: AltScreenMode::Auto,
+12
View File
@@ -667,6 +667,14 @@ impl fmt::Display for NotificationMethod {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ModelAvailabilityNuxConfig {
/// Number of times a startup availability NUX has been shown per model slug.
#[serde(default, flatten)]
pub shown_count: HashMap<String, u32>,
}
/// Collection of settings that are specific to the TUI.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]
@@ -716,6 +724,10 @@ pub struct Tui {
/// Use `/theme` in the TUI or see `$CODEX_HOME/themes` for custom themes.
#[serde(default)]
pub theme: Option<String>,
/// Startup tooltip availability NUX state persisted by the TUI.
#[serde(default)]
pub model_availability_nux: ModelAvailabilityNuxConfig,
}
const fn default_true() -> bool {