Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,14 @@
},
"type": "object"
},
"ModelAvailabilityNuxConfig": {
"additionalProperties": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"type": "object"
},
"ModelProviderInfo": {
"additionalProperties": false,
"description": "Serializable representation of a provider definition.",
Expand Down Expand Up @@ -1420,6 +1428,15 @@
"description": "Enable animations (welcome screen, shimmer effects, spinners). Defaults to `true`.",
"type": "boolean"
},
"model_availability_nux": {
"allOf": [
{
"$ref": "#/definitions/ModelAvailabilityNuxConfig"
}
],
"default": {},
"description": "Startup tooltip availability NUX state persisted by the TUI."
},
"notification_method": {
"allOf": [
{
Expand Down
47 changes: 47 additions & 0 deletions codex-rs/core/src/config/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()],
}];
Comment on lines +83 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid rewriting the full NUX map from stale state

model_availability_nux_count_edits clears tui.model_availability_nux and rewrites every key. Combined with startup code that builds updates from an in-memory snapshot, concurrent Codex launches can clobber each other's increments (lost update). This breaks the 4-exposure cap by regressing or dropping per-model counters.

Useful? React with 👍 / 👎.

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;
Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -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");
Expand Down
60 changes: 60 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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#"
Expand Down Expand Up @@ -2673,6 +2728,7 @@ theme = "dracula"
alternate_screen: AltScreenMode::Auto,
status_line: None,
theme: None,
model_availability_nux: ModelAvailabilityNuxConfig::default(),
}
);
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/core/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading