This commit is contained in:
Ahmed Ibrahim
2026-01-05 10:02:44 -08:00
parent 5f56a2328b
commit 88d090a2b7
6 changed files with 130 additions and 28 deletions
+5
View File
@@ -279,6 +279,11 @@ impl App {
}
};
// Startup-only: show any previously scheduled model migration notice, then refresh the
// schedule to reflect the current config + model presets (at most one scheduled notice).
chat_widget.maybe_show_pending_model_migration_notice();
chat_widget.refresh_pending_model_migration_notice();
chat_widget.maybe_prompt_windows_sandbox_enable();
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
+3 -9
View File
@@ -418,7 +418,6 @@ impl ChatWidget {
event,
self.show_welcome_banner,
));
self.maybe_show_pending_model_migration_notice();
if let Some(messages) = initial_messages {
self.replay_initial_messages(messages);
}
@@ -435,7 +434,6 @@ impl ChatWidget {
// turn now that we know the session is configured.
self.maybe_send_next_queued_input();
}
self.maybe_schedule_model_migration_notice(&model_for_header);
if !self.suppress_session_configured_redraw {
self.request_redraw();
}
@@ -750,7 +748,7 @@ impl ChatWidget {
self.request_redraw();
}
fn maybe_show_pending_model_migration_notice(&mut self) {
pub(crate) fn maybe_show_pending_model_migration_notice(&mut self) {
let Some(notice) = model_migration::take_pending_model_migration_notice(&mut self.config)
else {
return;
@@ -770,17 +768,13 @@ impl ChatWidget {
});
}
fn maybe_schedule_model_migration_notice(&self, model: &str) {
pub(crate) fn refresh_pending_model_migration_notice(&self) {
let available_models = match self.models_manager.try_list_models(&self.config) {
Ok(models) => models,
Err(_) => return,
};
model_migration::maybe_schedule_model_migration_notice(
&self.config,
model,
&available_models,
);
model_migration::refresh_pending_model_migration_notice(&self.config, &available_models);
}
fn on_mcp_startup_complete(&mut self, ev: McpStartupCompleteEvent) {
+57 -5
View File
@@ -15,6 +15,9 @@ pub(crate) struct PendingModelMigrationNotice {
// Used to respect hide flags even if config changes between scheduling and display.
#[serde(default)]
pub(crate) migration_config_key: Option<String>,
/// Unix timestamp (seconds) when this notice was scheduled. Used to expire stale notices.
#[serde(default)]
pub(crate) scheduled_at_unix_seconds: Option<u64>,
}
/// Read and clear the one-shot migration notice file, returning the notice if it should be shown.
@@ -50,6 +53,11 @@ pub(crate) fn take_pending_model_migration_notice(
}
};
if notice_expired(&notice) {
let _ = std::fs::remove_file(&notice_path);
return None;
}
if let Some(migration_config_key) = notice.migration_config_key.as_deref()
&& migration_prompt_hidden(config, migration_config_key)
{
@@ -75,11 +83,30 @@ pub(crate) fn take_pending_model_migration_notice(
Some(notice)
}
pub(crate) fn maybe_schedule_model_migration_notice(
/// Persist the migration notice for the next startup, replacing any existing scheduled notice.
///
/// Scheduling is intentionally independent of session configuration: it uses the user's config
/// (or the default model preset) to determine what to schedule.
pub(crate) fn refresh_pending_model_migration_notice(
config: &Config,
current_model: &str,
available_models: &[ModelPreset],
) {
let current_model = config
.model
.as_deref()
.filter(|model| !model.is_empty())
.or_else(|| {
available_models
.iter()
.find(|preset| preset.is_default)
.map(|preset| preset.model.as_str())
});
let Some(current_model) = current_model else {
clear_pending_model_migration_notice(config);
return;
};
let Some(ModelUpgrade {
id: target_model,
migration_config_key,
@@ -89,10 +116,12 @@ pub(crate) fn maybe_schedule_model_migration_notice(
.find(|preset| preset.model == current_model)
.and_then(|preset| preset.upgrade.as_ref())
else {
clear_pending_model_migration_notice(config);
return;
};
if migration_prompt_hidden(config, migration_config_key.as_str()) {
clear_pending_model_migration_notice(config);
return;
}
@@ -100,6 +129,7 @@ pub(crate) fn maybe_schedule_model_migration_notice(
.iter()
.all(|preset| preset.model != target_model.as_str())
{
clear_pending_model_migration_notice(config);
return;
}
@@ -109,18 +139,17 @@ pub(crate) fn maybe_schedule_model_migration_notice(
available_models,
config,
) {
clear_pending_model_migration_notice(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()),
scheduled_at_unix_seconds: now_unix_seconds(),
};
let Ok(json_line) = serde_json::to_string(&notice).map(|json| format!("{json}\n")) else {
return;
@@ -167,6 +196,29 @@ fn migration_prompt_hidden(config: &Config, migration_config_key: &str) -> bool
}
}
fn clear_pending_model_migration_notice(config: &Config) {
let _ = std::fs::remove_file(pending_model_migration_notice_path(config));
}
fn now_unix_seconds() -> Option<u64> {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
}
fn notice_expired(notice: &PendingModelMigrationNotice) -> bool {
let Some(scheduled_at) = notice.scheduled_at_unix_seconds else {
return false;
};
let Some(now) = now_unix_seconds() else {
return false;
};
const WEEK_SECONDS: u64 = 7 * 24 * 60 * 60;
now.saturating_sub(scheduled_at) > WEEK_SECONDS
}
fn should_show_model_migration_notice(
current_model: &str,
target_model: &str,