mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
files
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -17,8 +15,6 @@ use codex_core::features::Feature;
|
||||
use codex_core::git_info::current_branch_name;
|
||||
use codex_core::git_info::local_git_branches;
|
||||
use codex_core::models_manager::manager::ModelsManager;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_core::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
use codex_core::protocol::AgentMessageEvent;
|
||||
@@ -82,8 +78,6 @@ use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Wrap;
|
||||
@@ -148,23 +142,14 @@ use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally";
|
||||
const USER_SHELL_COMMAND_HELP_HINT: &str = "Example: !ls";
|
||||
const PENDING_MODEL_MIGRATION_NOTICE_FILENAME: &str = "pending_model_migration_notice.json";
|
||||
use crate::model_migration_logic;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PendingModelMigrationNotice {
|
||||
from_model: String,
|
||||
to_model: String,
|
||||
// Used to respect hide flags even if config changes between scheduling and display.
|
||||
#[serde(default)]
|
||||
migration_config_key: Option<String>,
|
||||
}
|
||||
// Track information about an in-flight exec command.
|
||||
struct RunningCommand {
|
||||
command: Vec<String>,
|
||||
@@ -424,98 +409,12 @@ impl ChatWidget {
|
||||
self.conversation_id.is_some()
|
||||
}
|
||||
|
||||
fn pending_model_migration_notice_path(&self) -> PathBuf {
|
||||
self.config
|
||||
.codex_home
|
||||
.join(PENDING_MODEL_MIGRATION_NOTICE_FILENAME)
|
||||
}
|
||||
|
||||
fn migration_prompt_hidden(config: &Config, migration_config_key: &str) -> bool {
|
||||
match migration_config_key {
|
||||
HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG => config
|
||||
.notices
|
||||
.hide_gpt_5_1_codex_max_migration_prompt
|
||||
.unwrap_or(false),
|
||||
HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG => {
|
||||
config.notices.hide_gpt5_1_migration_prompt.unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show_model_migration_notice(
|
||||
current_model: &str,
|
||||
target_model: &str,
|
||||
seen_migrations: &BTreeMap<String, String>,
|
||||
available_models: &[ModelPreset],
|
||||
) -> bool {
|
||||
if target_model == current_model {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(seen_target) = seen_migrations.get(current_model)
|
||||
&& seen_target == target_model
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.any(|preset| preset.model == current_model && preset.upgrade.is_some())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.any(|preset| preset.upgrade.as_ref().map(|u| u.id.as_str()) == Some(target_model))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn maybe_show_pending_model_migration_notice(&mut self) {
|
||||
let notice_path = self.pending_model_migration_notice_path();
|
||||
let contents = match std::fs::read_to_string(¬ice_path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to read pending model migration notice"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let notice: PendingModelMigrationNotice = match serde_json::from_str(&contents) {
|
||||
Ok(notice) => notice,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to parse pending model migration notice"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(migration_config_key) = notice.migration_config_key.as_deref()
|
||||
&& Self::migration_prompt_hidden(&self.config, migration_config_key)
|
||||
{
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
let Some(notice) =
|
||||
model_migration_logic::take_pending_model_migration_notice(&mut self.config)
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(seen_target) = self.config.notices.model_migrations.get(¬ice.from_model)
|
||||
&& seen_target == ¬ice.to_model
|
||||
{
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.add_to_history(history_cell::new_info_event(
|
||||
format!(
|
||||
@@ -524,16 +423,6 @@ impl ChatWidget {
|
||||
),
|
||||
None,
|
||||
));
|
||||
|
||||
// Best-effort: clear the one-shot file so it doesn't appear again.
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
|
||||
// Update in-memory state to prevent re-scheduling within this run, and persist so future
|
||||
// runs don't re-show the notice.
|
||||
self.config
|
||||
.notices
|
||||
.model_migrations
|
||||
.insert(notice.from_model.clone(), notice.to_model.clone());
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistModelMigrationPromptAcknowledged {
|
||||
from_model: notice.from_model,
|
||||
@@ -547,70 +436,11 @@ impl ChatWidget {
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let Some(ModelUpgrade {
|
||||
id: target_model,
|
||||
migration_config_key,
|
||||
..
|
||||
}) = available_models
|
||||
.iter()
|
||||
.find(|preset| preset.model == model)
|
||||
.and_then(|preset| preset.upgrade.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if Self::migration_prompt_hidden(&self.config, migration_config_key.as_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.all(|preset| preset.model != target_model.as_str())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if !Self::should_show_model_migration_notice(
|
||||
model_migration_logic::maybe_schedule_model_migration_notice(
|
||||
&self.config,
|
||||
model,
|
||||
target_model.as_str(),
|
||||
&self.config.notices.model_migrations,
|
||||
&available_models,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notice_path = self.pending_model_migration_notice_path();
|
||||
if notice_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let notice = PendingModelMigrationNotice {
|
||||
from_model: model.to_string(),
|
||||
to_model: target_model.to_string(),
|
||||
migration_config_key: Some(migration_config_key.to_string()),
|
||||
};
|
||||
let Ok(json_line) = serde_json::to_string(¬ice).map(|json| format!("{json}\n")) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(parent) = notice_path.parent()
|
||||
&& let Err(err) = std::fs::create_dir_all(parent)
|
||||
{
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to create directory for pending model migration notice"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::write(¬ice_path, json_line) {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to persist pending model migration notice"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn flush_answer_stream_with_separator(&mut self) {
|
||||
@@ -1634,7 +1464,12 @@ impl ChatWidget {
|
||||
model,
|
||||
} = common;
|
||||
let mut config = config;
|
||||
config.model = Some(model.clone());
|
||||
// `model` is an optional override provided by the app. Avoid clobbering the configured
|
||||
// model with an empty string during startup; that would propagate to core and render as a
|
||||
// blank model in the session header (/model current label, etc).
|
||||
if !model.is_empty() {
|
||||
config.model = Some(model.clone());
|
||||
}
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
||||
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager);
|
||||
|
||||
@@ -60,6 +60,7 @@ mod markdown_render;
|
||||
mod markdown_stream;
|
||||
#[cfg(test)]
|
||||
mod model_migration;
|
||||
mod model_migration_logic;
|
||||
mod notifications;
|
||||
pub mod onboarding;
|
||||
mod oss_selection;
|
||||
|
||||
196
codex-rs/tui/src/model_migration_logic.rs
Normal file
196
codex-rs/tui/src/model_migration_logic.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use codex_core::config::Config;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const PENDING_MODEL_MIGRATION_NOTICE_FILENAME: &str = "pending_model_migration_notice.json";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct PendingModelMigrationNotice {
|
||||
pub(crate) from_model: String,
|
||||
pub(crate) to_model: String,
|
||||
// Used to respect hide flags even if config changes between scheduling and display.
|
||||
#[serde(default)]
|
||||
pub(crate) migration_config_key: Option<String>,
|
||||
}
|
||||
|
||||
fn pending_model_migration_notice_path(config: &Config) -> PathBuf {
|
||||
config
|
||||
.codex_home
|
||||
.join(PENDING_MODEL_MIGRATION_NOTICE_FILENAME)
|
||||
}
|
||||
|
||||
fn migration_prompt_hidden(config: &Config, migration_config_key: &str) -> bool {
|
||||
match migration_config_key {
|
||||
HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG => config
|
||||
.notices
|
||||
.hide_gpt_5_1_codex_max_migration_prompt
|
||||
.unwrap_or(false),
|
||||
HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG => {
|
||||
config.notices.hide_gpt5_1_migration_prompt.unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show_model_migration_notice(
|
||||
current_model: &str,
|
||||
target_model: &str,
|
||||
available_models: &[ModelPreset],
|
||||
config: &Config,
|
||||
) -> bool {
|
||||
if target_model == current_model {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(seen_target) = config.notices.model_migrations.get(current_model)
|
||||
&& seen_target == target_model
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.any(|preset| preset.model == current_model && preset.upgrade.is_some())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
available_models
|
||||
.iter()
|
||||
.any(|preset| preset.upgrade.as_ref().map(|u| u.id.as_str()) == Some(target_model))
|
||||
}
|
||||
|
||||
/// Read and clear the one-shot migration notice file, returning the notice if it should be shown.
|
||||
///
|
||||
/// If the notice is returned, this also updates `config.notices.model_migrations` to prevent
|
||||
/// re-scheduling within the current process.
|
||||
pub(crate) fn take_pending_model_migration_notice(
|
||||
config: &mut Config,
|
||||
) -> Option<PendingModelMigrationNotice> {
|
||||
let notice_path = pending_model_migration_notice_path(config);
|
||||
let contents = match std::fs::read_to_string(¬ice_path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return None,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to read pending model migration notice"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let notice: PendingModelMigrationNotice = match serde_json::from_str(&contents) {
|
||||
Ok(notice) => notice,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to parse pending model migration notice"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(migration_config_key) = notice.migration_config_key.as_deref()
|
||||
&& migration_prompt_hidden(config, migration_config_key)
|
||||
{
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(seen_target) = config.notices.model_migrations.get(¬ice.from_model)
|
||||
&& seen_target == ¬ice.to_model
|
||||
{
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Best-effort: clear the one-shot file so it doesn't appear again.
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
|
||||
config
|
||||
.notices
|
||||
.model_migrations
|
||||
.insert(notice.from_model.clone(), notice.to_model.clone());
|
||||
|
||||
Some(notice)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_schedule_model_migration_notice(
|
||||
config: &Config,
|
||||
current_model: &str,
|
||||
available_models: &[ModelPreset],
|
||||
) {
|
||||
let Some(ModelUpgrade {
|
||||
id: target_model,
|
||||
migration_config_key,
|
||||
..
|
||||
}) = available_models
|
||||
.iter()
|
||||
.find(|preset| preset.model == current_model)
|
||||
.and_then(|preset| preset.upgrade.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if migration_prompt_hidden(config, migration_config_key.as_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.all(|preset| preset.model != target_model.as_str())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if !should_show_model_migration_notice(
|
||||
current_model,
|
||||
target_model.as_str(),
|
||||
available_models,
|
||||
config,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notice_path = pending_model_migration_notice_path(config);
|
||||
if notice_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let notice = PendingModelMigrationNotice {
|
||||
from_model: current_model.to_string(),
|
||||
to_model: target_model.to_string(),
|
||||
migration_config_key: Some(migration_config_key.to_string()),
|
||||
};
|
||||
let Ok(json_line) = serde_json::to_string(¬ice).map(|json| format!("{json}\n")) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(parent) = notice_path.parent()
|
||||
&& let Err(err) = std::fs::create_dir_all(parent)
|
||||
{
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to create directory for pending model migration notice"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::write(¬ice_path, json_line) {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to persist pending model migration notice"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -15,8 +13,6 @@ use codex_core::config::types::Notifications;
|
||||
use codex_core::git_info::current_branch_name;
|
||||
use codex_core::git_info::local_git_branches;
|
||||
use codex_core::models_manager::manager::ModelsManager;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_core::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
use codex_core::protocol::AgentMessageEvent;
|
||||
@@ -82,8 +78,6 @@ use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Wrap;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::debug;
|
||||
@@ -142,23 +136,14 @@ use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally";
|
||||
const USER_SHELL_COMMAND_HELP_HINT: &str = "Example: !ls";
|
||||
const PENDING_MODEL_MIGRATION_NOTICE_FILENAME: &str = "pending_model_migration_notice.json";
|
||||
use crate::model_migration_logic;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PendingModelMigrationNotice {
|
||||
from_model: String,
|
||||
to_model: String,
|
||||
// Used to respect hide flags even if config changes between scheduling and display.
|
||||
#[serde(default)]
|
||||
migration_config_key: Option<String>,
|
||||
}
|
||||
// Track information about an in-flight exec command.
|
||||
struct RunningCommand {
|
||||
command: Vec<String>,
|
||||
@@ -389,98 +374,12 @@ impl ChatWidget {
|
||||
self.conversation_id.is_some()
|
||||
}
|
||||
|
||||
fn pending_model_migration_notice_path(&self) -> PathBuf {
|
||||
self.config
|
||||
.codex_home
|
||||
.join(PENDING_MODEL_MIGRATION_NOTICE_FILENAME)
|
||||
}
|
||||
|
||||
fn migration_prompt_hidden(config: &Config, migration_config_key: &str) -> bool {
|
||||
match migration_config_key {
|
||||
HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG => config
|
||||
.notices
|
||||
.hide_gpt_5_1_codex_max_migration_prompt
|
||||
.unwrap_or(false),
|
||||
HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG => {
|
||||
config.notices.hide_gpt5_1_migration_prompt.unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show_model_migration_notice(
|
||||
current_model: &str,
|
||||
target_model: &str,
|
||||
seen_migrations: &BTreeMap<String, String>,
|
||||
available_models: &[ModelPreset],
|
||||
) -> bool {
|
||||
if target_model == current_model {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(seen_target) = seen_migrations.get(current_model)
|
||||
&& seen_target == target_model
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.any(|preset| preset.model == current_model && preset.upgrade.is_some())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.any(|preset| preset.upgrade.as_ref().map(|u| u.id.as_str()) == Some(target_model))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn maybe_show_pending_model_migration_notice(&mut self) {
|
||||
let notice_path = self.pending_model_migration_notice_path();
|
||||
let contents = match std::fs::read_to_string(¬ice_path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to read pending model migration notice"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let notice: PendingModelMigrationNotice = match serde_json::from_str(&contents) {
|
||||
Ok(notice) => notice,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to parse pending model migration notice"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(migration_config_key) = notice.migration_config_key.as_deref()
|
||||
&& Self::migration_prompt_hidden(&self.config, migration_config_key)
|
||||
{
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
let Some(notice) =
|
||||
model_migration_logic::take_pending_model_migration_notice(&mut self.config)
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(seen_target) = self.config.notices.model_migrations.get(¬ice.from_model)
|
||||
&& seen_target == ¬ice.to_model
|
||||
{
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.add_to_history(history_cell::new_info_event(
|
||||
format!(
|
||||
@@ -489,16 +388,6 @@ impl ChatWidget {
|
||||
),
|
||||
None,
|
||||
));
|
||||
|
||||
// Best-effort: clear the one-shot file so it doesn't appear again.
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
|
||||
// Update in-memory state to prevent re-scheduling within this run, and persist so future
|
||||
// runs don't re-show the notice.
|
||||
self.config
|
||||
.notices
|
||||
.model_migrations
|
||||
.insert(notice.from_model.clone(), notice.to_model.clone());
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistModelMigrationPromptAcknowledged {
|
||||
from_model: notice.from_model,
|
||||
@@ -512,70 +401,11 @@ impl ChatWidget {
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let Some(ModelUpgrade {
|
||||
id: target_model,
|
||||
migration_config_key,
|
||||
..
|
||||
}) = available_models
|
||||
.iter()
|
||||
.find(|preset| preset.model == model)
|
||||
.and_then(|preset| preset.upgrade.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if Self::migration_prompt_hidden(&self.config, migration_config_key.as_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.all(|preset| preset.model != target_model.as_str())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if !Self::should_show_model_migration_notice(
|
||||
model_migration_logic::maybe_schedule_model_migration_notice(
|
||||
&self.config,
|
||||
model,
|
||||
target_model.as_str(),
|
||||
&self.config.notices.model_migrations,
|
||||
&available_models,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notice_path = self.pending_model_migration_notice_path();
|
||||
if notice_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let notice = PendingModelMigrationNotice {
|
||||
from_model: model.to_string(),
|
||||
to_model: target_model.to_string(),
|
||||
migration_config_key: Some(migration_config_key.to_string()),
|
||||
};
|
||||
let Ok(json_line) = serde_json::to_string(¬ice).map(|json| format!("{json}\n")) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(parent) = notice_path.parent()
|
||||
&& let Err(err) = std::fs::create_dir_all(parent)
|
||||
{
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to create directory for pending model migration notice"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::write(¬ice_path, json_line) {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to persist pending model migration notice"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn flush_answer_stream_with_separator(&mut self) {
|
||||
@@ -1498,7 +1328,12 @@ impl ChatWidget {
|
||||
model,
|
||||
} = common;
|
||||
let mut config = config;
|
||||
config.model = Some(model.clone());
|
||||
// `model` is an optional override provided by the app. Avoid clobbering the configured
|
||||
// model with an empty string during startup; that would propagate to core and render as a
|
||||
// blank model in the session header (/model current label, etc).
|
||||
if !model.is_empty() {
|
||||
config.model = Some(model.clone());
|
||||
}
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
||||
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager);
|
||||
|
||||
@@ -60,6 +60,7 @@ mod markdown_render;
|
||||
mod markdown_stream;
|
||||
#[cfg(test)]
|
||||
mod model_migration;
|
||||
mod model_migration_logic;
|
||||
mod notifications;
|
||||
pub mod onboarding;
|
||||
mod oss_selection;
|
||||
|
||||
196
codex-rs/tui2/src/model_migration_logic.rs
Normal file
196
codex-rs/tui2/src/model_migration_logic.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
use codex_core::config::Config;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const PENDING_MODEL_MIGRATION_NOTICE_FILENAME: &str = "pending_model_migration_notice.json";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct PendingModelMigrationNotice {
|
||||
pub(crate) from_model: String,
|
||||
pub(crate) to_model: String,
|
||||
// Used to respect hide flags even if config changes between scheduling and display.
|
||||
#[serde(default)]
|
||||
pub(crate) migration_config_key: Option<String>,
|
||||
}
|
||||
|
||||
fn pending_model_migration_notice_path(config: &Config) -> PathBuf {
|
||||
config
|
||||
.codex_home
|
||||
.join(PENDING_MODEL_MIGRATION_NOTICE_FILENAME)
|
||||
}
|
||||
|
||||
fn migration_prompt_hidden(config: &Config, migration_config_key: &str) -> bool {
|
||||
match migration_config_key {
|
||||
HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG => config
|
||||
.notices
|
||||
.hide_gpt_5_1_codex_max_migration_prompt
|
||||
.unwrap_or(false),
|
||||
HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG => {
|
||||
config.notices.hide_gpt5_1_migration_prompt.unwrap_or(false)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show_model_migration_notice(
|
||||
current_model: &str,
|
||||
target_model: &str,
|
||||
available_models: &[ModelPreset],
|
||||
config: &Config,
|
||||
) -> bool {
|
||||
if target_model == current_model {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(seen_target) = config.notices.model_migrations.get(current_model)
|
||||
&& seen_target == target_model
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.any(|preset| preset.model == current_model && preset.upgrade.is_some())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
available_models
|
||||
.iter()
|
||||
.any(|preset| preset.upgrade.as_ref().map(|u| u.id.as_str()) == Some(target_model))
|
||||
}
|
||||
|
||||
/// Read and clear the one-shot migration notice file, returning the notice if it should be shown.
|
||||
///
|
||||
/// If the notice is returned, this also updates `config.notices.model_migrations` to prevent
|
||||
/// re-scheduling within the current process.
|
||||
pub(crate) fn take_pending_model_migration_notice(
|
||||
config: &mut Config,
|
||||
) -> Option<PendingModelMigrationNotice> {
|
||||
let notice_path = pending_model_migration_notice_path(config);
|
||||
let contents = match std::fs::read_to_string(¬ice_path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return None,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to read pending model migration notice"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let notice: PendingModelMigrationNotice = match serde_json::from_str(&contents) {
|
||||
Ok(notice) => notice,
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to parse pending model migration notice"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(migration_config_key) = notice.migration_config_key.as_deref()
|
||||
&& migration_prompt_hidden(config, migration_config_key)
|
||||
{
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(seen_target) = config.notices.model_migrations.get(¬ice.from_model)
|
||||
&& seen_target == ¬ice.to_model
|
||||
{
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Best-effort: clear the one-shot file so it doesn't appear again.
|
||||
let _ = std::fs::remove_file(¬ice_path);
|
||||
|
||||
config
|
||||
.notices
|
||||
.model_migrations
|
||||
.insert(notice.from_model.clone(), notice.to_model.clone());
|
||||
|
||||
Some(notice)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_schedule_model_migration_notice(
|
||||
config: &Config,
|
||||
current_model: &str,
|
||||
available_models: &[ModelPreset],
|
||||
) {
|
||||
let Some(ModelUpgrade {
|
||||
id: target_model,
|
||||
migration_config_key,
|
||||
..
|
||||
}) = available_models
|
||||
.iter()
|
||||
.find(|preset| preset.model == current_model)
|
||||
.and_then(|preset| preset.upgrade.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if migration_prompt_hidden(config, migration_config_key.as_str()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if available_models
|
||||
.iter()
|
||||
.all(|preset| preset.model != target_model.as_str())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if !should_show_model_migration_notice(
|
||||
current_model,
|
||||
target_model.as_str(),
|
||||
available_models,
|
||||
config,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notice_path = pending_model_migration_notice_path(config);
|
||||
if notice_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let notice = PendingModelMigrationNotice {
|
||||
from_model: current_model.to_string(),
|
||||
to_model: target_model.to_string(),
|
||||
migration_config_key: Some(migration_config_key.to_string()),
|
||||
};
|
||||
let Ok(json_line) = serde_json::to_string(¬ice).map(|json| format!("{json}\n")) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(parent) = notice_path.parent()
|
||||
&& let Err(err) = std::fs::create_dir_all(parent)
|
||||
{
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to create directory for pending model migration notice"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = std::fs::write(¬ice_path, json_line) {
|
||||
tracing::error!(
|
||||
error = %err,
|
||||
notice_path = %notice_path.display(),
|
||||
"failed to persist pending model migration notice"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user